@arquimedes.co/eureka-forms 3.0.71-test → 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.
Files changed (167) hide show
  1. package/README +33 -1
  2. package/dist/@Types/AvailabilityFormStep.d.ts +48 -0
  3. package/dist/@Types/AvailabilityFormStep.js +24 -0
  4. package/dist/@Types/BankAccountFormStep.d.ts +67 -0
  5. package/dist/@Types/BankAccountFormStep.js +14 -0
  6. package/dist/@Types/BookingFormStep.d.ts +249 -0
  7. package/dist/@Types/BookingFormStep.js +65 -0
  8. package/dist/@Types/CalendarFormStep.d.ts +209 -0
  9. package/dist/@Types/CalendarFormStep.js +55 -0
  10. package/dist/@Types/CommunicationChannelFormStep.d.ts +51 -0
  11. package/dist/@Types/CommunicationChannelFormStep.js +15 -0
  12. package/dist/@Types/ConsentFormStep.d.ts +36 -0
  13. package/dist/@Types/ConsentFormStep.js +8 -0
  14. package/dist/@Types/EntityFormFormStep.d.ts +66 -0
  15. package/dist/@Types/EntityFormFormStep.js +20 -0
  16. package/dist/@Types/EventFormStep.d.ts +52 -0
  17. package/dist/@Types/EventFormStep.js +13 -0
  18. package/dist/@Types/ExternalReferenceFormStep.d.ts +74 -0
  19. package/dist/@Types/ExternalReferenceFormStep.js +20 -0
  20. package/dist/@Types/FormStep.d.ts +7 -2
  21. package/dist/@Types/FormStep.js +2 -1
  22. package/dist/@Types/LocationFormStep.d.ts +159 -0
  23. package/dist/@Types/LocationFormStep.js +46 -0
  24. package/dist/@Types/NumericFormSteps.d.ts +108 -0
  25. package/dist/@Types/NumericFormSteps.js +20 -0
  26. package/dist/@Types/PredefinedSelectorFormStep.d.ts +51 -0
  27. package/dist/@Types/PredefinedSelectorFormStep.js +39 -0
  28. package/dist/@Types/StepRef.d.ts +135 -0
  29. package/dist/@Types/StepRef.js +49 -0
  30. package/dist/App/App.stories.d.ts +10 -0
  31. package/dist/App/App.stories.js +189 -0
  32. package/dist/App/App.submitError.test.d.ts +1 -0
  33. package/dist/App/App.submitError.test.js +53 -0
  34. package/dist/App/AppHooks.js +8 -0
  35. package/dist/App/AppHooks.test.js +8 -0
  36. package/dist/BookingManage/AppointmentText.d.ts +5 -0
  37. package/dist/BookingManage/AppointmentText.js +20 -0
  38. package/dist/BookingManage/BookingManagePage.d.ts +17 -0
  39. package/dist/BookingManage/BookingManagePage.js +177 -0
  40. package/dist/BookingManage/BookingManagePage.module.css +115 -0
  41. package/dist/BookingManage/BookingManagePage.test.d.ts +1 -0
  42. package/dist/BookingManage/BookingManagePage.test.js +149 -0
  43. package/dist/Form/ConfirmationDialog/ConfirmationDialog.d.ts +14 -5
  44. package/dist/Form/ConfirmationDialog/ConfirmationDialog.js +8 -1
  45. package/dist/Form/ConfirmationDialog/ConfirmationDialog.module.css +12 -0
  46. package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.d.ts +1 -0
  47. package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.js +21 -0
  48. package/dist/Form/Form.d.ts +3 -0
  49. package/dist/Form/Form.js +16 -8
  50. package/dist/Form/FormFunctions.d.ts +17 -0
  51. package/dist/Form/FormFunctions.js +22 -0
  52. package/dist/Form/FormFunctions.test.js +28 -1
  53. package/dist/Form/FormTypes/ColumnForm/ColumnForm.d.ts +1 -1
  54. package/dist/Form/FormTypes/ColumnForm/ColumnForm.js +3 -2
  55. package/dist/Form/FormTypes/StepperForm/StepperForm.d.ts +1 -1
  56. package/dist/Form/FormTypes/StepperForm/StepperForm.js +3 -2
  57. package/dist/Form/SubmitError/SubmitError.d.ts +11 -0
  58. package/dist/Form/SubmitError/SubmitError.js +9 -0
  59. package/dist/Form/SubmitError/SubmitError.module.css +25 -0
  60. package/dist/Form/SubmitError/SubmitError.test.d.ts +1 -0
  61. package/dist/Form/SubmitError/SubmitError.test.js +34 -0
  62. package/dist/Form/SubmitError/SubmitErrorReason.d.ts +13 -0
  63. package/dist/Form/SubmitError/SubmitErrorReason.js +20 -0
  64. package/dist/FormSteps/AvailabilityStep/AvailabilityStep.d.ts +7 -0
  65. package/dist/FormSteps/AvailabilityStep/AvailabilityStep.js +14 -0
  66. package/dist/FormSteps/AvailabilityStep/MaterialAvailabilityStep/MaterialAvailabilityStep.d.ts +8 -0
  67. package/dist/FormSteps/AvailabilityStep/MaterialAvailabilityStep/MaterialAvailabilityStep.js +32 -0
  68. package/dist/FormSteps/BookingStep/BookingStep.d.ts +17 -0
  69. package/dist/FormSteps/BookingStep/BookingStep.js +14 -0
  70. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.d.ts +21 -0
  71. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +289 -0
  72. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.d.ts +1 -0
  73. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +275 -0
  74. package/dist/FormSteps/CheckBoxStep/CheckBoxStep.mobileLabel.test.js +20 -12
  75. package/dist/FormSteps/CheckBoxStep/MaterialCheckBoxStep/MaterialCheckBoxStep.js +6 -13
  76. package/dist/FormSteps/CheckBoxStep/MaterialCheckBoxStep/MaterialCheckBoxStep.module.css +2 -13
  77. package/dist/FormSteps/ClassifierSelectorStep/ClassifierSelectorStep.lateClassifiers.test.d.ts +1 -0
  78. package/dist/FormSteps/ClassifierSelectorStep/ClassifierSelectorStep.lateClassifiers.test.js +70 -0
  79. package/dist/FormSteps/ClassifierSelectorStep/MaterialClassifierSelectorStep/MaterialClassifierSelectorStep.js +3 -1
  80. package/dist/FormSteps/EntityValueStep/MaterialEntityValuePickerStep/MaterialEntityValuePickerStep.js +26 -26
  81. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.d.ts +12 -0
  82. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.js +35 -0
  83. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.test.d.ts +1 -0
  84. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.test.js +31 -0
  85. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.d.ts +27 -0
  86. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.js +64 -0
  87. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.d.ts +1 -0
  88. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.js +95 -0
  89. package/dist/FormSteps/LocationStep/LocationStep.d.ts +10 -0
  90. package/dist/FormSteps/LocationStep/LocationStep.js +14 -0
  91. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.d.ts +3 -0
  92. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.js +54 -0
  93. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.module.css +18 -0
  94. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.d.ts +3 -0
  95. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.js +170 -0
  96. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.module.css +96 -0
  97. package/dist/FormSteps/NumberStep/NumberStep.d.ts +10 -0
  98. package/dist/FormSteps/NumberStep/NumberStep.js +14 -0
  99. package/dist/FormSteps/Step.js +16 -0
  100. package/dist/FormSteps/StepFunctions.js +14 -0
  101. package/dist/FormSteps/StepFunctions.test.js +54 -0
  102. package/dist/FormSteps/StepHooks.d.ts +21 -0
  103. package/dist/FormSteps/StepHooks.js +55 -8
  104. package/dist/FormSteps/StepHooks.test.d.ts +1 -0
  105. package/dist/FormSteps/StepHooks.test.js +92 -0
  106. package/dist/FormSteps/isStepHiddenByCondition.test.d.ts +1 -0
  107. package/dist/FormSteps/isStepHiddenByCondition.test.js +42 -0
  108. package/dist/Icons/AddIcon.d.ts +3 -0
  109. package/dist/Icons/AddIcon.js +7 -0
  110. package/dist/Icons/EditIcon.d.ts +3 -0
  111. package/dist/Icons/EditIcon.js +7 -0
  112. package/dist/Icons/RemoveIcon.d.ts +3 -0
  113. package/dist/Icons/RemoveIcon.js +7 -0
  114. package/dist/Icons/ViewDayIcon.d.ts +3 -0
  115. package/dist/Icons/ViewDayIcon.js +7 -0
  116. package/dist/Icons/ViewWeekIcon.d.ts +3 -0
  117. package/dist/Icons/ViewWeekIcon.js +7 -0
  118. package/dist/Services/BookingService.d.ts +226 -0
  119. package/dist/Services/BookingService.js +264 -0
  120. package/dist/Services/BookingService.test.d.ts +1 -0
  121. package/dist/Services/BookingService.test.js +178 -0
  122. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.d.ts +19 -0
  123. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.js +80 -0
  124. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.module.css +88 -0
  125. package/dist/Shared/ErkAddressAutocomplete/googlePlaces.d.ts +29 -0
  126. package/dist/Shared/ErkAddressAutocomplete/googlePlaces.js +139 -0
  127. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.d.ts +25 -0
  128. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.js +112 -0
  129. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.module.css +88 -0
  130. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.d.ts +1 -0
  131. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.js +73 -0
  132. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +105 -0
  133. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +399 -0
  134. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +646 -0
  135. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +36 -0
  136. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +177 -0
  137. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.d.ts +1 -0
  138. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +47 -0
  139. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.d.ts +1 -0
  140. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +440 -0
  141. package/dist/Shared/ErkDatePicker/ErkDatePicker.js +1 -3
  142. package/dist/Shared/ErkDateRangePicker/ErkDateRangePicker.d.ts +2 -3
  143. package/dist/Shared/ErkDateRangePicker/ErkDateRangePicker.js +1 -2
  144. package/dist/Shared/ErkIconButton/ErkIconButton.d.ts +17 -0
  145. package/dist/Shared/ErkIconButton/ErkIconButton.js +11 -0
  146. package/dist/Shared/ErkIconButton/ErkIconButton.module.css +41 -0
  147. package/dist/Shared/ErkLocationField/ErkLocationField.d.ts +24 -0
  148. package/dist/Shared/ErkLocationField/ErkLocationField.js +128 -0
  149. package/dist/Shared/ErkLocationField/ErkLocationField.module.css +164 -0
  150. package/dist/Shared/ErkLocationField/ErkLocationField.test.d.ts +1 -0
  151. package/dist/Shared/ErkLocationField/ErkLocationField.test.js +82 -0
  152. package/dist/Shared/ErkLocationField/addressFields.d.ts +12 -0
  153. package/dist/Shared/ErkLocationField/addressFields.js +34 -0
  154. package/dist/Shared/ErkLocationField/locationRestrictions.d.ts +6 -0
  155. package/dist/Shared/ErkLocationField/locationRestrictions.js +51 -0
  156. package/dist/Shared/ErkLocationInput/ErkLocationInput.d.ts +15 -0
  157. package/dist/Shared/ErkLocationInput/ErkLocationInput.js +502 -0
  158. package/dist/Shared/ErkLocationInput/ErkLocationInput.module.css +128 -0
  159. package/dist/Shared/ErkNumberField/ErkNumberField.d.ts +42 -0
  160. package/dist/Shared/ErkNumberField/ErkNumberField.js +169 -0
  161. package/dist/Utils/AxiosWidget.js +9 -1
  162. package/dist/constants/FormStepTypes.d.ts +5 -1
  163. package/dist/constants/FormStepTypes.js +4 -0
  164. package/dist/index.lib.d.ts +4 -2
  165. package/dist/index.lib.js +3 -1
  166. package/dist/main.js +4 -1
  167. package/package.json +2 -1
@@ -0,0 +1,399 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Dialog } from '@mui/material';
4
+ import { addDays, addMonths, format, isBefore, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek, } from 'date-fns';
5
+ import { es } from 'date-fns/locale';
6
+ import { BookingMode, PersonSelectionMode, } from '../../@Types/BookingFormStep';
7
+ import ErkButton from '../ErkButton/ErkButton';
8
+ import ErkTextField from '../ErkTextField/ErkTextField';
9
+ import { browserTimeZone, clockIn, isValidEmail, nonEmpty, zoneLabel } from '../../Services/BookingService';
10
+ import styles from './ErkBookingPicker.module.css';
11
+ const WEEK_STARTS_ON = 1;
12
+ const DOW = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
13
+ const MODE_LABEL = {
14
+ [BookingMode.IN_PERSON]: 'Presencial',
15
+ [BookingMode.VIRTUAL]: 'Virtual',
16
+ [BookingMode.HYBRID]: 'Híbrida',
17
+ };
18
+ const STEP_LABEL = {
19
+ type: 'Tipo',
20
+ person: 'Persona',
21
+ day: 'Día',
22
+ hour: 'Hora',
23
+ confirm: 'Reservar',
24
+ };
25
+ const STEP_HINT = {
26
+ type: 'Elige qué necesitas',
27
+ person: 'Elige con quién agendar',
28
+ day: 'Elige un día',
29
+ hour: 'Elige una hora libre',
30
+ confirm: 'Reserva tu horario',
31
+ };
32
+ const GENERIC_SLOTS_ERROR = 'No pudimos cargar los horarios de este día.';
33
+ const GENERIC_DAYS_ERROR = 'No pudimos cargar los días disponibles.';
34
+ const NO_EMAIL_HINT = 'Sin correo no podremos enviarte la confirmación.';
35
+ /** Duración del fade al reestructurarse el layout (entrar/salir del paso final). */
36
+ const XFADE_MS = 160;
37
+ /** El CTA de confirmar usa el acento del form (secondaryContrastColor vía --booking-accent). */
38
+ const ACCENT_SX = {
39
+ backgroundColor: 'var(--booking-accent, #ee6c4d)',
40
+ '&:hover': { backgroundColor: 'var(--booking-accent, #ee6c4d)', filter: 'brightness(0.95)' },
41
+ '&:disabled': { backgroundColor: 'var(--booking-accent, #ee6c4d)', color: '#fff' },
42
+ };
43
+ const BOOKING_COPY = {
44
+ open: 'Agendar cita',
45
+ confirm: 'Reservar',
46
+ confirming: 'Reservando…',
47
+ title: 'Revisa y reserva tu horario',
48
+ note: 'La cita se confirma cuando envíes el formulario.',
49
+ };
50
+ function fmtDate(date) {
51
+ return format(date, 'yyyy-MM-dd');
52
+ }
53
+ function longDate(date) {
54
+ return format(date, 'EEEE d MMM', { locale: es });
55
+ }
56
+ function fullDate(date) {
57
+ return format(date, "EEEE d 'de' MMMM 'de' yyyy", { locale: es });
58
+ }
59
+ function errorText(error, fallback) {
60
+ return error instanceof Error && error.message ? error.message : fallback;
61
+ }
62
+ /** Mañana hasta el mediodía, tarde desde ahí, en el reloj en que se muestran las horas. */
63
+ function groupSlots(slots) {
64
+ const morning = slots.filter((s) => s.startTime < '12:00');
65
+ const afternoon = slots.filter((s) => s.startTime >= '12:00');
66
+ return [
67
+ { label: 'Mañana', slots: morning },
68
+ { label: 'Tarde', slots: afternoon },
69
+ ].filter((group) => group.slots.length > 0);
70
+ }
71
+ function initials(name) {
72
+ return name
73
+ .split(' ')
74
+ .map((p) => p[0])
75
+ .filter(Boolean)
76
+ .slice(0, 2)
77
+ .join('')
78
+ .toUpperCase();
79
+ }
80
+ function capitalize(text) {
81
+ return text.charAt(0).toUpperCase() + text.slice(1);
82
+ }
83
+ /**
84
+ * Selector de cita: una tarjeta de entrada ("Agendar cita") abre el wizard en un modal —
85
+ * una decisión por pantalla (persona → día → hora → confirmar), con panel izquierdo de
86
+ * resumen que se va llenando, breadcrumb navegable arriba y barra inferior Atrás / Siguiente.
87
+ * En el paso final el panel derecho desaparece y el resumen se expande en una columna
88
+ * centrada (cross-fade corto). "Reservar" retira el horario de la oferta por unos minutos, cierra el
89
+ * modal y deja el resumen compacto; la cita se confirma cuando se envía el formulario.
90
+ */
91
+ function ErkBookingPicker({ value, onChange, label, description, durationMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, eventTemplates, timeZone, knownContact, askEmail = false, lapsed, fetchSlots, fetchDays, holdSlot, disabled = false, withoutContact = false, copy: copyOverrides, readOnly = false, error, loadError, onRetryLoad, }) {
92
+ const copy = { ...BOOKING_COPY, ...copyOverrides };
93
+ const needsPerson = !!personSelectionMode && personSelectionMode !== PersonSelectionMode.NONE;
94
+ const personsOptional = personSelectionMode === PersonSelectionMode.OPTIONAL;
95
+ const needsType = (eventTemplates?.length ?? 0) > 1;
96
+ const today = startOfDay(new Date());
97
+ const wizardKeys = useMemo(() => [
98
+ ...(needsType ? ['type'] : []),
99
+ ...(needsPerson ? ['person'] : []),
100
+ ...['day', 'hour', 'confirm'],
101
+ ], [needsType, needsPerson]);
102
+ const finalIdx = wizardKeys.length - 1;
103
+ const [open, setOpen] = useState(false);
104
+ const [wizStep, setWizStep] = useState(0);
105
+ const [xfade, setXfade] = useState(false);
106
+ const [eventTemplate, setEventTemplate] = useState(undefined);
107
+ const [person, setPerson] = useState(undefined);
108
+ const [personTouched, setPersonTouched] = useState(false);
109
+ const [date, setDate] = useState(undefined);
110
+ const [viewMonth, setViewMonth] = useState(() => startOfMonth(today));
111
+ const [slot, setSlot] = useState(undefined);
112
+ const [slots, setSlots] = useState([]);
113
+ const [loading, setLoading] = useState(false);
114
+ const [slotsError, setSlotsError] = useState(undefined);
115
+ const [contactName, setContactName] = useState('');
116
+ const [contactEmail, setContactEmail] = useState('');
117
+ const [emailTouched, setEmailTouched] = useState(false);
118
+ /** `undefined` mientras se consultan; sin `fetchDays` nunca se consultan y todo día futuro vale. */
119
+ const [days, setDays] = useState(undefined);
120
+ const [daysError, setDaysError] = useState(undefined);
121
+ const [daysAttempt, setDaysAttempt] = useState(0);
122
+ const [holding, setHolding] = useState(false);
123
+ const [holdError, setHoldError] = useState(undefined);
124
+ /** Cada pedido de horas lleva un número; solo el último puede escribir el estado (A5.a). */
125
+ const slotsRequest = useRef(0);
126
+ // A hold lost after it was made sends the requester back to the hours, with the reason on screen.
127
+ const [seenLapsed, setSeenLapsed] = useState(lapsed);
128
+ if (lapsed !== seenLapsed) {
129
+ setSeenLapsed(lapsed);
130
+ if (lapsed) {
131
+ setSlot(undefined);
132
+ setHoldError(lapsed);
133
+ setWizStep(Math.max(0, wizardKeys.indexOf(date ? 'hour' : 'day')));
134
+ }
135
+ }
136
+ const knownName = nonEmpty(knownContact?.name);
137
+ const knownEmail = nonEmpty(knownContact?.email);
138
+ const askName = !withoutContact && !knownName;
139
+ const nameValue = knownName ?? contactName.trim();
140
+ const emailValue = askEmail ? contactEmail.trim() : knownEmail;
141
+ const emailInvalid = !withoutContact && askEmail && !isValidEmail(contactEmail);
142
+ const askContactEmail = !withoutContact && askEmail;
143
+ const localZone = useMemo(() => browserTimeZone(), []);
144
+ const showLocal = !!timeZone && !!localZone && localZone !== timeZone;
145
+ const reduceMotion = useMemo(() => typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches, []);
146
+ const monthCells = useMemo(() => {
147
+ const first = startOfWeek(startOfMonth(viewMonth), { weekStartsOn: WEEK_STARTS_ON });
148
+ return Array.from({ length: 42 }, (_, i) => addDays(first, i));
149
+ }, [viewMonth]);
150
+ const loadSlots = useCallback(async (day, personId, templateId) => {
151
+ if (!fetchSlots)
152
+ return;
153
+ const ticket = ++slotsRequest.current;
154
+ setLoading(true);
155
+ setSlots([]);
156
+ setSlotsError(undefined);
157
+ try {
158
+ const loaded = await fetchSlots({ personId, date: fmtDate(day), eventTemplate: templateId });
159
+ if (ticket === slotsRequest.current)
160
+ setSlots(loaded);
161
+ }
162
+ catch (e) {
163
+ if (ticket === slotsRequest.current)
164
+ setSlotsError(errorText(e, GENERIC_SLOTS_ERROR));
165
+ }
166
+ finally {
167
+ if (ticket === slotsRequest.current)
168
+ setLoading(false);
169
+ }
170
+ }, [fetchSlots]);
171
+ useEffect(() => {
172
+ if (value || !date)
173
+ return;
174
+ // The picker synchronises with the slots API here, and `loadSlots` raises its own
175
+ // loading flag as it goes out — the very shape the rule asks effects to be used for.
176
+ // oxlint-disable-next-line react/set-state-in-effect
177
+ void loadSlots(date, person?.id, eventTemplate?.id);
178
+ }, [value, date, person, eventTemplate, loadSlots]);
179
+ useEffect(() => {
180
+ if (!fetchDays || !open || value)
181
+ return;
182
+ // The days depend on the type; until one is picked there is nothing to ask for.
183
+ if (needsType && !eventTemplate)
184
+ return;
185
+ let cancelled = false;
186
+ // Same shape as the slots effect: the days come from the host's API and the picker only mirrors them.
187
+ // oxlint-disable-next-line react/set-state-in-effect
188
+ setDays(undefined);
189
+ setDaysError(undefined);
190
+ fetchDays({ personId: person?.id, eventTemplate: eventTemplate?.id })
191
+ .then((available) => {
192
+ if (cancelled)
193
+ return;
194
+ setDays(available);
195
+ // The month on screen may hold nothing; the first day on offer is where the requester needs to be.
196
+ const first = available[0];
197
+ if (!first)
198
+ return;
199
+ setViewMonth((current) => available.some((d) => isSameMonth(new Date(d + 'T00:00:00'), current))
200
+ ? current
201
+ : startOfMonth(new Date(first + 'T00:00:00')));
202
+ })
203
+ .catch((e) => {
204
+ if (!cancelled)
205
+ setDaysError(errorText(e, GENERIC_DAYS_ERROR));
206
+ });
207
+ return () => {
208
+ cancelled = true;
209
+ };
210
+ }, [fetchDays, open, value, person, eventTemplate, needsType, daysAttempt]);
211
+ // Al entrar/salir del paso final el layout se reestructura; el reacomodo
212
+ // ocurre mientras la tarjeta está invisible (fade corto en dos fases).
213
+ const goToStep = (target) => {
214
+ const crossesFinal = (wizStep === finalIdx) !== (target === finalIdx);
215
+ if (!crossesFinal || reduceMotion) {
216
+ setWizStep(target);
217
+ return;
218
+ }
219
+ setXfade(true);
220
+ window.setTimeout(() => {
221
+ setWizStep(target);
222
+ requestAnimationFrame(() => requestAnimationFrame(() => setXfade(false)));
223
+ }, XFADE_MS);
224
+ };
225
+ const currentKey = wizardKeys[wizStep];
226
+ const canAdvance = (() => {
227
+ switch (currentKey) {
228
+ case 'type':
229
+ return !!eventTemplate;
230
+ case 'person':
231
+ return personsOptional ? personTouched : !!person;
232
+ case 'day':
233
+ return !!date;
234
+ case 'hour':
235
+ return !!slot;
236
+ case 'confirm':
237
+ return withoutContact || (nameValue.length > 0 && !emailInvalid);
238
+ default:
239
+ return true;
240
+ }
241
+ })();
242
+ const confirm = async () => {
243
+ if (!slot || !date)
244
+ return;
245
+ const selection = {
246
+ type: 'BOOKING_VALUE',
247
+ slotId: slot.id,
248
+ date: fmtDate(date),
249
+ startTime: slot.startTime,
250
+ endTime: slot.endTime,
251
+ personId: person?.id,
252
+ personName: person?.name,
253
+ contactName: nonEmpty(nameValue),
254
+ contactEmail: nonEmpty(emailValue),
255
+ status: 'PENDING',
256
+ };
257
+ if (!holdSlot) {
258
+ setOpen(false);
259
+ onChange(selection);
260
+ return;
261
+ }
262
+ setHolding(true);
263
+ setHoldError(undefined);
264
+ try {
265
+ const held = await holdSlot({
266
+ slot,
267
+ personId: person?.id,
268
+ eventTemplate: eventTemplate?.id,
269
+ contact: { name: nameValue, email: nonEmpty(emailValue) },
270
+ });
271
+ setOpen(false);
272
+ onChange({ ...selection, ...held });
273
+ }
274
+ catch (e) {
275
+ // Somebody else took the slot while the form was being finished: back to the hour, with fresh slots.
276
+ setHoldError(e instanceof Error && e.message ? e.message : 'No pudimos reservar ese horario.');
277
+ setSlot(undefined);
278
+ goToStep(wizardKeys.indexOf('hour'));
279
+ void loadSlots(date, person?.id, eventTemplate?.id);
280
+ }
281
+ finally {
282
+ setHolding(false);
283
+ }
284
+ };
285
+ const reset = () => {
286
+ onChange(null);
287
+ setWizStep(0);
288
+ setEventTemplate(undefined);
289
+ setPerson(undefined);
290
+ setPersonTouched(false);
291
+ setDate(undefined);
292
+ setSlot(undefined);
293
+ setDays(undefined);
294
+ setHoldError(undefined);
295
+ setContactName('');
296
+ setContactEmail('');
297
+ setEmailTouched(false);
298
+ };
299
+ // Another type means another duration: the day and hour picked for the previous one no longer hold.
300
+ const pickType = (template) => {
301
+ if (template.id === eventTemplate?.id)
302
+ return;
303
+ setEventTemplate(template);
304
+ setDate(undefined);
305
+ setSlot(undefined);
306
+ setDays(undefined);
307
+ setHoldError(undefined);
308
+ };
309
+ const pickPerson = (p) => {
310
+ setPerson(p);
311
+ setPersonTouched(true);
312
+ setSlot(undefined);
313
+ };
314
+ // Read-only / already booked → compact summary with the same card aesthetic
315
+ if (readOnly || value) {
316
+ const pending = !!value && value.status === 'PENDING' && !readOnly;
317
+ return (_jsx("div", { className: styles.container, children: _jsxs("div", { className: styles.entry, children: [_jsx("div", { className: styles.bookedIcon, children: "\u2713" }), _jsxs("div", { className: styles.entryInfo, children: [_jsx("span", { className: styles.entryLabel, children: label }), _jsx("span", { className: styles.bookedMain, children: value
318
+ ? [
319
+ capitalize(longDate(new Date(value.date + 'T00:00:00'))),
320
+ `${value.startTime}–${value.endTime}`,
321
+ value.personName ? `con ${value.personName}` : undefined,
322
+ ]
323
+ .filter(Boolean)
324
+ .join(' · ')
325
+ : 'Sin cita' }), value?.eventTemplate && (_jsxs("span", { className: styles.entrySub, children: [value.eventTemplate.name, " \u00B7 ", value.eventTemplate.durationMinutes, " min"] })), pending && (_jsxs("span", { className: styles.entrySub, children: [value.lockedUntil && timeZone
326
+ ? `Reservado hasta las ${clockIn(value.lockedUntil, timeZone)}. `
327
+ : 'Horario reservado. ', "Env\u00EDa el formulario para confirmar la cita."] }))] }), value && !readOnly && _jsx(ErkButton, { text: "Cambiar", variant: "outlined", onClick: reset })] }) }));
328
+ }
329
+ const isFinal = wizStep === finalIdx;
330
+ const duration = eventTemplate?.durationMinutes ?? durationMinutes;
331
+ const mode = eventTemplate?.bookingMode ?? bookingMode;
332
+ const personLabel = person?.name ?? (personTouched && personsOptional ? 'Cualquiera' : undefined);
333
+ const dayText = date ? capitalize(longDate(date)) : undefined;
334
+ const localStart = showLocal && localZone && slot?.start ? clockIn(slot.start, localZone) : undefined;
335
+ const hourText = slot
336
+ ? `${slot.startTime} – ${slot.endTime}${localStart ? ` (${localStart} en tu hora)` : ''}`
337
+ : undefined;
338
+ const zoneText = timeZone ? `Horas en ${zoneLabel(timeZone, date ?? new Date())}` : undefined;
339
+ const firstAvailable = slots.find((s) => s.available);
340
+ const summaryRows = [
341
+ ...(needsType
342
+ ? [{ key: 'type', placeholder: 'Tipo de atención', text: eventTemplate?.name }]
343
+ : []),
344
+ ...(needsPerson ? [{ key: 'person', placeholder: 'Persona', text: personLabel }] : []),
345
+ { key: 'day', placeholder: 'Día', text: dayText },
346
+ { key: 'hour', placeholder: 'Hora', text: hourText },
347
+ ];
348
+ const navParts = [];
349
+ if (eventTemplate)
350
+ navParts.push(_jsx("b", { children: eventTemplate.name }, "t"));
351
+ if (personLabel)
352
+ navParts.push(_jsx("b", { children: personLabel }, "p"));
353
+ if (dayText)
354
+ navParts.push(_jsx("span", { children: dayText }, "d"));
355
+ if (hourText)
356
+ navParts.push(_jsx("span", { className: styles.navHl, children: hourText }, "h"));
357
+ return (_jsxs("div", { className: styles.container, children: [_jsxs("div", { className: styles.entry, children: [_jsx("div", { className: styles.avatar, children: "\uD83D\uDCC5" }), _jsxs("div", { className: styles.entryInfo, children: [_jsx("span", { className: styles.entryLabel, children: label }), _jsx("span", { className: styles.entrySub, children: description ?? 'Agenda tu cita' })] }), _jsx(ErkButton, { text: copy.open, disabled: disabled, onClick: () => setOpen(true) })] }), loadError && (_jsxs("div", { className: styles.retry, role: "alert", children: [_jsx("span", { children: loadError }), onRetryLoad && _jsx(ErkButton, { text: "Reintentar", variant: "outlined", onClick: onRetryLoad })] })), lapsed && !open && (_jsx("div", { className: styles.err, role: "alert", children: lapsed })), _jsx(Dialog, { open: open, onClose: () => setOpen(false), maxWidth: "md", fullWidth: true, slotProps: { paper: { style: { borderRadius: 16, background: 'transparent', boxShadow: 'none' } } }, children: _jsxs("div", { className: `${styles.card}${isFinal ? ` ${styles.cardFinal}` : ''}${xfade ? ` ${styles.xfade}` : ''}`, children: [_jsxs("div", { className: isFinal ? `${styles.book} ${styles.bookFinal}` : styles.book, children: [_jsxs("aside", { className: styles.who, children: [_jsxs("div", { className: styles.brand, children: [_jsx("div", { className: styles.avatar, children: person ? initials(person.name) : '📅' }), _jsxs("div", { children: [_jsx("h2", { className: styles.brandName, children: person?.name ?? label }), _jsx("p", { className: styles.brandRole, children: person?.role ?? description ?? 'Agenda tu cita' })] })] }), _jsxs("div", { className: styles.meta, children: [_jsxs("div", { className: styles.row, children: [_jsx("span", { className: styles.ic, children: "\u25F7" }), duration, " min"] }), mode && (_jsxs("div", { className: styles.row, children: [_jsx("span", { className: styles.ic, children: "\u25F1" }), _jsx("span", { className: styles.pill, children: MODE_LABEL[mode] })] }))] }), _jsx("div", { className: styles.sum, "aria-live": "polite", children: summaryRows.map((r) => (_jsxs("div", { className: r.text ? `${styles.srow} ${styles.srowOn}` : styles.srow, children: [_jsx("span", { className: styles.dot, children: "\u2713" }), _jsx("span", { children: r.text ?? r.placeholder })] }, r.key))) }), isFinal && (askName || askContactEmail) && (_jsxs("div", { className: styles.cfields, children: [askName && (_jsx(ErkTextField, { label: "Nombre", value: contactName, onChange: setContactName, required: true })), askContactEmail && (_jsx(ErkTextField, { label: "Correo", type: "email", value: contactEmail, onChange: setContactEmail, onBlur: () => setEmailTouched(true), required: true, error: emailTouched && emailInvalid, helperText: emailTouched && contactEmail.trim() && emailInvalid
358
+ ? 'Escribe un correo válido.'
359
+ : NO_EMAIL_HINT }))] })), isFinal && withoutContact && _jsx("div", { className: styles.lock, children: copy.note }), isFinal && !withoutContact && (_jsxs("div", { className: styles.lock, children: ["\u23F3", ' ', temporaryLockMinutes ? (_jsxs(_Fragment, { children: ["Reservaremos el horario ", _jsxs("b", { children: [temporaryLockMinutes, " min"] }), ".", ' '] })) : ('Reservaremos el horario. '), copy.note] }))] }), _jsxs("div", { className: styles.right, children: [_jsx("nav", { className: styles.stepper, "aria-label": "Progreso", children: wizardKeys.map((key, i) => (_jsxs("span", { className: styles.stepGroup, children: [_jsxs("button", { type: "button", disabled: i >= wizStep, onClick: () => goToStep(i), className: i < wizStep
360
+ ? `${styles.stepItem} ${styles.stepDone}`
361
+ : i === wizStep
362
+ ? `${styles.stepItem} ${styles.stepOn}`
363
+ : styles.stepItem, children: [_jsx("span", { className: styles.stepNum, children: i < wizStep ? '✓' : i + 1 }), key === 'confirm' ? copy.confirm : STEP_LABEL[key]] }), i < wizardKeys.length - 1 && _jsx("span", { className: styles.stepSep })] }, key))) }), currentKey === 'type' && (_jsxs("section", { className: styles.pane, children: [_jsx("h3", { className: styles.q, children: "\u00BFQu\u00E9 necesitas?" }), _jsx("p", { className: styles.qsub, children: "Elige el tipo de atenci\u00F3n; de \u00E9l dependen la duraci\u00F3n y los horarios." }), _jsx("div", { className: styles.persons, children: (eventTemplates ?? []).map((t) => (_jsxs("button", { type: "button", "aria-pressed": eventTemplate?.id === t.id, className: eventTemplate?.id === t.id
364
+ ? `${styles.pcard} ${styles.pcardOn}`
365
+ : styles.pcard, onClick: () => pickType(t), children: [_jsxs("span", { className: styles.pinfo, children: [_jsx("span", { className: styles.pn, children: t.name }), t.description && (_jsx("span", { className: styles.pr, children: t.description }))] }), _jsxs("span", { className: styles.tdur, children: [t.durationMinutes, " min"] }), _jsx("span", { className: styles.chk, children: "\u2713" })] }, t.id))) })] }, "type")), currentKey === 'person' && (_jsxs("section", { className: styles.pane, children: [_jsx("h3", { className: styles.q, children: "\u00BFCon qui\u00E9n quieres agendar?" }), _jsx("p", { className: styles.qsub, children: personsOptional
366
+ ? 'Elige una persona o deja que el sistema asigne a quien esté disponible.'
367
+ : 'Elige la persona con la que quieres tu cita.' }), _jsxs("div", { className: styles.persons, children: [(persons ?? []).map((p) => (_jsxs("button", { type: "button", className: person?.id === p.id
368
+ ? `${styles.pcard} ${styles.pcardOn}`
369
+ : styles.pcard, onClick: () => pickPerson(p), children: [_jsx("span", { className: styles.pav, children: initials(p.name) }), _jsxs("span", { className: styles.pinfo, children: [_jsx("span", { className: styles.pn, children: p.name }), p.role && _jsx("span", { className: styles.pr, children: p.role })] }), _jsx("span", { className: styles.chk, children: "\u2713" })] }, p.id))), personsOptional && (_jsxs("button", { type: "button", className: personTouched && !person
370
+ ? `${styles.pcard} ${styles.pcardAny} ${styles.pcardOn}`
371
+ : `${styles.pcard} ${styles.pcardAny}`, onClick: () => pickPerson(undefined), children: [_jsx("span", { className: styles.pav, children: "\u2726" }), _jsxs("span", { className: styles.pinfo, children: [_jsx("span", { className: styles.pn, children: "Cualquiera" }), _jsx("span", { className: styles.pr, children: "El sistema asigna" })] }), _jsx("span", { className: styles.chk, children: "\u2713" })] }))] })] }, "person")), currentKey === 'day' && (_jsxs("section", { className: styles.pane, children: [_jsx("h3", { className: styles.q, children: "Elige un d\u00EDa" }), _jsx("p", { className: styles.qsub, children: fetchDays
372
+ ? 'Solo se pueden elegir los días con horarios disponibles.'
373
+ : 'Solo se muestran fechas a partir de hoy.' }), _jsxs("div", { className: styles.monthWrap, children: [_jsxs("div", { className: styles.monthHead, children: [_jsx("span", { className: styles.monthTitle, children: capitalize(format(viewMonth, 'MMMM yyyy', { locale: es })) }), _jsxs("span", { className: styles.monthNav, children: [_jsx("button", { type: "button", "aria-label": "Mes anterior", onClick: () => setViewMonth((m) => addMonths(m, -1)), children: "\u2039" }), _jsx("button", { type: "button", "aria-label": "Mes siguiente", onClick: () => setViewMonth((m) => addMonths(m, 1)), children: "\u203A" })] })] }), _jsxs("div", { className: styles.mgrid, children: [DOW.map((d, i) => (_jsx("span", { className: styles.mdow, children: d }, i))), monthCells.map((d) => {
374
+ const outside = !isSameMonth(d, viewMonth);
375
+ const past = isBefore(d, today);
376
+ const offered = !daysError && (!days || days.includes(fmtDate(d)));
377
+ const selectable = !outside && !past && offered;
378
+ const cls = date && isSameDay(d, date)
379
+ ? `${styles.mday} ${styles.mdaySel}`
380
+ : selectable
381
+ ? `${styles.mday} ${styles.mdayFree}`
382
+ : `${styles.mday} ${styles.mdayNone}`;
383
+ return (_jsx("button", { type: "button", disabled: !selectable, "aria-label": fullDate(d), className: cls, onClick: () => {
384
+ setDate(d);
385
+ setSlot(undefined);
386
+ }, children: format(d, 'd') }, d.toISOString()));
387
+ })] })] }), daysError ? (_jsxs("div", { className: styles.retry, role: "alert", children: [_jsx("span", { children: daysError }), _jsx(ErkButton, { text: "Reintentar", variant: "outlined", onClick: () => setDaysAttempt((n) => n + 1) })] })) : fetchDays && days === undefined ? (_jsx("div", { className: styles.empty, children: "Buscando d\u00EDas con horarios\u2026" })) : (days?.length === 0 && (_jsx("div", { className: styles.empty, children: "No hay d\u00EDas con horarios disponibles." })))] }, "day")), currentKey === 'hour' && (_jsxs("section", { className: styles.pane, children: [_jsx("h3", { className: styles.q, children: "Elige una hora" }), _jsxs("p", { className: styles.qsub, children: [dayText, " \u00B7 citas de ", duration, " min"] }), holdError && (_jsx("div", { className: styles.err, role: "alert", children: holdError })), zoneText && _jsx("p", { className: styles.tz, children: zoneText }), loading ? (_jsx("div", { className: styles.empty, children: "Buscando horarios\u2026" })) : slotsError ? (_jsxs("div", { className: styles.retry, role: "alert", children: [_jsx("span", { children: slotsError }), _jsx(ErkButton, { text: "Reintentar", variant: "outlined", onClick: () => date && void loadSlots(date, person?.id, eventTemplate?.id) })] })) : slots.length === 0 ? (_jsx("div", { className: styles.empty, children: "No hay horarios disponibles ese d\u00EDa." })) : (groupSlots(slots).map((group) => (_jsxs("div", { className: styles.slotGroup, children: [_jsx("h4", { className: styles.slotGroupTitle, children: group.label }), _jsx("div", { className: styles.slots, children: group.slots.map((s) => (_jsxs("button", { type: "button", disabled: !s.available, title: `${s.startTime} – ${s.endTime}`, className: slot?.id === s.id
388
+ ? `${styles.slot} ${styles.slotSel}`
389
+ : s.available
390
+ ? styles.slot
391
+ : `${styles.slot} ${styles.slotTaken}`, onClick: () => s.available && setSlot(s), children: [s.startTime, s === firstAvailable && (_jsx("span", { className: styles.firstTag, children: "Primera disponible" }))] }, s.id))) })] }, group.label)))), localStart && slot && (_jsxs("p", { className: styles.tz, children: [slot.startTime, " es ", localStart, " en tu hora."] }))] }, "hour")), currentKey === 'confirm' && (_jsxs("section", { className: `${styles.pane} ${styles.paneFinal}`, children: [_jsx("h3", { className: styles.q, children: copy.title }), _jsx("p", { className: styles.qsub, children: withoutContact
392
+ ? copy.note
393
+ : askName || askEmail
394
+ ? 'Completa tus datos y reserva el horario. La cita se confirma al enviar el formulario.'
395
+ : 'Reserva el horario. La cita se confirma al enviar el formulario.' })] }, "confirm"))] })] }), _jsxs("div", { className: styles.navbar, children: [_jsx("div", { className: styles.navSummary, children: isFinal || !canAdvance || navParts.length === 0
396
+ ? STEP_HINT[currentKey]
397
+ : navParts.reduce((acc, part, i) => i === 0 ? [part] : [...acc, _jsx("span", { children: " \u00B7 " }, `s${i}`), part], []) }), wizStep > 0 && (_jsx(ErkButton, { text: "Atr\u00E1s", variant: "outlined", onClick: () => goToStep(wizStep - 1) })), isFinal ? (_jsx(ErkButton, { text: holding ? copy.confirming : copy.confirm, sx: ACCENT_SX, disabled: !slot || holding || !canAdvance, onClick: () => void confirm() })) : (_jsx(ErkButton, { text: "Siguiente", disabled: !canAdvance, onClick: () => goToStep(wizStep + 1) }))] })] }) }), error && _jsx("div", { className: styles.err, children: error })] }));
398
+ }
399
+ export default ErkBookingPicker;