Files

265 lines
11 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.
// Unified popover for both timetable performances and queue items.
// Design language is the same; the body just adapts to whether a time/stage exists.
//
// - Header: avatar + name + meta line (genre · stage OR "In wachtrij" · genre)
// - Time: tijdvak (or "Geen tijdslot")
// - Status: dropdown (workflow-ready — disabled options can be added later)
// - Advancing: progress bar + checklist
// - Footer: Verwijderen (timetable only) + full-width "Open detailpagina"
//
// Off-screen safe: positioning prefers below, flips above when needed,
// clamps to viewport with margin.
const PV = (function () {
const POP_W = 340;
const POP_H = 460;
const MARGIN = 12;
function pickPos(anchorRect, preferSide) {
// Horizontal: preferred side first, then opposite, then clamp.
const right = anchorRect.right + MARGIN;
const left = anchorRect.left - POP_W - MARGIN;
let l, side;
const fitsRight = right + POP_W < window.innerWidth - MARGIN;
const fitsLeft = left > MARGIN;
if (preferSide === "left") {
if (fitsLeft) { l = left; side = "left"; }
else if (fitsRight) { l = right; side = "right"; }
else { l = Math.max(MARGIN, anchorRect.left); side = "right"; }
} else {
if (fitsRight) { l = right; side = "right"; }
else if (fitsLeft) { l = left; side = "left"; }
else { l = Math.max(MARGIN, anchorRect.left); side = "right"; }
}
// Vertical: anchor top-aligned. If overflows bottom, push up. Never go above MARGIN.
let t = anchorRect.top - 8;
if (t + POP_H > window.innerHeight - MARGIN) {
t = window.innerHeight - POP_H - MARGIN;
}
if (t < MARGIN) t = MARGIN;
return { left: l, top: t, side };
}
// ─── Status dropdown (workflow-ready, supports disabled) ───────────
function StatusDropdown({ value, onChange, disabledKeys }) {
const STATUS = window.CrewliHelpers.STATUS;
const ORDER = ["concept", "requested", "option", "confirmed", "contracted", "cancelled"];
const [open, setOpen] = React.useState(false);
const ref = React.useRef(null);
React.useEffect(() => {
if (!open) return;
const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
window.addEventListener("mousedown", onDown);
return () => window.removeEventListener("mousedown", onDown);
}, [open]);
const cur = STATUS[value];
const dis = disabledKeys || new Set();
return (
<div className="cw-pop-dd" ref={ref}>
<button type="button" className="cw-pop-dd-btn"
onClick={() => setOpen(o => !o)}
style={{ background: cur.pillBg, color: cur.pillFg, borderColor: cur.dot + "80" }}>
<span className="cw-status-dot" style={{ background: cur.dot }}></span>
<span className="cw-pop-dd-lbl">{cur.label}</span>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.4" style={{ opacity: .7, marginLeft: 4 }}>
<path d="M2.5 4 L5 6.5 L7.5 4"/>
</svg>
</button>
{open && (
<div className="cw-pop-dd-menu" role="listbox">
{ORDER.map(k => {
const s = STATUS[k];
const isCur = k === value;
const isDisabled = dis.has(k);
return (
<button key={k} type="button"
className={"cw-pop-dd-item" + (isCur ? " is-current" : "") + (isDisabled ? " is-disabled" : "")}
disabled={isDisabled}
onClick={() => { if (!isDisabled) { onChange(k); setOpen(false); } }}>
<span className="cw-status-dot" style={{ background: s.dot, opacity: isDisabled ? .35 : 1 }}></span>
<span className="cw-pop-dd-item-lbl">{s.label}</span>
{isCur && (
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ marginLeft: "auto" }}>
<path d="M2.5 5.5 L4.5 7.5 L8.5 3.5"/>
</svg>
)}
</button>
);
})}
</div>
)}
</div>
);
}
// ─── Shared body (time + status + advancing) ─────────────────────
function PopoverBody({ status, time, dur, advDone, advTotal, sections, artist, statusKey, onChangeStatus }) {
const advPct = Math.round(advDone / advTotal * 100);
return (
<>
<div className="cw-pop-row">
<div className="cw-pop-time">
<div className={"cw-pop-time-main" + (time ? "" : " cw-pop-time-empty")}>
{time || (dur != null ? `${Math.floor(dur/60)}u${dur%60 ? ` ${dur%60}m` : ""}` : "Geen tijdslot")}
</div>
<div className="cw-pop-time-sub">
{time
? `${Math.floor(dur/60)}u${dur%60 ? ` ${dur%60}m` : ""}`
: (dur != null ? "Verwachte duur" : "Sleep naar timetable om te plannen")}
</div>
</div>
</div>
<div className="cw-pop-section-lbl">Status</div>
<StatusDropdown value={statusKey} onChange={onChangeStatus} />
<div className="cw-pop-section-lbl">
Advancing
<span className="cw-pop-section-meta">{advPct}% · {advDone}/{advTotal}</span>
</div>
<div className="cw-progress" style={{ marginBottom: 8 }}>
<div className="cw-progress-fill" style={{ width: advPct + "%" }}></div>
</div>
<div className="cw-adv-list">
{sections.map(s => {
const done = !!(artist.advance && artist.advance[s.key]);
return (
<div key={s.key} className={"cw-adv-row" + (done ? " is-done" : "")}>
<span className="cw-adv-tick">
{done ? (
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 5 L4 7 L8 3" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
) : null}
</span>
<span className="cw-adv-label">{s.label}</span>
</div>
);
})}
</div>
</>
);
}
// ─── Performance popover (timetable block) ─────────────────────────
function Popover({ perf, artist, stage, anchorRect, sections, onChangeStatus, onOpenDetailPage, onClose }) {
const ref = React.useRef(null);
const [pos, setPos] = React.useState(() => pickPos(anchorRect, "right"));
React.useEffect(() => {
const update = () => setPos(pickPos(anchorRect, "right"));
update();
window.addEventListener("resize", update);
window.addEventListener("scroll", update, true);
return () => { window.removeEventListener("resize", update); window.removeEventListener("scroll", update, true); };
}, [anchorRect]);
React.useEffect(() => {
const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
window.addEventListener("mousedown", onDown);
return () => window.removeEventListener("mousedown", onDown);
}, []);
const adv = window.CrewliHelpers.advanceCount(artist, sections);
const time = window.CrewliHelpers.fmtTime(perf.start, window.CrewliData.TIME.startHour) +
" " + window.CrewliHelpers.fmtTime(perf.end, window.CrewliData.TIME.startHour);
const dur = perf.end - perf.start;
return (
<div ref={ref} className="cw-popover" style={{ left: pos.left, top: pos.top }}>
<div className="cw-pop-arrow" data-side={pos.side}></div>
<div className="cw-pop-hd">
<div className="cw-pop-avatar" style={{ background: stage.color + "1f", color: "#1a2530", border: "1px solid " + stage.color + "55" }}>
{artist.initials}
</div>
<div className="cw-pop-titles">
<div className="cw-pop-name">{artist.name}</div>
<div className="cw-pop-meta">
<span className="cw-pop-meta-genre">{artist.genre}</span>
<span className="cw-pop-meta-sep">·</span>
<span className="cw-stage-dot" style={{ background: stage.color }}></span>
{stage.name}
</div>
</div>
<button className="cw-icon-btn" onClick={onClose} aria-label="Sluiten">×</button>
</div>
<PopoverBody
time={time} dur={dur}
advDone={adv.done} advTotal={adv.total}
sections={sections} artist={artist}
statusKey={perf.status} onChangeStatus={onChangeStatus}
/>
<div className="cw-pop-ft cw-pop-ft-stack">
<button className="cw-btn cw-btn-primary cw-btn-block"
onClick={() => onOpenDetailPage(artist, perf)}>
Open detailpagina
</button>
</div>
</div>
);
}
// ─── Queue popover (parking column) ─────────────────────────────────
function QueuePopover({ item, artist, anchorRect, sections, onChangeStatus, onOpenDetailPage, onClose }) {
const ref = React.useRef(null);
const [pos, setPos] = React.useState(() => pickPos(anchorRect, "left"));
React.useEffect(() => {
const update = () => setPos(pickPos(anchorRect, "left"));
update();
window.addEventListener("resize", update);
window.addEventListener("scroll", update, true);
return () => { window.removeEventListener("resize", update); window.removeEventListener("scroll", update, true); };
}, [anchorRect]);
React.useEffect(() => {
const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
window.addEventListener("mousedown", onDown);
return () => window.removeEventListener("mousedown", onDown);
}, []);
const adv = window.CrewliHelpers.advanceCount(artist, sections);
const dur = item.dur;
return (
<div ref={ref} className="cw-popover cw-popover-queue" style={{ left: pos.left, top: pos.top }}>
<div className="cw-pop-arrow" data-side={pos.side}></div>
<div className="cw-pop-hd">
<div className="cw-pop-avatar" style={{ background: "#e6efe9", color: "#1a2530", border: "1px solid var(--border)" }}>
{artist.initials}
</div>
<div className="cw-pop-titles">
<div className="cw-pop-name">{artist.name}</div>
<div className="cw-pop-meta">
<span className="cw-pop-meta-genre">{artist.genre}</span>
<span className="cw-pop-meta-sep">·</span>
<span className="cw-pop-queue-tag">In wachtrij</span>
</div>
</div>
<button className="cw-icon-btn" onClick={onClose} aria-label="Sluiten">×</button>
</div>
<PopoverBody
time={null} dur={dur}
advDone={adv.done} advTotal={adv.total}
sections={sections} artist={artist}
statusKey={item.status} onChangeStatus={onChangeStatus}
/>
<div className="cw-pop-ft cw-pop-ft-stack">
<button className="cw-btn cw-btn-primary cw-btn-block"
onClick={() => onOpenDetailPage(artist)}>
Open detailpagina
</button>
</div>
</div>
);
}
return { Popover, QueuePopover };
})();
Object.assign(window, { CrewliPopover: PV });