/** * When a branch delivers. * * A branch offers at most three windows a day — morning, afternoon, evening — * and a shopper picks one at checkout. The window is a PREFERENCE, not a * promise: every order is accepted and no window ever fills up. * * ── A branch with no windows is not broken ────────────────────────────────── * * Every tenant trading today has none, and all of them keep taking orders. * An empty list means "order without a window", never "this shop is closed". * Nothing here may treat it as an error state. * * ── The console edits; it does not decide what is open ────────────────────── * * Whether a window is still open today is a clock comparison, and the server * owns it — see deliverySlotService.go. This module reads what a branch has * configured and writes it back. The filtered, dated list a shopper sees is an * app concern and is not fetched here. */ import { api, WEB } from './client'; /** The three, in the order a shopper reads them. */ export const SLOT_KEYS = ['morning', 'afternoon', 'evening'] as const; export type SlotKey = (typeof SLOT_KEYS)[number]; export interface DeliverySlot { deliveryslotid?: number; tenantid?: number; locationid?: number; slotkey: SlotKey; /** What the shopper reads. Blank falls back to the capitalised key. */ name: string; /** "HH:MM", 24-hour, in the shop's own local time. */ starttime: string; /** * Also the cut-off. A window takes orders right up to the moment it ends — * there is deliberately no separate cut-off to configure. */ endtime: string; status: 'active' | 'inactive'; } /** * What a branch starts with when nobody has set anything. * * Seeded rather than blank so onboarding asks a shopkeeper to CONFIRM rather * than to invent: three empty time fields is a form most people abandon, and a * tenant that abandons it has a shop that cannot offer windows at all. */ export const DEFAULT_SLOTS: DeliverySlot[] = [ { slotkey: 'morning', name: 'Morning', starttime: '08:00', endtime: '10:00', status: 'active' }, { slotkey: 'afternoon', name: 'Afternoon', starttime: '12:00', endtime: '15:00', status: 'active' }, { slotkey: 'evening', name: 'Evening', starttime: '17:00', endtime: '20:00', status: 'active' }, ]; export const deliverySlotsApi = { /** Everything this branch has configured, active or not. */ list: (tenantid: number, locationid: number) => api.get<{ details: DeliverySlot[] }>(`${WEB}/deliveryslots`, { tenantid, locationid }), /** * All three together, never one at a time. * * They are edited as a set on one screen, and sending them together is what * lets the server reject the whole edit when one row is wrong instead of * applying half of it — a shop with two new windows and one old one, and * nothing on screen saying which took, is worse than a shop with none. */ save: (tenantid: number, locationid: number, slots: DeliverySlot[]) => api.put(`${WEB}/deliveryslots`, { tenantid, locationid, slots }), }; /** * Is this set fit to send? * * Mirrors the server's rules so the shopkeeper hears about a mistake while * their hands are still on it, rather than as a 409 after pressing save. The * server re-checks all of it — this is courtesy, not security. */ export function slotProblems(slots: DeliverySlot[]): string[] { const problems: string[] = []; for (const slot of slots) { const label = slot.name.trim() || slot.slotkey; if (!/^\d{2}:\d{2}$/.test(slot.starttime) || !/^\d{2}:\d{2}$/.test(slot.endtime)) { problems.push(`${label} needs a start and end time.`); continue; } // An end at or before its start never passes the server's "still running" // test, so the window would simply never appear to a shopper, with nothing // saying why. if (slot.endtime <= slot.starttime) { problems.push(`${label} ends at or before it starts (${slot.starttime}–${slot.endtime}).`); } } return problems; }