/* TMD Portal — Supabase session, sign-in, and the Inquiries page. Inquiries come from the public site (themarketingdesk.net) into public.tmd_inquiries. Only emails listed in public.tmd_staff can read or update them (enforced by row security). */ const { useState: uSi, useEffect: uEi, useMemo: uMi, useCallback: uCi } = React; const SB = window.supabase.createClient(window.TMD_CONFIG.supabaseUrl, window.TMD_CONFIG.supabaseKey, { auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: true } }); const INQ_STATUS = [ { v: 'new', l: 'New', sem: 'todo' }, { v: 'scoped', l: 'Scoped', sem: 'doing' }, { v: 'queued', l: 'Queued', sem: 'waiting' }, { v: 'delivered', l: 'Delivered', sem: 'done' }, { v: 'declined', l: 'Declined', sem: 'blocked' } ]; const inqStatus = v => INQ_STATUS.find(s => s.v === v) || INQ_STATUS[0]; const OPEN = ['new', 'scoped', 'queued']; /* ---------- session: Supabase auth + staff row ---------- */ function useSession() { const [state, setState] = uSi({ loading: true, session: null, staff: null, staffList: [] }); // Arriving from an invite or password-reset email: the URL hash says so, and Supabase fires PASSWORD_RECOVERY. const [needsPassword, setNeedsPassword] = uSi(() => /type=(invite|recovery|magiclink)/.test(location.hash)); const loadStaff = uCi(async session => { if (!session) { setState({ loading: false, session: null, staff: null, staffList: [] }); return; } const email = (session.user.email || '').toLowerCase(); const { data, error } = await SB.from('tmd_staff').select('email,name,role,person_id,active').order('name'); const list = error ? [] : (data || []); const me = list.find(s => (s.email || '').toLowerCase() === email && s.active) || null; setState({ loading: false, session, staff: me, staffList: list }); }, []); uEi(() => { let alive = true; SB.auth.getSession().then(({ data }) => { if (alive) loadStaff(data.session); }); // Defer: querying inside the auth callback can deadlock supabase-js. const { data: sub } = SB.auth.onAuthStateChange((evt, session) => { if (evt === 'PASSWORD_RECOVERY') setNeedsPassword(true); if (evt === 'SIGNED_OUT') setNeedsPassword(false); setTimeout(() => { if (alive) loadStaff(session); }, 0); }); return () => { alive = false; sub.subscription.unsubscribe(); }; }, [loadStaff]); return Object.assign({}, state, { needsPassword: needsPassword && !!state.session, passwordSet: () => { setNeedsPassword(false); history.replaceState(null, '', location.pathname); loadStaff(state.session); }, reload: () => loadStaff(state.session), signOut: () => SB.auth.signOut() }); } /* ---------- sign in / create account ---------- */ function SignIn() { const [mode, setMode] = uSi('in'); const [email, setEmail] = uSi(''); const [pw, setPw] = uSi(''); const [busy, setBusy] = uSi(false); const [err, setErr] = uSi(''); const [note, setNote] = uSi(''); const submit = async e => { if (e && e.preventDefault) e.preventDefault(); if (busy) return; setErr(''); setNote(''); if (!email.trim() || !pw) { setErr('Email and password are both needed.'); return; } setBusy(true); try { if (mode === 'in') { const { error } = await SB.auth.signInWithPassword({ email: email.trim(), password: pw }); if (error) throw error; } else if (mode === 'up') { if (pw.length < 8) throw new Error('Use at least 8 characters.'); const { data, error } = await SB.auth.signUp({ email: email.trim(), password: pw, options: { emailRedirectTo: location.origin + location.pathname } }); if (error) throw error; if (data.session) return; // confirmations off: signed in already setNote('Account created. Check your inbox for the confirmation link, then sign in.'); setMode('in'); } else { const { error } = await SB.auth.resetPasswordForEmail(email.trim(), { redirectTo: location.origin + location.pathname }); if (error) throw error; setNote('If that address is on the staff list, a reset link is on its way.'); setMode('in'); } } catch (ex) { setErr(ex.message || 'Sign-in failed.'); } finally { setBusy(false); } }; const title = mode === 'in' ? 'Sign in' : mode === 'up' ? 'Create account' : 'Reset password'; return (
{mode !== 'reset' ? : null} {err ?

{err}

: null} {note ?

{note}

: null} {/* The design-system button renders type="button", so submit explicitly. Enter in a field still submits the form. */} {busy ? 'Working' : title}
{mode !== 'in' ? : null} {mode === 'in' ? : null} {mode === 'in' ? : null}

Only addresses on the Desk’s staff list can see anything once signed in. Ask Abidh to be added.

); } function NotStaff({ email, onOut }) { return (
No access

This account is not on the staff list.

You are signed in as {email}. The Desk only opens for addresses Abidh has added. Nothing here is hidden by accident.

Sign out
); } /* ---------- helpers ---------- */ function fmtStamp(iso) { if (!iso) return '—'; const d = new Date(iso); const hh = String(d.getHours()).padStart(2, '0'), mm = String(d.getMinutes()).padStart(2, '0'); return d.getDate() + ' ' + MONTHS[d.getMonth()] + ' · ' + hh + ':' + mm; } function ago(iso) { const ms = Date.now() - new Date(iso).getTime(); const h = Math.floor(ms / 3600000); if (h < 1) return 'just now'; if (h < 24) return h + 'h ago'; const d = Math.floor(h / 24); return d + 'd ago'; } const waLink = phone => 'https://wa.me/' + String(phone || '').replace(/[^\d]/g, ''); function InqStatus({ v, size }) { const s = inqStatus(v); return ; } /* ---------- the page ---------- */ function Inquiries({ app, session }) { const [rows, setRows] = uSi(null); const [err, setErr] = uSi(''); const [status, setStatus] = uSi('open'); const [kind, setKind] = uSi('all'); const [openId, setOpenId] = uSi(null); const load = uCi(async () => { const { data, error } = await SB.from('tmd_inquiries').select('*').order('created_at', { ascending: false }).limit(500); if (error) { setErr(error.message); return; } setErr(''); setRows(data || []); }, []); uEi(() => { load(); const t = setInterval(load, 60000); return () => clearInterval(t); }, [load]); const list = uMi(() => (rows || []).filter(r => (status === 'all' ? true : status === 'open' ? OPEN.includes(r.status) : r.status === status) && (kind === 'all' ? true : r.kind === kind) ), [rows, status, kind]); const counts = uMi(() => { const c = { new: 0, scoped: 0, queued: 0, delivered: 0, declined: 0 }; (rows || []).forEach(r => { c[r.status] = (c[r.status] || 0) + 1; }); return c; }, [rows]); const current = openId ? (rows || []).find(r => r.id === openId) : null; const save = async (id, patch) => { const { data, error } = await SB.from('tmd_inquiries').update(patch).eq('id', id).select().single(); if (error) { app.toast('Not saved: ' + error.message); return false; } setRows(rs => rs.map(r => r.id === id ? data : r)); return true; }; return (
Refresh} />
setStatus('new')} /> setStatus('scoped')} /> setStatus('queued')} /> setStatus('delivered')} />
({ v: s.v, l: s.l })))} />
{err ?
Could not load inquiries.

{err}

: null} {rows && !err && list.length === 0 ? : null} {!rows && !err ?

Loading

: null} {list.length ? ( {list.map(r => ( setOpenId(r.id)}> ))}
ReceivedFromTypeNeedsBudgetStatusOwner
{fmtStamp(r.created_at)}{ago(r.created_at)} {r.name}{r.company ? · {r.company} : null} {r.kind === 'website' ? 'Website' : 'Client'} {r.kind === 'website' ? (r.site_type || '—') : (r.services || '—')} {r.budget || '—'} {r.owner || —}
) : null} setOpenId(null)} onSave={save} />
); } function InquirySheet({ inq, staff, onClose, onSave }) { const [notes, setNotes] = uSi(''); const [dirty, setDirty] = uSi(false); uEi(() => { setNotes(inq ? (inq.notes || '') : ''); setDirty(false); }, [inq && inq.id]); if (!inq) return null; const rowsForKind = inq.kind === 'website' ? [['Site type', inq.site_type], ['Existing website', inq.existing_url], ['Pages', inq.pages]] : [['Services', inq.services]]; const facts = [['Company or brand', inq.company]].concat(rowsForKind, [['Budget', inq.budget], ['Timeline', inq.timeline], ['Submitted from', inq.page]]); const owners = (staff || []).filter(s => s.active).map(s => s.name); const saveNotes = async () => { if (await onSave(inq.id, { notes })) setDirty(false); }; return ( CloseSave notes}>

{inq.name}

Received {fmtStamp(inq.created_at)} · {ago(inq.created_at)}

{inq.email} {inq.phone ? WhatsApp {inq.phone} : null}
Brief

{inq.brief}

Details
{facts.filter(([, v]) => v).map(([k, v]) =>
{k}
{/^https?:/.test(v) ? {v} : v}
)}
Desk notes