// Project detail slide-over panel
const { useState: useStateDetail } = React;
const ProjectDetail = ({ p, onClose }) => {
const [tab, setTab] = useStateDetail('plan');
if (!p) return null;
const klant = window.klantById(p.klant);
const bucket = window.bucketById(p.huidigeBucket);
const teamC = window.TEAM_COLORS[bucket.team];
const steps = window.BUCKETS
.filter(b => p.plan[b.id])
.map(b => ({ ...b, ...p.plan[b.id] }));
const done = steps.filter(s => s.done).length;
const total = steps.length || 1;
return (
<>
{/* Header */}
{p.id}
{klant.naam}
{p.naam}
{bucket.naam}
Opgevolgd door
DEADLINE
{window.fmtDateLong(p.einde)}
{/* Tabs */}
{[
{ id: 'plan', label: 'Planning per stap', count: steps.length },
{ id: 'sauzen', label: 'Sauzen', count: p.sauzen.length },
{ id: 'chat', label: 'Opmerkingen', count: p.opmerkingen },
{ id: 'files', label: 'Bijlagen', count: p.bijlagen },
].map(t => (
setTab(t.id)} style={{
padding: '10px 12px', border: 'none', background: 'transparent',
fontSize: 12.5, fontWeight: 550, cursor: 'pointer',
color: tab === t.id ? 'var(--ink-1)' : 'var(--ink-3)',
borderBottom: tab === t.id ? '2px solid var(--ink-1)' : '2px solid transparent',
marginBottom: -1,
}}>
{t.label} {t.count}
))}
{/* Content */}
{tab === 'plan' &&
}
{tab === 'sauzen' &&
}
{tab === 'chat' &&
}
{tab === 'files' &&
}
>
);
};
const PlanPerStap = ({ p, steps }) => {
return (
Elke stap heeft een eigen deadline. Zo blijft het project niet liggen tot de laatste week.
{steps.map((s, i) => {
const tc = window.TEAM_COLORS[s.team];
const isCurrent = s.id === p.huidigeBucket;
const overdue = !s.done && window.daysUntil(s.deadline) < 0;
const owner = s.owner ? window.medewerkerById(s.owner) : null;
return (
{s.done && }
{s.naam}
{!s.done && }
{s.done && ✓ klaar }
{owner && (
{owner.naam}
uiterlijk {window.fmtDateLong(s.deadline)}
)}
);
})}
Stap toevoegen of aanpassen
{p.notities && (
)}
);
};
const SauzenTab = ({ p }) => {
return (
Dit project bestaat uit {p.sauzen.length} saus{p.sauzen.length > 1 ? 'en' : ''}. Elke saus kan zelfstandig door de buckets bewegen.
{p.sauzen.map((s, i) => {
const b = window.bucketById(s.bucketId);
const prog = window.sausProgress(p, s.bucketId);
return (
{s.naam}
{b.naam}
{Math.round(prog * 100)}%
);
})}
);
};
const ChatTab = ({ p }) => {
const msgs = [
{ who: 'sanne', time: 'gisteren 16:42', text: 'Monster nr 3 is vandaag verstuurd naar klant. Wacht op feedback.' },
{ who: 'marco', time: 'gisteren 17:10', text: 'Heb de receptuur iets bijgesteld — 0.4% meer zuurgraad. Nieuwe charge morgenvroeg.' },
{ who: 'wilco', time: 'vandaag 09:02', text: 'Calculatie staat klaar voor controle. Heb wel een vraag over de verpakkingskosten, zie bijlage.' },
{ who: 'patrick', time: 'vandaag 10:33', text: 'Wilco, zijn die verpakkingskosten excl. labels? Anders klopt de prijs per eenheid niet.' },
];
return (
{msgs.map((m, i) => {
const pers = window.medewerkerById(m.who);
return (
{pers.naam}
{m.time}
{m.text}
);
})}
Plaats
);
};
const FilesTab = ({ p }) => {
const files = [
{ naam: 'Specificatie v3.pdf', grootte: '412 kB', type: 'pdf', wie: 'linda', wanneer: 'gisteren' },
{ naam: 'Calculatie lente 2026.xlsx', grootte: '89 kB', type: 'xls', wie: 'patrick', wanneer: 'vandaag' },
{ naam: 'Klant briefing.eml', grootte: '12 kB', type: 'mail', wie: 'sanne', wanneer: '12 apr' },
{ naam: 'Monster foto.jpg', grootte: '2.1 MB', type: 'img', wie: 'marco', wanneer: '14 apr' },
];
return (
{files.slice(0, p.bijlagen).map((f, i) => {
const pers = window.medewerkerById(f.wie);
return (
{f.type.toUpperCase()}
{f.naam}
{f.grootte} · {pers?.naam.split(' ')[0]} · {f.wanneer}
);
})}
+ Bestand uploaden
);
};
window.ProjectDetail = ProjectDetail;