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