arky-sdk 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 (79) hide show
  1. package/dist/api/cms.d.ts +19 -0
  2. package/dist/api/cms.d.ts.map +1 -0
  3. package/dist/api/cms.js +41 -0
  4. package/dist/api/eshop.d.ts +89 -0
  5. package/dist/api/eshop.d.ts.map +1 -0
  6. package/dist/api/eshop.js +183 -0
  7. package/dist/api/index.d.ts +6 -0
  8. package/dist/api/index.d.ts.map +1 -0
  9. package/dist/api/index.js +5 -0
  10. package/dist/api/newsletter.d.ts +32 -0
  11. package/dist/api/newsletter.d.ts.map +1 -0
  12. package/dist/api/newsletter.js +70 -0
  13. package/dist/api/reservation.d.ts +84 -0
  14. package/dist/api/reservation.d.ts.map +1 -0
  15. package/dist/api/reservation.js +239 -0
  16. package/dist/config.d.ts +15 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +20 -0
  19. package/dist/index.d.ts +26 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +57 -0
  22. package/dist/services/auth.d.ts +17 -0
  23. package/dist/services/auth.d.ts.map +1 -0
  24. package/dist/services/auth.js +62 -0
  25. package/dist/services/http.d.ts +20 -0
  26. package/dist/services/http.d.ts.map +1 -0
  27. package/dist/services/http.js +73 -0
  28. package/dist/stores/business.d.ts +28 -0
  29. package/dist/stores/business.d.ts.map +1 -0
  30. package/dist/stores/business.js +122 -0
  31. package/dist/stores/cart.d.ts +8 -0
  32. package/dist/stores/cart.d.ts.map +1 -0
  33. package/dist/stores/cart.js +20 -0
  34. package/dist/stores/eshop.d.ts +121 -0
  35. package/dist/stores/eshop.d.ts.map +1 -0
  36. package/dist/stores/eshop.js +377 -0
  37. package/dist/stores/index.d.ts +7 -0
  38. package/dist/stores/index.d.ts.map +1 -0
  39. package/dist/stores/index.js +19 -0
  40. package/dist/stores/reservation.d.ts +237 -0
  41. package/dist/stores/reservation.d.ts.map +1 -0
  42. package/dist/stores/reservation.js +853 -0
  43. package/dist/types/index.d.ts +244 -0
  44. package/dist/types/index.d.ts.map +1 -0
  45. package/dist/types/index.js +8 -0
  46. package/dist/utils/blocks.d.ts +30 -0
  47. package/dist/utils/blocks.d.ts.map +1 -0
  48. package/dist/utils/blocks.js +237 -0
  49. package/dist/utils/currency.d.ts +9 -0
  50. package/dist/utils/currency.d.ts.map +1 -0
  51. package/dist/utils/currency.js +99 -0
  52. package/dist/utils/errors.d.ts +121 -0
  53. package/dist/utils/errors.d.ts.map +1 -0
  54. package/dist/utils/errors.js +114 -0
  55. package/dist/utils/i18n.d.ts +5 -0
  56. package/dist/utils/i18n.d.ts.map +1 -0
  57. package/dist/utils/i18n.js +37 -0
  58. package/dist/utils/index.d.ts +9 -0
  59. package/dist/utils/index.d.ts.map +1 -0
  60. package/dist/utils/index.js +10 -0
  61. package/dist/utils/price.d.ts +33 -0
  62. package/dist/utils/price.d.ts.map +1 -0
  63. package/dist/utils/price.js +141 -0
  64. package/dist/utils/queryParams.d.ts +21 -0
  65. package/dist/utils/queryParams.d.ts.map +1 -0
  66. package/dist/utils/queryParams.js +47 -0
  67. package/dist/utils/svg.d.ts +17 -0
  68. package/dist/utils/svg.d.ts.map +1 -0
  69. package/dist/utils/svg.js +62 -0
  70. package/dist/utils/text.d.ts +26 -0
  71. package/dist/utils/text.d.ts.map +1 -0
  72. package/dist/utils/text.js +64 -0
  73. package/dist/utils/timezone.d.ts +9 -0
  74. package/dist/utils/timezone.d.ts.map +1 -0
  75. package/dist/utils/timezone.js +49 -0
  76. package/dist/utils/validation.d.ts +9 -0
  77. package/dist/utils/validation.d.ts.map +1 -0
  78. package/dist/utils/validation.js +44 -0
  79. package/package.json +58 -0
@@ -0,0 +1,853 @@
1
+ // Reservation store with TypeScript - Simplified with Business Store
2
+ import { computed, deepMap } from "nanostores";
3
+ import { persistentAtom } from "@nanostores/persistent";
4
+ import { getLocalizedString, getLocale } from "../utils/i18n";
5
+ import { API_URL, BUSINESS_ID, STORAGE_URL } from "../config";
6
+ import { reservationApi } from "../api/reservation";
7
+ import { getMarketPrice, getPriceAmount, createPaymentForCheckout } from "../utils/price";
8
+ import * as authService from "../services/auth";
9
+ import { validatePhoneNumber } from "../utils/validation";
10
+ import { tzGroups, findTimeZone } from "../utils/timezone";
11
+ import { selectedMarket, currency, businessActions } from "./business";
12
+ import { PaymentMethod } from "../types";
13
+ export const cartParts = persistentAtom("reservationCart", [], {
14
+ encode: JSON.stringify,
15
+ decode: JSON.parse,
16
+ });
17
+ export const store = deepMap({
18
+ currentStep: 1,
19
+ totalSteps: 4,
20
+ steps: {
21
+ 1: { name: "method", labelKey: "method" },
22
+ 2: { name: "provider", labelKey: "provider" },
23
+ 3: { name: "datetime", labelKey: "datetime" },
24
+ 4: { name: "review", labelKey: "review" },
25
+ },
26
+ // Calendar data
27
+ weekdays: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
28
+ monthYear: "",
29
+ days: [],
30
+ current: new Date(),
31
+ // Selection state
32
+ selectedDate: null,
33
+ slots: [],
34
+ selectedSlot: null,
35
+ selectedMethod: null,
36
+ selectedProvider: null,
37
+ providers: [],
38
+ // Status flags
39
+ loading: false,
40
+ startDate: null,
41
+ endDate: null,
42
+ isMultiDay: false,
43
+ // Phone verification
44
+ phoneNumber: "",
45
+ phoneError: null,
46
+ phoneSuccess: null,
47
+ verificationCode: "",
48
+ verifyError: null,
49
+ isPhoneVerified: false,
50
+ isSendingCode: false,
51
+ isVerifying: false,
52
+ codeSentAt: null,
53
+ canResendAt: null,
54
+ // Quote state
55
+ fetchingQuote: false,
56
+ quote: null,
57
+ quoteError: null,
58
+ // Service & config
59
+ guestToken: null,
60
+ service: null,
61
+ apiUrl: API_URL,
62
+ businessId: BUSINESS_ID,
63
+ storageUrl: STORAGE_URL,
64
+ timezone: findTimeZone(tzGroups),
65
+ tzGroups,
66
+ parts: [],
67
+ });
68
+ export const currentStepName = computed(store, (state) => {
69
+ return state?.steps?.[state?.currentStep]?.name || "";
70
+ });
71
+ export const canProceed = computed(store, (state) => {
72
+ const stepName = state?.steps?.[state?.currentStep]?.name;
73
+ switch (stepName) {
74
+ case "method":
75
+ return !!state.selectedMethod;
76
+ case "provider":
77
+ return !!state.selectedProvider;
78
+ case "datetime":
79
+ return state.isMultiDay
80
+ ? !!(state.startDate && state.endDate && state.selectedSlot)
81
+ : !!(state.selectedDate && state.selectedSlot);
82
+ case "review":
83
+ return true;
84
+ default:
85
+ return false;
86
+ }
87
+ });
88
+ const createCalendarGrid = (date) => {
89
+ const first = new Date(date.getFullYear(), date.getMonth(), 1);
90
+ const last = new Date(date.getFullYear(), date.getMonth() + 1, 0);
91
+ const cells = [];
92
+ // Leading blanks
93
+ const pad = (first.getDay() + 6) % 7;
94
+ for (let i = 0; i < pad; i++)
95
+ cells.push({ key: `b-${i}`, blank: true });
96
+ // Date cells
97
+ for (let d = 1; d <= last.getDate(); d++) {
98
+ cells.push({
99
+ key: `d-${d}`,
100
+ blank: false,
101
+ date: new Date(date.getFullYear(), date.getMonth(), d),
102
+ available: false,
103
+ });
104
+ }
105
+ // Trailing blanks
106
+ const suffix = (7 - (cells.length % 7)) % 7;
107
+ for (let i = 0; i < suffix; i++)
108
+ cells.push({ key: `b2-${i}`, blank: true });
109
+ return cells;
110
+ };
111
+ const formatTimeSlot = (from, to, timezone) => {
112
+ const opts = { hour: "2-digit", minute: "2-digit", timeZone: timezone };
113
+ return `${new Date(from * 1000).toLocaleTimeString([], opts)} – ${new Date(to * 1000).toLocaleTimeString([], opts)}`;
114
+ };
115
+ // Actions
116
+ export const actions = {
117
+ // Calendar management
118
+ updateCalendarGrid() {
119
+ const state = store.get();
120
+ const cur = state.current || new Date(new Date().getFullYear(), new Date().getMonth(), 1);
121
+ const days = createCalendarGrid(cur);
122
+ store.setKey("current", cur);
123
+ store.setKey("monthYear", cur.toLocaleString(undefined, { month: "long", year: "numeric" }));
124
+ store.setKey("days", days);
125
+ },
126
+ updateCalendar() {
127
+ this.updateCalendarGrid();
128
+ const state = store.get();
129
+ if (state.service)
130
+ this.fetchAvailability("month");
131
+ },
132
+ prevMonth() {
133
+ const { current } = store.get();
134
+ store.setKey("current", new Date(current.getFullYear(), current.getMonth() - 1, 1));
135
+ this.updateCalendar();
136
+ },
137
+ nextMonth() {
138
+ const { current } = store.get();
139
+ store.setKey("current", new Date(current.getFullYear(), current.getMonth() + 1, 1));
140
+ this.updateCalendar();
141
+ },
142
+ // Service initialization
143
+ setService(service) {
144
+ store.setKey("service", service);
145
+ store.setKey("selectedMethod", null);
146
+ store.setKey("selectedProvider", null);
147
+ store.setKey("providers", []);
148
+ store.setKey("selectedDate", null);
149
+ store.setKey("startDate", null);
150
+ store.setKey("endDate", null);
151
+ store.setKey("slots", []);
152
+ store.setKey("selectedSlot", null);
153
+ store.setKey("currentStep", 1);
154
+ store.setKey("isMultiDay", !!service?.reservationConfigs?.isMultiDay);
155
+ const now = new Date();
156
+ store.setKey("current", new Date(now.getFullYear(), now.getMonth(), 1));
157
+ this.updateCalendarGrid();
158
+ // Auto-select if only one method available
159
+ if (service.reservationMethods?.length === 1) {
160
+ const method = service.reservationMethods[0];
161
+ store.setKey("selectedMethod", method);
162
+ this.determineTotalSteps();
163
+ this.handleMethodSelection(method, false);
164
+ }
165
+ else {
166
+ this.determineTotalSteps();
167
+ }
168
+ this.fetchAvailability("month");
169
+ },
170
+ // Step management
171
+ determineTotalSteps() {
172
+ const state = store.get();
173
+ if (!state.service) {
174
+ store.setKey("totalSteps", 1);
175
+ return 1;
176
+ }
177
+ const active = [];
178
+ if (state.service.reservationMethods?.length > 1) {
179
+ active.push({ name: "method", label: "Choose Reservation Type" });
180
+ }
181
+ if (state.selectedMethod?.includes("SPECIFIC")) {
182
+ active.push({ name: "provider", label: "Choose Provider" });
183
+ }
184
+ if (state.selectedMethod && state.selectedMethod !== "ORDER") {
185
+ active.push({
186
+ name: "datetime",
187
+ label: state.isMultiDay ? "Choose Date Range" : "Choose Date & Time",
188
+ });
189
+ }
190
+ active.push({ name: "review", label: "Review & Confirm" });
191
+ const stepObj = {};
192
+ active.forEach((st, idx) => {
193
+ stepObj[idx + 1] = st;
194
+ });
195
+ store.setKey("steps", stepObj);
196
+ store.setKey("totalSteps", active.length);
197
+ if (state.currentStep > active.length) {
198
+ store.setKey("currentStep", active.length);
199
+ }
200
+ return active.length;
201
+ },
202
+ async getGuestToken() {
203
+ const state = store.get();
204
+ const token = await authService.getGuestToken(state.guestToken);
205
+ if (token !== state.guestToken) {
206
+ store.setKey("guestToken", token);
207
+ }
208
+ return token;
209
+ },
210
+ getStepNumberByName(name) {
211
+ const { steps } = store.get();
212
+ for (const [k, v] of Object.entries(steps)) {
213
+ if (v.name === name)
214
+ return Number(k);
215
+ }
216
+ return null;
217
+ },
218
+ nextStep() {
219
+ const state = store.get();
220
+ if (state.currentStep >= state.totalSteps || !canProceed.get())
221
+ return;
222
+ const next = state.currentStep + 1;
223
+ const name = state.steps[next]?.name;
224
+ store.setKey("currentStep", next);
225
+ if (name === "datetime") {
226
+ this.fetchAvailability("month");
227
+ if (!state.selectedDate && !state.startDate) {
228
+ this.findFirstAvailable();
229
+ }
230
+ }
231
+ },
232
+ prevStep() {
233
+ const state = store.get();
234
+ if (state.currentStep <= 1)
235
+ return;
236
+ this.clearCurrentStepState();
237
+ store.setKey("currentStep", state.currentStep - 1);
238
+ },
239
+ clearCurrentStepState() {
240
+ const name = currentStepName.get();
241
+ if (name === "method") {
242
+ store.setKey("selectedMethod", null);
243
+ }
244
+ else if (name === "provider") {
245
+ store.setKey("selectedProvider", null);
246
+ store.setKey("providers", []);
247
+ }
248
+ else if (name === "datetime") {
249
+ store.setKey("selectedDate", null);
250
+ store.setKey("startDate", null);
251
+ store.setKey("endDate", null);
252
+ store.setKey("slots", []);
253
+ store.setKey("selectedSlot", null);
254
+ }
255
+ },
256
+ goToStep(step) {
257
+ const state = store.get();
258
+ if (step < 1 || step > state.totalSteps)
259
+ return;
260
+ if (step < state.currentStep) {
261
+ for (let i = state.currentStep; i > step; i--) {
262
+ const n = state.steps[i]?.name;
263
+ if (n === "datetime") {
264
+ store.setKey("selectedDate", null);
265
+ store.setKey("startDate", null);
266
+ store.setKey("endDate", null);
267
+ store.setKey("slots", []);
268
+ store.setKey("selectedSlot", null);
269
+ }
270
+ else if (n === "provider") {
271
+ store.setKey("selectedProvider", null);
272
+ store.setKey("providers", []);
273
+ }
274
+ else if (n === "method") {
275
+ store.setKey("selectedMethod", null);
276
+ }
277
+ }
278
+ }
279
+ store.setKey("currentStep", step);
280
+ if (state.steps[step]?.name === "datetime") {
281
+ this.fetchAvailability("month");
282
+ if (!state.selectedDate && !state.startDate) {
283
+ this.findFirstAvailable();
284
+ }
285
+ }
286
+ },
287
+ // Method selection
288
+ async handleMethodSelection(method, advance = true) {
289
+ store.setKey("selectedDate", null);
290
+ store.setKey("startDate", null);
291
+ store.setKey("endDate", null);
292
+ store.setKey("slots", []);
293
+ store.setKey("selectedSlot", null);
294
+ store.setKey("selectedMethod", method);
295
+ this.determineTotalSteps();
296
+ if (method === "ORDER") {
297
+ this.handleOrderMethod();
298
+ if (advance) {
299
+ const reviewStep = this.getStepNumberByName("review");
300
+ if (reviewStep)
301
+ this.goToStep(reviewStep);
302
+ return;
303
+ }
304
+ }
305
+ else if (method.includes("SPECIFIC")) {
306
+ await this.loadProviders();
307
+ const state = store.get();
308
+ if (advance && state.providers.length === 1) {
309
+ this.selectProvider(state.providers[0]);
310
+ const datetimeStep = this.getStepNumberByName("datetime");
311
+ if (datetimeStep)
312
+ this.goToStep(datetimeStep);
313
+ return;
314
+ }
315
+ }
316
+ else if (method === "STANDARD" && advance) {
317
+ const datetimeStep = this.getStepNumberByName("datetime");
318
+ if (datetimeStep)
319
+ this.goToStep(datetimeStep);
320
+ return;
321
+ }
322
+ if (advance && store.get().currentStep < store.get().totalSteps) {
323
+ this.nextStep();
324
+ }
325
+ },
326
+ handleOrderMethod() {
327
+ const state = store.get();
328
+ const now = new Date();
329
+ const dur = state.service.durations?.reduce((a, c) => a + c.duration, 0) || 3600;
330
+ const from = Math.floor(now.getTime() / 1000);
331
+ const to = from + dur;
332
+ store.setKey("selectedSlot", {
333
+ from,
334
+ to,
335
+ timeText: formatTimeSlot(from, to, state.timezone),
336
+ });
337
+ },
338
+ // Provider management
339
+ async loadProviders() {
340
+ store.setKey("loading", true);
341
+ store.setKey("providers", []);
342
+ try {
343
+ const { businessId, service } = store.get();
344
+ const res = await reservationApi.getProviders({ businessId, serviceId: service.id });
345
+ store.setKey("providers", res.success ? res.data : []);
346
+ }
347
+ catch (e) {
348
+ console.error("Error loading providers:", e);
349
+ }
350
+ finally {
351
+ store.setKey("loading", false);
352
+ }
353
+ },
354
+ selectProvider(provider) {
355
+ store.setKey("selectedProvider", provider);
356
+ store.setKey("selectedDate", null);
357
+ store.setKey("startDate", null);
358
+ store.setKey("endDate", null);
359
+ store.setKey("slots", []);
360
+ store.setKey("selectedSlot", null);
361
+ if (currentStepName.get() === "datetime") {
362
+ this.fetchAvailability("month");
363
+ this.findFirstAvailable();
364
+ }
365
+ },
366
+ // Availability and date management
367
+ async fetchAvailability(type, date = null) {
368
+ const state = store.get();
369
+ if (!state.service || currentStepName.get() !== "datetime")
370
+ return;
371
+ store.setKey("loading", true);
372
+ try {
373
+ let from, to, limit;
374
+ if (type === "month") {
375
+ from = Math.floor(new Date(state.current.getFullYear(), state.current.getMonth(), 1).getTime() / 1000);
376
+ to = Math.floor(new Date(state.current.getFullYear(), state.current.getMonth() + 1, 0).getTime() / 1000);
377
+ limit = 100;
378
+ }
379
+ else if (type === "day" && date) {
380
+ const dObj = typeof date === "string" ? new Date(date) : date;
381
+ from = Math.floor(dObj.getTime() / 1000);
382
+ to = from + 24 * 3600;
383
+ limit = 100;
384
+ }
385
+ else if (type === "first") {
386
+ const now = new Date();
387
+ from = Math.floor(now.setHours(0, 0, 0, 0) / 1000);
388
+ to = Math.floor(new Date(now.getFullYear(), now.getMonth() + 3, 0).getTime() / 1000);
389
+ limit = 1;
390
+ }
391
+ else {
392
+ store.setKey("loading", false);
393
+ return;
394
+ }
395
+ const params = { businessId: state.businessId, serviceId: state.service.id, from, to, limit };
396
+ if (state.selectedProvider)
397
+ params.providerId = state.selectedProvider.id;
398
+ const result = await reservationApi.getAvailableSlots(params);
399
+ if (!result.success) {
400
+ console.error(`Error fetching availability (${type}):`, result.error);
401
+ return;
402
+ }
403
+ if (type === "month") {
404
+ const avail = new Set(result.data.map((i) => {
405
+ const date = new Date(i.from * 1000);
406
+ return date.toISOString().slice(0, 10);
407
+ }));
408
+ store.setKey("days", state.days.map((c) => {
409
+ if (!c.blank && c.date) {
410
+ const iso = c.date.toISOString().slice(0, 10);
411
+ return { ...c, available: avail.has(iso) };
412
+ }
413
+ return c;
414
+ }));
415
+ }
416
+ else if (type === "day") {
417
+ const slots = result.data.map((i, idx) => ({
418
+ ...i,
419
+ id: `slot-${i.from}-${idx}`,
420
+ day: new Date(i.from * 1000).toISOString().slice(0, 10),
421
+ timeText: formatTimeSlot(i.from, i.to, state.timezone),
422
+ }));
423
+ store.setKey("slots", slots);
424
+ if (slots.length && !state.selectedSlot) {
425
+ store.setKey("selectedSlot", slots[0]);
426
+ }
427
+ }
428
+ else if (type === "first" && result.data.length) {
429
+ const first = new Date(result.data[0].from * 1000);
430
+ const iso = first.toISOString().slice(0, 10);
431
+ store.setKey("current", new Date(first.getFullYear(), first.getMonth(), 1));
432
+ this.updateCalendarGrid();
433
+ await this.fetchAvailability("month");
434
+ if (state.isMultiDay) {
435
+ store.setKey("startDate", iso);
436
+ store.setKey("selectedDate", iso);
437
+ }
438
+ else {
439
+ store.setKey("selectedDate", iso);
440
+ await this.fetchAvailability("day", iso);
441
+ }
442
+ }
443
+ }
444
+ catch (err) {
445
+ console.error(`Error in fetchAvailability (${type}):`, err);
446
+ }
447
+ finally {
448
+ store.setKey("loading", false);
449
+ }
450
+ },
451
+ findFirstAvailable() {
452
+ if (currentStepName.get() === "datetime")
453
+ this.fetchAvailability("first");
454
+ },
455
+ // Date selection
456
+ selectDate(cell) {
457
+ if (!cell.date || !cell.available)
458
+ return;
459
+ // Store date components directly to avoid timezone issues
460
+ const dateInfo = {
461
+ year: cell.date.getFullYear(),
462
+ month: cell.date.getMonth() + 1,
463
+ day: cell.date.getDate(),
464
+ iso: `${cell.date.getFullYear()}-${String(cell.date.getMonth() + 1).padStart(2, '0')}-${String(cell.date.getDate()).padStart(2, '0')}`
465
+ };
466
+ const state = store.get();
467
+ if (state.isMultiDay) {
468
+ if (!state.startDate) {
469
+ store.setKey("startDate", dateInfo.iso);
470
+ store.setKey("selectedSlot", null);
471
+ store.setKey("selectedDate", dateInfo.iso);
472
+ store.setKey("endDate", null);
473
+ }
474
+ else if (!state.endDate) {
475
+ const start = new Date(state.startDate).getTime();
476
+ const cellT = cell.date.getTime();
477
+ if (cellT < start) {
478
+ store.setKey("endDate", state.startDate);
479
+ store.setKey("startDate", dateInfo.iso);
480
+ }
481
+ else {
482
+ store.setKey("endDate", dateInfo.iso);
483
+ }
484
+ }
485
+ else {
486
+ store.setKey("startDate", dateInfo.iso);
487
+ store.setKey("selectedDate", dateInfo.iso);
488
+ store.setKey("endDate", null);
489
+ store.setKey("selectedSlot", null);
490
+ }
491
+ }
492
+ else {
493
+ store.setKey("selectedSlot", null);
494
+ store.setKey("selectedDate", dateInfo.iso);
495
+ this.fetchAvailability("day", dateInfo.iso);
496
+ }
497
+ },
498
+ createMultiDaySlot() {
499
+ const state = store.get();
500
+ if (!state.startDate || !state.endDate)
501
+ return;
502
+ const startDT = new Date(state.startDate);
503
+ startDT.setHours(9, 0, 0, 0);
504
+ const endDT = new Date(state.endDate);
505
+ endDT.setHours(17, 0, 0, 0);
506
+ const from = Math.floor(startDT.getTime() / 1000);
507
+ const to = Math.floor(endDT.getTime() / 1000);
508
+ const rangeSlot = {
509
+ id: `multi-day-slot-${from}-${to}`,
510
+ from,
511
+ to,
512
+ isMultiDay: true,
513
+ timeText: `9:00 AM - 5:00 PM daily`,
514
+ dateRange: `${this.formatDateDisplay(state.startDate)} to ${this.formatDateDisplay(state.endDate)}`,
515
+ day: state.startDate,
516
+ };
517
+ store.setKey("slots", [rangeSlot]);
518
+ store.setKey("selectedSlot", rangeSlot);
519
+ },
520
+ resetDateSelection() {
521
+ store.setKey("startDate", null);
522
+ store.setKey("endDate", null);
523
+ store.setKey("selectedDate", null);
524
+ store.setKey("slots", []);
525
+ store.setKey("selectedSlot", null);
526
+ },
527
+ selectTimeSlot(slot) {
528
+ store.setKey("selectedSlot", slot);
529
+ },
530
+ setSelectedTimeZone(zone) {
531
+ const state = store.get();
532
+ if (zone === state.timezone)
533
+ return;
534
+ store.setKey("timezone", zone);
535
+ if (currentStepName.get() === "datetime") {
536
+ if (state.selectedDate) {
537
+ this.fetchAvailability("day", state.selectedDate);
538
+ }
539
+ else if (!state.selectedDate && !state.startDate) {
540
+ this.findFirstAvailable();
541
+ }
542
+ }
543
+ },
544
+ // Calendar helpers
545
+ isAvailable(cell) {
546
+ return cell.date && cell.available;
547
+ },
548
+ isSelectedDay(cell) {
549
+ if (cell.blank || !cell.date)
550
+ return false;
551
+ const iso = `${cell.date.getFullYear()}-${String(cell.date.getMonth() + 1).padStart(2, '0')}-${String(cell.date.getDate()).padStart(2, '0')}`;
552
+ const state = store.get();
553
+ return iso === state.startDate || iso === state.endDate || iso === state.selectedDate;
554
+ },
555
+ isInSelectedRange(cell) {
556
+ const state = store.get();
557
+ if (cell.blank || !cell.date || !state.startDate || !state.endDate)
558
+ return false;
559
+ const t = cell.date.getTime();
560
+ const a = new Date(state.startDate).getTime();
561
+ const b = new Date(state.endDate).getTime();
562
+ return t >= a && t <= b;
563
+ },
564
+ formatDateDisplay(ds) {
565
+ if (!ds)
566
+ return "";
567
+ const d = new Date(ds);
568
+ return d.toLocaleDateString(getLocale(), { month: "short", day: "numeric" });
569
+ },
570
+ // Cart operations
571
+ addToCart(slot) {
572
+ const state = store.get();
573
+ const id = crypto.randomUUID();
574
+ let dateDisplay, timeText;
575
+ if (state.isMultiDay && slot.isMultiDay) {
576
+ const a = new Date(slot.from * 1000), b = new Date(slot.to * 1000);
577
+ dateDisplay = `${a.toLocaleDateString(getLocale(), { month: "short", day: "numeric" })} - ${b.toLocaleDateString(getLocale(), { month: "short", day: "numeric", year: "numeric" })}`;
578
+ timeText = slot.timeText;
579
+ }
580
+ else {
581
+ const date = state.selectedDate ? new Date(state.selectedDate) : new Date(slot.from * 1000);
582
+ dateDisplay = date.toLocaleDateString(getLocale(), {
583
+ weekday: "short",
584
+ year: "numeric",
585
+ month: "short",
586
+ day: "numeric",
587
+ });
588
+ timeText = slot.timeText;
589
+ }
590
+ const blocks = (state.service?.reservationBlocks || []).map((f) => ({
591
+ ...f,
592
+ value: Array.isArray(f.value) ? f.value : [f.value],
593
+ }));
594
+ const newPart = {
595
+ id,
596
+ serviceId: state.service.id,
597
+ serviceName: getLocalizedString(state.service.name, getLocale()),
598
+ date: dateDisplay,
599
+ from: slot.from,
600
+ to: slot.to,
601
+ timeText,
602
+ isMultiDay: state.isMultiDay && (!!state.endDate || slot.isMultiDay),
603
+ reservationMethod: state.selectedMethod || '',
604
+ providerId: state.selectedProvider?.id,
605
+ blocks,
606
+ };
607
+ const newParts = [...state.parts, newPart];
608
+ store.setKey("parts", newParts);
609
+ cartParts.set(newParts);
610
+ this.resetDateSelection();
611
+ store.setKey("currentStep", 1);
612
+ if (state.service.reservationMethods?.length > 1) {
613
+ store.setKey("selectedMethod", null);
614
+ }
615
+ },
616
+ removePart(id) {
617
+ const filteredParts = store.get().parts.filter((p) => p.id !== id);
618
+ store.setKey("parts", filteredParts);
619
+ cartParts.set(filteredParts);
620
+ },
621
+ // Phone validation helper (using shared utility)
622
+ validatePhoneNumber(phone) {
623
+ const result = validatePhoneNumber(phone);
624
+ return result.isValid;
625
+ },
626
+ // Phone verification
627
+ async updateProfilePhone() {
628
+ store.setKey("phoneError", null);
629
+ store.setKey("phoneSuccess", null);
630
+ store.setKey("isSendingCode", true);
631
+ try {
632
+ const phoneNumber = store.get().phoneNumber;
633
+ // Validate phone number format
634
+ if (!this.validatePhoneNumber(phoneNumber)) {
635
+ store.setKey("phoneError", "Please enter a valid phone number");
636
+ return false;
637
+ }
638
+ const token = await this.getGuestToken();
639
+ await authService.updateProfilePhone(token, phoneNumber);
640
+ store.setKey("phoneSuccess", "Verification code sent successfully!");
641
+ store.setKey("codeSentAt", Date.now());
642
+ return true;
643
+ }
644
+ catch (e) {
645
+ store.setKey("phoneError", e.message);
646
+ return false;
647
+ }
648
+ finally {
649
+ store.setKey("isSendingCode", false);
650
+ }
651
+ },
652
+ async verifyPhoneCode() {
653
+ store.setKey("verifyError", null);
654
+ store.setKey("isVerifying", true);
655
+ try {
656
+ const { phoneNumber, verificationCode } = store.get();
657
+ // Validate code format
658
+ if (!verificationCode || verificationCode.length !== 4) {
659
+ store.setKey("verifyError", "Please enter a 4-digit verification code");
660
+ return false;
661
+ }
662
+ const token = await this.getGuestToken();
663
+ await authService.verifyPhoneCode(token, phoneNumber, verificationCode);
664
+ store.setKey("isPhoneVerified", true);
665
+ store.setKey("phoneSuccess", null);
666
+ store.setKey("verificationCode", "");
667
+ return true;
668
+ }
669
+ catch (e) {
670
+ // Provide user-friendly error messages
671
+ let errorMessage = "Invalid verification code";
672
+ if (e.message?.includes("expired")) {
673
+ errorMessage = "Verification code has expired. Please request a new one.";
674
+ }
675
+ else if (e.message?.includes("incorrect") || e.message?.includes("invalid")) {
676
+ errorMessage = "Incorrect verification code. Please try again.";
677
+ }
678
+ store.setKey("verifyError", errorMessage);
679
+ return false;
680
+ }
681
+ finally {
682
+ store.setKey("isVerifying", false);
683
+ }
684
+ },
685
+ async checkout(paymentMethod = PaymentMethod.Cash, reservationBlocks, promoCode) {
686
+ const state = store.get();
687
+ if (state.loading || !state.parts.length)
688
+ return { success: false, error: "No parts in cart" };
689
+ store.setKey("loading", true);
690
+ try {
691
+ const token = await this.getGuestToken();
692
+ const result = await reservationApi.checkout({
693
+ token,
694
+ businessId: state.businessId,
695
+ blocks: reservationBlocks || [],
696
+ parts: state.parts,
697
+ paymentMethod,
698
+ market: 'us',
699
+ promoCode,
700
+ });
701
+ if (result.success) {
702
+ return {
703
+ success: true,
704
+ data: {
705
+ reservationId: result.data?.reservationId,
706
+ clientSecret: result.data?.clientSecret,
707
+ },
708
+ };
709
+ }
710
+ else {
711
+ throw new Error(result.error);
712
+ }
713
+ }
714
+ catch (e) {
715
+ console.error("Reservation checkout error:", e);
716
+ return { success: false, error: e.message };
717
+ }
718
+ finally {
719
+ store.setKey("loading", false);
720
+ }
721
+ },
722
+ async fetchQuote(paymentMethod = PaymentMethod.Cash, promoCode) {
723
+ const state = store.get();
724
+ console.log('fetchQuote called with promoCode:', promoCode);
725
+ if (!state.parts.length) {
726
+ store.setKey("quote", null);
727
+ store.setKey("quoteError", null);
728
+ return;
729
+ }
730
+ store.setKey("fetchingQuote", true);
731
+ store.setKey("quoteError", null);
732
+ try {
733
+ const token = await this.getGuestToken();
734
+ const marketObj = selectedMarket.get();
735
+ const market = marketObj?.id || 'us';
736
+ const curr = currency.get() || 'USD';
737
+ console.log('Calling reservationApi.getQuote with:', { market, currency: curr, promoCode });
738
+ const result = await reservationApi.getQuote({
739
+ token,
740
+ businessId: state.businessId,
741
+ market,
742
+ currency: curr,
743
+ userId: token, // Use token as userId for guests
744
+ parts: state.parts,
745
+ paymentMethod,
746
+ promoCode,
747
+ });
748
+ if (result.success && result.data) {
749
+ console.log('Quote received:', result.data);
750
+ store.setKey("quote", result.data);
751
+ store.setKey("quoteError", null);
752
+ }
753
+ else {
754
+ console.error('Quote error:', result.error);
755
+ store.setKey("quote", null);
756
+ store.setKey("quoteError", mapQuoteError(result.code, result.error));
757
+ }
758
+ }
759
+ catch (e) {
760
+ console.error("Fetch quote error:", e);
761
+ store.setKey("quote", null);
762
+ store.setKey("quoteError", e.message || "Failed to get quote");
763
+ }
764
+ finally {
765
+ store.setKey("fetchingQuote", false);
766
+ }
767
+ },
768
+ // Helpers
769
+ getLabel(block, locale = getLocale()) {
770
+ if (!block)
771
+ return "";
772
+ if (block.properties?.label) {
773
+ if (typeof block.properties.label === "object") {
774
+ return (block.properties.label[locale] ||
775
+ block.properties.label.en ||
776
+ Object.values(block.properties.label)[0] ||
777
+ "");
778
+ }
779
+ if (typeof block.properties.label === "string") {
780
+ return block.properties.label;
781
+ }
782
+ }
783
+ return block.key || "";
784
+ },
785
+ getServicePrice() {
786
+ const state = store.get();
787
+ if (state.service?.prices && Array.isArray(state.service.prices)) {
788
+ // Market-based pricing (amounts are minor units)
789
+ // TODO: Get market from business config instead of hardcoded 'us'
790
+ return getMarketPrice(state.service.prices, 'us');
791
+ }
792
+ return '';
793
+ },
794
+ // NEW: Get reservation total as Payment structure
795
+ getReservationPayment() {
796
+ const state = store.get();
797
+ const subtotalMinor = state.parts.reduce((sum, part) => {
798
+ const servicePrices = state.service?.prices || [];
799
+ // amounts are in minor units
800
+ const amountMinor = servicePrices.length > 0 ? getPriceAmount(servicePrices, 'US') : 0;
801
+ return sum + amountMinor;
802
+ }, 0);
803
+ const currencyCode = currency.get();
804
+ return createPaymentForCheckout(subtotalMinor, 'US', currencyCode, PaymentMethod.Cash);
805
+ },
806
+ };
807
+ export function initReservationStore() {
808
+ actions.updateCalendarGrid();
809
+ businessActions.init(); // Use unified business store
810
+ const savedParts = cartParts.get();
811
+ if (savedParts && savedParts.length > 0) {
812
+ store.setKey("parts", savedParts);
813
+ }
814
+ store.listen((state) => {
815
+ if (state.isMultiDay &&
816
+ state.startDate &&
817
+ state.endDate &&
818
+ currentStepName.get() === "datetime" &&
819
+ (!state.slots.length || !state.slots[0].isMultiDay)) {
820
+ actions.createMultiDaySlot();
821
+ }
822
+ if (JSON.stringify(state.parts) !== JSON.stringify(cartParts.get())) {
823
+ cartParts.set(state.parts);
824
+ }
825
+ });
826
+ cartParts.listen((parts) => {
827
+ const currentParts = store.get().parts;
828
+ if (JSON.stringify(parts) !== JSON.stringify(currentParts)) {
829
+ store.setKey("parts", [...parts]);
830
+ }
831
+ });
832
+ }
833
+ function mapQuoteError(code, fallback) {
834
+ switch (code) {
835
+ case 'PROMO.MIN_ORDER':
836
+ return fallback || 'Promo requires a higher minimum order.';
837
+ case 'PROMO.NOT_ACTIVE':
838
+ return 'Promo code is not active.';
839
+ case 'PROMO.NOT_YET_VALID':
840
+ return 'Promo code is not yet valid.';
841
+ case 'PROMO.EXPIRED':
842
+ return 'Promo code has expired.';
843
+ case 'PROMO.MAX_USES':
844
+ return 'Promo code usage limit exceeded.';
845
+ case 'PROMO.MAX_USES_PER_USER':
846
+ return 'You have already used this promo code.';
847
+ case 'PROMO.NOT_FOUND':
848
+ return 'Promo code not found.';
849
+ default:
850
+ return fallback || 'Failed to get quote.';
851
+ }
852
+ }
853
+ export default { store, actions, initReservationStore };