/* 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 (
setAdding(true)}>Add person : null} /> {err ?
Could not load people.

{err}

: null} {!rows && !err ?

Loading

: null} {rows ? ( {isAdmin ? : null} {active.concat(inactive).map(r => ( {isAdmin ? ( ) : null} ))}
NameEmailRoleStatusActions
{initialsOf(r.name)}{r.name}{isMe(r) ? you : null} {r.email} {canTouch(r) && !isMe(r) ? ( ) : r.role} {r.active ? : } {r.invited_at ? invited {fmtDate(r.invited_at.slice(0, 10), true)} : null} {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.'}

setAdding(false)} session={session} isOwner={isOwner} onDone={async note => { setAdding(false); app.toast(note); await load(); }} /> This deletes the staff record and the sign-in account for {confirm && confirm.email}. It cannot be undone. Use Deactivate if you only want to pause access.

} confirmLabel="Remove" onConfirm={async () => { const r = confirm; setConfirm(null); await act(r, 'remove', r.name + ' removed'); }} onClose={() => setConfirm(null)} />
); } function AddPerson({ open, onClose, session, isOwner, onDone }) { const [name, setName] = uSs(''); const [email, setEmail] = uSs(''); const [role, setRole] = uSs('Manager'); const [invite, setInvite] = uSs(true); const [busy, setBusy] = uSs(false); const [err, setErr] = uSs(''); uEs(() => { if (open) { setName(''); setEmail(''); setRole('Manager'); setInvite(true); setErr(''); setBusy(false); } }, [open]); const submit = async () => { setErr(''); if (!name.trim()) { setErr('A name is needed.'); return; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { setErr('Enter a working email address.'); return; } setBusy(true); try { if (invite) { const out = await staffCall(session, { action: 'invite', email: email.trim(), name: name.trim(), role }); onDone(out.note || (out.invited ? 'Invite sent to ' + email.trim() : 'Added')); } else { const { error } = await SB.from('tmd_staff').upsert({ email: email.trim().toLowerCase(), name: name.trim(), role, active: true, added_by: session.user.email }, { onConflict: 'email' }); if (error) throw error; onDone(name.trim() + ' added. They can create their own account on the sign-in screen.'); } } catch (e) { setErr(e.message || 'Could not add this person.'); setBusy(false); } }; return ( Cancel{busy ? 'Working' : invite ? 'Add and send invite' : 'Add'}}>

Owner and Manager see money and can manage people. Intern sees work and tasks only.

{err ?

{err}

: null}
); } /* Landing from an invite or password-reset link: set a password before entering. */ function SetPassword({ email, onDone, onOut }) { const [pw, setPw] = uSs(''); const [pw2, setPw2] = uSs(''); const [busy, setBusy] = uSs(false); const [err, setErr] = uSs(''); const submit = async e => { if (e && e.preventDefault) e.preventDefault(); setErr(''); if (pw.length < 8) { setErr('Use at least 8 characters.'); return; } if (pw !== pw2) { setErr('The two passwords do not match.'); return; } setBusy(true); const { error } = await SB.auth.updateUser({ password: pw }); setBusy(false); if (error) { setErr(error.message); return; } onDone(); }; return (
Welcome

Set your password.

For {email}. You will use it with this email to sign in from now on.

{err ?

{err}

: null} {busy ? 'Working' : 'Save and continue'}
); } Object.assign(window, { Staff, SetPassword, staffCall });