@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,23 +1,61 @@
1
- import { BookablePerson, BookingMode, BookingSlot, BookingValue, PersonSelectionMode } from '../../@Types/BookingFormStep';
1
+ import { BookablePerson, BookingEventTemplateOption, BookingMode, BookingSlot, BookingValue, PersonSelectionMode } from '../../@Types/BookingFormStep';
2
+ /**
3
+ * Los textos que cambian cuando el wizard no reserva una cita nueva sino que mueve una que ya existe
4
+ * (la página de "Elegir otra hora" del enlace del correo).
5
+ */
6
+ export interface BookingPickerCopy {
7
+ /** Botón de la tarjeta de entrada. */
8
+ open: string;
9
+ /** Paso final, su botón y su título. */
10
+ confirm: string;
11
+ /** El botón mientras se confirma. */
12
+ confirming: string;
13
+ /** El título del paso final. */
14
+ title: string;
15
+ /** La explicación bajo el título y junto al resumen. */
16
+ note: string;
17
+ }
2
18
  export interface ErkBookingPickerProps {
3
19
  value: BookingValue | null;
4
20
  onChange: (value: BookingValue | null) => void;
5
21
  label: string;
6
22
  description?: string | null;
7
23
  durationMinutes: number;
8
- bufferMinutes?: number;
9
24
  bookingMode?: BookingMode;
10
25
  temporaryLockMinutes?: number;
11
26
  personSelectionMode?: PersonSelectionMode;
12
27
  persons?: BookablePerson[];
13
- /** Recopila nombre/correo del que agenda. */
14
- collectContact?: boolean;
15
- /** El nombre es obligatorio para confirmar: el paso no admite reservas anónimas. */
16
- requireContact?: boolean;
17
- /** Provee los slots disponibles para una persona + día. */
28
+ /**
29
+ * Tipos de atención entre los que elige quien agenda, antes que todo lo demás. Con dos o más, el
30
+ * wizard gana el paso "¿Qué necesitas?" y la duración, los días y las horas son los del tipo elegido;
31
+ * con uno o ninguno no hay nada que elegir.
32
+ */
33
+ eventTemplates?: BookingEventTemplateOption[];
34
+ /**
35
+ * Zona horaria de la organización (IANA). Con ella el paso de hora dice en qué reloj están las horas
36
+ * y, si el navegador está en otra zona, la equivalencia en la hora de quien agenda.
37
+ */
38
+ timeZone?: string;
39
+ /**
40
+ * Datos de contacto que el formulario ya tiene (pasos de nombre o de correo anteriores). El nombre
41
+ * se pide siempre, salvo que llegue aquí; el correo solo se pide con `askEmail`.
42
+ */
43
+ knownContact?: {
44
+ name?: string;
45
+ email?: string;
46
+ };
47
+ /** Pide el correo en el agendamiento, validado y obligatorio. */
48
+ askEmail?: boolean;
49
+ /**
50
+ * Mensaje de una reserva que se perdió después de hecha (la renovación falló). Al cambiar, el
51
+ * wizard vuelve al paso de hora con el mensaje y horas frescas.
52
+ */
53
+ lapsed?: string;
54
+ /** Provee los slots disponibles para una persona + día (+ el tipo de atención elegido). */
18
55
  fetchSlots?: (args: {
19
56
  personId?: string;
20
57
  date: string;
58
+ eventTemplate?: string;
21
59
  }) => Promise<BookingSlot[]>;
22
60
  /**
23
61
  * Los días (`YYYY-MM-DD`) con algún horario para la persona elegida. Sin él, cualquier día desde
@@ -25,6 +63,7 @@ export interface ErkBookingPickerProps {
25
63
  */
26
64
  fetchDays?: (args: {
27
65
  personId?: string;
66
+ eventTemplate?: string;
28
67
  }) => Promise<string[]>;
29
68
  /**
30
69
  * Retira el slot de la oferta al confirmar. Lo que devuelve se guarda en el valor junto con la
@@ -33,22 +72,34 @@ export interface ErkBookingPickerProps {
33
72
  holdSlot?: (args: {
34
73
  slot: BookingSlot;
35
74
  personId?: string;
36
- contact?: {
37
- name?: string;
75
+ eventTemplate?: string;
76
+ contact: {
77
+ name: string;
38
78
  email?: string;
39
79
  };
40
- }) => Promise<Pick<BookingValue, 'appointment' | 'hold' | 'lockedUntil'>>;
80
+ }) => Promise<Partial<BookingValue>>;
41
81
  /** Deshabilita la tarjeta de entrada mientras el host termina de cargar la oferta. */
42
82
  disabled?: boolean;
83
+ /** No pide nombre ni correo: la cita que se está moviendo ya los tiene. */
84
+ withoutContact?: boolean;
85
+ /** Textos del wizard; por defecto los de una reserva nueva dentro de un formulario. */
86
+ copy?: Partial<BookingPickerCopy>;
43
87
  readOnly?: boolean;
44
88
  error?: string;
89
+ /**
90
+ * La oferta no cargó: el mensaje se muestra bajo la tarjeta de entrada con "Reintentar", que llama a
91
+ * `onRetryLoad`.
92
+ */
93
+ loadError?: string;
94
+ onRetryLoad?: () => void;
45
95
  }
46
96
  /**
47
97
  * Selector de cita: una tarjeta de entrada ("Agendar cita") abre el wizard en un modal —
48
98
  * una decisión por pantalla (persona → día → hora → confirmar), con panel izquierdo de
49
99
  * resumen que se va llenando, breadcrumb navegable arriba y barra inferior Atrás / Siguiente.
50
100
  * En el paso final el panel derecho desaparece y el resumen se expande en una columna
51
- * centrada (cross-fade corto). Confirmar cierra el modal y deja el resumen compacto.
101
+ * centrada (cross-fade corto). "Reservar" retira el horario de la oferta por unos minutos, cierra el
102
+ * modal y deja el resumen compacto; la cita se confirma cuando se envía el formulario.
52
103
  */
53
- declare function ErkBookingPicker({ value, onChange, label, description, durationMinutes, bufferMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, collectContact, requireContact, fetchSlots, fetchDays, holdSlot, disabled, readOnly, error, }: ErkBookingPickerProps): JSX.Element;
104
+ declare function ErkBookingPicker({ value, onChange, label, description, durationMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, eventTemplates, timeZone, knownContact, askEmail, lapsed, fetchSlots, fetchDays, holdSlot, disabled, withoutContact, copy: copyOverrides, readOnly, error, loadError, onRetryLoad, }: ErkBookingPickerProps): JSX.Element;
54
105
  export default ErkBookingPicker;
@@ -1,11 +1,12 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Dialog } from '@mui/material';
4
4
  import { addDays, addMonths, format, isBefore, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek, } from 'date-fns';
5
5
  import { es } from 'date-fns/locale';
6
6
  import { BookingMode, PersonSelectionMode, } from '../../@Types/BookingFormStep';
7
7
  import ErkButton from '../ErkButton/ErkButton';
8
8
  import ErkTextField from '../ErkTextField/ErkTextField';
9
+ import { browserTimeZone, clockIn, isValidEmail, nonEmpty, zoneLabel } from '../../Services/BookingService';
9
10
  import styles from './ErkBookingPicker.module.css';
10
11
  const WEEK_STARTS_ON = 1;
11
12
  const DOW = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
@@ -15,17 +16,22 @@ const MODE_LABEL = {
15
16
  [BookingMode.HYBRID]: 'Híbrida',
16
17
  };
17
18
  const STEP_LABEL = {
19
+ type: 'Tipo',
18
20
  person: 'Persona',
19
21
  day: 'Día',
20
22
  hour: 'Hora',
21
- confirm: 'Confirmar',
23
+ confirm: 'Reservar',
22
24
  };
23
25
  const STEP_HINT = {
26
+ type: 'Elige qué necesitas',
24
27
  person: 'Elige con quién agendar',
25
28
  day: 'Elige un día',
26
29
  hour: 'Elige una hora libre',
27
- confirm: 'Confirma tu cita',
30
+ confirm: 'Reserva tu horario',
28
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.';
29
35
  /** Duración del fade al reestructurarse el layout (entrar/salir del paso final). */
30
36
  const XFADE_MS = 160;
31
37
  /** El CTA de confirmar usa el acento del form (secondaryContrastColor vía --booking-accent). */
@@ -34,12 +40,34 @@ const ACCENT_SX = {
34
40
  '&:hover': { backgroundColor: 'var(--booking-accent, #ee6c4d)', filter: 'brightness(0.95)' },
35
41
  '&:disabled': { backgroundColor: 'var(--booking-accent, #ee6c4d)', color: '#fff' },
36
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
+ };
37
50
  function fmtDate(date) {
38
51
  return format(date, 'yyyy-MM-dd');
39
52
  }
40
53
  function longDate(date) {
41
54
  return format(date, 'EEEE d MMM', { locale: es });
42
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
+ }
43
71
  function initials(name) {
44
72
  return name
45
73
  .split(' ')
@@ -57,17 +85,25 @@ function capitalize(text) {
57
85
  * una decisión por pantalla (persona → día → hora → confirmar), con panel izquierdo de
58
86
  * resumen que se va llenando, breadcrumb navegable arriba y barra inferior Atrás / Siguiente.
59
87
  * En el paso final el panel derecho desaparece y el resumen se expande en una columna
60
- * centrada (cross-fade corto). Confirmar cierra el modal y deja el resumen compacto.
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.
61
90
  */
62
- function ErkBookingPicker({ value, onChange, label, description, durationMinutes, bufferMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, collectContact = false, requireContact = false, fetchSlots, fetchDays, holdSlot, disabled = false, readOnly = false, error, }) {
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 };
63
93
  const needsPerson = !!personSelectionMode && personSelectionMode !== PersonSelectionMode.NONE;
64
94
  const personsOptional = personSelectionMode === PersonSelectionMode.OPTIONAL;
95
+ const needsType = (eventTemplates?.length ?? 0) > 1;
65
96
  const today = startOfDay(new Date());
66
- const wizardKeys = useMemo(() => (needsPerson ? ['person', 'day', 'hour', 'confirm'] : ['day', 'hour', 'confirm']), [needsPerson]);
97
+ const wizardKeys = useMemo(() => [
98
+ ...(needsType ? ['type'] : []),
99
+ ...(needsPerson ? ['person'] : []),
100
+ ...['day', 'hour', 'confirm'],
101
+ ], [needsType, needsPerson]);
67
102
  const finalIdx = wizardKeys.length - 1;
68
103
  const [open, setOpen] = useState(false);
69
104
  const [wizStep, setWizStep] = useState(0);
70
105
  const [xfade, setXfade] = useState(false);
106
+ const [eventTemplate, setEventTemplate] = useState(undefined);
71
107
  const [person, setPerson] = useState(undefined);
72
108
  const [personTouched, setPersonTouched] = useState(false);
73
109
  const [date, setDate] = useState(undefined);
@@ -75,30 +111,61 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
75
111
  const [slot, setSlot] = useState(undefined);
76
112
  const [slots, setSlots] = useState([]);
77
113
  const [loading, setLoading] = useState(false);
114
+ const [slotsError, setSlotsError] = useState(undefined);
78
115
  const [contactName, setContactName] = useState('');
79
116
  const [contactEmail, setContactEmail] = useState('');
117
+ const [emailTouched, setEmailTouched] = useState(false);
80
118
  /** `undefined` mientras se consultan; sin `fetchDays` nunca se consultan y todo día futuro vale. */
81
119
  const [days, setDays] = useState(undefined);
120
+ const [daysError, setDaysError] = useState(undefined);
121
+ const [daysAttempt, setDaysAttempt] = useState(0);
82
122
  const [holding, setHolding] = useState(false);
83
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;
84
145
  const reduceMotion = useMemo(() => typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches, []);
85
146
  const monthCells = useMemo(() => {
86
147
  const first = startOfWeek(startOfMonth(viewMonth), { weekStartsOn: WEEK_STARTS_ON });
87
148
  return Array.from({ length: 42 }, (_, i) => addDays(first, i));
88
149
  }, [viewMonth]);
89
- const loadSlots = useCallback(async (day, personId) => {
150
+ const loadSlots = useCallback(async (day, personId, templateId) => {
90
151
  if (!fetchSlots)
91
152
  return;
153
+ const ticket = ++slotsRequest.current;
92
154
  setLoading(true);
93
155
  setSlots([]);
156
+ setSlotsError(undefined);
94
157
  try {
95
- setSlots(await fetchSlots({ personId, date: fmtDate(day) }));
158
+ const loaded = await fetchSlots({ personId, date: fmtDate(day), eventTemplate: templateId });
159
+ if (ticket === slotsRequest.current)
160
+ setSlots(loaded);
96
161
  }
97
- catch {
98
- setSlots([]);
162
+ catch (e) {
163
+ if (ticket === slotsRequest.current)
164
+ setSlotsError(errorText(e, GENERIC_SLOTS_ERROR));
99
165
  }
100
166
  finally {
101
- setLoading(false);
167
+ if (ticket === slotsRequest.current)
168
+ setLoading(false);
102
169
  }
103
170
  }, [fetchSlots]);
104
171
  useEffect(() => {
@@ -107,16 +174,20 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
107
174
  // The picker synchronises with the slots API here, and `loadSlots` raises its own
108
175
  // loading flag as it goes out — the very shape the rule asks effects to be used for.
109
176
  // oxlint-disable-next-line react/set-state-in-effect
110
- void loadSlots(date, person?.id);
111
- }, [value, date, person, loadSlots]);
177
+ void loadSlots(date, person?.id, eventTemplate?.id);
178
+ }, [value, date, person, eventTemplate, loadSlots]);
112
179
  useEffect(() => {
113
180
  if (!fetchDays || !open || value)
114
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;
115
185
  let cancelled = false;
116
186
  // Same shape as the slots effect: the days come from the host's API and the picker only mirrors them.
117
187
  // oxlint-disable-next-line react/set-state-in-effect
118
188
  setDays(undefined);
119
- fetchDays({ personId: person?.id })
189
+ setDaysError(undefined);
190
+ fetchDays({ personId: person?.id, eventTemplate: eventTemplate?.id })
120
191
  .then((available) => {
121
192
  if (cancelled)
122
193
  return;
@@ -129,14 +200,14 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
129
200
  ? current
130
201
  : startOfMonth(new Date(first + 'T00:00:00')));
131
202
  })
132
- .catch(() => {
203
+ .catch((e) => {
133
204
  if (!cancelled)
134
- setDays([]);
205
+ setDaysError(errorText(e, GENERIC_DAYS_ERROR));
135
206
  });
136
207
  return () => {
137
208
  cancelled = true;
138
209
  };
139
- }, [fetchDays, open, value, person]);
210
+ }, [fetchDays, open, value, person, eventTemplate, needsType, daysAttempt]);
140
211
  // Al entrar/salir del paso final el layout se reestructura; el reacomodo
141
212
  // ocurre mientras la tarjeta está invisible (fade corto en dos fases).
142
213
  const goToStep = (target) => {
@@ -154,6 +225,8 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
154
225
  const currentKey = wizardKeys[wizStep];
155
226
  const canAdvance = (() => {
156
227
  switch (currentKey) {
228
+ case 'type':
229
+ return !!eventTemplate;
157
230
  case 'person':
158
231
  return personsOptional ? personTouched : !!person;
159
232
  case 'day':
@@ -161,7 +234,7 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
161
234
  case 'hour':
162
235
  return !!slot;
163
236
  case 'confirm':
164
- return !requireContact || contactName.trim().length > 0;
237
+ return withoutContact || (nameValue.length > 0 && !emailInvalid);
165
238
  default:
166
239
  return true;
167
240
  }
@@ -177,8 +250,8 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
177
250
  endTime: slot.endTime,
178
251
  personId: person?.id,
179
252
  personName: person?.name,
180
- contactName: contactName || undefined,
181
- contactEmail: contactEmail || undefined,
253
+ contactName: nonEmpty(nameValue),
254
+ contactEmail: nonEmpty(emailValue),
182
255
  status: 'PENDING',
183
256
  };
184
257
  if (!holdSlot) {
@@ -192,9 +265,8 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
192
265
  const held = await holdSlot({
193
266
  slot,
194
267
  personId: person?.id,
195
- contact: collectContact
196
- ? { name: contactName || undefined, email: contactEmail || undefined }
197
- : undefined,
268
+ eventTemplate: eventTemplate?.id,
269
+ contact: { name: nameValue, email: nonEmpty(emailValue) },
198
270
  });
199
271
  setOpen(false);
200
272
  onChange({ ...selection, ...held });
@@ -204,7 +276,7 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
204
276
  setHoldError(e instanceof Error && e.message ? e.message : 'No pudimos reservar ese horario.');
205
277
  setSlot(undefined);
206
278
  goToStep(wizardKeys.indexOf('hour'));
207
- void loadSlots(date, person?.id);
279
+ void loadSlots(date, person?.id, eventTemplate?.id);
208
280
  }
209
281
  finally {
210
282
  setHolding(false);
@@ -213,6 +285,7 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
213
285
  const reset = () => {
214
286
  onChange(null);
215
287
  setWizStep(0);
288
+ setEventTemplate(undefined);
216
289
  setPerson(undefined);
217
290
  setPersonTouched(false);
218
291
  setDate(undefined);
@@ -221,6 +294,17 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
221
294
  setHoldError(undefined);
222
295
  setContactName('');
223
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);
224
308
  };
225
309
  const pickPerson = (p) => {
226
310
  setPerson(p);
@@ -229,37 +313,56 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
229
313
  };
230
314
  // Read-only / already booked → compact summary with the same card aesthetic
231
315
  if (readOnly || value) {
316
+ const pending = !!value && value.status === 'PENDING' && !readOnly;
232
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
233
318
  ? [
234
- value.personName,
235
319
  capitalize(longDate(new Date(value.date + 'T00:00:00'))),
236
320
  `${value.startTime}–${value.endTime}`,
321
+ value.personName ? `con ${value.personName}` : undefined,
237
322
  ]
238
323
  .filter(Boolean)
239
324
  .join(' · ')
240
- : 'Sin cita' })] }), value && !readOnly && _jsx(ErkButton, { text: "Cambiar", variant: "outlined", onClick: reset })] }) }));
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 })] }) }));
241
328
  }
242
329
  const isFinal = wizStep === finalIdx;
330
+ const duration = eventTemplate?.durationMinutes ?? durationMinutes;
331
+ const mode = eventTemplate?.bookingMode ?? bookingMode;
243
332
  const personLabel = person?.name ?? (personTouched && personsOptional ? 'Cualquiera' : undefined);
244
333
  const dayText = date ? capitalize(longDate(date)) : undefined;
245
- const hourText = slot ? `${slot.startTime} – ${slot.endTime}` : 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);
246
340
  const summaryRows = [
341
+ ...(needsType
342
+ ? [{ key: 'type', placeholder: 'Tipo de atención', text: eventTemplate?.name }]
343
+ : []),
247
344
  ...(needsPerson ? [{ key: 'person', placeholder: 'Persona', text: personLabel }] : []),
248
345
  { key: 'day', placeholder: 'Día', text: dayText },
249
346
  { key: 'hour', placeholder: 'Hora', text: hourText },
250
347
  ];
251
348
  const navParts = [];
349
+ if (eventTemplate)
350
+ navParts.push(_jsx("b", { children: eventTemplate.name }, "t"));
252
351
  if (personLabel)
253
352
  navParts.push(_jsx("b", { children: personLabel }, "p"));
254
353
  if (dayText)
255
354
  navParts.push(_jsx("span", { children: dayText }, "d"));
256
355
  if (hourText)
257
356
  navParts.push(_jsx("span", { className: styles.navHl, children: hourText }, "h"));
258
- 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: "Agendar cita", disabled: disabled, onClick: () => setOpen(true) })] }), _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" }), durationMinutes, " min", bufferMinutes ? ` · buffer ${bufferMinutes} min` : ''] }), bookingMode && (_jsxs("div", { className: styles.row, children: [_jsx("span", { className: styles.ic, children: "\u25F1" }), _jsx("span", { className: styles.pill, children: MODE_LABEL[bookingMode] })] }))] }), _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 && collectContact && (_jsxs("div", { className: styles.cfields, children: [_jsx(ErkTextField, { label: "Nombre", value: contactName, onChange: setContactName }), _jsx(ErkTextField, { label: "Correo", value: contactEmail, onChange: setContactEmail })] })), !!temporaryLockMinutes && (_jsxs("div", { className: styles.lock, children: ["\u23F3 Reservaremos el horario ", _jsxs("b", { children: [temporaryLockMinutes, " min"] }), " mientras confirmas."] }))] }), _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
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
259
360
  ? `${styles.stepItem} ${styles.stepDone}`
260
361
  : i === wizStep
261
362
  ? `${styles.stepItem} ${styles.stepOn}`
262
- : styles.stepItem, children: [_jsx("span", { className: styles.stepNum, children: i < wizStep ? '✓' : i + 1 }), STEP_LABEL[key]] }), i < wizardKeys.length - 1 && _jsx("span", { className: styles.stepSep })] }, key))) }), 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
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
263
366
  ? 'Elige una persona o deja que el sistema asigne a quien esté disponible.'
264
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
265
368
  ? `${styles.pcard} ${styles.pcardOn}`
@@ -270,25 +373,27 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
270
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) => {
271
374
  const outside = !isSameMonth(d, viewMonth);
272
375
  const past = isBefore(d, today);
273
- const offered = !days || days.includes(fmtDate(d));
376
+ const offered = !daysError && (!days || days.includes(fmtDate(d)));
274
377
  const selectable = !outside && !past && offered;
275
378
  const cls = date && isSameDay(d, date)
276
379
  ? `${styles.mday} ${styles.mdaySel}`
277
380
  : selectable
278
381
  ? `${styles.mday} ${styles.mdayFree}`
279
382
  : `${styles.mday} ${styles.mdayNone}`;
280
- return (_jsx("button", { type: "button", disabled: !selectable, className: cls, onClick: () => {
383
+ return (_jsx("button", { type: "button", disabled: !selectable, "aria-label": fullDate(d), className: cls, onClick: () => {
281
384
  setDate(d);
282
385
  setSlot(undefined);
283
386
  }, children: format(d, 'd') }, d.toISOString()));
284
- })] })] }), 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 ", durationMinutes, " min"] }), holdError && (_jsx("div", { className: styles.err, role: "alert", children: holdError })), loading ? (_jsx("div", { className: styles.empty, children: "Buscando horarios\u2026" })) : slots.length === 0 ? (_jsx("div", { className: styles.empty, children: "No hay horarios disponibles ese d\u00EDa." })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.slots, children: slots.map((s) => (_jsx("button", { type: "button", disabled: !s.available, className: slot?.id === s.id
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
285
388
  ? `${styles.slot} ${styles.slotSel}`
286
389
  : s.available
287
390
  ? styles.slot
288
- : `${styles.slot} ${styles.slotTaken}`, onClick: () => s.available && setSlot(s), children: s.startTime }, s.id))) }), _jsx("p", { className: styles.takenNote, children: "Tachados = ocupados por otro evento o cita." })] }))] }, "hour")), currentKey === 'confirm' && (_jsxs("section", { className: `${styles.pane} ${styles.paneFinal}`, children: [_jsx("h3", { className: styles.q, children: "Revisa y confirma tu cita" }), _jsx("p", { className: styles.qsub, children: collectContact
289
- ? 'Si todo está bien, completa tus datos y confirma.'
290
- : 'Si todo está bien, confirma tu cita.' })] }, "confirm"))] })] }), _jsxs("div", { className: styles.navbar, children: [_jsx("div", { className: styles.navSummary, children: isFinal || !canAdvance || navParts.length === 0
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
291
396
  ? STEP_HINT[currentKey]
292
- : 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 ? 'Reservando…' : 'Confirmar cita', 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 })] }));
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 })] }));
293
398
  }
294
399
  export default ErkBookingPicker;
@@ -303,6 +303,17 @@
303
303
  .pcardOn .chk {
304
304
  visibility: visible;
305
305
  }
306
+ .tdur {
307
+ margin-left: auto;
308
+ flex: none;
309
+ font-size: 12px;
310
+ font-weight: 700;
311
+ color: var(--eureka-primary, #3d5a7f);
312
+ white-space: nowrap;
313
+ }
314
+ .tdur + .chk {
315
+ margin-left: 0;
316
+ }
306
317
 
307
318
  /* step: day (month grid) */
308
319
  .monthWrap {
@@ -425,11 +436,38 @@
425
436
  cursor: not-allowed;
426
437
  text-decoration: line-through;
427
438
  }
428
- .takenNote {
429
- margin: 10px 0 0;
430
- font-size: 11.5px;
439
+ .slotGroup + .slotGroup {
440
+ margin-top: 12px;
441
+ }
442
+ .slotGroupTitle {
443
+ margin: 0 0 6px;
444
+ font-size: 12px;
445
+ font-weight: 600;
446
+ color: #787878;
447
+ text-transform: uppercase;
448
+ letter-spacing: 0.04em;
449
+ }
450
+ .firstTag {
451
+ display: block;
452
+ margin-top: 2px;
453
+ font-size: 10px;
454
+ font-weight: 500;
455
+ opacity: 0.8;
456
+ }
457
+ .tz {
458
+ margin: 0 0 10px;
459
+ font-size: 12px;
431
460
  color: #787878;
432
461
  }
462
+ .retry {
463
+ display: flex;
464
+ align-items: center;
465
+ flex-wrap: wrap;
466
+ gap: 10px;
467
+ font-size: 13px;
468
+ color: #d0473f;
469
+ padding: 12px 0;
470
+ }
433
471
  .empty {
434
472
  font-size: 13px;
435
473
  color: #787878;
@@ -20,9 +20,17 @@ export declare const WithPersonSelection: Story;
20
20
  export declare const PersonOptional: Story;
21
21
  /** No person selection → straight to date + time. */
22
22
  export declare const NoPerson: Story;
23
- /** Extra contact step (collectContact). */
24
- export declare const WithContact: Story;
25
- /** Full config: contact data, buffer, lock notice and in-person mode. */
23
+ /** The email comes from an earlier step of the form: the picker only asks for the name. */
24
+ export declare const EmailFromStep: Story;
25
+ /** Full config: name and email asked in the picker, lock notice, in-person mode and the organization clock. */
26
26
  export declare const Complete: Story;
27
+ /** A hold that lapsed after it was made: the requester is sent back to the hours with the reason. */
28
+ export declare const Lapsed: Story;
27
29
  /** Already booked / read-only summary. */
28
30
  export declare const ReadOnly: Story;
31
+ /** Several event templates: the requester picks what they need first, and its duration rules the hours. */
32
+ export declare const PickEventTemplate: Story;
33
+ /** Booked with an event template: the summary names it with its duration. */
34
+ export declare const BookedWithEventTemplate: Story;
35
+ /** The offer did not load: the entry card stays disabled and offers "Reintentar". */
36
+ export declare const OfferFailed: Story;