@medalsocial/booking 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +2 -1
- package/dist/core/config.d.mts +14 -0
- package/dist/core/config.mjs +9 -4
- package/dist/core/config.mjs.map +1 -1
- package/dist/core/draft-store.d.mts +5 -0
- package/dist/core/draft-store.mjs +1 -0
- package/dist/core/draft-store.mjs.map +1 -1
- package/dist/core/index.d.mts +2 -1
- package/dist/core/index.mjs +2 -1
- package/dist/core/machine.d.mts +6 -0
- package/dist/core/machine.mjs +22 -3
- package/dist/core/machine.mjs.map +1 -1
- package/dist/core/range.d.mts +30 -0
- package/dist/core/range.mjs +32 -0
- package/dist/core/range.mjs.map +1 -0
- package/dist/next/catalogue.mjs +4 -5
- package/dist/next/catalogue.mjs.map +1 -1
- package/dist/next/handler.mjs +63 -5
- package/dist/next/handler.mjs.map +1 -1
- package/dist/next/medal.mjs +47 -15
- package/dist/next/medal.mjs.map +1 -1
- package/dist/next/routes/create.mjs +3 -6
- package/dist/next/routes/create.mjs.map +1 -1
- package/dist/next/routes/manage.mjs +5 -8
- package/dist/next/routes/manage.mjs.map +1 -1
- package/dist/next/routes/shared.mjs +57 -14
- package/dist/next/routes/shared.mjs.map +1 -1
- package/dist/react/BookingWizard.d.mts +7 -1
- package/dist/react/BookingWizard.mjs +15 -0
- package/dist/react/BookingWizard.mjs.map +1 -1
- package/dist/react/index.d.mts +2 -2
- package/dist/react/labels/screens.mjs +4 -0
- package/dist/react/labels/screens.mjs.map +1 -1
- package/dist/react/useBooking.d.mts +11 -1
- package/dist/react/useBooking.mjs +105 -29
- package/dist/react/useBooking.mjs.map +1 -1
- package/dist/react/wizard/index.d.mts +2 -2
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBooking.mjs","names":[],"sources":["../../src/react/useBooking.ts"],"sourcesContent":["/**\n * `useBooking()` — the booking wizard without its markup.\n *\n * Everything `<BookingWizard>` knows lives here, and the wizard is built on\n * it, so the headless path is never second-class: the machine (through\n * `useReducer`), the fetches to the site's own routes under `config.paths.api`\n * (availability, stylists, hours, the submission — the Medal key never leaves\n * the server), the draft / attempt / rebook stores, the restore of a booking\n * a reload or a Vipps login interrupted, the deep-link and «book again»\n * prefill, the 409 slot-taken refresh and the idempotency nonce.\n *\n * What it does NOT own is the page: focus, scrolling, which screen is drawn.\n * Those are the caller's (see `BookingWizard.tsx`).\n *\n * Moved from the source app's wizard shell with its behaviour unchanged; the\n * long-form reasoning for each rule is kept beside the rule.\n */\n\nimport { useSearchParams } from 'next/navigation';\nimport { useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from 'react';\nimport type { AgeRange } from '../core/age';\nimport { ATTEMPT_TTL_MS, type BookingAttempt, type SubmittedVisit } from '../core/attempt-store';\nimport { serviceMatches } from '../core/deep-link';\nimport { stylistDisplayName } from '../core/display-name';\nimport { DRAFT_MAX_AGE_MS, type WizardDraft, type WizardDraftItem } from '../core/draft-store';\nimport { fill, labelText } from '../core/labels';\nimport {\n SELF_KEY,\n type WizardAction,\n type WizardItem,\n type WizardPerson,\n type WizardPrefill,\n type WizardService,\n type WizardState,\n} from '../core/machine';\nimport { firstOpeningPerResource } from '../core/next-available';\nimport { type PartySlot, type SlotsByService, slotKeysFor } from '../core/party-slots';\nimport { stripVippsReturn, vippsConfirmFrom } from '../core/portal/vipps-return';\nimport type {\n BookingDayDto,\n BookingFamilyMember,\n BookingGuardian,\n BookingResourceDto,\n BookingServiceDto,\n BookingSlotDto,\n BookingSubmission,\n} from '../core/types';\nimport { visitKey, visitServicesOf } from '../core/visit';\nimport type { BookingKit } from './kit';\nimport { type BookingOverrides, useBookingKit } from './Provider';\n\n/**\n * What the booking page prefetched on the server. Structurally the `/next`\n * loader's seed; every part but `services` may be missing, and whatever is\n * missing is fetched on demand into the same place.\n */\nexport interface BookingSeed {\n /** The full catalogue. */\n services: BookingServiceDto[];\n /** The stylists, or `null`/absent when they could not be read. */\n resources?: BookingResourceDto[] | null;\n /** Openings by service id, unfiltered by stylist. */\n slots?: Record<string, BookingSlotDto[]>;\n /** The business's hours over the window, by service id. */\n schedules?: Record<string, BookingDayDto[]>;\n /** «Next available» per stylist, by service id. */\n nextAvailable?: Record<string, Record<string, number>>;\n /** The instant the seed's window starts at; pinned once when absent. */\n fromTs?: number;\n}\n\nexport interface UseBookingOptions extends BookingOverrides {\n seed: BookingSeed;\n /**\n * The business's phone and address for this request, over `config.contact`\n * — what `/next`'s `loadBookingPage` answers as `contact`.\n */\n contact?: { phone?: string | null; address?: string | null };\n /** The parent the page found a portal session for; `null` for a guest. */\n guardian?: BookingGuardian | null;\n /** How many days the window covers. Default `config.window.rangeDays`. */\n rangeDays?: number;\n /**\n * Whether the shell can put a login in front of the visitor again. Under\n * `account.required` a lost session (`/create` → `accountRequired`) is a\n * login when it can — `login.sessionLost` — and an ordinary submit error\n * when it cannot. Default `true`: a headless shell reads `sessionLost`.\n */\n loginOffered?: boolean;\n}\n\n/** A person's «same as last time», and the note when it was swapped for their age. */\nexport interface BookingSuggestion {\n service: BookingServiceDto;\n note?: string;\n}\n\n/** One booked line on the confirmation. */\nexport interface ConfirmedLine {\n item: SubmittedVisit['items'][number];\n bookingId: string;\n stylistName: string | null;\n manageHref: string | null;\n}\n\n/** The confirmation, once a booking came back. */\nexport interface BookingConfirmation {\n bookings: Array<{ id: string; manageHref: string | null }>;\n submitted: SubmittedVisit;\n}\n\n/**\n * A line item as a draft keeps it. `idOnly` — nobody is logged in — keeps a\n * saved child by its id ALONE: the login the draft waits for decides, and only\n * that child's own parent gets the name back (`reseatFor`).\n */\nfunction draftItem(item: WizardItem, idOnly = false): WizardDraftItem {\n const saved = idOnly && item.bookedForPersonId !== undefined;\n return {\n serviceId: item.service.id,\n // Only when there are some: a one-service line is stored as it always was.\n ...(item.extraServices?.length\n ? { extraServiceIds: item.extraServices.map((service) => service.id) }\n : {}),\n bookedForName: saved ? null : (item.bookedForName ?? null),\n bookedForBirthYear: saved ? null : (item.bookedForBirthYear ?? null),\n adult: item.adult === true,\n personId: item.bookedForPersonId ?? null,\n };\n}\n\n/** How long a replayed submission may take before the restore gate gives up. */\nconst REPLAY_TIMEOUT_MS = 15_000;\n\n/** «hydrated» only ever flips once; `useSyncExternalStore` wants a subscription. */\nfunction noSubscription(): () => void {\n return () => {\n // Nothing to unsubscribe from.\n };\n}\n\n/** The `freshSlots` of a `slotTaken` answer, narrowed to the lost visits' lists. */\nfunction freshSlotsOf(raw: unknown, lost: ReadonlySet<string>): Record<string, BookingSlotDto[]> {\n if (typeof raw !== 'object' || raw === null) return {};\n return Object.fromEntries(\n Object.entries(raw as Record<string, unknown>).filter(\n (entry): entry is [string, BookingSlotDto[]] => lost.has(entry[0]) && Array.isArray(entry[1])\n )\n );\n}\n\n/**\n * The window query for ONE visit, named by its key (`a` or `a+b`): the first\n * service as `service_id`, the rest as `extra_service_ids`. The extras are\n * added only when there are some, so a one-service visit asks with exactly the\n * URL it always did — the slot cache and every seeded answer are keyed by it.\n */\nfunction windowQuery(\n key: string,\n fromTs: number,\n toTs: number,\n resourceId?: string | null\n): URLSearchParams {\n const [serviceId, ...extras] = key.split('+');\n return new URLSearchParams({\n service_id: serviceId,\n ...(extras.length === 0 ? {} : { extra_service_ids: extras.join(',') }),\n // Omitted for «first available»: an empty resource_id would still narrow.\n ...(resourceId ? { resource_id: resourceId } : {}),\n from_ts: String(fromTs),\n to_ts: String(toTs),\n });\n}\n\n/**\n * The submission with each line's extras on it. meda's details screen builds\n * the body from its own types, which have one service per line, so the shell\n * is the only place that knows the rest of a person's visit. Index-aligned with\n * the basket the screen was drawn from; a body of another length is not one\n * this basket produced, and is sent as it came rather than with extras pinned\n * to the wrong people.\n */\nfunction withExtraServices(\n submission: BookingSubmission,\n items: ReadonlyArray<SubmittedVisit['items'][number]>\n): BookingSubmission | null {\n if (!items.some((item) => (item.extraServices ?? []).length > 0)) return submission;\n // Lines that cannot be matched to the basket are REFUSED, not sent bare: a\n // body without the extras books the first service only, while the card and\n // the calendar entry would describe the whole visit.\n if (submission.items.length !== items.length) {\n console.warn(\n '[@medalsocial/booking] The submission has %d lines for a basket of %d; not sent.',\n submission.items.length,\n items.length\n );\n return null;\n }\n return {\n ...submission,\n items: submission.items.map((line, index) => {\n const extras = items[index].extraServices ?? [];\n return extras.length === 0\n ? line\n : { ...line, extraServiceIds: extras.map((service) => service.id) };\n }),\n };\n}\n\n/**\n * Under `account.required`, each line's saved child back on it whatever phone\n * was typed. The details screen sends a person id only under the parent's own\n * number (the phone rule), but with an account required the booking goes on\n * the logged-in parent's contact anyway, so a child sent by name alone would\n * be created there a second time. The machine's id is the line's own: a name\n * typed over a saved child clears it. The route checks every id against the\n * session's own family before it is forwarded.\n */\nfunction withSessionPersons(\n submission: BookingSubmission,\n items: ReadonlyArray<SubmittedVisit['items'][number]>\n): BookingSubmission {\n return {\n ...submission,\n items: submission.items.map((line, index) => {\n const personId = items[index]?.bookedForPersonId;\n return personId === undefined ? line : { ...line, bookedForPersonId: personId };\n }),\n };\n}\n\n/** What the create route said, narrowed to what the machine can hold. */\nfunction asSubmitFailure(\n code: unknown\n): Exclude<NonNullable<WizardState['error']>, 'slotTaken' | 'maxParty'> {\n switch (code) {\n case 'conflict':\n case 'invalidInput':\n case 'unconfigured':\n case 'inProgress':\n return code;\n default:\n return 'upstreamError';\n }\n}\n\n/** What the named stylist can do; `undefined` for «nobody named» or «not loaded». */\nfunction serviceIdsOf(\n resources: BookingResourceDto[],\n resourceId: string | null\n): readonly string[] | undefined {\n if (resourceId === null) return undefined;\n return resources.find((resource) => resource.id === resourceId)?.serviceIds;\n}\n\n/**\n * Whether a create answer is a whole one: exactly one booking, with its own\n * distinct id, per submitted line. Anything else — none, fewer, more, an id\n * missing or repeated — is not a\n * confirmation; the wizard shows it as the generic create failure\n * (`upstreamError`, «we still do not know»), whose attempt survives for the\n * replay under the same idempotency key.\n */\nexport function isCompleteConfirmation(\n bookings: ReadonlyArray<{ id?: unknown }> | null | undefined,\n itemCount: number\n): boolean {\n if (!Array.isArray(bookings) || itemCount <= 0 || bookings.length !== itemCount) return false;\n const ids = bookings.map((booking) => booking?.id);\n // Distinct, too: one id on two lines is two calendar entries with one UID.\n return (\n ids.every((id) => typeof id === 'string' && id.length > 0) && new Set(ids).size === ids.length\n );\n}\n\n/**\n * The basket and the create response, zipped by INDEX — each line its OWN\n * booking. Only a whole answer (`isCompleteConfirmation`) gets this far; a line\n * the answer has no booking for is left out rather than handed a sibling's id,\n * which would give two calendar entries one UID.\n */\nexport function confirmationLines(confirmation: BookingConfirmation): ConfirmedLine[] {\n const { submitted, bookings } = confirmation;\n return submitted.items.flatMap((item, index) => {\n const booking = bookings[index];\n if (booking === undefined) return [];\n return [\n {\n item,\n bookingId: booking.id,\n stylistName: submitted.stylistNames[index] ?? null,\n manageHref: booking.manageHref,\n },\n ];\n });\n}\n\n/** Only the stylist the visitor asked for. */\nfunction onlyResource(slots: readonly BookingSlotDto[], resourceId: string): BookingSlotDto[] {\n return slots.filter((slot) => slot.resourceId === resourceId);\n}\n\n/** What a link asked for that needs no service to be true yet. */\ninterface DeepLink {\n category: string | null;\n stylist: string | null;\n party: number | null;\n seat: { children: number } | { adult: true } | null;\n}\n\n/** The catalogue a prefill is checked against: what step 2 lets the visitor TAP. */\nfunction prefillCatalogue(services: BookingServiceDto[], resources: BookingResourceDto[]) {\n return { services: services.filter((service) => service.bookableOnline), resources };\n}\n\n/** A stylist without a service is nothing to finish. */\nfunction stylistLeftToFill(prefill: WizardPrefill | null): WizardPrefill | null {\n return prefill?.serviceId && prefill.resourceId ? prefill : null;\n}\n\n/** Whether the stylist list can now vouch for the pending link. */\nfunction canFinishStylist(\n pending: WizardPrefill | null,\n resources: BookingResourceDto[],\n primaryVisit: string | null\n): pending is WizardPrefill {\n return pending !== null && resources.length > 0 && primaryVisit === pending.serviceId;\n}\n\n/**\n * A saved child as the person the machine seats: by `personId` when Medal has\n * one, else by their PLACE in the profile with name and year beside it, so two\n * «Emma 2018» are two seats and a reordered list matches nobody.\n */\nexport function personForChild(child: BookingFamilyMember, index: number): WizardPerson {\n return {\n key: child.personId ? `p:${child.personId}` : `n:${index}:${child.name}:${child.birthYear}`,\n name: child.name,\n birthYear: child.birthYear,\n ...(child.personId ? { personId: child.personId } : {}),\n ...(child.birthMonth ? { birthMonth: child.birthMonth } : {}),\n };\n}\n\n/** The pure parts of the wizard's setup, bound to one kit. */\nfunction setupFor(kit: BookingKit) {\n const { config, wizard, deepLinks } = kit;\n const q = config.query;\n\n /** «Bestill igjen» (`?service=&stylist=`) or a marketing `?tjeneste=` link, or `null`. */\n function prefillFromQuery(\n query: URLSearchParams | null,\n services: BookingServiceDto[]\n ): WizardPrefill | null {\n if (query === null) return null;\n const linked = query.get(q.service);\n const linkedService = linked\n ? services.find((service) => service.bookableOnline && serviceMatches(service, linked))\n : undefined;\n const serviceId = query.get(q.rebookService) || linkedService?.id || null;\n const resourceId = query.get(q.rebookStylist) || (serviceId ? query.get(q.stylist) : null);\n if (!serviceId && !resourceId) return null;\n return {\n serviceId,\n resourceId,\n party: serviceId ? deepLinks.parseParty(query.get(q.party)) : null,\n };\n }\n\n function deepLinkFromQuery(query: URLSearchParams | null): DeepLink {\n return {\n category: deepLinks.parseCategory(query?.get(q.category)),\n stylist: query?.get(q.stylist)?.trim() || null,\n party: deepLinks.parseParty(query?.get(q.party)),\n seat: deepLinks.impliedParty(query),\n };\n }\n\n /** One grown-up seated, as the adult chip does. */\n function withAdult(): WizardState {\n return wizard.reduce(wizard.initialState(), {\n type: 'choosePeople',\n people: [{ key: 'adult', adult: true }],\n advance: true,\n });\n }\n\n /** `n` guest children seated, clamped to the chips there are. */\n function withGuests(party: number): WizardState {\n const count = Math.min(Math.max(1, party), wizard.maxPeople);\n const people = Array.from({ length: count }, (_, index) => wizard.guestChild(index + 1));\n return wizard.reduce(wizard.initialState(), { type: 'choosePeople', people, advance: true });\n }\n\n /** `state` with the link's service held for step 1, when the catalogue has it. */\n function heldLink(\n state: WizardState,\n prefill: WizardPrefill | null,\n services: BookingServiceDto[],\n resources: BookingResourceDto[]\n ): WizardState {\n const service = prefill?.serviceId\n ? prefillCatalogue(services, resources).services.find(\n (entry) => entry.id === prefill.serviceId\n )\n : undefined;\n return service === undefined ? state : wizard.reduce(state, { type: 'holdService', service });\n }\n\n /**\n * Where the wizard starts: empty, or with the link folded in — in the\n * reducer's initialiser, so the server render already shows that step.\n */\n function initialWizardState(\n prefill: WizardPrefill | null,\n services: BookingServiceDto[],\n resources: BookingResourceDto[],\n seat: DeepLink['seat'] = null,\n familyKnown = false\n ): WizardState {\n // A parent the page knows is asked who is coming FIRST, whatever the link says.\n if (familyKnown) return heldLink(wizard.initialState(), prefill, services, resources);\n // `party.askWhoFirst: false`: a guest starts on step 2 as the common visit —\n // one child where the site has a children's menu, else one grown-up — and\n // step 1 stays one tap back for a party.\n const unasked = config.party.askWhoFirst\n ? null\n : kit.childCategory !== null\n ? { children: 1 }\n : ({ adult: true } as const);\n const seated = seat ?? unasked;\n const base = seated !== null && 'adult' in seated ? withAdult() : wizard.initialState();\n const state =\n prefill === null\n ? seated === null\n ? wizard.initialState()\n : 'adult' in seated\n ? withAdult()\n : withGuests(seated.children)\n : wizard.applyPrefill(base, prefill, prefillCatalogue(services, resources));\n // `screens.guestParty`: a guest still on step 1 with nobody chosen — no\n // link, or a link that holds a service for step 1 — starts on the common\n // answer, one child, already in the state, so the server-rendered step\n // shows «1» and a live «next». A link that seats people stays as it was.\n if (\n config.screens.guestParty &&\n seated === null &&\n state.step === 'who' &&\n state.people.length === 0\n ) {\n return wizard.reduce(state, {\n type: 'choosePeople',\n people: [wizard.guestChild(1)],\n advance: false,\n });\n }\n return state;\n }\n\n return { prefillFromQuery, deepLinkFromQuery, heldLink, initialWizardState };\n}\n\nconst CONTACT_FIELDS = ['name', 'phone', 'email'] as const;\n/** Contact fields by value, each only while a profile still owns it. */\ntype ProfileOwned = Partial<Record<(typeof CONTACT_FIELDS)[number], string>>;\n\nexport function useBooking(options: UseBookingOptions) {\n const { kit } = useBookingKit(options, options.contact);\n const { config, labels, wizard, clock, age, phone, deepLinks } = kit;\n const services = options.seed.services;\n const arrivedAs = options.guardian ?? null;\n const rangeDays = options.rangeDays ?? config.window.rangeDays;\n const setup = useMemo(() => setupFor(kit), [kit]);\n\n /**\n * Who logged in from the login sheet — and whether anybody did, which is\n * not the same question: a good code whose profile read failed is still a\n * logged-in parent, who must not be offered the login again.\n */\n const [signedIn, setSignedIn] = useState<{ guardian: BookingGuardian | null } | null>(null);\n /**\n * The create route said nobody is logged in (`accountRequired`): the session\n * the page arrived with — or the one taken here — has run out. From then on\n * the parent the page found is not trusted; only a fresh login is. Sticky:\n * a fresh login clears `sessionLost` (the notice), never this.\n */\n const [arrivalExpired, setArrivalExpired] = useState(false);\n /** The gate is back because the session ran out — until the next login. */\n const [sessionLost, setSessionLost] = useState(false);\n const arrivedAsNow = arrivalExpired ? null : arrivedAs;\n // A login decides who the parent is, even one whose profile read failed:\n // falling back to who ARRIVED would hand account B the family and\n // details of account A, whose seats `signIn` has just let go.\n const guardian = signedIn === null ? arrivedAsNow : signedIn.guardian;\n\n // The link, read ONCE, in the initialisers: a prefill that re-applied later\n // would answer questions the visitor has since changed their mind about.\n const query: URLSearchParams | null = useSearchParams();\n const [prefill] = useState(() => setup.prefillFromQuery(query, services));\n const [deepLink] = useState(() => setup.deepLinkFromQuery(query));\n const deepLinkPending = useRef(prefill === null || prefill.serviceId == null);\n const [linked] = useState(() => prefill !== null || deepLinks.hasDeepLink(query));\n const [vippsResumePath] = useState(() => deepLinks.resumePath(query));\n const [resuming] = useState(() => query?.get(config.query.resume) === '1');\n const [vippsConfirm] = useState<{ to: string | null } | null>(\n () => vippsConfirmFrom(query) ?? null\n );\n\n // The address bar is cleaned straight after the first render: the marker and\n // the masked address must not stay in the history entry.\n useEffect(() => {\n if (vippsConfirm !== null) stripVippsReturn();\n }, [vippsConfirm]);\n /** `false` on the server and through hydration, `true` from the first render after it. */\n const hydrated = useSyncExternalStore(\n noSubscription,\n () => true,\n () => false\n );\n const [restoreExpected] = useState(\n () =>\n typeof window !== 'undefined' &&\n kit.restoreGate.restorePendingInStorage(\n kit.attempts.ATTEMPT_STORAGE_KEY,\n ATTEMPT_TTL_MS,\n kit.drafts.DRAFT_STORAGE_KEY,\n DRAFT_MAX_AGE_MS,\n linked,\n resuming\n )\n );\n const [restoreSettled, setRestoreSettled] = useState(false);\n const restoring = restoreExpected && !restoreSettled;\n const [state, dispatch] = useReducer(wizard.reduce, prefill, (initial) =>\n setup.initialWizardState(\n initial,\n services,\n options.seed.resources ?? [],\n deepLink.seat,\n arrivedAs !== null\n )\n );\n\n /** Children a logged-in parent added from step 1 during this visit. */\n const [addedChildren, setAddedChildren] = useState<BookingFamilyMember[]>([]);\n const family = useMemo(\n () => (guardian === null ? null : [...guardian.family, ...addedChildren]),\n [guardian, addedChildren]\n );\n /**\n * Guest chairs an account change left standing for a seat with services —\n * the previous parent's child unseated, or a sent visit's child nobody could\n * vouch for — which `seatFamily` keeps: they are the visit being booked.\n */\n const [keptSeats, setKeptSeats] = useState<readonly string[]>([]);\n /** Guest seats go when a family appears over them (`seatFamily`). */\n const guestSeated =\n family !== null &&\n state.people.some((person) => wizard.isGuestSeat(person) && !keptSeats.includes(person.key));\n // biome-ignore lint/correctness/useExhaustiveDependencies: re-asked on every step change, as the source did.\n useEffect(() => {\n if (guestSeated) dispatch({ type: 'seatFamily', keep: keptSeats });\n }, [guestSeated, state.step]);\n /**\n * The saved child behind each guest chair a visit was rebuilt into while\n * nobody was logged in (`restoreDraft`), by seat key — names left out — for\n * the next login to claim by id (`reseatFor`).\n */\n const awaitingIds = useRef<Map<string, string> | null>(null);\n\n /**\n * The account the party's saved children were seated under — its e-mail,\n * `null` before anybody. Kept across a lost session on purpose: the seats\n * outlive it, held under the gate, and it is the NEXT login that decides\n * whether they still fit (`reseatFor`).\n */\n const seatedFor = useRef<string | null>(arrivedAs?.email ?? null);\n useEffect(() => {\n if (guardian !== null) seatedFor.current = guardian.email;\n }, [guardian]);\n\n /**\n * A login from the sheet or the gate, before it lands: when it is ANOTHER\n * account than the one the party was seated under — or one whose profile\n * nobody could read — the previous parent's children are not this one's to\n * book. Their seats become guest chairs (the services and the hour stay),\n * and what the old parent added here goes with them. Only a person id says\n * a child is the new parent's too: a child without one is matched by place,\n * name and year, which two families can share. A guest chair rebuilt from a\n * sent visit while nobody was logged in (`awaitingIds`) is this parent's\n * child again when the id is theirs, and stays a blank chair when it is not;\n * a login with no readable profile decides nothing, so the ids wait on for\n * the next one — including those of the seats it just let go, which the\n * next readable login claims back when they are its own. In the same batch as the login, so the form it opens never draws the old\n * names; the chairs it leaves keep their services on a step back.\n */\n function reseatFor(who: BookingGuardian | null) {\n const before = seatedFor.current;\n const now = who?.email ?? null;\n const awaiting = who === null ? null : awaitingIds.current;\n seatedFor.current = now;\n if (who !== null) awaitingIds.current = null;\n const switched = before !== null && before !== now;\n if (!switched && awaiting === null) return;\n if (switched) setAddedChildren([]);\n const theirs = who?.family ?? [];\n const keys: string[] = [];\n const into: Record<string, WizardPerson> = {};\n for (const person of state.people) {\n const id = awaiting?.get(person.key) ?? person.personId;\n const at = theirs.findIndex((child) => id !== undefined && child.personId === id);\n if (at !== -1 && person.personId === id) continue;\n if (at !== -1) into[person.key] = personForChild(theirs[at], at);\n if (\n at !== -1 ||\n (switched &&\n person.key !== SELF_KEY &&\n (person.personId !== undefined || !wizard.isGuestSeat(person)))\n ) {\n keys.push(person.key);\n }\n }\n const action = { type: 'unseatPeople', keys, into } as const;\n const after = wizard.reduce(state, action).people;\n const kept = after.filter(\n (person, index) =>\n wizard.isGuestSeat(person) &&\n (keys.includes(state.people[index].key) || awaiting?.has(person.key))\n );\n setKeptSeats((current) => [...current, ...kept.map((person) => person.key)]);\n if (who === null && switched) {\n const waiting = new Map(awaitingIds.current ?? []);\n after.forEach((person, index) => {\n const before = state.people[index];\n if (keys.includes(before.key) && before.personId !== undefined) {\n waiting.set(person.key, before.personId);\n }\n });\n awaitingIds.current = waiting.size > 0 ? waiting : null;\n }\n if (keys.length > 0) dispatch(action);\n }\n\n /** A stylist a party link named, applied once the family reaches step 3. */\n const partyStylist = useRef<string | null>(\n arrivedAs !== null ? (prefill?.resourceId ?? deepLink.stylist) : null\n );\n const childByKey = useMemo(\n () => new Map((family ?? []).map((child, index) => [personForChild(child, index).key, child])),\n [family]\n );\n\n // Pinned once: the seeded slots and the day strip describe the same days.\n const [fromTs] = useState(() => options.seed.fromTs ?? Date.now());\n const { toTs, days } = useMemo(() => clock.window(fromTs, rangeDays), [clock, fromTs, rangeDays]);\n /** The business date the ages on steps 1 and 2 are read on: the chosen day, else today. */\n const ageDayKey = clock.dayKey(state.startTs ?? fromTs);\n\n /** A child's age on the business day `dayKey`; `null` for a grown-up or an unknown year. */\n function personAge(person: WizardPerson, dayKey: string): AgeRange | null {\n if (person.adult || person.birthYear === undefined) return null;\n return age.ageOnDay(person.birthYear, person.birthMonth, dayKey);\n }\n\n /** «Samme som sist»: the child's last visit, where it is still taken online. */\n const suggestionFor = (person: WizardPerson): BookingSuggestion | null => {\n const serviceId = childByKey.get(person.key)?.lastVisit?.serviceId;\n if (!serviceId) return null;\n const bookable = services.filter((entry) => entry.bookableOnline);\n const last = bookable.find((entry) => entry.id === serviceId);\n if (!last) return null;\n // Age on the appointment day, not the day of the last visit.\n const ageNow = personAge(person, ageDayKey);\n if (age.fitsAge(last, ageNow)) return { service: last };\n const grownUp = age.grownUpEquivalent(last, bookable, ageNow);\n if (!grownUp) return null;\n return {\n service: grownUp,\n note: fill(labels['wizard.suggestion.outgrown'], {\n last: last.name,\n name: person.name ?? labelText(labels['wizard.suggestion.someone']),\n next: grownUp.name,\n }),\n };\n };\n const pendingStylist = useRef(stylistLeftToFill(prefill));\n\n /**\n * Unfiltered openings, keyed by VISIT (`visitKey`): a service id for a\n * one-service visit — which is all the server seed ever holds — and `a+b`\n * for a person having both, whose list is the server's answer for the whole\n * visit rather than either service's own.\n */\n const [slots, setSlots] = useState<Record<string, readonly BookingSlotDto[]>>(\n () => options.seed.slots ?? {}\n );\n const [slotsFailed, setSlotsFailed] = useState(false);\n const [resources, setResources] = useState<BookingResourceDto[]>(\n () => options.seed.resources ?? []\n );\n /** Whether the stylist list has been answered at all — seeded, fetched or failed. */\n const [resourcesKnown, setResourcesKnown] = useState(() => options.seed.resources != null);\n const [seededNextAvailable, setSeededNextAvailable] = useState(() =>\n options.seed.resources ? (options.seed.nextAvailable ?? {}) : {}\n );\n const [seededSchedule] = useState(() => options.seed.schedules ?? {});\n /** The last «next available» answer, and for WHICH visit (its key). */\n const [fetchedNextAvailable, setFetchedNextAvailable] = useState<{\n key: string;\n times: Record<string, number>;\n } | null>(null);\n\n const [submitting, setSubmitting] = useState(false);\n /**\n * The attempt (and its nonce), null until the browser has it: storage does\n * not exist during the server render, and reading it in an initialiser\n * would give the two sides different trees.\n */\n const [attempt, setAttempt] = useState<BookingAttempt | null>(null);\n /** The submission whose answer is still unknown; a retry resends THAT body. */\n const [pendingAttempt, setPendingAttempt] = useState<NonNullable<\n BookingAttempt['pending']\n > | null>(null);\n const submissionNonce = attempt?.nonce;\n /** The instant that was taken while the visitor was filling in the form. */\n const [takenSlotTs, setTakenSlotTs] = useState<number | null>(null);\n /** Set once, by a successful submission — one entry per line item. */\n const [confirmed, setConfirmed] = useState<BookingConfirmation | null>(null);\n\n const resumed = useRef(false);\n /** The whole «book again» link, kept to apply AGAIN after a refused replay. */\n const rebookLink = useRef<WizardPrefill | null>(prefill);\n const latestResources = useRef(resources);\n useEffect(() => {\n latestResources.current = resources;\n }, [resources]);\n\n // The child's name a «book again» tap stashed for this tab, taken once.\n const applyRebookWho = () => {\n const who = kit.rebook.takeRebookWho();\n if (who === null) return;\n rebookLink.current = { ...(rebookLink.current ?? {}), who };\n // A known parent: seat THEIR child by id, for a name exactly one child has.\n const known = guardian?.family ?? [];\n const named = known.filter((child) => child.name === who);\n if (named.length === 1) {\n const service = state.items[0]?.service ?? state.pendingService;\n dispatch({\n type: 'choosePeople',\n people: [personForChild(named[0], known.indexOf(named[0]))],\n services: [service],\n });\n if (service !== null) dispatch({ type: 'pickServiceFor', index: 0, service });\n return;\n }\n dispatch({ type: 'prefill', prefill: { who }, catalogue: prefillCatalogue(services, []) });\n };\n\n /** Put the «book again» link back onto a wizard that has just started over, once. */\n const restartFromLink = () => {\n const link = rebookLink.current;\n if (link === null) return;\n rebookLink.current = null;\n const current = latestResources.current;\n if (guardian !== null) {\n const held = setup.heldLink(wizard.initialState(), link, services, current);\n dispatch({ type: 'holdService', service: held.pendingService });\n partyStylist.current = link.resourceId ?? null;\n return;\n }\n dispatch({ type: 'prefill', prefill: link, catalogue: prefillCatalogue(services, current) });\n if (current.length === 0) pendingStylist.current = stylistLeftToFill(link);\n };\n\n /** The stylists a restored slot was booked with, until the catalogue can vouch for them. */\n const restoredStylists = useRef<string[] | null>(null);\n\n /**\n * A logged-in parent's saved children back in their chairs: each draft line\n * whose stored `personId` is a child of THIS guardian's family is reseated as\n * that child (`choosePeople`, then its service again), so it is submitted\n * with `booked_for_person_id` as it would have been without the round trip.\n * An id the family does not have — another parent's, a child removed since,\n * a hand-edited jar — is ignored, and its line restores as a name and a year\n * exactly as a draft without ids does. Returns the reseated line indexes.\n */\n const reseatFamily = (draft: WizardDraft, lines: BookingServiceDto[]): Set<number> => {\n const reseated = new Set<number>();\n if (family === null) return reseated;\n const keys = new Set<string>();\n // The seats `pickService` / `addService` just built (`peopleFor`), for the\n // lines that are not reseated. A parent known at mount starts with nobody\n // seated (`initialWizardState`), so every one of them is a guest chair.\n const people = draft.items.map((item, index) => {\n const built = wizard.guestChild(index + 1);\n const at = item.personId ? family.findIndex((child) => child.personId === item.personId) : -1;\n const child = at === -1 ? null : personForChild(family[at], at);\n if (child === null || keys.has(child.key)) {\n keys.add(built.key);\n return built;\n }\n keys.add(child.key);\n reseated.add(index);\n return child;\n });\n if (reseated.size === 0) return reseated;\n dispatch({ type: 'choosePeople', people });\n for (const index of reseated) {\n dispatch({ type: 'pickServiceFor', index, service: lines[index] });\n }\n return reseated;\n };\n\n /**\n * Each draft line's extras back on it, through `toggleServiceFor` like a tap.\n * Returns whether EVERY extra came back. The machine's refusals are checked\n * here first rather than read off its state, which a dispatch does not hand\n * back: an id the catalogue no longer books, one already on the line (a\n * toggle would take it OFF again), one past `maxServicesPerPerson`, or one\n * more taker than the service's `maxPerBooking` — each is skipped, as an\n * unknown main service is, and makes the answer `false`.\n */\n const restoreExtras = (\n draft: WizardDraft,\n lines: BookingServiceDto[],\n bookable: ReadonlyMap<string, BookingServiceDto>\n ): boolean => {\n let whole = true;\n const lists = lines.map((service) => [service]);\n draft.items.forEach((item, index) => {\n for (const id of item.extraServiceIds ?? []) {\n const service = bookable.get(id);\n const list = lists[index];\n const takers = lists.filter((other) => other.some((entry) => entry.id === id)).length;\n if (\n service === undefined ||\n list.some((entry) => entry.id === id) ||\n list.length >= config.party.maxServicesPerPerson ||\n takers + 1 > service.maxPerBooking\n ) {\n whole = false;\n continue;\n }\n list.push(service);\n dispatch({ type: 'toggleServiceFor', index, service });\n }\n });\n return whole;\n };\n\n /**\n * The booking a Vipps login interrupted, rebuilt through the machine's own\n * actions — so it obeys every rule a tapped one does — and trusting nothing\n * beyond the catalogue.\n */\n const restoreDraft = (draft: WizardDraft) => {\n const bookable = new Map(\n services.filter((service) => service.bookableOnline).map((service) => [service.id, service])\n );\n const resolved = draft.items.map((item) => bookable.get(item.serviceId));\n const [first, ...rest] = resolved;\n if (first === undefined || resolved.some((service) => service === undefined)) return;\n\n // Nobody to vouch for a saved child's id: its chair waits for the next login.\n if (family === null) {\n const ids = new Map<string, string>();\n draft.items.forEach((item, index) => {\n if (item.personId && !item.adult) ids.set(wizard.guestChild(index + 1).key, item.personId);\n });\n awaitingIds.current = ids.size > 0 ? ids : null;\n }\n // A guest is reseated as they left; a family's seats are its own children.\n if (family === null && draft.items.every((item) => typeof item.adult === 'boolean')) {\n dispatch({\n type: 'choosePeople',\n people: draft.items.map((item, index) =>\n item.adult ? { key: 'adult', adult: true } : wizard.guestChild(index + 1)\n ),\n advance: true,\n });\n }\n dispatch({ type: 'pickService', service: first });\n // Every line resolved (checked above), so `rest` holds services only.\n for (const service of rest as BookingServiceDto[]) dispatch({ type: 'addService', service });\n const reseated = reseatFamily(draft, resolved as BookingServiceDto[]);\n // After the seats are final (a reseat moves people, and their extras with\n // them) and before the stylist, so no toggle is refused for a preference\n // the draft is about to restore anyway.\n const wholeVisits = restoreExtras(draft, resolved as BookingServiceDto[], bookable);\n // Before the stylist: `setPartyMode('parallel')` drops the preference.\n if (draft.partyMode === 'parallel') dispatch({ type: 'setPartyMode', mode: 'parallel' });\n dispatch({ type: 'pickResource', resourceId: draft.resourceId });\n draft.items.forEach((item, index) => {\n // A reseated child's name and year are the family's own, already on the line.\n if (reseated.has(index)) return;\n if (item.bookedForName !== null) {\n dispatch({\n type: 'setItemField',\n index,\n field: 'bookedForName',\n value: item.bookedForName,\n });\n }\n if (item.bookedForBirthYear !== null) {\n dispatch({\n type: 'setItemField',\n index,\n field: 'bookedForBirthYear',\n value: item.bookedForBirthYear,\n });\n }\n });\n\n // The hour was chosen for the visits as they were. One that came back\n // shorter — an extra retired since, or one the rules now refuse — would be\n // submitted at a time that was only ever free for the longer visit (or\n // offered for the shorter one by nobody), so the visitor picks again.\n if (draft.startTs === null || !wholeVisits) return;\n if (draft.partyResourceIds !== null && draft.partyResourceIds.length === resolved.length) {\n restoredStylists.current = draft.partyResourceIds;\n dispatch({\n type: 'pickPartySlot',\n startTs: draft.startTs,\n resourceIds: draft.partyResourceIds,\n mode: draft.partyMode,\n });\n return;\n }\n restoredStylists.current =\n draft.resolvedResourceId === null ? null : [draft.resolvedResourceId];\n dispatch({ type: 'pickSlot', startTs: draft.startTs, resourceId: draft.resolvedResourceId });\n };\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: once per mount; the ref, not the dependencies, makes it so.\n useEffect(() => {\n if (resumed.current) return;\n resumed.current = true;\n const replay = restoreFromStorage();\n if (replay === null) setRestoreSettled(true);\n else void replay.finally(() => setRestoreSettled(true));\n }, []);\n\n /** What the mount effect restores, in order; the replay when it started one. */\n function restoreFromStorage(): Promise<void> | null {\n let stored = kit.attempts.readAttempt();\n // A fresh link beats a remembered confirmation (not a pending attempt).\n if (linked && stored.confirmed !== undefined && stored.pending === undefined) {\n kit.attempts.clearAttempt();\n stored = kit.attempts.readAttempt();\n }\n // A remembered confirmation that is not a whole one is no confirmation.\n if (\n stored.confirmed !== undefined &&\n !isCompleteConfirmation(stored.confirmed.bookings, stored.confirmed.submitted.items.length)\n ) {\n kit.attempts.clearAttempt();\n stored = kit.attempts.readAttempt();\n }\n setAttempt(stored);\n applyRebookWho();\n if (stored.confirmed !== undefined) {\n setConfirmed(stored.confirmed);\n return null;\n }\n if (stored.pending === undefined) {\n // Nothing owed an answer: the one moment a Vipps round trip gets its draft back.\n if (resuming) {\n const draft = kit.drafts.takeDraft();\n if (draft !== null) restoreDraft(draft);\n }\n return null;\n }\n // The OLD attempt comes first, and the link waits for its answer.\n if (rebookLink.current !== null) {\n pendingStylist.current = null;\n dispatch({ type: 'startOver' });\n }\n setPendingAttempt(stored.pending);\n return send(stored.pending.submission, stored.pending.submitted, { replay: true });\n }\n\n /** The draft, kept current on every answer rather than written on the Vipps tap. */\n useEffect(() => {\n if (confirmed !== null || state.items.length === 0) {\n // A booked visit's chairs wait for nobody.\n if (confirmed !== null) awaitingIds.current = null;\n kit.drafts.clearDraft();\n return;\n }\n kit.drafts.stashDraft({\n // Nobody logged in — the session ran out under the gate — and a line\n // still naming a saved child: kept by its id ALONE. The login the draft\n // waits for decides: that parent's child is reseated by id, anybody\n // else gets a guest chair rather than the previous parent's child's name.\n // A chair still waiting for its child's parent (`awaitingIds`) keeps the id too.\n items: state.items.map((item, index) => {\n const line = draftItem(item, guardian === null);\n const waiting = awaitingIds.current?.get(state.people[index].key) ?? null;\n return { ...line, personId: line.personId ?? waiting };\n }),\n resourceId: state.resourceId,\n partyMode: state.partyMode,\n startTs: state.startTs,\n resolvedResourceId: state.resolvedResourceId,\n partyResourceIds: state.partyResourceIds,\n });\n }, [\n kit,\n confirmed,\n guardian,\n state.items,\n state.people,\n state.resourceId,\n state.partyMode,\n state.startTs,\n state.resolvedResourceId,\n state.partyResourceIds,\n ]);\n\n /**\n * The fields a logged-in parent's profile wrote, by value, for as long as it\n * still owns them — and only those: a field the visitor filled or changed is\n * never in here, whoever logs in next.\n */\n const guardianFilled = useRef<{ email: string; owns: ProfileOwned } | null>(null);\n /** A live mirror of the contact fields, for the prefill below. */\n const latestContact = useRef(state.contact);\n useEffect(() => {\n latestContact.current = state.contact;\n // A field that ever differs from what the profile wrote is the visitor's\n // from then on — typed back to the same value included — so neither a\n // switch nor an unreadable login below takes it.\n const owns = guardianFilled.current?.owns;\n if (owns === undefined) return;\n for (const field of CONTACT_FIELDS) {\n if (field in owns && state.contact[field] !== owns[field]) delete owns[field];\n }\n });\n\n /**\n * The logged-in parent's details into the fields — once per parent. A field\n * the previous parent's profile still owns is theirs to replace: with this\n * parent's value, or emptied when this profile has none (a shared device\n * must not keep the last account's phone). A blank field is filled. A field\n * the visitor owns is never touched, and stays theirs.\n */\n useEffect(() => {\n const contact = latestContact.current;\n const previous = guardianFilled.current?.owns ?? {};\n if (guardian === null) {\n // A login with no readable profile is somebody else until shown\n // otherwise: what the previous parent's profile wrote goes, and what\n // the visitor typed stays. (No login at all — the session ran out —\n // keeps the fields for the same parent's next login.)\n if (signedIn === null || guardianFilled.current === null) return;\n guardianFilled.current = null;\n for (const field of CONTACT_FIELDS) {\n if (field in previous) dispatch({ type: 'setContact', field, value: '' });\n }\n return;\n }\n if (guardianFilled.current?.email === guardian.email) return;\n const profile = {\n name: [guardian.firstName, guardian.lastName].filter(Boolean).join(' ').trim(),\n phone: guardian.phone === null ? '' : phone.nationalDigits(guardian.phone),\n email: guardian.email,\n };\n const owns: ProfileOwned = {};\n guardianFilled.current = { email: guardian.email, owns };\n\n for (const field of CONTACT_FIELDS) {\n const value = profile[field];\n const theirs = field in previous;\n if (!theirs && contact[field].trim() !== '') continue;\n if (value !== '') owns[field] = value;\n if (value !== '' || theirs) dispatch({ type: 'setContact', field, value });\n }\n }, [guardian, signedIn, phone]);\n\n // One entry per distinct VISIT, not per service: a person having cut and wash\n // is one question to the engine (one stylist, back to back), and the cut's\n // own list would offer somebody who cannot wash.\n const basketVisits = useMemo(() => slotKeysFor(state.items), [state.items]);\n // A string, so an effect keyed on it does not refetch on every keystroke.\n // `|` between visits, as `+` is already inside one.\n const basketKey = basketVisits.join('|');\n const primaryServiceId = state.items[0]?.service.id ?? null;\n /**\n * The first person's whole visit, by key — what the stylists' «neste ledige»\n * and the business's hours are asked for. Equal to `primaryServiceId` for a\n * one-service visit, so the seed (keyed by service id, and only ever for one\n * service) answers exactly the visits it can, and a visit with extras is\n * fetched instead of being read off its first service's openings.\n */\n const primaryVisit = state.items[0] ? visitKey(state.items[0]) : null;\n\n const nextAvailableTs = useMemo<Record<string, number>>(() => {\n if (primaryVisit === null) return {};\n const seeded = seededNextAvailable[primaryVisit];\n if (seeded) return seeded;\n return fetchedNextAvailable?.key === primaryVisit ? fetchedNextAvailable.times : {};\n }, [primaryVisit, seededNextAvailable, fetchedNextAvailable]);\n\n const missingKey = basketVisits.filter((key) => !(key in slots)).join('|');\n /** Bumped to ask for the openings again after a failure. */\n const [slotsAttempt, setSlotsAttempt] = useState(0);\n const api = config.paths.api;\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: `slotsAttempt` is the retry trigger.\n useEffect(() => {\n if (missingKey === '') return;\n const wanted = missingKey.split('|');\n let cancelled = false;\n setSlotsFailed(false);\n\n (async () => {\n try {\n const fetched = await Promise.all(\n wanted.map(async (key) => {\n const response = await fetch(`${api}/availability?${windowQuery(key, fromTs, toTs)}`);\n if (!response.ok) throw new Error(`availability ${response.status}`);\n const body = (await response.json()) as { slots?: BookingSlotDto[] };\n // Stored under the WHOLE visit's key — the key it is read back by.\n return [key, body.slots ?? []] as const;\n })\n );\n if (cancelled) return;\n setSlots((previous) => ({ ...previous, ...Object.fromEntries(fetched) }));\n } catch {\n // Nothing is cached on a failure; the step says so rather than «full».\n if (!cancelled) setSlotsFailed(true);\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [api, missingKey, fromTs, toTs, slotsAttempt]);\n\n useEffect(() => {\n if (primaryVisit === null) return;\n if (primaryVisit in seededNextAvailable) return;\n let cancelled = false;\n\n (async () => {\n try {\n const response = await fetch(`${api}/resources?${windowQuery(primaryVisit, fromTs, toTs)}`);\n if (!response.ok) throw new Error(`resources ${response.status}`);\n const body = (await response.json()) as {\n resources?: BookingResourceDto[];\n nextAvailableTs?: Record<string, number>;\n };\n if (cancelled) return;\n setResources(body.resources ?? []);\n setResourcesKnown(true);\n setFetchedNextAvailable({ key: primaryVisit, times: body.nextAvailableTs ?? {} });\n } catch {\n // Answered «none» FOR THIS VISIT; the stylist LIST is kept.\n if (!cancelled) {\n setResourcesKnown(true);\n setFetchedNextAvailable({ key: primaryVisit, times: {} });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [api, primaryVisit, fromTs, toTs, seededNextAvailable]);\n\n /**\n * The stylist half of a «book again» link, once a list for its service has\n * landed. Compared by VISIT: the link named one service, and a visitor who\n * has since added a wash to it is asking a different question — the list now\n * in hand answers the longer visit, not the one the link vouched for.\n */\n useEffect(() => {\n const pending = pendingStylist.current;\n if (pending && primaryVisit !== null && primaryVisit !== pending.serviceId) {\n pendingStylist.current = null;\n return;\n }\n if (!canFinishStylist(pending, resources, primaryVisit)) return;\n pendingStylist.current = null;\n dispatch({\n type: 'prefill',\n prefill: pending,\n catalogue: prefillCatalogue(services, resources),\n });\n }, [resources, primaryVisit, services]);\n\n /** A party link's stylist, once the family has its services and is on step 3. */\n useEffect(() => {\n const wanted = partyStylist.current;\n if (wanted === null || state.step !== 'when' || primaryServiceId === null) return;\n if (resources.length === 0) return;\n partyStylist.current = null;\n dispatch({\n type: 'prefill',\n prefill: { serviceId: primaryServiceId, resourceId: wanted },\n catalogue: prefillCatalogue(services, resources),\n });\n }, [state.step, primaryServiceId, resources, services]);\n\n /** A restored slot whose stylist has left the roster, dropped before it is submitted. */\n useEffect(() => {\n const restored = restoredStylists.current;\n if (restored === null || resources.length === 0 || primaryServiceId === null) return;\n restoredStylists.current = null;\n const known = new Set(resources.map((resource) => resource.id));\n if (restored.every((id) => known.has(id))) return;\n dispatch({ type: 'pickResource', resourceId: null });\n }, [resources, primaryServiceId]);\n\n /**\n * The open dates over the window, for the first person's VISIT (cutoffs are\n * per service, and `visitTailMinutes` assumes the answer was for item 0's\n * whole visit — see the machine).\n *\n * The seed is the SALON's week, which is what «first available» draws. A named\n * stylist works a different week, so their hours are fetched with `resource_id`\n * and the seed is not allowed to answer for them — a seeded salon week would\n * otherwise paint every open day as theirs.\n */\n const [fetchedSchedule, setFetchedSchedule] = useState<{\n key: string;\n days: BookingDayDto[];\n } | null>(null);\n const [scheduleFailedFor, setScheduleFailedFor] = useState<string | null>(null);\n // The visit, plus the stylist when one is named. The two answers must not\n // share a key: a late salon response must not land on a stylist's calendar.\n const scheduleKey =\n primaryVisit === null\n ? null\n : state.resourceId === null\n ? primaryVisit\n : `${primaryVisit}#${state.resourceId}`;\n\n // `null`, not `[]`, until this service's hours are in hand: «we do not know».\n // A named stylist's own week replaces the salon's when it arrives. Until then\n // the salon answer stays, so the time step does not unmount mid-tap. A failed\n // read is not «still waiting»: their hours are unknown, and the salon week\n // must not stay on screen as theirs.\n const openDays = useMemo<BookingDayDto[] | null>(() => {\n if (primaryVisit === null || scheduleKey === null) return null;\n if (fetchedSchedule?.key === scheduleKey) return fetchedSchedule.days;\n if (state.resourceId === null) {\n return seededSchedule[primaryVisit] ?? null;\n }\n if (scheduleFailedFor === scheduleKey) return null;\n return (\n seededSchedule[primaryVisit] ??\n (fetchedSchedule?.key === primaryVisit ? fetchedSchedule.days : null)\n );\n }, [\n primaryVisit,\n state.resourceId,\n scheduleKey,\n seededSchedule,\n fetchedSchedule,\n scheduleFailedFor,\n ]);\n\n useEffect(() => {\n if (primaryVisit === null || scheduleKey === null) return;\n // The seed already answers «first available». A named stylist always asks.\n if (state.resourceId === null && primaryVisit in seededSchedule) return;\n let cancelled = false;\n const key = scheduleKey;\n const resourceId = state.resourceId;\n // A new read is not the previous failure. Leaving that flag set would settle\n // the retry before it has answered.\n setScheduleFailedFor((current) => (current === key ? null : current));\n\n (async () => {\n try {\n const response = await fetch(\n `${api}/schedule?${windowQuery(primaryVisit, fromTs, toTs, resourceId)}`\n );\n if (!response.ok) throw new Error(`schedule ${response.status}`);\n const body = (await response.json()) as { days?: BookingDayDto[] };\n if (cancelled) return;\n setFetchedSchedule({ key, days: body.days ?? [] });\n } catch {\n // Left `null`: the hours are unknown, which is not the same as «closed».\n // Keyed like the success, so one stylist's failure does not settle the next.\n if (!cancelled) {\n setScheduleFailedFor(key);\n // The previous success for this stylist is stale once this read failed.\n setFetchedSchedule((current) => (current?.key === key ? null : current));\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [api, primaryVisit, fromTs, toTs, seededSchedule, state.resourceId, scheduleKey]);\n\n /** A stylist's display name for an id; `''` from Medal reads as none. */\n const resolveStylistName = useMemo(\n () => (resourceId: string) => {\n const name = resources.find((resource) => resource.id === resourceId)?.name;\n return name ? stylistDisplayName(name) || null : null;\n },\n [resources]\n );\n const chosenStylistName = state.resourceId === null ? null : resolveStylistName(state.resourceId);\n const chosenStylistServiceIds = serviceIdsOf(resources, state.resourceId);\n\n /**\n * A NAMED stylist restored before the list landed who turns out not to cover\n * this basket: «first available» instead, and said so — only on step 3, and\n * only against a list that actually arrived.\n */\n const [stylistNotice, setStylistNotice] = useState<string | null>(null);\n useEffect(() => {\n if (!resourcesKnown || resources.length === 0) return;\n if (state.step !== 'when' || state.resourceId === null) return;\n const chosen = resources.find((resource) => resource.id === state.resourceId);\n // Every service of every visit: a stylist who cuts but does not wash cannot\n // take the person having both.\n const covers =\n chosen !== undefined &&\n state.items.every((item) =>\n visitServicesOf(item).every((service) => chosen.serviceIds.includes(service.id))\n );\n if (covers) return;\n dispatch({ type: 'pickResource', resourceId: null });\n setStylistNotice(labelText(labels['wizard.stylistGone']));\n }, [labels, resourcesKnown, resources, state.step, state.resourceId, state.items]);\n\n /** The open dates with the cutoffs pulled back to fit the WHOLE visit (the safe direction). */\n const openDaysForVisit = useMemo(() => {\n if (openDays === null) return null;\n const extraMs = wizard.visitTailMinutes(state.items, state.partyMode) * 60_000;\n if (extraMs <= 0) return openDays;\n return openDays.map((day) => ({\n ...day,\n lastStartTs: day.lastStartTs === null ? null : day.lastStartTs - extraMs,\n }));\n }, [wizard, openDays, state.items, state.partyMode]);\n\n const party = wizard.showsPartyMode(state);\n const haveEveryService = basketVisits.every((key) => key in slots);\n const scheduleSettled =\n openDays !== null || (scheduleKey !== null && scheduleFailedFor === scheduleKey);\n const nextAvailableLoading = party\n ? !haveEveryService && !slotsFailed\n : primaryVisit !== null &&\n !(primaryVisit in seededNextAvailable) &&\n fetchedNextAvailable?.key !== primaryVisit;\n\n /** The basket's openings, unfiltered — the parallel search needs two stylists. */\n const basketSlots: SlotsByService = useMemo(() => {\n const out: Record<string, readonly BookingSlotDto[]> = {};\n for (const id of basketKey === '' ? [] : basketKey.split('|')) out[id] = slots[id] ?? [];\n return out;\n }, [basketKey, slots]);\n\n const preferredSlots: SlotsByService = useMemo(() => {\n const preferred = state.resourceId;\n if (preferred === null) return basketSlots;\n return Object.fromEntries(\n Object.entries(basketSlots).map(([id, list]) => [id, onlyResource(list, preferred)])\n );\n }, [basketSlots, state.resourceId]);\n\n const partyNextAvailableTs = useMemo<Record<string, number>>(\n () =>\n party && state.partyMode === 'sequential' && haveEveryService\n ? kit.partySlots.firstPartyStartPerResource(state.items, basketSlots)\n : {},\n [kit, party, state.partyMode, haveEveryService, state.items, basketSlots]\n );\n\n const partySlots = useMemo(\n () =>\n party ? kit.partySlots.findPartySlots(state.items, preferredSlots, state.partyMode) : [],\n [kit, party, state.items, preferredSlots, state.partyMode]\n );\n\n /** The parallel visits, for the day with nothing back to back — over UNFILTERED slots. */\n const partyAlternatives = useMemo(\n () =>\n party && state.partyMode === 'sequential'\n ? kit.partySlots.findPartySlots(state.items, basketSlots, 'parallel')\n : [],\n [kit, party, state.items, basketSlots, state.partyMode]\n );\n\n const singleSlots = useMemo(() => {\n if (party || primaryVisit === null) return [];\n const list = slots[primaryVisit] ?? [];\n return state.resourceId === null ? [...list] : onlyResource(list, state.resourceId);\n }, [party, primaryVisit, slots, state.resourceId]);\n\n /** Create a logged-in parent's new child through the site's portal route. */\n async function saveChild(child: {\n name: string;\n birthYear: number;\n birthMonth?: number;\n notes?: string;\n }): Promise<{ ok: true; child: BookingFamilyMember } | { ok: false; message: string }> {\n const copy: Record<string, string> = {\n invalid: labelText(labels['wizard.addChild.invalid']),\n session: labelText(labels['wizard.addChild.session']),\n throttled: labelText(labels['wizard.addChild.throttled']),\n };\n const unreachable = labelText(labels['wizard.addChild.unreachable']);\n try {\n const response = await fetch(`${config.paths.portalApi}/persons`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(child),\n cache: 'no-store',\n credentials: 'same-origin',\n });\n const body = (await response.json().catch(() => null)) as {\n ok?: boolean;\n child?: BookingFamilyMember;\n reason?: string;\n message?: string;\n } | null;\n if (body?.ok === true && body.child) return { ok: true, child: body.child };\n return { ok: false, message: body?.message ?? copy[body?.reason ?? ''] ?? unreachable };\n } catch {\n return { ok: false, message: unreachable };\n }\n }\n\n /**\n * Step 1's «+ add child». A logged-in parent's child is created first and\n * ticked once it exists; a guest's lives in this booking.\n */\n async function addChild(child: {\n name: string;\n birthYear: number;\n birthMonth?: number;\n notes?: string;\n }): Promise<{ ok: true } | { ok: false; message: string }> {\n const named = state.people.filter((person) => person.key.startsWith('new:'));\n const seated = guardian === null ? named : state.people;\n if (seated.length >= wizard.maxPeople) {\n return { ok: false, message: fill(labels['who.family.limit'], { max: wizard.maxPeople }) };\n }\n if (guardian === null) {\n // Out of the sheet's transition first: seated inside it, the child would\n // land a render AFTER the sheet that added them has closed.\n await Promise.resolve();\n const person: WizardPerson = {\n key: `new:${Date.now()}:${named.length}`,\n name: child.name,\n birthYear: child.birthYear,\n ...(child.birthMonth === undefined ? {} : { birthMonth: child.birthMonth }),\n };\n // A named child replaces the count chips.\n dispatch({ type: 'choosePeople', people: [...named, person], advance: false });\n return { ok: true };\n }\n const saved = await saveChild(child);\n if (!saved.ok) return saved;\n setAddedChildren((current) => [...current, saved.child]);\n // Against the party as it is NOW, placed after every child the list holds.\n // A guardian is known here, so `family` is their list.\n const known = family as BookingFamilyMember[];\n dispatch({ type: 'addPerson', person: personForChild(saved.child, known.length) });\n return { ok: true };\n }\n\n async function submit(submission: BookingSubmission) {\n // An unresolved attempt is resent VERBATIM: the key hashes nonce AND body.\n if (pendingAttempt !== null) {\n await send(pendingAttempt.submission, pendingAttempt.submitted, { pending: true });\n return;\n }\n // Read BEFORE the await: the card describes what left the browser.\n const resourceIds = wizard.itemResourceIds(state);\n const extended = withExtraServices(submission, state.items);\n const body =\n extended !== null && config.account.required\n ? withSessionPersons(extended, state.items)\n : extended;\n if (body === null) {\n dispatch({ type: 'submitFailed', error: 'invalidInput' });\n return;\n }\n await send(body, {\n items: state.items,\n startTs: state.startTs ?? 0,\n partyMode: state.partyMode,\n resourceIds,\n stylistNames: resourceIds.map((id) => (id === null ? null : resolveStylistName(id))),\n });\n }\n\n /** The POST, and everything that can come back from it (also the replay's). */\n async function send(\n submission: BookingSubmission,\n submitted: SubmittedVisit,\n sendOptions?: { replay?: boolean; pending?: boolean }\n ) {\n setSubmitting(true);\n const current = attempt ?? kit.attempts.readAttempt();\n if (attempt === null) setAttempt(current);\n // Written before the request leaves: the body must be there if the answer is not.\n kit.attempts.rememberPending(current, { submission, submitted });\n setPendingAttempt({ submission, submitted });\n if (sendOptions?.replay !== true && sendOptions?.pending !== true) rebookLink.current = null;\n try {\n const response = await fetch(`${api}/create`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...submission, window: { fromTs, toTs } }),\n ...(sendOptions?.replay === true ? { signal: AbortSignal.timeout(REPLAY_TIMEOUT_MS) } : {}),\n });\n const payload = (await response.json().catch(() => null)) as {\n error?: string;\n bookings?: Array<{ id: string; manageToken?: string }>;\n freshSlots?: Record<string, BookingSlotDto[]>;\n } | null;\n\n if (!response.ok) {\n // `upstreamError` is «we still do not know»: the attempt survives it.\n const definite = payload?.error !== undefined && payload.error !== 'upstreamError';\n // `account.required` and the session gone: the login again, not an\n // error. Nothing else moves — the wizard keeps the hour, and «Bekreft»\n // turns back into the login over it.\n const sessionGone = payload?.error === 'accountRequired';\n if (definite) {\n // The session gone keeps the NONCE (and drops only the body): an\n // earlier try of this submission may have booked with its answer\n // lost, and the same parent's resend after the login — same nonce,\n // same visit — derives the same key and meets it rather than booking\n // twice. The body is not kept: whoever logs in next is asked again.\n if (sessionGone) kit.attempts.releasePending(current);\n else kit.attempts.clearAttempt();\n setPendingAttempt(null);\n }\n // A RESUMED replay (or the resend of one while a link waits) has no\n // basket to rescue: a clean wizard, or the link it came by.\n const wasResumed =\n sendOptions?.replay === true ||\n (sendOptions?.pending === true && rebookLink.current !== null);\n if (sessionGone) {\n setSignedIn(null);\n setArrivalExpired(true);\n setSessionLost(true);\n // Before the resumed reset below: the gate holds the slot, a clean\n // wizard would drop it. A replay or a resend of a pending attempt\n // sent a visit the machine need not hold (never hydrated; or the\n // parent stepped back and picked another hour while the attempt\n // waited, and «Bekreft» resent the old one verbatim), so the visit\n // it SENT is rebuilt under the gate — the one whose key a resend\n // after the login has to meet. Any other send is the machine's own.\n if (options.loginOffered !== false) {\n if (sendOptions?.replay === true || sendOptions?.pending === true) {\n rebuildSubmitted(submitted);\n }\n return;\n }\n }\n if (wasResumed) {\n if (definite) {\n forgetChairs();\n dispatch({ type: 'startOver' });\n restartFromLink();\n } else dispatch({ type: 'submitFailed', error: 'upstreamError' });\n return;\n }\n if (sessionGone) {\n // Nowhere to log in again (no login actions): an error, not a dead «Bekreft».\n dispatch({ type: 'submitFailed', error: 'upstreamError' });\n return;\n }\n if (payload?.error === 'slotTaken') {\n // From the SUBMITTED visit: after a reload there is no live basket.\n setTakenSlotTs(submitted.startTs);\n // The cache goes too, or the time step reopens on the SAME chip:\n // replaced by the 409's live `freshSlots`, else evicted for a re-read.\n // By VISIT, as the server keys `freshSlots` and the wizard its cache.\n const lost = new Set(submitted.items.map((item) => visitKey(item)));\n const fresh = freshSlotsOf(payload?.freshSlots, lost);\n setSlots((previous) => {\n const next = { ...previous };\n for (const key of lost) {\n if (key in fresh) next[key] = fresh[key];\n else delete next[key];\n }\n return next;\n });\n // «Next available» was read out of the same stale openings.\n const canSeed = resources.length > 0;\n setSeededNextAvailable((previous) => ({\n ...Object.fromEntries(Object.entries(previous).filter(([id]) => !lost.has(id))),\n ...(canSeed\n ? Object.fromEntries(\n Object.entries(fresh).map(([id, list]) => [id, firstOpeningPerResource(list)])\n )\n : {}),\n }));\n setFetchedNextAvailable((previous) =>\n previous !== null && lost.has(previous.key) ? null : previous\n );\n kit.attempts.clearAttempt();\n dispatch({ type: 'slotTaken' });\n return;\n }\n if (payload?.error !== undefined && payload.error !== 'upstreamError') {\n kit.attempts.clearAttempt();\n }\n dispatch({ type: 'submitFailed', error: asSubmitFailure(payload?.error) });\n return;\n }\n\n const bookings = payload?.bookings ?? [];\n if (!isCompleteConfirmation(bookings, submitted.items.length)) {\n // A 201 with no booking in it is not a booking, and one short of the\n // basket is not a confirmation: the attempt stays pending.\n dispatch({ type: 'submitFailed', error: 'upstreamError' });\n return;\n }\n // Every token: each is returned in plaintext exactly once.\n const confirmedBookings = bookings.map((booking) => ({\n id: booking.id,\n manageHref: booking.manageToken ? kit.paths.managePath(booking.manageToken) : null,\n }));\n setConfirmed({ bookings: confirmedBookings, submitted });\n // Kept, not cleared, so a refresh cannot take the manage links with it.\n kit.attempts.rememberConfirmed(current, { bookings: confirmedBookings, submitted });\n setPendingAttempt(null);\n } catch {\n dispatch({ type: 'submitFailed', error: 'upstreamError' });\n } finally {\n setSubmitting(false);\n }\n }\n\n /**\n * The visit a refused replay sent, back in the machine as a draft would be\n * — the hour on «Bekreft» — in place of the link that was waiting on it.\n */\n function rebuildSubmitted(submitted: SubmittedVisit) {\n rebookLink.current = null;\n const { items, resourceIds } = submitted;\n // Nobody is logged in now: a saved child goes back by id alone (`draftItem`).\n restoreDraft({\n items: items.map((item) => draftItem(item, true)),\n resourceId: null,\n partyMode: submitted.partyMode,\n startTs: submitted.startTs,\n resolvedResourceId: resourceIds[0] ?? null,\n partyResourceIds:\n items.length > 1 && resourceIds.every((id) => id !== null)\n ? (resourceIds as string[])\n : null,\n savedAt: Date.now(),\n });\n }\n\n /** «Book again» on the confirmation: a clean wizard with a fresh attempt. */\n function startOver() {\n kit.attempts.clearAttempt();\n setAttempt(kit.attempts.readAttempt());\n setPendingAttempt(null);\n // The openings were read before the booking just made: drop them.\n setSlots({});\n setSlotsFailed(false);\n setConfirmed(null);\n setTakenSlotTs(null);\n forgetChairs();\n dispatch({ type: 'startOver' });\n restartFromLink();\n }\n\n /** A new visit: no chair of the last one is kept or waits for its parent. */\n function forgetChairs() {\n setKeptSeats([]);\n awaitingIds.current = null;\n }\n\n /** A service tap on step 2 for a party of one — the first carries the link's stylist. */\n function pickService(service: BookingServiceDto) {\n const first = deepLinkPending.current && state.items.length === 0;\n deepLinkPending.current = false;\n if (first && deepLink.stylist && resources.length === 0) {\n pendingStylist.current = { serviceId: service.id, resourceId: deepLink.stylist };\n }\n if (!first) {\n dispatch({ type: 'pickService', service });\n return;\n }\n dispatch({\n type: 'prefill',\n prefill: { serviceId: service.id, resourceId: deepLink.stylist, party: null },\n catalogue: prefillCatalogue(services, resources),\n });\n }\n\n /** One family member's service on step 2. */\n function pickServiceFor(index: number, service: WizardService) {\n if (deepLinkPending.current) {\n deepLinkPending.current = false;\n partyStylist.current = deepLink.stylist;\n }\n dispatch({\n type: 'pickServiceFor',\n index,\n service,\n resourceServiceIds: chosenStylistServiceIds,\n });\n }\n\n /**\n * The multi-select card: person `index` gains `service`, or loses it if they\n * have it. Never moves the visitor — they say when their list is done with\n * `continueFromService`. The named stylist's services ride along as for\n * `pickServiceFor`, so a stylist who cannot do the longer visit is released.\n */\n function toggleServiceFor(index: number, service: WizardService) {\n if (deepLinkPending.current) {\n deepLinkPending.current = false;\n partyStylist.current = deepLink.stylist;\n }\n dispatch({\n type: 'toggleServiceFor',\n index,\n service,\n resourceServiceIds: chosenStylistServiceIds,\n });\n }\n\n /** «Neste» under a multi-select card: on to the hour once everybody has a service. */\n function continueFromService() {\n if (state.step !== 'service' || !wizard.canAdvance(state)) return;\n dispatch({ type: 'goToStep', step: 'when' });\n }\n\n function pickResource(resourceId: string | null) {\n setStylistNotice(null);\n dispatch({ type: 'pickResource', resourceId });\n }\n\n function pickSlot(slot: BookingSlotDto) {\n setTakenSlotTs(null);\n dispatch({ type: 'pickSlot', startTs: slot.startTs, resourceId: slot.resourceId });\n }\n\n function pickPartySlot(slot: PartySlot) {\n setTakenSlotTs(null);\n dispatch({\n type: 'pickPartySlot',\n startTs: slot.startTs,\n resourceIds: slot.seats.map((seat) => seat.resourceId),\n // The slot's own mode: accepting the parallel offer changes both at once.\n mode: slot.mode,\n });\n }\n\n /** Who a parent seats on step 1; each new child starts on «same as last time». */\n function choosePeople(people: WizardPerson[], advance: boolean) {\n // A kept or waiting chair is that chair only while step 1 passes it through\n // as it is: one replaced — by a family child, or a count chip of the same\n // key — is somebody new.\n const stays = (key: string) =>\n people.includes(state.people.find((p) => p.key === key) as WizardPerson);\n setKeptSeats((current) => current.filter(stays));\n const waiting = [...(awaitingIds.current ?? [])].filter(([key]) => stays(key));\n awaitingIds.current = waiting.length > 0 ? new Map(waiting) : null;\n dispatch({\n type: 'choosePeople',\n people,\n advance,\n services: people.map((person) => suggestionFor(person)?.service ?? null),\n });\n }\n\n /** The step after this one, as «Next» means it. */\n function next() {\n const order = ['who', 'service', 'when', 'details'] as const;\n const following = order[order.indexOf(state.step) + 1];\n if (following) dispatch({ type: 'goToStep', step: following });\n }\n\n const action = (value: WizardAction) => dispatch(value);\n\n return {\n kit,\n state,\n dispatch: action,\n /**\n * The parent: who arrived (`null` once the create route said the session\n * ran out), who logged in from the sheet, the merged answer, and whether\n * anybody is logged in at all (a good code whose profile read failed is).\n */\n login: {\n arrivedAs: arrivedAsNow,\n signedIn,\n guardian,\n loggedIn: signedIn !== null || arrivedAsNow !== null,\n /** `/create` said the session ran out, and no login has happened since. */\n sessionLost,\n signIn: (who: BookingGuardian | null) => {\n reseatFor(who);\n setSessionLost(false);\n setSignedIn({ guardian: who });\n },\n vippsConfirm,\n resumePath: vippsResumePath,\n },\n people: {\n family,\n ageDayKey,\n personAge,\n suggestionFor,\n choosePeople,\n addChild,\n },\n catalogue: {\n services,\n resources,\n resourcesKnown,\n resolveStylistName,\n chosenStylistName,\n stylistNotice,\n deepLinkCategory: deepLink.category,\n },\n slots: {\n fromTs,\n toTs,\n days,\n single: singleSlots,\n party: partySlots,\n alternatives: partyAlternatives,\n failed: slotsFailed,\n ready: haveEveryService && scheduleSettled,\n takenSlotTs,\n nextAvailableTs: party ? partyNextAvailableTs : nextAvailableTs,\n nextAvailableLoading,\n },\n schedule: { openDays: openDaysForVisit, settled: scheduleSettled },\n derived: {\n canAdvance: wizard.canAdvance(state),\n party,\n total: wizard.totalPriceOre(state.items, state.startTs),\n summary: wizard.summaryLine(state, resolveStylistName),\n summaryParts: wizard.summaryParts(state, resolveStylistName),\n visitEnd:\n state.startTs === null\n ? null\n : wizard.visitEndTs(state.items, state.startTs, state.partyMode),\n },\n pickService,\n pickServiceFor,\n toggleServiceFor,\n continueFromService,\n pickResource,\n pickSlot,\n pickPartySlot,\n next,\n submit,\n submitting,\n submissionNonce,\n confirmed,\n startOver,\n /** Ask for the openings again after a failure. */\n refresh: () => setSlotsAttempt((n) => n + 1),\n restore: { hydrated, restoring, linked, resuming },\n };\n}\n\nexport type BookingController = ReturnType<typeof useBooking>;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useBooking.mjs","names":[],"sources":["../../src/react/useBooking.ts"],"sourcesContent":["/**\n * `useBooking()` — the booking wizard without its markup.\n *\n * Everything `<BookingWizard>` knows lives here, and the wizard is built on\n * it, so the headless path is never second-class: the machine (through\n * `useReducer`), the fetches to the site's own routes under `config.paths.api`\n * (availability, stylists, hours, the submission — the Medal key never leaves\n * the server), the draft / attempt / rebook stores, the restore of a booking\n * a reload or a Vipps login interrupted, the deep-link and «book again»\n * prefill, the 409 slot-taken refresh and the idempotency nonce.\n *\n * What it does NOT own is the page: focus, scrolling, which screen is drawn.\n * Those are the caller's (see `BookingWizard.tsx`).\n *\n * Moved from the source app's wizard shell with its behaviour unchanged; the\n * long-form reasoning for each rule is kept beside the rule.\n */\n\nimport { useSearchParams } from 'next/navigation';\nimport {\n useCallback,\n useEffect,\n useMemo,\n useReducer,\n useRef,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport type { AgeRange } from '../core/age';\nimport { ATTEMPT_TTL_MS, type BookingAttempt, type SubmittedVisit } from '../core/attempt-store';\nimport { serviceMatches } from '../core/deep-link';\nimport { stylistDisplayName } from '../core/display-name';\nimport { DRAFT_MAX_AGE_MS, type WizardDraft, type WizardDraftItem } from '../core/draft-store';\nimport { fill, labelText } from '../core/labels';\nimport {\n SELF_KEY,\n type WizardAction,\n type WizardItem,\n type WizardPerson,\n type WizardPrefill,\n type WizardService,\n type WizardState,\n} from '../core/machine';\nimport { firstOpeningPerResource } from '../core/next-available';\nimport { type PartySlot, type SlotsByService, slotKeysFor } from '../core/party-slots';\nimport { stripVippsReturn, vippsConfirmFrom } from '../core/portal/vipps-return';\nimport type {\n BookingDayDto,\n BookingFamilyMember,\n BookingGuardian,\n BookingResourceDto,\n BookingServiceDto,\n BookingSlotDto,\n BookingSubmission,\n} from '../core/types';\nimport { visitKey, visitServicesOf } from '../core/visit';\nimport type { BookingKit } from './kit';\nimport { type BookingOverrides, useBookingKit } from './Provider';\n\n/**\n * What the booking page prefetched on the server. Structurally the `/next`\n * loader's seed; every part but `services` may be missing, and whatever is\n * missing is fetched on demand into the same place.\n */\nexport interface BookingSeed {\n /** The full catalogue. */\n services: BookingServiceDto[];\n /** The stylists, or `null`/absent when they could not be read. */\n resources?: BookingResourceDto[] | null;\n /** Openings by service id, unfiltered by stylist. */\n slots?: Record<string, BookingSlotDto[]>;\n /** The business's hours over the window, by service id. */\n schedules?: Record<string, BookingDayDto[]>;\n /** «Next available» per stylist, by service id. */\n nextAvailable?: Record<string, Record<string, number>>;\n /** The instant the seed's window starts at; pinned once when absent. */\n fromTs?: number;\n}\n\nexport interface UseBookingOptions extends BookingOverrides {\n seed: BookingSeed;\n /**\n * The business's phone and address for this request, over `config.contact`\n * — what `/next`'s `loadBookingPage` answers as `contact`.\n */\n contact?: { phone?: string | null; address?: string | null };\n /** The parent the page found a portal session for; `null` for a guest. */\n guardian?: BookingGuardian | null;\n /** How many days the window covers. Default `config.window.rangeDays`. */\n rangeDays?: number;\n /**\n * Whether the shell can put a login in front of the visitor again. Under\n * `account.required` a lost session (`/create` → `accountRequired`) is a\n * login when it can — `login.sessionLost` — and an ordinary submit error\n * when it cannot. Default `true`: a headless shell reads `sessionLost`.\n */\n loginOffered?: boolean;\n}\n\n/** A person's «same as last time», and the note when it was swapped for their age. */\nexport interface BookingSuggestion {\n service: BookingServiceDto;\n note?: string;\n}\n\n/** One booked line on the confirmation. */\nexport interface ConfirmedLine {\n item: SubmittedVisit['items'][number];\n bookingId: string;\n stylistName: string | null;\n manageHref: string | null;\n}\n\n/** The confirmation, once a booking came back. */\nexport interface BookingConfirmation {\n bookings: Array<{ id: string; manageHref: string | null }>;\n submitted: SubmittedVisit;\n}\n\n/**\n * A line item as a draft keeps it. `idOnly` — nobody is logged in — keeps a\n * saved child by its id ALONE: the login the draft waits for decides, and only\n * that child's own parent gets the name back (`reseatFor`).\n */\nfunction draftItem(item: WizardItem, idOnly = false): WizardDraftItem {\n const saved = idOnly && item.bookedForPersonId !== undefined;\n return {\n serviceId: item.service.id,\n // Only when there are some: a one-service line is stored as it always was.\n ...(item.extraServices?.length\n ? { extraServiceIds: item.extraServices.map((service) => service.id) }\n : {}),\n bookedForName: saved ? null : (item.bookedForName ?? null),\n bookedForBirthYear: saved ? null : (item.bookedForBirthYear ?? null),\n adult: item.adult === true,\n personId: item.bookedForPersonId ?? null,\n };\n}\n\n/** How long a replayed submission may take before the restore gate gives up. */\nconst REPLAY_TIMEOUT_MS = 15_000;\n\n/**\n * `AbortSignal.timeout` where the browser has it, a timer-driven controller\n * where it does not. Safari only gained it in 16, and a booking resumed on an\n * older iPhone threw on the call and showed «we do not know yet» instead of\n * asking.\n */\nfunction timeoutSignal(ms: number): AbortSignal {\n if (typeof AbortSignal.timeout === 'function') return AbortSignal.timeout(ms);\n const controller = new AbortController();\n setTimeout(controller.abort.bind(controller), ms);\n return controller.signal;\n}\n\n/** The three reads the time step waits on. */\nexport type BookingReadEndpoint = 'availability' | 'resources' | 'schedule';\n\n/** A read the wizard could not use: its endpoint, and the HTTP status (`null`: no answer at all). */\nexport interface BookingReadError {\n endpoint: BookingReadEndpoint;\n status: number | null;\n /** Bumped per failure, so two identical failures are two events. */\n seq: number;\n}\n\n/** How many failed reads the controller keeps for the monitoring tap. */\nconst READ_ERRORS_KEPT = 20;\n\n/** A non-2xx answer, keeping the status a monitoring tap reports. */\nclass ReadFailure extends Error {\n constructor(\n readonly endpoint: BookingReadEndpoint,\n readonly status: number\n ) {\n super(`${endpoint} ${status}`);\n }\n}\n\n/** The seed's schedules once a retry has moved the window past them. */\nconst NO_SEEDED_SCHEDULE: Record<string, BookingDayDto[]> = {};\n\n/** «hydrated» only ever flips once; `useSyncExternalStore` wants a subscription. */\nfunction noSubscription(): () => void {\n return () => {\n // Nothing to unsubscribe from.\n };\n}\n\n/** The `freshSlots` of a `slotTaken` answer, narrowed to the lost visits' lists. */\nfunction freshSlotsOf(raw: unknown, lost: ReadonlySet<string>): Record<string, BookingSlotDto[]> {\n if (typeof raw !== 'object' || raw === null) return {};\n return Object.fromEntries(\n Object.entries(raw as Record<string, unknown>).filter(\n (entry): entry is [string, BookingSlotDto[]] => lost.has(entry[0]) && Array.isArray(entry[1])\n )\n );\n}\n\n/**\n * The window query for ONE visit, named by its key (`a` or `a+b`): the first\n * service as `service_id`, the rest as `extra_service_ids`. The extras are\n * added only when there are some, so a one-service visit asks with exactly the\n * URL it always did — the slot cache and every seeded answer are keyed by it.\n */\nfunction windowQuery(\n key: string,\n fromTs: number,\n toTs: number,\n resourceId?: string | null\n): URLSearchParams {\n const [serviceId, ...extras] = key.split('+');\n return new URLSearchParams({\n service_id: serviceId,\n ...(extras.length === 0 ? {} : { extra_service_ids: extras.join(',') }),\n // Omitted for «first available»: an empty resource_id would still narrow.\n ...(resourceId ? { resource_id: resourceId } : {}),\n from_ts: String(fromTs),\n to_ts: String(toTs),\n });\n}\n\n/**\n * The submission with each line's extras on it. meda's details screen builds\n * the body from its own types, which have one service per line, so the shell\n * is the only place that knows the rest of a person's visit. Index-aligned with\n * the basket the screen was drawn from; a body of another length is not one\n * this basket produced, and is sent as it came rather than with extras pinned\n * to the wrong people.\n */\nfunction withExtraServices(\n submission: BookingSubmission,\n items: ReadonlyArray<SubmittedVisit['items'][number]>\n): BookingSubmission | null {\n if (!items.some((item) => (item.extraServices ?? []).length > 0)) return submission;\n // Lines that cannot be matched to the basket are REFUSED, not sent bare: a\n // body without the extras books the first service only, while the card and\n // the calendar entry would describe the whole visit.\n if (submission.items.length !== items.length) {\n console.warn(\n '[@medalsocial/booking] The submission has %d lines for a basket of %d; not sent.',\n submission.items.length,\n items.length\n );\n return null;\n }\n return {\n ...submission,\n items: submission.items.map((line, index) => {\n const extras = items[index].extraServices ?? [];\n return extras.length === 0\n ? line\n : { ...line, extraServiceIds: extras.map((service) => service.id) };\n }),\n };\n}\n\n/**\n * Under `account.required`, each line's saved child back on it whatever phone\n * was typed. The details screen sends a person id only under the parent's own\n * number (the phone rule), but with an account required the booking goes on\n * the logged-in parent's contact anyway, so a child sent by name alone would\n * be created there a second time. The machine's id is the line's own: a name\n * typed over a saved child clears it. The route checks every id against the\n * session's own family before it is forwarded.\n */\nfunction withSessionPersons(\n submission: BookingSubmission,\n items: ReadonlyArray<SubmittedVisit['items'][number]>\n): BookingSubmission {\n return {\n ...submission,\n items: submission.items.map((line, index) => {\n const personId = items[index]?.bookedForPersonId;\n return personId === undefined ? line : { ...line, bookedForPersonId: personId };\n }),\n };\n}\n\n/** What the create route said, narrowed to what the machine can hold. */\nfunction asSubmitFailure(\n code: unknown\n): Exclude<NonNullable<WizardState['error']>, 'slotTaken' | 'maxParty'> {\n switch (code) {\n case 'conflict':\n case 'invalidInput':\n case 'unconfigured':\n case 'inProgress':\n return code;\n default:\n return 'upstreamError';\n }\n}\n\n/** What the named stylist can do; `undefined` for «nobody named» or «not loaded». */\nfunction serviceIdsOf(\n resources: BookingResourceDto[],\n resourceId: string | null\n): readonly string[] | undefined {\n if (resourceId === null) return undefined;\n return resources.find((resource) => resource.id === resourceId)?.serviceIds;\n}\n\n/**\n * Whether a create answer is a whole one: exactly one booking, with its own\n * distinct id, per submitted line. Anything else — none, fewer, more, an id\n * missing or repeated — is not a\n * confirmation; the wizard shows it as the generic create failure\n * (`upstreamError`, «we still do not know»), whose attempt survives for the\n * replay under the same idempotency key.\n */\nexport function isCompleteConfirmation(\n bookings: ReadonlyArray<{ id?: unknown }> | null | undefined,\n itemCount: number\n): boolean {\n if (!Array.isArray(bookings) || itemCount <= 0 || bookings.length !== itemCount) return false;\n const ids = bookings.map((booking) => booking?.id);\n // Distinct, too: one id on two lines is two calendar entries with one UID.\n return (\n ids.every((id) => typeof id === 'string' && id.length > 0) && new Set(ids).size === ids.length\n );\n}\n\n/**\n * The basket and the create response, zipped by INDEX — each line its OWN\n * booking. Only a whole answer (`isCompleteConfirmation`) gets this far; a line\n * the answer has no booking for is left out rather than handed a sibling's id,\n * which would give two calendar entries one UID.\n */\nexport function confirmationLines(confirmation: BookingConfirmation): ConfirmedLine[] {\n const { submitted, bookings } = confirmation;\n return submitted.items.flatMap((item, index) => {\n const booking = bookings[index];\n if (booking === undefined) return [];\n return [\n {\n item,\n bookingId: booking.id,\n stylistName: submitted.stylistNames[index] ?? null,\n manageHref: booking.manageHref,\n },\n ];\n });\n}\n\n/** Only the stylist the visitor asked for. */\nfunction onlyResource(slots: readonly BookingSlotDto[], resourceId: string): BookingSlotDto[] {\n return slots.filter((slot) => slot.resourceId === resourceId);\n}\n\n/** What a link asked for that needs no service to be true yet. */\ninterface DeepLink {\n category: string | null;\n stylist: string | null;\n party: number | null;\n seat: { children: number } | { adult: true } | null;\n}\n\n/** The catalogue a prefill is checked against: what step 2 lets the visitor TAP. */\nfunction prefillCatalogue(services: BookingServiceDto[], resources: BookingResourceDto[]) {\n return { services: services.filter((service) => service.bookableOnline), resources };\n}\n\n/** A stylist without a service is nothing to finish. */\nfunction stylistLeftToFill(prefill: WizardPrefill | null): WizardPrefill | null {\n return prefill?.serviceId && prefill.resourceId ? prefill : null;\n}\n\n/** Whether the stylist list can now vouch for the pending link. */\nfunction canFinishStylist(\n pending: WizardPrefill | null,\n resources: BookingResourceDto[],\n primaryVisit: string | null\n): pending is WizardPrefill {\n return pending !== null && resources.length > 0 && primaryVisit === pending.serviceId;\n}\n\n/**\n * A saved child as the person the machine seats: by `personId` when Medal has\n * one, else by their PLACE in the profile with name and year beside it, so two\n * «Emma 2018» are two seats and a reordered list matches nobody.\n */\nexport function personForChild(child: BookingFamilyMember, index: number): WizardPerson {\n return {\n key: child.personId ? `p:${child.personId}` : `n:${index}:${child.name}:${child.birthYear}`,\n name: child.name,\n birthYear: child.birthYear,\n ...(child.personId ? { personId: child.personId } : {}),\n ...(child.birthMonth ? { birthMonth: child.birthMonth } : {}),\n };\n}\n\n/** The pure parts of the wizard's setup, bound to one kit. */\nfunction setupFor(kit: BookingKit) {\n const { config, wizard, deepLinks } = kit;\n const q = config.query;\n\n /** «Bestill igjen» (`?service=&stylist=`) or a marketing `?tjeneste=` link, or `null`. */\n function prefillFromQuery(\n query: URLSearchParams | null,\n services: BookingServiceDto[]\n ): WizardPrefill | null {\n if (query === null) return null;\n const linked = query.get(q.service);\n const linkedService = linked\n ? services.find((service) => service.bookableOnline && serviceMatches(service, linked))\n : undefined;\n const serviceId = query.get(q.rebookService) || linkedService?.id || null;\n const resourceId = query.get(q.rebookStylist) || (serviceId ? query.get(q.stylist) : null);\n if (!serviceId && !resourceId) return null;\n return {\n serviceId,\n resourceId,\n party: serviceId ? deepLinks.parseParty(query.get(q.party)) : null,\n };\n }\n\n function deepLinkFromQuery(query: URLSearchParams | null): DeepLink {\n return {\n category: deepLinks.parseCategory(query?.get(q.category)),\n stylist: query?.get(q.stylist)?.trim() || null,\n party: deepLinks.parseParty(query?.get(q.party)),\n seat: deepLinks.impliedParty(query),\n };\n }\n\n /** One grown-up seated, as the adult chip does. */\n function withAdult(): WizardState {\n return wizard.reduce(wizard.initialState(), {\n type: 'choosePeople',\n people: [{ key: 'adult', adult: true }],\n advance: true,\n });\n }\n\n /** `n` guest children seated, clamped to the chips there are. */\n function withGuests(party: number): WizardState {\n const count = Math.min(Math.max(1, party), wizard.maxPeople);\n const people = Array.from({ length: count }, (_, index) => wizard.guestChild(index + 1));\n return wizard.reduce(wizard.initialState(), { type: 'choosePeople', people, advance: true });\n }\n\n /** `state` with the link's service held for step 1, when the catalogue has it. */\n function heldLink(\n state: WizardState,\n prefill: WizardPrefill | null,\n services: BookingServiceDto[],\n resources: BookingResourceDto[]\n ): WizardState {\n const service = prefill?.serviceId\n ? prefillCatalogue(services, resources).services.find(\n (entry) => entry.id === prefill.serviceId\n )\n : undefined;\n return service === undefined ? state : wizard.reduce(state, { type: 'holdService', service });\n }\n\n /**\n * Where the wizard starts: empty, or with the link folded in — in the\n * reducer's initialiser, so the server render already shows that step.\n */\n function initialWizardState(\n prefill: WizardPrefill | null,\n services: BookingServiceDto[],\n resources: BookingResourceDto[],\n seat: DeepLink['seat'] = null,\n familyKnown = false\n ): WizardState {\n // A parent the page knows is asked who is coming FIRST, whatever the link says.\n if (familyKnown) return heldLink(wizard.initialState(), prefill, services, resources);\n // `party.askWhoFirst: false`: a guest starts on step 2 as the common visit —\n // one child where the site has a children's menu, else one grown-up — and\n // step 1 stays one tap back for a party.\n const unasked = config.party.askWhoFirst\n ? null\n : kit.childCategory !== null\n ? { children: 1 }\n : ({ adult: true } as const);\n const seated = seat ?? unasked;\n const base = seated !== null && 'adult' in seated ? withAdult() : wizard.initialState();\n const state =\n prefill === null\n ? seated === null\n ? wizard.initialState()\n : 'adult' in seated\n ? withAdult()\n : withGuests(seated.children)\n : wizard.applyPrefill(base, prefill, prefillCatalogue(services, resources));\n // `screens.guestParty`: a guest still on step 1 with nobody chosen — no\n // link, or a link that holds a service for step 1 — starts on the common\n // answer, one child, already in the state, so the server-rendered step\n // shows «1» and a live «next». A link that seats people stays as it was.\n if (\n config.screens.guestParty &&\n seated === null &&\n state.step === 'who' &&\n state.people.length === 0\n ) {\n return wizard.reduce(state, {\n type: 'choosePeople',\n people: [wizard.guestChild(1)],\n advance: false,\n });\n }\n return state;\n }\n\n return { prefillFromQuery, deepLinkFromQuery, heldLink, initialWizardState };\n}\n\nconst CONTACT_FIELDS = ['name', 'phone', 'email'] as const;\n/** Contact fields by value, each only while a profile still owns it. */\ntype ProfileOwned = Partial<Record<(typeof CONTACT_FIELDS)[number], string>>;\n\nexport function useBooking(options: UseBookingOptions) {\n const { kit } = useBookingKit(options, options.contact);\n const { config, labels, wizard, clock, age, phone, deepLinks } = kit;\n const services = options.seed.services;\n const arrivedAs = options.guardian ?? null;\n const rangeDays = options.rangeDays ?? config.window.rangeDays;\n const setup = useMemo(() => setupFor(kit), [kit]);\n\n /**\n * Who logged in from the login sheet — and whether anybody did, which is\n * not the same question: a good code whose profile read failed is still a\n * logged-in parent, who must not be offered the login again.\n */\n const [signedIn, setSignedIn] = useState<{ guardian: BookingGuardian | null } | null>(null);\n /**\n * The create route said nobody is logged in (`accountRequired`): the session\n * the page arrived with — or the one taken here — has run out. From then on\n * the parent the page found is not trusted; only a fresh login is. Sticky:\n * a fresh login clears `sessionLost` (the notice), never this.\n */\n const [arrivalExpired, setArrivalExpired] = useState(false);\n /** The gate is back because the session ran out — until the next login. */\n const [sessionLost, setSessionLost] = useState(false);\n const arrivedAsNow = arrivalExpired ? null : arrivedAs;\n // A login decides who the parent is, even one whose profile read failed:\n // falling back to who ARRIVED would hand account B the family and\n // details of account A, whose seats `signIn` has just let go.\n const guardian = signedIn === null ? arrivedAsNow : signedIn.guardian;\n\n // The link, read ONCE, in the initialisers: a prefill that re-applied later\n // would answer questions the visitor has since changed their mind about.\n const query: URLSearchParams | null = useSearchParams();\n const [prefill] = useState(() => setup.prefillFromQuery(query, services));\n const [deepLink] = useState(() => setup.deepLinkFromQuery(query));\n const deepLinkPending = useRef(prefill === null || prefill.serviceId == null);\n const [linked] = useState(() => prefill !== null || deepLinks.hasDeepLink(query));\n const [vippsResumePath] = useState(() => deepLinks.resumePath(query));\n const [resuming] = useState(() => query?.get(config.query.resume) === '1');\n const [vippsConfirm] = useState<{ to: string | null } | null>(\n () => vippsConfirmFrom(query) ?? null\n );\n\n // The address bar is cleaned straight after the first render: the marker and\n // the masked address must not stay in the history entry.\n useEffect(() => {\n if (vippsConfirm !== null) stripVippsReturn();\n }, [vippsConfirm]);\n /** `false` on the server and through hydration, `true` from the first render after it. */\n const hydrated = useSyncExternalStore(\n noSubscription,\n () => true,\n () => false\n );\n const [restoreExpected] = useState(\n () =>\n typeof window !== 'undefined' &&\n kit.restoreGate.restorePendingInStorage(\n kit.attempts.ATTEMPT_STORAGE_KEY,\n ATTEMPT_TTL_MS,\n kit.drafts.DRAFT_STORAGE_KEY,\n DRAFT_MAX_AGE_MS,\n linked,\n resuming\n )\n );\n const [restoreSettled, setRestoreSettled] = useState(false);\n const restoring = restoreExpected && !restoreSettled;\n const [state, dispatch] = useReducer(wizard.reduce, prefill, (initial) =>\n setup.initialWizardState(\n initial,\n services,\n options.seed.resources ?? [],\n deepLink.seat,\n arrivedAs !== null\n )\n );\n\n /** Children a logged-in parent added from step 1 during this visit. */\n const [addedChildren, setAddedChildren] = useState<BookingFamilyMember[]>([]);\n const family = useMemo(\n () => (guardian === null ? null : [...guardian.family, ...addedChildren]),\n [guardian, addedChildren]\n );\n /**\n * Guest chairs an account change left standing for a seat with services —\n * the previous parent's child unseated, or a sent visit's child nobody could\n * vouch for — which `seatFamily` keeps: they are the visit being booked.\n */\n const [keptSeats, setKeptSeats] = useState<readonly string[]>([]);\n /** Guest seats go when a family appears over them (`seatFamily`). */\n const guestSeated =\n family !== null &&\n state.people.some((person) => wizard.isGuestSeat(person) && !keptSeats.includes(person.key));\n // biome-ignore lint/correctness/useExhaustiveDependencies: re-asked on every step change, as the source did.\n useEffect(() => {\n if (guestSeated) dispatch({ type: 'seatFamily', keep: keptSeats });\n }, [guestSeated, state.step]);\n /**\n * The saved child behind each guest chair a visit was rebuilt into while\n * nobody was logged in (`restoreDraft`), by seat key — names left out — for\n * the next login to claim by id (`reseatFor`).\n */\n const awaitingIds = useRef<Map<string, string> | null>(null);\n\n /**\n * The account the party's saved children were seated under — its e-mail,\n * `null` before anybody. Kept across a lost session on purpose: the seats\n * outlive it, held under the gate, and it is the NEXT login that decides\n * whether they still fit (`reseatFor`).\n */\n const seatedFor = useRef<string | null>(arrivedAs?.email ?? null);\n useEffect(() => {\n if (guardian !== null) seatedFor.current = guardian.email;\n }, [guardian]);\n\n /**\n * A login from the sheet or the gate, before it lands: when it is ANOTHER\n * account than the one the party was seated under — or one whose profile\n * nobody could read — the previous parent's children are not this one's to\n * book. Their seats become guest chairs (the services and the hour stay),\n * and what the old parent added here goes with them. Only a person id says\n * a child is the new parent's too: a child without one is matched by place,\n * name and year, which two families can share. A guest chair rebuilt from a\n * sent visit while nobody was logged in (`awaitingIds`) is this parent's\n * child again when the id is theirs, and stays a blank chair when it is not;\n * a login with no readable profile decides nothing, so the ids wait on for\n * the next one — including those of the seats it just let go, which the\n * next readable login claims back when they are its own. In the same batch as the login, so the form it opens never draws the old\n * names; the chairs it leaves keep their services on a step back.\n */\n function reseatFor(who: BookingGuardian | null) {\n const before = seatedFor.current;\n const now = who?.email ?? null;\n const awaiting = who === null ? null : awaitingIds.current;\n seatedFor.current = now;\n if (who !== null) awaitingIds.current = null;\n const switched = before !== null && before !== now;\n if (!switched && awaiting === null) return;\n if (switched) setAddedChildren([]);\n const theirs = who?.family ?? [];\n const keys: string[] = [];\n const into: Record<string, WizardPerson> = {};\n for (const person of state.people) {\n const id = awaiting?.get(person.key) ?? person.personId;\n const at = theirs.findIndex((child) => id !== undefined && child.personId === id);\n if (at !== -1 && person.personId === id) continue;\n if (at !== -1) into[person.key] = personForChild(theirs[at], at);\n if (\n at !== -1 ||\n (switched &&\n person.key !== SELF_KEY &&\n (person.personId !== undefined || !wizard.isGuestSeat(person)))\n ) {\n keys.push(person.key);\n }\n }\n const action = { type: 'unseatPeople', keys, into } as const;\n const after = wizard.reduce(state, action).people;\n const kept = after.filter(\n (person, index) =>\n wizard.isGuestSeat(person) &&\n (keys.includes(state.people[index].key) || awaiting?.has(person.key))\n );\n setKeptSeats((current) => [...current, ...kept.map((person) => person.key)]);\n if (who === null && switched) {\n const waiting = new Map(awaitingIds.current ?? []);\n after.forEach((person, index) => {\n const before = state.people[index];\n if (keys.includes(before.key) && before.personId !== undefined) {\n waiting.set(person.key, before.personId);\n }\n });\n awaitingIds.current = waiting.size > 0 ? waiting : null;\n }\n if (keys.length > 0) dispatch(action);\n }\n\n /** A stylist a party link named, applied once the family reaches step 3. */\n const partyStylist = useRef<string | null>(\n arrivedAs !== null ? (prefill?.resourceId ?? deepLink.stylist) : null\n );\n const childByKey = useMemo(\n () => new Map((family ?? []).map((child, index) => [personForChild(child, index).key, child])),\n [family]\n );\n\n // Pinned: the seeded slots and the day strip describe the same days. Moved\n // only by a retry on a later business day (`refresh`), which drops the seed\n // with it — a tab left open overnight would otherwise ask about yesterday.\n const [fromTs, setFromTs] = useState(() => options.seed.fromTs ?? Date.now());\n const { toTs, days } = useMemo(() => clock.window(fromTs, rangeDays), [clock, fromTs, rangeDays]);\n /** The business date the ages on steps 1 and 2 are read on: the chosen day, else today. */\n const ageDayKey = clock.dayKey(state.startTs ?? fromTs);\n\n /** A child's age on the business day `dayKey`; `null` for a grown-up or an unknown year. */\n function personAge(person: WizardPerson, dayKey: string): AgeRange | null {\n if (person.adult || person.birthYear === undefined) return null;\n return age.ageOnDay(person.birthYear, person.birthMonth, dayKey);\n }\n\n /** «Samme som sist»: the child's last visit, where it is still taken online. */\n const suggestionFor = (person: WizardPerson): BookingSuggestion | null => {\n const serviceId = childByKey.get(person.key)?.lastVisit?.serviceId;\n if (!serviceId) return null;\n const bookable = services.filter((entry) => entry.bookableOnline);\n const last = bookable.find((entry) => entry.id === serviceId);\n if (!last) return null;\n // Age on the appointment day, not the day of the last visit.\n const ageNow = personAge(person, ageDayKey);\n if (age.fitsAge(last, ageNow)) return { service: last };\n const grownUp = age.grownUpEquivalent(last, bookable, ageNow);\n if (!grownUp) return null;\n return {\n service: grownUp,\n note: fill(labels['wizard.suggestion.outgrown'], {\n last: last.name,\n name: person.name ?? labelText(labels['wizard.suggestion.someone']),\n next: grownUp.name,\n }),\n };\n };\n const pendingStylist = useRef(stylistLeftToFill(prefill));\n\n /**\n * Unfiltered openings, keyed by VISIT (`visitKey`): a service id for a\n * one-service visit — which is all the server seed ever holds — and `a+b`\n * for a person having both, whose list is the server's answer for the whole\n * visit rather than either service's own.\n */\n const [slots, setSlots] = useState<Record<string, readonly BookingSlotDto[]>>(\n () => options.seed.slots ?? {}\n );\n const [slotsFailed, setSlotsFailed] = useState(false);\n const [resources, setResources] = useState<BookingResourceDto[]>(\n () => options.seed.resources ?? []\n );\n /** Whether the stylist list has been answered at all — seeded, fetched or failed. */\n const [resourcesKnown, setResourcesKnown] = useState(() => options.seed.resources != null);\n const [seededNextAvailable, setSeededNextAvailable] = useState(() =>\n options.seed.resources ? (options.seed.nextAvailable ?? {}) : {}\n );\n const [initialSchedule] = useState(() => options.seed.schedules ?? {});\n /** Set once a retry has moved the window past the seed's day; the seed no longer answers. */\n const [seedRetired, setSeedRetired] = useState(false);\n const seededSchedule = seedRetired ? NO_SEEDED_SCHEDULE : initialSchedule;\n /** The last «next available» answer, and for WHICH visit (its key). */\n const [fetchedNextAvailable, setFetchedNextAvailable] = useState<{\n key: string;\n times: Record<string, number>;\n } | null>(null);\n\n const [submitting, setSubmitting] = useState(false);\n /**\n * The attempt (and its nonce), null until the browser has it: storage does\n * not exist during the server render, and reading it in an initialiser\n * would give the two sides different trees.\n */\n const [attempt, setAttempt] = useState<BookingAttempt | null>(null);\n /** The submission whose answer is still unknown; a retry resends THAT body. */\n const [pendingAttempt, setPendingAttempt] = useState<NonNullable<\n BookingAttempt['pending']\n > | null>(null);\n const submissionNonce = attempt?.nonce;\n /** The instant that was taken while the visitor was filling in the form. */\n const [takenSlotTs, setTakenSlotTs] = useState<number | null>(null);\n /** Set once, by a successful submission — one entry per line item. */\n const [confirmed, setConfirmed] = useState<BookingConfirmation | null>(null);\n\n const resumed = useRef(false);\n /** The whole «book again» link, kept to apply AGAIN after a refused replay. */\n const rebookLink = useRef<WizardPrefill | null>(prefill);\n const latestResources = useRef(resources);\n useEffect(() => {\n latestResources.current = resources;\n }, [resources]);\n\n // The child's name a «book again» tap stashed for this tab, taken once.\n const applyRebookWho = () => {\n const who = kit.rebook.takeRebookWho();\n if (who === null) return;\n rebookLink.current = { ...(rebookLink.current ?? {}), who };\n // A known parent: seat THEIR child by id, for a name exactly one child has.\n const known = guardian?.family ?? [];\n const named = known.filter((child) => child.name === who);\n if (named.length === 1) {\n const service = state.items[0]?.service ?? state.pendingService;\n dispatch({\n type: 'choosePeople',\n people: [personForChild(named[0], known.indexOf(named[0]))],\n services: [service],\n });\n if (service !== null) dispatch({ type: 'pickServiceFor', index: 0, service });\n return;\n }\n dispatch({ type: 'prefill', prefill: { who }, catalogue: prefillCatalogue(services, []) });\n };\n\n /** Put the «book again» link back onto a wizard that has just started over, once. */\n const restartFromLink = () => {\n const link = rebookLink.current;\n if (link === null) return;\n rebookLink.current = null;\n const current = latestResources.current;\n if (guardian !== null) {\n const held = setup.heldLink(wizard.initialState(), link, services, current);\n dispatch({ type: 'holdService', service: held.pendingService });\n partyStylist.current = link.resourceId ?? null;\n return;\n }\n dispatch({ type: 'prefill', prefill: link, catalogue: prefillCatalogue(services, current) });\n if (current.length === 0) pendingStylist.current = stylistLeftToFill(link);\n };\n\n /** The stylists a restored slot was booked with, until the catalogue can vouch for them. */\n const restoredStylists = useRef<string[] | null>(null);\n\n /**\n * A logged-in parent's saved children back in their chairs: each draft line\n * whose stored `personId` is a child of THIS guardian's family is reseated as\n * that child (`choosePeople`, then its service again), so it is submitted\n * with `booked_for_person_id` as it would have been without the round trip.\n * An id the family does not have — another parent's, a child removed since,\n * a hand-edited jar — is ignored, and its line restores as a name and a year\n * exactly as a draft without ids does. Returns the reseated line indexes.\n */\n const reseatFamily = (draft: WizardDraft, lines: BookingServiceDto[]): Set<number> => {\n const reseated = new Set<number>();\n if (family === null) return reseated;\n const keys = new Set<string>();\n // The seats `pickService` / `addService` just built (`peopleFor`), for the\n // lines that are not reseated. A parent known at mount starts with nobody\n // seated (`initialWizardState`), so every one of them is a guest chair.\n const people = draft.items.map((item, index) => {\n const built = wizard.guestChild(index + 1);\n const at = item.personId ? family.findIndex((child) => child.personId === item.personId) : -1;\n const child = at === -1 ? null : personForChild(family[at], at);\n if (child === null || keys.has(child.key)) {\n keys.add(built.key);\n return built;\n }\n keys.add(child.key);\n reseated.add(index);\n return child;\n });\n if (reseated.size === 0) return reseated;\n dispatch({ type: 'choosePeople', people });\n for (const index of reseated) {\n dispatch({ type: 'pickServiceFor', index, service: lines[index] });\n }\n return reseated;\n };\n\n /**\n * Each draft line's extras back on it, through `toggleServiceFor` like a tap.\n * Returns whether EVERY extra came back. The machine's refusals are checked\n * here first rather than read off its state, which a dispatch does not hand\n * back: an id the catalogue no longer books, one already on the line (a\n * toggle would take it OFF again), one past `maxServicesPerPerson`, or one\n * more taker than the service's `maxPerBooking` — each is skipped, as an\n * unknown main service is, and makes the answer `false`.\n */\n const restoreExtras = (\n draft: WizardDraft,\n lines: BookingServiceDto[],\n bookable: ReadonlyMap<string, BookingServiceDto>\n ): boolean => {\n let whole = true;\n const lists = lines.map((service) => [service]);\n draft.items.forEach((item, index) => {\n for (const id of item.extraServiceIds ?? []) {\n const service = bookable.get(id);\n const list = lists[index];\n const takers = lists.filter((other) => other.some((entry) => entry.id === id)).length;\n if (\n service === undefined ||\n list.some((entry) => entry.id === id) ||\n list.length >= config.party.maxServicesPerPerson ||\n takers + 1 > service.maxPerBooking\n ) {\n whole = false;\n continue;\n }\n list.push(service);\n dispatch({ type: 'toggleServiceFor', index, service });\n }\n });\n return whole;\n };\n\n /**\n * The booking a Vipps login interrupted, rebuilt through the machine's own\n * actions — so it obeys every rule a tapped one does — and trusting nothing\n * beyond the catalogue.\n */\n const restoreDraft = (draft: WizardDraft) => {\n const bookable = new Map(\n services.filter((service) => service.bookableOnline).map((service) => [service.id, service])\n );\n const resolved = draft.items.map((item) => bookable.get(item.serviceId));\n const [first, ...rest] = resolved;\n if (first === undefined || resolved.some((service) => service === undefined)) return;\n\n // Nobody to vouch for a saved child's id: its chair waits for the next login.\n if (family === null) {\n const ids = new Map<string, string>();\n draft.items.forEach((item, index) => {\n if (item.personId && !item.adult) ids.set(wizard.guestChild(index + 1).key, item.personId);\n });\n awaitingIds.current = ids.size > 0 ? ids : null;\n }\n // A guest is reseated as they left; a family's seats are its own children.\n if (family === null && draft.items.every((item) => typeof item.adult === 'boolean')) {\n dispatch({\n type: 'choosePeople',\n people: draft.items.map((item, index) =>\n item.adult ? { key: 'adult', adult: true } : wizard.guestChild(index + 1)\n ),\n advance: true,\n });\n }\n dispatch({ type: 'pickService', service: first });\n // Every line resolved (checked above), so `rest` holds services only.\n for (const service of rest as BookingServiceDto[]) dispatch({ type: 'addService', service });\n const reseated = reseatFamily(draft, resolved as BookingServiceDto[]);\n // After the seats are final (a reseat moves people, and their extras with\n // them) and before the stylist, so no toggle is refused for a preference\n // the draft is about to restore anyway.\n const wholeVisits = restoreExtras(draft, resolved as BookingServiceDto[], bookable);\n // Before the stylist: `setPartyMode('parallel')` drops the preference. A\n // mode the parent picked comes back as theirs, either way, so\n // `party.adultTogether` cannot re-decide it; one the default picked is\n // re-derived from the same seats, and stays the default's to change.\n if (draft.partyModeChosen !== false) {\n dispatch({ type: 'setPartyMode', mode: draft.partyMode });\n }\n dispatch({ type: 'pickResource', resourceId: draft.resourceId });\n draft.items.forEach((item, index) => {\n // A reseated child's name and year are the family's own, already on the line.\n if (reseated.has(index)) return;\n if (item.bookedForName !== null) {\n dispatch({\n type: 'setItemField',\n index,\n field: 'bookedForName',\n value: item.bookedForName,\n });\n }\n if (item.bookedForBirthYear !== null) {\n dispatch({\n type: 'setItemField',\n index,\n field: 'bookedForBirthYear',\n value: item.bookedForBirthYear,\n });\n }\n });\n\n // The hour was chosen for the visits as they were. One that came back\n // shorter — an extra retired since, or one the rules now refuse — would be\n // submitted at a time that was only ever free for the longer visit (or\n // offered for the shorter one by nobody), so the visitor picks again.\n if (draft.startTs === null || !wholeVisits) return;\n if (draft.partyResourceIds !== null && draft.partyResourceIds.length === resolved.length) {\n restoredStylists.current = draft.partyResourceIds;\n dispatch({\n type: 'pickPartySlot',\n startTs: draft.startTs,\n resourceIds: draft.partyResourceIds,\n mode: draft.partyMode,\n });\n return;\n }\n restoredStylists.current =\n draft.resolvedResourceId === null ? null : [draft.resolvedResourceId];\n dispatch({ type: 'pickSlot', startTs: draft.startTs, resourceId: draft.resolvedResourceId });\n };\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: once per mount; the ref, not the dependencies, makes it so.\n useEffect(() => {\n if (resumed.current) return;\n resumed.current = true;\n const replay = restoreFromStorage();\n if (replay === null) setRestoreSettled(true);\n else void replay.finally(() => setRestoreSettled(true));\n }, []);\n\n /** What the mount effect restores, in order; the replay when it started one. */\n function restoreFromStorage(): Promise<void> | null {\n let stored = kit.attempts.readAttempt();\n // A fresh link beats a remembered confirmation (not a pending attempt).\n if (linked && stored.confirmed !== undefined && stored.pending === undefined) {\n kit.attempts.clearAttempt();\n stored = kit.attempts.readAttempt();\n }\n // A remembered confirmation that is not a whole one is no confirmation.\n if (\n stored.confirmed !== undefined &&\n !isCompleteConfirmation(stored.confirmed.bookings, stored.confirmed.submitted.items.length)\n ) {\n kit.attempts.clearAttempt();\n stored = kit.attempts.readAttempt();\n }\n setAttempt(stored);\n applyRebookWho();\n if (stored.confirmed !== undefined) {\n setConfirmed(stored.confirmed);\n return null;\n }\n if (stored.pending === undefined) {\n // Nothing owed an answer: the one moment a Vipps round trip gets its draft back.\n if (resuming) {\n const draft = kit.drafts.takeDraft();\n if (draft !== null) restoreDraft(draft);\n }\n return null;\n }\n // The OLD attempt comes first, and the link waits for its answer.\n if (rebookLink.current !== null) {\n pendingStylist.current = null;\n dispatch({ type: 'startOver' });\n }\n setPendingAttempt(stored.pending);\n return send(stored.pending.submission, stored.pending.submitted, { replay: true });\n }\n\n /** The draft, kept current on every answer rather than written on the Vipps tap. */\n useEffect(() => {\n if (confirmed !== null || state.items.length === 0) {\n // A booked visit's chairs wait for nobody.\n if (confirmed !== null) awaitingIds.current = null;\n kit.drafts.clearDraft();\n return;\n }\n kit.drafts.stashDraft({\n // Nobody logged in — the session ran out under the gate — and a line\n // still naming a saved child: kept by its id ALONE. The login the draft\n // waits for decides: that parent's child is reseated by id, anybody\n // else gets a guest chair rather than the previous parent's child's name.\n // A chair still waiting for its child's parent (`awaitingIds`) keeps the id too.\n items: state.items.map((item, index) => {\n const line = draftItem(item, guardian === null);\n const waiting = awaitingIds.current?.get(state.people[index].key) ?? null;\n return { ...line, personId: line.personId ?? waiting };\n }),\n resourceId: state.resourceId,\n partyMode: state.partyMode,\n partyModeChosen: state.partyModeChosen === true,\n startTs: state.startTs,\n resolvedResourceId: state.resolvedResourceId,\n partyResourceIds: state.partyResourceIds,\n });\n }, [\n kit,\n confirmed,\n guardian,\n state.items,\n state.people,\n state.resourceId,\n state.partyMode,\n state.partyModeChosen,\n state.startTs,\n state.resolvedResourceId,\n state.partyResourceIds,\n ]);\n\n /**\n * The fields a logged-in parent's profile wrote, by value, for as long as it\n * still owns them — and only those: a field the visitor filled or changed is\n * never in here, whoever logs in next.\n */\n const guardianFilled = useRef<{ email: string; owns: ProfileOwned } | null>(null);\n /** A live mirror of the contact fields, for the prefill below. */\n const latestContact = useRef(state.contact);\n useEffect(() => {\n latestContact.current = state.contact;\n // A field that ever differs from what the profile wrote is the visitor's\n // from then on — typed back to the same value included — so neither a\n // switch nor an unreadable login below takes it.\n const owns = guardianFilled.current?.owns;\n if (owns === undefined) return;\n for (const field of CONTACT_FIELDS) {\n if (field in owns && state.contact[field] !== owns[field]) delete owns[field];\n }\n });\n\n /**\n * The logged-in parent's details into the fields — once per parent. A field\n * the previous parent's profile still owns is theirs to replace: with this\n * parent's value, or emptied when this profile has none (a shared device\n * must not keep the last account's phone). A blank field is filled. A field\n * the visitor owns is never touched, and stays theirs.\n */\n useEffect(() => {\n const contact = latestContact.current;\n const previous = guardianFilled.current?.owns ?? {};\n if (guardian === null) {\n // A login with no readable profile is somebody else until shown\n // otherwise: what the previous parent's profile wrote goes, and what\n // the visitor typed stays. (No login at all — the session ran out —\n // keeps the fields for the same parent's next login.)\n if (signedIn === null || guardianFilled.current === null) return;\n guardianFilled.current = null;\n for (const field of CONTACT_FIELDS) {\n if (field in previous) dispatch({ type: 'setContact', field, value: '' });\n }\n return;\n }\n if (guardianFilled.current?.email === guardian.email) return;\n const profile = {\n name: [guardian.firstName, guardian.lastName].filter(Boolean).join(' ').trim(),\n phone: guardian.phone === null ? '' : phone.nationalDigits(guardian.phone),\n email: guardian.email,\n };\n const owns: ProfileOwned = {};\n guardianFilled.current = { email: guardian.email, owns };\n\n for (const field of CONTACT_FIELDS) {\n const value = profile[field];\n const theirs = field in previous;\n if (!theirs && contact[field].trim() !== '') continue;\n if (value !== '') owns[field] = value;\n if (value !== '' || theirs) dispatch({ type: 'setContact', field, value });\n }\n }, [guardian, signedIn, phone]);\n\n // One entry per distinct VISIT, not per service: a person having cut and wash\n // is one question to the engine (one stylist, back to back), and the cut's\n // own list would offer somebody who cannot wash.\n const basketVisits = useMemo(() => slotKeysFor(state.items), [state.items]);\n // A string, so an effect keyed on it does not refetch on every keystroke.\n // `|` between visits, as `+` is already inside one.\n const basketKey = basketVisits.join('|');\n const primaryServiceId = state.items[0]?.service.id ?? null;\n /**\n * The first person's whole visit, by key — what the stylists' «neste ledige»\n * and the business's hours are asked for. Equal to `primaryServiceId` for a\n * one-service visit, so the seed (keyed by service id, and only ever for one\n * service) answers exactly the visits it can, and a visit with extras is\n * fetched instead of being read off its first service's openings.\n */\n const primaryVisit = state.items[0] ? visitKey(state.items[0]) : null;\n\n const nextAvailableTs = useMemo<Record<string, number>>(() => {\n if (primaryVisit === null) return {};\n const seeded = seededNextAvailable[primaryVisit];\n if (seeded) return seeded;\n return fetchedNextAvailable?.key === primaryVisit ? fetchedNextAvailable.times : {};\n }, [primaryVisit, seededNextAvailable, fetchedNextAvailable]);\n\n const missingKey = basketVisits.filter((key) => !(key in slots)).join('|');\n /** Bumped to ask for the openings again after a failure. */\n const [slotsAttempt, setSlotsAttempt] = useState(0);\n /**\n * The last read that failed, for the monitoring tap (`onEvent`). Every\n * failure here used to be swallowed into a card: the visitor saw «Vi får\n * ikke hentet ledige tider», and nobody else ever learnt that they had.\n */\n const [readErrors, setReadErrors] = useState<readonly BookingReadError[]>([]);\n // A queue, not a last-error slot: two visits failing in one batch are two\n // updates React may render as one, and the second must still be reported.\n const noteReadError = useCallback((endpoint: BookingReadEndpoint, error: unknown) => {\n setReadErrors((previous) => {\n const seq = (previous[previous.length - 1]?.seq ?? 0) + 1;\n const next = { endpoint, status: error instanceof ReadFailure ? error.status : null, seq };\n return [...previous.slice(-(READ_ERRORS_KEPT - 1)), next];\n });\n }, []);\n const api = config.paths.api;\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: `slotsAttempt` is the retry trigger.\n useEffect(() => {\n if (missingKey === '') return;\n const wanted = missingKey.split('|');\n let cancelled = false;\n setSlotsFailed(false);\n\n (async () => {\n // Settled one by one: a party of three whose third visit failed keeps the\n // two answers it has, and the retry asks only for the one still missing.\n const settled = await Promise.allSettled(\n wanted.map(async (key) => {\n const response = await fetch(`${api}/availability?${windowQuery(key, fromTs, toTs)}`);\n if (!response.ok) throw new ReadFailure('availability', response.status);\n const body = (await response.json()) as { slots?: BookingSlotDto[] };\n // Stored under the WHOLE visit's key — the key it is read back by.\n return [key, body.slots ?? []] as const;\n })\n );\n if (cancelled) return;\n const fetched = settled.flatMap((result) =>\n result.status === 'fulfilled' ? [result.value] : []\n );\n if (fetched.length > 0) {\n setSlots((previous) => ({ ...previous, ...Object.fromEntries(fetched) }));\n }\n // Nothing is cached for a failed visit; the step says so rather than «full».\n // Each failed visit is its own report.\n for (const result of settled) {\n if (result.status === 'rejected') noteReadError('availability', result.reason);\n }\n if (fetched.length < wanted.length) setSlotsFailed(true);\n })();\n\n return () => {\n cancelled = true;\n };\n }, [api, missingKey, fromTs, toTs, slotsAttempt, noteReadError]);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: `slotsAttempt` is the retry trigger.\n useEffect(() => {\n if (primaryVisit === null) return;\n if (primaryVisit in seededNextAvailable) return;\n let cancelled = false;\n\n (async () => {\n try {\n const response = await fetch(`${api}/resources?${windowQuery(primaryVisit, fromTs, toTs)}`);\n if (!response.ok) throw new ReadFailure('resources', response.status);\n const body = (await response.json()) as {\n resources?: BookingResourceDto[];\n nextAvailableTs?: Record<string, number>;\n };\n if (cancelled) return;\n setResources(body.resources ?? []);\n setResourcesKnown(true);\n setFetchedNextAvailable({ key: primaryVisit, times: body.nextAvailableTs ?? {} });\n } catch (error) {\n // Answered «none» FOR THIS VISIT; the stylist LIST is kept.\n if (!cancelled) {\n noteReadError('resources', error);\n setResourcesKnown(true);\n setFetchedNextAvailable({ key: primaryVisit, times: {} });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [api, primaryVisit, fromTs, toTs, seededNextAvailable, slotsAttempt, noteReadError]);\n\n /**\n * The stylist half of a «book again» link, once a list for its service has\n * landed. Compared by VISIT: the link named one service, and a visitor who\n * has since added a wash to it is asking a different question — the list now\n * in hand answers the longer visit, not the one the link vouched for.\n */\n useEffect(() => {\n const pending = pendingStylist.current;\n if (pending && primaryVisit !== null && primaryVisit !== pending.serviceId) {\n pendingStylist.current = null;\n return;\n }\n if (!canFinishStylist(pending, resources, primaryVisit)) return;\n pendingStylist.current = null;\n dispatch({\n type: 'prefill',\n prefill: pending,\n catalogue: prefillCatalogue(services, resources),\n });\n }, [resources, primaryVisit, services]);\n\n /** A party link's stylist, once the family has its services and is on step 3. */\n useEffect(() => {\n const wanted = partyStylist.current;\n if (wanted === null || state.step !== 'when' || primaryServiceId === null) return;\n if (resources.length === 0) return;\n partyStylist.current = null;\n dispatch({\n type: 'prefill',\n prefill: { serviceId: primaryServiceId, resourceId: wanted },\n catalogue: prefillCatalogue(services, resources),\n });\n }, [state.step, primaryServiceId, resources, services]);\n\n /** A restored slot whose stylist has left the roster, dropped before it is submitted. */\n useEffect(() => {\n const restored = restoredStylists.current;\n if (restored === null || resources.length === 0 || primaryServiceId === null) return;\n restoredStylists.current = null;\n const known = new Set(resources.map((resource) => resource.id));\n if (restored.every((id) => known.has(id))) return;\n dispatch({ type: 'pickResource', resourceId: null });\n }, [resources, primaryServiceId]);\n\n /**\n * The open dates over the window, for the first person's VISIT (cutoffs are\n * per service, and `visitTailMinutes` assumes the answer was for item 0's\n * whole visit — see the machine).\n *\n * The seed is the SALON's week, which is what «first available» draws. A named\n * stylist works a different week, so their hours are fetched with `resource_id`\n * and the seed is not allowed to answer for them — a seeded salon week would\n * otherwise paint every open day as theirs.\n */\n const [fetchedSchedule, setFetchedSchedule] = useState<{\n key: string;\n days: BookingDayDto[];\n } | null>(null);\n const [scheduleFailedFor, setScheduleFailedFor] = useState<string | null>(null);\n // The visit, plus the stylist when one is named. The two answers must not\n // share a key: a late salon response must not land on a stylist's calendar.\n const scheduleKey =\n primaryVisit === null\n ? null\n : state.resourceId === null\n ? primaryVisit\n : `${primaryVisit}#${state.resourceId}`;\n\n // `null`, not `[]`, until this service's hours are in hand: «we do not know».\n // A named stylist's own week replaces the salon's when it arrives. Until then\n // the salon answer stays, so the time step does not unmount mid-tap. A failed\n // read is not «still waiting»: their hours are unknown, and the salon week\n // must not stay on screen as theirs.\n const openDays = useMemo<BookingDayDto[] | null>(() => {\n if (primaryVisit === null || scheduleKey === null) return null;\n if (fetchedSchedule?.key === scheduleKey) return fetchedSchedule.days;\n if (state.resourceId === null) {\n return seededSchedule[primaryVisit] ?? null;\n }\n if (scheduleFailedFor === scheduleKey) return null;\n return (\n seededSchedule[primaryVisit] ??\n (fetchedSchedule?.key === primaryVisit ? fetchedSchedule.days : null)\n );\n }, [\n primaryVisit,\n state.resourceId,\n scheduleKey,\n seededSchedule,\n fetchedSchedule,\n scheduleFailedFor,\n ]);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: `slotsAttempt` is the retry trigger.\n useEffect(() => {\n if (primaryVisit === null || scheduleKey === null) return;\n // The seed already answers «first available». A named stylist always asks.\n if (state.resourceId === null && primaryVisit in seededSchedule) return;\n let cancelled = false;\n const key = scheduleKey;\n const resourceId = state.resourceId;\n // A new read is not the previous failure. Leaving that flag set would settle\n // the retry before it has answered.\n setScheduleFailedFor((current) => (current === key ? null : current));\n\n (async () => {\n try {\n const response = await fetch(\n `${api}/schedule?${windowQuery(primaryVisit, fromTs, toTs, resourceId)}`\n );\n if (!response.ok) throw new ReadFailure('schedule', response.status);\n const body = (await response.json()) as { days?: BookingDayDto[] };\n if (cancelled) return;\n setFetchedSchedule({ key, days: body.days ?? [] });\n } catch (error) {\n // Left `null`: the hours are unknown, which is not the same as «closed».\n // Keyed like the success, so one stylist's failure does not settle the next.\n if (!cancelled) {\n noteReadError('schedule', error);\n setScheduleFailedFor(key);\n // The previous success for this stylist is stale once this read failed.\n setFetchedSchedule((current) => (current?.key === key ? null : current));\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n api,\n primaryVisit,\n fromTs,\n toTs,\n seededSchedule,\n state.resourceId,\n scheduleKey,\n slotsAttempt,\n noteReadError,\n ]);\n\n /**\n * «Prøv igjen»: ask for the openings, the stylists and the hours again.\n *\n * The seed's stylists and hours are answers too, so a retry asks them again\n * rather than trusting the page's copy of them. A page left open past\n * midnight also starts a fresh window from now — a retry that resent the\n * same request would get the same answer, and yesterday's window would\n * offer hours that are gone and stop a day short — dropping every opening\n * with it, since they describe the old window.\n */\n const refresh = useCallback(() => {\n const now = Date.now();\n setSeededNextAvailable({});\n setSeedRetired(true);\n if (clock.dayKey(now) !== clock.dayKey(fromTs)) {\n setSlots({});\n // The stylists are asked again for the new window; until they answer the\n // step shows them loading rather than yesterday's list as current.\n setResourcesKnown(false);\n setFetchedNextAvailable(null);\n setFetchedSchedule(null);\n setFromTs(now);\n }\n setSlotsAttempt((n) => n + 1);\n }, [clock, fromTs]);\n\n /** A stylist's display name for an id; `''` from Medal reads as none. */\n const resolveStylistName = useMemo(\n () => (resourceId: string) => {\n const name = resources.find((resource) => resource.id === resourceId)?.name;\n return name ? stylistDisplayName(name) || null : null;\n },\n [resources]\n );\n const chosenStylistName = state.resourceId === null ? null : resolveStylistName(state.resourceId);\n const chosenStylistServiceIds = serviceIdsOf(resources, state.resourceId);\n\n /**\n * A NAMED stylist restored before the list landed who turns out not to cover\n * this basket: «first available» instead, and said so — only on step 3, and\n * only against a list that actually arrived.\n */\n const [stylistNotice, setStylistNotice] = useState<string | null>(null);\n useEffect(() => {\n if (!resourcesKnown || resources.length === 0) return;\n if (state.step !== 'when' || state.resourceId === null) return;\n const chosen = resources.find((resource) => resource.id === state.resourceId);\n // Every service of every visit: a stylist who cuts but does not wash cannot\n // take the person having both.\n const covers =\n chosen !== undefined &&\n state.items.every((item) =>\n visitServicesOf(item).every((service) => chosen.serviceIds.includes(service.id))\n );\n if (covers) return;\n dispatch({ type: 'pickResource', resourceId: null });\n setStylistNotice(labelText(labels['wizard.stylistGone']));\n }, [labels, resourcesKnown, resources, state.step, state.resourceId, state.items]);\n\n /** The open dates with the cutoffs pulled back to fit the WHOLE visit (the safe direction). */\n const openDaysForVisit = useMemo(() => {\n if (openDays === null) return null;\n const extraMs = wizard.visitTailMinutes(state.items, state.partyMode) * 60_000;\n if (extraMs <= 0) return openDays;\n return openDays.map((day) => ({\n ...day,\n lastStartTs: day.lastStartTs === null ? null : day.lastStartTs - extraMs,\n }));\n }, [wizard, openDays, state.items, state.partyMode]);\n\n const party = wizard.showsPartyMode(state);\n const haveEveryService = basketVisits.every((key) => key in slots);\n const scheduleSettled =\n openDays !== null || (scheduleKey !== null && scheduleFailedFor === scheduleKey);\n const nextAvailableLoading = party\n ? !haveEveryService && !slotsFailed\n : primaryVisit !== null &&\n !(primaryVisit in seededNextAvailable) &&\n fetchedNextAvailable?.key !== primaryVisit;\n\n /** The basket's openings, unfiltered — the parallel search needs two stylists. */\n const basketSlots: SlotsByService = useMemo(() => {\n const out: Record<string, readonly BookingSlotDto[]> = {};\n for (const id of basketKey === '' ? [] : basketKey.split('|')) out[id] = slots[id] ?? [];\n return out;\n }, [basketKey, slots]);\n\n const preferredSlots: SlotsByService = useMemo(() => {\n const preferred = state.resourceId;\n if (preferred === null) return basketSlots;\n return Object.fromEntries(\n Object.entries(basketSlots).map(([id, list]) => [id, onlyResource(list, preferred)])\n );\n }, [basketSlots, state.resourceId]);\n\n const partyNextAvailableTs = useMemo<Record<string, number>>(\n () =>\n party && state.partyMode === 'sequential' && haveEveryService\n ? kit.partySlots.firstPartyStartPerResource(state.items, basketSlots)\n : {},\n [kit, party, state.partyMode, haveEveryService, state.items, basketSlots]\n );\n\n const partySlots = useMemo(\n () =>\n party ? kit.partySlots.findPartySlots(state.items, preferredSlots, state.partyMode) : [],\n [kit, party, state.items, preferredSlots, state.partyMode]\n );\n\n /** The parallel visits, for the day with nothing back to back — over UNFILTERED slots. */\n const partyAlternatives = useMemo(\n () =>\n party && state.partyMode === 'sequential'\n ? kit.partySlots.findPartySlots(state.items, basketSlots, 'parallel')\n : [],\n [kit, party, state.items, basketSlots, state.partyMode]\n );\n\n const singleSlots = useMemo(() => {\n if (party || primaryVisit === null) return [];\n const list = slots[primaryVisit] ?? [];\n return state.resourceId === null ? [...list] : onlyResource(list, state.resourceId);\n }, [party, primaryVisit, slots, state.resourceId]);\n\n /** Create a logged-in parent's new child through the site's portal route. */\n async function saveChild(child: {\n name: string;\n birthYear: number;\n birthMonth?: number;\n notes?: string;\n }): Promise<{ ok: true; child: BookingFamilyMember } | { ok: false; message: string }> {\n const copy: Record<string, string> = {\n invalid: labelText(labels['wizard.addChild.invalid']),\n session: labelText(labels['wizard.addChild.session']),\n throttled: labelText(labels['wizard.addChild.throttled']),\n };\n const unreachable = labelText(labels['wizard.addChild.unreachable']);\n try {\n const response = await fetch(`${config.paths.portalApi}/persons`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(child),\n cache: 'no-store',\n credentials: 'same-origin',\n });\n const body = (await response.json().catch(() => null)) as {\n ok?: boolean;\n child?: BookingFamilyMember;\n reason?: string;\n message?: string;\n } | null;\n if (body?.ok === true && body.child) return { ok: true, child: body.child };\n return { ok: false, message: body?.message ?? copy[body?.reason ?? ''] ?? unreachable };\n } catch {\n return { ok: false, message: unreachable };\n }\n }\n\n /**\n * Step 1's «+ add child». A logged-in parent's child is created first and\n * ticked once it exists; a guest's lives in this booking.\n */\n async function addChild(child: {\n name: string;\n birthYear: number;\n birthMonth?: number;\n notes?: string;\n }): Promise<{ ok: true } | { ok: false; message: string }> {\n const named = state.people.filter((person) => person.key.startsWith('new:'));\n const seated = guardian === null ? named : state.people;\n if (seated.length >= wizard.maxPeople) {\n return { ok: false, message: fill(labels['who.family.limit'], { max: wizard.maxPeople }) };\n }\n if (guardian === null) {\n // Out of the sheet's transition first: seated inside it, the child would\n // land a render AFTER the sheet that added them has closed.\n await Promise.resolve();\n const person: WizardPerson = {\n key: `new:${Date.now()}:${named.length}`,\n name: child.name,\n birthYear: child.birthYear,\n ...(child.birthMonth === undefined ? {} : { birthMonth: child.birthMonth }),\n };\n // A named child replaces the count chips.\n dispatch({ type: 'choosePeople', people: [...named, person], advance: false });\n return { ok: true };\n }\n const saved = await saveChild(child);\n if (!saved.ok) return saved;\n setAddedChildren((current) => [...current, saved.child]);\n // Against the party as it is NOW, placed after every child the list holds.\n // A guardian is known here, so `family` is their list.\n const known = family as BookingFamilyMember[];\n dispatch({ type: 'addPerson', person: personForChild(saved.child, known.length) });\n return { ok: true };\n }\n\n async function submit(submission: BookingSubmission) {\n // An unresolved attempt is resent VERBATIM: the key hashes nonce AND body.\n if (pendingAttempt !== null) {\n await send(pendingAttempt.submission, pendingAttempt.submitted, { pending: true });\n return;\n }\n // Read BEFORE the await: the card describes what left the browser.\n const resourceIds = wizard.itemResourceIds(state);\n const extended = withExtraServices(submission, state.items);\n const body =\n extended !== null && config.account.required\n ? withSessionPersons(extended, state.items)\n : extended;\n if (body === null) {\n dispatch({ type: 'submitFailed', error: 'invalidInput' });\n return;\n }\n await send(body, {\n items: state.items,\n startTs: state.startTs ?? 0,\n partyMode: state.partyMode,\n resourceIds,\n stylistNames: resourceIds.map((id) => (id === null ? null : resolveStylistName(id))),\n });\n }\n\n /** The POST, and everything that can come back from it (also the replay's). */\n async function send(\n submission: BookingSubmission,\n submitted: SubmittedVisit,\n sendOptions?: { replay?: boolean; pending?: boolean }\n ) {\n setSubmitting(true);\n const current = attempt ?? kit.attempts.readAttempt();\n if (attempt === null) setAttempt(current);\n // Written before the request leaves: the body must be there if the answer is not.\n kit.attempts.rememberPending(current, { submission, submitted });\n setPendingAttempt({ submission, submitted });\n if (sendOptions?.replay !== true && sendOptions?.pending !== true) rebookLink.current = null;\n try {\n const response = await fetch(`${api}/create`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...submission, window: { fromTs, toTs } }),\n ...(sendOptions?.replay === true ? { signal: timeoutSignal(REPLAY_TIMEOUT_MS) } : {}),\n });\n const payload = (await response.json().catch(() => null)) as {\n error?: string;\n bookings?: Array<{ id: string; manageToken?: string }>;\n freshSlots?: Record<string, BookingSlotDto[]>;\n } | null;\n\n if (!response.ok) {\n // `upstreamError` is «we still do not know»: the attempt survives it.\n const definite = payload?.error !== undefined && payload.error !== 'upstreamError';\n // `account.required` and the session gone: the login again, not an\n // error. Nothing else moves — the wizard keeps the hour, and «Bekreft»\n // turns back into the login over it.\n const sessionGone = payload?.error === 'accountRequired';\n if (definite) {\n // The session gone keeps the NONCE (and drops only the body): an\n // earlier try of this submission may have booked with its answer\n // lost, and the same parent's resend after the login — same nonce,\n // same visit — derives the same key and meets it rather than booking\n // twice. The body is not kept: whoever logs in next is asked again.\n if (sessionGone) kit.attempts.releasePending(current);\n else kit.attempts.clearAttempt();\n setPendingAttempt(null);\n }\n // A RESUMED replay (or the resend of one while a link waits) has no\n // basket to rescue: a clean wizard, or the link it came by.\n const wasResumed =\n sendOptions?.replay === true ||\n (sendOptions?.pending === true && rebookLink.current !== null);\n if (sessionGone) {\n setSignedIn(null);\n setArrivalExpired(true);\n setSessionLost(true);\n // Before the resumed reset below: the gate holds the slot, a clean\n // wizard would drop it. A replay or a resend of a pending attempt\n // sent a visit the machine need not hold (never hydrated; or the\n // parent stepped back and picked another hour while the attempt\n // waited, and «Bekreft» resent the old one verbatim), so the visit\n // it SENT is rebuilt under the gate — the one whose key a resend\n // after the login has to meet. Any other send is the machine's own.\n if (options.loginOffered !== false) {\n if (sendOptions?.replay === true || sendOptions?.pending === true) {\n rebuildSubmitted(submitted);\n }\n return;\n }\n }\n if (wasResumed) {\n if (definite) {\n forgetChairs();\n dispatch({ type: 'startOver' });\n restartFromLink();\n } else dispatch({ type: 'submitFailed', error: 'upstreamError' });\n return;\n }\n if (sessionGone) {\n // Nowhere to log in again (no login actions): an error, not a dead «Bekreft».\n dispatch({ type: 'submitFailed', error: 'upstreamError' });\n return;\n }\n if (payload?.error === 'slotTaken') {\n // From the SUBMITTED visit: after a reload there is no live basket.\n setTakenSlotTs(submitted.startTs);\n // The cache goes too, or the time step reopens on the SAME chip:\n // replaced by the 409's live `freshSlots`, else evicted for a re-read.\n // By VISIT, as the server keys `freshSlots` and the wizard its cache.\n const lost = new Set(submitted.items.map((item) => visitKey(item)));\n const fresh = freshSlotsOf(payload?.freshSlots, lost);\n setSlots((previous) => {\n const next = { ...previous };\n for (const key of lost) {\n if (key in fresh) next[key] = fresh[key];\n else delete next[key];\n }\n return next;\n });\n // «Next available» was read out of the same stale openings.\n const canSeed = resources.length > 0;\n setSeededNextAvailable((previous) => ({\n ...Object.fromEntries(Object.entries(previous).filter(([id]) => !lost.has(id))),\n ...(canSeed\n ? Object.fromEntries(\n Object.entries(fresh).map(([id, list]) => [id, firstOpeningPerResource(list)])\n )\n : {}),\n }));\n setFetchedNextAvailable((previous) =>\n previous !== null && lost.has(previous.key) ? null : previous\n );\n kit.attempts.clearAttempt();\n dispatch({ type: 'slotTaken' });\n return;\n }\n if (payload?.error !== undefined && payload.error !== 'upstreamError') {\n kit.attempts.clearAttempt();\n }\n dispatch({ type: 'submitFailed', error: asSubmitFailure(payload?.error) });\n return;\n }\n\n const bookings = payload?.bookings ?? [];\n if (!isCompleteConfirmation(bookings, submitted.items.length)) {\n // A 201 with no booking in it is not a booking, and one short of the\n // basket is not a confirmation: the attempt stays pending.\n dispatch({ type: 'submitFailed', error: 'upstreamError' });\n return;\n }\n // Every token: each is returned in plaintext exactly once.\n const confirmedBookings = bookings.map((booking) => ({\n id: booking.id,\n manageHref: booking.manageToken ? kit.paths.managePath(booking.manageToken) : null,\n }));\n setConfirmed({ bookings: confirmedBookings, submitted });\n // Kept, not cleared, so a refresh cannot take the manage links with it.\n kit.attempts.rememberConfirmed(current, { bookings: confirmedBookings, submitted });\n setPendingAttempt(null);\n } catch {\n dispatch({ type: 'submitFailed', error: 'upstreamError' });\n } finally {\n setSubmitting(false);\n }\n }\n\n /**\n * The visit a refused replay sent, back in the machine as a draft would be\n * — the hour on «Bekreft» — in place of the link that was waiting on it.\n */\n function rebuildSubmitted(submitted: SubmittedVisit) {\n rebookLink.current = null;\n const { items, resourceIds } = submitted;\n // Nobody is logged in now: a saved child goes back by id alone (`draftItem`).\n restoreDraft({\n items: items.map((item) => draftItem(item, true)),\n resourceId: null,\n partyMode: submitted.partyMode,\n startTs: submitted.startTs,\n resolvedResourceId: resourceIds[0] ?? null,\n partyResourceIds:\n items.length > 1 && resourceIds.every((id) => id !== null)\n ? (resourceIds as string[])\n : null,\n savedAt: Date.now(),\n });\n }\n\n /** «Book again» on the confirmation: a clean wizard with a fresh attempt. */\n function startOver() {\n kit.attempts.clearAttempt();\n setAttempt(kit.attempts.readAttempt());\n setPendingAttempt(null);\n // The openings were read before the booking just made: drop them.\n setSlots({});\n setSlotsFailed(false);\n setConfirmed(null);\n setTakenSlotTs(null);\n forgetChairs();\n dispatch({ type: 'startOver' });\n restartFromLink();\n }\n\n /** A new visit: no chair of the last one is kept or waits for its parent. */\n function forgetChairs() {\n setKeptSeats([]);\n awaitingIds.current = null;\n }\n\n /** A service tap on step 2 for a party of one — the first carries the link's stylist. */\n function pickService(service: BookingServiceDto) {\n const first = deepLinkPending.current && state.items.length === 0;\n deepLinkPending.current = false;\n if (first && deepLink.stylist && resources.length === 0) {\n pendingStylist.current = { serviceId: service.id, resourceId: deepLink.stylist };\n }\n if (!first) {\n dispatch({ type: 'pickService', service });\n return;\n }\n dispatch({\n type: 'prefill',\n prefill: { serviceId: service.id, resourceId: deepLink.stylist, party: null },\n catalogue: prefillCatalogue(services, resources),\n });\n }\n\n /** One family member's service on step 2. */\n function pickServiceFor(index: number, service: WizardService) {\n if (deepLinkPending.current) {\n deepLinkPending.current = false;\n partyStylist.current = deepLink.stylist;\n }\n dispatch({\n type: 'pickServiceFor',\n index,\n service,\n resourceServiceIds: chosenStylistServiceIds,\n });\n }\n\n /**\n * The multi-select card: person `index` gains `service`, or loses it if they\n * have it. Never moves the visitor — they say when their list is done with\n * `continueFromService`. The named stylist's services ride along as for\n * `pickServiceFor`, so a stylist who cannot do the longer visit is released.\n */\n function toggleServiceFor(index: number, service: WizardService) {\n if (deepLinkPending.current) {\n deepLinkPending.current = false;\n partyStylist.current = deepLink.stylist;\n }\n dispatch({\n type: 'toggleServiceFor',\n index,\n service,\n resourceServiceIds: chosenStylistServiceIds,\n });\n }\n\n /** «Neste» under a multi-select card: on to the hour once everybody has a service. */\n function continueFromService() {\n if (state.step !== 'service' || !wizard.canAdvance(state)) return;\n dispatch({ type: 'goToStep', step: 'when' });\n }\n\n function pickResource(resourceId: string | null) {\n setStylistNotice(null);\n dispatch({ type: 'pickResource', resourceId });\n }\n\n function pickSlot(slot: BookingSlotDto) {\n setTakenSlotTs(null);\n dispatch({ type: 'pickSlot', startTs: slot.startTs, resourceId: slot.resourceId });\n }\n\n function pickPartySlot(slot: PartySlot) {\n setTakenSlotTs(null);\n dispatch({\n type: 'pickPartySlot',\n startTs: slot.startTs,\n resourceIds: slot.seats.map((seat) => seat.resourceId),\n // The slot's own mode: accepting the parallel offer changes both at once.\n mode: slot.mode,\n });\n }\n\n /** Who a parent seats on step 1; each new child starts on «same as last time». */\n function choosePeople(people: WizardPerson[], advance: boolean) {\n // A kept or waiting chair is that chair only while step 1 passes it through\n // as it is: one replaced — by a family child, or a count chip of the same\n // key — is somebody new.\n const stays = (key: string) =>\n people.includes(state.people.find((p) => p.key === key) as WizardPerson);\n setKeptSeats((current) => current.filter(stays));\n const waiting = [...(awaitingIds.current ?? [])].filter(([key]) => stays(key));\n awaitingIds.current = waiting.length > 0 ? new Map(waiting) : null;\n dispatch({\n type: 'choosePeople',\n people,\n advance,\n services: people.map((person) => suggestionFor(person)?.service ?? null),\n });\n }\n\n /** The step after this one, as «Next» means it. */\n function next() {\n const order = ['who', 'service', 'when', 'details'] as const;\n const following = order[order.indexOf(state.step) + 1];\n if (following) dispatch({ type: 'goToStep', step: following });\n }\n\n const action = (value: WizardAction) => dispatch(value);\n\n return {\n kit,\n state,\n dispatch: action,\n /**\n * The parent: who arrived (`null` once the create route said the session\n * ran out), who logged in from the sheet, the merged answer, and whether\n * anybody is logged in at all (a good code whose profile read failed is).\n */\n login: {\n arrivedAs: arrivedAsNow,\n signedIn,\n guardian,\n loggedIn: signedIn !== null || arrivedAsNow !== null,\n /** `/create` said the session ran out, and no login has happened since. */\n sessionLost,\n signIn: (who: BookingGuardian | null) => {\n reseatFor(who);\n setSessionLost(false);\n setSignedIn({ guardian: who });\n },\n vippsConfirm,\n resumePath: vippsResumePath,\n },\n people: {\n family,\n ageDayKey,\n personAge,\n suggestionFor,\n choosePeople,\n addChild,\n },\n catalogue: {\n services,\n resources,\n resourcesKnown,\n resolveStylistName,\n chosenStylistName,\n stylistNotice,\n deepLinkCategory: deepLink.category,\n },\n slots: {\n fromTs,\n toTs,\n days,\n single: singleSlots,\n party: partySlots,\n alternatives: partyAlternatives,\n failed: slotsFailed,\n ready: haveEveryService && scheduleSettled,\n takenSlotTs,\n nextAvailableTs: party ? partyNextAvailableTs : nextAvailableTs,\n nextAvailableLoading,\n },\n schedule: { openDays: openDaysForVisit, settled: scheduleSettled },\n derived: {\n canAdvance: wizard.canAdvance(state),\n party,\n total: wizard.totalPriceOre(state.items, state.startTs),\n summary: wizard.summaryLine(state, resolveStylistName),\n summaryParts: wizard.summaryParts(state, resolveStylistName),\n visitEnd:\n state.startTs === null\n ? null\n : wizard.visitEndTs(state.items, state.startTs, state.partyMode),\n },\n pickService,\n pickServiceFor,\n toggleServiceFor,\n continueFromService,\n pickResource,\n pickSlot,\n pickPartySlot,\n next,\n submit,\n submitting,\n submissionNonce,\n confirmed,\n startOver,\n refresh,\n /** The recent reads that failed, oldest first (`onEvent`'s `read_error`). */\n readErrors,\n restore: { hydrated, restoring, linked, resuming },\n };\n}\n\nexport type BookingController = ReturnType<typeof useBooking>;\n"],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@ import { WizardLabels } from "../labels/wizard.mjs";
|
|
|
5
5
|
import { BookingLabels, BookingLabelsInput } from "../labels/index.mjs";
|
|
6
6
|
import { BookingKit } from "../kit.mjs";
|
|
7
7
|
import { BookingClassNames, BookingComponents, BookingOverrides, BookingProvider, BookingProviderProps, ResolvedBooking, useBookingKit } from "../Provider.mjs";
|
|
8
|
-
import { BookingConfirmation, BookingController, BookingSeed, BookingSuggestion, ConfirmedLine, UseBookingOptions, confirmationLines, isCompleteConfirmation, personForChild, useBooking } from "../useBooking.mjs";
|
|
8
|
+
import { BookingConfirmation, BookingController, BookingReadEndpoint, BookingReadError, BookingSeed, BookingSuggestion, ConfirmedLine, UseBookingOptions, confirmationLines, isCompleteConfirmation, personForChild, useBooking } from "../useBooking.mjs";
|
|
9
9
|
import { BookingWizard, BookingWizardEvent, BookingWizardProps } from "../BookingWizard.mjs";
|
|
10
10
|
import { LoginSheet, LoginSheetProps, VippsConfirm } from "../LoginSheet.mjs";
|
|
11
|
-
export { type ActionAnswer, type ActionFailure, BookingClassNames, BookingComponents, BookingConfirmation, BookingController, type BookingKit, type BookingLabels, type BookingLabelsInput, BookingOverrides, BookingProvider, BookingProviderProps, BookingSeed, BookingSuggestion, BookingWizard, BookingWizardEvent, BookingWizardProps, ConfirmedLine, type InvalidFailure, type LoginLabels, LoginSheet, LoginSheetProps, type PersonActionInput, type PersonActionResult, type PersonActionTarget, type PortalActions, type ProfileActionResult, ResolvedBooking, type SafeActionEnvelope, type ScreenLabels, type SessionFailure, type ThrottledFailure, UseBookingOptions, type VippsConfirm, type WizardLabels, confirmationLines, isCompleteConfirmation, personForChild, useBooking, useBookingKit };
|
|
11
|
+
export { type ActionAnswer, type ActionFailure, BookingClassNames, BookingComponents, BookingConfirmation, BookingController, type BookingKit, type BookingLabels, type BookingLabelsInput, BookingOverrides, BookingProvider, BookingProviderProps, BookingReadEndpoint, BookingReadError, BookingSeed, BookingSuggestion, BookingWizard, BookingWizardEvent, BookingWizardProps, ConfirmedLine, type InvalidFailure, type LoginLabels, LoginSheet, LoginSheetProps, type PersonActionInput, type PersonActionResult, type PersonActionTarget, type PortalActions, type ProfileActionResult, ResolvedBooking, type SafeActionEnvelope, type ScreenLabels, type SessionFailure, type ThrottledFailure, UseBookingOptions, type VippsConfirm, type WizardLabels, confirmationLines, isCompleteConfirmation, personForChild, useBooking, useBookingKit };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medalsocial/booking",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Medal Social booking: the wizard rules, clock, money and portal helpers behind a Medal booking site",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Medal Social / Ali Aljumaili",
|
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
"typescript"
|
|
81
81
|
],
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@medalsocial/sdk": "^1.
|
|
83
|
+
"@medalsocial/sdk": "^1.15.0",
|
|
84
84
|
"server-only": "^0.0.1"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@medalsocial/meda": "^3.
|
|
87
|
+
"@medalsocial/meda": "^3.8.0",
|
|
88
88
|
"lucide-react": "^1.8.0",
|
|
89
89
|
"next": ">=16.3",
|
|
90
90
|
"react": ">=19",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@medalsocial/meda": "^3.
|
|
103
|
+
"@medalsocial/meda": "^3.8.0",
|
|
104
104
|
"@testing-library/jest-dom": "^7.0.1",
|
|
105
105
|
"@testing-library/react": "^16.3.3",
|
|
106
106
|
"@testing-library/user-event": "^14.6.7",
|