Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.
People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.
Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.
Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.
Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
65 lines
2.4 KiB
HTML
65 lines
2.4 KiB
HTML
{{/* operator_persons.html
|
|
People directory body partial for GET /operator/persons
|
|
(operator-people-directory D6), dispatched by GetPersonsPage.
|
|
Governed by operator-list-scale (operator_list_controls.html):
|
|
search over display name and email, true-total pagination. No status
|
|
facet -- People has no status vocabulary controls filter on.
|
|
Newest-joined first, so "who joined this week?" is page one.
|
|
|
|
Three states:
|
|
- Error: the load failed.
|
|
- Zero rows AND Nav.Filtered: the search/clear-search matched
|
|
nothing -- listNoMatch, distinct from true-empty
|
|
(empty-state-guidance).
|
|
- Zero rows AND not Nav.Filtered: the deployment has no persons at
|
|
all yet -- honest empty copy naming how a person row comes to
|
|
exist, no controls (nothing to search or clear).
|
|
*/}}
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<h1 class="h2 mb-0">People</h1>
|
|
</div>
|
|
{{ if .Error }}
|
|
<div class="alert alert-danger" role="alert">
|
|
<strong>Error:</strong> {{ .Error }}
|
|
</div>
|
|
{{ else if eq (len .Persons) 0 }}
|
|
{{ if .Nav.Filtered }}
|
|
{{ template "listControls" .Nav }}
|
|
{{ template "listNoMatch" .Nav }}
|
|
{{ else }}
|
|
<div class="text-center py-4">
|
|
<p class="text-muted mb-0">No one has signed in yet. Person records are created automatically the first time someone logs in.</p>
|
|
</div>
|
|
{{ end }}
|
|
{{ else }}
|
|
{{ template "listControls" .Nav }}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
<th>Status</th>
|
|
<th>Joined</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .Persons }}
|
|
<tr>
|
|
<td>
|
|
<a href="/operator/persons/{{ .PersonID }}">{{ .DisplayName }}</a>
|
|
</td>
|
|
<td>
|
|
{{ .Email }}
|
|
{{ if .EmailVerified }}<span class="badge text-bg-secondary ms-1">Verified</span>{{ else }}<span class="badge text-bg-warning ms-1">Unverified</span>{{ end }}
|
|
</td>
|
|
<td class="text-muted">{{ .Status }}</td>
|
|
<td>{{ .JoinedAt }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ template "listPager" .Nav }}
|
|
{{ end }}
|