@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.
- package/dist/@Types/BookingFormStep.d.ts +70 -18
- package/dist/@Types/BookingFormStep.js +21 -0
- package/dist/App/App.submitError.test.d.ts +1 -0
- package/dist/App/App.submitError.test.js +53 -0
- package/dist/BookingManage/AppointmentText.d.ts +5 -0
- package/dist/BookingManage/AppointmentText.js +20 -0
- package/dist/BookingManage/BookingManagePage.d.ts +17 -0
- package/dist/BookingManage/BookingManagePage.js +177 -0
- package/dist/BookingManage/BookingManagePage.module.css +115 -0
- package/dist/BookingManage/BookingManagePage.test.d.ts +1 -0
- package/dist/BookingManage/BookingManagePage.test.js +149 -0
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.d.ts +14 -5
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.js +8 -1
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.module.css +12 -0
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.d.ts +1 -0
- package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.js +21 -0
- package/dist/Form/Form.d.ts +3 -0
- package/dist/Form/Form.js +9 -2
- package/dist/Form/FormTypes/ColumnForm/ColumnForm.d.ts +1 -1
- package/dist/Form/FormTypes/ColumnForm/ColumnForm.js +3 -2
- package/dist/Form/FormTypes/StepperForm/StepperForm.d.ts +1 -1
- package/dist/Form/FormTypes/StepperForm/StepperForm.js +3 -2
- package/dist/Form/SubmitError/SubmitError.d.ts +11 -0
- package/dist/Form/SubmitError/SubmitError.js +9 -0
- package/dist/Form/SubmitError/SubmitError.module.css +25 -0
- package/dist/Form/SubmitError/SubmitError.test.d.ts +1 -0
- package/dist/Form/SubmitError/SubmitError.test.js +34 -0
- package/dist/Form/SubmitError/SubmitErrorReason.d.ts +13 -0
- package/dist/Form/SubmitError/SubmitErrorReason.js +20 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.d.ts +8 -1
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +197 -41
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.d.ts +1 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +275 -0
- package/dist/Services/BookingService.d.ts +159 -12
- package/dist/Services/BookingService.js +210 -12
- package/dist/Services/BookingService.test.js +130 -9
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +63 -12
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +141 -36
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +41 -3
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +11 -3
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +69 -9
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +13 -15
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +331 -62
- package/dist/main.js +4 -1
- package/package.json +1 -1
|
@@ -1,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
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
37
|
-
|
|
75
|
+
eventTemplate?: string;
|
|
76
|
+
contact: {
|
|
77
|
+
name: string;
|
|
38
78
|
email?: string;
|
|
39
79
|
};
|
|
40
|
-
}) => Promise<
|
|
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).
|
|
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,
|
|
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: '
|
|
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: '
|
|
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).
|
|
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,
|
|
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(() =>
|
|
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
|
-
|
|
158
|
+
const loaded = await fetchSlots({ personId, date: fmtDate(day), eventTemplate: templateId });
|
|
159
|
+
if (ticket === slotsRequest.current)
|
|
160
|
+
setSlots(loaded);
|
|
96
161
|
}
|
|
97
|
-
catch {
|
|
98
|
-
|
|
162
|
+
catch (e) {
|
|
163
|
+
if (ticket === slotsRequest.current)
|
|
164
|
+
setSlotsError(errorText(e, GENERIC_SLOTS_ERROR));
|
|
99
165
|
}
|
|
100
166
|
finally {
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
181
|
-
contactEmail:
|
|
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
|
-
|
|
196
|
-
|
|
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' })
|
|
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
|
|
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:
|
|
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 === '
|
|
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
|
|
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))) }),
|
|
289
|
-
?
|
|
290
|
-
:
|
|
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 ?
|
|
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
|
-
.
|
|
429
|
-
margin:
|
|
430
|
-
|
|
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
|
-
/**
|
|
24
|
-
export declare const
|
|
25
|
-
/** Full config:
|
|
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;
|