@arquimedes.co/eureka-forms 3.0.72 → 3.0.73-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.
- package/dist/BookingManage/BookingManagePage.test.js +0 -1
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +3 -1
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +20 -14
- package/dist/Icons/CheckIcon.d.ts +3 -0
- package/dist/Icons/CheckIcon.js +7 -0
- package/dist/Icons/VideoIcon.d.ts +3 -0
- package/dist/Icons/VideoIcon.js +7 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +19 -16
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +204 -132
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +606 -429
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +9 -5
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +37 -5
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +18 -15
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +178 -52
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useId, 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';
|
|
@@ -7,51 +7,92 @@ import { BookingMode, PersonSelectionMode, } from '../../@Types/BookingFormStep'
|
|
|
7
7
|
import ErkButton from '../ErkButton/ErkButton';
|
|
8
8
|
import ErkTextField from '../ErkTextField/ErkTextField';
|
|
9
9
|
import { browserTimeZone, clockIn, isValidEmail, nonEmpty, zoneLabel } from '../../Services/BookingService';
|
|
10
|
+
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon';
|
|
11
|
+
import ArrowRightIcon from '../../Icons/ArrowRightIcon';
|
|
12
|
+
import CalendarIcon from '../../Icons/CalendarIcon';
|
|
13
|
+
import CheckCircleIcon from '../../Icons/CheckCircleIcon';
|
|
14
|
+
import CheckIcon from '../../Icons/CheckIcon';
|
|
15
|
+
import ClockIcon from '../../Icons/ClockIcon';
|
|
16
|
+
import CloseIcon from '../../Icons/CloseIcon';
|
|
17
|
+
import EmailIcon from '../../Icons/EmailIcon';
|
|
18
|
+
import GroupIcon from '../../Icons/GroupIcon';
|
|
19
|
+
import InfoIcon from '../../Icons/InfoIcon';
|
|
20
|
+
import LocationIcon from '../../Icons/LocationIcon';
|
|
21
|
+
import PersonIcon from '../../Icons/PersonIcon';
|
|
22
|
+
import VideoIcon from '../../Icons/VideoIcon';
|
|
10
23
|
import styles from './ErkBookingPicker.module.css';
|
|
11
24
|
const WEEK_STARTS_ON = 1;
|
|
12
|
-
const DOW = ['L', 'M', '
|
|
25
|
+
const DOW = ['L', 'M', 'X', 'J', 'V', 'S', 'D'];
|
|
13
26
|
const MODE_LABEL = {
|
|
14
27
|
[BookingMode.IN_PERSON]: 'Presencial',
|
|
15
28
|
[BookingMode.VIRTUAL]: 'Virtual',
|
|
16
29
|
[BookingMode.HYBRID]: 'Híbrida',
|
|
17
30
|
};
|
|
31
|
+
/** Cómo se nombra la modalidad en el resumen del paso final. */
|
|
32
|
+
const MODE_SUMMARY = {
|
|
33
|
+
[BookingMode.IN_PERSON]: { title: 'Presencial' },
|
|
34
|
+
[BookingMode.VIRTUAL]: { title: 'Reunión virtual', sub: 'Recibirás el enlace en tu correo' },
|
|
35
|
+
[BookingMode.HYBRID]: { title: 'Presencial o virtual' },
|
|
36
|
+
};
|
|
18
37
|
const STEP_LABEL = {
|
|
19
|
-
type: 'Tipo',
|
|
38
|
+
type: 'Tipo de atención',
|
|
20
39
|
person: 'Persona',
|
|
21
|
-
|
|
22
|
-
hour: 'Hora',
|
|
23
|
-
confirm: 'Reservar',
|
|
40
|
+
date: 'Día y hora',
|
|
24
41
|
};
|
|
42
|
+
/** Lo que falta en el paso en pantalla, en la barra inferior. */
|
|
25
43
|
const STEP_HINT = {
|
|
26
|
-
type: 'Elige qué necesitas',
|
|
27
|
-
person: 'Elige con quién agendar',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
type: 'Elige qué necesitas.',
|
|
45
|
+
person: 'Elige con quién agendar.',
|
|
46
|
+
date: 'Elige un día.',
|
|
47
|
+
};
|
|
48
|
+
/** Lo que sigue, cuando el paso en pantalla ya está completo. */
|
|
49
|
+
const NEXT_HINT = {
|
|
50
|
+
person: 'Ahora elige con quién agendar.',
|
|
51
|
+
date: 'Ahora elige el día y la hora.',
|
|
31
52
|
};
|
|
32
53
|
const GENERIC_SLOTS_ERROR = 'No pudimos cargar los horarios de este día.';
|
|
33
54
|
const GENERIC_DAYS_ERROR = 'No pudimos cargar los días disponibles.';
|
|
34
55
|
const NO_EMAIL_HINT = 'Sin correo no podremos enviarte la confirmación.';
|
|
35
|
-
/**
|
|
36
|
-
const
|
|
56
|
+
/** Los botones de la barra inferior miden 40px, como en el diseño. */
|
|
57
|
+
const NAV_SX = { height: 40, padding: '0 18px' };
|
|
37
58
|
/** El CTA de confirmar usa el acento del form (secondaryContrastColor vía --booking-accent). */
|
|
38
59
|
const ACCENT_SX = {
|
|
60
|
+
...NAV_SX,
|
|
61
|
+
padding: '0 22px',
|
|
39
62
|
backgroundColor: 'var(--booking-accent, #ee6c4d)',
|
|
40
63
|
'&:hover': { backgroundColor: 'var(--booking-accent, #ee6c4d)', filter: 'brightness(0.95)' },
|
|
41
64
|
'&:disabled': { backgroundColor: 'var(--booking-accent, #ee6c4d)', color: '#fff' },
|
|
42
65
|
};
|
|
66
|
+
/** Un botón con borde deshabilitado se queda con borde (ErkButton lo rellenaría). */
|
|
67
|
+
const OUTLINED_DISABLED = { '&:disabled': { backgroundColor: 'transparent', color: 'primary.main' } };
|
|
68
|
+
/** El modal no pone marco: la tarjeta es el marco, del mismo tamaño en todos los pasos. */
|
|
69
|
+
const PAPER_SX = {
|
|
70
|
+
margin: '16px',
|
|
71
|
+
maxWidth: 'none',
|
|
72
|
+
maxHeight: 'none',
|
|
73
|
+
background: 'transparent',
|
|
74
|
+
boxShadow: 'none',
|
|
75
|
+
borderRadius: '16px',
|
|
76
|
+
overflow: 'visible',
|
|
77
|
+
'@media (max-width: 760px)': { margin: 0, width: '100%', borderRadius: 0 },
|
|
78
|
+
};
|
|
43
79
|
const BOOKING_COPY = {
|
|
44
|
-
open: '
|
|
80
|
+
open: 'Elegir horario',
|
|
45
81
|
confirm: 'Reservar',
|
|
46
82
|
confirming: 'Reservando…',
|
|
47
83
|
title: 'Revisa y reserva tu horario',
|
|
48
|
-
note: 'La cita
|
|
84
|
+
note: 'La cita queda confirmada cuando envíes el formulario.',
|
|
49
85
|
};
|
|
50
86
|
function fmtDate(date) {
|
|
51
87
|
return format(date, 'yyyy-MM-dd');
|
|
52
88
|
}
|
|
53
|
-
|
|
54
|
-
|
|
89
|
+
/** "Viernes 30 de octubre". */
|
|
90
|
+
function longDay(date) {
|
|
91
|
+
return capitalize(format(date, "EEEE d 'de' MMMM", { locale: es }));
|
|
92
|
+
}
|
|
93
|
+
/** "Vie 30 oct". */
|
|
94
|
+
function shortDay(date) {
|
|
95
|
+
return capitalize(format(date, 'EEE d MMM', { locale: es }));
|
|
55
96
|
}
|
|
56
97
|
function fullDate(date) {
|
|
57
98
|
return format(date, "EEEE d 'de' MMMM 'de' yyyy", { locale: es });
|
|
@@ -59,15 +100,6 @@ function fullDate(date) {
|
|
|
59
100
|
function errorText(error, fallback) {
|
|
60
101
|
return error instanceof Error && error.message ? error.message : fallback;
|
|
61
102
|
}
|
|
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
103
|
function initials(name) {
|
|
72
104
|
return name
|
|
73
105
|
.split(' ')
|
|
@@ -80,15 +112,52 @@ function initials(name) {
|
|
|
80
112
|
function capitalize(text) {
|
|
81
113
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
82
114
|
}
|
|
115
|
+
/** The theme the host sets as CSS variables; the modal lives in a portal and has to carry them itself. */
|
|
116
|
+
const THEME_VARS = [
|
|
117
|
+
'--eureka-primary',
|
|
118
|
+
'--eureka-text',
|
|
119
|
+
'--eureka-outline',
|
|
120
|
+
'--eureka-bg',
|
|
121
|
+
'--eureka-description',
|
|
122
|
+
'--eureka-error',
|
|
123
|
+
'--booking-accent',
|
|
124
|
+
];
|
|
125
|
+
function themeOf(element) {
|
|
126
|
+
if (!element)
|
|
127
|
+
return {};
|
|
128
|
+
const computed = window.getComputedStyle(element);
|
|
129
|
+
const vars = {};
|
|
130
|
+
for (const name of THEME_VARS) {
|
|
131
|
+
const value = computed.getPropertyValue(name).trim();
|
|
132
|
+
if (value)
|
|
133
|
+
vars[name] = value;
|
|
134
|
+
}
|
|
135
|
+
return vars;
|
|
136
|
+
}
|
|
137
|
+
function cx(...names) {
|
|
138
|
+
return names.filter(Boolean).join(' ');
|
|
139
|
+
}
|
|
140
|
+
function ModeIcon({ mode, size }) {
|
|
141
|
+
return mode === BookingMode.IN_PERSON ? _jsx(LocationIcon, { size: size }) : _jsx(VideoIcon, { size: size });
|
|
142
|
+
}
|
|
143
|
+
/** Una opción de los pasos de tipo y de persona. */
|
|
144
|
+
function OptionCard({ selected, dashed = false, avatar, title, subtitle, aside, onClick, }) {
|
|
145
|
+
return (_jsxs("button", { type: "button", "aria-pressed": selected, className: cx(styles.option, dashed && styles.optionAny, selected && styles.optionOn), onClick: onClick, children: [avatar, _jsxs("span", { className: styles.optionText, children: [_jsx("span", { className: styles.optionTitle, children: title }), subtitle && _jsx("span", { className: styles.optionSub, children: subtitle })] }), aside && _jsx("span", { className: styles.optionAside, children: aside }), selected && _jsx(CheckCircleIcon, { size: 20, className: styles.optionCheck })] }));
|
|
146
|
+
}
|
|
147
|
+
/** Una fila del resumen "Tu cita" del paso final. */
|
|
148
|
+
function SummaryRow({ icon, title, sub, change, onChange, }) {
|
|
149
|
+
return (_jsxs("div", { className: styles.sumRow, children: [_jsx("span", { className: styles.sumIcon, children: icon }), _jsxs("span", { className: styles.sumText, children: [_jsx("span", { className: styles.sumTitle, children: title }), sub && _jsx("span", { className: styles.sumSub, children: sub })] }), onChange && (_jsx("button", { type: "button", className: styles.sumChange, "aria-label": change, onClick: onChange, children: "Cambiar" }))] }));
|
|
150
|
+
}
|
|
83
151
|
/**
|
|
84
|
-
* Selector de cita
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
152
|
+
* Selector de cita. En el formulario es un control suelto como el de archivos: etiqueta, descripción y
|
|
153
|
+
* el botón "Elegir horario"; con un horario elegido, una fila con el resumen y "Cambiar".
|
|
154
|
+
*
|
|
155
|
+
* El botón abre el wizard en un modal de tamaño fijo — una decisión por pantalla ([tipo] → [persona] →
|
|
156
|
+
* día y hora → reservar), con stepper navegable arriba y barra inferior con lo elegido y Atrás /
|
|
157
|
+
* Siguiente. "Reservar" retira el horario de la oferta por unos minutos, cierra el modal y deja el
|
|
158
|
+
* resumen; la cita se confirma cuando se envía el formulario.
|
|
90
159
|
*/
|
|
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, }) {
|
|
160
|
+
function ErkBookingPicker({ value, onChange, label, description, required = false, 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
161
|
const copy = { ...BOOKING_COPY, ...copyOverrides };
|
|
93
162
|
const needsPerson = !!personSelectionMode && personSelectionMode !== PersonSelectionMode.NONE;
|
|
94
163
|
const personsOptional = personSelectionMode === PersonSelectionMode.OPTIONAL;
|
|
@@ -97,12 +166,14 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
|
|
|
97
166
|
const wizardKeys = useMemo(() => [
|
|
98
167
|
...(needsType ? ['type'] : []),
|
|
99
168
|
...(needsPerson ? ['person'] : []),
|
|
100
|
-
...['
|
|
169
|
+
...['date', 'confirm'],
|
|
101
170
|
], [needsType, needsPerson]);
|
|
102
171
|
const finalIdx = wizardKeys.length - 1;
|
|
172
|
+
const titleId = useId();
|
|
103
173
|
const [open, setOpen] = useState(false);
|
|
174
|
+
const containerRef = useRef(null);
|
|
175
|
+
const [theme, setTheme] = useState({});
|
|
104
176
|
const [wizStep, setWizStep] = useState(0);
|
|
105
|
-
const [xfade, setXfade] = useState(false);
|
|
106
177
|
const [eventTemplate, setEventTemplate] = useState(undefined);
|
|
107
178
|
const [person, setPerson] = useState(undefined);
|
|
108
179
|
const [personTouched, setPersonTouched] = useState(false);
|
|
@@ -123,14 +194,14 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
|
|
|
123
194
|
const [holdError, setHoldError] = useState(undefined);
|
|
124
195
|
/** Cada pedido de horas lleva un número; solo el último puede escribir el estado (A5.a). */
|
|
125
196
|
const slotsRequest = useRef(0);
|
|
126
|
-
// A hold lost after it was made sends the requester back to the
|
|
197
|
+
// A hold lost after it was made sends the requester back to the day and hour, with the reason on screen.
|
|
127
198
|
const [seenLapsed, setSeenLapsed] = useState(lapsed);
|
|
128
199
|
if (lapsed !== seenLapsed) {
|
|
129
200
|
setSeenLapsed(lapsed);
|
|
130
201
|
if (lapsed) {
|
|
131
202
|
setSlot(undefined);
|
|
132
203
|
setHoldError(lapsed);
|
|
133
|
-
setWizStep(Math.max(0, wizardKeys.indexOf(date
|
|
204
|
+
setWizStep(Math.max(0, wizardKeys.indexOf('date')));
|
|
134
205
|
}
|
|
135
206
|
}
|
|
136
207
|
const knownName = nonEmpty(knownContact?.name);
|
|
@@ -142,7 +213,6 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
|
|
|
142
213
|
const askContactEmail = !withoutContact && askEmail;
|
|
143
214
|
const localZone = useMemo(() => browserTimeZone(), []);
|
|
144
215
|
const showLocal = !!timeZone && !!localZone && localZone !== timeZone;
|
|
145
|
-
const reduceMotion = useMemo(() => typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches, []);
|
|
146
216
|
const monthCells = useMemo(() => {
|
|
147
217
|
const first = startOfWeek(startOfMonth(viewMonth), { weekStartsOn: WEEK_STARTS_ON });
|
|
148
218
|
return Array.from({ length: 42 }, (_, i) => addDays(first, i));
|
|
@@ -208,20 +278,6 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
|
|
|
208
278
|
cancelled = true;
|
|
209
279
|
};
|
|
210
280
|
}, [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
281
|
const currentKey = wizardKeys[wizStep];
|
|
226
282
|
const canAdvance = (() => {
|
|
227
283
|
switch (currentKey) {
|
|
@@ -229,10 +285,8 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
|
|
|
229
285
|
return !!eventTemplate;
|
|
230
286
|
case 'person':
|
|
231
287
|
return personsOptional ? personTouched : !!person;
|
|
232
|
-
case '
|
|
233
|
-
return !!date;
|
|
234
|
-
case 'hour':
|
|
235
|
-
return !!slot;
|
|
288
|
+
case 'date':
|
|
289
|
+
return !!date && !!slot;
|
|
236
290
|
case 'confirm':
|
|
237
291
|
return withoutContact || (nameValue.length > 0 && !emailInvalid);
|
|
238
292
|
default:
|
|
@@ -272,10 +326,10 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
|
|
|
272
326
|
onChange({ ...selection, ...held });
|
|
273
327
|
}
|
|
274
328
|
catch (e) {
|
|
275
|
-
// Somebody else took the slot while the form was being finished: back to the
|
|
329
|
+
// Somebody else took the slot while the form was being finished: back to the hours, with fresh slots.
|
|
276
330
|
setHoldError(e instanceof Error && e.message ? e.message : 'No pudimos reservar ese horario.');
|
|
277
331
|
setSlot(undefined);
|
|
278
|
-
|
|
332
|
+
setWizStep(wizardKeys.indexOf('date'));
|
|
279
333
|
void loadSlots(date, person?.id, eventTemplate?.id);
|
|
280
334
|
}
|
|
281
335
|
finally {
|
|
@@ -311,89 +365,107 @@ function ErkBookingPicker({ value, onChange, label, description, durationMinutes
|
|
|
311
365
|
setPersonTouched(true);
|
|
312
366
|
setSlot(undefined);
|
|
313
367
|
};
|
|
314
|
-
|
|
368
|
+
const pickDay = (d) => {
|
|
369
|
+
setDate(d);
|
|
370
|
+
setSlot(undefined);
|
|
371
|
+
};
|
|
372
|
+
const labelText = required ? `${label} *` : label;
|
|
373
|
+
const head = (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.label, children: labelText }), description && _jsx("div", { className: styles.description, children: description })] }));
|
|
374
|
+
// Read-only / already booked → a light row with what was booked.
|
|
315
375
|
if (readOnly || value) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
376
|
+
if (!value) {
|
|
377
|
+
return (_jsxs("div", { className: styles.container, children: [head, _jsx("div", { className: styles.note, children: "Sin cita" })] }));
|
|
378
|
+
}
|
|
379
|
+
const confirmed = readOnly || value.status === 'CONFIRMED';
|
|
380
|
+
const email = nonEmpty(value.contactEmail) ?? knownEmail;
|
|
381
|
+
const status = confirmed
|
|
382
|
+
? 'Cita confirmada'
|
|
383
|
+
: value.lockedUntil && timeZone
|
|
384
|
+
? `Reservado hasta las ${clockIn(value.lockedUntil, timeZone)}`
|
|
385
|
+
: 'Horario reservado';
|
|
386
|
+
return (_jsxs("div", { className: styles.container, children: [head, _jsxs("div", { className: styles.booked, children: [_jsx("span", { className: cx(styles.bookedIcon, confirmed && styles.bookedIconOk), children: confirmed ? _jsx(CheckCircleIcon, { size: 18 }) : _jsx(CalendarIcon, { size: 18 }) }), _jsxs("span", { className: styles.bookedText, children: [_jsxs("span", { className: styles.bookedMain, children: [longDay(new Date(value.date + 'T00:00:00')), " \u00B7 ", value.startTime, " \u2013 ", value.endTime] }), _jsx("span", { className: styles.bookedSub, children: [
|
|
387
|
+
value.personName,
|
|
388
|
+
value.eventTemplate
|
|
389
|
+
? `${value.eventTemplate.name} · ${value.eventTemplate.durationMinutes} min`
|
|
390
|
+
: undefined,
|
|
391
|
+
status,
|
|
322
392
|
]
|
|
323
393
|
.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 })] }) }));
|
|
394
|
+
.join(' · ') })] }), !readOnly && (_jsx(ErkButton, { text: "Cambiar", variant: "outlined", sx: { height: 32, minHeight: 32, padding: '0 12px', fontSize: 13 }, onClick: reset }))] }), !confirmed && !withoutContact && (_jsx("div", { className: styles.note, children: email ? (_jsxs(_Fragment, { children: ["Te confirmaremos a ", _jsx("b", { children: email }), " cuando env\u00EDes el formulario."] })) : ('Envía el formulario para confirmar la cita.') })), confirmed && value.status === 'CONFIRMED' && value.contactEmail && (_jsxs("div", { className: styles.note, children: ["Te enviamos la invitaci\u00F3n a ", value.contactEmail, "."] }))] }));
|
|
328
395
|
}
|
|
329
396
|
const isFinal = wizStep === finalIdx;
|
|
330
397
|
const duration = eventTemplate?.durationMinutes ?? durationMinutes;
|
|
331
398
|
const mode = eventTemplate?.bookingMode ?? bookingMode;
|
|
332
399
|
const personLabel = person?.name ?? (personTouched && personsOptional ? 'Cualquiera' : undefined);
|
|
333
|
-
const dayText = date ? capitalize(longDate(date)) : undefined;
|
|
334
400
|
const localStart = showLocal && localZone && slot?.start ? clockIn(slot.start, localZone) : undefined;
|
|
335
|
-
const hourText = slot
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
401
|
+
const hourText = slot ? `${slot.startTime} – ${slot.endTime}` : undefined;
|
|
402
|
+
const zoneText = timeZone ? capitalize(zoneLabel(timeZone, date ?? new Date())) : undefined;
|
|
403
|
+
const contactLine = withoutContact
|
|
404
|
+
? undefined
|
|
405
|
+
: knownEmail
|
|
406
|
+
? 'email'
|
|
407
|
+
: askName
|
|
408
|
+
? 'Te pediremos nombre y correo al reservar.'
|
|
409
|
+
: askContactEmail
|
|
410
|
+
? 'Te pediremos el correo al reservar.'
|
|
411
|
+
: undefined;
|
|
412
|
+
const hint = (() => {
|
|
413
|
+
if (currentKey === 'confirm')
|
|
414
|
+
return undefined;
|
|
415
|
+
if (!canAdvance)
|
|
416
|
+
return currentKey === 'date' && date ? 'Elige una hora.' : STEP_HINT[currentKey];
|
|
417
|
+
return NEXT_HINT[wizardKeys[wizStep + 1]];
|
|
418
|
+
})();
|
|
419
|
+
const chips = [
|
|
420
|
+
...(eventTemplate ? [{ key: 'type', text: eventTemplate.name }] : []),
|
|
421
|
+
...(personLabel ? [{ key: 'person', text: personLabel }] : []),
|
|
422
|
+
...(date ? [{ key: 'day', text: shortDay(date) }] : []),
|
|
423
|
+
...(hourText ? [{ key: 'hour', text: hourText, accent: true }] : []),
|
|
347
424
|
];
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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 })] }));
|
|
425
|
+
const stepLabel = (key) => (key === 'confirm' ? copy.confirm : STEP_LABEL[key]);
|
|
426
|
+
const goTo = (key) => setWizStep(wizardKeys.indexOf(key));
|
|
427
|
+
const lockNote = (_jsxs("div", { className: styles.info, children: [_jsx(InfoIcon, { size: 14, className: styles.infoIcon }), _jsxs("span", { children: [temporaryLockMinutes ? (_jsxs(_Fragment, { children: ["Reservaremos el horario durante ", _jsxs("b", { children: [temporaryLockMinutes, " min"] }), ".", ' '] })) : ('Reservaremos el horario. '), copy.note] })] }));
|
|
428
|
+
const renderType = () => (_jsxs("div", { className: styles.pane, children: [_jsxs("div", { className: styles.question, 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.options, children: (eventTemplates ?? []).map((t) => (_jsx(OptionCard, { selected: eventTemplate?.id === t.id, title: t.name, subtitle: t.description, aside: `${t.durationMinutes} min`, onClick: () => pickType(t) }, t.id))) })] }));
|
|
429
|
+
const renderPerson = () => (_jsxs("div", { className: styles.pane, children: [_jsxs("div", { className: styles.question, children: [_jsx("h3", { className: styles.q, children: "\u00BFCon qui\u00E9n quieres agendar?" }), _jsx("p", { className: styles.qsub, children: personsOptional
|
|
430
|
+
? 'Elige una persona o deja que el sistema asigne a quien esté disponible.'
|
|
431
|
+
: 'Elige la persona con la que quieres tu cita.' })] }), _jsxs("div", { className: styles.options, children: [(persons ?? []).map((p) => (_jsx(OptionCard, { selected: person?.id === p.id, avatar: _jsx("span", { className: styles.avatar, children: initials(p.name) }), title: p.name, subtitle: p.role, onClick: () => pickPerson(p) }, p.id))), personsOptional && (_jsx(OptionCard, { selected: personTouched && !person, dashed: true, avatar: _jsx("span", { className: cx(styles.avatar, styles.avatarAny), children: _jsx(GroupIcon, { size: 18 }) }), title: "Cualquiera", subtitle: "El sistema asigna a quien est\u00E9 disponible", onClick: () => pickPerson(undefined) }))] })] }));
|
|
432
|
+
const renderHours = () => {
|
|
433
|
+
if (!date)
|
|
434
|
+
return _jsx("p", { className: styles.empty, children: "Elige un d\u00EDa para ver sus horas." });
|
|
435
|
+
if (loading)
|
|
436
|
+
return _jsx("p", { className: styles.empty, children: "Buscando horarios\u2026" });
|
|
437
|
+
if (slotsError) {
|
|
438
|
+
return (_jsxs("div", { className: styles.retry, role: "alert", children: [_jsx("span", { children: slotsError }), _jsx(ErkButton, { text: "Reintentar", variant: "outlined", onClick: () => void loadSlots(date, person?.id, eventTemplate?.id) })] }));
|
|
439
|
+
}
|
|
440
|
+
if (slots.length === 0)
|
|
441
|
+
return _jsx("p", { className: styles.empty, children: "No hay horarios disponibles ese d\u00EDa." });
|
|
442
|
+
return (_jsx("div", { className: styles.hourList, children: slots.map((s) => (_jsx("button", { type: "button", disabled: !s.available, "aria-pressed": slot?.id === s.id, "aria-label": `${s.startTime} – ${s.endTime}${s.available ? '' : ' (ocupado)'}`, className: cx(styles.hour, slot?.id === s.id && styles.hourOn, !s.available && styles.hourTaken), onClick: () => s.available && setSlot(s), children: s.startTime }, s.id))) }));
|
|
443
|
+
};
|
|
444
|
+
const renderDate = () => (_jsxs("div", { className: styles.split, children: [_jsxs("div", { className: styles.calendar, 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", className: styles.iconBtn, onClick: () => setViewMonth((m) => addMonths(m, -1)), children: _jsx(ArrowLeftIcon, { size: 16 }) }), _jsx("button", { type: "button", "aria-label": "Mes siguiente", className: styles.iconBtn, onClick: () => setViewMonth((m) => addMonths(m, 1)), children: _jsx(ArrowRightIcon, { size: 16 }) })] })] }), _jsx("div", { className: styles.dow, "aria-hidden": true, children: DOW.map((d, i) => (_jsx("span", { children: d }, i))) }), _jsx("div", { className: styles.days, children: monthCells.map((d) => {
|
|
445
|
+
if (!isSameMonth(d, viewMonth))
|
|
446
|
+
return _jsx("span", {}, d.toISOString());
|
|
447
|
+
const past = isBefore(d, today);
|
|
448
|
+
const offered = !daysError && (!days || days.includes(fmtDate(d)));
|
|
449
|
+
const selectable = !past && offered;
|
|
450
|
+
const selected = !!date && isSameDay(d, date);
|
|
451
|
+
return (_jsx("button", { type: "button", disabled: !selectable, "aria-label": fullDate(d), "aria-pressed": selected, className: cx(styles.day, selected
|
|
452
|
+
? styles.dayOn
|
|
453
|
+
: selectable
|
|
454
|
+
? styles.dayFree
|
|
455
|
+
: isSameDay(d, today) && styles.dayToday), onClick: () => pickDay(d), children: format(d, 'd') }, d.toISOString()));
|
|
456
|
+
}) }), 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("p", { className: styles.calNote, children: "Buscando d\u00EDas con horarios\u2026" })) : days?.length === 0 ? (_jsx("p", { className: styles.calNote, children: "No hay d\u00EDas con horarios disponibles." })) : (_jsx("p", { className: styles.calNote, children: fetchDays
|
|
457
|
+
? 'Solo se pueden elegir los días con horarios disponibles.'
|
|
458
|
+
: 'Solo se muestran fechas a partir de hoy.' }))] }), _jsxs("div", { className: styles.hours, children: [_jsxs("div", { className: styles.hoursHead, children: [_jsx("span", { className: styles.hoursTitle, children: date ? longDay(date) : 'Elige un día' }), _jsx("span", { className: styles.hoursSub, children: zoneText ? `${zoneText} · citas de ${duration} min` : `Citas de ${duration} min` }), localStart && slot && (_jsxs("span", { className: styles.hoursSub, children: [slot.startTime, " es ", localStart, " en tu hora."] }))] }), holdError && (_jsx("div", { className: styles.alert, role: "alert", children: holdError })), renderHours()] })] }));
|
|
459
|
+
const renderConfirm = () => (_jsxs("div", { className: styles.split, children: [_jsxs("div", { className: styles.summary, children: [_jsx("p", { className: styles.overline, children: "Tu cita" }), eventTemplate && (_jsx(SummaryRow, { icon: _jsx(ClockIcon, { size: 16 }), title: eventTemplate.name, sub: `${eventTemplate.durationMinutes} min`, change: "Cambiar el tipo de atenci\u00F3n", onChange: () => goTo('type') })), needsPerson && personLabel && (_jsx(SummaryRow, { icon: _jsx(PersonIcon, { size: 16 }), title: personLabel, sub: person ? person.role : 'El sistema asigna a quien esté disponible', change: "Cambiar la persona", onChange: () => goTo('person') })), date && (_jsx(SummaryRow, { icon: _jsx(CalendarIcon, { size: 16 }), title: longDay(date), sub: [hourText, `${duration} min`, localStart ? `${localStart} en tu hora` : undefined]
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.join(' · '), change: "Cambiar el d\u00EDa y la hora", onChange: () => goTo('date') })), mode && (_jsx(SummaryRow, { icon: _jsx(ModeIcon, { mode: mode, size: 16 }), title: MODE_SUMMARY[mode].title, sub: MODE_SUMMARY[mode].sub }))] }), _jsx("div", { className: styles.side, children: withoutContact ? (_jsxs("div", { className: styles.question, children: [_jsx("h3", { className: styles.q, children: copy.title }), _jsx("p", { className: styles.qsub, children: copy.note })] })) : askName || askContactEmail ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.question, children: [_jsx("h3", { className: styles.q, children: "Tus datos" }), askContactEmail && (_jsx("p", { className: styles.qsub, children: mode === BookingMode.VIRTUAL
|
|
462
|
+
? 'Te enviaremos la confirmación y el enlace de la reunión.'
|
|
463
|
+
: 'Te enviaremos la confirmación a tu correo.' }))] }), 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
|
|
464
|
+
? 'Escribe un correo válido.'
|
|
465
|
+
: NO_EMAIL_HINT })), lockNote] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.question, children: [_jsx("h3", { className: styles.q, children: "Confirmaci\u00F3n" }), _jsx("p", { className: styles.qsub, children: "Usaremos los datos que ya escribiste en el formulario." })] }), _jsxs("div", { className: styles.known, children: [knownName && (_jsxs("div", { className: styles.knownRow, children: [_jsx(PersonIcon, { size: 16, className: styles.knownIcon }), _jsx("span", { className: styles.knownValue, children: knownName }), _jsx("span", { className: styles.knownFrom, children: "del campo Nombre" })] })), knownEmail && (_jsxs("div", { className: styles.knownRow, children: [_jsx(EmailIcon, { size: 16, className: styles.knownIcon }), _jsx("span", { className: styles.knownValue, children: knownEmail }), _jsx("span", { className: styles.knownFrom, children: "del campo Correo" })] }))] }), _jsx("p", { className: styles.fineprint, children: "\u00BFHay un error? Cierra el selector y corr\u00EDgelo en el formulario; el horario sigue elegido." }), lockNote] })) })] }));
|
|
466
|
+
return (_jsxs("div", { ref: containerRef, className: styles.container, children: [head, _jsx("div", { className: styles.control, children: _jsx(ErkButton, { text: copy.open, variant: "outlined", color: error ? 'error' : 'primary', disabled: disabled, startIcon: _jsx(CalendarIcon, { size: 16 }), sx: { height: 36, padding: '0 14px', ...OUTLINED_DISABLED }, onClick: () => {
|
|
467
|
+
setTheme(themeOf(containerRef.current));
|
|
468
|
+
setOpen(true);
|
|
469
|
+
} }) }), contactLine && (_jsx("div", { className: styles.note, children: contactLine === 'email' ? (_jsxs(_Fragment, { children: ["Te confirmaremos a ", _jsx("b", { children: knownEmail }), " cuando env\u00EDes el formulario."] })) : (contactLine) })), loadError && (_jsxs("div", { className: cx(styles.errorMsg, styles.loadError), role: "alert", children: [_jsx("span", { children: loadError }), onRetryLoad && (_jsx(ErkButton, { text: "Reintentar", variant: "outlined", sx: { height: 28, fontSize: 13 }, onClick: onRetryLoad }))] })), lapsed && !open && (_jsx("div", { className: styles.errorMsg, role: "alert", children: lapsed })), error && (_jsx("div", { className: styles.errorMsg, role: "alert", children: error })), _jsx(Dialog, { open: open, onClose: () => setOpen(false), maxWidth: false, "aria-labelledby": titleId, slotProps: { paper: { sx: PAPER_SX } }, children: _jsxs("div", { className: styles.card, style: theme, children: [_jsxs("header", { className: styles.header, children: [_jsxs("div", { className: styles.headText, children: [_jsx("h2", { id: titleId, className: styles.title, children: label }), _jsxs("div", { className: styles.meta, children: [_jsxs("span", { className: styles.metaItem, children: [_jsx(ClockIcon, { size: 14 }), duration, " min"] }), mode && (_jsxs("span", { className: styles.metaItem, children: [_jsx(ModeIcon, { mode: mode, size: 14 }), MODE_LABEL[mode]] }))] })] }), _jsx("button", { type: "button", "aria-label": "Cerrar", className: styles.close, onClick: () => setOpen(false), children: _jsx(CloseIcon, { size: 16 }) })] }), _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, "aria-current": i === wizStep ? 'step' : undefined, onClick: () => setWizStep(i), className: cx(styles.step, i < wizStep && styles.stepDone, i === wizStep && styles.stepOn), children: [_jsx("span", { className: styles.stepNum, children: i < wizStep ? _jsx(CheckIcon, { size: 12 }) : i + 1 }), _jsx("span", { className: styles.stepLabel, children: stepLabel(key) })] }), i < wizardKeys.length - 1 && _jsx("span", { className: styles.stepSep })] }, key))) }), _jsxs("div", { className: styles.body, children: [currentKey === 'type' && renderType(), currentKey === 'person' && renderPerson(), currentKey === 'date' && renderDate(), currentKey === 'confirm' && renderConfirm()] }, currentKey), _jsxs("footer", { className: styles.footer, children: [_jsxs("div", { className: styles.footSummary, "aria-live": "polite", children: [chips.map((chip) => (_jsx("span", { className: cx(styles.chip, chip.accent && styles.chipAccent), children: chip.text }, chip.key))), hint && _jsx("span", { className: styles.footHint, children: hint })] }), _jsxs("div", { className: styles.footActions, children: [wizStep > 0 && (_jsx(ErkButton, { text: "Atr\u00E1s", variant: "outlined", sx: NAV_SX, onClick: () => setWizStep(wizStep - 1) })), isFinal ? (_jsx(ErkButton, { text: holding ? copy.confirming : copy.confirm, sx: ACCENT_SX, className: styles.primaryAction, disabled: !slot || holding || !canAdvance, onClick: () => void confirm() })) : (_jsx(ErkButton, { text: "Siguiente", sx: { ...NAV_SX, padding: '0 22px' }, className: styles.primaryAction, disabled: !canAdvance, onClick: () => setWizStep(wizStep + 1) }))] })] })] }) })] }));
|
|
398
470
|
}
|
|
399
471
|
export default ErkBookingPicker;
|