Files

380 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (
<div className="cw-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
{children}
</div>
);
}
function Field({ label, hint, error, children }) {
return (
<label className={"cw-field" + (error ? " cw-field-error" : "")}>
<span className="cw-field-lbl">{label}</span>
{children}
{error
? <span className="cw-field-err" role="alert">
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
<circle cx="6" cy="6" r="5.4" fill="#d63d4b"/>
<rect x="5.4" y="3" width="1.2" height="3.6" fill="white"/>
<rect x="5.4" y="7.4" width="1.2" height="1.2" fill="white"/>
</svg>
{error}
</span>
: hint && <span className="cw-field-hint">{hint}</span>}
</label>
);
}
// ─── 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 (
<Backdrop onClose={onClose}>
<div className="cw-modal" style={{ width: 480 }} onMouseDown={(e) => e.stopPropagation()}>
<div className="cw-modal-hd">
<div className="cw-modal-title">
{mode === "add" ? "Performance toevoegen" : "Performance bewerken"}
<span className="cw-modal-sub">{day.label} · {D.EVENT.name}</span>
</div>
<button className="cw-icon-btn" onClick={onClose} aria-label="Sluiten">×</button>
</div>
<div className="cw-modal-body">
<Field label="Naam Act" error={touched && nameError ? nameError : null}>
<input className={"cw-input" + (touched && nameError ? " cw-input-error" : "")}
type="text" autoFocus
placeholder="bijv. Mau P"
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={() => setTouched(true)}
disabled={mode === "edit"}
maxLength={80}
aria-invalid={touched && !!nameError}
aria-describedby={touched && nameError ? "cw-name-err" : undefined} />
</Field>
<div className="cw-row-2">
<Field label="Genre">
<select className="cw-input" value={genre}
onChange={(e) => setGenre(e.target.value)}
disabled={mode === "edit"}>
{D.GENRES.map(g => <option key={g} value={g}>{g}</option>)}
</select>
</Field>
<Field label="Status">
<select className="cw-input" value={status}
onChange={(e) => setStatus(e.target.value)}>
{Object.keys(H.STATUS).map(k =>
<option key={k} value={k}>{H.STATUS[k].label}</option>)}
</select>
</Field>
</div>
{fromTimetable && stage && (
<div className="cw-mctx">
<div className="cw-mctx-stage">
<span className="cw-mctx-stage-bar" style={{ background: stage.color }}></span>
<div className="cw-mctx-stage-body">
<span className="cw-mctx-eyebrow">Stage</span>
<span className="cw-mctx-stage-name">{stage.name}</span>
</div>
<span className="cw-mctx-readonly" title="Sleep het block in de timetable om te wijzigen">vast</span>
</div>
<div className="cw-mctx-time">
<div className="cw-mctx-time-col">
<span className="cw-mctx-eyebrow">Starttijd</span>
<span className="cw-mctx-time-val">{startStr}</span>
</div>
<span className="cw-mctx-arrow" aria-hidden="true"></span>
<div className="cw-mctx-time-col">
<span className="cw-mctx-eyebrow">Eindtijd</span>
<span className="cw-mctx-time-val cw-mctx-time-soft">{endStr}</span>
</div>
<div className="cw-mctx-time-spacer"></div>
<span className="cw-mctx-dur">60 min</span>
</div>
</div>
)}
{!fromTimetable && mode === "add" && (
<>
<Field label="Duur optreden" hint="Wordt gebruikt zodra je het block uit de wachtrij naar de timetable sleept">
<select className="cw-input" value={duration}
onChange={(e) => setDuration(+e.target.value)}>
<option value={30}>30 min</option>
<option value={45}>45 min</option>
<option value={60}>60 min (1 uur)</option>
<option value={75}>75 min</option>
<option value={90}>90 min (1u30)</option>
<option value={120}>120 min (2 uur)</option>
<option value={150}>150 min (2u30)</option>
<option value={180}>180 min (3 uur)</option>
</select>
</Field>
<div className="cw-modal-hint">
Geen tijdslot gekozen landt in de <b>wachtrij</b>.
</div>
</>
)}
</div>
<div className="cw-modal-ft">
{mode === "edit" && (
<button className="cw-btn cw-btn-danger" onClick={() => onDelete(perf.id)}>Verwijderen</button>
)}
<div style={{ flex: 1 }}></div>
<button className="cw-btn" onClick={onClose}>Annuleren</button>
<button className="cw-btn cw-btn-primary" onClick={save} disabled={!canSave}>
{mode === "add" ? "Toevoegen" : "Opslaan"}
</button>
</div>
</div>
</Backdrop>
);
}
// ─── 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 (
<Backdrop onClose={onClose}>
<div className="cw-modal" style={{ width: 480 }} onMouseDown={(e) => e.stopPropagation()}>
<div className="cw-modal-hd">
<div className="cw-modal-title">
{isCreate ? "Stage aanmaken" : "Stage bewerken"}
<span className="cw-modal-sub">{isCreate ? "Nieuwe stage" : stage.name}</span>
</div>
<button className="cw-icon-btn" onClick={onClose} aria-label="Sluiten">×</button>
</div>
<div className="cw-modal-body">
<Field label="Naam">
<input className="cw-input" value={name} onChange={(e) => setName(e.target.value)} />
</Field>
<Field label="Capaciteit" hint="Voor capacity-warnings op artiesten met grote draw">
<input className="cw-input" type="number" value={capacity} onChange={(e) => setCapacity(e.target.value)} />
</Field>
<Field label="Kleur">
<div className="cw-swatch-row">
{SWATCHES.map(c => (
<button key={c}
className={"cw-swatch" + (c === color ? " is-active" : "")}
style={{ background: c }}
onClick={() => setColor(c)} />
))}
</div>
</Field>
<Field label="Actief op dagen" hint="Deze stage verschijnt alleen in de timetable van geselecteerde dagen">
<div className="cw-day-chips">
{days.map(d => (
<button key={d.id}
className={"cw-chip" + (activeDays.includes(d.id) ? " is-active" : "")}
onClick={() => toggleDay(d.id)}>
{d.label}
</button>
))}
</div>
</Field>
</div>
<div className="cw-modal-ft">
{!isCreate && (
<button className="cw-btn cw-btn-danger" onClick={() => onDelete(stage.id)}>Verwijder stage</button>
)}
<div style={{ flex: 1 }}></div>
<button className="cw-btn" onClick={onClose}>Annuleren</button>
<button className="cw-btn cw-btn-primary" onClick={save}>{isCreate ? "Aanmaken" : "Opslaan"}</button>
</div>
</div>
</Backdrop>
);
}
// ─── Lineup matrix: stages × days bulk editor ────────────────────────
function LineupMatrix({ stages, days, stageDays, onSave, onClose }) {
// Build state: { stageId: Set<dayId> }
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 (
<Backdrop onClose={onClose}>
<div className="cw-modal" style={{ width: 640 }} onMouseDown={(e) => e.stopPropagation()}>
<div className="cw-modal-hd">
<div className="cw-modal-title">
Lineup per dag
<span className="cw-modal-sub">Welke stages draaien op welke dag</span>
</div>
<button className="cw-icon-btn" onClick={onClose} aria-label="Sluiten">×</button>
</div>
<div className="cw-modal-body">
<table className="cw-matrix">
<thead>
<tr>
<th>Stage</th>
{days.map(d => <th key={d.id}>{d.label}</th>)}
</tr>
</thead>
<tbody>
{stages.map(s => (
<tr key={s.id}>
<td>
<span className="cw-stage-dot" style={{ background: s.color }}></span>
{s.name}
</td>
{days.map(d => (
<td key={d.id} className="cw-matrix-cell">
<button
className={"cw-check" + (matrix[s.id].has(d.id) ? " is-checked" : "")}
onClick={() => toggle(s.id, d.id)}
aria-label={`Toggle ${s.name} on ${d.label}`}>
{matrix[s.id].has(d.id) && (
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M2 6 L5 9 L10 3" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</button>
</td>
))}
</tr>
))}
</tbody>
</table>
<p className="cw-hint">
Tip: stages die op meerdere dagen draaien (zoals Silent Disco of Schirmbar)
kun je hier in één keer instellen.
</p>
</div>
<div className="cw-modal-ft">
<div style={{ flex: 1 }}></div>
<button className="cw-btn" onClick={onClose}>Annuleren</button>
<button className="cw-btn cw-btn-primary" onClick={save}>Opslaan</button>
</div>
</div>
</Backdrop>
);
}
return { PerformanceModal, StageEditor, LineupMatrix };
})();
Object.assign(window, { CrewliModals: M });