@arquimedes.co/eureka-forms 3.0.71 → 3.0.72-test

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 (45) hide show
  1. package/dist/@Types/BookingFormStep.d.ts +70 -18
  2. package/dist/@Types/BookingFormStep.js +21 -0
  3. package/dist/App/App.submitError.test.d.ts +1 -0
  4. package/dist/App/App.submitError.test.js +53 -0
  5. package/dist/BookingManage/AppointmentText.d.ts +5 -0
  6. package/dist/BookingManage/AppointmentText.js +20 -0
  7. package/dist/BookingManage/BookingManagePage.d.ts +17 -0
  8. package/dist/BookingManage/BookingManagePage.js +177 -0
  9. package/dist/BookingManage/BookingManagePage.module.css +115 -0
  10. package/dist/BookingManage/BookingManagePage.test.d.ts +1 -0
  11. package/dist/BookingManage/BookingManagePage.test.js +149 -0
  12. package/dist/Form/ConfirmationDialog/ConfirmationDialog.d.ts +14 -5
  13. package/dist/Form/ConfirmationDialog/ConfirmationDialog.js +8 -1
  14. package/dist/Form/ConfirmationDialog/ConfirmationDialog.module.css +12 -0
  15. package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.d.ts +1 -0
  16. package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.js +21 -0
  17. package/dist/Form/Form.d.ts +3 -0
  18. package/dist/Form/Form.js +9 -2
  19. package/dist/Form/FormTypes/ColumnForm/ColumnForm.d.ts +1 -1
  20. package/dist/Form/FormTypes/ColumnForm/ColumnForm.js +3 -2
  21. package/dist/Form/FormTypes/StepperForm/StepperForm.d.ts +1 -1
  22. package/dist/Form/FormTypes/StepperForm/StepperForm.js +3 -2
  23. package/dist/Form/SubmitError/SubmitError.d.ts +11 -0
  24. package/dist/Form/SubmitError/SubmitError.js +9 -0
  25. package/dist/Form/SubmitError/SubmitError.module.css +25 -0
  26. package/dist/Form/SubmitError/SubmitError.test.d.ts +1 -0
  27. package/dist/Form/SubmitError/SubmitError.test.js +34 -0
  28. package/dist/Form/SubmitError/SubmitErrorReason.d.ts +13 -0
  29. package/dist/Form/SubmitError/SubmitErrorReason.js +20 -0
  30. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.d.ts +8 -1
  31. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +197 -41
  32. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.d.ts +1 -0
  33. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +275 -0
  34. package/dist/Services/BookingService.d.ts +159 -12
  35. package/dist/Services/BookingService.js +210 -12
  36. package/dist/Services/BookingService.test.js +130 -9
  37. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +63 -12
  38. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +141 -36
  39. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +41 -3
  40. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +11 -3
  41. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +69 -9
  42. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +13 -15
  43. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +331 -62
  44. package/dist/main.js +4 -1
  45. 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
- // El número de hoy puede aparecer también como día de un mes adyacente (deshabilitado).
22
- const candidates = screen.getAllByRole('button', { name: format(new Date(), 'd') });
23
- const enabled = candidates.find((b) => !b.disabled);
24
- if (!enabled)
25
- throw new Error('No selectable day found for today');
26
- return enabled;
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 → confirm', async () => {
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
- render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: onChange, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.OPTIONAL, persons: PERSONS, collectContact: true, fetchSlots: fetchSlots }) }));
34
- // La tarjeta de entrada abre el wizard en un modal
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
- const taken = await screen.findByRole('button', { name: '10:00' });
52
- expect(taken).toBeDisabled();
53
- await user.click(screen.getByRole('button', { name: '09:00' }));
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
- // Paso 4: confirmar — datos de contacto en la barra y CTA
56
- expect(await screen.findByRole('heading', { name: 'Revisa y confirma tu cita' })).toBeInTheDocument();
57
- await user.type(screen.getByLabelText('Nombre'), 'Juan Pérez');
58
- await user.type(screen.getByLabelText('Correo'), 'juan@correo.com');
59
- await user.click(screen.getByRole('button', { name: 'Confirmar cita' }));
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
- render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: () => { }, label: "Agendar cita", durationMinutes: 30, personSelectionMode: PersonSelectionMode.NONE, fetchSlots: () => Promise.resolve(SLOTS) }) }));
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
- render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: () => { }, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.REQUIRED, persons: PERSONS, fetchSlots: () => Promise.resolve(SLOTS) }) }));
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({ appointment: 'appt-1', hold: 'secret-1', lockedUntil: '2026-09-22T14:15:00.000Z' }));
100
- render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: onChange, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.NONE, fetchDays: fetchDays, fetchSlots: () => Promise.resolve(SLOTS), holdSlot: holdSlot }) }));
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 screen.findByRole('button', { name: '09:00' }));
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: 'Confirmar cita' }));
115
- // The hold is what the value carries on top of the selection: what submit needs to confirm it.
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
- render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: onChange, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.NONE, fetchSlots: fetchSlots, holdSlot: holdSlot }) }));
132
- await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
133
- await user.click(clickableToday());
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('does not confirm without a name when the step requires one', async () => {
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 onChange = vi.fn();
147
- render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: onChange, label: "Agendar cita", durationMinutes: 30, personSelectionMode: PersonSelectionMode.NONE, collectContact: true, requireContact: true, fetchSlots: () => Promise.resolve(SLOTS) }) }));
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
- await user.click(await screen.findByRole('button', { name: '09:00' }));
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
- const confirm = await screen.findByRole('button', { name: 'Confirmar cita' });
154
- expect(confirm).toBeDisabled();
155
- await user.type(screen.getByLabelText('Nombre'), 'Juan');
156
- expect(confirm).toBeEnabled();
157
- });
158
- it('renders the compact summary when a value exists', () => {
159
- render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: {
160
- type: 'BOOKING_VALUE',
161
- slotId: 's-3',
162
- date: '2026-07-02',
163
- startTime: '11:00',
164
- endTime: '11:50',
165
- personName: 'María González',
166
- status: 'CONFIRMED',
167
- }, onChange: () => { }, label: "Cita confirmada", durationMinutes: 50 }) }));
168
- expect(screen.getByText(/María González .* 11:00–11:50/)).toBeInTheDocument();
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
- ReactDOM.createRoot(document.getElementById('eureka-root')).render(_jsx("div", { className: styles.eurekaForm, children: _jsx(App, { isWidget: false, idOrganization: idOrganization, apiKey: window.location.pathname.replace('/', ''), valuesData: dataValues, scrollToTop: scrollToTop }) }));
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.71",
4
+ "version":"3.0.72-test",
5
5
  "scripts": {
6
6
  "watch": "node node_modules/@typescript/native/bin/tsc --noEmit --watch --project tsconfig.app.json",
7
7
  "start": "vite",