alerte-secours/src/utils/dae/getDefibAvailability.js
2026-03-07 07:53:08 +01:00

174 lines
4.9 KiB
JavaScript

/**
* @typedef {{
* days: number[]|null,
* slots: {open: string, close: string}[]|null,
* is24h?: boolean,
* businessHours?: boolean,
* nightHours?: boolean,
* events?: boolean,
* notes?: string,
* }} HorairesStd
*/
/**
* @typedef {{ status: "open"|"closed"|"unknown", label: string }} DefibAvailability
*/
function pad2(n) {
return String(n).padStart(2, "0");
}
function minutesSinceMidnight(date) {
return date.getHours() * 60 + date.getMinutes();
}
function parseHHMM(str) {
if (typeof str !== "string") return null;
const m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(str.trim());
if (!m) return null;
return Number(m[1]) * 60 + Number(m[2]);
}
// ISO 8601 day number: 1=Mon ... 7=Sun
function isoDayNumber(date) {
const js = date.getDay(); // 0=Sun..6=Sat
return js === 0 ? 7 : js;
}
const DAY_LABELS = [null, "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];
function daysLabel(days) {
if (!Array.isArray(days) || days.length === 0) return "";
const uniq = Array.from(new Set(days)).filter((d) => d >= 1 && d <= 7);
uniq.sort((a, b) => a - b);
if (uniq.length === 1) return DAY_LABELS[uniq[0]];
return `${DAY_LABELS[uniq[0]]}-${DAY_LABELS[uniq[uniq.length - 1]]}`;
}
function formatTimeFromMinutes(mins) {
const h = Math.floor(mins / 60);
const m = mins % 60;
return `${pad2(h)}:${pad2(m)}`;
}
function isWithinSlot(nowMin, openMin, closeMin) {
if (openMin == null || closeMin == null) return false;
if (openMin === closeMin) return true;
// Cross-midnight slot (e.g. 20:00-08:00)
if (closeMin < openMin) {
return nowMin >= openMin || nowMin < closeMin;
}
return nowMin >= openMin && nowMin < closeMin;
}
/**
* Determine availability for a given defib schedule.
* Priority logic per PLAN_DAE-merged.md.
*
* @param {HorairesStd|null|undefined} horaires_std
* @param {number|boolean|null|undefined} disponible_24h
* @param {Date} [now]
* @returns {DefibAvailability}
*/
export function getDefibAvailability(
horaires_std,
disponible_24h,
now = new Date(),
) {
if (disponible_24h === 1 || disponible_24h === true) {
return { status: "open", label: "24h/24 7j/7" };
}
/** @type {HorairesStd} */
const h =
horaires_std && typeof horaires_std === "object" ? horaires_std : null;
if (!h) {
return { status: "unknown", label: "Horaires non renseignés" };
}
const today = isoDayNumber(now);
const nowMin = minutesSinceMidnight(now);
const days = Array.isArray(h.days) ? h.days : null;
const hasToday = Array.isArray(days) ? days.includes(today) : null;
// 2. is24h + today
if (h.is24h === true && hasToday === true) {
return { status: "open", label: "24h/24" };
}
// 3. days known and today not included
if (Array.isArray(days) && hasToday === false) {
const label = daysLabel(days);
return { status: "closed", label: label || "Fermé aujourd'hui" };
}
// 4. explicit slots for today
if (hasToday === true && Array.isArray(h.slots) && h.slots.length > 0) {
let isOpen = false;
let nextBoundaryLabel = "";
for (const slot of h.slots) {
const openMin = parseHHMM(slot.open);
const closeMin = parseHHMM(slot.close);
if (openMin == null || closeMin == null) continue;
if (isWithinSlot(nowMin, openMin, closeMin)) {
isOpen = true;
nextBoundaryLabel = `Jusqu'à ${formatTimeFromMinutes(closeMin)}`;
break;
}
}
if (isOpen) {
return { status: "open", label: nextBoundaryLabel || "Ouvert" };
}
// Not within any slot: show next opening time if any (same-day).
const opens = h.slots
.map((s) => parseHHMM(s.open))
.filter((m) => typeof m === "number")
.sort((a, b) => a - b);
const nextOpen = opens.find((m) => m > nowMin);
if (typeof nextOpen === "number") {
return {
status: "closed",
label: `Ouvre à ${formatTimeFromMinutes(nextOpen)}`,
};
}
return { status: "closed", label: "Fermé" };
}
// 5. business hours approximation (Mon-Fri 08:00-18:00)
if (h.businessHours === true) {
const isWeekday = today >= 1 && today <= 5;
const openMin = 8 * 60;
const closeMin = 18 * 60;
const isOpen = isWeekday && nowMin >= openMin && nowMin < closeMin;
return {
status: isOpen ? "open" : "closed",
label: isOpen ? "Heures ouvrables" : "Fermé (heures ouvrables)",
};
}
// 6. night hours approximation (20:00-08:00)
if (h.nightHours === true) {
const openMin = 20 * 60;
const closeMin = 8 * 60;
const isOpen = isWithinSlot(nowMin, openMin, closeMin);
return {
status: isOpen ? "open" : "closed",
label: isOpen ? "Heures de nuit" : "Fermé (heures de nuit)",
};
}
// 7. events
if (h.events === true) {
return { status: "unknown", label: "Selon événements" };
}
// 8. fallback
const notes = typeof h.notes === "string" ? h.notes.trim() : "";
return { status: "unknown", label: notes || "Horaires non renseignés" };
}