curaflow/super-admin.html

799 lines
45 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Platform Command Center | Curio</title>
<meta name="description" content="Curio Super Admin — manage all clinic tenants, users and subscriptions.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="globals.css">
<link rel="stylesheet" href="dashboard.css">
<script src="globals.js"></script>
<script src="tenantLoader.js"></script>
<style>
:root { --sidebar-w: 230px; }
body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text-main); display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }
/* 🟦 Sidebar 🟦 */
.sa-sidebar { background: var(--primary); color: white; display: flex; flex-direction: column;
position: sticky; top: 0; height: 100vh; overflow-y: auto; }
.sa-logo { padding: 1.5rem 1.25rem; display: flex; align-items: center; gap: 0.75rem;
border-bottom: 1px solid rgba(255,255,255,0.1); }
.sa-logo-icon { width: 36px; height: 36px; background: rgba(255,255,255,0.15); border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 1rem; }
.sa-logo-text { font-family: 'Outfit', sans-serif; font-weight: 700; font-size: 1.05rem; }
.sa-logo-text small { display: block; font-size: 0.65rem; opacity: 0.6; font-weight: 400; }
.sa-nav { flex: 1; padding: 1rem 0; overflow-y: auto; }
.sa-nav a { display: flex; align-items: center; gap: 0.75rem; padding: 0.7rem 1.25rem;
color: rgba(255,255,255,0.7); text-decoration: none; font-size: 0.875rem; font-weight: 500;
border-left: 3px solid transparent; transition: all 0.2s; cursor: pointer; }
.sa-nav a:hover { background: rgba(255,255,255,0.08); color: white; }
.sa-nav a.active { background: rgba(255,255,255,0.12); color: white; border-left-color: rgba(255,255,255,0.8); }
.sa-nav .nav-badge { margin-left: auto; background: #EF4444; color: white; font-size: 0.65rem;
font-weight: 700; padding: 1px 6px; border-radius: 10px; min-width: 18px; text-align: center; }
.sa-footer { padding: 1rem 1.25rem; border-top: 1px solid rgba(255,255,255,0.1); }
.sa-user { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; }
.sa-avatar { width: 36px; height: 36px; background: rgba(255,255,255,0.2); border-radius: 50%;
display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.85rem; }
.sa-user-info strong { display: block; font-size: 0.85rem; }
.sa-user-info span { font-size: 0.72rem; opacity: 0.6; }
.sa-logout { display: flex; align-items: center; gap: 0.5rem; color: rgba(255,255,255,0.6);
text-decoration: none; font-size: 0.8rem; padding: 0.4rem 0; transition: color 0.2s; }
.sa-logout:hover { color: white; }
/* 🟦 Main area 🟦 */
.sa-main { display: flex; flex-direction: column; min-height: 100vh; overflow-x: hidden; }
.sa-header { background: white; border-bottom: 1px solid var(--border); padding: 1rem 2rem;
display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 50; }
.sa-header h1 { font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 700; }
.sa-header p { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.1rem; }
.sa-content { padding: 2rem; flex: 1; }
/* ── Stats row ── */
.sa-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.sa-stat { background: white; border: 1px solid var(--border); border-radius: 14px; padding: 1.25rem;
box-shadow: var(--shadow-sm); }
.sa-stat-label { font-size: 0.75rem; color: var(--text-muted); font-weight: 500; margin-bottom: 0.4rem; }
.sa-stat-value { font-size: 2rem; font-weight: 800; font-family: 'Outfit', sans-serif; color: var(--primary); }
.sa-stat-sub { font-size: 0.72rem; color: var(--text-muted); margin-top: 0.2rem; }
/* ── Tab panels ── */
.sa-tab { display: none; }
.sa-tab.active { display: block; }
/* ── Section card ── */
.sa-card { background: white; border: 1px solid var(--border); border-radius: 16px;
box-shadow: var(--shadow-sm); overflow: hidden; }
.sa-card-header { padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.sa-card-header h2 { font-size: 1rem; font-weight: 700; }
.sa-card-header input { padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: 8px;
font-size: 0.85rem; min-width: 220px; outline: none; }
.sa-card-header input:focus { border-color: var(--primary); }
/* ── Table ── */
.sa-table { width: 100%; border-collapse: collapse; }
.sa-table th { background: var(--bg); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--text-muted); padding: 0.75rem 1.5rem;
text-align: left; border-bottom: 1px solid var(--border); }
.sa-table td { padding: 1rem 1.5rem; font-size: 0.875rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
.sa-table tr:last-child td { border-bottom: none; }
.sa-table tr:hover td { background: #F8FAFC; }
.sa-table .empty-row td { text-align: center; color: var(--text-muted); padding: 3rem; }
/* ── Pills ── */
.pill { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.25rem 0.65rem;
border-radius: 20px; font-size: 0.72rem; font-weight: 600; }
.pill-active { background: #D1FAE5; color: #065F46; }
.pill-suspended{ background: #FEE2E2; color: #991B1B; }
.pill-pending { background: #FEF3C7; color: #92400E; }
.pill-approved { background: #D1FAE5; color: #065F46; }
.pill-rejected { background: #F3F4F6; color: #6B7280; }
.pill-basic { background: #EFF6FF; color: #1E40AF; }
.pill-pro { background: #F5F3FF; color: #6D28D9; }
.pill-enterprise { background: #FFF7ED; color: #C2410C; }
.pill-owner { background: #ECFDF5; color: #047857; }
.pill-doctor { background: #EFF6FF; color: #1D4ED8; }
.pill-receptionist { background: #FEF9C3; color: #92400E; }
.pill-pharmacist { background: #F0FDF4; color: #166534; }
.pill-lab_tech { background: #F5F3FF; color: #5B21B6; }
/* ── Buttons ── */
.btn-sm { padding: 0.35rem 0.8rem; border-radius: 7px; font-size: 0.78rem; font-weight: 600;
border: none; cursor: pointer; transition: all 0.15s; }
.btn-sm-primary { background: var(--primary); color: white; }
.btn-sm-primary:hover { opacity: 0.88; }
.btn-sm-danger { background: #FEF2F2; color: #DC2626; border: 1px solid #FCA5A5; }
.btn-sm-danger:hover { background: #DC2626; color: white; }
.btn-sm-warn { background: #FEF3C7; color: #92400E; border: 1px solid #FCD34D; }
.btn-sm-warn:hover { background: #F59E0B; color: white; }
.btn-sm-neutral { background: #F3F4F6; color: #374151; border: 1px solid var(--border); }
.btn-sm-neutral:hover { background: #E5E7EB; }
.btn-sm-success { background: #D1FAE5; color: #065F46; border: 1px solid #6EE7B7; }
.btn-sm-success:hover { background: #10B981; color: white; }
.action-group { display: flex; gap: 0.4rem; flex-wrap: wrap; }
/* ── Modal ── */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5);
z-index: 1000; align-items: center; justify-content: center; padding: 1rem; }
.modal-overlay.open { display: flex; }
.modal-box { background: white; border-radius: 20px; padding: 2rem; width: 100%; max-width: 540px;
max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.2); }
.modal-box h2 { font-family: 'Outfit', sans-serif; font-size: 1.25rem; margin-bottom: 1.5rem; }
.modal-footer { display: flex; gap: 0.75rem; margin-top: 1.5rem; }
.modal-footer .btn-primary { flex: 2; }
.modal-footer .btn-secondary { flex: 1; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.form-group { display: flex; flex-direction: column; gap: 0.35rem; }
.form-group.full { grid-column: 1 / -1; }
.form-group label { font-size: 0.78rem; font-weight: 600; color: var(--text-muted); }
.form-group input, .form-group select { padding: 0.6rem 0.75rem; border: 1px solid var(--border);
border-radius: 8px; font-size: 0.875rem; outline: none; font-family: inherit; }
.form-group input:focus, .form-group select:focus { border-color: var(--primary); }
/* ── Drawer (user list per clinic) ── */
.drawer-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 900; }
.drawer-overlay.open { display: block; }
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: 420px; background: white;
box-shadow: -8px 0 30px rgba(0,0,0,0.12); z-index: 901; display: flex; flex-direction: column; transform: translateX(100%); transition: transform 0.3s ease; }
.drawer.open { transform: translateX(0); }
.drawer-header { padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.drawer-header h3 { font-size: 1rem; font-weight: 700; }
.drawer-close { background: none; border: none; font-size: 1.4rem; cursor: pointer; color: var(--text-muted); }
.drawer-body { flex: 1; overflow-y: auto; padding: 1rem; }
/* ── Alerts ── */
.sa-alert { padding: 0.75rem 1rem; border-radius: 8px; font-size: 0.85rem; margin-bottom: 1rem; display: none; }
.sa-alert.show { display: flex; align-items: center; gap: 0.5rem; }
.sa-alert-success { background: #D1FAE5; color: #065F46; border: 1px solid #6EE7B7; }
.sa-alert-error { background: #FEF2F2; color: #DC2626; border: 1px solid #FCA5A5; }
/* ── Loading ── */
.loading-row td { text-align: center; padding: 2rem; color: var(--text-muted); }
</style>
</head>
<body>
<!-- Sidebar -->
<aside class="sa-sidebar">
<div class="sa-logo">
<div class="sa-logo-icon">C</div>
<div class="sa-logo-text">Curio Central <small>Super Admin</small></div>
</div>
<nav class="sa-nav" id="saNav">
<a href="#" class="active" data-tab="overview" id="nav-overview">📊 Overview</a>
<a href="#" data-tab="clinics" id="nav-clinics">🏥 Clinics</a>
<a href="#" data-tab="pending" id="nav-pending">⏳ Pending <span class="nav-badge" id="pendingBadge" style="display:none">0</span></a>
<a href="#" data-tab="users" id="nav-users">👤 All Users</a>
</nav>
<div class="sa-footer">
<div class="sa-user">
<div class="sa-avatar" id="saAvatar">SA</div>
<div class="sa-user-info">
<strong id="saName">Super Admin</strong>
<span id="saEmail">admin@curio.app</span>
</div>
</div>
<a href="login.html" class="sa-logout" onclick="logout()">🚪 Logout</a>
</div>
</aside>
<!-- Main -->
<main class="sa-main">
<!-- Header -->
<header class="sa-header">
<div>
<h1 id="pageTitle">Platform Overview</h1>
<p id="pageSubtitle">Real-time analytics across all registered hospital tenants</p>
</div>
<div style="display:flex; gap:0.75rem;">
<button class="btn-primary" id="headerActionBtn" onclick="openCreateClinicModal()" style="display:none;">
+ New Clinic
</button>
</div>
</header>
<div class="sa-content">
<!-- Global alert -->
<div class="sa-alert sa-alert-success" id="globalSuccess"><span id="globalSuccessMsg"></span></div>
<div class="sa-alert sa-alert-error" id="globalError">⚠️ <span id="globalErrorMsg"></span></div>
<!-- ── OVERVIEW TAB ── -->
<div class="sa-tab active" id="tab-overview">
<div class="sa-stats" id="statsRow">
<div class="sa-stat"><div class="sa-stat-label">Active Clinics</div><div class="sa-stat-value" id="stat-clinics"></div><div class="sa-stat-sub">Across all plans</div></div>
<div class="sa-stat"><div class="sa-stat-label">Pending Approvals</div><div class="sa-stat-value" id="stat-pending"></div><div class="sa-stat-sub">Awaiting review</div></div>
<div class="sa-stat"><div class="sa-stat-label">Total Users</div><div class="sa-stat-value" id="stat-users"></div><div class="sa-stat-sub">Clinic staff</div></div>
<div class="sa-stat"><div class="sa-stat-label">System Health</div><div class="sa-stat-value" style="color:#10B981;">99.9%</div><div class="sa-stat-sub">All regions healthy</div></div>
</div>
<div class="sa-card" style="margin-top: 0;">
<div class="sa-card-header"><h2>Quick Actions</h2></div>
<div style="padding: 1.5rem; display: flex; gap: 1rem; flex-wrap: wrap;">
<button class="btn-primary" onclick="switchTab('clinics')">🏥 Manage Clinics</button>
<button class="btn-primary" onclick="switchTab('pending')" style="background: #F59E0B;">⏳ Review Pending</button>
<button class="btn-primary" onclick="openCreateClinicModal()" style="background: #10B981;">+ Create Clinic</button>
<button class="btn-primary" onclick="switchTab('users')" style="background: #6D28D9;">👤 View All Users</button>
</div>
</div>
</div>
<!-- ── CLINICS TAB ── -->
<div class="sa-tab" id="tab-clinics">
<div class="sa-card">
<div class="sa-card-header">
<h2>All Clinics <span id="clinicCount" style="font-size:0.8rem; color:var(--text-muted); font-weight:400;"></span></h2>
<input type="text" id="clinicSearch" placeholder="🔍 Search by name or city..." oninput="filterClinics()">
</div>
<div style="overflow-x:auto;">
<table class="sa-table" id="clinicsTable">
<thead><tr>
<th>Clinic ID</th><th>Name</th><th>City</th><th>Plan</th><th>Users</th><th>Status</th><th>Actions</th>
</tr></thead>
<tbody id="clinicsBody">
<tr class="loading-row"><td colspan="7">Loading clinics...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── PENDING TAB ── -->
<div class="sa-tab" id="tab-pending">
<div class="sa-card">
<div class="sa-card-header">
<h2>Pending Registrations</h2>
<button class="btn-primary" onclick="loadPending()" style="font-size:0.8rem; padding:0.4rem 0.9rem;">↻ Refresh</button>
</div>
<div style="overflow-x:auto;">
<table class="sa-table">
<thead><tr>
<th>Submitted</th><th>Clinic Name</th><th>Owner</th><th>Email</th><th>WhatsApp</th><th>Plan</th><th>Status</th><th>Actions</th>
</tr></thead>
<tbody id="pendingBody">
<tr class="loading-row"><td colspan="8">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── USERS TAB ── -->
<div class="sa-tab" id="tab-users">
<div class="sa-card">
<div class="sa-card-header">
<h2>All Users</h2>
<input type="text" id="userSearch" placeholder="🔍 Search by name or email..." oninput="filterUsers()">
</div>
<div style="overflow-x:auto;">
<table class="sa-table">
<thead><tr>
<th>Name</th><th>Email</th><th>Role</th><th>Clinic</th><th>Status</th><th>Actions</th>
</tr></thead>
<tbody id="usersBody">
<tr class="loading-row"><td colspan="6">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
<!-- ── CREATE CLINIC MODAL ── -->
<div class="modal-overlay" id="createClinicModal">
<div class="modal-box">
<h2>🏥 Create New Clinic</h2>
<div class="sa-alert sa-alert-error" id="createClinicError">⚠️ <span id="createClinicErrorMsg"></span></div>
<div class="form-grid">
<div class="form-group full"><label>Clinic Name *</label><input id="cc-name" placeholder="Sharma Heart Center"></div>
<div class="form-group"><label>Specialty</label><input id="cc-specialty" placeholder="Cardiology"></div>
<div class="form-group"><label>City</label><input id="cc-city" placeholder="Hyderabad"></div>
<div class="form-group"><label>WhatsApp Number</label><input id="cc-wa" placeholder="+91 98765 43210"></div>
<div class="form-group">
<label>Plan</label>
<select id="cc-plan">
<option value="basic">Basic</option>
<option value="pro">Pro</option>
<option value="enterprise">Enterprise</option>
</select>
</div>
<div class="form-group"><label>Consultation Fee (₹)</label><input id="cc-fee" type="number" placeholder="500" value="500"></div>
<div class="form-group full" style="border-top:1px solid var(--border); padding-top:1rem; margin-top:0.5rem;">
<strong style="font-size:0.85rem;">Owner Account</strong>
</div>
<div class="form-group full"><label>Owner Full Name *</label><input id="cc-owner-name" placeholder="Dr. Satish Sharma"></div>
<div class="form-group"><label>Owner Email *</label><input id="cc-owner-email" type="email" placeholder="owner@clinic.com"></div>
<div class="form-group"><label>Temporary Password *</label><input id="cc-owner-pass" type="password" placeholder="Min. 8 characters"></div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeModal('createClinicModal')">Cancel</button>
<button class="btn-primary" id="createClinicBtn" onclick="submitCreateClinic()">Create Clinic</button>
</div>
</div>
</div>
<!-- ── ADD USER MODAL ── -->
<div class="modal-overlay" id="addUserModal">
<div class="modal-box">
<h2>👤 Add User to <span id="addUserClinicName"></span></h2>
<div class="sa-alert sa-alert-error" id="addUserError">⚠️ <span id="addUserErrorMsg"></span></div>
<div class="form-grid">
<div class="form-group full"><label>Full Name *</label><input id="au-name" placeholder="Dr. Priya Nair"></div>
<div class="form-group"><label>Email *</label><input id="au-email" type="email" placeholder="priya@clinic.com"></div>
<div class="form-group">
<label>Role *</label>
<select id="au-role">
<option value="DOCTOR">Doctor</option>
<option value="RECEPTIONIST">Receptionist</option>
<option value="PHARMACIST">Pharmacist</option>
<option value="LAB_TECH">Lab Technician</option>
<option value="OWNER">Owner</option>
</select>
</div>
<div class="form-group"><label>Password *</label><input id="au-pass" type="password" placeholder="Min. 8 characters"></div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeModal('addUserModal')">Cancel</button>
<button class="btn-primary" id="addUserBtn" onclick="submitAddUser()">Add User</button>
</div>
</div>
</div>
<!-- ── USERS DRAWER ── -->
<div class="drawer-overlay" id="drawerOverlay" onclick="closeDrawer()"></div>
<div class="drawer" id="usersDrawer">
<div class="drawer-header">
<h3>Users — <span id="drawerClinicName">Clinic</span></h3>
<button class="drawer-close" onclick="closeDrawer()">×</button>
</div>
<div class="drawer-body">
<button class="btn-primary" style="width:100%; margin-bottom:1rem;" onclick="openAddUserModal()">+ Add User</button>
<div id="drawerUserList"></div>
</div>
</div>
<script>
// ── Auth guard ──────────────────────────────────────────────────────────────
let currentUser = null;
(function() {
const token = localStorage.getItem('curio_token');
if (!token) { window.location.href = '/'; return; }
try {
const p = JSON.parse(atob(token.split('.')[1]));
if (p.role !== 'SUPER_ADMIN') { window.location.href = '/'; return; }
if (p.exp && Date.now()/1000 > p.exp) { window.location.href = '/'; return; }
currentUser = p;
document.getElementById('saName').textContent = p.name || 'Super Admin';
document.getElementById('saEmail').textContent = p.email || '';
const initials = (p.name || 'SA').split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase();
document.getElementById('saAvatar').textContent = initials;
} catch(_) { window.location.href = '/'; }
})();
function logout() {
localStorage.clear();
window.location.href = '/';
}
// ── Fetch helper ────────────────────────────────────────────────────────────
function authFetch(url, opts = {}) {
const token = localStorage.getItem('curio_token');
opts.headers = { ...(opts.headers || {}), 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
return fetch(url, opts);
}
// ── Alerts ──────────────────────────────────────────────────────────────────
function showSuccess(msg) {
document.getElementById('globalSuccessMsg').textContent = msg;
const el = document.getElementById('globalSuccess');
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 4000);
}
function showGlobalError(msg) {
document.getElementById('globalErrorMsg').textContent = msg;
const el = document.getElementById('globalError');
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 5000);
}
// ── Tab navigation ──────────────────────────────────────────────────────────
const tabConfig = {
overview: { title: 'Platform Overview', subtitle: 'Real-time analytics across all registered hospital tenants', showBtn: false },
clinics: { title: 'All Clinics', subtitle: 'Create, manage and suspend clinic tenants', showBtn: true },
pending: { title: 'Pending Registrations', subtitle: 'Approve or reject self-registration requests', showBtn: false },
users: { title: 'All Users', subtitle: 'View and manage staff accounts across all clinics', showBtn: false },
};
function switchTab(tab) {
document.querySelectorAll('.sa-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.sa-nav a').forEach(a => a.classList.remove('active'));
document.getElementById(`tab-${tab}`).classList.add('active');
document.getElementById(`nav-${tab}`).classList.add('active');
const cfg = tabConfig[tab];
document.getElementById('pageTitle').textContent = cfg.title;
document.getElementById('pageSubtitle').textContent = cfg.subtitle;
document.getElementById('headerActionBtn').style.display = cfg.showBtn ? '' : 'none';
if (tab === 'clinics') loadClinics();
if (tab === 'pending') loadPending();
if (tab === 'users') loadAllUsers();
if (tab === 'overview') loadStats();
}
document.getElementById('saNav').addEventListener('click', e => {
const a = e.target.closest('[data-tab]');
if (a) { e.preventDefault(); switchTab(a.dataset.tab); }
});
// ── Stats ────────────────────────────────────────────────────────────────────
async function loadStats() {
try {
const res = await authFetch('/api/super/stats');
const data = await res.json();
document.getElementById('stat-clinics').textContent = data.activeClinics ?? '—';
document.getElementById('stat-pending').textContent = data.pendingCount ?? '—';
document.getElementById('stat-users').textContent = data.totalUsers ?? '—';
// Update pending badge
if (data.pendingCount > 0) {
const badge = document.getElementById('pendingBadge');
badge.textContent = data.pendingCount;
badge.style.display = '';
}
} catch(e) { console.error('Stats load failed', e); }
}
// ── Clinics ──────────────────────────────────────────────────────────────────
let allClinics = [];
function planPill(plan) {
const map = { basic: 'pill-basic', pro: 'pill-pro', enterprise: 'pill-enterprise' };
return `<span class="pill ${map[plan] || 'pill-basic'}">${(plan||'basic').toUpperCase()}</span>`;
}
function statusPill(status) {
return status === 'active'
? `<span class="pill pill-active">● Active</span>`
: `<span class="pill pill-suspended">● Suspended</span>`;
}
async function loadClinics() {
document.getElementById('clinicsBody').innerHTML = '<tr class="loading-row"><td colspan="7">Loading...</td></tr>';
try {
const res = await authFetch('/api/super/clinics');
allClinics = await res.json();
renderClinics(allClinics);
} catch(e) { showGlobalError('Failed to load clinics.'); }
}
function renderClinics(list) {
document.getElementById('clinicCount').textContent = `(${list.length})`;
const tbody = document.getElementById('clinicsBody');
if (!list.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">No clinics yet. Create one with the button above.</td></tr>';
return;
}
tbody.innerHTML = list.map(c => `
<tr>
<td><code style="font-size:0.78rem;">${c.id}</code></td>
<td><strong>${c.name}</strong>${c.specialty ? `<br><small style="color:var(--text-muted)">${c.specialty}</small>` : ''}</td>
<td>${c.city || '<span style="color:var(--text-muted)">—</span>'}</td>
<td>${planPill(c.plan)}</td>
<td><span style="font-weight:600;">${c.user_count}</span></td>
<td>${statusPill(c.status)}</td>
<td>
<div class="action-group">
<button class="btn-sm btn-sm-neutral" onclick="openUsersDrawer('${c.id}','${c.name.replace(/'/g,"\\'")}')">👥 Users</button>
${c.status === 'active'
? `<button class="btn-sm btn-sm-warn" onclick="toggleClinicStatus('${c.id}','suspended')">Suspend</button>`
: `<button class="btn-sm btn-sm-success" onclick="toggleClinicStatus('${c.id}','active')">Resume</button>`}
<button class="btn-sm btn-sm-danger" onclick="deleteClinic('${c.id}','${c.name.replace(/'/g,"\\'")}')">Delete</button>
</div>
</td>
</tr>`).join('');
}
function filterClinics() {
const q = document.getElementById('clinicSearch').value.toLowerCase();
renderClinics(allClinics.filter(c =>
c.name.toLowerCase().includes(q) || (c.city||'').toLowerCase().includes(q) || c.id.includes(q)
));
}
async function toggleClinicStatus(id, status) {
try {
await authFetch(`/api/super/clinics/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status }) });
showSuccess(`Clinic ${status === 'active' ? 'activated' : 'suspended'}.`);
loadClinics();
} catch(e) { showGlobalError('Failed to update status.'); }
}
async function deleteClinic(id, name) {
if (!confirm(`⚠️ DELETE "${name}" and ALL its users permanently?\n\nThis cannot be undone.`)) return;
try {
await authFetch(`/api/super/clinics/${id}`, { method: 'DELETE' });
showSuccess(`Clinic "${name}" deleted.`);
loadClinics();
loadStats();
} catch(e) { showGlobalError('Failed to delete clinic.'); }
}
// ── Create Clinic Modal ──────────────────────────────────────────────────────
function openCreateClinicModal() {
['cc-name','cc-specialty','cc-city','cc-wa','cc-owner-name','cc-owner-email','cc-owner-pass'].forEach(id => {
document.getElementById(id).value = '';
});
document.getElementById('cc-fee').value = '500';
document.getElementById('cc-plan').value = 'basic';
document.getElementById('createClinicError').classList.remove('show');
openModal('createClinicModal');
}
async function submitCreateClinic() {
const btn = document.getElementById('createClinicBtn');
btn.disabled = true; btn.textContent = 'Creating...';
const errEl = document.getElementById('createClinicError');
const errMsg = document.getElementById('createClinicErrorMsg');
const payload = {
clinicName: document.getElementById('cc-name').value.trim(),
specialty: document.getElementById('cc-specialty').value.trim(),
city: document.getElementById('cc-city').value.trim(),
whatsappNumber:document.getElementById('cc-wa').value.trim(),
plan: document.getElementById('cc-plan').value,
consultationFee: parseInt(document.getElementById('cc-fee').value) || 500,
ownerName: document.getElementById('cc-owner-name').value.trim(),
ownerEmail: document.getElementById('cc-owner-email').value.trim(),
ownerPassword: document.getElementById('cc-owner-pass').value,
};
if (!payload.clinicName || !payload.ownerName || !payload.ownerEmail || !payload.ownerPassword) {
errMsg.textContent = 'Please fill all required fields.';
errEl.classList.add('show');
btn.disabled = false; btn.textContent = 'Create Clinic';
return;
}
try {
const res = await authFetch('/api/super/clinics', { method: 'POST', body: JSON.stringify(payload) });
const data = await res.json();
if (!res.ok) { errMsg.textContent = data.error || 'Failed'; errEl.classList.add('show'); return; }
closeModal('createClinicModal');
showSuccess(`Clinic "${payload.clinicName}" created! Tenant ID: ${data.tenantId}`);
loadClinics(); loadStats();
} catch(e) {
errMsg.textContent = 'Network error.'; errEl.classList.add('show');
} finally {
btn.disabled = false; btn.textContent = 'Create Clinic';
}
}
// ── Pending ──────────────────────────────────────────────────────────────────
async function loadPending() {
document.getElementById('pendingBody').innerHTML = '<tr class="loading-row"><td colspan="8">Loading...</td></tr>';
try {
const res = await authFetch('/api/super/pending');
const list = await res.json();
const badge = document.getElementById('pendingBadge');
const pendingCount = list.filter(p => p.status === 'pending').length;
badge.textContent = pendingCount;
badge.style.display = pendingCount > 0 ? '' : 'none';
if (!list.length) {
document.getElementById('pendingBody').innerHTML = '<tr class="empty-row"><td colspan="8">No registrations yet.</td></tr>';
return;
}
document.getElementById('pendingBody').innerHTML = list.map(p => {
const date = new Date(p.created_at).toLocaleDateString('en-IN', { day:'numeric', month:'short', year:'numeric' });
const statusPillHtml = p.status === 'pending'
? `<span class="pill pill-pending">⏳ Pending</span>`
: p.status === 'approved'
? `<span class="pill pill-approved">✅ Approved</span>`
: `<span class="pill pill-rejected">❌ Rejected</span>`;
const actions = p.status === 'pending' ? `
<div class="action-group">
<button class="btn-sm btn-sm-success" onclick="approvePending(${p.id},'${p.clinic_name.replace(/'/g,"\\'")}')">✅ Approve</button>
<button class="btn-sm btn-sm-danger" onclick="rejectPending(${p.id})">Reject</button>
</div>` : '—';
return `<tr>
<td style="white-space:nowrap; font-size:0.78rem;">${date}</td>
<td><strong>${p.clinic_name}</strong>${p.specialty ? `<br><small>${p.specialty}</small>` : ''}</td>
<td>${p.owner_name}</td>
<td style="font-size:0.8rem;">${p.email}</td>
<td style="font-size:0.8rem;">${p.whatsapp_number}</td>
<td>${planPill(p.plan)}</td>
<td>${statusPillHtml}</td>
<td>${actions}</td>
</tr>`;
}).join('');
} catch(e) { showGlobalError('Failed to load pending registrations.'); }
}
async function approvePending(id, name) {
if (!confirm(`Approve "${name}" and create their tenant + owner account?`)) return;
try {
const res = await authFetch(`/api/super/pending/${id}/approve`, { method: 'POST' });
const data = await res.json();
if (!res.ok) { showGlobalError(data.error || 'Failed to approve'); return; }
showSuccess(`"${name}" approved! Tenant ID: ${data.tenantId}`);
loadPending(); loadStats();
} catch(e) { showGlobalError('Network error.'); }
}
async function rejectPending(id) {
if (!confirm('Reject this registration request?')) return;
try {
await authFetch(`/api/super/pending/${id}/reject`, { method: 'POST' });
showSuccess('Registration rejected.');
loadPending();
} catch(e) { showGlobalError('Network error.'); }
}
// ── All Users tab ────────────────────────────────────────────────────────────
let allUsers = [];
let userClinicMap = {};
function rolePill(role) {
const map = {
OWNER:'pill-owner', DOCTOR:'pill-doctor', RECEPTIONIST:'pill-receptionist',
PHARMACIST:'pill-pharmacist', LAB_TECH:'pill-lab_tech'
};
return `<span class="pill ${map[role]||'pill-basic'}">${role.replace('_',' ')}</span>`;
}
async function loadAllUsers() {
document.getElementById('usersBody').innerHTML = '<tr class="loading-row"><td colspan="6">Loading...</td></tr>';
try {
// We need clinics list to resolve names
if (!allClinics.length) {
const res = await authFetch('/api/super/clinics');
allClinics = await res.json();
}
userClinicMap = {};
allClinics.forEach(c => userClinicMap[c.id] = c.name);
// Gather users per clinic in parallel
const usersByClinic = await Promise.all(allClinics.map(c =>
authFetch(`/api/super/clinics/${c.id}/users`).then(r => r.json()).then(u => u.map(x => ({ ...x, clinicId: c.id })))
));
allUsers = usersByClinic.flat();
renderUsers(allUsers);
} catch(e) { showGlobalError('Failed to load users.'); }
}
function renderUsers(list) {
const tbody = document.getElementById('usersBody');
if (!list.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="6">No users found.</td></tr>';
return;
}
tbody.innerHTML = list.map(u => `
<tr>
<td><strong>${u.name}</strong></td>
<td style="font-size:0.82rem;">${u.email}</td>
<td>${rolePill(u.role)}</td>
<td style="font-size:0.82rem;">${userClinicMap[u.clinicId] || u.clinicId}</td>
<td><span class="pill ${u.status==='active'?'pill-active':'pill-suspended'}">${u.status}</span></td>
<td>
<div class="action-group">
<button class="btn-sm btn-sm-danger" onclick="deleteUser(${u.id},'${u.name.replace(/'/g,"\\'")}')">Remove</button>
</div>
</td>
</tr>`).join('');
}
function filterUsers() {
const q = document.getElementById('userSearch').value.toLowerCase();
renderUsers(allUsers.filter(u => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)));
}
async function deleteUser(id, name) {
if (!confirm(`Remove user "${name}" from the system?`)) return;
try {
await authFetch(`/api/super/users/${id}`, { method: 'DELETE' });
showSuccess(`User "${name}" removed.`);
loadAllUsers();
} catch(e) { showGlobalError('Failed to remove user.'); }
}
// ── Users Drawer ─────────────────────────────────────────────────────────────
let drawerClinicId = null;
async function openUsersDrawer(clinicId, clinicName) {
drawerClinicId = clinicId;
document.getElementById('drawerClinicName').textContent = clinicName;
document.getElementById('drawerOverlay').classList.add('open');
document.getElementById('usersDrawer').classList.add('open');
await loadDrawerUsers(clinicId);
}
async function loadDrawerUsers(clinicId) {
document.getElementById('drawerUserList').innerHTML = '<p style="text-align:center;color:var(--text-muted);">Loading...</p>';
try {
const res = await authFetch(`/api/super/clinics/${clinicId}/users`);
const list = await res.json();
if (!list.length) {
document.getElementById('drawerUserList').innerHTML = '<p style="text-align:center;color:var(--text-muted);">No users yet.</p>';
return;
}
document.getElementById('drawerUserList').innerHTML = list.map(u => `
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.75rem; border:1px solid var(--border); border-radius:10px; margin-bottom:0.5rem;">
<div>
<div style="font-weight:600; font-size:0.875rem;">${u.name}</div>
<div style="font-size:0.75rem; color:var(--text-muted);">${u.email}</div>
<div style="margin-top:0.25rem;">${rolePill(u.role)}</div>
</div>
<button class="btn-sm btn-sm-danger" onclick="deleteUser(${u.id},'${u.name.replace(/'/g,"\\'")}').then(()=>loadDrawerUsers('${clinicId}'))">✕</button>
</div>`).join('');
} catch(e) { document.getElementById('drawerUserList').innerHTML = '<p style="color:red;">Failed to load.</p>'; }
}
function closeDrawer() {
document.getElementById('drawerOverlay').classList.remove('open');
document.getElementById('usersDrawer').classList.remove('open');
drawerClinicId = null;
}
// ── Add User Modal ───────────────────────────────────────────────────────────
function openAddUserModal() {
if (!drawerClinicId) return;
const clinicName = document.getElementById('drawerClinicName').textContent;
document.getElementById('addUserClinicName').textContent = clinicName;
['au-name','au-email','au-pass'].forEach(id => document.getElementById(id).value = '');
document.getElementById('au-role').value = 'DOCTOR';
document.getElementById('addUserError').classList.remove('show');
openModal('addUserModal');
}
async function submitAddUser() {
const btn = document.getElementById('addUserBtn');
btn.disabled = true; btn.textContent = 'Adding...';
const errEl = document.getElementById('addUserError');
const errMsg = document.getElementById('addUserErrorMsg');
errEl.classList.remove('show');
const payload = {
name: document.getElementById('au-name').value.trim(),
email: document.getElementById('au-email').value.trim(),
password: document.getElementById('au-pass').value,
role: document.getElementById('au-role').value,
};
if (!payload.name || !payload.email || !payload.password) {
errMsg.textContent = 'All fields are required.';
errEl.classList.add('show');
btn.disabled = false; btn.textContent = 'Add User';
return;
}
try {
const res = await authFetch(`/api/super/clinics/${drawerClinicId}/users`, { method:'POST', body: JSON.stringify(payload) });
const data = await res.json();
if (!res.ok) { errMsg.textContent = data.error || 'Failed'; errEl.classList.add('show'); return; }
closeModal('addUserModal');
showSuccess(`User "${payload.name}" added successfully.`);
await loadDrawerUsers(drawerClinicId);
loadAllUsers();
} catch(e) {
errMsg.textContent = 'Network error.'; errEl.classList.add('show');
} finally {
btn.disabled = false; btn.textContent = 'Add User';
}
}
// ── Modal helpers ────────────────────────────────────────────────────────────
function openModal(id) { document.getElementById(id).classList.add('open'); }
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
// Close modal on overlay click
document.querySelectorAll('.modal-overlay').forEach(o => {
o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); });
});
// ── Init ─────────────────────────────────────────────────────────────────────
loadStats();
</script>
</body>
</html>