/* TMD Portal — People: staff list and access management (backed by public.tmd_staff). Owners and Managers add people, send invites, change roles, deactivate or remove. Only an Owner can touch another Owner. Interns see the list only. */ const { useState: uSs, useEffect: uEs, useMemo: uMs, useCallback: uCs } = React; const STAFF_ROLES = ['Owner', 'Manager', 'Intern']; const initialsOf = name => (name || '?').split(/\s+/).map(w => w[0]).join('').slice(0, 2).toUpperCase(); async function staffCall(_session, payload) { // Always use the current token: the one captured at page load may have been refreshed since. const { data: { session } } = await SB.auth.getSession(); if (!session) throw new Error('Not signed in.'); const res = await fetch(window.TMD_CONFIG.supabaseUrl + '/functions/v1/manage-staff', { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': window.TMD_CONFIG.supabaseKey, 'Authorization': 'Bearer ' + session.access_token }, body: JSON.stringify(payload) }); const data = await res.json().catch(() => ({})); if (!res.ok || data.error) throw new Error(data.error || ('Request failed (HTTP ' + res.status + ')')); return data; } function Staff({ app, session, reloadSession }) { const [rows, setRows] = uSs(null); const [err, setErr] = uSs(''); const [adding, setAdding] = uSs(false); const [confirm, setConfirm] = uSs(null); const [busy, setBusy] = uSs(''); const me = (session && session.user.email || '').toLowerCase(); const myRole = app.role; const isAdmin = myRole === 'Owner' || myRole === 'Manager'; const isOwner = myRole === 'Owner'; const load = uCs(async () => { const { data, error } = await SB.from('tmd_staff').select('*').order('role').order('name'); if (error) { setErr(error.message); return; } setErr(''); setRows(data || []); }, []); uEs(() => { load(); }, [load]); const canTouch = r => isAdmin && (isOwner || r.role !== 'Owner'); const isMe = r => (r.email || '').toLowerCase() === me; const update = async (r, patch, label) => { setBusy(r.email); const { error } = await SB.from('tmd_staff').update(patch).eq('email', r.email); setBusy(''); if (error) { app.toast('Not saved: ' + error.message); return; } app.toast(label || 'Saved'); await load(); if (isMe(r) && reloadSession) reloadSession(); }; const act = async (r, action, label) => { setBusy(r.email); try { const out = await staffCall(session, { action, email: r.email }); app.toast(out.note || label); await load(); } catch (e) { app.toast(e.message); } finally { setBusy(''); } }; const active = (rows || []).filter(r => r.active), inactive = (rows || []).filter(r => !r.active); return (
{err}
Loading
: null} {rows ? (| Name | Role | Status | {isAdmin ?Actions | : null}|
|---|---|---|---|---|
| {initialsOf(r.name)}{r.name}{isMe(r) ? you : null} | {r.email} | {canTouch(r) && !isMe(r) ? ( ) : r.role} |
{r.active ? |
{isAdmin ? (
{canTouch(r) && !isMe(r) ? ( <> > ) : —} | ) : null}
Adding a person emails them an invite link to set a password. Supabase’s built-in mailer allows only a few of these an hour. Deactivating keeps the record but closes access immediately. Removing deletes the record and the account. {isOwner ? ' Only you, as Owner, can add or change other Owners.' : ' Only an Owner can add or change Owners.'}
Owner and Manager see money and can manage people. Intern sees work and tasks only.
{err ?{err}
: null}