// Shared UI primitives const Pill = ({ children, bg, fg, border, style }) => ( {children} ); const Dot = ({ color, size = 8 }) => ( ); const Avatar = ({ m, size = 24, ring }) => { if (!m) return null; return (
{m.initials}
); }; const AvatarStack = ({ ids, max = 4, size = 22 }) => { const list = ids.map(window.medewerkerById).filter(Boolean); const shown = list.slice(0, max); const extra = list.length - shown.length; return (
{shown.map((m, i) => (
))} {extra > 0 && (
+{extra}
)}
); }; const PrioPill = ({ prio }) => { const p = window.PRIORITY[prio]; return {p.label}; }; const TeamChip = ({ team, compact }) => { const c = window.TEAM_COLORS[team] || window.TEAM_COLORS['—']; return ( {team} ); }; const DeadlineChip = ({ deadline, done }) => { if (done) return ✓ {fmtDate(deadline)}; const d = window.daysUntil(deadline); let bg = 'var(--surface-2)', fg = 'var(--ink-2)', prefix = ''; if (d < 0) { bg = 'oklch(0.94 0.08 25)'; fg = 'oklch(0.38 0.18 25)'; prefix = '⚠ '; } else if (d === 0) { bg = 'oklch(0.94 0.07 55)'; fg = 'oklch(0.42 0.14 55)'; prefix = '● '; } else if (d <= 2) { bg = 'oklch(0.95 0.05 60)'; fg = 'oklch(0.45 0.12 55)'; } return {prefix}{fmtRel(deadline)}; }; function fmtDate(iso) { const d = new Date(iso); return d.toLocaleDateString('nl-NL', { day: '2-digit', month: 'short' }); } function fmtRel(iso) { const d = window.daysUntil(iso); if (d === 0) return 'vandaag'; if (d === 1) return 'morgen'; if (d === -1) return 'gisteren'; if (d < 0) return `${-d} dgn te laat`; if (d < 7) return `over ${d} dgn`; return fmtDate(iso); } function fmtDateLong(iso) { const d = new Date(iso); return d.toLocaleDateString('nl-NL', { day: '2-digit', month: 'long' }); } const Icon = ({ name, size = 16 }) => { const paths = { kanban: <>, today: <>, klant: <>, lijst: <>, team: <>, search: <>, plus: <>, filter: , clock: <>, chat: , attach: , check: , alert: <>, chevron: , down: , edit: <>, x: <>, drop: , sparkle: , settings: <>, bell: <>, }; return ( {paths[name] || null} ); }; // ─── Logo ─────────────────────────────────────────────── const Logo = () => (
Saucecompany
NPD · ONTWIKKELING
); // ─── Sidebar ──────────────────────────────────────────── const Sidebar = ({ view, setView, onNewProject }) => { const items = [ { id: 'kanban', label: 'Kanban', icon: 'kanban', count: 15 }, { id: 'mijndag', label: 'Mijn dag', icon: 'today', count: 5, hot: true }, { id: 'klanten', label: 'Per klant', icon: 'klant' }, { id: 'lijst', label: 'Alle projecten', icon: 'lijst' }, { id: 'team', label: 'Team workload', icon: 'team' }, ]; return ( ); }; // ─── Topbar ───────────────────────────────────────────── const Topbar = ({ view, onToggleFilter, search, setSearch }) => { const titles = { kanban: 'NPD Ontwikkeling', mijndag: 'Mijn dag', klanten: 'Per klant', lijst: 'Alle projecten', team: 'Team workload', }; const subtitles = { kanban: '15 lopende projecten · 22 buckets', mijndag: `${window.getMyTasks(window.CURRENT_USER_ID).length} taken openstaand · zaterdag 18 april`, klanten: 'Aanvragen gegroepeerd per klant', lijst: 'Zoek, filter en exporteer', team: 'Bezetting en doorlooptijden', }; return (

{titles[view]}

{subtitles[view]}
setSearch(e.target.value)} placeholder="Zoek project, klant, saus…" style={{ border: 'none', outline: 'none', background: 'transparent', fontSize: 13, flex: 1, color: 'var(--ink-1)', fontFamily: 'inherit', }} /> ⌘K
); }; Object.assign(window, { Pill, Dot, Avatar, AvatarStack, PrioPill, TeamChip, DeadlineChip, Icon, Logo, Sidebar, Topbar, fmtDate, fmtRel, fmtDateLong, });