marmar-context / web / context / src / logic / hours.ts
hours.ts
Raw
// Weekly hours as Settings edits them: one span a day, Monday first, or closed. A day stored with
// more than one span keeps the others as they are; closing the day clears them all. Pure.
import type { WeeklyHours } from "../api/types";

/** View days, Monday first (0 is Sunday). */
export const WEEK = [1, 2, 3, 4, 5, 6, 0] as const;
export const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

type Span = { open: string; close: string };
export type DayRow = { day: number; open: boolean; from: string; to: string; more: Span[] };

export function dayRows(hours: WeeklyHours): DayRow[] {
  return WEEK.map((day) => {
    const spans = hours.filter((h) => h.day === day).sort((a, b) => a.open.localeCompare(b.open));
    return { day, open: spans.length > 0, from: spans[0]?.open ?? "08:00", to: spans[0]?.close ?? "17:00", more: spans.slice(1).map(({ open, close }) => ({ open, close })) };
  });
}

export function hoursFrom(rows: DayRow[]): WeeklyHours {
  return rows.flatMap((r) => (r.open ? [{ day: r.day, open: r.from, close: r.to }, ...r.more.map((m) => ({ day: r.day, ...m }))] : []));
}

export const changeDay = (rows: DayRow[], day: number, change: Partial<Omit<DayRow, "day">>): DayRow[] =>
  rows.map((r) => (r.day === day ? { ...r, ...change, more: change.open === false ? [] : r.more } : r));

const CLOCK = /^([01]\d|2[0-3]):[0-5]\d$/;
/** Each open day's problem in plain words, by day. Empty when the hours can be saved. */
export function hoursErrors(rows: DayRow[]): Record<number, string> {
  const errors: Record<number, string> = {};
  for (const r of rows) {
    if (!r.open) continue;
    if (!CLOCK.test(r.from) || !CLOCK.test(r.to)) errors[r.day] = `${DAY_NAMES[r.day]}: enter an opening and a closing time, for example 08:00 and 17:00.`;
    else if (r.from >= r.to) errors[r.day] = `${DAY_NAMES[r.day]}: the closing time must be after the opening time.`;
  }
  return errors;
}

/** Same hours in the same order, so Settings sends them only when they changed. */
export const sameHours = (a: WeeklyHours, b: WeeklyHours) => JSON.stringify(hoursFrom(dayRows(a))) === JSON.stringify(hoursFrom(dayRows(b)));

/** Every time zone the browser knows, with the clinic's own first in the list if it is missing. */
export function timeZones(current: string): string[] {
  const known = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : [];
  return known.includes(current) ? known : [current, ...known];
}