Files
crewli/resources/Crewli - Artist Timetable Management/helpers.js

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 };
})();