/* How loaded each editor is, week by week. The rule: two short videos a day or one long one, Monday to Saturday. So a short video costs half a day, a long one a full day, and the cap is six days a week. A task lands in the week of its deadline — work with no deadline cannot be placed and is called out separately, because unplanned work is exactly what blows a week up. */ function WeekCell({ week, capacity, onOpen, directory }) { const used = week.days; const pct = capacity ? Math.min(100, (used / capacity) * 100) : 0; const over = used > capacity; const tight = !over && used >= capacity * 0.85; const tone = over ? 'var(--red)' : tight ? 'var(--amber)' : 'var(--green)'; return (
{used % 1 === 0 ? used : used.toFixed(1)} / {capacity} days {over && +{(used - capacity).toFixed(1)}}
{week.tasks.map((t) => { const client = directory.byId[t.client_id]; return (
onOpen(t.id)} title={`${t.title} · ${t.days} ${t.days === 1 ? 'day' : 'days'} · due ${t.due_date}`}> {client ? client.name : '—'} · {t.quantity > 1 ? `${t.quantity}× ` : ''}{t.video_length === 'long' ? 'long' : 'short'} {t.days}
); })}
); } function WorkloadScreen({ go, directory }) { const [data, setData] = React.useState(null); const [start, setStart] = React.useState(null); const load = React.useCallback(() => { const qs = start ? `?start=${start}&weeks=4` : '?weeks=4'; api.get('/api/workload' + qs).then(setData).catch((e) => toast(e.message, 'error')); }, [start]); React.useEffect(load, [load]); if (!data) return
Loading…
; if (!data.people.length) { return ( go('team')}>Open Team} /> ); } const shift = (n) => { const d = new Date((start || data.start) + 'T00:00:00'); d.setDate(d.getDate() + n * 7); setStart(iso(d)); }; const weekLabel = (k) => { const d = new Date(k + 'T00:00:00'); const end = new Date(d); end.setDate(end.getDate() + 5); // Monday to Saturday const fmt = (x) => x.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }); return `${fmt(d)} – ${fmt(end)}`; }; const thisWeekKey = data.weeks.find((k) => { const d = new Date(k + 'T00:00:00'); const t = new Date(data.today + 'T00:00:00'); return t >= d && t < new Date(d.getTime() + 7 * 86400000); }); return ( <>
Two short videos a day or one long one, Monday to Saturday. A task counts in the week of its deadline.
{data.weeks.map((k) => ( ))} {data.people.map((p) => ( {data.weeks.map((k) => ( ))} ))}
Editor {weekLabel(k)}{k === thisWeekKey ? ' · this week' : ''}
{p.name}
{p.job_title || 'Editor'} · cap {p.capacity}/week
{p.unscheduled.length > 0 && (
t.title).join('\n')}> {p.unscheduled.reduce((a, t) => a + t.days, 0)} days with no deadline
)}
go('task', id)} />

Only video counts against the cap, and published ads drop out. A batch task multiplies: three short videos in one task is a day and a half.

); }