// 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 && (
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}
{!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
| Stage |
{days.map(d => {d.label} | )}
{stages.map(s => (
|
{s.name}
|
{days.map(d => (
|
))}
))}
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 });