// Main app — wires data, state, modals, popover, drawer, tweaks together. const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "zoom": 1, "density": "regular", "showPercent": true }/*EDITMODE-END*/; function App() { const D = window.CrewliData; const H = window.CrewliHelpers; const M = window.CrewliModals; const P = window.CrewliPopover; const TT = window.CrewliTimetable; // ─── Mutable state (local, no backend) ──────────────────────────── const [stages, setStages] = React.useState(D.STAGES); const [stageDays, setStageDays] = React.useState(D.STAGE_DAYS); const [artists, setArtists] = React.useState(D.ARTISTS); const [performances, setPerformances] = React.useState(D.PERFORMANCES); const [parked, setParked] = React.useState(D.PARKED); const [pending, setPending] = React.useState(D.PENDING); const [activeDayId, setActiveDayId] = React.useState(D.EVENT.days[0].id); // ─── UI state ───────────────────────────────────────────────────── const [popover, setPopover] = React.useState(null); // { perf, rect } or { kind: 'queue', queueKey, item, rect } const [drawer, setDrawer] = React.useState(null); // { perf } const [perfModal, setPerfModal] = React.useState(null); // { mode, perf } const [stageModal, setStageModal] = React.useState(null); // stage const [matrixOpen, setMatrixOpen] = React.useState(false); const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const activeDay = D.EVENT.days.find(d => d.id === activeDayId); // Stages active on this day const dayStages = React.useMemo(() => { const ids = new Set(stageDays.filter(sd => sd.day_id === activeDayId).map(sd => sd.stage_id)); return stages.filter(s => ids.has(s.id)); }, [stages, stageDays, activeDayId]); const dayPerformances = performances.filter(p => p.day_id === activeDayId); // Reducer-ish dispatch function dispatch(action) { if (action.type === "update_perf") { setPerformances(prev => prev.map(p => p.id === action.perf.id ? action.perf : p)); } else if (action.type === "delete_perf") { setPerformances(prev => prev.filter(p => p.id !== action.id)); } else if (action.type === "add_perf") { setPerformances(prev => [...prev, action.perf]); } else if (action.type === "update_stage") { setStages(prev => prev.map(s => s.id === action.stage.id ? action.stage : s)); } else if (action.type === "delete_stage") { setStages(prev => prev.filter(s => s.id !== action.id)); setStageDays(prev => prev.filter(sd => sd.stage_id !== action.id)); // Move scheduled performances on this stage to the wachtrij (parked) i.p.v. ze te verwijderen. setPerformances(prev => prev.filter(p => p.stage_id !== action.id)); setParked(prev => [ ...prev, ...performances .filter(p => p.stage_id === action.id) .map(p => ({ ...p, stage_id: null, dur: (p.end - p.start) || 60 })), ]); } else if (action.type === "add_stage") { setStages(prev => [...prev, action.stage]); setStageDays(prev => [...prev, ...action.days.map(did => ({ stage_id: action.stage.id, day_id: did }))]); } else if (action.type === "reorder_stages") { // dayStageIds = the new ordering for the day's stages. // Strategy: keep non-day stages in their relative positions; replace the // dayStages slice with the new order. setStages(prev => { const dayIdSet = new Set(action.dayStageIds); const dayLookup = Object.fromEntries(prev.filter(s => dayIdSet.has(s.id)).map(s => [s.id, s])); const orderedDay = action.dayStageIds.map(id => dayLookup[id]).filter(Boolean); const result = []; let cursor = 0; for (const s of prev) { if (dayIdSet.has(s.id)) { // Replace with the next entry from orderedDay if (cursor < orderedDay.length) result.push(orderedDay[cursor++]); } else { result.push(s); } } return result; }); } else if (action.type === "set_stage_days_for_stage") { setStageDays(prev => [ ...prev.filter(sd => sd.stage_id !== action.stageId), ...action.dayIds.map(did => ({ stage_id: action.stageId, day_id: did })), ]); } else if (action.type === "set_stage_days_all") { setStageDays(action.list); } else if (action.type === "park_perf") { const p = performances.find(x => x.id === action.id); if (!p) return; setPerformances(prev => prev.filter(x => x.id !== action.id)); setParked(prev => [...prev, { ...p, stage_id: null }]); } else if (action.type === "unpark_perf") { const p = parked.find(x => x.id === action.perf.id); if (!p) return; setParked(prev => prev.filter(x => x.id !== action.perf.id)); setPerformances(prev => [...prev, action.perf]); } else if (action.type === "schedule_pending") { setPending(prev => prev.filter(x => x.id !== action.pendingId)); setPerformances(prev => [...prev, action.perf]); } else if (action.type === "delete_pending") { setPending(prev => prev.filter(x => x.id !== action.id)); } else if (action.type === "delete_parked") { setParked(prev => prev.filter(x => x.id !== action.id)); } else if (action.type === "update_pending_status") { // Pending items don't actually carry a status — promoting their status // means moving them into parked (no stage_id) with that status. const pa = pending.find(x => x.id === action.id); if (!pa) return; const newParked = { id: "p_" + Math.random().toString(36).slice(2, 8), artist_id: pa.artist_id, day_id: pa.day_id, stage_id: null, start: null, end: null, status: action.status, }; setPending(prev => prev.filter(x => x.id !== action.id)); setParked(prev => [...prev, newParked]); } else if (action.type === "update_parked_status") { setParked(prev => prev.map(p => p.id === action.id ? { ...p, status: action.status } : p)); } else if (action.type === "add_parked") { setParked(prev => [...prev, action.parked]); } else if (action.type === "add_artist") { setArtists(prev => [...prev, action.artist]); } } // Move parked → performances when used via drag React.useEffect(() => { const movedBack = performances.filter(p => p.stage_id !== null && parked.some(pk => pk.id === p.id)); if (movedBack.length > 0) { setParked(prev => prev.filter(pk => !movedBack.some(m => m.id === pk.id))); } }, [performances]); // Layout numerics const pxPerMin = 4 * t.zoom; const rowHeight = t.density === "compact" ? 52 : t.density === "comfy" ? 84 : 64; // ─── Conflict counts (for header chip) ───────────────────────────── const conflictIds = H.findConflicts(dayPerformances); // Day counts const stageCountByDay = {}; D.EVENT.days.forEach(d => { stageCountByDay[d.id] = stageDays.filter(sd => sd.day_id === d.id).length; }); // ─── Handlers ───────────────────────────────────────────────────── function handleSelectPerf(perf, rect) { setPopover({ kind: "perf", perfId: perf.id, rect }); setDrawer(null); } function handleSelectQueueItem(item, rect) { // item: { kind, id, artist, status, dur, src } setPopover({ kind: "queue", queueKey: item.kind + ":" + item.id, item, rect }); setDrawer(null); } function handleClickEmpty(stage, minute, end, lane) { setPerfModal({ mode: "add", perf: { stage_id: stage.id, start: minute, end: typeof end === "number" ? end : minute + 60, status: "concept", ...(Number.isInteger(lane) ? { lane } : {}), }, }); setPopover(null); } function handleEditStage(stage) { setStageModal(stage); setPopover(null); } function handleAddStage() { // Draft stage — niet meteen toevoegen aan state. StageEditor commit'et bij Opslaan. const id = "s_" + Math.random().toString(36).slice(2, 8); const draft = { id, name: "Nieuwe stage", color: "#3cc2a8", capacity: 2000, __draft: true }; setStageModal(draft); } const popoverPerf = popover && popover.kind === "perf" ? performances.find(p => p.id === popover.perfId) : null; const drawerPerf = drawer ? performances.find(p => p.id === drawer.perfId) : null; return (