// Kanban view — the main overview const { useState, useMemo, useRef, useEffect } = React; const FASE_GROEPEN = [ { id: 'aanvraag', label: 'Aanvraag' }, { id: 'ontwikkeling',label: 'Ontwikkeling' }, { id: 'specificatie',label: 'Specificatie' }, { id: 'calculatie', label: 'Calculatie' }, { id: 'commercie', label: 'Commercie' }, { id: 'productie', label: 'Productie' }, { id: 'klaar', label: 'Klaar' }, { id: 'wachten', label: 'Wachtkamer' }, ]; const KanbanCard = ({ p, onClick, density }) => { const klant = window.klantById(p.klant); const bucket = window.bucketById(p.huidigeBucket); const step = p.plan[p.huidigeBucket]; const overdue = step && !step.done && window.daysUntil(step.deadline) < 0; const isCompact = density === 'compact'; const sauzenOpKaart = p.sauzen.slice(0, 4); return (
{ e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.06)'; }} onMouseLeave={e => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.boxShadow = overdue ? '0 0 0 1px oklch(0.75 0.15 25 / 0.5), 0 1px 2px rgba(0,0,0,0.03)' : '0 1px 2px rgba(0,0,0,0.03)'; }} >
{p.id} {p.prio === 'dringend' && }
{p.naam}
{klant.naam}
{/* Sauzen mini-voortgang */} {!isCompact && p.sauzen.length > 0 && (
{sauzenOpKaart.map((s, i) => { const prog = window.sausProgress(p, s.bucketId); const isHold = prog === -1; const inThisBucket = s.bucketId === p.huidigeBucket; return (
{s.naam}
); })} {p.sauzen.length > 4 && (
+ {p.sauzen.length - 4} meer
)}
)} {/* Footer */}
{p.opmerkingen > 0 && ( {p.opmerkingen} )} {p.bijlagen > 0 && ( {p.bijlagen} )} {step && !step.done && ( )}
); }; const KanbanColumn = ({ bucket, projecten, onPick, density }) => { const color = window.TEAM_COLORS[bucket.team] || window.TEAM_COLORS['—']; const count = projecten.length; return (
{bucket.naam} {count}
{projecten.map(p => ( onPick(p)} density={density} /> ))} {count === 0 && (
)}
); }; const Kanban = ({ onPick, density, search }) => { const [activeFase, setActiveFase] = useState('all'); const projects = window.PROJECTEN; const buckets = window.BUCKETS; const filteredProjecten = useMemo(() => { const s = search.toLowerCase(); if (!s) return projects; return projects.filter(p => p.naam.toLowerCase().includes(s) || p.id.toLowerCase().includes(s) || window.klantById(p.klant).naam.toLowerCase().includes(s) || p.sauzen.some(sa => sa.naam.toLowerCase().includes(s)) ); }, [search, projects]); const byBucket = useMemo(() => { const m = {}; buckets.forEach(b => m[b.id] = []); filteredProjecten.forEach(p => { (m[p.huidigeBucket] = m[p.huidigeBucket] || []).push(p); }); return m; }, [filteredProjecten, buckets]); const faseCounts = useMemo(() => { const m = { all: filteredProjecten.length }; FASE_GROEPEN.forEach(f => { m[f.id] = buckets.filter(b => b.fase === f.id).reduce((sum, b) => sum + (byBucket[b.id]?.length || 0), 0); }); return m; }, [byBucket, filteredProjecten, buckets]); const visibleBuckets = activeFase === 'all' ? buckets : buckets.filter(b => b.fase === activeFase); return (
{/* Fase tabs */}
{FASE_GROEPEN.map(f => ( ))}
{/* Columns */}
{visibleBuckets.map(b => ( ))}
); }; window.Kanban = Kanban;