/* Admin: user management. List of every registered user with role + subscription chips and quick actions to grant/revoke a subscription or change role. Admin gate is enforced server-side too — non-admins hitting /api/admin/users get 403. Owner-only actions (Items 4 and 5): • Upload-grant toggle — flips can_upload. Capped at 1 non-owner uploader per Ishaan's spec; server returns 400 if the cap is hit. • Comp subscription — bypass-payment endpoint, logs bypass_reason. Both buttons are hidden for non-owner admins (Ishaan = AICAN_OWNER_EMAIL). */ const AdminView = ({ user }) => { const t = window.useLanguage().t; const [rows, setRows] = React.useState([]); const [loading, setLoading] = React.useState(false); const [err, setErr] = React.useState(''); const [busyKey, setBusyKey] = React.useState(''); // disables the row's buttons mid-call const [filter, setFilter] = React.useState(''); const isOwner = !!(user && user.is_owner); const refresh = React.useCallback(async () => { setLoading(true); setErr(''); try { const r = await window.AicanAPI.adminListUsers(); setRows((r && r.users) || []); } catch (e) { setErr(e.message || 'Failed to load users'); } finally { setLoading(false); } }, []); React.useEffect(() => { refresh(); }, [refresh]); const updateUser = async (key, patch) => { setBusyKey(key); setErr(''); try { const r = await window.AicanAPI.adminUpdateUser(key, patch); // Replace just this row in-place so the table doesn't flicker. setRows(prev => prev.map(u => u._email_key === key ? (r.user || u) : u)); } catch (e) { setErr(e.message || 'Update failed'); } finally { setBusyKey(''); } }; // Item 5 — owner-only comp subscription. Prompts for a free-text reason // (stored on the user record as payment_bypass.reason for audit). const bypassPayment = async (key, displayName) => { const reason = window.prompt( t('Reason for waiving payment for') + ` ${displayName}?\n` + t('This is logged for audit (e.g. "VIP partner", "trial extension").'), '' ); if (reason === null) return; // user cancelled setBusyKey(key); setErr(''); try { const r = await window.AicanAPI.adminBypassPayment(key, reason); setRows(prev => prev.map(u => u._email_key === key ? (r.user || u) : u)); } catch (e) { setErr(e.message || 'Bypass failed'); } finally { setBusyKey(''); } }; const visible = rows.filter(u => { if (!filter) return true; const f = filter.toLowerCase(); return (u.email || '').toLowerCase().includes(f) || (u.name || '').toLowerCase().includes(f) || (u.medical_reg_no || '').toLowerCase().includes(f) || (u.role || '').toLowerCase().includes(f); }); const subChip = (s) => s === 'active' ? {t('Active')} : {t('Inactive')}; const roleChip = (r) => { const map = { admin: 'crit', doctor: 'info', patient: 'neutral' }; return {(r || '?').toUpperCase()}; }; // Access-control status (Item 3). Legacy rows without the field read as // "approved" (matches the backend's grandfathering in _public_user). const accessChip = (a) => { const v = a || 'approved'; const map = { approved: 'ok', pending: 'warn', denied: 'crit' }; const label = { approved: t('Approved'), pending: t('Pending'), denied: t('Denied') }; return {label[v] || v}; }; return (
| {t('Name')} | {t('Email')} | {t('Medical Reg. No')} | {t('Role')} | {t('Access')} | {t('Subscription')} | {t('Upload')} | {t('Created')} | {t('Actions')} |
|---|---|---|---|---|---|---|---|---|
|
{u.name || '—'}
{u.is_admin ? '★ ' : ''}{u.google_sub ? 'Google · ' : ''}
{u.created_at ? new Date(u.created_at * 1000).toLocaleDateString() : ''}
|
{u.email} | {u.medical_reg_no || '—'} | {roleChip(u.role)} | {accessChip(u.access_status)} | {subChip(sub)} | {/* Item 4 — upload permission chip. Owner is implicit (always allowed); show "OWNER" for them so the row isn't ambiguous. */} {u.is_owner ? {t('OWNER')} : (u.can_upload ? {t('Granted')} : {t('No')})} | {u.created_at ? new Date(u.created_at * 1000).toLocaleString() : '—'} | {/* Access approval (Item 3). Hidden for admins — they're always approved server-side, so the buttons would be no-ops. */} {!u.is_admin && ( (u.access_status || 'approved') === 'approved' ? ( ) : ( ) )} {/* Subscription toggle */} {sub === 'active' ? ( ) : ( )} {/* Item 4 — Upload grant toggle (owner-only). Hidden for the owner row itself (always allowed) and shown as a tooltip-disabled button for non-owner admins. */} {isOwner && !u.is_owner && ( u.can_upload ? ( ) : ( ) )} {/* Item 5 — owner-only comp subscription (bypass payment). */} {isOwner && !u.is_owner && sub !== 'active' && ( )} {/* Role select (lightweight inline editor) */} |