@medalsocial/booking 0.0.0-placeholder.0 → 0.1.0

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.
Files changed (261) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +220 -1
  5. package/dist/core/age.d.mts +34 -0
  6. package/dist/core/age.mjs +95 -0
  7. package/dist/core/age.mjs.map +1 -0
  8. package/dist/core/attempt-store.d.mts +111 -0
  9. package/dist/core/attempt-store.mjs +159 -0
  10. package/dist/core/attempt-store.mjs.map +1 -0
  11. package/dist/core/categories.d.mts +27 -0
  12. package/dist/core/categories.mjs +42 -0
  13. package/dist/core/categories.mjs.map +1 -0
  14. package/dist/core/clock.d.mts +33 -0
  15. package/dist/core/clock.mjs +251 -0
  16. package/dist/core/clock.mjs.map +1 -0
  17. package/dist/core/config.d.mts +171 -0
  18. package/dist/core/config.mjs +392 -0
  19. package/dist/core/config.mjs.map +1 -0
  20. package/dist/core/consent.d.mts +14 -0
  21. package/dist/core/consent.mjs +17 -0
  22. package/dist/core/consent.mjs.map +1 -0
  23. package/dist/core/deep-link.d.mts +40 -0
  24. package/dist/core/deep-link.mjs +160 -0
  25. package/dist/core/deep-link.mjs.map +1 -0
  26. package/dist/core/display-name.d.mts +18 -0
  27. package/dist/core/display-name.mjs +30 -0
  28. package/dist/core/display-name.mjs.map +1 -0
  29. package/dist/core/draft-store.d.mts +83 -0
  30. package/dist/core/draft-store.mjs +115 -0
  31. package/dist/core/draft-store.mjs.map +1 -0
  32. package/dist/core/dto.d.mts +46 -0
  33. package/dist/core/dto.mjs +157 -0
  34. package/dist/core/dto.mjs.map +1 -0
  35. package/dist/core/ics.d.mts +63 -0
  36. package/dist/core/ics.mjs +130 -0
  37. package/dist/core/ics.mjs.map +1 -0
  38. package/dist/core/index.d.mts +34 -0
  39. package/dist/core/index.mjs +31 -0
  40. package/dist/core/labels.d.mts +88 -0
  41. package/dist/core/labels.mjs +93 -0
  42. package/dist/core/labels.mjs.map +1 -0
  43. package/dist/core/machine.d.mts +598 -0
  44. package/dist/core/machine.mjs +878 -0
  45. package/dist/core/machine.mjs.map +1 -0
  46. package/dist/core/money.d.mts +12 -0
  47. package/dist/core/money.mjs +51 -0
  48. package/dist/core/money.mjs.map +1 -0
  49. package/dist/core/next-available.d.mts +20 -0
  50. package/dist/core/next-available.mjs +36 -0
  51. package/dist/core/next-available.mjs.map +1 -0
  52. package/dist/core/next-free.d.mts +25 -0
  53. package/dist/core/next-free.mjs +44 -0
  54. package/dist/core/next-free.mjs.map +1 -0
  55. package/dist/core/party-slots.d.mts +72 -0
  56. package/dist/core/party-slots.mjs +223 -0
  57. package/dist/core/party-slots.mjs.map +1 -0
  58. package/dist/core/paths.d.mts +11 -0
  59. package/dist/core/paths.mjs +56 -0
  60. package/dist/core/paths.mjs.map +1 -0
  61. package/dist/core/phone.d.mts +17 -0
  62. package/dist/core/phone.mjs +58 -0
  63. package/dist/core/phone.mjs.map +1 -0
  64. package/dist/core/portal/cookies.d.mts +29 -0
  65. package/dist/core/portal/dto.d.mts +140 -0
  66. package/dist/core/portal/dto.mjs +142 -0
  67. package/dist/core/portal/dto.mjs.map +1 -0
  68. package/dist/core/portal/family.d.mts +39 -0
  69. package/dist/core/portal/family.mjs +39 -0
  70. package/dist/core/portal/family.mjs.map +1 -0
  71. package/dist/core/portal/login-input.d.mts +29 -0
  72. package/dist/core/portal/login-input.mjs +29 -0
  73. package/dist/core/portal/login-input.mjs.map +1 -0
  74. package/dist/core/portal/next-path.d.mts +12 -0
  75. package/dist/core/portal/next-path.mjs +98 -0
  76. package/dist/core/portal/next-path.mjs.map +1 -0
  77. package/dist/core/portal/return-path.d.mts +7 -0
  78. package/dist/core/portal/return-path.mjs +49 -0
  79. package/dist/core/portal/return-path.mjs.map +1 -0
  80. package/dist/core/portal/session-cookie.d.mts +27 -0
  81. package/dist/core/portal/session-cookie.mjs +30 -0
  82. package/dist/core/portal/session-cookie.mjs.map +1 -0
  83. package/dist/core/portal/vipps-link.d.mts +56 -0
  84. package/dist/core/portal/vipps-link.mjs +136 -0
  85. package/dist/core/portal/vipps-link.mjs.map +1 -0
  86. package/dist/core/portal/vipps-return.d.mts +39 -0
  87. package/dist/core/portal/vipps-return.mjs +69 -0
  88. package/dist/core/portal/vipps-return.mjs.map +1 -0
  89. package/dist/core/rebook-store.d.mts +28 -0
  90. package/dist/core/rebook-store.mjs +60 -0
  91. package/dist/core/rebook-store.mjs.map +1 -0
  92. package/dist/core/restore-gate.d.mts +40 -0
  93. package/dist/core/restore-gate.mjs +96 -0
  94. package/dist/core/restore-gate.mjs.map +1 -0
  95. package/dist/core/stores.d.mts +8 -0
  96. package/dist/core/stores.mjs +23 -0
  97. package/dist/core/stores.mjs.map +1 -0
  98. package/dist/core/types.d.mts +241 -0
  99. package/dist/core/wire.d.mts +37 -0
  100. package/dist/next/cache/memory.d.mts +14 -0
  101. package/dist/next/cache/memory.mjs +51 -0
  102. package/dist/next/cache/memory.mjs.map +1 -0
  103. package/dist/next/cache/next-data.d.mts +6 -0
  104. package/dist/next/cache/next-data.mjs +35 -0
  105. package/dist/next/cache/next-data.mjs.map +1 -0
  106. package/dist/next/cache/noop.d.mts +6 -0
  107. package/dist/next/cache/noop.mjs +15 -0
  108. package/dist/next/cache/noop.mjs.map +1 -0
  109. package/dist/next/cache/workers.d.mts +29 -0
  110. package/dist/next/cache/workers.mjs +74 -0
  111. package/dist/next/cache/workers.mjs.map +1 -0
  112. package/dist/next/catalogue.d.mts +38 -0
  113. package/dist/next/catalogue.mjs +197 -0
  114. package/dist/next/catalogue.mjs.map +1 -0
  115. package/dist/next/handler.d.mts +11 -0
  116. package/dist/next/handler.mjs +150 -0
  117. package/dist/next/handler.mjs.map +1 -0
  118. package/dist/next/index.d.mts +17 -0
  119. package/dist/next/index.mjs +14 -0
  120. package/dist/next/loaders.d.mts +79 -0
  121. package/dist/next/loaders.mjs +195 -0
  122. package/dist/next/loaders.mjs.map +1 -0
  123. package/dist/next/manage-dto.d.mts +6 -0
  124. package/dist/next/manage-dto.mjs +28 -0
  125. package/dist/next/manage-dto.mjs.map +1 -0
  126. package/dist/next/medal.d.mts +98 -0
  127. package/dist/next/medal.mjs +154 -0
  128. package/dist/next/medal.mjs.map +1 -0
  129. package/dist/next/options.d.mts +158 -0
  130. package/dist/next/options.mjs +65 -0
  131. package/dist/next/options.mjs.map +1 -0
  132. package/dist/next/portal/guardian.d.mts +42 -0
  133. package/dist/next/portal/guardian.mjs +76 -0
  134. package/dist/next/portal/guardian.mjs.map +1 -0
  135. package/dist/next/portal/medal-portal.d.mts +161 -0
  136. package/dist/next/portal/medal-portal.mjs +825 -0
  137. package/dist/next/portal/medal-portal.mjs.map +1 -0
  138. package/dist/next/portal/session.d.mts +38 -0
  139. package/dist/next/portal/session.mjs +40 -0
  140. package/dist/next/portal/session.mjs.map +1 -0
  141. package/dist/next/portal/vipps-flash.d.mts +14 -0
  142. package/dist/next/portal/vipps-flash.mjs +41 -0
  143. package/dist/next/portal/vipps-flash.mjs.map +1 -0
  144. package/dist/next/portal.d.mts +205 -0
  145. package/dist/next/portal.mjs +469 -0
  146. package/dist/next/portal.mjs.map +1 -0
  147. package/dist/next/request.d.mts +51 -0
  148. package/dist/next/request.mjs +123 -0
  149. package/dist/next/request.mjs.map +1 -0
  150. package/dist/next/routes/catalogue-routes.mjs +330 -0
  151. package/dist/next/routes/catalogue-routes.mjs.map +1 -0
  152. package/dist/next/routes/create.mjs +422 -0
  153. package/dist/next/routes/create.mjs.map +1 -0
  154. package/dist/next/routes/manage.mjs +191 -0
  155. package/dist/next/routes/manage.mjs.map +1 -0
  156. package/dist/next/routes/portal-routes.mjs +312 -0
  157. package/dist/next/routes/portal-routes.mjs.map +1 -0
  158. package/dist/next/routes/shared.mjs +134 -0
  159. package/dist/next/routes/shared.mjs.map +1 -0
  160. package/dist/next/routes/vipps-return.mjs +166 -0
  161. package/dist/next/routes/vipps-return.mjs.map +1 -0
  162. package/dist/next/runtime.d.mts +49 -0
  163. package/dist/next/runtime.mjs +94 -0
  164. package/dist/next/runtime.mjs.map +1 -0
  165. package/dist/next/seed.d.mts +71 -0
  166. package/dist/next/seed.mjs +495 -0
  167. package/dist/next/seed.mjs.map +1 -0
  168. package/dist/next/server.d.mts +26 -0
  169. package/dist/next/server.mjs +63 -0
  170. package/dist/next/server.mjs.map +1 -0
  171. package/dist/react/BookingLink.d.mts +61 -0
  172. package/dist/react/BookingLink.mjs +102 -0
  173. package/dist/react/BookingLink.mjs.map +1 -0
  174. package/dist/react/BookingWizard.d.mts +49 -0
  175. package/dist/react/BookingWizard.mjs +577 -0
  176. package/dist/react/BookingWizard.mjs.map +1 -0
  177. package/dist/react/LoginPageQuery.d.mts +37 -0
  178. package/dist/react/LoginPageQuery.mjs +106 -0
  179. package/dist/react/LoginPageQuery.mjs.map +1 -0
  180. package/dist/react/LoginSheet.d.mts +49 -0
  181. package/dist/react/LoginSheet.mjs +98 -0
  182. package/dist/react/LoginSheet.mjs.map +1 -0
  183. package/dist/react/ManageBooking.d.mts +50 -0
  184. package/dist/react/ManageBooking.mjs +197 -0
  185. package/dist/react/ManageBooking.mjs.map +1 -0
  186. package/dist/react/PortalDashboard.d.mts +10 -0
  187. package/dist/react/Provider.d.mts +82 -0
  188. package/dist/react/Provider.mjs +132 -0
  189. package/dist/react/Provider.mjs.map +1 -0
  190. package/dist/react/actions.d.mts +133 -0
  191. package/dist/react/actions.mjs +90 -0
  192. package/dist/react/actions.mjs.map +1 -0
  193. package/dist/react/index.d.mts +28 -0
  194. package/dist/react/index.mjs +20 -0
  195. package/dist/react/kit.d.mts +54 -0
  196. package/dist/react/kit.mjs +127 -0
  197. package/dist/react/kit.mjs.map +1 -0
  198. package/dist/react/labels/index.d.mts +23 -0
  199. package/dist/react/labels/index.mjs +44 -0
  200. package/dist/react/labels/index.mjs.map +1 -0
  201. package/dist/react/labels/login.d.mts +10 -0
  202. package/dist/react/labels/login.mjs +13 -0
  203. package/dist/react/labels/login.mjs.map +1 -0
  204. package/dist/react/labels/manage.d.mts +15 -0
  205. package/dist/react/labels/manage.mjs +19 -0
  206. package/dist/react/labels/manage.mjs.map +1 -0
  207. package/dist/react/labels/portal.d.mts +25 -0
  208. package/dist/react/labels/portal.mjs +41 -0
  209. package/dist/react/labels/portal.mjs.map +1 -0
  210. package/dist/react/labels/screens.d.mts +11 -0
  211. package/dist/react/labels/screens.mjs +709 -0
  212. package/dist/react/labels/screens.mjs.map +1 -0
  213. package/dist/react/labels/wizard.d.mts +69 -0
  214. package/dist/react/labels/wizard.mjs +95 -0
  215. package/dist/react/labels/wizard.mjs.map +1 -0
  216. package/dist/react/link/index.d.mts +5 -0
  217. package/dist/react/link/index.mjs +5 -0
  218. package/dist/react/portal/SessionTouch.d.mts +19 -0
  219. package/dist/react/portal/SessionTouch.mjs +40 -0
  220. package/dist/react/portal/SessionTouch.mjs.map +1 -0
  221. package/dist/react/portal/age-prompt.d.mts +10 -0
  222. package/dist/react/portal/age-prompt.mjs +21 -0
  223. package/dist/react/portal/age-prompt.mjs.map +1 -0
  224. package/dist/react/portal/book-again.d.mts +18 -0
  225. package/dist/react/portal/book-again.mjs +21 -0
  226. package/dist/react/portal/book-again.mjs.map +1 -0
  227. package/dist/react/portal/child-summary.d.mts +17 -0
  228. package/dist/react/portal/child-summary.mjs +45 -0
  229. package/dist/react/portal/child-summary.mjs.map +1 -0
  230. package/dist/react/portal/dashboard.d.mts +74 -0
  231. package/dist/react/portal/dashboard.mjs +204 -0
  232. package/dist/react/portal/dashboard.mjs.map +1 -0
  233. package/dist/react/portal/leave.d.mts +17 -0
  234. package/dist/react/portal/leave.mjs +21 -0
  235. package/dist/react/portal/leave.mjs.map +1 -0
  236. package/dist/react/portal/sections.d.mts +12 -0
  237. package/dist/react/portal/sections.mjs +48 -0
  238. package/dist/react/portal/sections.mjs.map +1 -0
  239. package/dist/react/portal/tabs.d.mts +23 -0
  240. package/dist/react/portal/tabs.mjs +36 -0
  241. package/dist/react/portal/tabs.mjs.map +1 -0
  242. package/dist/react/portal/vipps-flash.d.mts +11 -0
  243. package/dist/react/portal/vipps-flash.mjs +18 -0
  244. package/dist/react/portal/vipps-flash.mjs.map +1 -0
  245. package/dist/react/portal/wired.mjs +202 -0
  246. package/dist/react/portal/wired.mjs.map +1 -0
  247. package/dist/react/screen-labels.mjs +14 -0
  248. package/dist/react/screen-labels.mjs.map +1 -0
  249. package/dist/react/shared.d.mts +13 -0
  250. package/dist/react/shared.mjs +7 -0
  251. package/dist/react/use-next-free.d.mts +19 -0
  252. package/dist/react/use-next-free.mjs +56 -0
  253. package/dist/react/use-next-free.mjs.map +1 -0
  254. package/dist/react/useBooking.d.mts +177 -0
  255. package/dist/react/useBooking.mjs +1205 -0
  256. package/dist/react/useBooking.mjs.map +1 -0
  257. package/dist/react/wizard/adapters.mjs +187 -0
  258. package/dist/react/wizard/adapters.mjs.map +1 -0
  259. package/dist/react/wizard/index.d.mts +11 -0
  260. package/dist/react/wizard/index.mjs +6 -0
  261. package/package.json +126 -5
@@ -0,0 +1,598 @@
1
+ import { BookingConfig } from "./config.mjs";
2
+ //#region src/core/machine.d.ts
3
+ /**
4
+ * The three steps the design draws, and the three the machine holds.
5
+ *
6
+ * `when` is one screen and two questions — the stylist and the hour — because
7
+ * that is what a parent answers in one breath and what the frames show as
8
+ * «Frisør og tid». They were separate steps until the redesign, and the pair of
9
+ * them cost a parent a whole extra «Neste» to say a thing they had already
10
+ * decided.
11
+ *
12
+ * There is no `login` step, and there was one. It sat between the hour and
13
+ * «Bekreft» and asked who the parent was — Vipps, an e-mail code, or neither —
14
+ * at the one moment a walk-in parent least wants a decision. The login is an
15
+ * offer in a sheet now (the login sheet), opened from a row
16
+ * on `service` and `details`, and the machine knows nothing about it: a login
17
+ * changes who the contact fields are filled from, never which step is next.
18
+ */
19
+ export type WizardStep = 'who' | 'service' | 'when' | 'details';
20
+ /**
21
+ * «Hvem skal klippes?» — step 1's answer (SP10), one entry per chair.
22
+ *
23
+ * The wizard used to learn who a haircut was for last, in two text fields on
24
+ * «Bekreft», and a logged-in parent's children were a row of name chips there.
25
+ * Asking first is what lets everything after it be ABOUT the child: step 2
26
+ * offers «Samme som sist» for Theo, the ages that filter the menu are Theo's
27
+ * on the appointment day, and the booking goes to Medal as Theo's person id
28
+ * rather than a name a parent typed.
29
+ *
30
+ * `key` identifies the chair across edits and is never shown: `p:<id>` for a
31
+ * saved child, `self` for the logged-in parent, `adult` for a guest adult,
32
+ * `guest:<n>` for a guest's n-th child. Everything else is optional and ABSENT
33
+ * rather than blank, the rule the line items follow — a guest child has no
34
+ * name until «Bekreft» asks for one.
35
+ */
36
+ export interface WizardPerson {
37
+ key: string;
38
+ /** A grown-up: no child fields on «Bekreft», the full menu on step 2. */
39
+ adult?: boolean;
40
+ /** Medal's id for a saved child — what `booked_for_person_id` carries. */
41
+ personId?: string;
42
+ name?: string;
43
+ birthYear?: number;
44
+ /** 1–12, when the parent gave one. */
45
+ birthMonth?: number;
46
+ }
47
+ /** The logged-in parent themselves, on step 1's «Meg selv (voksen)». */
48
+ export declare const SELF_KEY = "self";
49
+ /**
50
+ * A seat only a GUEST's step 1 can draw: a count chip's `guest:n`, a child
51
+ * named in the guest sheet (`new:…`), the «Voksen» chip's `adult`. A parent's
52
+ * step 1 draws cards for saved children and «Meg selv» and nothing else — so
53
+ * one of these under a family is a seat nobody can see or untick, counting
54
+ * towards the three.
55
+ */
56
+ export declare function isGuestSeat(person: WizardPerson): boolean;
57
+ export interface WizardService {
58
+ id: string;
59
+ name: string;
60
+ category: string;
61
+ durationMinutes: number;
62
+ priceOre: number;
63
+ maxPerBooking: number;
64
+ /** Drives step 3's weekend note and the surcharged price shown on the chip. */
65
+ weekendSurchargePct: number;
66
+ /**
67
+ * The salon's prep and cleanup time around this service, in minutes.
68
+ *
69
+ * Carried because a FAMILY needs them: two children seated back to back on
70
+ * one stylist are separated by the first one's cleanup plus the second one's
71
+ * prep, and `itemStartTimes` cannot honour a gap it does not know about.
72
+ * Availability is asked per service and each opening is judged on its own, so
73
+ * both instants can come back free while the engine refuses the combined
74
+ * submission — a visit offered to the parent and then rejected.
75
+ *
76
+ * Zero is the ordinary value and the wire's own default.
77
+ */
78
+ bufferBeforeMinutes: number;
79
+ bufferAfterMinutes: number;
80
+ /**
81
+ * Who the service is for, in whole years, inclusive (SP10) — «Barnehageklipp»
82
+ * up to 6, say. Used to SUGGEST, never to refuse: Medal does not reject a
83
+ * booking on age, and a parent may know better. Absent is «any age».
84
+ */
85
+ ageMinYears?: number;
86
+ ageMaxYears?: number;
87
+ }
88
+ export interface WizardItem {
89
+ service: WizardService;
90
+ /** The child this line is for. Asked inline in step 4. */
91
+ bookedForName?: string;
92
+ /** Optional fødselsår — powers "Jonas (9)" in the CRM later. */
93
+ bookedForBirthYear?: number;
94
+ /**
95
+ * The saved child this line is for (SP10) — sent as `booked_for_person_id`
96
+ * when the phone rule allows it (see the create route). Never typed: it
97
+ * comes only from a child card on step 1.
98
+ */
99
+ bookedForPersonId?: string;
100
+ /** The child's birth month, for the age on the appointment day. */
101
+ bookedForBirthMonth?: number;
102
+ /** A grown-up's line: «Bekreft» asks nothing about a child for it. */
103
+ adult?: boolean;
104
+ }
105
+ export interface WizardState {
106
+ step: WizardStep;
107
+ /**
108
+ * Who is coming — step 1's answer. One entry per chair, in the order the
109
+ * line items follow: `items[i]` is `people[i]`'s haircut once every chair
110
+ * has a service.
111
+ */
112
+ people: WizardPerson[];
113
+ /**
114
+ * Step 2's answers so far, one per person — `null` where that person has no
115
+ * service yet. `items` is built from these and `people` only once none is
116
+ * `null`, so a family half-way through step 2 has no basket and step 3
117
+ * stays shut.
118
+ */
119
+ choices: Array<WizardService | null>;
120
+ items: WizardItem[];
121
+ /** null = «Første ledige» — the default, and the utilisation-maximising one. */
122
+ resourceId: string | null;
123
+ /**
124
+ * Whether the stylist question has been ANSWERED, as against left at its
125
+ * default.
126
+ *
127
+ * `resourceId` cannot say. `null` is «Første ledige», which is both the value
128
+ * a fresh wizard starts on and a real choice a parent makes by tapping «Alle»
129
+ * — and while the stylist and the hour were separate steps the difference was
130
+ * legible in `step` alone: a visitor who had answered was on `time`. One
131
+ * screen for both questions took that away, and the thing that noticed is
132
+ * `applyPrefill`, whose whole contract is «fills blanks, never overwrites»:
133
+ * without this a «Bestill igjen» link naming Sara would land on a parent who
134
+ * had just tapped «Alle» and quietly replace their answer.
135
+ *
136
+ * Set by `pickResource` either way, and by the parallel `setPartyMode` — «two
137
+ * stylists at once» is an answer about who cuts the hair even though it
138
+ * clears the preference. Cleared whenever the preference itself is cleared
139
+ * for a reason the parent did not choose: a replaced basket, or a sibling the
140
+ * named stylist cannot take.
141
+ */
142
+ stylistAnswered: boolean;
143
+ partyMode: 'sequential' | 'parallel';
144
+ startTs: number | null;
145
+ resolvedResourceId: string | null;
146
+ /**
147
+ * The stylist each line resolved to, in basket order — the half of a party
148
+ * slot that `resolvedResourceId` cannot hold.
149
+ *
150
+ * `null` for everything except a party slot chosen on step 3, where
151
+ * `resolvedResourceId` still answers for the whole visit. Non-null it is one
152
+ * id per line item, and it is what a parallel party actually needs: two
153
+ * children seen at the same instant are seen by two *different* stylists, so
154
+ * there is no single id that describes the visit.
155
+ *
156
+ * Read through `itemResourceIds` rather than directly, so the submission and
157
+ * the confirmation card cannot disagree about who is cutting whose hair.
158
+ */
159
+ partyResourceIds: string[] | null;
160
+ contact: {
161
+ phone: string;
162
+ name: string;
163
+ email: string;
164
+ };
165
+ notes: string;
166
+ consentTerms: boolean;
167
+ consentMarketing: boolean;
168
+ /**
169
+ * A code, never a sentence. The Norwegian copy for each case belongs to the
170
+ * component that shows it, so the same rule can be a toast in one place and
171
+ * inline text in another without this file knowing either.
172
+ *
173
+ * Wider than what `reduce` can currently produce, on purpose. The submit route
174
+ * answers with `conflict`, `invalidInput`, `unconfigured` and `upstreamError`
175
+ * as well as `slotTaken`, and those have to land somewhere. Somewhere is here:
176
+ * if the union only admitted what this file raises, the details step would grow
177
+ * a second error slot of its own and the sticky bar and the form would start
178
+ * disagreeing about whether anything is wrong.
179
+ *
180
+ * `upstreamError` is the create route's 502 — Medal answered something this
181
+ * site has no mapping for, or did not answer at all. It is an outage rather
182
+ * than a mistake the visitor made, but it arrives with them waiting on a
183
+ * button they have just pressed, so it needs a sentence of its own rather
184
+ * than being folded into `conflict` — which says «prøv en gang til», and
185
+ * trying again is exactly what will not help here.
186
+ */
187
+ /**
188
+ * A service a link named for a parent whose family is known, held until
189
+ * step 1 says who it is for (`holdService`). Each person seated after that
190
+ * starts on it where it suits them — a kids' service for a child, anything
191
+ * else for a grown-up — instead of a guest chair being invented for it.
192
+ */
193
+ pendingService: WizardService | null;
194
+ error: 'maxParty' | 'slotTaken' | 'conflict' | 'invalidInput' | 'unconfigured' | 'upstreamError' |
195
+ /**
196
+ * The create route's 409 for a submission Medal already holds under its
197
+ * key: it may well be booked, so the parent is sent to their e-mail
198
+ * rather than to the button.
199
+ */
200
+ 'inProgress' | null;
201
+ }
202
+ export type WizardAction =
203
+ /**
204
+ * Back to an empty wizard, for «Bestill ny time» on the confirmation.
205
+ *
206
+ * A parent who has just booked one child and needs a SEPARATE appointment —
207
+ * another service whose `maxPerBooking` is 1, or a sibling who wants a
208
+ * different hour — has to be able to start over. The confirmation is now
209
+ * restored across a reload, so without this the only ways out of it are
210
+ * closing the tab or waiting for the stored attempt to age out.
211
+ */
212
+ {
213
+ type: 'startOver';
214
+ } |
215
+ /**
216
+ * Step 1's answer: who is coming. Replaces the party; a person who stays
217
+ * (the same `key`) keeps the service already chosen for them, so going back
218
+ * to add a sibling does not re-ask the first child. `advance` moves on to
219
+ * step 2 — a guest's «2 barn» chip is one tap, a logged-in parent ticks
220
+ * children and presses «Neste».
221
+ */
222
+ {
223
+ type: 'choosePeople';
224
+ people: WizardPerson[];
225
+ advance?: boolean;
226
+ /**
227
+ * «Samme som sist»: a service to start each person NEW to the party on,
228
+ * aligned with `people` — the child's last haircut, where the shell
229
+ * knows one. A person already in the party keeps their own answer.
230
+ */
231
+ services?: ReadonlyArray<WizardService | null>;
232
+ } |
233
+ /**
234
+ * Step 2's answer for ONE person. For a party of one it is the old one-tap
235
+ * `pickService` and moves on; for a family it fills that child's line and
236
+ * waits for the rest. `resourceServiceIds` is `addService`'s: what the named
237
+ * stylist can do, so a service they cannot do releases the preference.
238
+ */
239
+ {
240
+ type: 'pickServiceFor';
241
+ index: number;
242
+ service: WizardService;
243
+ resourceServiceIds?: readonly string[];
244
+ } | {
245
+ type: 'pickService';
246
+ service: WizardService;
247
+ } |
248
+ /**
249
+ * The parent's family has appeared (a login, a guardian read after the
250
+ * first render) or step 1 is shown again under it: the guest seats go —
251
+ * see `isGuestSeat` — and a guest «Voksen» becomes «Meg selv». Only while
252
+ * nothing is held on them (steps 1 and 2); from the hour on they are real
253
+ * chairs with a time, and «Bekreft» names them. A party emptied by it goes
254
+ * back to step 1, where the children are.
255
+ */
256
+ {
257
+ type: 'seatFamily';
258
+ } |
259
+ /**
260
+ * One person joins the party as it is WHEN this lands — a child the parent
261
+ * just created in Medal, after the await. Refused at the limit, a no-op for
262
+ * somebody already seated.
263
+ */
264
+ {
265
+ type: 'addPerson';
266
+ person: WizardPerson;
267
+ } |
268
+ /** A link's service, held for the people step 1 is about to seat — see `pendingService`. */
269
+ {
270
+ type: 'holdService';
271
+ service: WizardService | null;
272
+ } |
273
+ /**
274
+ * A sibling, added on step 1 — and the one fact about the stylist catalogue
275
+ * this file is allowed to know.
276
+ *
277
+ * `resourceServiceIds` is what the currently preferred stylist can do. It
278
+ * rides on the action because the alternative was worse in both directions:
279
+ * the machine cannot fetch the catalogue, and a shell that cleared the
280
+ * preference itself would need an action that changes `resourceId` without
281
+ * moving the visitor off step 1, which `pickResource` is not.
282
+ *
283
+ * Absent is «the caller does not know», not «the stylist does nothing». A
284
+ * missing list keeps the preference, because silence is not evidence.
285
+ */
286
+ {
287
+ type: 'addService';
288
+ service: WizardService;
289
+ resourceServiceIds?: readonly string[];
290
+ } | {
291
+ type: 'pickResource';
292
+ resourceId: string | null;
293
+ } | {
294
+ type: 'pickSlot';
295
+ startTs: number;
296
+ resourceId: string | null;
297
+ } |
298
+ /**
299
+ * How the party is seen — step 2's second question, and the one that decides
300
+ * which search step 3 runs.
301
+ *
302
+ * Its own action rather than a field on `pickResource`, because the two
303
+ * answers are independent: «Sara» and «rett etter hverandre» are one visit,
304
+ * and a parent who changes only the mode has not changed their mind about
305
+ * the stylist.
306
+ */
307
+ {
308
+ type: 'setPartyMode';
309
+ mode: WizardState['partyMode'];
310
+ } |
311
+ /**
312
+ * A whole family seated in one tap: the instant the visit starts and the
313
+ * stylist each child got.
314
+ *
315
+ * Separate from `pickSlot` because a party slot answers a question
316
+ * `pickSlot` has no room for. `pickSlot` carries one resolved stylist, which
317
+ * is all a single booking ever has; a party in `parallel` has one per child
318
+ * and they are deliberately different people.
319
+ *
320
+ * `resourceIds` is one id per line item, in basket order — never null,
321
+ * because a slot that cannot name the two stylists cannot promise that two
322
+ * children are seen at once.
323
+ *
324
+ * `mode` travels with the slot rather than being read off the state, and it
325
+ * is what makes step 3's «men begge kan tas samtidig kl. 15:00» work: that
326
+ * offer appears while the machine still says `sequential`, and seating a
327
+ * parallel slot sequentially would submit the second child half an hour
328
+ * after a stylist who is free at the hour itself. Carrying it here also
329
+ * removes the ordering trap — `setPartyMode` clears the slot, so a caller
330
+ * that dispatched the two actions the other way round would throw away the
331
+ * time the parent had just accepted.
332
+ */
333
+ {
334
+ type: 'pickPartySlot';
335
+ startTs: number;
336
+ resourceIds: string[];
337
+ mode: WizardState['partyMode'];
338
+ } | {
339
+ type: 'slotTaken';
340
+ } |
341
+ /**
342
+ * A submission the create route refused, in the vocabulary `WizardState.error`
343
+ * already speaks.
344
+ *
345
+ * Without it the four codes below are a union nothing can produce: the route
346
+ * answers `conflict`, `invalidInput`, `unconfigured` and `upstreamError`, and
347
+ * step 4 has a Norwegian sentence for each, but there was no action that could
348
+ * put one into `state.error` — so the failure would have had to be held beside
349
+ * the machine, in a second error slot the machine's own clearing rules never
350
+ * touch.
351
+ *
352
+ * `slotTaken` and `maxParty` are excluded rather than admitted. Each already
353
+ * has an action that does more than raise a code — `slotTaken` drops the slot
354
+ * and sends the visitor back to step 3, `addService` refuses the child while
355
+ * leaving the ones already chosen alone — and a second spelling of either is
356
+ * how a stolen slot ends up raised without the slot being dropped.
357
+ */
358
+ {
359
+ type: 'submitFailed';
360
+ error: Exclude<NonNullable<WizardState['error']>, 'slotTaken' | 'maxParty'>;
361
+ } | {
362
+ type: 'goToStep';
363
+ step: WizardStep;
364
+ } | {
365
+ type: 'setContact';
366
+ field: keyof WizardState['contact'];
367
+ value: string;
368
+ } |
369
+ /**
370
+ * The two answers about the child, written onto the line item they belong to.
371
+ *
372
+ * Indexed rather than keyed by service, because two children in one party can
373
+ * genuinely share a service and the index is the only thing that tells those
374
+ * rows apart — the same reason step 4 keys its fields on it.
375
+ *
376
+ * Split over the two fields rather than taking a `Partial<WizardItem>`, so a
377
+ * caller cannot reach through this action and overwrite `service`, which is
378
+ * step 1's answer and not step 4's to change.
379
+ */
380
+ {
381
+ type: 'setItemField';
382
+ index: number;
383
+ field: 'bookedForName';
384
+ value: string;
385
+ } |
386
+ /** `null` is «cleared», which an emptied year input is and `undefined` would
387
+ * be ambiguous about — a patch that says nothing versus one that says nothing
388
+ * is there. */
389
+ {
390
+ type: 'setItemField';
391
+ index: number;
392
+ field: 'bookedForBirthYear';
393
+ value: number | null;
394
+ } |
395
+ /** A saved child picked for a guest line on «Bekreft» (a family chip); `null` detaches. */
396
+ {
397
+ type: 'setItemField';
398
+ index: number;
399
+ field: 'bookedForPersonId';
400
+ value: string | null;
401
+ } | {
402
+ type: 'setNotes';
403
+ value: string;
404
+ } |
405
+ /** One action for both boxes rather than two near-identical ones: they are the
406
+ * same gesture, and the pair that must never be confused — a marketing opt-in
407
+ * standing in for terms — is harder to write by accident when the difference
408
+ * is a required discriminant. */
409
+ {
410
+ type: 'setConsent';
411
+ which: 'terms' | 'marketing';
412
+ accepted: boolean;
413
+ } |
414
+ /**
415
+ * A «Bestill igjen» link, applied. The shell dispatches this rather than
416
+ * calling `applyPrefill` on the state it holds, because `useReducer` is the
417
+ * only writer of that state — see `applyPrefill` for the rules.
418
+ */
419
+ {
420
+ type: 'prefill';
421
+ prefill: WizardPrefill;
422
+ catalogue: WizardCatalogue;
423
+ };
424
+ /**
425
+ * What a `/min-side` «Bestill igjen» link can carry into the wizard:
426
+ * `?service=<id>&stylist=<id>` off the query string, and the child's name
427
+ * from the tab's session store (`rebook-store.ts`) — never from the URL.
428
+ *
429
+ * Every field is optional AND nullable, because `URLSearchParams.get` answers
430
+ * `null` for a missing key and the shell should not have to translate that.
431
+ * An empty string means the same as absent.
432
+ */
433
+ export interface WizardPrefill {
434
+ serviceId?: string | null;
435
+ /** An id, or — from a «Bestill hos Siv» link — a display name or its slug. */
436
+ resourceId?: string | null;
437
+ who?: string | null;
438
+ /**
439
+ * `?antall=`: how many children. Clamped to the service's `maxPerBooking`
440
+ * and only meaningful for a kids' service, where it pre-creates that many
441
+ * lines; ignored for anything else.
442
+ */
443
+ party?: number | null;
444
+ }
445
+ /**
446
+ * What a prefill is checked against — the services step 1 would let the visitor
447
+ * tap, and the stylists step 2 would show for them.
448
+ *
449
+ * The SHELL decides what goes in here, and that is the point: the machine has
450
+ * no `bookableOnline` and no fetched stylist list, so «this id is one the
451
+ * visitor could have chosen» is a fact only the caller can supply. An id absent
452
+ * from the catalogue is ignored, whatever the link said.
453
+ */
454
+ export interface WizardCatalogue {
455
+ services: readonly WizardService[];
456
+ resources: ReadonlyArray<{
457
+ id: string;
458
+ name?: string;
459
+ serviceIds: readonly string[];
460
+ }>;
461
+ }
462
+ /**
463
+ * The engine's own bound on `booked_for_name` — `maxLength: 200` in the SDK's
464
+ * OpenAPI document. Step 4's input has no `maxLength`, so this is the one limit
465
+ * that exists, and a prefilled name is cut to it rather than being carried into
466
+ * a body the engine answers 400 to.
467
+ */
468
+ export declare const BOOKED_FOR_NAME_MAX_LENGTH = 200;
469
+ export declare function initialState(): WizardState;
470
+ /** The guest child in chair `n` (1-based) — a chip on step 1, or a sibling a link added. */
471
+ export declare function guestChild(n: number): WizardPerson;
472
+ /**
473
+ * True when the visitor may leave the current step — and on `details`, when they
474
+ * may submit at all. Answers about `state.step` only, so a Neste button can be
475
+ * `disabled={!canAdvance(state)}` and nothing else.
476
+ */
477
+ export declare function canAdvance(state: WizardState): boolean;
478
+ /**
479
+ * True when a step may be jumped to directly — the predicate behind whether a
480
+ * progress dot is tappable, and the guard `goToStep` applies to itself.
481
+ *
482
+ * Bounded by what is *behind* the target, never the target itself: reaching
483
+ * `details` requires a service and a time, but obviously not the contact
484
+ * details you are going there to type. Going back is therefore always allowed,
485
+ * because nothing precedes `service` — which is the case that matters, since
486
+ * back is what the dots are mostly for.
487
+ */
488
+ export declare function canGoToStep(state: WizardState, step: WizardStep): boolean;
489
+ /**
490
+ * When each child in the party sits down.
491
+ *
492
+ * `sequential` is the report's default and the ordinary case — one stylist,
493
+ * «rett etter hverandre», so the second child starts when the first is
494
+ * finished. `parallel` is two stylists at once, and every line starts together.
495
+ *
496
+ * The engine takes a `start_ts` per line item rather than one for the visit, so
497
+ * somebody has to do this arithmetic, and it is a rule about the booking rather
498
+ * than about a screen. Step 4 submits with it and the confirmation card reads
499
+ * «15:00 Jonas / 15:30 Emma» off the same function, which is the only way those
500
+ * two can be made to agree.
501
+ *
502
+ * Task 9 owns the slot SEARCH for a party; this is only the seating of a slot
503
+ * already chosen.
504
+ */
505
+ export declare function itemStartTimes(items: WizardItem[], startTs: number, partyMode: WizardState['partyMode']): number[];
506
+ /**
507
+ * How long the family is in the salon — the number the summary bar quotes as
508
+ * «60 min totalt» and the length of the .ics entry.
509
+ *
510
+ * The two modes are two different visits: `sequential` is one chair used twice,
511
+ * so the minutes add up, while `parallel` is two chairs at once and the family
512
+ * leaves when the *longest* cut finishes. Quoting the sum for a parallel party
513
+ * would tell a parent to set aside an hour for a visit that takes half of one,
514
+ * which is the whole reason «To frisører samtidig» is worth offering.
515
+ *
516
+ * `Math.max` seeded with 0 rather than spread over the durations alone:
517
+ * `Math.max()` of nothing is `-Infinity`, and an empty basket is a visit of no
518
+ * length rather than a negative one.
519
+ */
520
+ export declare function visitMinutes(items: WizardItem[], partyMode: WizardState['partyMode']): number;
521
+ /**
522
+ * How much EARLIER a family has to start than a single first child would.
523
+ *
524
+ * The schedule endpoint answers for one service, so `lastStartTs` is «the last
525
+ * start a Gutteklipp fits» — the last minute at which that service's prep,
526
+ * duration and cleanup all land inside the day's window. A family needs its
527
+ * whole chain to fit, so its own last start is earlier by exactly the part of
528
+ * the chain that hangs off the end of the first child's busy span.
529
+ *
530
+ * NOT `visitMinutes - firstDuration`, which was the first attempt and is wrong
531
+ * in a way that only shows on a mixed basket: `visitMinutes` deliberately
532
+ * excludes the LAST child's cleanup — the family has left by then — so the
533
+ * subtraction silently keeps the FIRST child's cleanup in place of the last
534
+ * one's. Where the later service has the bigger `bufferAfterMinutes` the cutoff
535
+ * comes out too late, which is the error in the direction that matters: it says
536
+ * «Fullt» about an afternoon that is actually over.
537
+ *
538
+ * Sequential is the tail of the chain: every child after the first costs their
539
+ * prep, their duration and their cleanup, and the first child's own cleanup is
540
+ * already inside the number being adjusted. Parallel is one chair each, so what
541
+ * has to fit is the LONGEST busy span rather than the sum.
542
+ */
543
+ export declare function visitTailMinutes(items: WizardItem[], partyMode: WizardState['partyMode']): number;
544
+ /** When the family leaves. The same arithmetic as `visitMinutes`, which is why
545
+ * it is that function and not a second copy of it: an .ics that ended before
546
+ * `visitMinutes` said the visit did would tell a calendar the parent was free
547
+ * while a child was still in the chair. */
548
+ export declare function visitEndTs(items: WizardItem[], startTs: number, partyMode: WizardState['partyMode']): number;
549
+ /**
550
+ * Whether step 2 asks how the party is seen.
551
+ *
552
+ * One child is not a party, and «Samme frisør, rett etter hverandre» is not a
553
+ * choice when there is nobody to be after. The machine answers rather than the
554
+ * component so that the picker and the search cannot disagree about when a
555
+ * booking has become a family visit.
556
+ */
557
+ export declare function showsPartyMode(state: WizardState): boolean;
558
+ /**
559
+ * Who is cutting whose hair, one answer per line item.
560
+ *
561
+ * The single reader for both shapes the machine can be in: a party slot names a
562
+ * stylist per child, and everything else has one answer for the whole visit —
563
+ * including `null`, which is «Første ledige» still to be resolved by the engine.
564
+ *
565
+ * Step 4's submission and the confirmation card both read it, which is the only
566
+ * way «hos Marcus» on the card can be guaranteed to be the `resource_id` that
567
+ * was actually booked for that child.
568
+ */
569
+ export declare function itemResourceIds(state: WizardState): Array<string | null>;
570
+ export type WizardConfig = Pick<BookingConfig, 'party' | 'categories' | 'fallbackCategory' | 'timeZone' | 'locale' | 'currency' | 'dayparts' | 'labels'>;
571
+ export interface Wizard {
572
+ /**
573
+ * The most chairs one visit can take (`party.maxPeople`). The business's own
574
+ * ceiling is per service (`maxPerBooking`), and step 2 still honours it; this
575
+ * is the ceiling of the question on step 1, before any service is known.
576
+ */
577
+ readonly maxPeople: number;
578
+ initialState: typeof initialState;
579
+ reduce(state: WizardState, action: WizardAction): WizardState;
580
+ applyPrefill(state: WizardState, prefill: WizardPrefill, catalogue: WizardCatalogue): WizardState;
581
+ canAdvance: typeof canAdvance;
582
+ canGoToStep: typeof canGoToStep;
583
+ itemPriceOre(service: WizardService, startTs: number | null): number;
584
+ totalPriceOre(items: WizardItem[], startTs: number | null): number;
585
+ itemStartTimes: typeof itemStartTimes;
586
+ visitMinutes: typeof visitMinutes;
587
+ visitTailMinutes: typeof visitTailMinutes;
588
+ visitEndTs: typeof visitEndTs;
589
+ showsPartyMode: typeof showsPartyMode;
590
+ itemResourceIds: typeof itemResourceIds;
591
+ summaryLine(state: WizardState, resolveStylistName: (resourceId: string) => string | null, now?: number): string;
592
+ guestChild: typeof guestChild;
593
+ isGuestSeat: typeof isGuestSeat;
594
+ }
595
+ /** The machine for one site: its party ceiling, kids' categories, clock, currency and words. */
596
+ export declare function createWizard(config: WizardConfig): Wizard;
597
+ //#endregion
598
+ //# sourceMappingURL=machine.d.mts.map