- Add deployment-name branding to titles, mastheads, and OG tags - Share one grant delivery-state query with lineage across grants surfaces - Show pool status/usage, org owners, and config readiness - Make billing views projection-aware with recency and sync vocabulary - Guard FedWiki creation without domains and render route-aware 404s
470 lines
24 KiB
HTML
470 lines
24 KiB
HTML
<!-- Organization enrollment detail partial for operator -->
|
|
{{ if .Error }}
|
|
<div class="alert alert-danger" role="alert">
|
|
<strong>Error:</strong> {{ .Error }}
|
|
</div>
|
|
{{ end }}
|
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<h1 class="mb-0 h6">Enrollment: {{ .OrgName }}</h1>
|
|
<small class="text-muted"><code>{{ .OrgSlug }}</code></small>
|
|
<small class="text-muted d-block">
|
|
Owner:
|
|
{{ if .OrgOwnerPersonID }}
|
|
<a href="/operator/persons/{{ .OrgOwnerPersonID }}">{{ if .OrgOwnerName }}{{ .OrgOwnerName }}{{ else }}{{ .OrgOwnerPersonID }}{{ end }}</a>
|
|
{{ else }}
|
|
<em>none</em>
|
|
{{ end }}
|
|
</small>
|
|
</div>
|
|
<a class="btn btn-outline-secondary btn-sm" href="/operator/organizations">
|
|
← Back to organizations
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Billing Summary -->
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<h2 class="card-title mb-0 h6">Billing summary</h2>
|
|
{{ if .BillingSummary.HasAccount }}
|
|
<a class="btn btn-outline-secondary btn-sm" href="/operator/billing/accounts">Full billing →</a>
|
|
{{ end }}
|
|
</div>
|
|
{{ if not .BillingSummary.HasAccount }}
|
|
<p class="text-muted small mb-0">No billing account exists for this organization yet.</p>
|
|
{{ else }}
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<div class="text-muted small">Subscription</div>
|
|
{{ if .BillingSummary.HasSubscription }}
|
|
<div>
|
|
<span class="badge bg-secondary">{{ .BillingSummary.SubscriptionStatus }}</span>
|
|
{{ if .BillingSummary.SubscriptionPeriodEnd }}
|
|
<small class="text-muted">· renews {{ .BillingSummary.SubscriptionPeriodEnd }}</small>
|
|
{{ end }}
|
|
{{ if .BillingSummary.CancelAtPeriodEnd }}
|
|
<small class="text-warning">· cancels at period end</small>
|
|
{{ end }}
|
|
</div>
|
|
{{ else }}
|
|
<div class="text-muted small">No subscription</div>
|
|
{{ end }}
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="text-muted small">Latest invoice</div>
|
|
{{ if .BillingSummary.HasInvoice }}
|
|
<div>
|
|
{{ if .BillingSummary.LatestInvoicePaid }}
|
|
<span class="badge text-bg-success">paid</span>
|
|
{{ else }}
|
|
<span class="badge text-bg-warning">unpaid</span>
|
|
{{ end }}
|
|
<strong>{{ .BillingSummary.LatestInvoiceAmountDue }}</strong>
|
|
<small class="text-muted">· {{ .BillingSummary.LatestInvoiceDate }}</small>
|
|
</div>
|
|
{{ else }}
|
|
<div class="text-muted small">No invoices issued</div>
|
|
{{ end }}
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="text-muted small">Outstanding balance</div>
|
|
<div>
|
|
{{ if gt .BillingSummary.OutstandingBalance 0 }}
|
|
<strong class="text-warning">{{ .BillingSummary.OutstandingBalanceFormatted }}</strong>
|
|
<small class="text-muted">across open invoices</small>
|
|
{{ else }}
|
|
<strong class="text-success">0</strong>
|
|
<small class="text-muted">no open invoices</small>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pool Enrollment State -->
|
|
<h2 class="mt-4 h6">Resource Pools</h2>
|
|
{{ if .PoolMissing }}
|
|
<!-- Every organization gets exactly one default pool at provisioning; one
|
|
with none is broken, not merely empty (ux-honest-surfaces: "a pool-less
|
|
organization is presented as broken, not empty"). The Issue Grant
|
|
section below renders the same reason instead of a submittable form. -->
|
|
<div class="alert alert-danger" role="alert">
|
|
<strong>Missing default resource pool.</strong> This organization has no
|
|
resource pool. Every organization should have exactly one; without it,
|
|
grant delivery is impossible until the pool is repaired.
|
|
</div>
|
|
{{ else if eq (len .Pools) 0 }}
|
|
<div class="text-center py-3">
|
|
<p class="text-muted mb-0">No pools for this organization.</p>
|
|
</div>
|
|
{{ else }}
|
|
{{ range .Pools }}
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h3 class="card-title mb-1 h6">
|
|
{{ .PoolName }} <span class="badge bg-secondary">{{ .PoolType }}</span>
|
|
{{ if eq .Status "active" }}
|
|
<span class="badge text-bg-success">{{ .Status }}</span>
|
|
{{ else }}
|
|
<span class="badge text-bg-danger">{{ .Status }}</span>
|
|
{{ end }}
|
|
</h3>
|
|
{{ if ne .Status "active" }}
|
|
<p class="text-danger small mb-1">This pool is not active; it is not delivering entitlements.</p>
|
|
{{ end }}
|
|
{{ if .HasAttachment }}
|
|
<p class="mb-0">
|
|
<span class="badge bg-primary">{{ .LadderKey }}</span>
|
|
<strong>{{ .TierName }}</strong> at rank {{ .Rank }}
|
|
<small class="text-muted">· since {{ .ActivatedAt }}</small>
|
|
</p>
|
|
{{ else }}
|
|
<p class="text-muted mb-0">No active plan</p>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
{{ if gt (len .Usage) 0 }}
|
|
<div class="table-responsive mt-2">
|
|
<table class="table table-sm mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Resource</th>
|
|
<th>Used</th>
|
|
<th>Limit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .Usage }}
|
|
<tr{{ if ge .Used .Limit }} class="text-danger"{{ end }}>
|
|
<td><code>{{ .ResourceKey }}</code></td>
|
|
<td>{{ .Used }}</td>
|
|
<td>{{ .Limit }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
{{ end }}
|
|
|
|
<!-- Issuance — ONE form over all published products (Doc 41 dissolved the
|
|
plan/non-plan form split; the conferral primitive derives position
|
|
behavior from the product's shape). Internal wrap products carry an
|
|
"internal" badge; supersession advisories are server-rendered from the
|
|
pre-check flags on IssuanceProducts (no client JS — CSP). -->
|
|
{{ if .PoolMissing }}
|
|
<!-- ux-honest-surfaces: "the Issue Grant form SHALL be blocked with that
|
|
reason". Same reason stated in the Resource Pools warning above, and
|
|
matched by IssueGrant's own defense-in-depth refusal (poolMissingMessage). -->
|
|
<div class="alert alert-danger mb-3" role="alert">
|
|
<strong>Issue grant unavailable.</strong> This organization is missing
|
|
its default resource pool, so grant delivery is impossible until the
|
|
pool is repaired. See the Resource Pools section above.
|
|
</div>
|
|
{{ else if .ProductsError }}
|
|
<div class="alert alert-danger" role="alert">{{ .ProductsError }}</div>
|
|
{{ else if eq (len .IssuanceProducts) 0 }}
|
|
<div class="text-muted small mb-3">No published products yet — publish a product to enable issuance.</div>
|
|
{{ else }}
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h2 class="card-title h6">Issue grant</h2>
|
|
{{ $issueActive := and (eq $.FormName "issue") (eq $.FormPoolID "") }}
|
|
<p class="text-muted small mb-2">
|
|
Records the grant and updates this organization's entitlements
|
|
right away. If the product occupies a plan-ladder position, the
|
|
pool moves to it; leave <em>Valid until</em> blank for an
|
|
open-ended grant.
|
|
</p>
|
|
{{ $formErr := fieldErr $.FormName "issue" $.FormPoolID "" $.FieldErrors "" }}
|
|
{{ with $formErr }}
|
|
<div class="alert alert-danger" role="alert">{{ . }}</div>
|
|
{{ end }}
|
|
<form autocomplete="off" hx-post="{{ routeURL "/partials/operator/organizations/{orgID}/grant/create" .OrgID }}"
|
|
hx-target="#operator-body" hx-swap="innerHTML"
|
|
hx-disabled-elt="find button[type=submit]">
|
|
<div class="row g-2">
|
|
<div class="col-md-6 mb-2">
|
|
<label class="form-label form-label-sm" for="issue-product">Product</label>
|
|
{{ $err := fieldErr $.FormName "issue" $.FormPoolID "" $.FieldErrors "product_id" }}
|
|
<select id="issue-product" name="product_id" class="form-select form-select-sm {{ if $err }}is-invalid{{ end }}" aria-invalid="{{ if $err }}true{{ else }}false{{ end }}" required>
|
|
<option value="">Select product…</option>
|
|
{{ range $.IssuanceProducts }}
|
|
<option value="{{ .ProductID }}" {{ if and $issueActive (eq .ProductID (index $.FormValues "product_id")) }}selected{{ end }}>{{ .Name }}{{ if .IsInternal }} [internal]{{ end }}{{ if .SupersedesSubscription }} ⚠{{ end }}</option>
|
|
{{ end }}
|
|
</select>
|
|
{{ with $err }}<div class="invalid-feedback">{{ . }}</div>{{ end }}
|
|
</div>
|
|
<div class="col-md-3 mb-2">
|
|
<label class="form-label form-label-sm" for="issue-quantity">Quantity</label>
|
|
{{ $err := fieldErr $.FormName "issue" $.FormPoolID "" $.FieldErrors "quantity" }}
|
|
<input autocomplete="off" id="issue-quantity" type="number" name="quantity" min="1" max="1000000" value="{{ if $issueActive }}{{ index $.FormValues "quantity" }}{{ else }}1{{ end }}" class="form-control form-control-sm {{ if $err }}is-invalid{{ end }}" aria-invalid="{{ if $err }}true{{ else }}false{{ end }}">
|
|
{{ with $err }}<div class="invalid-feedback">{{ . }}</div>{{ end }}
|
|
<div class="form-text">Above 1 only for per-seat products — most products deliver one position.</div>
|
|
</div>
|
|
<div class="col-md-3 mb-2">
|
|
<label class="form-label form-label-sm" for="issue-valid-until">Valid until <span class="text-muted">(optional)</span></label>
|
|
{{ $err := fieldErr $.FormName "issue" $.FormPoolID "" $.FieldErrors "valid_until" }}
|
|
<input id="issue-valid-until" type="datetime-local" name="valid_until" value="{{ if $issueActive }}{{ index $.FormValues "valid_until" }}{{ end }}" class="form-control form-control-sm {{ if $err }}is-invalid{{ end }}" aria-invalid="{{ if $err }}true{{ else }}false{{ end }}">
|
|
{{ with $err }}<div class="invalid-feedback">{{ . }}</div>{{ end }}
|
|
</div>
|
|
</div>
|
|
<div class="row g-2">
|
|
<div class="col-md-4 mb-2">
|
|
<label class="form-label form-label-sm" for="issue-reason">Reason</label>
|
|
{{ $err := fieldErr $.FormName "issue" $.FormPoolID "" $.FieldErrors "reason" }}
|
|
<select id="issue-reason" name="reason" class="form-select form-select-sm {{ if $err }}is-invalid{{ end }}" aria-invalid="{{ if $err }}true{{ else }}false{{ end }}" required>
|
|
<option value="">Select reason…</option>
|
|
{{ range $.GrantReasons }}
|
|
<option value="{{ . }}" {{ if and $issueActive (eq . (index $.FormValues "reason")) }}selected{{ end }}>{{ . }}</option>
|
|
{{ end }}
|
|
</select>
|
|
{{ with $err }}<div class="invalid-feedback">{{ . }}</div>{{ end }}
|
|
</div>
|
|
<div class="col-md-8 mb-2">
|
|
<label class="form-label form-label-sm" for="issue-description">Description <span class="text-muted">(optional)</span></label>
|
|
{{ $err := fieldErr $.FormName "issue" $.FormPoolID "" $.FieldErrors "description" }}
|
|
<textarea id="issue-description" name="description" rows="1" maxlength="500" class="form-control form-control-sm {{ if $err }}is-invalid{{ end }}" aria-invalid="{{ if $err }}true{{ else }}false{{ end }}" placeholder="Audit note — why this grant?">{{ if $issueActive }}{{ index $.FormValues "description" }}{{ end }}</textarea>
|
|
{{ with $err }}<div class="invalid-feedback">{{ . }}</div>{{ end }}
|
|
</div>
|
|
</div>
|
|
{{ $superseders := false }}
|
|
{{ range $.IssuanceProducts }}{{ if .SupersedesSubscription }}{{ $superseders = true }}{{ end }}{{ end }}
|
|
{{ if $superseders }}
|
|
<div class="alert alert-warning py-1 px-2 small mb-2" role="alert">
|
|
Products marked ⚠ would supersede a position currently held by a subscription —
|
|
the subscription keeps billing until it is changed in the billing view.
|
|
</div>
|
|
{{ end }}
|
|
<p class="text-muted small mb-2">
|
|
Issuing records the grant and updates entitlements now.
|
|
Connected services (for example forum or site access) apply
|
|
the change in the background and can retry or fail; the
|
|
<a href="/operator">overview</a>'s system panel shows queued
|
|
work for services that use the delivery queue.
|
|
</p>
|
|
<button type="submit" class="btn btn-primary btn-sm">Issue grant</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Extend Current Tier -->
|
|
{{ range $pool := .Pools }}
|
|
{{ if $pool.HasAttachment }}
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h2 class="card-title h6">Extend current tier</h2>
|
|
<p class="text-muted small mb-2">
|
|
Issue a new grant on the current tier (<strong>{{ $pool.TierName }}</strong>) without changing position.
|
|
Set <em>Expires at</em> to extend a trial; leave it empty for an open-ended comp or gift period.
|
|
</p>
|
|
{{ if not $pool.HasGrantDelivery }}
|
|
<!-- Extend targets the pool's active grant-backed provision; without
|
|
one the handler can only reject, so render the precondition
|
|
instead of a form that always fails. -->
|
|
<p class="text-muted small mb-0">
|
|
Nothing to extend right now — this pool's current delivery is not
|
|
grant-backed (it may be subscription-billed or ended). Use
|
|
<em>Issue grant</em> to grant a position instead.
|
|
</p>
|
|
{{ else }}
|
|
{{ $extendActive := and (eq $.FormName "extend") (eq $.FormPoolID $pool.PoolID) }}
|
|
<form autocomplete="off" hx-post="{{ routeURL "/partials/operator/organizations/{orgID}/pools/{poolID}/grant/extend" $.OrgID $pool.PoolID }}"
|
|
hx-target="#operator-body" hx-swap="innerHTML"
|
|
hx-disabled-elt="find button[type=submit]"
|
|
hx-indicator="find .htmx-indicator">
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="extend-reason-{{ $pool.PoolID }}">Reason</label>
|
|
{{ $err := fieldErr $.FormName "extend" $.FormPoolID $pool.PoolID $.FieldErrors "reason" }}
|
|
<input autocomplete="off" type="text" id="extend-reason-{{ $pool.PoolID }}" name="reason" value="{{ if $extendActive }}{{ index $.FormValues "reason" }}{{ end }}"
|
|
class="form-control {{ if $err }}is-invalid{{ end }}" aria-invalid="{{ if $err }}true{{ else }}false{{ end }}" placeholder="Why extend?" required>
|
|
{{ with $err }}<div class="invalid-feedback">{{ . }}</div>{{ end }}
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="extend-valid-until-{{ $pool.PoolID }}">Expires at <span class="text-muted">(optional)</span></label>
|
|
{{ $err := fieldErr $.FormName "extend" $.FormPoolID $pool.PoolID $.FieldErrors "valid_until" }}
|
|
<input type="datetime-local" id="extend-valid-until-{{ $pool.PoolID }}" name="valid_until" value="{{ if $extendActive }}{{ index $.FormValues "valid_until" }}{{ end }}"
|
|
class="form-control {{ if $err }}is-invalid{{ end }}" aria-invalid="{{ if $err }}true{{ else }}false{{ end }}">
|
|
{{ with $err }}<div class="invalid-feedback">{{ . }}</div>{{ end }}
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-outline-primary w-100">
|
|
<span class="htmx-indicator spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
|
Extend tier
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
{{ end }}
|
|
|
|
<!-- Grants — every grant for the org, annotated with delivery state.
|
|
The grants table is intentionally a ledger (design/entitlements/model.md
|
|
treats grants as audit records). The Delivery column collapses the
|
|
join to pool_provisions / pool_provision_ladders so operators can see
|
|
at a glance which row is actually backing the pool right now vs. which
|
|
are historical. Destructive actions render only on Live rows; system
|
|
defaults always show "System-managed" since they're owned by org-type
|
|
config, not per-grant operator action. Same vocabulary and badge
|
|
treatment as /operator/grants (design decision 1: one shared query, one
|
|
rendering). -->
|
|
{{ if .GrantsError }}
|
|
<div class="alert alert-danger" role="alert">{{ .GrantsError }}</div>
|
|
{{ end }}
|
|
{{ if gt (len .Grants) 0 }}
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h2 class="card-title h6">Grants</h2>
|
|
<p class="text-muted small mb-2">
|
|
One row per grant ever issued — this is the issuance ledger, not
|
|
the delivery record. Delivery state is derived from the pool
|
|
provision each grant created: 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-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Product</th>
|
|
<th>Reason</th>
|
|
<th>Delivery</th>
|
|
<th>Qty</th>
|
|
<th>Activated</th>
|
|
<th>Ended</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .Grants }}
|
|
<tr{{ if ne .DeliveryState "live" }} class="text-muted"{{ end }}>
|
|
<td>{{ if .ProductName }}{{ .ProductName }}{{ else }}—{{ end }}</td>
|
|
<td><span class="badge bg-secondary">{{ .GrantReason }}</span></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>{{ .Quantity }}</td>
|
|
<td><small>{{ if .ActivatedAt }}{{ .ActivatedAt }}{{ else }}—{{ end }}</small></td>
|
|
<td><small>{{ if .EndedAt }}{{ .EndedAt }}{{ else }}—{{ end }}</small></td>
|
|
<td>
|
|
{{ if eq .DeliveryState "live" }}
|
|
{{ if eq .GrantReason "default" }}
|
|
<span class="text-muted small" title="System-managed default. Change the org type's default ladder to remove.">System-managed</span>
|
|
{{ else }}
|
|
<button type="button" class="btn btn-outline-danger btn-sm"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#confirmActionModal"
|
|
data-action-url="{{ routeURL "/partials/operator/grants/{grantID}/revoke-and-transition" .GrantID }}"
|
|
data-action-method="post"
|
|
data-action-target="#operator-body"
|
|
data-action-title="Revoke grant"
|
|
data-action-body="{{ if .ProductName }}Revoke {{ .ProductName }} for this organization?{{ else }}Revoke this grant for this organization?{{ end }} The pool returns to its org-type default and a downgrade or end transition is recorded."
|
|
data-action-confirm-label="Revoke grant"
|
|
data-action-fields='{"org_id":"{{ $.OrgID }}"}'>Revoke</button>
|
|
{{ end }}
|
|
{{ end }}
|
|
</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Members -->
|
|
<h2 class="mt-4 h6">Members</h2>
|
|
{{ if .MembersError }}
|
|
<div class="alert alert-danger" role="alert">{{ .MembersError }}</div>
|
|
{{ end }}
|
|
{{ if eq (len .Members) 0 }}
|
|
<div class="text-muted small py-2">No members enrolled.</div>
|
|
{{ else }}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Joined</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .Members }}
|
|
<tr>
|
|
<td><a href="/operator/persons/{{ .PersonID }}">{{ .DisplayName }}</a></td>
|
|
<td><small class="text-muted">{{ .Email }}</small></td>
|
|
<td><span class="badge bg-secondary">{{ .RoleName }}</span></td>
|
|
<td><small>{{ .JoinedAt }}</small></td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Transition History -->
|
|
<h2 class="mt-4 h6">Transition History</h2>
|
|
{{ if .TransitionsError }}
|
|
<div class="alert alert-danger" role="alert">{{ .TransitionsError }}</div>
|
|
{{ end }}
|
|
{{ if eq (len .Transitions) 0 }}
|
|
<div class="text-center py-3">
|
|
<p class="text-muted mb-0">No transitions recorded.</p>
|
|
</div>
|
|
{{ else }}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Type</th>
|
|
<th>From</th>
|
|
<th>To</th>
|
|
<th>Actor</th>
|
|
<th>Reason</th>
|
|
<th>Effective</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .Transitions }}
|
|
<tr>
|
|
<td><span class="badge text-bg-info">{{ .TransitionType }}</span></td>
|
|
<td>{{ .FromRank }}</td>
|
|
<td>{{ .ToRank }}</td>
|
|
<td>
|
|
{{ if .ActorName }}{{ .ActorName }}{{ else }}<em class="text-muted">{{ .ActorType }}</em>{{ end }}
|
|
</td>
|
|
<td><small>{{ .Reason }}</small></td>
|
|
<td><small>{{ .EffectiveAt }}</small></td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ end }}
|