@arquimedes.co/eureka-forms 3.0.71 → 3.0.72
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/dist/@Types/BookingFormStep.d.ts +70 -18
- package/dist/@Types/BookingFormStep.js +21 -0
- package/dist/App/App.submitError.test.d.ts +1 -0
- package/dist/App/App.submitError.test.js +53 -0
- package/dist/BookingManage/AppointmentText.d.ts +5 -0
- package/dist/BookingManage/AppointmentText.js +20 -0
- package/dist/BookingManage/BookingManagePage.d.ts +17 -0
- package/dist/BookingManage/BookingManagePage.js +177 -0
- package/dist/BookingManage/BookingManagePage.module.css +115 -0
- package/dist/BookingManage/BookingManagePage.test.d.ts +1 -0
- package/dist/BookingManage/BookingManagePage.test.js +149 -0
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.d.ts +14 -5
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.js +8 -1
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.module.css +12 -0
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.d.ts +1 -0
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.js +21 -0
- package/dist/Form/Form.d.ts +3 -0
- package/dist/Form/Form.js +9 -2
- package/dist/Form/FormTypes/ColumnForm/ColumnForm.d.ts +1 -1
- package/dist/Form/FormTypes/ColumnForm/ColumnForm.js +3 -2
- package/dist/Form/FormTypes/StepperForm/StepperForm.d.ts +1 -1
- package/dist/Form/FormTypes/StepperForm/StepperForm.js +3 -2
- package/dist/Form/SubmitError/SubmitError.d.ts +11 -0
- package/dist/Form/SubmitError/SubmitError.js +9 -0
- package/dist/Form/SubmitError/SubmitError.module.css +25 -0
- package/dist/Form/SubmitError/SubmitError.test.d.ts +1 -0
- package/dist/Form/SubmitError/SubmitError.test.js +34 -0
- package/dist/Form/SubmitError/SubmitErrorReason.d.ts +13 -0
- package/dist/Form/SubmitError/SubmitErrorReason.js +20 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.d.ts +8 -1
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +197 -41
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.d.ts +1 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +275 -0
- package/dist/Services/BookingService.d.ts +159 -12
- package/dist/Services/BookingService.js +210 -12
- package/dist/Services/BookingService.test.js +130 -9
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +63 -12
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +141 -36
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +41 -3
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +11 -3
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +69 -9
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +13 -15
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +331 -62
- package/dist/main.js +4 -1
- package/package.json +1 -1
|
@@ -1,62 +1,93 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { useState } from 'react';
|
|
4
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
6
|
import '@testing-library/jest-dom';
|
|
6
|
-
import { format } from 'date-fns';
|
|
7
|
+
import { addDays, format } from 'date-fns';
|
|
8
|
+
import { es } from 'date-fns/locale';
|
|
7
9
|
import ErkBookingPicker from './ErkBookingPicker';
|
|
8
10
|
import MaterialProviders from '../../Utils/MaterialProviders';
|
|
9
11
|
import InternalFormStyle from '../../constants/InternalFormStyle';
|
|
10
12
|
import { PersonSelectionMode } from '../../@Types/BookingFormStep';
|
|
11
13
|
const SLOTS = [
|
|
12
14
|
{ id: 's-1', startTime: '09:00', endTime: '09:50', available: true },
|
|
13
|
-
{ id: 's-2', startTime: '10:00', endTime: '10:50', available: false },
|
|
14
15
|
{ id: 's-3', startTime: '11:00', endTime: '11:50', available: true },
|
|
16
|
+
{ id: 's-4', startTime: '14:00', endTime: '14:50', available: true },
|
|
17
|
+
];
|
|
18
|
+
const TEMPLATES = [
|
|
19
|
+
{ id: 't-1', name: 'Valoración', description: 'Primera visita, sin costo.', durationMinutes: 30 },
|
|
20
|
+
{ id: 't-2', name: 'Asesoría de crédito', durationMinutes: 45 },
|
|
15
21
|
];
|
|
16
22
|
const PERSONS = [
|
|
17
23
|
{ id: 'p-1', name: 'María González', role: 'Asesora' },
|
|
18
24
|
{ id: 'p-2', name: 'Carlos Ruiz', role: 'Técnico' },
|
|
19
25
|
];
|
|
26
|
+
/** The accessible name of a day button: its full date. */
|
|
27
|
+
function dayName(date) {
|
|
28
|
+
return format(date, "EEEE d 'de' MMMM 'de' yyyy", { locale: es });
|
|
29
|
+
}
|
|
20
30
|
function clickableToday() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
return screen.getByRole('button', { name: dayName(new Date()) });
|
|
32
|
+
}
|
|
33
|
+
/** A slot button by its start time; the first one of the day also reads "Primera disponible". */
|
|
34
|
+
function hour(time) {
|
|
35
|
+
return screen.findByRole('button', { name: new RegExp(`^${time}`) });
|
|
36
|
+
}
|
|
37
|
+
function renderPicker(props) {
|
|
38
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: () => { }, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.NONE, ...props }) }));
|
|
39
|
+
}
|
|
40
|
+
/** Opens the wizard and walks to the confirm step with today's 09:00. */
|
|
41
|
+
async function walkToConfirm(user) {
|
|
42
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
43
|
+
await user.click(clickableToday());
|
|
44
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
45
|
+
await user.click(await hour('09:00'));
|
|
46
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
47
|
+
await screen.findByRole('heading', { name: 'Revisa y reserva tu horario' });
|
|
27
48
|
}
|
|
28
49
|
describe('ErkBookingPicker (wizard)', () => {
|
|
29
|
-
it('drives the full flow: person → day → hour →
|
|
50
|
+
it('drives the full flow: person → day → hour → reserve, asking for name and email', async () => {
|
|
30
51
|
const user = userEvent.setup();
|
|
31
52
|
const onChange = vi.fn();
|
|
32
53
|
const fetchSlots = vi.fn(() => Promise.resolve(SLOTS));
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
renderPicker({
|
|
55
|
+
onChange,
|
|
56
|
+
personSelectionMode: PersonSelectionMode.OPTIONAL,
|
|
57
|
+
persons: PERSONS,
|
|
58
|
+
askEmail: true,
|
|
59
|
+
fetchSlots,
|
|
60
|
+
});
|
|
35
61
|
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
36
|
-
// Paso 1: persona — Siguiente bloqueado hasta elegir
|
|
37
62
|
expect(await screen.findByRole('heading', { name: '¿Con quién quieres agendar?' })).toBeInTheDocument();
|
|
38
63
|
const next = screen.getByRole('button', { name: 'Siguiente' });
|
|
39
64
|
expect(next).toBeDisabled();
|
|
40
65
|
await user.click(screen.getByText('María González'));
|
|
41
66
|
expect(next).toBeEnabled();
|
|
42
67
|
await user.click(next);
|
|
43
|
-
// Paso 2: día
|
|
44
68
|
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
45
69
|
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeDisabled();
|
|
46
70
|
await user.click(clickableToday());
|
|
47
71
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
48
|
-
// Paso 3: hora — slots del fetch; ocupados deshabilitados
|
|
49
72
|
expect(await screen.findByRole('heading', { name: 'Elige una hora' })).toBeInTheDocument();
|
|
50
73
|
expect(fetchSlots).toHaveBeenCalledWith({ personId: 'p-1', date: format(new Date(), 'yyyy-MM-dd') });
|
|
51
|
-
|
|
52
|
-
expect(
|
|
53
|
-
|
|
74
|
+
// Grouped by part of the day, with the first one flagged; nothing is shown as taken.
|
|
75
|
+
expect(screen.getByRole('heading', { name: 'Mañana' })).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByRole('heading', { name: 'Tarde' })).toBeInTheDocument();
|
|
77
|
+
expect(await hour('09:00')).toHaveTextContent('Primera disponible');
|
|
78
|
+
expect(screen.queryByText(/Tachados/)).not.toBeInTheDocument();
|
|
79
|
+
await user.click(await hour('09:00'));
|
|
54
80
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
55
|
-
|
|
56
|
-
expect(
|
|
57
|
-
|
|
58
|
-
await user.type(screen.getByLabelText(
|
|
59
|
-
|
|
81
|
+
expect(await screen.findByRole('heading', { name: 'Revisa y reserva tu horario' })).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByText(/La cita se confirma cuando envíes el formulario/)).toBeInTheDocument();
|
|
83
|
+
const reserve = screen.getByRole('button', { name: 'Reservar' });
|
|
84
|
+
await user.type(screen.getByLabelText(/Nombre/), 'Juan Pérez');
|
|
85
|
+
expect(reserve).toBeDisabled();
|
|
86
|
+
await user.type(screen.getByLabelText(/Correo/), 'juan@');
|
|
87
|
+
expect(reserve).toBeDisabled();
|
|
88
|
+
await user.type(screen.getByLabelText(/Correo/), 'correo.com');
|
|
89
|
+
expect(reserve).toBeEnabled();
|
|
90
|
+
await user.click(reserve);
|
|
60
91
|
expect(onChange).toHaveBeenCalledWith({
|
|
61
92
|
type: 'BOOKING_VALUE',
|
|
62
93
|
slotId: 's-1',
|
|
@@ -70,9 +101,43 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
70
101
|
status: 'PENDING',
|
|
71
102
|
});
|
|
72
103
|
});
|
|
104
|
+
it('explains that without an email there is no confirmation', async () => {
|
|
105
|
+
const user = userEvent.setup();
|
|
106
|
+
renderPicker({ askEmail: true, fetchSlots: () => Promise.resolve(SLOTS) });
|
|
107
|
+
await walkToConfirm(user);
|
|
108
|
+
expect(screen.getByText('Sin correo no podremos enviarte la confirmación.')).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
it('reuses the contact the form already has and asks for neither field', async () => {
|
|
111
|
+
const user = userEvent.setup();
|
|
112
|
+
const holdSlot = vi.fn(() => Promise.resolve({ appointment: 'a', hold: 'h', lockedUntil: 'x' }));
|
|
113
|
+
renderPicker({
|
|
114
|
+
knownContact: { name: 'Ana Gómez', email: 'ana@correo.com' },
|
|
115
|
+
fetchSlots: () => Promise.resolve(SLOTS),
|
|
116
|
+
holdSlot,
|
|
117
|
+
});
|
|
118
|
+
await walkToConfirm(user);
|
|
119
|
+
expect(screen.queryByLabelText(/Nombre/)).not.toBeInTheDocument();
|
|
120
|
+
expect(screen.queryByLabelText(/Correo/)).not.toBeInTheDocument();
|
|
121
|
+
await user.click(screen.getByRole('button', { name: 'Reservar' }));
|
|
122
|
+
expect(holdSlot).toHaveBeenCalledWith({
|
|
123
|
+
slot: SLOTS[0],
|
|
124
|
+
personId: undefined,
|
|
125
|
+
contact: { name: 'Ana Gómez', email: 'ana@correo.com' },
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
it('always asks for the name, even when the email comes from another step', async () => {
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
renderPicker({ knownContact: { email: 'ana@correo.com' }, fetchSlots: () => Promise.resolve(SLOTS) });
|
|
131
|
+
await walkToConfirm(user);
|
|
132
|
+
const reserve = screen.getByRole('button', { name: 'Reservar' });
|
|
133
|
+
expect(reserve).toBeDisabled();
|
|
134
|
+
expect(screen.queryByLabelText(/Correo/)).not.toBeInTheDocument();
|
|
135
|
+
await user.type(screen.getByLabelText(/Nombre/), 'Juan');
|
|
136
|
+
expect(reserve).toBeEnabled();
|
|
137
|
+
});
|
|
73
138
|
it('skips the person step when personSelectionMode is NONE', async () => {
|
|
74
139
|
const user = userEvent.setup();
|
|
75
|
-
|
|
140
|
+
renderPicker({ durationMinutes: 30, fetchSlots: () => Promise.resolve(SLOTS) });
|
|
76
141
|
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
77
142
|
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
78
143
|
expect(screen.queryByText('¿Con quién quieres agendar?')).not.toBeInTheDocument();
|
|
@@ -80,15 +145,17 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
80
145
|
});
|
|
81
146
|
it('allows navigating back through the breadcrumb without losing the selection', async () => {
|
|
82
147
|
const user = userEvent.setup();
|
|
83
|
-
|
|
148
|
+
renderPicker({
|
|
149
|
+
personSelectionMode: PersonSelectionMode.REQUIRED,
|
|
150
|
+
persons: PERSONS,
|
|
151
|
+
fetchSlots: () => Promise.resolve(SLOTS),
|
|
152
|
+
});
|
|
84
153
|
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
85
154
|
await user.click(await screen.findByText('Carlos Ruiz'));
|
|
86
155
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
87
156
|
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
88
|
-
// Volver por el breadcrumb: el paso "Persona" completado es clicable
|
|
89
157
|
await user.click(screen.getByRole('button', { name: /Persona/ }));
|
|
90
158
|
expect(await screen.findByRole('heading', { name: '¿Con quién quieres agendar?' })).toBeInTheDocument();
|
|
91
|
-
// La selección se mantiene y permite avanzar de nuevo
|
|
92
159
|
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeEnabled();
|
|
93
160
|
});
|
|
94
161
|
it('offers only the days the host names, and holds the slot before the value is set', async () => {
|
|
@@ -96,12 +163,24 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
96
163
|
const onChange = vi.fn();
|
|
97
164
|
const today = format(new Date(), 'yyyy-MM-dd');
|
|
98
165
|
const fetchDays = vi.fn(() => Promise.resolve([today]));
|
|
99
|
-
const holdSlot = vi.fn(() => Promise.resolve({
|
|
100
|
-
|
|
166
|
+
const holdSlot = vi.fn(() => Promise.resolve({
|
|
167
|
+
appointment: 'appt-1',
|
|
168
|
+
hold: 'secret-1',
|
|
169
|
+
lockedUntil: '2026-09-22T14:15:00.000Z',
|
|
170
|
+
startTime: '09:15',
|
|
171
|
+
endTime: '10:05',
|
|
172
|
+
personName: 'Bruno Pérez',
|
|
173
|
+
}));
|
|
174
|
+
renderPicker({
|
|
175
|
+
onChange,
|
|
176
|
+
knownContact: { name: 'Ana', email: 'ana@correo.com' },
|
|
177
|
+
fetchDays,
|
|
178
|
+
fetchSlots: () => Promise.resolve(SLOTS),
|
|
179
|
+
holdSlot,
|
|
180
|
+
});
|
|
101
181
|
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
102
182
|
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
103
183
|
expect(fetchDays).toHaveBeenCalledWith({ personId: undefined });
|
|
104
|
-
// Only today is on offer: every other day of the month stays disabled.
|
|
105
184
|
const todayButton = clickableToday();
|
|
106
185
|
const enabledDays = screen
|
|
107
186
|
.getAllByRole('button')
|
|
@@ -109,14 +188,16 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
109
188
|
expect(enabledDays).toEqual([todayButton]);
|
|
110
189
|
await user.click(todayButton);
|
|
111
190
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
112
|
-
await user.click(await
|
|
191
|
+
await user.click(await hour('09:00'));
|
|
113
192
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
114
|
-
await user.click(await screen.findByRole('button', { name: '
|
|
115
|
-
//
|
|
116
|
-
expect(holdSlot).toHaveBeenCalledWith({ slot: SLOTS[0], personId: undefined, contact: undefined });
|
|
193
|
+
await user.click(await screen.findByRole('button', { name: 'Reservar' }));
|
|
194
|
+
// What the hold says wins over the picker's own selection: real hours and the assigned person.
|
|
117
195
|
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({
|
|
118
196
|
slotId: 's-1',
|
|
119
197
|
date: today,
|
|
198
|
+
startTime: '09:15',
|
|
199
|
+
endTime: '10:05',
|
|
200
|
+
personName: 'Bruno Pérez',
|
|
120
201
|
appointment: 'appt-1',
|
|
121
202
|
hold: 'secret-1',
|
|
122
203
|
lockedUntil: '2026-09-22T14:15:00.000Z',
|
|
@@ -128,44 +209,232 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
128
209
|
const onChange = vi.fn();
|
|
129
210
|
const fetchSlots = vi.fn(() => Promise.resolve(SLOTS));
|
|
130
211
|
const holdSlot = vi.fn(() => Promise.reject(new Error('Ese horario ya fue reservado por alguien más')));
|
|
131
|
-
|
|
132
|
-
await user
|
|
133
|
-
await user.click(
|
|
134
|
-
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
135
|
-
await user.click(await screen.findByRole('button', { name: '09:00' }));
|
|
136
|
-
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
137
|
-
await user.click(await screen.findByRole('button', { name: 'Confirmar cita' }));
|
|
212
|
+
renderPicker({ onChange, knownContact: { name: 'Ana' }, fetchSlots, holdSlot });
|
|
213
|
+
await walkToConfirm(user);
|
|
214
|
+
await user.click(await screen.findByRole('button', { name: 'Reservar' }));
|
|
138
215
|
expect(await screen.findByRole('alert')).toHaveTextContent('Ese horario ya fue reservado por alguien más');
|
|
139
216
|
expect(screen.getByRole('heading', { name: 'Elige una hora' })).toBeInTheDocument();
|
|
140
|
-
// The slots are asked for again, since the offer just changed under the requester.
|
|
141
217
|
expect(fetchSlots).toHaveBeenCalledTimes(2);
|
|
142
218
|
expect(onChange).not.toHaveBeenCalled();
|
|
143
219
|
});
|
|
144
|
-
it('
|
|
220
|
+
it('keeps the hours of the last day picked when an earlier answer arrives late', async () => {
|
|
145
221
|
const user = userEvent.setup();
|
|
146
|
-
const
|
|
147
|
-
|
|
222
|
+
const tomorrow = addDays(new Date(), 1);
|
|
223
|
+
const resolvers = {};
|
|
224
|
+
const fetchSlots = vi.fn(({ date }) => new Promise((resolve) => {
|
|
225
|
+
resolvers[date] = resolve;
|
|
226
|
+
}));
|
|
227
|
+
const todayKey = format(new Date(), 'yyyy-MM-dd');
|
|
228
|
+
const tomorrowKey = format(tomorrow, 'yyyy-MM-dd');
|
|
229
|
+
renderPicker({
|
|
230
|
+
fetchDays: () => Promise.resolve([todayKey, tomorrowKey]),
|
|
231
|
+
fetchSlots,
|
|
232
|
+
});
|
|
233
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
234
|
+
await user.click(await screen.findByRole('button', { name: dayName(new Date()) }));
|
|
235
|
+
if (format(tomorrow, 'M') !== format(new Date(), 'M')) {
|
|
236
|
+
await user.click(screen.getByRole('button', { name: 'Mes siguiente' }));
|
|
237
|
+
}
|
|
238
|
+
await user.click(screen.getByRole('button', { name: dayName(tomorrow) }));
|
|
239
|
+
await waitFor(() => expect(resolvers[tomorrowKey]).toBeDefined());
|
|
240
|
+
resolvers[tomorrowKey]([{ id: 't-1', startTime: '15:00', endTime: '15:50', available: true }]);
|
|
241
|
+
resolvers[todayKey]([{ id: 'd-1', startTime: '08:00', endTime: '08:50', available: true }]);
|
|
242
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
243
|
+
expect(await hour('15:00')).toBeInTheDocument();
|
|
244
|
+
expect(screen.queryByRole('button', { name: /^08:00/ })).not.toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
it('tells a failed load of hours apart from an empty day, and retries', async () => {
|
|
247
|
+
const user = userEvent.setup();
|
|
248
|
+
const fetchSlots = vi
|
|
249
|
+
.fn()
|
|
250
|
+
.mockRejectedValueOnce(new Error('Hay muchas consultas en este momento. Intenta de nuevo en unos segundos.'))
|
|
251
|
+
.mockResolvedValueOnce(SLOTS);
|
|
252
|
+
renderPicker({ fetchSlots });
|
|
148
253
|
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
149
254
|
await user.click(clickableToday());
|
|
150
255
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
151
|
-
|
|
256
|
+
expect(await screen.findByRole('alert')).toHaveTextContent('Hay muchas consultas en este momento');
|
|
257
|
+
expect(screen.queryByText('No hay horarios disponibles ese día.')).not.toBeInTheDocument();
|
|
258
|
+
await user.click(screen.getByRole('button', { name: 'Reintentar' }));
|
|
259
|
+
expect(await hour('09:00')).toBeInTheDocument();
|
|
260
|
+
});
|
|
261
|
+
it('still says a day is empty when it really has no hours', async () => {
|
|
262
|
+
const user = userEvent.setup();
|
|
263
|
+
renderPicker({ fetchSlots: () => Promise.resolve([]) });
|
|
264
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
265
|
+
await user.click(clickableToday());
|
|
266
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
267
|
+
expect(await screen.findByText('No hay horarios disponibles ese día.')).toBeInTheDocument();
|
|
268
|
+
});
|
|
269
|
+
it('tells a failed load of days apart from a person without days, and retries', async () => {
|
|
270
|
+
const user = userEvent.setup();
|
|
271
|
+
const today = format(new Date(), 'yyyy-MM-dd');
|
|
272
|
+
const fetchDays = vi
|
|
273
|
+
.fn()
|
|
274
|
+
.mockRejectedValueOnce(new Error('Esa persona ya no está disponible'))
|
|
275
|
+
.mockResolvedValueOnce([today]);
|
|
276
|
+
renderPicker({ fetchDays, fetchSlots: () => Promise.resolve(SLOTS) });
|
|
277
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
278
|
+
expect(await screen.findByRole('alert')).toHaveTextContent('Esa persona ya no está disponible');
|
|
279
|
+
expect(screen.queryByText('No hay días con horarios disponibles.')).not.toBeInTheDocument();
|
|
280
|
+
expect(clickableToday()).toBeDisabled();
|
|
281
|
+
await user.click(screen.getByRole('button', { name: 'Reintentar' }));
|
|
282
|
+
await waitFor(() => expect(clickableToday()).toBeEnabled());
|
|
283
|
+
});
|
|
284
|
+
it('names the organization clock and the requester clock when they differ', async () => {
|
|
285
|
+
const user = userEvent.setup();
|
|
286
|
+
const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
287
|
+
const zone = local === 'Asia/Tokyo' ? 'America/Bogota' : 'Asia/Tokyo';
|
|
288
|
+
const start = new Date();
|
|
289
|
+
start.setUTCHours(0, 0, 0, 0);
|
|
290
|
+
const slot = {
|
|
291
|
+
id: 'z-1',
|
|
292
|
+
startTime: new Intl.DateTimeFormat('en-GB', {
|
|
293
|
+
hour: '2-digit',
|
|
294
|
+
minute: '2-digit',
|
|
295
|
+
hourCycle: 'h23',
|
|
296
|
+
timeZone: zone,
|
|
297
|
+
}).format(start),
|
|
298
|
+
endTime: '23:59',
|
|
299
|
+
available: true,
|
|
300
|
+
start: start.toISOString(),
|
|
301
|
+
};
|
|
302
|
+
const localTime = new Intl.DateTimeFormat('en-GB', {
|
|
303
|
+
hour: '2-digit',
|
|
304
|
+
minute: '2-digit',
|
|
305
|
+
hourCycle: 'h23',
|
|
306
|
+
timeZone: local,
|
|
307
|
+
}).format(start);
|
|
308
|
+
renderPicker({ timeZone: zone, fetchSlots: () => Promise.resolve([slot]) });
|
|
309
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
310
|
+
await user.click(clickableToday());
|
|
152
311
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
312
|
+
expect(await screen.findByText(/^Horas en hora de Tokyo|^Horas en hora de Bogotá/)).toBeInTheDocument();
|
|
313
|
+
await user.click(await hour(slot.startTime));
|
|
314
|
+
expect(screen.getAllByText(new RegExp(`\\(${localTime} en tu hora\\)`)).length).toBeGreaterThan(0);
|
|
315
|
+
});
|
|
316
|
+
it('does not show the buffer, an internal detail', async () => {
|
|
317
|
+
const user = userEvent.setup();
|
|
318
|
+
renderPicker({ fetchSlots: () => Promise.resolve(SLOTS) });
|
|
319
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
320
|
+
expect(screen.queryByText(/buffer/)).not.toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
it('sends the requester back to the hours when a held slot lapses', async () => {
|
|
323
|
+
const user = userEvent.setup();
|
|
324
|
+
const fetchSlots = vi.fn(() => Promise.resolve(SLOTS));
|
|
325
|
+
function Host({ lapsed }) {
|
|
326
|
+
const [value, setValue] = useState(null);
|
|
327
|
+
return (_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: lapsed ? null : value, onChange: setValue, label: "Agendar cita", durationMinutes: 50, knownContact: { name: 'Ana' }, lapsed: lapsed, fetchSlots: fetchSlots, holdSlot: () => Promise.resolve({ appointment: 'a', hold: 'h', lockedUntil: 'x' }) }) }));
|
|
328
|
+
}
|
|
329
|
+
const { rerender } = render(_jsx(Host, {}));
|
|
330
|
+
await walkToConfirm(user);
|
|
331
|
+
await user.click(screen.getByRole('button', { name: 'Reservar' }));
|
|
332
|
+
expect(await screen.findByRole('button', { name: 'Cambiar' })).toBeInTheDocument();
|
|
333
|
+
rerender(_jsx(Host, { lapsed: "Tu reserva venci\u00F3; elige la hora de nuevo." }));
|
|
334
|
+
expect(await screen.findByRole('alert')).toHaveTextContent('Tu reserva venció; elige la hora de nuevo.');
|
|
335
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
336
|
+
expect(await screen.findByRole('heading', { name: 'Elige una hora' })).toBeInTheDocument();
|
|
337
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Tu reserva venció; elige la hora de nuevo.');
|
|
338
|
+
// Fresh hours: the lapsed slot may be gone.
|
|
339
|
+
await waitFor(() => expect(fetchSlots).toHaveBeenCalledTimes(2));
|
|
340
|
+
});
|
|
341
|
+
it('renders the compact summary with the assigned person and the pending note', () => {
|
|
342
|
+
renderPicker({
|
|
343
|
+
value: {
|
|
344
|
+
type: 'BOOKING_VALUE',
|
|
345
|
+
slotId: 's-3',
|
|
346
|
+
date: '2026-07-02',
|
|
347
|
+
startTime: '11:00',
|
|
348
|
+
endTime: '11:50',
|
|
349
|
+
personName: 'María González',
|
|
350
|
+
status: 'PENDING',
|
|
351
|
+
lockedUntil: '2026-07-01T15:30:00.000Z',
|
|
352
|
+
},
|
|
353
|
+
timeZone: 'America/Bogota',
|
|
354
|
+
});
|
|
355
|
+
expect(screen.getByText(/11:00–11:50 · con María González/)).toBeInTheDocument();
|
|
356
|
+
expect(screen.getByText(/Reservado hasta las 10:30\. Envía el formulario para confirmar la cita\./)).toBeInTheDocument();
|
|
169
357
|
expect(screen.getByRole('button', { name: 'Cambiar' })).toBeInTheDocument();
|
|
170
358
|
});
|
|
359
|
+
it('asks for the event template first and narrows days, hours and the hold to it', async () => {
|
|
360
|
+
const user = userEvent.setup();
|
|
361
|
+
const onChange = vi.fn();
|
|
362
|
+
const fetchDays = vi.fn(() => Promise.resolve([format(new Date(), 'yyyy-MM-dd')]));
|
|
363
|
+
const fetchSlots = vi.fn(() => Promise.resolve(SLOTS));
|
|
364
|
+
const holdSlot = vi.fn(() => Promise.resolve({ eventTemplate: { name: 'Asesoría de crédito', durationMinutes: 45 } }));
|
|
365
|
+
renderPicker({
|
|
366
|
+
onChange,
|
|
367
|
+
eventTemplates: TEMPLATES,
|
|
368
|
+
knownContact: { name: 'Ana', email: 'ana@correo.com' },
|
|
369
|
+
fetchDays,
|
|
370
|
+
fetchSlots,
|
|
371
|
+
holdSlot,
|
|
372
|
+
});
|
|
373
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
374
|
+
expect(await screen.findByRole('heading', { name: '¿Qué necesitas?' })).toBeInTheDocument();
|
|
375
|
+
expect(screen.getByRole('button', { name: /^1Tipo/ })).toBeInTheDocument();
|
|
376
|
+
// Nothing is asked of the edge before a type is picked: the days depend on it.
|
|
377
|
+
expect(fetchDays).not.toHaveBeenCalled();
|
|
378
|
+
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeDisabled();
|
|
379
|
+
expect(screen.getByText('Primera visita, sin costo.')).toBeInTheDocument();
|
|
380
|
+
await user.click(screen.getByRole('button', { name: /Asesoría de crédito/ }));
|
|
381
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
382
|
+
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
383
|
+
expect(fetchDays).toHaveBeenLastCalledWith({ personId: undefined, eventTemplate: 't-2' });
|
|
384
|
+
expect(screen.getByText('45 min')).toBeInTheDocument();
|
|
385
|
+
await user.click(clickableToday());
|
|
386
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
387
|
+
expect(await screen.findByText(/citas de 45 min/)).toBeInTheDocument();
|
|
388
|
+
expect(fetchSlots).toHaveBeenLastCalledWith({
|
|
389
|
+
personId: undefined,
|
|
390
|
+
date: format(new Date(), 'yyyy-MM-dd'),
|
|
391
|
+
eventTemplate: 't-2',
|
|
392
|
+
});
|
|
393
|
+
await user.click(await hour('09:00'));
|
|
394
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
395
|
+
await user.click(await screen.findByRole('button', { name: 'Reservar' }));
|
|
396
|
+
expect(holdSlot).toHaveBeenCalledWith(expect.objectContaining({ eventTemplate: 't-2' }));
|
|
397
|
+
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ eventTemplate: { name: 'Asesoría de crédito', durationMinutes: 45 } }));
|
|
398
|
+
});
|
|
399
|
+
it('drops the day and hour picked for a type when the requester goes back and picks another', async () => {
|
|
400
|
+
const user = userEvent.setup();
|
|
401
|
+
renderPicker({ eventTemplates: TEMPLATES, fetchSlots: () => Promise.resolve(SLOTS) });
|
|
402
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
403
|
+
await user.click(await screen.findByRole('button', { name: /Valoración/ }));
|
|
404
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
405
|
+
await user.click(clickableToday());
|
|
406
|
+
await user.click(screen.getByRole('button', { name: 'Atrás' }));
|
|
407
|
+
await user.click(screen.getByRole('button', { name: /Asesoría de crédito/ }));
|
|
408
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
409
|
+
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
410
|
+
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeDisabled();
|
|
411
|
+
});
|
|
412
|
+
it('offers no type step with a single event template', async () => {
|
|
413
|
+
const user = userEvent.setup();
|
|
414
|
+
renderPicker({ eventTemplates: [TEMPLATES[0]], fetchSlots: () => Promise.resolve(SLOTS) });
|
|
415
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
416
|
+
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
417
|
+
expect(screen.queryByRole('heading', { name: '¿Qué necesitas?' })).not.toBeInTheDocument();
|
|
418
|
+
});
|
|
419
|
+
it('shows the picked event template and its duration in the compact summary', () => {
|
|
420
|
+
renderPicker({
|
|
421
|
+
value: {
|
|
422
|
+
type: 'BOOKING_VALUE',
|
|
423
|
+
date: '2026-07-02',
|
|
424
|
+
startTime: '11:00',
|
|
425
|
+
endTime: '11:45',
|
|
426
|
+
status: 'PENDING',
|
|
427
|
+
eventTemplate: { name: 'Asesoría de crédito', durationMinutes: 45 },
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
expect(screen.getByText('Asesoría de crédito · 45 min')).toBeInTheDocument();
|
|
431
|
+
});
|
|
432
|
+
it('offers to retry when the offer did not load', async () => {
|
|
433
|
+
const user = userEvent.setup();
|
|
434
|
+
const onRetryLoad = vi.fn();
|
|
435
|
+
renderPicker({ disabled: true, loadError: 'No pudimos cargar los horarios.', onRetryLoad });
|
|
436
|
+
expect(screen.getByRole('alert')).toHaveTextContent('No pudimos cargar los horarios.');
|
|
437
|
+
await user.click(screen.getByRole('button', { name: 'Reintentar' }));
|
|
438
|
+
expect(onRetryLoad).toHaveBeenCalledTimes(1);
|
|
439
|
+
});
|
|
171
440
|
});
|
package/dist/main.js
CHANGED
|
@@ -3,6 +3,7 @@ import './Init';
|
|
|
3
3
|
import ReactDOM from 'react-dom/client';
|
|
4
4
|
import styles from './index.module.css';
|
|
5
5
|
import App from './App/App';
|
|
6
|
+
import BookingManagePage from './BookingManage/BookingManagePage';
|
|
6
7
|
import './index.css';
|
|
7
8
|
import './Utils/MuiClassnameSetup';
|
|
8
9
|
const urlParams = new URLSearchParams(window.location.search);
|
|
@@ -22,4 +23,6 @@ const scrollToTop = () => {
|
|
|
22
23
|
location.href = '#eureka-root';
|
|
23
24
|
location.href = '#';
|
|
24
25
|
};
|
|
25
|
-
|
|
26
|
+
/** The link in the appointment emails, `/booking/<token>`: the appointment's own page, not a form. */
|
|
27
|
+
const bookingLink = /^\/booking\/([^/]+)\/?$/.exec(window.location.pathname);
|
|
28
|
+
ReactDOM.createRoot(document.getElementById('eureka-root')).render(_jsx("div", { className: styles.eurekaForm, children: bookingLink ? (_jsx(BookingManagePage, { token: decodeURIComponent(bookingLink[1]), idOrganization: idOrganization })) : (_jsx(App, { isWidget: false, idOrganization: idOrganization, apiKey: window.location.pathname.replace('/', ''), valuesData: dataValues, scrollToTop: scrollToTop })) }));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arquimedes.co/eureka-forms",
|
|
3
3
|
"repository": "git://github.com/Arquimede5/Eureka-Forms.git",
|
|
4
|
-
"version": "3.0.
|
|
4
|
+
"version": "3.0.72",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"watch": "node node_modules/@typescript/native/bin/tsc --noEmit --watch --project tsconfig.app.json",
|
|
7
7
|
"start": "vite",
|