/* Clients: the Drive folder that publishing needs, and the monthly quota with how much of it is actually done. One screen answers "who are we behind on". */ function ClientModal({ client, onClose, onSaved }) { const [form, setForm] = React.useState({ name: client ? client.name : '', color: (client && client.color) || '#2563eb', drive_client_folder_url: (client && client.drive_client_folder_url) || '', drive_main_folder_url: (client && client.drive_main_folder_url) || '', drive_internal_folder_url: '', monthly_videos: client ? client.monthly_videos : 0, monthly_statics: client ? client.monthly_statics : 0, retainer_note: (client && client.retainer_note) || '', is_active: client ? client.is_active : true, }); const [saving, setSaving] = React.useState(false); const set = (k, v) => setForm((f) => ({ ...f, [k]: v })); const save = () => { if (!form.name.trim()) { toast('Name required', 'error'); return; } setSaving(true); const body = { ...form, monthly_videos: Number(form.monthly_videos) || 0, monthly_statics: Number(form.monthly_statics) || 0, retainer_note: form.retainer_note || null, }; const call = client ? api.patch(`/api/clients/${client.id}`, body) : api.post('/api/clients', body); call.then(() => { toast('Saved'); onSaved(); onClose(); }) .catch((e) => toast(e.message, 'error')) .finally(() => setSaving(false)); }; return ( }>
set('name', e.target.value)} autoFocus />
set('color', e.target.value)} style={{ width: 56, height: 34, padding: 2, border: '1px solid var(--border)', borderRadius: 8 }} />
set('monthly_videos', e.target.value)} />
set('monthly_statics', e.target.value)} />
set('retainer_note', e.target.value)} />
set('drive_main_folder_url', e.target.value)} />

The brand's top folder — everything about that client. Opened from the client profile, never published into.

set('drive_client_folder_url', e.target.value)} />

The client-facing folder, the one holding the month folders. Studio creates the month and angle subfolders itself when an ad is confirmed.

); } function QuotaBar({ label, data }) { if (!data.quota && !data.planned) return ; const quota = data.quota || data.planned; const pct = (n) => Math.min(100, (n / quota) * 100); const short = data.missing > 0; return (
{data.done}/{data.quota || '?'} {label} {data.in_progress > 0 && {data.in_progress} in flight} {short && {data.missing} to brief}
); } function ClientsScreen({ directory }) { const [editing, setEditing] = React.useState(null); const [creating, setCreating] = React.useState(false); const [month, setMonth] = React.useState(isoMonth(new Date())); const [quota, setQuota] = React.useState(null); const load = React.useCallback(() => { api.get(`/api/clients/quota?month=${month}`) .then(setQuota) .catch((e) => toast(e.message, 'error')); }, [month]); React.useEffect(load, [load]); const rowFor = (id) => (quota ? quota.clients.find((c) => c.client_id === id) : null); const shiftMonth = (n) => { const d = new Date(month + '-01T00:00:00'); d.setMonth(d.getMonth() + n); setMonth(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`); }; const totals = quota ? quota.clients.reduce((acc, c) => ({ video_done: acc.video_done + c.video.done, video_quota: acc.video_quota + c.video.quota, static_done: acc.static_done + c.static.done, static_quota: acc.static_quota + c.static.quota, missing: acc.missing + c.video.missing + c.static.missing, }), { video_done: 0, video_quota: 0, static_done: 0, static_quota: 0, missing: 0 }) : null; return ( <>
{new Date(month + '-01T00:00:00').toLocaleDateString('en-GB', { month: 'long', year: 'numeric' })} {totals && ( {totals.video_done}/{totals.video_quota} videos · {totals.static_done}/{totals.static_quota} statics {totals.missing > 0 ? ` · ${totals.missing} still to brief` : ' · all briefed'} )}
{directory.clients.map((c) => { const row = rowFor(c.id); return ( setEditing(c)}> ); })}
ClientVideos this monthStatics this month Main folderPublish to
{c.name} {c.retainer_note &&
{c.retainer_note}
}
{row ? : } {row ? : } {c.drive_main_folder_url ? e.stopPropagation()}>Open : } {c.drive_client_folder_url ? e.stopPropagation()}>Assets folder : not set — cannot publish}
{directory.clients.length === 0 && }

Counted by delivery month, not by the day a task was created. Done means confirmed or published.

{(creating || editing) && ( { setCreating(false); setEditing(null); }} onSaved={() => { directory.reload(); load(); }} /> )} ); }