799 lines
45 KiB
HTML
799 lines
45 KiB
HTML
<!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>
|