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.
91 lines
3.8 KiB
HTML
91 lines
3.8 KiB
HTML
<!-- Grants management partial for operator — READ-ONLY.
|
|
Per operator-mpa-conversion D8, Issue/Extend/Revoke and CreateGrant
|
|
are exposed only on /operator/organizations/{orgID}. This view is
|
|
browse/audit only.
|
|
|
|
Delivery renders the derived state from the shared delivery-state
|
|
derivation (ux-honest-surfaces design decision 1; the paged query
|
|
ListGrantsWithDeliveryPage repeats the identical CASE expression,
|
|
design D4) — the same vocabulary and badge treatment the org-detail
|
|
composite's Grants table uses, so the two surfaces can never disagree.
|
|
Ledger (grants.status) is issuance history and is deliberately NOT a
|
|
colored state badge — see the explanation below.
|
|
|
|
Governed by operator-list-scale: server-side search, the delivery-state
|
|
filter, and true-total pagination (operator_list_controls.html's
|
|
listControls/listPager/listNoMatch, driven by .Nav). -->
|
|
<h1 class="h2 mb-3">Grants</h1>
|
|
{{ if .Error }}
|
|
<div class="alert alert-danger" role="alert">
|
|
<strong>Error:</strong> {{ .Error }}
|
|
</div>
|
|
{{ else }}
|
|
|
|
{{ template "listControls" .Nav }}
|
|
|
|
{{ if eq (len .Grants) 0 }}
|
|
{{ if .Nav.Filtered }}
|
|
{{ template "listNoMatch" .Nav }}
|
|
{{ else }}
|
|
<div class="text-center py-4">
|
|
<p class="text-muted mb-2">No grants found.</p>
|
|
{{ if eq (len .Products) 0 }}
|
|
<p class="text-muted small mb-0">A grant delivers a published product to an organization, but no product has been published yet. <a href="/operator/products">Go to Products</a> to publish one.</p>
|
|
{{ else }}
|
|
<p class="text-muted small mb-0">Grants are issued from each organization's detail page, not from here.</p>
|
|
{{ end }}
|
|
</div>
|
|
{{ end }}
|
|
{{ else }}
|
|
<p class="text-muted small mb-2">
|
|
Grants are the issuance ledger, not the delivery record: Ledger is when
|
|
and why a grant was recorded (and whether it was later revoked or
|
|
expired). Delivery is derived by joining each grant to its pool
|
|
provision — at most one grant per ladder position is
|
|
<span class="badge text-bg-success">Live</span> at a time; the rest are
|
|
history.
|
|
</p>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Organization</th>
|
|
<th>Grant Target</th>
|
|
<th>Qty</th>
|
|
<th>Reason</th>
|
|
<th>Ledger</th>
|
|
<th>Delivery</th>
|
|
<th>Created</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .Grants }}
|
|
<tr{{ if ne .DeliveryState "live" }} class="text-muted"{{ end }}>
|
|
<td>{{ .OrgName }}</td>
|
|
<td>{{ if .ProductName }}{{ .ProductName }} <span class="badge bg-secondary">product</span>{{ else }}{{ .EntitlementSetName }} <span class="badge text-bg-info">set</span>{{ end }}</td>
|
|
<td>{{ .Quantity }}</td>
|
|
<td>{{ .GrantReason }}</td>
|
|
<td><small>{{ .Status }}</small></td>
|
|
<td>
|
|
{{ if eq .DeliveryState "live" }}
|
|
<span class="badge text-bg-success">Live</span>
|
|
{{ if .ExtendsLabel }}<br><small class="text-muted">extends {{ .ExtendsLabel }}</small>{{ end }}
|
|
{{ else if eq .DeliveryState "superseded" }}
|
|
<span class="badge text-bg-secondary">Superseded</span>
|
|
{{ if .ReplacedByLabel }}<br><small class="text-muted">replaced by {{ .ReplacedByLabel }}</small>{{ end }}
|
|
{{ else }}
|
|
<span class="badge text-bg-light border">Inactive</span>
|
|
{{ end }}
|
|
</td>
|
|
<td>{{ .CreatedAt }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ end }}
|
|
|
|
{{ template "listPager" .Nav }}
|
|
|
|
{{ end }}
|