@devpablocristo/modules-scheduling 0.4.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.
- package/package.json +49 -0
- package/src/BlockedRangeModal.tsx +256 -0
- package/src/PublicSchedulingFlow.test.tsx +192 -0
- package/src/PublicSchedulingFlow.tsx +570 -0
- package/src/QueueOperatorBoard.test.tsx +169 -0
- package/src/QueueOperatorBoard.tsx +650 -0
- package/src/SchedulingBookingModal.test.tsx +241 -0
- package/src/SchedulingBookingModal.tsx +551 -0
- package/src/SchedulingCalendar.test.tsx +830 -0
- package/src/SchedulingCalendarBoard.tsx +1596 -0
- package/src/SchedulingDaySummary.test.tsx +136 -0
- package/src/SchedulingDaySummary.tsx +190 -0
- package/src/client.test.ts +105 -0
- package/src/client.ts +300 -0
- package/src/index.ts +66 -0
- package/src/locale.test.ts +36 -0
- package/src/locale.ts +91 -0
- package/src/next.ts +66 -0
- package/src/schedulingCalendarLogic.ts +373 -0
- package/src/styles.css +649 -0
- package/src/styles.next.css +1 -0
- package/src/types.ts +653 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AvailabilityRule,
|
|
3
|
+
BlockedRange,
|
|
4
|
+
BlockedRangePayload,
|
|
5
|
+
Booking,
|
|
6
|
+
Branch,
|
|
7
|
+
CreateBookingPayload,
|
|
8
|
+
DashboardStats,
|
|
9
|
+
DayAgendaItem,
|
|
10
|
+
ListBlockedRangesQuery,
|
|
11
|
+
ListBookingsFilter,
|
|
12
|
+
PublicAvailabilityQuery,
|
|
13
|
+
PublicAvailabilitySlot,
|
|
14
|
+
PublicBusinessInfo,
|
|
15
|
+
PublicBookPayload,
|
|
16
|
+
PublicBooking,
|
|
17
|
+
PublicMyBookingsQuery,
|
|
18
|
+
PublicQueuePosition,
|
|
19
|
+
PublicQueueSummary,
|
|
20
|
+
PublicQueueTicket,
|
|
21
|
+
PublicQueueTicketPayload,
|
|
22
|
+
PublicService,
|
|
23
|
+
PublicWaitlistEntry,
|
|
24
|
+
PublicWaitlistPayload,
|
|
25
|
+
Queue,
|
|
26
|
+
QueuePosition,
|
|
27
|
+
QueueTicket,
|
|
28
|
+
RescheduleBookingPayload,
|
|
29
|
+
Resource,
|
|
30
|
+
Service,
|
|
31
|
+
SlotQuery,
|
|
32
|
+
TimeSlot,
|
|
33
|
+
WaitlistEntry,
|
|
34
|
+
SchedulingTransport,
|
|
35
|
+
} from './types';
|
|
36
|
+
|
|
37
|
+
export type SchedulingClient = ReturnType<typeof createSchedulingClient>;
|
|
38
|
+
export type PublicSchedulingClient = ReturnType<typeof createPublicSchedulingClient>;
|
|
39
|
+
|
|
40
|
+
function appendParam(params: URLSearchParams, key: string, value: string | number | boolean | null | undefined) {
|
|
41
|
+
if (value === undefined || value === null || value === '') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
params.set(key, String(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function queryString(values: Record<string, string | number | boolean | null | undefined>): string {
|
|
48
|
+
const params = new URLSearchParams();
|
|
49
|
+
for (const [key, value] of Object.entries(values)) {
|
|
50
|
+
appendParam(params, key, value);
|
|
51
|
+
}
|
|
52
|
+
const serialized = params.toString();
|
|
53
|
+
return serialized ? `?${serialized}` : '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createSchedulingClient(request: SchedulingTransport) {
|
|
57
|
+
return {
|
|
58
|
+
listBranches() {
|
|
59
|
+
return request<{ items: Branch[] }>('/v1/scheduling/branches').then((response) => response.items ?? []);
|
|
60
|
+
},
|
|
61
|
+
listServices() {
|
|
62
|
+
return request<{ items: Service[] }>('/v1/scheduling/services').then((response) => response.items ?? []);
|
|
63
|
+
},
|
|
64
|
+
listResources(branchId?: string | null) {
|
|
65
|
+
return request<{ items: Resource[] }>(`/v1/scheduling/resources${queryString({ branch_id: branchId })}`).then(
|
|
66
|
+
(response) => response.items ?? [],
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
listAvailabilityRules(branchId?: string | null, resourceId?: string | null) {
|
|
70
|
+
return request<{ items: AvailabilityRule[] }>(
|
|
71
|
+
`/v1/scheduling/availability-rules${queryString({ branch_id: branchId, resource_id: resourceId })}`,
|
|
72
|
+
).then((response) => response.items ?? []);
|
|
73
|
+
},
|
|
74
|
+
listBlockedRanges(query: ListBlockedRangesQuery = {}) {
|
|
75
|
+
return request<{ items: BlockedRange[] }>(
|
|
76
|
+
`/v1/scheduling/blocked-ranges${queryString({
|
|
77
|
+
branch_id: query.branchId,
|
|
78
|
+
resource_id: query.resourceId,
|
|
79
|
+
date: query.date,
|
|
80
|
+
})}`,
|
|
81
|
+
).then((response) => response.items ?? []);
|
|
82
|
+
},
|
|
83
|
+
createBlockedRange(payload: BlockedRangePayload) {
|
|
84
|
+
return request<BlockedRange>('/v1/scheduling/blocked-ranges', { method: 'POST', body: payload });
|
|
85
|
+
},
|
|
86
|
+
updateBlockedRange(id: string, payload: BlockedRangePayload) {
|
|
87
|
+
return request<BlockedRange>(`/v1/scheduling/blocked-ranges/${id}`, { method: 'PATCH', body: payload });
|
|
88
|
+
},
|
|
89
|
+
deleteBlockedRange(id: string) {
|
|
90
|
+
return request<void>(`/v1/scheduling/blocked-ranges/${id}`, { method: 'DELETE' });
|
|
91
|
+
},
|
|
92
|
+
listSlots(query: SlotQuery) {
|
|
93
|
+
return request<{ items: TimeSlot[] }>(
|
|
94
|
+
`/v1/scheduling/slots${queryString({
|
|
95
|
+
branch_id: query.branchId,
|
|
96
|
+
service_id: query.serviceId,
|
|
97
|
+
resource_id: query.resourceId,
|
|
98
|
+
date: query.date,
|
|
99
|
+
})}`,
|
|
100
|
+
).then((response) => response.items ?? []);
|
|
101
|
+
},
|
|
102
|
+
listBookings(filter: ListBookingsFilter = {}) {
|
|
103
|
+
return request<{ items: Booking[] }>(
|
|
104
|
+
`/v1/scheduling/bookings${queryString({
|
|
105
|
+
branch_id: filter.branchId,
|
|
106
|
+
date: filter.date,
|
|
107
|
+
status: filter.status,
|
|
108
|
+
limit: filter.limit ?? 200,
|
|
109
|
+
})}`,
|
|
110
|
+
).then((response) => response.items ?? []);
|
|
111
|
+
},
|
|
112
|
+
getBooking(id: string) {
|
|
113
|
+
return request<Booking>(`/v1/scheduling/bookings/${id}`);
|
|
114
|
+
},
|
|
115
|
+
createBooking(payload: CreateBookingPayload) {
|
|
116
|
+
return request<Booking>('/v1/scheduling/bookings', { method: 'POST', body: payload });
|
|
117
|
+
},
|
|
118
|
+
confirmBooking(id: string) {
|
|
119
|
+
return request<Booking>(`/v1/scheduling/bookings/${id}/confirm`, { method: 'POST', body: {} });
|
|
120
|
+
},
|
|
121
|
+
cancelBooking(id: string, reason?: string) {
|
|
122
|
+
return request<Booking>(`/v1/scheduling/bookings/${id}/cancel`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: { reason: reason ?? '' },
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
checkInBooking(id: string) {
|
|
128
|
+
return request<Booking>(`/v1/scheduling/bookings/${id}/check-in`, { method: 'POST', body: {} });
|
|
129
|
+
},
|
|
130
|
+
startService(id: string) {
|
|
131
|
+
return request<Booking>(`/v1/scheduling/bookings/${id}/start-service`, { method: 'POST', body: {} });
|
|
132
|
+
},
|
|
133
|
+
completeBooking(id: string) {
|
|
134
|
+
return request<Booking>(`/v1/scheduling/bookings/${id}/complete`, { method: 'POST', body: {} });
|
|
135
|
+
},
|
|
136
|
+
markBookingNoShow(id: string, reason?: string) {
|
|
137
|
+
return request<Booking>(`/v1/scheduling/bookings/${id}/no-show`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
body: { reason: reason ?? '' },
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
rescheduleBooking(id: string, payload: RescheduleBookingPayload) {
|
|
143
|
+
return request<Booking>(`/v1/scheduling/bookings/${id}/reschedule`, { method: 'POST', body: payload });
|
|
144
|
+
},
|
|
145
|
+
listWaitlist(branchId?: string | null, serviceId?: string | null, status?: string, limit = 100) {
|
|
146
|
+
return request<{ items: WaitlistEntry[] }>(
|
|
147
|
+
`/v1/scheduling/waitlist${queryString({
|
|
148
|
+
branch_id: branchId,
|
|
149
|
+
service_id: serviceId,
|
|
150
|
+
status,
|
|
151
|
+
limit,
|
|
152
|
+
})}`,
|
|
153
|
+
).then((response) => response.items ?? []);
|
|
154
|
+
},
|
|
155
|
+
listQueues(branchId?: string | null) {
|
|
156
|
+
return request<{ items: Queue[] }>(`/v1/scheduling/queues${queryString({ branch_id: branchId })}`).then(
|
|
157
|
+
(response) => response.items ?? [],
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
createQueueTicket(queueId: string, payload: {
|
|
161
|
+
customer_name: string;
|
|
162
|
+
customer_phone: string;
|
|
163
|
+
customer_email?: string;
|
|
164
|
+
priority?: number;
|
|
165
|
+
notes?: string;
|
|
166
|
+
}) {
|
|
167
|
+
return request<QueueTicket>(`/v1/scheduling/queues/${queueId}/tickets`, { method: 'POST', body: payload });
|
|
168
|
+
},
|
|
169
|
+
getQueuePosition(queueId: string, ticketId: string) {
|
|
170
|
+
return request<QueuePosition>(`/v1/scheduling/queues/${queueId}/tickets/${ticketId}/position`);
|
|
171
|
+
},
|
|
172
|
+
pauseQueue(queueId: string) {
|
|
173
|
+
return request<Queue>(`/v1/scheduling/queues/${queueId}/pause`, { method: 'POST', body: {} });
|
|
174
|
+
},
|
|
175
|
+
reopenQueue(queueId: string) {
|
|
176
|
+
return request<Queue>(`/v1/scheduling/queues/${queueId}/reopen`, { method: 'POST', body: {} });
|
|
177
|
+
},
|
|
178
|
+
closeQueue(queueId: string) {
|
|
179
|
+
return request<Queue>(`/v1/scheduling/queues/${queueId}/close`, { method: 'POST', body: {} });
|
|
180
|
+
},
|
|
181
|
+
callNext(queueId: string, payload?: { serving_resource_id?: string; operator_user_id?: string }) {
|
|
182
|
+
return request<QueueTicket>(`/v1/scheduling/queues/${queueId}/next`, {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
body: payload ?? {},
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
serveTicket(queueId: string, ticketId: string, payload?: { serving_resource_id?: string; operator_user_id?: string }) {
|
|
188
|
+
return request<QueueTicket>(`/v1/scheduling/queues/${queueId}/tickets/${ticketId}/serve`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
body: payload ?? {},
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
completeTicket(queueId: string, ticketId: string, payload?: { serving_resource_id?: string; operator_user_id?: string }) {
|
|
194
|
+
return request<QueueTicket>(`/v1/scheduling/queues/${queueId}/tickets/${ticketId}/complete`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
body: payload ?? {},
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
markTicketNoShow(queueId: string, ticketId: string) {
|
|
200
|
+
return request<QueueTicket>(`/v1/scheduling/queues/${queueId}/tickets/${ticketId}/no-show`, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
body: {},
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
cancelTicket(queueId: string, ticketId: string) {
|
|
206
|
+
return request<QueueTicket>(`/v1/scheduling/queues/${queueId}/tickets/${ticketId}/cancel`, {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
body: {},
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
returnTicketToWaiting(queueId: string, ticketId: string) {
|
|
212
|
+
return request<QueueTicket>(`/v1/scheduling/queues/${queueId}/tickets/${ticketId}/return-to-waiting`, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
body: {},
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
getDashboard(branchId: string | null | undefined, day: string) {
|
|
218
|
+
return request<DashboardStats>(`/v1/scheduling/dashboard${queryString({ branch_id: branchId, day })}`);
|
|
219
|
+
},
|
|
220
|
+
getDayAgenda(branchId: string | null | undefined, day: string) {
|
|
221
|
+
return request<{ items: DayAgendaItem[] }>(`/v1/scheduling/day${queryString({ branch_id: branchId, day })}`).then(
|
|
222
|
+
(response) => response.items ?? [],
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function createPublicSchedulingClient(request: SchedulingTransport) {
|
|
229
|
+
return {
|
|
230
|
+
getBusinessInfo(orgId: string) {
|
|
231
|
+
return request<PublicBusinessInfo>(`/v1/public/${orgId}/info`).then((response) => ({
|
|
232
|
+
...response,
|
|
233
|
+
scheduling_enabled:
|
|
234
|
+
typeof response.scheduling_enabled === 'boolean'
|
|
235
|
+
? response.scheduling_enabled
|
|
236
|
+
: Boolean(response.appointments_enabled),
|
|
237
|
+
appointments_enabled:
|
|
238
|
+
typeof response.scheduling_enabled === 'boolean'
|
|
239
|
+
? response.scheduling_enabled
|
|
240
|
+
: Boolean(response.appointments_enabled),
|
|
241
|
+
}));
|
|
242
|
+
},
|
|
243
|
+
listServices(orgId: string) {
|
|
244
|
+
return request<{ items: PublicService[] }>(`/v1/public/${orgId}/scheduling/services`).then(
|
|
245
|
+
(response) => response.items ?? [],
|
|
246
|
+
);
|
|
247
|
+
},
|
|
248
|
+
getAvailability(orgId: string, query: PublicAvailabilityQuery) {
|
|
249
|
+
return request<{ slots?: PublicAvailabilitySlot[] }>(
|
|
250
|
+
`/v1/public/${orgId}/scheduling/availability${queryString({
|
|
251
|
+
branch_id: query.branchId,
|
|
252
|
+
service_id: query.serviceId,
|
|
253
|
+
resource_id: query.resourceId,
|
|
254
|
+
date: query.date,
|
|
255
|
+
duration: query.duration,
|
|
256
|
+
})}`,
|
|
257
|
+
).then((response) => response.slots ?? []);
|
|
258
|
+
},
|
|
259
|
+
book(orgId: string, payload: PublicBookPayload) {
|
|
260
|
+
return request<PublicBooking>(`/v1/public/${orgId}/scheduling/book`, { method: 'POST', body: payload });
|
|
261
|
+
},
|
|
262
|
+
listMyBookings(orgId: string, query: PublicMyBookingsQuery) {
|
|
263
|
+
return request<{ items: PublicBooking[] }>(
|
|
264
|
+
`/v1/public/${orgId}/scheduling/my-bookings${queryString({ phone: query.phone, limit: query.limit ?? 20 })}`,
|
|
265
|
+
).then((response) => response.items ?? []);
|
|
266
|
+
},
|
|
267
|
+
listQueues(orgId: string, branchId?: string | null) {
|
|
268
|
+
return request<{ items: PublicQueueSummary[] }>(
|
|
269
|
+
`/v1/public/${orgId}/scheduling/queues${queryString({ branch_id: branchId })}`,
|
|
270
|
+
).then((response) => response.items ?? []);
|
|
271
|
+
},
|
|
272
|
+
createQueueTicket(orgId: string, queueId: string, payload: PublicQueueTicketPayload) {
|
|
273
|
+
return request<PublicQueueTicket>(`/v1/public/${orgId}/scheduling/queues/${queueId}/tickets`, {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
body: payload,
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
getQueuePosition(orgId: string, queueId: string, ticketId: string) {
|
|
279
|
+
return request<PublicQueuePosition>(`/v1/public/${orgId}/scheduling/queues/${queueId}/tickets/${ticketId}/position`);
|
|
280
|
+
},
|
|
281
|
+
joinWaitlist(orgId: string, payload: PublicWaitlistPayload) {
|
|
282
|
+
return request<PublicWaitlistEntry>(`/v1/public/${orgId}/scheduling/waitlist`, {
|
|
283
|
+
method: 'POST',
|
|
284
|
+
body: payload,
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
confirmBooking(orgId: string, token: string) {
|
|
288
|
+
return request<PublicBooking>(`/v1/public/${orgId}/scheduling/bookings/actions/confirm`, {
|
|
289
|
+
method: 'POST',
|
|
290
|
+
body: { token },
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
cancelBooking(orgId: string, token: string, reason?: string) {
|
|
294
|
+
return request<PublicBooking>(`/v1/public/${orgId}/scheduling/bookings/actions/cancel`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
body: { token, reason: reason ?? '' },
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SchedulingCalendar,
|
|
3
|
+
schedulingCalendarCopyPresets,
|
|
4
|
+
type SchedulingCalendarProps,
|
|
5
|
+
} from './SchedulingCalendarBoard';
|
|
6
|
+
export {
|
|
7
|
+
QueueOperatorBoard,
|
|
8
|
+
queueOperatorBoardCopyPresets,
|
|
9
|
+
type QueueOperatorBoardProps,
|
|
10
|
+
} from './QueueOperatorBoard';
|
|
11
|
+
export {
|
|
12
|
+
PublicSchedulingFlow,
|
|
13
|
+
publicSchedulingFlowCopyPresets,
|
|
14
|
+
type PublicSchedulingFlowProps,
|
|
15
|
+
} from './PublicSchedulingFlow';
|
|
16
|
+
export {
|
|
17
|
+
SchedulingDaySummary,
|
|
18
|
+
schedulingDaySummaryCopyPresets,
|
|
19
|
+
type SchedulingDaySummaryProps,
|
|
20
|
+
} from './SchedulingDaySummary';
|
|
21
|
+
export {
|
|
22
|
+
createSchedulingClient,
|
|
23
|
+
createPublicSchedulingClient,
|
|
24
|
+
type PublicSchedulingClient,
|
|
25
|
+
type SchedulingClient,
|
|
26
|
+
} from './client';
|
|
27
|
+
export {
|
|
28
|
+
formatSchedulingClock,
|
|
29
|
+
formatSchedulingCompactClock,
|
|
30
|
+
formatSchedulingDateOnly,
|
|
31
|
+
formatSchedulingDateTime,
|
|
32
|
+
formatSchedulingWeekdayNarrow,
|
|
33
|
+
resolveSchedulingCopyLocale,
|
|
34
|
+
} from './locale';
|
|
35
|
+
export type { SchedulingCopyLocale } from './locale';
|
|
36
|
+
export type {
|
|
37
|
+
Booking,
|
|
38
|
+
Branch,
|
|
39
|
+
CreateBookingPayload,
|
|
40
|
+
DashboardStats,
|
|
41
|
+
DayAgendaItem,
|
|
42
|
+
PublicAvailabilityQuery,
|
|
43
|
+
PublicAvailabilitySlot,
|
|
44
|
+
PublicBusinessInfo,
|
|
45
|
+
PublicBookPayload,
|
|
46
|
+
PublicBooking,
|
|
47
|
+
PublicQueuePosition,
|
|
48
|
+
PublicQueueSummary,
|
|
49
|
+
PublicQueueTicket,
|
|
50
|
+
PublicQueueTicketPayload,
|
|
51
|
+
PublicService,
|
|
52
|
+
PublicWaitlistEntry,
|
|
53
|
+
PublicWaitlistPayload,
|
|
54
|
+
Queue,
|
|
55
|
+
QueuePosition,
|
|
56
|
+
QueueTicket,
|
|
57
|
+
Resource,
|
|
58
|
+
PublicSchedulingFlowCopy,
|
|
59
|
+
QueueOperatorBoardCopy,
|
|
60
|
+
SchedulingCalendarCopy,
|
|
61
|
+
SchedulingRequestOptions,
|
|
62
|
+
SchedulingTransport,
|
|
63
|
+
Service,
|
|
64
|
+
TimeSlot,
|
|
65
|
+
WaitlistEntry,
|
|
66
|
+
} from './types';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
formatSchedulingClock,
|
|
4
|
+
formatSchedulingCompactClock,
|
|
5
|
+
formatSchedulingDateOnly,
|
|
6
|
+
formatSchedulingDateTime,
|
|
7
|
+
formatSchedulingWeekdayNarrow,
|
|
8
|
+
resolveSchedulingCopyLocale,
|
|
9
|
+
} from './locale';
|
|
10
|
+
|
|
11
|
+
describe('scheduling locale helpers', () => {
|
|
12
|
+
it('resolves regional locales to the supported copy presets', () => {
|
|
13
|
+
expect(resolveSchedulingCopyLocale('es-AR')).toBe('es');
|
|
14
|
+
expect(resolveSchedulingCopyLocale('en-US')).toBe('en');
|
|
15
|
+
expect(resolveSchedulingCopyLocale(undefined)).toBe('en');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('formats date and time values with the provided locale', () => {
|
|
19
|
+
expect(formatSchedulingClock('2099-12-01T10:00:00Z', 'es-AR')).not.toBe(formatSchedulingClock('2099-12-01T10:00:00Z', 'en-US'));
|
|
20
|
+
expect(formatSchedulingCompactClock('2099-12-01T10:00:00Z', 'es-AR')).toMatch(/\d{2}:\d{2}/);
|
|
21
|
+
expect(formatSchedulingDateTime('2099-12-01T10:00:00Z', 'es-AR')).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/);
|
|
22
|
+
expect(formatSchedulingDateTime('2099-12-01T10:00:00Z', 'en-US')).toMatch(/dec/i);
|
|
23
|
+
expect(formatSchedulingDateTime('invalid-date', 'es-AR')).toBe('invalid-date');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('formats YYYY-MM-DD for display without exposing raw ISO date', () => {
|
|
27
|
+
expect(formatSchedulingDateOnly('2099-04-08', 'es-AR')).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/);
|
|
28
|
+
expect(formatSchedulingDateOnly('2099-04-08', 'es-AR')).not.toContain('2099-04');
|
|
29
|
+
expect(formatSchedulingDateOnly('not-a-date', 'es-AR')).toBe('not-a-date');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('formats weekday picks with the provided locale', () => {
|
|
33
|
+
expect(formatSchedulingWeekdayNarrow(1, 'es-AR')).not.toBe('');
|
|
34
|
+
expect(formatSchedulingWeekdayNarrow(1, 'en-US')).not.toBe('');
|
|
35
|
+
});
|
|
36
|
+
});
|
package/src/locale.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type SchedulingCopyLocale = 'en' | 'es';
|
|
2
|
+
|
|
3
|
+
export function resolveSchedulingCopyLocale(locale: string | undefined): SchedulingCopyLocale {
|
|
4
|
+
return locale?.toLowerCase().startsWith('es') ? 'es' : 'en';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isValidDate(value: Date): boolean {
|
|
8
|
+
return !Number.isNaN(value.getTime());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatSchedulingDateTime(value: string | null | undefined, locale: string | undefined): string {
|
|
12
|
+
if (!value) {
|
|
13
|
+
return '—';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parsed = new Date(value);
|
|
17
|
+
if (!isValidDate(parsed)) {
|
|
18
|
+
return String(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Español: fecha numérica día/mes/año (LATAM) + hora; inglés: mes abreviado.
|
|
22
|
+
if (resolveSchedulingCopyLocale(locale) === 'es') {
|
|
23
|
+
return new Intl.DateTimeFormat(locale, {
|
|
24
|
+
weekday: 'short',
|
|
25
|
+
day: '2-digit',
|
|
26
|
+
month: '2-digit',
|
|
27
|
+
year: 'numeric',
|
|
28
|
+
hour: '2-digit',
|
|
29
|
+
minute: '2-digit',
|
|
30
|
+
}).format(parsed);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new Intl.DateTimeFormat(locale, {
|
|
34
|
+
weekday: 'short',
|
|
35
|
+
day: '2-digit',
|
|
36
|
+
month: 'short',
|
|
37
|
+
hour: '2-digit',
|
|
38
|
+
minute: '2-digit',
|
|
39
|
+
}).format(parsed);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Convierte `YYYY-MM-DD` (valor de input type=date) a fecha local legible (p. ej. dd/mm/yyyy en es-AR). */
|
|
43
|
+
export function formatSchedulingDateOnly(ymd: string, locale: string | undefined): string {
|
|
44
|
+
const trimmed = ymd.trim();
|
|
45
|
+
const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
|
|
46
|
+
if (!parts) {
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
49
|
+
const year = Number(parts[1]);
|
|
50
|
+
const month = Number(parts[2]);
|
|
51
|
+
const day = Number(parts[3]);
|
|
52
|
+
if (!year || month < 1 || month > 12 || day < 1 || day > 31) {
|
|
53
|
+
return trimmed;
|
|
54
|
+
}
|
|
55
|
+
const parsed = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
56
|
+
if (!isValidDate(parsed)) {
|
|
57
|
+
return trimmed;
|
|
58
|
+
}
|
|
59
|
+
return new Intl.DateTimeFormat(locale, {
|
|
60
|
+
day: '2-digit',
|
|
61
|
+
month: '2-digit',
|
|
62
|
+
year: 'numeric',
|
|
63
|
+
}).format(parsed);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatSchedulingClock(value: string, locale: string | undefined): string {
|
|
67
|
+
const parsed = new Date(value);
|
|
68
|
+
if (!isValidDate(parsed)) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }).format(parsed);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatSchedulingCompactClock(value: string, locale: string | undefined): string {
|
|
76
|
+
const parsed = new Date(value);
|
|
77
|
+
if (!isValidDate(parsed)) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Intl.DateTimeFormat(locale, {
|
|
82
|
+
hour: '2-digit',
|
|
83
|
+
minute: '2-digit',
|
|
84
|
+
hour12: false,
|
|
85
|
+
}).format(parsed);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatSchedulingWeekdayNarrow(weekday: number, locale: string | undefined): string {
|
|
89
|
+
const date = new Date(Date.UTC(2026, 0, 4 + weekday));
|
|
90
|
+
return new Intl.DateTimeFormat(locale, { weekday: 'narrow' }).format(date);
|
|
91
|
+
}
|
package/src/next.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SchedulingCalendar,
|
|
3
|
+
schedulingCalendarCopyPresets,
|
|
4
|
+
type SchedulingCalendarProps,
|
|
5
|
+
} from './SchedulingCalendarBoard';
|
|
6
|
+
export {
|
|
7
|
+
QueueOperatorBoard,
|
|
8
|
+
queueOperatorBoardCopyPresets,
|
|
9
|
+
type QueueOperatorBoardProps,
|
|
10
|
+
} from './QueueOperatorBoard';
|
|
11
|
+
export {
|
|
12
|
+
PublicSchedulingFlow,
|
|
13
|
+
publicSchedulingFlowCopyPresets,
|
|
14
|
+
type PublicSchedulingFlowProps,
|
|
15
|
+
} from './PublicSchedulingFlow';
|
|
16
|
+
export {
|
|
17
|
+
SchedulingDaySummary,
|
|
18
|
+
schedulingDaySummaryCopyPresets,
|
|
19
|
+
type SchedulingDaySummaryProps,
|
|
20
|
+
} from './SchedulingDaySummary';
|
|
21
|
+
export {
|
|
22
|
+
createSchedulingClient,
|
|
23
|
+
createPublicSchedulingClient,
|
|
24
|
+
type PublicSchedulingClient,
|
|
25
|
+
type SchedulingClient,
|
|
26
|
+
} from './client';
|
|
27
|
+
export {
|
|
28
|
+
formatSchedulingClock,
|
|
29
|
+
formatSchedulingCompactClock,
|
|
30
|
+
formatSchedulingDateOnly,
|
|
31
|
+
formatSchedulingDateTime,
|
|
32
|
+
formatSchedulingWeekdayNarrow,
|
|
33
|
+
resolveSchedulingCopyLocale,
|
|
34
|
+
} from './locale';
|
|
35
|
+
export type { SchedulingCopyLocale } from './locale';
|
|
36
|
+
export type {
|
|
37
|
+
Booking,
|
|
38
|
+
Branch,
|
|
39
|
+
CreateBookingPayload,
|
|
40
|
+
DashboardStats,
|
|
41
|
+
DayAgendaItem,
|
|
42
|
+
PublicAvailabilityQuery,
|
|
43
|
+
PublicAvailabilitySlot,
|
|
44
|
+
PublicBusinessInfo,
|
|
45
|
+
PublicBookPayload,
|
|
46
|
+
PublicBooking,
|
|
47
|
+
PublicQueuePosition,
|
|
48
|
+
PublicQueueSummary,
|
|
49
|
+
PublicQueueTicket,
|
|
50
|
+
PublicQueueTicketPayload,
|
|
51
|
+
PublicService,
|
|
52
|
+
PublicWaitlistEntry,
|
|
53
|
+
PublicWaitlistPayload,
|
|
54
|
+
Queue,
|
|
55
|
+
QueuePosition,
|
|
56
|
+
QueueTicket,
|
|
57
|
+
Resource,
|
|
58
|
+
PublicSchedulingFlowCopy,
|
|
59
|
+
QueueOperatorBoardCopy,
|
|
60
|
+
SchedulingCalendarCopy,
|
|
61
|
+
SchedulingRequestOptions,
|
|
62
|
+
SchedulingTransport,
|
|
63
|
+
Service,
|
|
64
|
+
TimeSlot,
|
|
65
|
+
WaitlistEntry,
|
|
66
|
+
} from './types';
|