// 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 (
);
})}
{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;