// Admin werkplaats agenda — drag/drop, statussen, resource view function Admin({ onNav }) { const [view, setView] = React.useState("week"); // day | week | month const [resourceMode, setResourceMode] = React.useState("bay"); // bay | mechanic const [selected, setSelected] = React.useState(null); const [appointments, setAppointments] = React.useState([]); const [stats, setStats] = React.useState(null); const [loading, setLoading] = React.useState(true); // Fetch live data on mount React.useEffect(() => { if (!window.api) { setAppointments(seedAppointments()); setLoading(false); return; } // Bereken huidige week (ma t/m za) const today = new Date(); const dayOfWeek = today.getDay() === 0 ? 6 : today.getDay() - 1; // 0=ma .. 6=zo const monday = new Date(today); monday.setDate(today.getDate() - dayOfWeek); const saturday = new Date(monday); saturday.setDate(monday.getDate() + 5); const fromStr = monday.toISOString().slice(0, 10); const toStr = saturday.toISOString().slice(0, 10); Promise.all([ window.api.admin.appointments({ from: fromStr, to: toStr }).catch(() => ({ appointments: [] })), window.api.admin.stats().catch(() => null), ]).then(([appts, s]) => { // Map naar calendar shape (day 0=ma..5=za, hour, minute, serviceShort, plate, customer) const mapped = (appts.appointments || []).map(a => { const dt = new Date(a.start_at); const dow = dt.getDay() === 0 ? 6 : dt.getDay() - 1; const statusMap = { booked: 'booked', arrived: 'arrived', in_progress: 'progress', done: 'done', picked: 'picked' }; return { id: a.id, day: Math.min(dow, 5), hour: dt.getHours(), minute: dt.getMinutes(), duration: a.service?.duration_min || 60, serviceShort: a.service?.name || '', plate: a.vehicle?.license_plate || '', customer: a.customer?.name || '', status: statusMap[a.status] || 'booked', mechanic: a.mechanic?.id || 1, bay: a.bay?.id || 1, }; }); // Als geen echte appointments, fallback to seed voor visuele demo setAppointments(mapped.length ? mapped : seedAppointments()); if (s) setStats(s); setLoading(false); }); }, []); const STATUSES = { booked: { label: "Geboekt", Icon: I.Calendar, color: "var(--text-muted)" }, arrived: { label: "Aangekomen", Icon: I.Car, color: "var(--accent)" }, progress: { label: "In behandeling", Icon: I.Wrench, color: "var(--warning)" }, done: { label: "Klaar", Icon: I.Check, color: "var(--success)" }, picked: { label: "Opgehaald", Icon: I.Flag, color: "var(--text-muted)" }, }; return (
{/* Top bar */}
Werkplaats
Maandag 4 — vrijdag 8 mei 2026
Vijfhuizen · 4 hefbruggen · 4 monteurs
{/* View toggle */}
{["day", "week", "month"].map(v => ( ))}
{[ { id: "bay", label: "Hefbruggen", icon: I.Lift }, { id: "mechanic", label: "Monteurs", icon: I.ShieldCheck }, ].map(opt => ( ))}
{/* KPI strip */}
{/* Main grid */}
{selected && ( a.id === selected)} statuses={STATUSES} onUpdate={(patch) => setAppointments(arr => arr.map(a => a.id === selected ? { ...a, ...patch } : a))} onClose={() => setSelected(null)} /> )}
); } function Kpi({ label, value, sub, accent }) { return (
{label}
{value}
{sub}
); } // Calendar grid: rows = hour labels, columns = resources × days function Calendar({ view, resourceMode, appointments, setAppointments, statuses, onSelect, selected }) { const days = view === "day" ? ["Ma 4 mei"] : view === "week" ? ["Ma 4", "Di 5", "Wo 6", "Do 7", "Vr 8", "Za 9"] : Array.from({ length: 30 }, (_, i) => `${i+1}`); const resources = resourceMode === "bay" ? [{ id: "b1", name: "Brug 1", color: "#D4AF37" }, { id: "b2", name: "Brug 2", color: "#7DD3FC" }, { id: "b3", name: "Brug 3", color: "#A78BFA" }, { id: "b4", name: "Brug 4", color: "#FB923C" }] : [{ id: "m1", name: "Bart", color: "#D4AF37" }, { id: "m2", name: "Sven", color: "#7DD3FC" }, { id: "m3", name: "Esra", color: "#A78BFA" }, { id: "m4", name: "Jeroen", color: "#FB923C" }]; if (view === "month") return ; const hours = Array.from({ length: 11 }, (_, i) => 8 + i); // 8 - 18 // Drag state const [dragId, setDragId] = React.useState(null); const onDrop = (day, resourceId, hour) => { if (!dragId) return; setAppointments(arr => arr.map(a => a.id === dragId ? { ...a, day, resourceId, hour, _moved: true } : a)); setDragId(null); }; return (
{/* Header row */}
{days.map((d, di) => (
{d}
{resources.map((r, ri) => (
{r.name}
))}
))}
{/* Hour rows */}
{hours.map((h, hi) => (
{String(h).padStart(2, "0")}:00
{days.flatMap((d, di) => resources.map((r, ri) => { const isLunch = h === 12; return (
e.preventDefault()} onDrop={() => onDrop(di, r.id, h)} style={{ borderLeft: ri === 0 ? "1px solid var(--border)" : "1px dashed color-mix(in oklab, var(--border) 60%, transparent)", borderRight: ri === resources.length - 1 && di < days.length - 1 ? "1px solid var(--border)" : "none", background: isLunch ? "repeating-linear-gradient(45deg, transparent 0 6px, color-mix(in oklab, var(--text-muted) 8%, transparent) 6px 12px)" : "transparent", position: "relative", }} /> ); }) )}
))} {/* Appointments overlay */}
); } function AppointmentsOverlay({ appointments, days, resources, hours, statuses, onSelect, selected, dragId, setDragId, view }) { const HOUR = 56; const HEADER = 0; const labelCol = 64; return (
{appointments.map(a => { if (a.day >= days.length) return null; const resIdx = resources.findIndex(r => r.id === a.resourceId); if (resIdx < 0) return null; const colW = `calc(((100% ) / ${days.length * resources.length}) * 1)`; const left = `calc(((100%) / ${days.length * resources.length}) * ${a.day * resources.length + resIdx})`; const top = (a.hour - 8 + (a.minute || 0) / 60) * HOUR; const height = (a.duration / 60) * HOUR; const isSel = selected === a.id; const status = statuses[a.status]; const resColor = resources[resIdx].color; return (
setDragId(a.id)} onDragEnd={() => setDragId(null)} onClick={() => onSelect(a.id)} style={{ position: "absolute", left, width: colW, top, height, padding: "1px 3px", pointerEvents: "auto", cursor: "grab", opacity: dragId === a.id ? 0.4 : 1, }} >
{String(a.hour).padStart(2, "0")}:{String(a.minute || 0).padStart(2, "0")} {status.Icon && }
{a.serviceShort}
{a.plate} · {a.customer}
); })}
); } function MonthView({ appointments, onSelect, statuses }) { const cells = Array.from({ length: 35 }, (_, i) => i + 1 - 4); // some negative for prev month return (
{["Ma","Di","Wo","Do","Vr","Za","Zo"].map(d => (
{d}
))} {cells.map((d, i) => { const valid = d > 0 && d <= 31; const apps = appointments.filter(a => a.day === ((d - 1) % 6) && d >= 1 && d <= 9).slice(0, 3); return (
{valid ? d : ""}
{apps.map(a => (
onSelect(a.id)} style={{ fontSize: 10, padding: "3px 6px", borderRadius: 4, cursor: "pointer", background: "color-mix(in oklab, var(--accent) 14%, var(--surface))", borderLeft: "2px solid var(--accent)", overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis", }}> {a.serviceShort}
))} {valid && apps.length === 0 && d > 0 && d < 31 && (
)}
); })}
); } function SidePanel({ appointment, statuses, onUpdate, onClose }) { if (!appointment) return null; const a = appointment; const status = statuses[a.status]; const order = ["booked", "arrived", "progress", "done", "picked"]; return (
Afspraak #{a.id.toString().padStart(4, "0")}

{a.service}

{/* Status flow */}
Status
{order.map((s, i) => { const cur = order.indexOf(a.status); const passed = i <= cur; return (
{status.Icon && } {status.label}
Klant krijgt notificatie bij wijziging
{a.vehicle}
{a.km} km · APK: {a.apk}
{a.customer}
{a.email}
{a.phone}

"{a.notes}"

{a.tasks.map((t, i) => ( ))}
); } function Section({ title, children, last }) { return (
{title}
{children}
); } function seedAppointments() { return [ { id: 1, day: 0, hour: 8, minute: 0, duration: 45, resourceId: "b1", status: "done", service: "APK keuring", serviceShort: "APK", plate: "X-123-YZ", vehicle: "VW Polo 1.0 TSI (2019)", km: "142.500", apk: "14-08-2026", customer: "Sanne de Boer", email: "sanne@example.nl", phone: "06-1234 5678", notes: "Lampje motormanagement brandt soms.", tasks: [{ label: "APK keuring uitvoeren", time: 30, done: true }, { label: "Foutcode uitlezen", time: 10, done: true }, { label: "Rapport opstellen", time: 5, done: true }] }, { id: 2, day: 0, hour: 9, minute: 0, duration: 60, resourceId: "b2", status: "progress", service: "Kleine onderhoudsbeurt", serviceShort: "Kleine beurt", plate: "GH-77-PR", vehicle: "BMW 320i (2021)", km: "67.890", apk: "02-11-2026", customer: "Marco Verhoeven", email: "m.verhoeven@example.nl", phone: "06-9876 5432", notes: "Alleen olie + filter, geen extra werk graag.", tasks: [{ label: "Olie verversen", time: 25, done: true }, { label: "Filter vervangen", time: 15, done: false }, { label: "30-punts inspectie", time: 20, done: false }] }, { id: 3, day: 0, hour: 10, minute: 30, duration: 30, resourceId: "b3", status: "arrived", service: "Airco service", serviceShort: "Airco", plate: "RT-44-VV", vehicle: "Renault Mégane (2018)", km: "98.200", apk: "21-03-2027", customer: "Linde Kraan", email: "linde.k@example.nl", phone: "06-1111 2222", notes: "Airco koelt slecht.", tasks: [{ label: "Druktest uitvoeren", time: 10, done: false }, { label: "Vacuüm trekken", time: 10, done: false }, { label: "Bijvullen R134a", time: 10, done: false }] }, { id: 4, day: 0, hour: 13, minute: 0, duration: 180, resourceId: "b1", status: "booked", service: "Grote beurt", serviceShort: "Grote beurt", plate: "PV-12-AA", vehicle: "Audi A4 Avant (2017)", km: "189.500", apk: "10-09-2026", customer: "Joris Hendriks", email: "j.hendriks@example.nl", phone: "06-3333 4444", notes: "Distributieriem kijken — staat op 180k.", tasks: [{ label: "Olie + filter", time: 30, done: false }, { label: "Distributiecheck", time: 60, done: false }, { label: "30-punts inspectie", time: 45, done: false }, { label: "Bougies vervangen", time: 45, done: false }] }, { id: 5, day: 0, hour: 14, minute: 0, duration: 60, resourceId: "b4", status: "booked", service: "Diagnose elektronisch", serviceShort: "Diagnose", plate: "JK-55-LM", vehicle: "Mercedes C220d (2020)", km: "78.100", apk: "06-06-2026", customer: "Eva Smit", email: "eva@example.nl", phone: "06-5555 6666", notes: "Storing in adaptive cruise — XENTRY graag.", tasks: [{ label: "XENTRY uitlezen", time: 20, done: false }, { label: "Sensoren checken", time: 25, done: false }, { label: "Rapport voor klant", time: 15, done: false }] }, { id: 6, day: 1, hour: 8, minute: 30, duration: 45, resourceId: "b1", status: "booked", service: "APK keuring", serviceShort: "APK", plate: "ZZ-99-XX", vehicle: "Toyota Yaris (2016)", km: "112.300", apk: "06-05-2026", customer: "Pieter Bakker", email: "pieter@example.nl", phone: "06-7777 8888", notes: "Verloopt over 2 dagen.", tasks: [{ label: "APK keuring", time: 45, done: false }] }, { id: 7, day: 1, hour: 10, minute: 0, duration: 240, resourceId: "b3", status: "booked", service: "Distributieriem", serviceShort: "Distributie", plate: "BB-22-CC", vehicle: "Ford Focus 1.0 EB (2015)", km: "203.000", apk: "11-12-2026", customer: "Hans Klaver", email: "h.klaver@example.nl", phone: "06-9999 0000", notes: "Op tijd voor de winter.", tasks: [{ label: "Distributieriem set", time: 180, done: false }, { label: "Waterpomp", time: 60, done: false }] }, { id: 8, day: 2, hour: 9, minute: 0, duration: 90, resourceId: "b2", status: "booked", service: "Remmen voor + ABS diag", serviceShort: "Remmen", plate: "QW-31-RT", vehicle: "Volvo XC60 (2019)", km: "94.100", apk: "08-08-2026", customer: "Anouk de Jong", email: "anouk@example.nl", phone: "06-1212 3434", notes: "Pulsering bij remmen.", tasks: [{ label: "Schijven & blokken voor", time: 60, done: false }, { label: "ABS uitlezen", time: 20, done: false }] }, { id: 9, day: 2, hour: 13, minute: 30, duration: 30, resourceId: "b4", status: "booked", service: "Banden wisselen", serviceShort: "Banden", plate: "MN-88-QQ", vehicle: "Skoda Octavia (2022)", km: "31.500", apk: "12-04-2027", customer: "Tom de Wit", email: "tom@example.nl", phone: "06-2323 4545", notes: "Zomerset uit opslag.", tasks: [{ label: "Wielen wisselen", time: 25, done: false }] }, { id: 10, day: 3, hour: 8, minute: 0, duration: 60, resourceId: "b1", status: "booked", service: "Kleine beurt", serviceShort: "Kleine beurt", plate: "FG-67-HH", vehicle: "Peugeot 308 (2020)", km: "55.700", apk: "20-10-2026", customer: "Kim van Dijk", email: "kim@example.nl", phone: "06-3434 5656", notes: "—", tasks: [{ label: "Olie + filter", time: 30, done: false }, { label: "30-punts inspectie", time: 25, done: false }] }, { id: 11, day: 3, hour: 14, minute: 0, duration: 45, resourceId: "b3", status: "booked", service: "APK keuring", serviceShort: "APK", plate: "AA-11-BB", vehicle: "Citroën C3 (2017)", km: "121.000", apk: "07-05-2026", customer: "Ruud Mulder", email: "r.mulder@example.nl", phone: "06-4545 6767", notes: "—", tasks: [{ label: "APK keuring", time: 45, done: false }] }, { id: 12, day: 4, hour: 9, minute: 30, duration: 60, resourceId: "b2", status: "booked", service: "Diagnose mechanisch", serviceShort: "Diag mech", plate: "HJ-12-KL", vehicle: "Opel Astra (2018)", km: "108.900", apk: "29-09-2026", customer: "Sandra Vermeer", email: "s.vermeer@example.nl", phone: "06-5656 7878", notes: "Geluid bij optrekken.", tasks: [{ label: "Proefrit", time: 15, done: false }, { label: "Onderzoek", time: 35, done: false }] }, { id: 13, day: 4, hour: 13, minute: 0, duration: 90, resourceId: "b4", status: "booked", service: "Remmen achter", serviceShort: "Remmen", plate: "PQ-99-ST", vehicle: "Hyundai i30 (2019)", km: "84.200", apk: "11-01-2027", customer: "Rik de Graaf", email: "rik@example.nl", phone: "06-6767 8989", notes: "—", tasks: [{ label: "Schijven & blokken achter", time: 75, done: false }] }, { id: 14, day: 5, hour: 9, minute: 0, duration: 45, resourceId: "b1", status: "booked", service: "APK keuring", serviceShort: "APK", plate: "UV-33-WX", vehicle: "Mazda CX-5 (2018)", km: "136.700", apk: "14-05-2026", customer: "Nadia Yilmaz", email: "n.yilmaz@example.nl", phone: "06-7878 9090", notes: "—", tasks: [{ label: "APK keuring", time: 45, done: false }] }, ]; } window.Admin = Admin;