145 lines
5.0 KiB
JavaScript
145 lines
5.0 KiB
JavaScript
// 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 };
|
|
})();
|