Files
member-console/internal/embeds/templates/operator.html
Christian Galo 690c70b113 Add cross-tab HTMX triggers and planLadderMutation
Signal dependent tabs to re-fetch when plan ladders change.
operator_plan_ladders handlers set HX-Trigger: planLadderMutation on
create/update/delete and tier operations so Org Types' plan dropdowns
refresh. operator.html adds explanatory comments and hx-trigger attrs
so Org Types, Grants, and Products panes listen for productMutation,
planLadderMutation, and entitlementMutation
2026-04-27 02:05:59 -05:00

342 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<title>Operator - Wiki Cafe</title>
<meta name="description" content="Operator console for Wiki Cafe member management">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
<link href="/static/bootstrap.css" rel="stylesheet">
<script defer src="/static/bootstrap.bundle.js"></script>
<script defer src="/static/htmx.min.js"></script>
<script defer src="/static/error-handler.js"></script>
<script defer src="/static/grant-toggle.js"></script>
<script defer src="/static/operator-tabs.js"></script>
<script defer src="/static/confirm-action-modal.js"></script>
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="manifest" href="/static/site.webmanifest">
<link href="/static/app.css" rel="stylesheet">
</head>
<body class="d-flex flex-column vh-100" hx-headers='{"X-CSRF-Token": "{{ .CSRFToken }}"}'>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Wiki Cafe Member Console</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto d-lg-none">
<li class="nav-item">
<a class="nav-link" href="/">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/products">Products</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/operator">Operator</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ .KeycloakAccountURL }}" target="_blank">Identity and Access</a>
</li>
<li class="nav-item">
<a class="nav-link text-danger" href="/logout">Logout</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="d-flex flex-column flex-lg-row flex-grow-1">
<!-- Sidebar - only visible on lg screens and up -->
<div class="bg-light d-none d-lg-flex flex-column flex-shrink-0 h-lg-100 border-end">
<ul class="nav flex-column p-3">
<li class="nav-item"><a class="nav-link" href="/">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="/products">Products</a></li>
<li class="nav-item"><a class="nav-link active" href="/operator"><strong>Operator</strong></a></li>
<li class="nav-item"><a class="nav-link" href="{{ .KeycloakAccountURL }}" target="_blank">Identity and
Access</a></li>
<li class="nav-item"><a class="nav-link text-danger" href="/logout">Logout</a></li>
</ul>
</div>
<!-- Main Content -->
<div class="flex-fill p-4">
<h1>Operator</h1>
<!-- Tabs -->
<ul class="nav nav-tabs mt-4" id="operatorTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="people-tab" data-bs-toggle="tab" data-bs-target="#people-pane"
type="button" role="tab" aria-controls="people-pane" aria-selected="true">People</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="orgs-tab" data-bs-toggle="tab" data-bs-target="#orgs-pane"
type="button" role="tab" aria-controls="orgs-pane" aria-selected="false">Organizations</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="org-types-tab" data-bs-toggle="tab" data-bs-target="#org-types-pane"
type="button" role="tab" aria-controls="org-types-pane" aria-selected="false">Org Types</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="grants-tab" data-bs-toggle="tab" data-bs-target="#grants-pane"
type="button" role="tab" aria-controls="grants-pane" aria-selected="false">Grants</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="products-tab" data-bs-toggle="tab" data-bs-target="#products-pane"
type="button" role="tab" aria-controls="products-pane" aria-selected="false">Products</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="plan-ladders-tab" data-bs-toggle="tab" data-bs-target="#plan-ladders-pane"
type="button" role="tab" aria-controls="plan-ladders-pane" aria-selected="false">Plan Ladders</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="entitlement-sets-tab" data-bs-toggle="tab" data-bs-target="#entitlement-sets-pane"
type="button" role="tab" aria-controls="entitlement-sets-pane" aria-selected="false">Entitlement Sets</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="billing-tab" data-bs-toggle="tab" data-bs-target="#billing-pane"
type="button" role="tab" aria-controls="billing-pane" aria-selected="false">Billing</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="sites-tab" data-bs-toggle="tab" data-bs-target="#sites-pane"
type="button" role="tab" aria-controls="sites-pane" aria-selected="false">FedWiki Sites</button>
</li>
</ul>
<div class="tab-content border border-top-0 rounded-bottom p-3" id="operatorTabsContent">
<!-- People Tab -->
<div class="tab-pane fade show active" id="people-pane" role="tabpanel" aria-labelledby="people-tab">
<div hx-get="/partials/operator/users" hx-trigger="load" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading people...</p>
</div>
</div>
</div>
<!-- Organizations Tab -->
<div class="tab-pane fade" id="orgs-pane" role="tabpanel" aria-labelledby="orgs-tab">
<div id="orgsContent" hx-get="/partials/operator/organizations" hx-trigger="revealed" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading organizations...</p>
</div>
</div>
</div>
<!-- Org Types Tab -->
<div class="tab-pane fade" id="org-types-pane" role="tabpanel" aria-labelledby="org-types-tab">
<!-- Cross-tab refresh: productMutation (operator_products.go) keeps the
default-product preview fresh; planLadderMutation (operator_plan_ladders.go)
keeps the Default Plan dropdown fresh after a ladder/tier mutation. -->
<div id="orgTypesContent" hx-get="/partials/operator/org-types" hx-trigger="revealed, productMutation from:body, planLadderMutation from:body" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading organization types...</p>
</div>
</div>
</div>
<!-- Grants Tab -->
<div class="tab-pane fade" id="grants-pane" role="tabpanel" aria-labelledby="grants-tab">
<!-- Cross-tab refresh: productMutation (operator_products.go) keeps the
product picker fresh; entitlementMutation (operator_entitlement_sets.go)
keeps grant-derived entitlement displays fresh. -->
<div id="grantsContent" hx-get="/partials/operator/grants" hx-trigger="revealed, productMutation from:body, entitlementMutation from:body" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading grants...</p>
</div>
</div>
</div>
<!-- Products Tab -->
<div class="tab-pane fade" id="products-pane" role="tabpanel" aria-labelledby="products-tab">
<!-- Cross-tab refresh: entitlementMutation (operator_entitlement_sets.go)
keeps the entitlement-set column on the products list fresh. -->
<div id="productsContent" hx-get="/partials/operator/products" hx-trigger="revealed, entitlementMutation from:body" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading products...</p>
</div>
</div>
</div>
<!-- Plan Ladders Tab -->
<div class="tab-pane fade" id="plan-ladders-pane" role="tabpanel" aria-labelledby="plan-ladders-tab">
<div id="planLaddersContent" hx-get="/partials/operator/plan-ladders" hx-trigger="revealed" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading plan ladders...</p>
</div>
</div>
</div>
<!-- Entitlement Sets Tab -->
<div class="tab-pane fade" id="entitlement-sets-pane" role="tabpanel" aria-labelledby="entitlement-sets-tab">
<div id="entitlementSetsContent" hx-get="/partials/operator/entitlement-sets" hx-trigger="revealed" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading entitlement sets...</p>
</div>
</div>
</div>
<!-- Billing Tab -->
<div class="tab-pane fade" id="billing-pane" role="tabpanel" aria-labelledby="billing-tab">
<!-- Billing Sub-navigation -->
<ul class="nav nav-pills mb-3" id="billingSubTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="billing-accounts-tab" data-bs-toggle="pill" data-bs-target="#billing-accounts-pane"
type="button" role="tab" aria-controls="billing-accounts-pane" aria-selected="true">Accounts</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="billing-subscriptions-tab" data-bs-toggle="pill" data-bs-target="#billing-subscriptions-pane"
type="button" role="tab" aria-controls="billing-subscriptions-pane" aria-selected="false">Subscriptions</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="billing-invoices-tab" data-bs-toggle="pill" data-bs-target="#billing-invoices-pane"
type="button" role="tab" aria-controls="billing-invoices-pane" aria-selected="false">Invoices</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="billing-payments-tab" data-bs-toggle="pill" data-bs-target="#billing-payments-pane"
type="button" role="tab" aria-controls="billing-payments-pane" aria-selected="false">Payments</button>
</li>
</ul>
<!-- Billing Sub-tab Content -->
<div class="tab-content" id="billingSubTabsContent">
<!-- Billing Accounts -->
<div class="tab-pane fade show active" id="billing-accounts-pane" role="tabpanel" aria-labelledby="billing-accounts-tab">
<div id="billingAccountsContent" hx-get="/partials/operator/billing/accounts" hx-trigger="revealed" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading billing accounts...</p>
</div>
</div>
</div>
<!-- Subscriptions -->
<div class="tab-pane fade" id="billing-subscriptions-pane" role="tabpanel" aria-labelledby="billing-subscriptions-tab">
<div id="billingSubscriptionsContent" hx-get="/partials/operator/billing/subscriptions" hx-trigger="revealed" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading subscriptions...</p>
</div>
</div>
</div>
<!-- Invoices -->
<div class="tab-pane fade" id="billing-invoices-pane" role="tabpanel" aria-labelledby="billing-invoices-tab">
<div id="billingInvoicesContent" hx-get="/partials/operator/billing/invoices" hx-trigger="revealed" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading invoices...</p>
</div>
</div>
</div>
<!-- Payments -->
<div class="tab-pane fade" id="billing-payments-pane" role="tabpanel" aria-labelledby="billing-payments-tab">
<div id="billingPaymentsContent" hx-get="/partials/operator/billing/payments" hx-trigger="revealed" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading payments...</p>
</div>
</div>
</div>
</div>
</div>
<!-- FedWiki Sites Tab -->
<div class="tab-pane fade" id="sites-pane" role="tabpanel" aria-labelledby="sites-tab">
<div hx-get="/partials/operator/sites" hx-trigger="revealed" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading sites...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Error Toast Container -->
<div class="toast-container">
<div id="errorToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive"
aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="errorToastBody">
An error occurred. Please try again.
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
</div>
</div>
<!-- Shared confirmation modal (driven by data-action-* attrs on triggers) -->
<div class="modal fade" id="confirmActionModal" tabindex="-1" aria-labelledby="confirmActionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmActionModalLabel">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="confirmActionForm"
hx-swap="innerHTML"
hx-disabled-elt="find button[type=submit]"
hx-indicator="find .htmx-indicator">
<div class="modal-body">
<p id="confirmActionBody" class="mb-0"></p>
<div id="confirmActionHiddenFields"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger" id="confirmActionSubmit">
<span class="htmx-indicator spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
<span id="confirmActionLabel">Confirm</span>
</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>