@wix/headless-bookings 0.0.12 → 0.0.13

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 (57) hide show
  1. package/cjs/dist/api/index.d.ts +1 -0
  2. package/cjs/dist/api/index.js +1 -0
  3. package/cjs/dist/api/query-services/index.d.ts +6 -7
  4. package/cjs/dist/api/query-services/index.js +4 -4
  5. package/cjs/dist/api/time-slot-list/index.d.ts +53 -0
  6. package/cjs/dist/api/time-slot-list/index.js +133 -0
  7. package/cjs/dist/react/core/booking/Booking.d.ts +2 -2
  8. package/cjs/dist/react/core/location/Location.d.ts +3 -3
  9. package/cjs/dist/react/core/location/Location.js +2 -2
  10. package/cjs/dist/react/core/service/Service.d.ts +8 -8
  11. package/cjs/dist/react/core/service-list/ServiceList.d.ts +2 -2
  12. package/cjs/dist/react/location/Location.d.ts +3 -3
  13. package/cjs/dist/react/service/Service.d.ts +9 -9
  14. package/cjs/dist/react/service/Service.js +5 -5
  15. package/cjs/dist/react/service-list/ServiceList.d.ts +3 -3
  16. package/cjs/dist/services/booking/booking.d.ts +12 -11
  17. package/cjs/dist/services/booking/booking.js +5 -3
  18. package/cjs/dist/services/index.d.ts +1 -0
  19. package/cjs/dist/services/index.js +1 -0
  20. package/cjs/dist/services/service/service.d.ts +5 -5
  21. package/cjs/dist/services/service/service.js +8 -8
  22. package/cjs/dist/services/service-list/service-list.d.ts +3 -3
  23. package/cjs/dist/services/time-slot-list/index.d.ts +6 -0
  24. package/cjs/dist/services/time-slot-list/index.js +6 -0
  25. package/cjs/dist/services/time-slot-list/time-slot-list.d.ts +58 -0
  26. package/cjs/dist/services/time-slot-list/time-slot-list.def.d.ts +84 -0
  27. package/cjs/dist/services/time-slot-list/time-slot-list.def.js +17 -0
  28. package/cjs/dist/services/time-slot-list/time-slot-list.js +349 -0
  29. package/dist/api/index.d.ts +1 -0
  30. package/dist/api/index.js +1 -0
  31. package/dist/api/query-services/index.d.ts +6 -7
  32. package/dist/api/query-services/index.js +4 -4
  33. package/dist/api/time-slot-list/index.d.ts +53 -0
  34. package/dist/api/time-slot-list/index.js +133 -0
  35. package/dist/react/core/booking/Booking.d.ts +2 -2
  36. package/dist/react/core/location/Location.d.ts +3 -3
  37. package/dist/react/core/location/Location.js +2 -2
  38. package/dist/react/core/service/Service.d.ts +8 -8
  39. package/dist/react/core/service-list/ServiceList.d.ts +2 -2
  40. package/dist/react/location/Location.d.ts +3 -3
  41. package/dist/react/service/Service.d.ts +9 -9
  42. package/dist/react/service/Service.js +5 -5
  43. package/dist/react/service-list/ServiceList.d.ts +3 -3
  44. package/dist/services/booking/booking.d.ts +12 -11
  45. package/dist/services/booking/booking.js +5 -3
  46. package/dist/services/index.d.ts +1 -0
  47. package/dist/services/index.js +1 -0
  48. package/dist/services/service/service.d.ts +5 -5
  49. package/dist/services/service/service.js +8 -8
  50. package/dist/services/service-list/service-list.d.ts +3 -3
  51. package/dist/services/time-slot-list/index.d.ts +6 -0
  52. package/dist/services/time-slot-list/index.js +6 -0
  53. package/dist/services/time-slot-list/time-slot-list.d.ts +58 -0
  54. package/dist/services/time-slot-list/time-slot-list.def.d.ts +84 -0
  55. package/dist/services/time-slot-list/time-slot-list.def.js +17 -0
  56. package/dist/services/time-slot-list/time-slot-list.js +349 -0
  57. package/package.json +2 -2
@@ -82,12 +82,14 @@ export const BookingService = implementService.withConfig()(BookingServiceDefini
82
82
  bookingSelectionsSignal.set([...current, newSelection]);
83
83
  return instanceId;
84
84
  },
85
- updateServiceByInstanceId(instanceId, updates) {
85
+ updateBookingSelection(updates, instanceId) {
86
86
  const current = bookingSelectionsSignal.get();
87
- const updated = current.map((s) => s.instanceId === instanceId ? { ...s, ...updates } : s);
87
+ // If no instanceId provided, update the first selection (index 0)
88
+ const targetId = instanceId ?? current[0]?.instanceId;
89
+ const updated = current.map((s) => s.instanceId === targetId ? { ...s, ...updates } : s);
88
90
  bookingSelectionsSignal.set(updated);
89
91
  },
90
- removeServiceByInstanceId(instanceId) {
92
+ removeBookingSelection(instanceId) {
91
93
  const current = bookingSelectionsSignal.get();
92
94
  if (!instanceId) {
93
95
  // Remove first selection if no instanceId provided
@@ -4,4 +4,5 @@
4
4
  */
5
5
  export { ServiceService, ServiceServiceDefinition, type ServiceServiceAPI, type ServiceServiceConfig, type ServiceActions, type ServicePaymentData, loadServiceServiceInitialData, type SuccessServiceServiceConfigResult, type NotFoundServiceServiceConfigResult, } from './service/service.js';
6
6
  export { BookingService, BookingServiceDefinition, type BookingServiceConfig, type BookingSelection, type Location, type BookingServiceActions, } from './booking/booking.js';
7
+ export { TimeSlotListService, TimeSlotListServiceDefinition, ViewMode, loadTimeSlotListConfig, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type SlotSelection, type LoadTimeSlotListConfigOptions, type LoadTimeSlotListConfigResult, } from './time-slot-list/index.js';
7
8
  export { ServiceListService, ServiceListServiceDefinition, type ServiceListServiceAPI, type ServiceListServiceConfig, type ServiceListActions, type QueryOptions, loadServiceListServiceInitialData, type SuccessServiceListServiceConfigResult, type ErrorServiceListServiceConfigResult, } from './service-list/service-list.js';
@@ -4,4 +4,5 @@
4
4
  */
5
5
  export { ServiceService, ServiceServiceDefinition, loadServiceServiceInitialData, } from './service/service.js';
6
6
  export { BookingService, BookingServiceDefinition, } from './booking/booking.js';
7
+ export { TimeSlotListService, TimeSlotListServiceDefinition, ViewMode, loadTimeSlotListConfig, } from './time-slot-list/index.js';
7
8
  export { ServiceListService, ServiceListServiceDefinition, loadServiceListServiceInitialData, } from './service-list/service-list.js';
@@ -3,7 +3,7 @@
3
3
  * Handles loading and managing a single service's data, loading state, and errors.
4
4
  */
5
5
  import { type Signal, type ReadOnlySignal } from '@wix/services-definitions/core-services/signals';
6
- import { services } from '@wix/bookings';
6
+ import { type Service, type Payment } from '@wix/auto_sdk_bookings_services';
7
7
  import type { MoneyData } from '@wix/headless-components/react';
8
8
  /**
9
9
  * Actions object for service selection
@@ -23,9 +23,9 @@ export interface ServiceActions {
23
23
  */
24
24
  export interface ServicePaymentData {
25
25
  /** The service payment information from the service */
26
- servicePayment: services.Payment | undefined;
26
+ servicePayment: Payment | undefined;
27
27
  /** The rate type of the service (FIXED, VARIED, CUSTOM, NO_FEE, etc.) */
28
- rateType: services.Payment['rateType'] | undefined;
28
+ rateType: Payment['rateType'] | undefined;
29
29
  /** Primary price (used for FIXED or VARIED default) */
30
30
  money: MoneyData | null;
31
31
  /** Minimum price (used for VARIED) */
@@ -46,7 +46,7 @@ export interface ServicePaymentData {
46
46
  */
47
47
  export interface ServiceServiceAPI {
48
48
  /** Reactive signal containing the current service data */
49
- serviceSignal: Signal<services.Service>;
49
+ serviceSignal: Signal<Service>;
50
50
  /** Reactive signal indicating if a service is currently being loaded */
51
51
  isLoadingSignal: Signal<boolean>;
52
52
  /** Reactive signal containing any error message, or null if no error */
@@ -83,7 +83,7 @@ export declare const ServiceServiceDefinition: string & {
83
83
  */
84
84
  export interface ServiceServiceConfig {
85
85
  /** The initial service data to configure the service with */
86
- service?: services.Service;
86
+ service?: Service;
87
87
  /** The service ID to load if no initial service is provided */
88
88
  serviceId?: string;
89
89
  /** The service slug to load if no initial service is provided */
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { defineService, implementService } from '@wix/services-definitions';
6
6
  import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
7
- import { services } from '@wix/bookings';
7
+ import { ServiceType, RateType, } from '@wix/auto_sdk_bookings_services';
8
8
  import { getServiceById, getServiceBySlug, } from '../../api/query-services/index.js';
9
9
  import { BookingServiceDefinition } from '../booking/booking.js';
10
10
  import { convertToMoneyData } from '../../utils/money.js';
@@ -88,8 +88,8 @@ export const ServiceService = implementService.withConfig()(ServiceServiceDefini
88
88
  const requiresAvailabilitySignal = signalsService.computed(() => {
89
89
  const currentService = serviceSignal.get();
90
90
  const serviceType = currentService.type;
91
- return (serviceType === services.ServiceType.APPOINTMENT ||
92
- serviceType === services.ServiceType.CLASS);
91
+ return (serviceType === ServiceType.APPOINTMENT ||
92
+ serviceType === ServiceType.CLASS);
93
93
  });
94
94
  // paymentData signal - computed payment data from service
95
95
  const paymentDataSignal = signalsService.computed(() => {
@@ -104,21 +104,21 @@ export const ServiceService = implementService.withConfig()(ServiceServiceDefini
104
104
  // Extract pricing based on rate type
105
105
  if (payment) {
106
106
  switch (rateType) {
107
- case services.RateType.FIXED:
107
+ case RateType.FIXED:
108
108
  // FIXED: Use payment.fixed.price
109
109
  money = convertToMoneyData(payment.fixed?.price);
110
110
  break;
111
- case services.RateType.VARIED:
111
+ case RateType.VARIED:
112
112
  // VARIED: Use defaultPrice as money, plus min/max for range
113
113
  money = convertToMoneyData(payment.varied?.defaultPrice);
114
114
  minPrice = convertToMoneyData(payment.varied?.minPrice);
115
115
  maxPrice = convertToMoneyData(payment.varied?.maxPrice);
116
116
  break;
117
- case services.RateType.CUSTOM:
117
+ case RateType.CUSTOM:
118
118
  // CUSTOM: Use description text (no numeric price)
119
119
  priceDescription = payment.custom?.description || null;
120
120
  break;
121
- case services.RateType.NO_FEE:
121
+ case RateType.NO_FEE:
122
122
  // NO_FEE: Free service (amount = 0)
123
123
  money = { amount: 0, currency: 'USD' };
124
124
  break;
@@ -203,7 +203,7 @@ export const ServiceService = implementService.withConfig()(ServiceServiceDefini
203
203
  .filter((s) => s.service._id === currentService._id)
204
204
  .map((s) => s.instanceId);
205
205
  instancesToRemove.forEach((instanceId) => {
206
- bookingService.actions.removeServiceByInstanceId(instanceId);
206
+ bookingService.actions.removeBookingSelection(instanceId);
207
207
  });
208
208
  },
209
209
  };
@@ -3,7 +3,7 @@
3
3
  * Handles querying and pagination of services list (NO selection management)
4
4
  */
5
5
  import { type Signal, type ReadOnlySignal } from '@wix/services-definitions/core-services/signals';
6
- import { services } from '@wix/bookings';
6
+ import type { Service } from '@wix/auto_sdk_bookings_services';
7
7
  import type { QueryServicesRequest, PagingMetadata } from '../../api/query-services/index.js';
8
8
  export type { ServiceListFilters } from '../../api/query-services/index.js';
9
9
  /**
@@ -15,7 +15,7 @@ export type QueryOptions = Omit<QueryServicesRequest, 'pagingMetadata'>;
15
15
  * Configuration for the ServiceList service
16
16
  */
17
17
  export interface ServiceListServiceConfig {
18
- services?: services.Service[];
18
+ services?: Service[];
19
19
  options?: QueryServicesRequest;
20
20
  }
21
21
  /**
@@ -30,7 +30,7 @@ export interface ServiceListActions {
30
30
  */
31
31
  export interface ServiceListServiceAPI {
32
32
  /** Reactive signal containing the list of services */
33
- servicesSignal: Signal<services.Service[]>;
33
+ servicesSignal: Signal<Service[]>;
34
34
  /** Reactive signal containing current query options (appId, filters, sort) - changes trigger re-fetch */
35
35
  queryOptionsSignal: Signal<QueryOptions>;
36
36
  /** Reactive signal containing pagination metadata (limit, offset, count, hasNext) - updated after fetches */
@@ -0,0 +1,6 @@
1
+ /**
2
+ * TimeSlotList Service
3
+ * Main export file for time slot list service
4
+ */
5
+ export { TimeSlotListServiceDefinition, ViewMode, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type SlotSelection, } from './time-slot-list.def.js';
6
+ export { TimeSlotListService, loadTimeSlotListConfig, type LoadTimeSlotListConfigOptions, type LoadTimeSlotListConfigResult, } from './time-slot-list.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * TimeSlotList Service
3
+ * Main export file for time slot list service
4
+ */
5
+ export { TimeSlotListServiceDefinition, ViewMode, } from './time-slot-list.def.js';
6
+ export { TimeSlotListService, loadTimeSlotListConfig, } from './time-slot-list.js';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * TimeSlotList Service Implementation
3
+ * Manages time slot fetching, selection, and state
4
+ */
5
+ import type { Service } from '@wix/auto_sdk_bookings_services';
6
+ import { type TimeSlotListServiceConfig, ViewMode } from './time-slot-list.def.js';
7
+ export declare const TimeSlotListService: import("@wix/services-definitions").ServiceFactory<string & {
8
+ __api: import("./time-slot-list.def.js").TimeSlotListServiceAPI;
9
+ __config: {};
10
+ isServiceDefinition?: boolean;
11
+ } & import("./time-slot-list.def.js").TimeSlotListServiceAPI, TimeSlotListServiceConfig>;
12
+ /**
13
+ * Options for loading time slots configuration on the server
14
+ */
15
+ export interface LoadTimeSlotListConfigOptions {
16
+ /** Services to fetch time slots for */
17
+ services?: Service[];
18
+ /** Start date for the time range (defaults to current date) */
19
+ startDate?: Date;
20
+ /** End date for the time range (defaults to 7 days from start) */
21
+ endDate?: Date;
22
+ /** Timezone for formatting (defaults to UTC) */
23
+ timezone?: string;
24
+ /** View mode (defaults to CLASSIC) */
25
+ viewMode?: ViewMode;
26
+ }
27
+ /**
28
+ * Result type for SSR loader - either success with config or error
29
+ */
30
+ export type LoadTimeSlotListConfigResult = {
31
+ type: 'success';
32
+ config: TimeSlotListServiceConfig;
33
+ } | {
34
+ type: 'error';
35
+ error: string;
36
+ };
37
+ /**
38
+ * SSR Loader: Pre-fetches time slots for server-side rendering
39
+ * Use this in Astro/Next.js getServerSideProps or page loaders
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // In Astro page
44
+ * const loadResult = await loadTimeSlotListConfig({
45
+ * services: bookingServices,
46
+ * startDate: new Date(),
47
+ * endDate: addDays(new Date(), 7),
48
+ * timezone: 'America/New_York',
49
+ * });
50
+ *
51
+ * if (loadResult.type === 'error') {
52
+ * // Handle error
53
+ * }
54
+ *
55
+ * const timeSlotListConfig = loadResult.config;
56
+ * ```
57
+ */
58
+ export declare function loadTimeSlotListConfig(options: LoadTimeSlotListConfigOptions): Promise<LoadTimeSlotListConfigResult>;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * TimeSlotList Service Definition
3
+ * Defines interfaces and service contract for time slot management
4
+ */
5
+ import type { Signal } from '@wix/services-definitions/core-services/signals';
6
+ import type { TimeSlot as ApiTimeSlot } from '@wix/auto_sdk_bookings_availability-time-slots';
7
+ import type { Service } from '@wix/auto_sdk_bookings_services';
8
+ export declare enum ViewMode {
9
+ CLASSIC = "classic",
10
+ AGENDA = "agenda"
11
+ }
12
+ /**
13
+ * Maps a location to its available staff members
14
+ */
15
+ export interface StaffByLocation {
16
+ location: {
17
+ id: string;
18
+ name: string;
19
+ };
20
+ staffMembers: {
21
+ id: string;
22
+ name: string;
23
+ }[];
24
+ }
25
+ /**
26
+ * Grouped time slot that represents a unique time with all available locations/staff
27
+ * Extends the API TimeSlot type but replaces location-specific fields with aggregated data
28
+ */
29
+ export interface TimeSlot extends Omit<ApiTimeSlot, 'location' | 'availableResources'> {
30
+ /** Maps each location to its available staff members */
31
+ staffByLocation: StaffByLocation[];
32
+ /** Original API slots for this time (needed for booking service) */
33
+ originalSlots: ApiTimeSlot[];
34
+ /** Unique identifier for the time slot */
35
+ id: string;
36
+ }
37
+ /**
38
+ * User's selection state
39
+ */
40
+ export interface SlotSelection {
41
+ slot: TimeSlot;
42
+ locationId: string | null;
43
+ staffMemberId: string | null;
44
+ }
45
+ export interface TimeSlotListServiceConfig {
46
+ /** Services to fetch time slots for */
47
+ services?: Service[];
48
+ /** Pre-loaded time slots (from SSR) - already grouped by time */
49
+ timeSlots?: TimeSlot[];
50
+ /** Start date for the time range */
51
+ startDate?: Date;
52
+ /** End date for the time range */
53
+ endDate?: Date;
54
+ /** Timezone for formatting */
55
+ timezone?: string;
56
+ /** View mode */
57
+ viewMode?: ViewMode;
58
+ }
59
+ export interface TimeSlotListServiceActions {
60
+ selectSlot: (slot: TimeSlot) => void;
61
+ selectLocation: (locationId: string) => void;
62
+ selectStaffMember: (staffMemberId: string) => void;
63
+ clearPreferences: () => void;
64
+ loadMore: () => Promise<void>;
65
+ setDateRange: (start: Date, end: Date) => void;
66
+ setTimezone: (tz: string) => void;
67
+ }
68
+ export interface TimeSlotListServiceAPI {
69
+ timeSlots: Signal<TimeSlot[]>;
70
+ selection: Signal<SlotSelection | null>;
71
+ isLoading: Signal<boolean>;
72
+ error: Signal<string | null>;
73
+ hasMore: Signal<boolean>;
74
+ viewMode: Signal<ViewMode>;
75
+ startDate: Signal<Date>;
76
+ endDate: Signal<Date>;
77
+ timezone: Signal<string | undefined>;
78
+ actions: TimeSlotListServiceActions;
79
+ }
80
+ export declare const TimeSlotListServiceDefinition: string & {
81
+ __api: TimeSlotListServiceAPI;
82
+ __config: {};
83
+ isServiceDefinition?: boolean;
84
+ } & TimeSlotListServiceAPI;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * TimeSlotList Service Definition
3
+ * Defines interfaces and service contract for time slot management
4
+ */
5
+ import { defineService } from '@wix/services-definitions';
6
+ // ============================================================================
7
+ // Enums
8
+ // ============================================================================
9
+ export var ViewMode;
10
+ (function (ViewMode) {
11
+ ViewMode["CLASSIC"] = "classic";
12
+ ViewMode["AGENDA"] = "agenda";
13
+ })(ViewMode || (ViewMode = {}));
14
+ // ============================================================================
15
+ // Service Definition
16
+ // ============================================================================
17
+ export const TimeSlotListServiceDefinition = defineService('time-slot-list');
@@ -0,0 +1,349 @@
1
+ /**
2
+ * TimeSlotList Service Implementation
3
+ * Manages time slot fetching, selection, and state
4
+ */
5
+ import { implementService } from '@wix/services-definitions';
6
+ import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
7
+ import { TimeSlotListServiceDefinition, ViewMode, } from './time-slot-list.def.js';
8
+ import { fetchAvailability } from '../../api/time-slot-list/index.js';
9
+ import { BookingServiceDefinition } from '../booking/booking.js';
10
+ // ============================================================================
11
+ // Helpers
12
+ // ============================================================================
13
+ function getDefaultEndDate() {
14
+ return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
15
+ }
16
+ function getClientTimezone() {
17
+ return typeof Intl !== 'undefined'
18
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone
19
+ : undefined;
20
+ }
21
+ /**
22
+ * Formats a Date to local ISO string (YYYY-MM-DDThh:mm:ss)
23
+ */
24
+ function toLocalDateString(date) {
25
+ const year = date.getFullYear();
26
+ const month = String(date.getMonth() + 1).padStart(2, '0');
27
+ const day = String(date.getDate()).padStart(2, '0');
28
+ const hours = String(date.getHours()).padStart(2, '0');
29
+ const minutes = String(date.getMinutes()).padStart(2, '0');
30
+ const seconds = String(date.getSeconds()).padStart(2, '0');
31
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
32
+ }
33
+ /**
34
+ * Merges new grouped slots with existing ones
35
+ * Slots with the same start time are merged
36
+ */
37
+ function mergeGroupedSlots(existing, newSlots) {
38
+ const mergedMap = new Map();
39
+ for (const slot of existing) {
40
+ mergedMap.set(slot.localStartDate, slot);
41
+ }
42
+ for (const newSlot of newSlots) {
43
+ const key = newSlot.localStartDate;
44
+ const existingSlot = mergedMap.get(key);
45
+ if (existingSlot) {
46
+ // Merge staffByLocation and originalSlots
47
+ existingSlot.staffByLocation.push(...newSlot.staffByLocation);
48
+ existingSlot.originalSlots.push(...newSlot.originalSlots);
49
+ }
50
+ else {
51
+ mergedMap.set(key, newSlot);
52
+ }
53
+ }
54
+ return Array.from(mergedMap.values());
55
+ }
56
+ // ============================================================================
57
+ // Service Implementation
58
+ // ============================================================================
59
+ export const TimeSlotListService = implementService.withConfig()(TimeSlotListServiceDefinition, ({ getService, config }) => {
60
+ // Get dependent services
61
+ const signalsService = getService(SignalsServiceDefinition);
62
+ const bookingService = getService(BookingServiceDefinition);
63
+ // Initialize default values
64
+ const currentDate = new Date();
65
+ // Initialize Signals
66
+ const timeSlotsSignal = signalsService.signal(config.timeSlots || []);
67
+ const selectionSignal = signalsService.signal(null);
68
+ const isLoadingSignal = signalsService.signal(false);
69
+ const errorSignal = signalsService.signal(null);
70
+ const hasMoreSignal = signalsService.signal(false);
71
+ const viewModeSignal = signalsService.signal(config.viewMode || ViewMode.CLASSIC);
72
+ const startDateSignal = signalsService.signal(config.startDate || currentDate);
73
+ const endDateSignal = signalsService.signal(config.endDate || getDefaultEndDate());
74
+ const timezoneSignal = signalsService.signal(config.timezone ?? getClientTimezone());
75
+ const cursorSignal = signalsService.signal(undefined);
76
+ /**
77
+ * Helper: Get services from config or BookingService
78
+ */
79
+ const getServices = () => {
80
+ if (config.services?.length)
81
+ return config.services;
82
+ return bookingService.bookingSelections.get().map((s) => s.service);
83
+ };
84
+ /**
85
+ * Fetch time slots from API
86
+ * Called on initialization or when filters change
87
+ */
88
+ const fetchTimeSlots = async () => {
89
+ const selectedServices = getServices();
90
+ isLoadingSignal.set(true);
91
+ errorSignal.set(null);
92
+ cursorSignal.set(undefined);
93
+ try {
94
+ const response = await fetchAvailability({
95
+ services: selectedServices,
96
+ fromLocalDate: toLocalDateString(startDateSignal.get()),
97
+ toLocalDate: toLocalDateString(endDateSignal.get()),
98
+ timeZone: timezoneSignal.get(),
99
+ viewMode: viewModeSignal.get(),
100
+ });
101
+ timeSlotsSignal.set(response.timeSlots);
102
+ const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
103
+ cursorSignal.set(nextCursor);
104
+ hasMoreSignal.set(!!nextCursor);
105
+ }
106
+ catch (error) {
107
+ const message = error instanceof Error
108
+ ? error.message
109
+ : 'Failed to load time slots';
110
+ errorSignal.set(message);
111
+ }
112
+ finally {
113
+ isLoadingSignal.set(false);
114
+ }
115
+ };
116
+ /**
117
+ * Action: Select a time slot
118
+ * Only updates booking service if there's exactly one location and one resource
119
+ * Otherwise, user must select location/resource before booking
120
+ */
121
+ const selectSlot = (slot) => {
122
+ const currentSelection = selectionSignal.get();
123
+ if (currentSelection?.slot?.id === slot.id)
124
+ return;
125
+ const hasOneLocation = slot.staffByLocation.length === 1;
126
+ const hasOneResource = hasOneLocation && slot.staffByLocation[0].staffMembers.length === 1;
127
+ if (hasOneLocation && hasOneResource) {
128
+ const originalSlot = slot.originalSlots[0];
129
+ const { location, staffMembers } = slot.staffByLocation[0];
130
+ const staffMemberId = staffMembers[0].id;
131
+ selectionSignal.set({
132
+ slot,
133
+ locationId: location.id,
134
+ staffMemberId,
135
+ });
136
+ bookingService.actions.setTimeSlot(originalSlot);
137
+ bookingService.actions.setLocation({
138
+ id: location.id,
139
+ name: location.name,
140
+ });
141
+ bookingService.actions.updateBookingSelection({
142
+ staff: {
143
+ _id: staffMemberId,
144
+ name: staffMembers[0].name,
145
+ },
146
+ });
147
+ }
148
+ else {
149
+ selectionSignal.set({
150
+ slot,
151
+ locationId: null,
152
+ staffMemberId: null,
153
+ });
154
+ }
155
+ };
156
+ /**
157
+ * Action: Select a location for the current slot
158
+ * Updates the booking service with the correct original slot for this location
159
+ */
160
+ const selectLocation = (locationId) => {
161
+ const currentSelections = selectionSignal.get();
162
+ if (!currentSelections?.slot)
163
+ return;
164
+ selectionSignal.set({ ...currentSelections, locationId });
165
+ const location = currentSelections.slot.originalSlots.find((s) => s.location?._id === locationId)?.location;
166
+ if (location) {
167
+ bookingService.actions.setLocation({
168
+ id: location._id,
169
+ name: location.name,
170
+ });
171
+ }
172
+ };
173
+ /**
174
+ * Action: Select a staff member for the current slot
175
+ * Updates the booking service with the selected resource for the slot's service
176
+ */
177
+ const selectStaffMember = (staffMemberId) => {
178
+ const currentSelections = selectionSignal.get();
179
+ if (!currentSelections?.slot)
180
+ return;
181
+ selectionSignal.set({ ...currentSelections, staffMemberId });
182
+ const slot = currentSelections.slot;
183
+ // Find the resource info from original slots
184
+ for (const originalSlot of slot.originalSlots) {
185
+ for (const resourceGroup of originalSlot.availableResources || []) {
186
+ const staff = resourceGroup.resources?.find((r) => r._id === staffMemberId);
187
+ if (staff) {
188
+ bookingService.actions.updateBookingSelection({
189
+ staff,
190
+ });
191
+ return;
192
+ }
193
+ }
194
+ }
195
+ };
196
+ /**
197
+ * Action: Clear preferences (location and staff) while keeping the slot selected
198
+ */
199
+ const clearPreferences = () => {
200
+ const currentSelection = selectionSignal.get();
201
+ if (!currentSelection?.slot)
202
+ return;
203
+ // Keep the slot, but clear location and staff selections
204
+ selectionSignal.set({
205
+ slot: currentSelection.slot,
206
+ locationId: null,
207
+ staffMemberId: null,
208
+ });
209
+ // Clear location in booking service
210
+ bookingService.actions.clearLocation();
211
+ // Clear resources (staff) in booking service
212
+ const serviceId = currentSelection.slot.serviceId;
213
+ if (serviceId) {
214
+ bookingService.actions.updateBookingSelection({
215
+ staff: undefined,
216
+ });
217
+ }
218
+ };
219
+ /**
220
+ * Action: Load more slots (pagination)
221
+ */
222
+ const loadMore = async () => {
223
+ const cursor = cursorSignal.get();
224
+ if (!cursor || isLoadingSignal.get())
225
+ return;
226
+ const selectedServices = getServices();
227
+ if (selectedServices.length === 0)
228
+ return;
229
+ isLoadingSignal.set(true);
230
+ try {
231
+ const response = await fetchAvailability({
232
+ services: selectedServices,
233
+ fromLocalDate: toLocalDateString(startDateSignal.get()),
234
+ toLocalDate: toLocalDateString(endDateSignal.get()),
235
+ timeZone: timezoneSignal.get(),
236
+ cursorPaging: { cursor },
237
+ viewMode: viewModeSignal.get(),
238
+ });
239
+ // API returns grouped slots, merge with existing
240
+ const existing = timeSlotsSignal.get();
241
+ timeSlotsSignal.set(mergeGroupedSlots(existing, response.timeSlots));
242
+ const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
243
+ cursorSignal.set(nextCursor);
244
+ hasMoreSignal.set(!!nextCursor);
245
+ }
246
+ catch (error) {
247
+ const message = error instanceof Error
248
+ ? error.message
249
+ : 'Failed to load more time slots';
250
+ errorSignal.set(message);
251
+ }
252
+ finally {
253
+ isLoadingSignal.set(false);
254
+ }
255
+ };
256
+ /**
257
+ * Action: Update date range and refetch slots
258
+ */
259
+ const setDateRange = (newStartDate, newEndDate) => {
260
+ startDateSignal.set(newStartDate);
261
+ endDateSignal.set(newEndDate);
262
+ fetchTimeSlots();
263
+ };
264
+ /**
265
+ * Action: Update timezone and refetch slots
266
+ */
267
+ const setTimezone = (newTimezone) => {
268
+ timezoneSignal.set(newTimezone);
269
+ fetchTimeSlots();
270
+ };
271
+ // Auto-fetch on initialization if no pre-loaded data provided
272
+ if (!config.timeSlots && config.services && config.services.length > 0) {
273
+ fetchTimeSlots();
274
+ }
275
+ // Return service API
276
+ return {
277
+ // State Signals
278
+ timeSlots: timeSlotsSignal,
279
+ selection: selectionSignal,
280
+ isLoading: isLoadingSignal,
281
+ error: errorSignal,
282
+ hasMore: hasMoreSignal,
283
+ // Configuration Signals
284
+ viewMode: viewModeSignal,
285
+ startDate: startDateSignal,
286
+ endDate: endDateSignal,
287
+ timezone: timezoneSignal,
288
+ // Actions
289
+ actions: {
290
+ selectSlot,
291
+ selectLocation,
292
+ selectStaffMember,
293
+ clearPreferences,
294
+ loadMore,
295
+ setDateRange,
296
+ setTimezone,
297
+ },
298
+ };
299
+ });
300
+ /**
301
+ * SSR Loader: Pre-fetches time slots for server-side rendering
302
+ * Use this in Astro/Next.js getServerSideProps or page loaders
303
+ *
304
+ * @example
305
+ * ```typescript
306
+ * // In Astro page
307
+ * const loadResult = await loadTimeSlotListConfig({
308
+ * services: bookingServices,
309
+ * startDate: new Date(),
310
+ * endDate: addDays(new Date(), 7),
311
+ * timezone: 'America/New_York',
312
+ * });
313
+ *
314
+ * if (loadResult.type === 'error') {
315
+ * // Handle error
316
+ * }
317
+ *
318
+ * const timeSlotListConfig = loadResult.config;
319
+ * ```
320
+ */
321
+ export async function loadTimeSlotListConfig(options) {
322
+ const now = new Date();
323
+ const startDate = options.startDate || now;
324
+ const endDate = options.endDate || getDefaultEndDate();
325
+ const viewMode = options.viewMode || ViewMode.CLASSIC;
326
+ // check for location and staff in config
327
+ try {
328
+ const response = await fetchAvailability({
329
+ services: options.services || [],
330
+ fromLocalDate: toLocalDateString(startDate),
331
+ toLocalDate: toLocalDateString(endDate),
332
+ timeZone: options.timezone ?? getClientTimezone(),
333
+ viewMode,
334
+ });
335
+ const config = {
336
+ services: options.services,
337
+ timeSlots: response.timeSlots || [],
338
+ startDate,
339
+ endDate,
340
+ timezone: options.timezone,
341
+ viewMode,
342
+ };
343
+ return { type: 'success', config };
344
+ }
345
+ catch (error) {
346
+ const message = error instanceof Error ? error.message : 'Failed to load time slots';
347
+ return { type: 'error', error: message };
348
+ }
349
+ }