/* TMD Portal — work management screens. */ const { useState: uS, useMemo: uM } = React; /* ================= Dashboard ================= */ function Dashboard({ app }) { const { role, go, dels, tasks } = app; if (role === 'Intern' || role === 'Executive') return ; const live = dels.filter(isLive); const overdue = live.filter(isOverdue); const waiting = live.filter(d => stageMeta(d).sem === 'waiting'); const blocked = live.filter(d => stageMeta(d).sem === 'blocked'); const published = dels.filter(d => d.published); const owner = role === 'Owner'; const retainer = T.clients.filter(c => c.status === 'Active').reduce((s, c) => s + c.retainer, 0); const inv = T.invoices; const invoiced = inv.filter(i => i.status !== 'draft').reduce((s, i) => s + i.lines.reduce((a, l) => a + l.amount, 0), 0); const collected = inv.reduce((s, i) => s + i.payments.reduce((a, p) => a + p.amount, 0), 0); const outstanding = invoiced - collected; const overdueAr = inv.filter(i => i.status === 'overdue').reduce((s, i) => s + i.lines.reduce((a, l) => a + l.amount, 0) - i.payments.reduce((a, p) => a + p.amount, 0), 0); const spend = T.expenses.reduce((s, e) => s + (e.paidBy === 'TMD' ? e.amount : 0), 0); return (
{owner ? ( <> Money this month
c.status === 'Active').length + ' active clients'} onClick={() => go('clients')} /> i.status !== 'draft').length + ' issued · ' + inv.filter(i => i.status === 'draft').length + ' draft'} onClick={() => go('invoices')} /> i.status === 'paid').length + ' paid in full'} onClick={() => go('invoices')} /> 0 ? 'bad' : null} onClick={() => go('invoices')} /> go('expenses')} /> go('board')} />
) : null} Work that is stuck
Waiting on someone{waiting.length + blocked.length}
{blocked.concat(waiting).map(d => ( ))}
Overdue{overdue.length}
{overdue.length ? overdue.sort((a, b) => a.due.localeCompare(b.due)).map(d => ( )) : }
This week, by person Published this month
go('board')} /> go('board')} /> go('board')} />
); } /* ================= Work board + list ================= */ function Board({ app }) { const { go, dels } = app; const [view, setView] = uS(app.mobile ? 'list' : 'board'); const [type, setType] = uS('reel'); const [cl, setCl] = uS('all'); const [asg, setAsg] = uS('all'); const [win, setWin] = uS('all'); const match = d => (cl === 'all' || d.client === cl) && (asg === 'all' || d.assignee === asg) && (win === 'all' || (win === 'over' ? isOverdue(d) : win === 'week' ? dayDiff(d.due) <= 7 && dayDiff(d.due) >= 0 : true)); const list = dels.filter(match); const boardList = list.filter(d => d.type === type); const stages = typeOf(type).stages; const filters = (
({ v: c.id, l: c.name })))} /> p.id !== 'un').map(p => ({ v: p.id, l: p.name })))} />
); return (
New deliverable} /> {view === 'board' ? ( <>
{T.types.map(t => )}
{filters}
{stages.map(s => { const col = boardList.filter(d => stageMeta(d).name === s.name); return (
{s.name}{col.length}
{s.approval ?
{s.approval === 'client' ? 'Client approval — recorded by staff' : 'Manager approval'}
: null}
{col.map(d => ( ))} {col.length === 0 ?
Nothing here
: null}
); })}
) : ( <> {filters} )}
); } function DelTable({ list, go, mobile }) { if (!list.length) return New deliverable} />; if (mobile) return (
{list.map(d => ( ))}
); return ( {list.map(d => ( go('del:' + d.id)} tabIndex={0} onKeyDown={e => e.key === 'Enter' && go('del:' + d.id)}> ))}
RefDeliverableClientTypeStageOwnerDue
{d.ref} {d.title} {client(d.client).name} {typeOf(d.type).name}
); } /* ================= My tasks ================= */ function MyTasks({ app }) { const { tasks, setDone, go, me, role } = app; const mine = tasks.filter(t => t.who === me); const open = mine.filter(t => t.state !== 'done'); const groups = [ { k: 'Overdue', items: open.filter(t => dayDiff(t.due) < 0), tone: 'bad' }, { k: 'Today', items: open.filter(t => dayDiff(t.due) === 0) }, { k: 'This week', items: open.filter(t => dayDiff(t.due) > 0 && dayDiff(t.due) <= 7) }, { k: 'Later', items: open.filter(t => dayDiff(t.due) > 7) }, { k: 'Done', items: mine.filter(t => t.state === 'done'), done: true } ].filter(g => g.items.length); return (
{open.length === 0 ? : null} {groups.map(g => (
{g.k}{g.items.length}
{g.items.map(t => (
{t.title}
{t.del ? : Standalone}
))}
))}
); } /* ================= Deliverable detail ================= */ function DelDetail({ app, id }) { const d = byId(app.dels, id); const [tab, setTab] = uS('Brief'); const [comment, setComment] = uS(''); const [rec, setRec] = uS(false); if (!d) return ; const t = typeOf(d.type), m = stageMeta(d), c = client(d.client); const idx = t.stages.findIndex(s => s.name === m.name); const copy = (d.copy || []).slice().sort((a, b) => b.v - a.v); const [ver, setVer] = uS(copy.length ? copy[0].v : 0); const cur = copy.find(x => x.v === ver); const ap = d.approvals || {}; const Brief = (

Brief

{d.brief || 'No brief written yet. Ask the requester for one line on what this is for — that is enough to start.'}

{d.blockedNote ?
Blocked{d.blockedNote}
: null}

Assets

{(d.assets || []).length ? ( ) : Add link} />} {(d.assets || []).length ? Add link : null} {d.published ?
Published{d.published.url}
: null}
); const Copy = (

Copy

{copy.length ?
{copy.map(v => )}
: null}
{cur ? ( <>
v{cur.v} · {person(cur.author).name} · {cur.at.replace('2026-08-', '').replace('-', ' ')}{cur.v !== copy[0].v ? Read only — older version : null}
{cur.text}
{cur.v === copy[0].v && app.role !== 'Intern' ? (
app.toast('Copy v' + cur.v + ' approved')}>Approve copy app.toast('Changes requested — Maneesha notified', true)}>Request changes
) : null} ) : Write v1} />}

Comments

{(d.comments || []).map((c2, i) => (
{person(c2.author).name} · {c2.at.slice(5).replace('-', '/')} · on v{c2.on}

{c2.text}

))} {!(d.comments || []).length ?

No comments. Type below to start the thread — mention with @.

: null}