// 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 (
);
};
// ─── 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 (
);
};
Object.assign(window, {
Pill, Dot, Avatar, AvatarStack, PrioPill, TeamChip, DeadlineChip,
Icon, Logo, Sidebar, Topbar, fmtDate, fmtRel, fmtDateLong,
});