@medalsocial/booking 0.2.0 → 0.2.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @medalsocial/booking
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#189](https://github.com/Medal-Social/MedalSocial-SDK/pull/189) [`352b030`](https://github.com/Medal-Social/MedalSocial-SDK/commit/352b030d1fbae39a8fdb82bc454b967f6821ac58) Thanks [@alioftech](https://github.com/alioftech)! - A named stylist's calendar follows that stylist's open days. «First available» keeps the salon's. The seeded schedule is the salon's week and no longer answers once a stylist is chosen; the wizard refetches `/schedule` with `resource_id`.
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
@@ -46,11 +46,12 @@ function freshSlotsOf(raw, lost) {
46
46
  * added only when there are some, so a one-service visit asks with exactly the
47
47
  * URL it always did — the slot cache and every seeded answer are keyed by it.
48
48
  */
49
- function windowQuery(key, fromTs, toTs) {
49
+ function windowQuery(key, fromTs, toTs, resourceId) {
50
50
  const [serviceId, ...extras] = key.split("+");
51
51
  return new URLSearchParams({
52
52
  service_id: serviceId,
53
53
  ...extras.length === 0 ? {} : { extra_service_ids: extras.join(",") },
54
+ ...resourceId ? { resource_id: resourceId } : {},
54
55
  from_ts: String(fromTs),
55
56
  to_ts: String(toTs)
56
57
  });
@@ -797,38 +798,50 @@ function useBooking(options) {
797
798
  });
798
799
  }, [resources, primaryServiceId]);
799
800
  /**
800
- * The business's open dates over the window, for the first person's VISIT
801
- * (cutoffs are per service, and `visitTailMinutes` assumes the answer was for
802
- * item 0's whole visit — see the machine).
801
+ * The open dates over the window, for the first person's VISIT (cutoffs are
802
+ * per service, and `visitTailMinutes` assumes the answer was for item 0's
803
+ * whole visit — see the machine).
804
+ *
805
+ * The seed is the SALON's week, which is what «first available» draws. A named
806
+ * stylist works a different week, so their hours are fetched with `resource_id`
807
+ * and the seed is not allowed to answer for them — a seeded salon week would
808
+ * otherwise paint every open day as theirs.
803
809
  */
804
810
  const [fetchedSchedule, setFetchedSchedule] = useState(null);
805
811
  const [scheduleFailedFor, setScheduleFailedFor] = useState(null);
812
+ const scheduleKey = primaryVisit === null ? null : state.resourceId === null ? primaryVisit : `${primaryVisit}#${state.resourceId}`;
806
813
  const openDays = useMemo(() => {
807
- if (primaryVisit === null) return null;
808
- const seeded = seededSchedule[primaryVisit];
809
- if (seeded) return seeded;
810
- return fetchedSchedule?.key === primaryVisit ? fetchedSchedule.days : null;
814
+ if (primaryVisit === null || scheduleKey === null) return null;
815
+ if (fetchedSchedule?.key === scheduleKey) return fetchedSchedule.days;
816
+ if (state.resourceId === null) return seededSchedule[primaryVisit] ?? null;
817
+ if (scheduleFailedFor === scheduleKey) return null;
818
+ return seededSchedule[primaryVisit] ?? (fetchedSchedule?.key === primaryVisit ? fetchedSchedule.days : null);
811
819
  }, [
812
820
  primaryVisit,
821
+ state.resourceId,
822
+ scheduleKey,
813
823
  seededSchedule,
814
- fetchedSchedule
824
+ fetchedSchedule,
825
+ scheduleFailedFor
815
826
  ]);
816
827
  useEffect(() => {
817
- if (primaryVisit === null) return;
818
- if (primaryVisit in seededSchedule) return;
828
+ if (primaryVisit === null || scheduleKey === null) return;
829
+ if (state.resourceId === null && primaryVisit in seededSchedule) return;
819
830
  let cancelled = false;
831
+ const key = scheduleKey;
832
+ const resourceId = state.resourceId;
820
833
  (async () => {
821
834
  try {
822
- const response = await fetch(`${api}/schedule?${windowQuery(primaryVisit, fromTs, toTs)}`);
835
+ const response = await fetch(`${api}/schedule?${windowQuery(primaryVisit, fromTs, toTs, resourceId)}`);
823
836
  if (!response.ok) throw new Error(`schedule ${response.status}`);
824
837
  const body = await response.json();
825
838
  if (cancelled) return;
826
839
  setFetchedSchedule({
827
- key: primaryVisit,
840
+ key,
828
841
  days: body.days ?? []
829
842
  });
830
843
  } catch {
831
- if (!cancelled) setScheduleFailedFor(primaryVisit);
844
+ if (!cancelled) setScheduleFailedFor(key);
832
845
  }
833
846
  })();
834
847
  return () => {
@@ -839,7 +852,9 @@ function useBooking(options) {
839
852
  primaryVisit,
840
853
  fromTs,
841
854
  toTs,
842
- seededSchedule
855
+ seededSchedule,
856
+ state.resourceId,
857
+ scheduleKey
843
858
  ]);
844
859
  /** A stylist's display name for an id; `''` from Medal reads as none. */
845
860
  const resolveStylistName = useMemo(() => (resourceId) => {
@@ -889,7 +904,7 @@ function useBooking(options) {
889
904
  ]);
890
905
  const party = wizard.showsPartyMode(state);
891
906
  const haveEveryService = basketVisits.every((key) => key in slots);
892
- const scheduleSettled = openDays !== null || primaryVisit !== null && scheduleFailedFor === primaryVisit;
907
+ const scheduleSettled = openDays !== null || scheduleKey !== null && scheduleFailedFor === scheduleKey;
893
908
  const nextAvailableLoading = party ? !haveEveryService && !slotsFailed : primaryVisit !== null && !(primaryVisit in seededNextAvailable) && fetchedNextAvailable?.key !== primaryVisit;
894
909
  /** The basket's openings, unfiltered — the parallel search needs two stylists. */
895
910
  const basketSlots = useMemo(() => {
@@ -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 } from '../core/draft-store';\nimport { fill, labelText } from '../core/labels';\nimport type {\n WizardAction,\n WizardPerson,\n WizardPrefill,\n WizardService,\n 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\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/** 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(key: string, fromTs: number, toTs: number): 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 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/** 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 if (prefill === null) {\n if (seated === null) return wizard.initialState();\n return 'adult' in seated ? withAdult() : withGuests(seated.children);\n }\n return wizard.applyPrefill(base, prefill, prefillCatalogue(services, resources));\n }\n\n return { prefillFromQuery, deepLinkFromQuery, heldLink, initialWizardState };\n}\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 const guardian = signedIn?.guardian ?? arrivedAs;\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 /** Guest seats go when a family appears over them (`seatFamily`). */\n const guestSeated = family !== null && state.people.some(wizard.isGuestSeat);\n // biome-ignore lint/correctness/useExhaustiveDependencies: re-asked on every step change, as the source did.\n useEffect(() => {\n if (guestSeated) dispatch({ type: 'seatFamily' });\n }, [guestSeated, state.step]);\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 // 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 kit.drafts.clearDraft();\n return;\n }\n kit.drafts.stashDraft({\n items: state.items.map((item) => ({\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: item.bookedForName ?? null,\n bookedForBirthYear: item.bookedForBirthYear ?? null,\n adult: item.adult === true,\n personId: item.bookedForPersonId ?? null,\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 state.items,\n state.resourceId,\n state.partyMode,\n state.startTs,\n state.resolvedResourceId,\n state.partyResourceIds,\n ]);\n\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 });\n\n /**\n * The logged-in parent's details into the fields — once per parent, into\n * fields still BLANK or still holding exactly what the PREVIOUS parent's\n * profile put there (a shared device), never over what the visitor typed.\n */\n const guardianFilled = useRef<{ email: string; wrote: WizardState['contact'] } | null>(null);\n useEffect(() => {\n if (guardian === null || guardianFilled.current?.email === guardian.email) return;\n const previous = guardianFilled.current?.wrote;\n const contact = latestContact.current;\n const wrote = {\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 guardianFilled.current = { email: guardian.email, wrote };\n\n for (const field of ['name', 'phone', 'email'] as const) {\n const value = wrote[field];\n if (value === '') continue;\n const held = contact[field].trim();\n if (held !== '' && held !== previous?.[field]) continue;\n dispatch({ type: 'setContact', field, value });\n }\n }, [guardian, 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 business's open dates over the window, for the first person's VISIT\n * (cutoffs are per service, and `visitTailMinutes` assumes the answer was for\n * item 0's whole visit — see the machine).\n */\n const [fetchedSchedule, setFetchedSchedule] = useState<{\n key: string;\n days: BookingDayDto[];\n } | null>(null);\n const [scheduleFailedFor, setScheduleFailedFor] = useState<string | null>(null);\n\n // `null`, not `[]`, until this service's hours are in hand: «we do not know».\n const openDays = useMemo<BookingDayDto[] | null>(() => {\n if (primaryVisit === null) return null;\n const seeded = seededSchedule[primaryVisit];\n if (seeded) return seeded;\n return fetchedSchedule?.key === primaryVisit ? fetchedSchedule.days : null;\n }, [primaryVisit, seededSchedule, fetchedSchedule]);\n\n useEffect(() => {\n if (primaryVisit === null) return;\n if (primaryVisit in seededSchedule) return;\n let cancelled = false;\n\n (async () => {\n try {\n const response = await fetch(`${api}/schedule?${windowQuery(primaryVisit, fromTs, toTs)}`);\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: primaryVisit, days: body.days ?? [] });\n } catch {\n // Left `null`: the hours are unknown, which is not the same as «closed».\n if (!cancelled) setScheduleFailedFor(primaryVisit);\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [api, primaryVisit, fromTs, toTs, seededSchedule]);\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 || (primaryVisit !== null && scheduleFailedFor === primaryVisit);\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 body = withExtraServices(submission, state.items);\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 if (definite) {\n 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 (wasResumed) {\n if (definite) {\n dispatch({ type: 'startOver' });\n restartFromLink();\n } else 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 /** «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 dispatch({ type: 'startOver' });\n restartFromLink();\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 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 /** The parent: who arrived, who logged in from the sheet, and the merged answer. */\n login: {\n arrivedAs,\n signedIn,\n guardian,\n signIn: (who: BookingGuardian | null) => setSignedIn({ guardian: who }),\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 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 { 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 } from '../core/draft-store';\nimport { fill, labelText } from '../core/labels';\nimport type {\n WizardAction,\n WizardPerson,\n WizardPrefill,\n WizardService,\n 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\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/** 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/** 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 if (prefill === null) {\n if (seated === null) return wizard.initialState();\n return 'adult' in seated ? withAdult() : withGuests(seated.children);\n }\n return wizard.applyPrefill(base, prefill, prefillCatalogue(services, resources));\n }\n\n return { prefillFromQuery, deepLinkFromQuery, heldLink, initialWizardState };\n}\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 const guardian = signedIn?.guardian ?? arrivedAs;\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 /** Guest seats go when a family appears over them (`seatFamily`). */\n const guestSeated = family !== null && state.people.some(wizard.isGuestSeat);\n // biome-ignore lint/correctness/useExhaustiveDependencies: re-asked on every step change, as the source did.\n useEffect(() => {\n if (guestSeated) dispatch({ type: 'seatFamily' });\n }, [guestSeated, state.step]);\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 // 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 kit.drafts.clearDraft();\n return;\n }\n kit.drafts.stashDraft({\n items: state.items.map((item) => ({\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: item.bookedForName ?? null,\n bookedForBirthYear: item.bookedForBirthYear ?? null,\n adult: item.adult === true,\n personId: item.bookedForPersonId ?? null,\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 state.items,\n state.resourceId,\n state.partyMode,\n state.startTs,\n state.resolvedResourceId,\n state.partyResourceIds,\n ]);\n\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 });\n\n /**\n * The logged-in parent's details into the fields — once per parent, into\n * fields still BLANK or still holding exactly what the PREVIOUS parent's\n * profile put there (a shared device), never over what the visitor typed.\n */\n const guardianFilled = useRef<{ email: string; wrote: WizardState['contact'] } | null>(null);\n useEffect(() => {\n if (guardian === null || guardianFilled.current?.email === guardian.email) return;\n const previous = guardianFilled.current?.wrote;\n const contact = latestContact.current;\n const wrote = {\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 guardianFilled.current = { email: guardian.email, wrote };\n\n for (const field of ['name', 'phone', 'email'] as const) {\n const value = wrote[field];\n if (value === '') continue;\n const held = contact[field].trim();\n if (held !== '' && held !== previous?.[field]) continue;\n dispatch({ type: 'setContact', field, value });\n }\n }, [guardian, 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\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) setScheduleFailedFor(key);\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 body = withExtraServices(submission, state.items);\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 if (definite) {\n 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 (wasResumed) {\n if (definite) {\n dispatch({ type: 'startOver' });\n restartFromLink();\n } else 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 /** «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 dispatch({ type: 'startOver' });\n restartFromLink();\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 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 /** The parent: who arrived, who logged in from the sheet, and the merged answer. */\n login: {\n arrivedAs,\n signedIn,\n guardian,\n signIn: (who: BookingGuardian | null) => setSignedIn({ guardian: who }),\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 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/booking",
3
- "version": "0.2.0",
3
+ "version": "0.2.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",