// Shared helpers + status palette — matches Crewli light-mode look window.CrewliHelpers = (function () { // Status palette tuned to Crewli's light surface + teal accent. // Each status: pill bg/fg for popover & inline switches; block bg/border for timetable rows. const STATUS = { concept: { label: "Concept", pillBg: "#eceae6", pillFg: "#5a574e", blockBg: "#f1efe9", blockBorder: "#dcd9d1", blockFg: "#3a3830", dot: "#a09c92", }, requested: { label: "Aangevraagd", pillBg: "#fdf2dc", pillFg: "#8a6a1d", blockBg: "#fff6e0", blockBorder: "#f0d99a", blockFg: "#5d4612", dot: "#d9a93c", }, option: { label: "Optie", pillBg: "#ece6f6", pillFg: "#5d4a8a", blockBg: "#f3eefa", blockBorder: "#d9c9ed", blockFg: "#3f2f6a", dot: "#9a82c7", }, confirmed: { label: "Bevestigd", pillBg: "#dff5ec", pillFg: "#1f7a5e", blockBg: "#e8f8f0", blockBorder: "#a9e0c8", blockFg: "#125541", dot: "#3cc2a8", }, contracted: { label: "Getekend", pillBg: "#dcecfa", pillFg: "#1f5a8a", blockBg: "#e6f1fb", blockBorder: "#a8cfee", blockFg: "#143b5d", dot: "#3a8acc", }, cancelled: { label: "Geannuleerd", pillBg: "#f0eeea", pillFg: "#8a8780", blockBg: "#f5f3ef", blockBorder: "#dedbd3", blockFg: "#7a7770", dot: "#a8a59d", }, }; // minute-of-grid -> "HH:MM" (handles past-midnight rollover) function fmtTime(minOfGrid, startHour) { const total = startHour * 60 + minOfGrid; const h = Math.floor(total / 60) % 24; const m = total % 60; return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0"); } function snap(min, step) { return Math.round(min / step) * step; } function findConflicts(performances) { const conflicts = new Set(); const byStage = {}; for (const p of performances) { if (p.status === "cancelled") continue; (byStage[p.stage_id] = byStage[p.stage_id] || []).push(p); } for (const sid in byStage) { const list = byStage[sid].slice().sort((a, b) => a.start - b.start); for (let i = 0; i < list.length; i++) { for (let j = i + 1; j < list.length; j++) { if (list[j].start < list[i].end) { conflicts.add(list[i].id); conflicts.add(list[j].id); } } } } return conflicts; } function findB2B(performances, gap = 5) { const links = []; const byStage = {}; for (const p of performances) { if (p.status === "cancelled") continue; (byStage[p.stage_id] = byStage[p.stage_id] || []).push(p); } for (const sid in byStage) { const list = byStage[sid].slice().sort((a, b) => a.start - b.start); for (let i = 0; i < list.length - 1; i++) { const gapMin = list[i + 1].start - list[i].end; if (gapMin >= 0 && gapMin <= gap) { links.push({ leftId: list[i].id, rightId: list[i + 1].id, gap: gapMin }); } } } return links; } function isCapacityWarn(artist, stage) { if (!artist || !stage) return false; return artist.draw > stage.capacity * 1.1; } // Lane-pack: assign each item a lane index so overlapping items stack vertically. // Items with explicit `lane` (integer) are honored exactly — even if they overlap // another item on the same lane (that's a real conflict and shows visually). // Items without `lane` get placed in the lowest lane that has no time-overlap // with already-placed items (explicit OR previously auto-placed). // Returns { laneOf: {id: lane}, laneCount } function assignLanes(items) { const sorted = items.slice().sort((a, b) => a.start - b.start || a.id.localeCompare(b.id)); const laneOf = {}; let maxLane = 0; const overlapsAt = (it, lane) => sorted.some(other => other.id !== it.id && laneOf[other.id] === lane && other.start < it.end && other.end > it.start ); // Pass 1 — items with explicit lane (sorted by lane asc): try requested lane, // bump down on conflict to avoid overlap. const explicit = sorted.filter(i => Number.isInteger(i.lane)) .sort((a, b) => a.lane - b.lane || a.start - b.start); for (const it of explicit) { let lane = Math.max(0, it.lane); while (overlapsAt(it, lane)) lane++; laneOf[it.id] = lane; if (lane > maxLane) maxLane = lane; } // Pass 2 — items without explicit lane: lowest free lane. for (const it of sorted) { if (it.id in laneOf) continue; let lane = 0; while (overlapsAt(it, lane)) lane++; laneOf[it.id] = lane; if (lane > maxLane) maxLane = lane; } return { laneOf, laneCount: maxLane + 1 }; } function advanceCount(artist, sections) { let done = 0; for (const s of sections) if (artist.advance && artist.advance[s.key]) done++; return { done, total: sections.length }; } return { STATUS, fmtTime, snap, findConflicts, findB2B, isCapacityWarn, advanceCount, assignLanes }; })();