// Modals: AddPerformance, EditPerformance, StageEditor, LineupMatrix // Each follows the same Crewli light-mode chrome. const M = (function () { const ESC_KEY = 27; function Backdrop({ children, onClose }) { React.useEffect(() => { const h = (e) => { if (e.keyCode === ESC_KEY) onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, []); return (
{ if (e.target === e.currentTarget) onClose(); }}> {children}
); } function Field({ label, hint, error, children }) { return ( ); } // ─── Add / Edit Performance ───────────────────────────────────────── // "Add" creates a brand-new act (artist + performance) on the fly. // Fields: Naam Act, Genre, Status. Stage/tijd komen mee als de popup // geopend is via een lege cel in de timetable — starttijd is dan // alleen-lezen, eindtijd = start + 1u. function PerformanceModal({ mode, perf, day, stages, artists, onSave, onClose, onDelete }) { const D = window.CrewliData; const H = window.CrewliHelpers; const fromTimetable = mode === "add" && perf?.stage_id != null && perf?.start != null; const start = fromTimetable ? perf.start : null; const end = fromTimetable ? perf.start + 60 : null; // Edit mode preloads the existing artist; add mode is a blank act form. const existingArtist = mode === "edit" ? artists.find(a => a.id === perf?.artist_id) : null; const [name, setName] = React.useState(existingArtist?.name || ""); const [genre, setGenre] = React.useState(existingArtist?.genre || D.GENRES[0]); const [status, setStatus] = React.useState(perf?.status || (fromTimetable ? "concept" : "requested")); // Duur (alleen relevant bij + Performance vanuit header — wachtrij heeft geen tijd) const [duration, setDuration] = React.useState(60); const stage = fromTimetable ? stages.find(s => s.id === perf.stage_id) : null; const startStr = start != null ? H.fmtTime(start, D.TIME.startHour) : null; const endStr = end != null ? H.fmtTime(end, D.TIME.startHour) : null; const [touched, setTouched] = React.useState(false); const trimmed = name.trim(); const nameError = trimmed.length === 0 ? "Naam Act is verplicht" : trimmed.length > 60 ? "Maximaal 60 tekens" : null; const canSave = !nameError; function save() { setTouched(true); if (!canSave) return; if (mode === "edit") { // Edit only updates status here (rename/genre niet binnen scope). onSave({ artistUpdate: null, perf: { ...perf, status } }); return; } // ── Add: create artist + performance ── // Initials: keep only Latin letters/digits to avoid weird glyphs in the avatar. const safeChars = trimmed.replace(/[^A-Za-z0-9 ]/g, "").trim(); const parts = safeChars.split(/\s+/).filter(Boolean); let initials = parts.length >= 2 ? (parts[0][0] + parts[1][0]) : (parts[0] ? parts[0].slice(0, 2) : ""); initials = initials.toUpperCase() || "??"; const newArtist = { id: "a_" + Math.random().toString(36).slice(2, 8), name: name.trim(), initials, genre, draw: 0, advance: { tour: false, hosp: false, travel: false, flight: false, rider: false }, }; const basePerf = { id: "p_" + Math.random().toString(36).slice(2, 8), artist_id: newArtist.id, day_id: day.id, status, }; if (fromTimetable) { const lanePart = Number.isInteger(perf.lane) ? { lane: perf.lane } : {}; onSave({ newArtist, perf: { ...basePerf, stage_id: perf.stage_id, start, end, ...lanePart } }); } else { // Geen timetable-context → wachtrij. Bewaar duur zodat block bij drop de juiste breedte krijgt. onSave({ newArtist, perf: { ...basePerf, stage_id: null, dur: duration } }); } } return (
e.stopPropagation()}>
{mode === "add" ? "Performance toevoegen" : "Performance bewerken"} {day.label} · {D.EVENT.name}
setName(e.target.value)} onBlur={() => setTouched(true)} disabled={mode === "edit"} maxLength={80} aria-invalid={touched && !!nameError} aria-describedby={touched && nameError ? "cw-name-err" : undefined} />
{fromTimetable && stage && (
Stage {stage.name}
vast
Starttijd {startStr}
Eindtijd {endStr}
60 min
)} {!fromTimetable && mode === "add" && ( <>
Geen tijdslot gekozen — landt in de wachtrij.
)}
{mode === "edit" && ( )}
); } // ─── Stage editor ──────────────────────────────────────────────────── function StageEditor({ stage, days, stageDays, onSave, onClose, onDelete, mode, defaultDayIds }) { const isCreate = mode === "create"; const [name, setName] = React.useState(stage.name); const [color, setColor] = React.useState(stage.color); const [capacity, setCapacity] = React.useState(stage.capacity); const [activeDays, setActiveDays] = React.useState( isCreate ? (defaultDayIds || []) : days.filter(d => stageDays.some(sd => sd.stage_id === stage.id && sd.day_id === d.id)).map(d => d.id) ); const SWATCHES = ["#e85d75", "#7a8af0", "#f0a04b", "#5fc9a8", "#c89af0", "#e8d05f", "#3cc2a8", "#5a8fcf", "#cf5a8f", "#8fcf5a"]; function toggleDay(id) { setActiveDays(activeDays.includes(id) ? activeDays.filter(x => x !== id) : [...activeDays, id]); } function save() { onSave({ ...stage, name, color, capacity: +capacity }, activeDays); } return (
e.stopPropagation()}>
{isCreate ? "Stage aanmaken" : "Stage bewerken"} {isCreate ? "Nieuwe stage" : stage.name}
setName(e.target.value)} /> setCapacity(e.target.value)} />
{SWATCHES.map(c => (
{days.map(d => ( ))}
{!isCreate && ( )}
); } // ─── Lineup matrix: stages × days bulk editor ──────────────────────── function LineupMatrix({ stages, days, stageDays, onSave, onClose }) { // Build state: { stageId: Set } const [matrix, setMatrix] = React.useState(() => { const m = {}; stages.forEach(s => { m[s.id] = new Set(stageDays.filter(sd => sd.stage_id === s.id).map(sd => sd.day_id)); }); return m; }); function toggle(sid, did) { setMatrix(prev => { const next = { ...prev }; const s = new Set(next[sid]); s.has(did) ? s.delete(did) : s.add(did); next[sid] = s; return next; }); } function save() { const out = []; for (const sid in matrix) { for (const did of matrix[sid]) out.push({ stage_id: sid, day_id: did }); } onSave(out); } return (
e.stopPropagation()}>
Lineup per dag Welke stages draaien op welke dag
{days.map(d => )} {stages.map(s => ( {days.map(d => ( ))} ))}
Stage{d.label}
{s.name}

Tip: stages die op meerdere dagen draaien (zoals Silent Disco of Schirmbar) kun je hier in één keer instellen.

); } return { PerformanceModal, StageEditor, LineupMatrix }; })(); Object.assign(window, { CrewliModals: M });