Add System Admin tab dashboard v1

This commit is contained in:
A R R R Associates
2026-07-03 13:55:10 +05:30
parent b84090d8c9
commit 3d7f0a9570
13 changed files with 723 additions and 0 deletions
@@ -0,0 +1,97 @@
{% extends "base/layout.html" %}
{% block content %}
<section class="space-y-6">
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-soft">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600">Platform Utility Centre</p>
<h1 class="mt-2 text-2xl font-bold text-slate-900">System Admin Dashboard</h1>
<p class="mt-1 text-sm text-slate-500">Monitor firms, service catalogue, SMTP, storage, reports, wizards and audit logs from one place.</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/wizards/system/firm/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">+ Create Firm</a>
<a href="/email/platform-smtp" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Platform SMTP</a>
</div>
</div>
{% set tabs = [
('overview', 'Overview'),
('firms', 'Firms'),
('catalogue', 'Service Catalogue'),
('smtp', 'Platform SMTP'),
('storage', 'Storage'),
('reports', 'Reports'),
('wizards', 'Wizards'),
('audit-logs', 'Audit Logs')
] %}
<div class="mt-6 overflow-x-auto">
<div class="flex min-w-max gap-2 rounded-2xl bg-slate-100 p-1">
{% for tab_key, tab_label in tabs %}
<a href="/system-admin/dashboard?tab={{ tab_key }}"
data-dashboard-tab="{{ tab_key }}"
class="system-admin-tab rounded-xl px-4 py-2 text-sm font-semibold transition {% if active_tab == tab_key %}bg-white text-brand-700 shadow-soft{% else %}text-slate-600 hover:bg-white hover:text-slate-900{% endif %}">
{{ tab_label }}
</a>
{% endfor %}
</div>
</div>
</div>
<div id="system-admin-dashboard-panel"
data-initial-tab="{{ active_tab }}"
class="min-h-[360px]">
<div class="rounded-3xl border border-slate-200 bg-white p-8 text-center text-sm text-slate-500 shadow-soft">Loading dashboard...</div>
</div>
</section>
<script>
(function () {
const panel = document.getElementById('system-admin-dashboard-panel');
const tabs = Array.from(document.querySelectorAll('[data-dashboard-tab]'));
if (!panel || !tabs.length) return;
function setActive(tabName) {
tabs.forEach((tab) => {
const active = tab.dataset.dashboardTab === tabName;
tab.classList.toggle('bg-white', active);
tab.classList.toggle('text-brand-700', active);
tab.classList.toggle('shadow-soft', active);
tab.classList.toggle('text-slate-600', !active);
});
}
async function loadTab(tabName, pushState) {
setActive(tabName);
panel.innerHTML = '<div class="rounded-3xl border border-slate-200 bg-white p-8 text-center text-sm text-slate-500 shadow-soft">Loading...</div>';
try {
const response = await fetch('/system-admin/dashboard/tab/' + encodeURIComponent(tabName), {
headers: { 'X-Requested-With': 'fetch' }
});
if (!response.ok) throw new Error('Tab load failed');
panel.innerHTML = await response.text();
if (pushState) {
const url = new URL(window.location.href);
url.searchParams.set('tab', tabName);
history.pushState({ tab: tabName }, '', url.toString());
}
} catch (err) {
panel.innerHTML = '<div class="rounded-3xl border border-red-200 bg-red-50 p-6 text-sm text-red-700 shadow-soft">Unable to load this tab. Please refresh the page.</div>';
}
}
tabs.forEach((tab) => {
tab.addEventListener('click', function (event) {
event.preventDefault();
loadTab(tab.dataset.dashboardTab, true);
});
});
window.addEventListener('popstate', function () {
const url = new URL(window.location.href);
loadTab(url.searchParams.get('tab') || 'overview', false);
});
loadTab(panel.dataset.initialTab || 'overview', false);
})();
</script>
{% endblock %}
@@ -0,0 +1,15 @@
<div class="rounded-3xl border border-slate-200 bg-white shadow-soft">
<div class="border-b border-slate-100 p-5"><h2 class="text-lg font-bold text-slate-900">Recent Audit Logs</h2><p class="text-sm text-slate-500">Latest 25 audit events.</p></div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><tr><th class="px-5 py-3">Time</th><th class="px-5 py-3">User</th><th class="px-5 py-3">Action</th><th class="px-5 py-3">Entity</th><th class="px-5 py-3">Status</th></tr></thead>
<tbody class="divide-y divide-slate-100">
{% for log in audit_logs %}
<tr class="hover:bg-slate-50"><td class="px-5 py-3 whitespace-nowrap">{{ log.created_at_utc }}</td><td class="px-5 py-3">{{ log.actor_email or '-' }}</td><td class="px-5 py-3 font-semibold text-slate-900">{{ log.action }}</td><td class="px-5 py-3">{{ log.entity_type }}{% if log.entity_name %} > {{ log.entity_name }}{% endif %}</td><td class="px-5 py-3"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700">{{ log.status }}</span></td></tr>
{% else %}
<tr><td colspan="5" class="px-5 py-8 text-center text-slate-500">No audit logs found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,27 @@
<div class="space-y-6">
<div class="grid gap-4 md:grid-cols-4">
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Categories</div><div class="mt-2 text-2xl font-bold">{{ catalogue.category_count }}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Services</div><div class="mt-2 text-2xl font-bold">{{ catalogue.service_count }}</div><div class="text-xs text-slate-500">{{ catalogue.active_service_count }} active</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Default Tasks</div><div class="mt-2 text-2xl font-bold">{{ catalogue.default_task_count }}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Due Rules</div><div class="mt-2 text-2xl font-bold">{{ catalogue.due_rule_count }}</div></div>
</div>
<div class="rounded-3xl border border-slate-200 bg-white shadow-soft">
<div class="flex flex-col gap-3 border-b border-slate-100 p-5 md:flex-row md:items-center md:justify-between">
<div><h2 class="text-lg font-bold text-slate-900">Service Catalogue Readiness</h2><p class="text-sm text-slate-500">First 100 catalogue services with default task/rule counts.</p></div>
<div class="flex flex-wrap gap-2"><a href="/services/catalogue" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Open Catalogue</a><a href="/services/default-templates" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Default Templates</a></div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><tr><th class="px-5 py-3">Code</th><th class="px-5 py-3">Service</th><th class="px-5 py-3">Category</th><th class="px-5 py-3">Recurrence</th><th class="px-5 py-3 text-right">Tasks</th><th class="px-5 py-3 text-right">Rules</th><th class="px-5 py-3">Status</th></tr></thead>
<tbody class="divide-y divide-slate-100">
{% for service in catalogue.services %}
<tr class="hover:bg-slate-50"><td class="px-5 py-3 font-semibold">{{ service.code }}</td><td class="px-5 py-3">{{ service.name }}</td><td class="px-5 py-3">{{ service.category }}</td><td class="px-5 py-3">{{ service.recurrence_type }}</td><td class="px-5 py-3 text-right font-semibold">{{ service.task_count }}</td><td class="px-5 py-3 text-right font-semibold">{{ service.rule_count }}</td><td class="px-5 py-3"><span class="rounded-full px-2 py-1 text-xs font-semibold {% if service.is_active %}bg-emerald-50 text-emerald-700{% else %}bg-slate-100 text-slate-600{% endif %}">{% if service.is_active %}Active{% else %}Inactive{% endif %}</span></td></tr>
{% else %}
<tr><td colspan="7" class="px-5 py-8 text-center text-slate-500">No service catalogue found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
@@ -0,0 +1,28 @@
<div class="rounded-3xl border border-slate-200 bg-white shadow-soft">
<div class="flex flex-col gap-3 border-b border-slate-100 p-5 md:flex-row md:items-center md:justify-between">
<div><h2 class="text-lg font-bold text-slate-900">Firms / Tenants</h2><p class="text-sm text-slate-500">Latest firms created on the platform.</p></div>
<a href="/wizards/system/firm/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">+ Create Firm</a>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
<tr><th class="px-5 py-3">Code</th><th class="px-5 py-3">Firm</th><th class="px-5 py-3">Status</th><th class="px-5 py-3">Firm Admin</th><th class="px-5 py-3 text-right">Branches</th><th class="px-5 py-3 text-right">Users</th><th class="px-5 py-3">Action</th></tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for firm in firms %}
<tr class="hover:bg-slate-50">
<td class="px-5 py-3 font-semibold text-slate-900">{{ firm.code }}</td>
<td class="px-5 py-3"><div class="font-medium text-slate-900">{{ firm.name }}</div><div class="text-xs text-slate-500">{{ firm.firm_type or '-' }}{% if firm.contact_email %} > {{ firm.contact_email }}{% endif %}</div></td>
<td class="px-5 py-3"><span class="rounded-full px-2.5 py-1 text-xs font-semibold {% if firm.is_active %}bg-emerald-50 text-emerald-700{% else %}bg-slate-100 text-slate-600{% endif %}">{% if firm.is_active %}Active{% else %}Inactive{% endif %}</span></td>
<td class="px-5 py-3 text-slate-700">{{ firm.primary_admin }}</td>
<td class="px-5 py-3 text-right font-semibold">{{ firm.branch_count }}</td>
<td class="px-5 py-3 text-right font-semibold">{{ firm.user_count }}</td>
<td class="px-5 py-3"><a href="/system-settings/tenants" class="text-sm font-semibold text-brand-700 hover:text-brand-900">Open Settings</a></td>
</tr>
{% else %}
<tr><td colspan="7" class="px-5 py-8 text-center text-slate-500">No firms found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,27 @@
<div class="space-y-6">
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Total Firms</div><div class="mt-2 text-3xl font-bold text-slate-900">{{ overview.total_firms }}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Active Firms</div><div class="mt-2 text-3xl font-bold text-emerald-700">{{ overview.active_firms }}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Pending Invites</div><div class="mt-2 text-3xl font-bold text-amber-700">{{ overview.pending_invites }}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Platform SMTP</div><div class="mt-2 text-lg font-bold {% if overview.smtp.configured and overview.smtp.active %}text-emerald-700{% else %}text-amber-700{% endif %}">{% if overview.smtp.configured and overview.smtp.active %}Ready{% else %}Needs Setup{% endif %}</div><div class="mt-1 text-xs text-slate-500">{{ overview.smtp.message }}</div></div>
</div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Service Catalogue</div><div class="mt-2 text-2xl font-bold text-slate-900">{{ overview.service_count }}</div><div class="mt-1 text-xs text-slate-500">{{ overview.active_service_count }} active services</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Default Task Templates</div><div class="mt-2 text-2xl font-bold text-slate-900">{{ overview.default_task_count }}</div><div class="mt-1 text-xs text-slate-500">{{ overview.due_rule_count }} due-date rules</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Storage Used</div><div class="mt-2 text-2xl font-bold text-slate-900">{{ overview.storage.used_display }}</div><div class="mt-1 text-xs text-slate-500">Branding files: {{ overview.storage.branding_file_count }}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Inactive Firms</div><div class="mt-2 text-2xl font-bold text-slate-900">{{ overview.inactive_firms }}</div><div class="mt-1 text-xs text-slate-500">Use Firms tab for details</div></div>
</div>
<div class="grid gap-4 lg:grid-cols-3">
<a href="/wizards/system/firm/new" class="rounded-3xl border border-brand-100 bg-white p-5 shadow-soft hover:border-brand-300 hover:bg-brand-50">
<div class="text-sm font-bold text-brand-700">Create Firm Wizard</div><p class="mt-2 text-sm text-slate-600">Create tenant, branch, Firm Admin, employee link and invite.</p>
</a>
<a href="/email/platform-smtp" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:bg-slate-50">
<div class="text-sm font-bold text-slate-900">Platform SMTP</div><p class="mt-2 text-sm text-slate-600">Configure system email for invites and platform notifications.</p>
</a>
<a href="/system-admin/dashboard?tab=reports" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:bg-slate-50">
<div class="text-sm font-bold text-slate-900">Reports Centre</div><p class="mt-2 text-sm text-slate-600">Open setup, SMTP, storage and audit report cards.</p>
</a>
</div>
</div>
@@ -0,0 +1,9 @@
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{% for card in report_cards %}
<a href="{{ card.href }}" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:border-brand-200 hover:bg-brand-50">
<div class="flex items-start justify-between gap-3"><h3 class="font-bold text-slate-900">{{ card.title }}</h3><span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-700">{{ card.metric }}</span></div>
<p class="mt-3 text-sm text-slate-600">{{ card.description }}</p>
<div class="mt-4 text-sm font-semibold text-brand-700">View report</div>
</a>
{% endfor %}
</div>
@@ -0,0 +1,18 @@
<div class="grid gap-6 lg:grid-cols-3">
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-soft lg:col-span-2">
<h2 class="text-lg font-bold text-slate-900">Platform SMTP</h2>
<p class="mt-1 text-sm text-slate-500">Used for Firm Creation Wizard invites and system-level emails.</p>
<dl class="mt-6 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl bg-slate-50 p-4"><dt class="text-xs font-semibold uppercase text-slate-500">Status</dt><dd class="mt-1 font-bold {% if smtp.configured and smtp.active %}text-emerald-700{% else %}text-amber-700{% endif %}">{% if smtp.configured and smtp.active %}Ready{% else %}Needs Setup{% endif %}</dd></div>
<div class="rounded-2xl bg-slate-50 p-4"><dt class="text-xs font-semibold uppercase text-slate-500">Host / Port</dt><dd class="mt-1 font-semibold text-slate-900">{{ smtp.host or '-' }}{% if smtp.port %}:{{ smtp.port }}{% endif %}</dd></div>
<div class="rounded-2xl bg-slate-50 p-4"><dt class="text-xs font-semibold uppercase text-slate-500">From Email</dt><dd class="mt-1 font-semibold text-slate-900">{{ smtp.from_email or '-' }}</dd></div>
<div class="rounded-2xl bg-slate-50 p-4"><dt class="text-xs font-semibold uppercase text-slate-500">Security</dt><dd class="mt-1 font-semibold text-slate-900">{{ smtp.security or '-' }}</dd></div>
</dl>
<p class="mt-4 text-sm text-slate-600">{{ smtp.message }}</p>
</div>
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-soft">
<h3 class="font-bold text-slate-900">Action</h3>
<p class="mt-2 text-sm text-slate-500">Configure or test platform SMTP from the existing settings page.</p>
<a href="/email/platform-smtp" class="mt-5 inline-flex rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Open Platform SMTP</a>
</div>
</div>
@@ -0,0 +1,15 @@
<div class="space-y-6">
<div class="grid gap-4 md:grid-cols-4">
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Storage Root</div><div class="mt-2 text-lg font-bold {% if storage.storage_root_exists %}text-emerald-700{% else %}text-red-700{% endif %}">{% if storage.storage_root_exists %}Available{% else %}Missing{% endif %}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Used</div><div class="mt-2 text-lg font-bold">{{ storage.used_display }}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Branding Files</div><div class="mt-2 text-lg font-bold">{{ storage.branding_file_count }}</div></div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">FY Backups</div><div class="mt-2 text-lg font-bold">{{ storage.backup_export_count }}</div></div>
</div>
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-soft">
<h2 class="text-lg font-bold text-slate-900">Persistent Storage Paths</h2>
<div class="mt-4 space-y-3 text-sm">
<div class="rounded-2xl bg-slate-50 p-4"><span class="font-semibold text-slate-700">Storage root:</span> <code class="text-slate-800">{{ storage.storage_root }}</code></div>
<div class="rounded-2xl bg-slate-50 p-4"><span class="font-semibold text-slate-700">Branding root:</span> <code class="text-slate-800">{{ storage.branding_root }}</code></div>
</div>
</div>
</div>
@@ -0,0 +1,9 @@
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{% for card in wizard_cards %}
<a href="{{ card.href }}" class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft hover:border-brand-200 hover:bg-brand-50">
<h3 class="font-bold text-slate-900">{{ card.title }}</h3>
<p class="mt-3 text-sm text-slate-600">{{ card.description }}</p>
<div class="mt-4 inline-flex rounded-xl bg-brand-600 px-3 py-2 text-sm font-semibold text-white">{{ card.action }}</div>
</a>
{% endfor %}
</div>