@arquimedes.co/eureka-forms 3.0.52-test → 3.0.53-new-steps
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/AvailabilityFormStep.d.ts +48 -0
- package/dist/@Types/AvailabilityFormStep.js +24 -0
- package/dist/@Types/BankAccountFormStep.d.ts +67 -0
- package/dist/@Types/BankAccountFormStep.js +14 -0
- package/dist/@Types/BookingFormStep.d.ts +166 -0
- package/dist/@Types/BookingFormStep.js +44 -0
- package/dist/@Types/CalendarFormStep.d.ts +192 -0
- package/dist/@Types/CalendarFormStep.js +48 -0
- package/dist/@Types/CommunicationChannelFormStep.d.ts +51 -0
- package/dist/@Types/CommunicationChannelFormStep.js +15 -0
- package/dist/@Types/ConsentFormStep.d.ts +36 -0
- package/dist/@Types/ConsentFormStep.js +8 -0
- package/dist/@Types/EntityFormFormStep.d.ts +66 -0
- package/dist/@Types/EntityFormFormStep.js +20 -0
- package/dist/@Types/EventFormStep.d.ts +52 -0
- package/dist/@Types/EventFormStep.js +13 -0
- package/dist/@Types/ExternalReferenceFormStep.d.ts +74 -0
- package/dist/@Types/ExternalReferenceFormStep.js +20 -0
- package/dist/@Types/FormStep.d.ts +7 -2
- package/dist/@Types/FormStep.js +2 -1
- package/dist/@Types/LocationFormStep.d.ts +159 -0
- package/dist/@Types/LocationFormStep.js +46 -0
- package/dist/@Types/NumericFormSteps.d.ts +108 -0
- package/dist/@Types/NumericFormSteps.js +20 -0
- package/dist/@Types/PredefinedSelectorFormStep.d.ts +51 -0
- package/dist/@Types/PredefinedSelectorFormStep.js +39 -0
- package/dist/@Types/StepRef.d.ts +135 -0
- package/dist/@Types/StepRef.js +49 -0
- package/dist/FormSteps/BookingStep/BookingStep.d.ts +17 -0
- package/dist/FormSteps/BookingStep/BookingStep.js +14 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.d.ts +7 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +32 -0
- package/dist/FormSteps/CalendarStep/CalendarStep.d.ts +7 -0
- package/dist/FormSteps/CalendarStep/CalendarStep.js +14 -0
- package/dist/FormSteps/CalendarStep/MaterialCalendarStep/MaterialCalendarStep.d.ts +7 -0
- package/dist/FormSteps/CalendarStep/MaterialCalendarStep/MaterialCalendarStep.js +31 -0
- package/dist/FormSteps/LocationStep/LocationStep.d.ts +10 -0
- package/dist/FormSteps/LocationStep/LocationStep.js +14 -0
- package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.d.ts +3 -0
- package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.js +47 -0
- package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.module.css +18 -0
- package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.d.ts +3 -0
- package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.js +168 -0
- package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.module.css +94 -0
- package/dist/FormSteps/NumberStep/NumberStep.d.ts +10 -0
- package/dist/FormSteps/NumberStep/NumberStep.js +14 -0
- package/dist/FormSteps/Step.js +16 -0
- package/dist/FormSteps/StepFunctions.js +14 -0
- package/dist/FormSteps/StepFunctions.test.js +54 -0
- package/dist/Icons/AddIcon.d.ts +3 -0
- package/dist/Icons/AddIcon.js +7 -0
- package/dist/Icons/EditIcon.d.ts +3 -0
- package/dist/Icons/EditIcon.js +7 -0
- package/dist/Icons/RemoveIcon.d.ts +3 -0
- package/dist/Icons/RemoveIcon.js +7 -0
- package/dist/Icons/ViewDayIcon.d.ts +3 -0
- package/dist/Icons/ViewDayIcon.js +7 -0
- package/dist/Icons/ViewWeekIcon.d.ts +3 -0
- package/dist/Icons/ViewWeekIcon.js +7 -0
- package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.d.ts +19 -0
- package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.js +80 -0
- package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.module.css +82 -0
- package/dist/Shared/ErkAddressAutocomplete/googlePlaces.d.ts +29 -0
- package/dist/Shared/ErkAddressAutocomplete/googlePlaces.js +139 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +31 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +223 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +608 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +28 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +117 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.d.ts +1 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +49 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.d.ts +1 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +107 -0
- package/dist/Shared/ErkCalendar/CalendarFunctions.d.ts +28 -0
- package/dist/Shared/ErkCalendar/CalendarFunctions.js +121 -0
- package/dist/Shared/ErkCalendar/CalendarPopovers.d.ts +45 -0
- package/dist/Shared/ErkCalendar/CalendarPopovers.js +110 -0
- package/dist/Shared/ErkCalendar/ErkCalendar.d.ts +73 -0
- package/dist/Shared/ErkCalendar/ErkCalendar.js +293 -0
- package/dist/Shared/ErkCalendar/ErkCalendar.module.css +775 -0
- package/dist/Shared/ErkCalendar/ErkCalendar.test.d.ts +1 -0
- package/dist/Shared/ErkCalendar/ErkCalendar.test.js +171 -0
- package/dist/Shared/ErkDatePicker/ErkDatePicker.js +1 -3
- package/dist/Shared/ErkDateRangePicker/ErkDateRangePicker.d.ts +2 -3
- package/dist/Shared/ErkDateRangePicker/ErkDateRangePicker.js +1 -2
- package/dist/Shared/ErkLocationField/ErkLocationField.d.ts +23 -0
- package/dist/Shared/ErkLocationField/ErkLocationField.js +73 -0
- package/dist/Shared/ErkLocationField/ErkLocationField.module.css +29 -0
- package/dist/Shared/ErkLocationField/addressFields.d.ts +12 -0
- package/dist/Shared/ErkLocationField/addressFields.js +34 -0
- package/dist/Shared/ErkLocationField/locationRestrictions.d.ts +6 -0
- package/dist/Shared/ErkLocationField/locationRestrictions.js +51 -0
- package/dist/Shared/ErkLocationInput/ErkLocationInput.d.ts +15 -0
- package/dist/Shared/ErkLocationInput/ErkLocationInput.js +502 -0
- package/dist/Shared/ErkLocationInput/ErkLocationInput.module.css +115 -0
- package/dist/Shared/ErkNumberField/ErkNumberField.d.ts +42 -0
- package/dist/Shared/ErkNumberField/ErkNumberField.js +170 -0
- package/dist/constants/FormStepTypes.d.ts +5 -1
- package/dist/constants/FormStepTypes.js +4 -0
- package/dist/index.lib.d.ts +4 -2
- package/dist/index.lib.js +3 -1
- package/package.json +2 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import ErkBookingPicker from './ErkBookingPicker';
|
|
4
|
+
import { withFormStepProviders } from '../../FormSteps/@StoryDecorators/FormStepDecorator';
|
|
5
|
+
import { BookingMode, PersonSelectionMode } from '../../@Types/BookingFormStep';
|
|
6
|
+
const SLOTS = [
|
|
7
|
+
{ id: 's-1', startTime: '09:00', endTime: '09:50', available: true },
|
|
8
|
+
{ id: 's-2', startTime: '10:00', endTime: '10:50', available: false },
|
|
9
|
+
{ id: 's-3', startTime: '11:00', endTime: '11:50', available: true },
|
|
10
|
+
{ id: 's-4', startTime: '14:00', endTime: '14:50', available: true },
|
|
11
|
+
{ id: 's-5', startTime: '15:00', endTime: '15:50', available: false },
|
|
12
|
+
{ id: 's-6', startTime: '16:00', endTime: '16:50', available: true },
|
|
13
|
+
];
|
|
14
|
+
function mockFetchSlots() {
|
|
15
|
+
return new Promise((resolve) => setTimeout(() => resolve(SLOTS), 300));
|
|
16
|
+
}
|
|
17
|
+
const PERSONS = [
|
|
18
|
+
{ id: 'p-1', name: 'María González', role: 'Asesora' },
|
|
19
|
+
{ id: 'p-2', name: 'Carlos Ruiz', role: 'Técnico' },
|
|
20
|
+
];
|
|
21
|
+
/** Controlled wrapper so the picker's value is interactive in Storybook. */
|
|
22
|
+
function Interactive(args) {
|
|
23
|
+
const [value, setValue] = useState(args.value);
|
|
24
|
+
return (_jsx("div", { style: { maxWidth: 660 }, children: _jsx(ErkBookingPicker, { ...args, value: value, onChange: setValue }) }));
|
|
25
|
+
}
|
|
26
|
+
const meta = {
|
|
27
|
+
component: ErkBookingPicker,
|
|
28
|
+
tags: ['autodocs'],
|
|
29
|
+
decorators: [withFormStepProviders],
|
|
30
|
+
render: Interactive,
|
|
31
|
+
args: {
|
|
32
|
+
// Supplied by the Interactive wrapper; declared here so stories don't each repeat it.
|
|
33
|
+
onChange: () => undefined,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
export default meta;
|
|
37
|
+
/** Full flow: person → date → time. */
|
|
38
|
+
export const WithPersonSelection = {
|
|
39
|
+
args: {
|
|
40
|
+
value: null,
|
|
41
|
+
label: 'Seleccionar cita',
|
|
42
|
+
description: 'Elige profesional, día y hora.',
|
|
43
|
+
durationMinutes: 50,
|
|
44
|
+
personSelectionMode: PersonSelectionMode.REQUIRED,
|
|
45
|
+
persons: PERSONS,
|
|
46
|
+
fetchSlots: mockFetchSlots,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
/** Optional person — adds a "Cualquiera" option. */
|
|
50
|
+
export const PersonOptional = {
|
|
51
|
+
args: {
|
|
52
|
+
value: null,
|
|
53
|
+
label: 'Reservar turno',
|
|
54
|
+
description: 'Puedes elegir o dejar que asignemos.',
|
|
55
|
+
durationMinutes: 30,
|
|
56
|
+
personSelectionMode: PersonSelectionMode.OPTIONAL,
|
|
57
|
+
persons: PERSONS,
|
|
58
|
+
fetchSlots: mockFetchSlots,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
/** No person selection → straight to date + time. */
|
|
62
|
+
export const NoPerson = {
|
|
63
|
+
args: {
|
|
64
|
+
value: null,
|
|
65
|
+
label: 'Seleccionar horario',
|
|
66
|
+
description: 'Elige tu día y hora.',
|
|
67
|
+
durationMinutes: 60,
|
|
68
|
+
personSelectionMode: PersonSelectionMode.NONE,
|
|
69
|
+
fetchSlots: mockFetchSlots,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
/** Extra contact step (collectContact). */
|
|
73
|
+
export const WithContact = {
|
|
74
|
+
args: {
|
|
75
|
+
value: null,
|
|
76
|
+
label: 'Agendar cita',
|
|
77
|
+
description: 'Elige y déjanos tus datos.',
|
|
78
|
+
durationMinutes: 45,
|
|
79
|
+
personSelectionMode: PersonSelectionMode.OPTIONAL,
|
|
80
|
+
persons: PERSONS,
|
|
81
|
+
collectContact: true,
|
|
82
|
+
fetchSlots: mockFetchSlots,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
/** Full config: contact data, buffer, lock notice and in-person mode. */
|
|
86
|
+
export const Complete = {
|
|
87
|
+
args: {
|
|
88
|
+
value: null,
|
|
89
|
+
label: 'Agendar cita',
|
|
90
|
+
description: 'Soporte postventa',
|
|
91
|
+
durationMinutes: 60,
|
|
92
|
+
bufferMinutes: 10,
|
|
93
|
+
bookingMode: BookingMode.IN_PERSON,
|
|
94
|
+
temporaryLockMinutes: 15,
|
|
95
|
+
personSelectionMode: PersonSelectionMode.OPTIONAL,
|
|
96
|
+
persons: PERSONS,
|
|
97
|
+
collectContact: true,
|
|
98
|
+
fetchSlots: mockFetchSlots,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
/** Already booked / read-only summary. */
|
|
102
|
+
export const ReadOnly = {
|
|
103
|
+
args: {
|
|
104
|
+
readOnly: true,
|
|
105
|
+
label: 'Cita confirmada',
|
|
106
|
+
durationMinutes: 50,
|
|
107
|
+
value: {
|
|
108
|
+
type: 'BOOKING_VALUE',
|
|
109
|
+
slotId: 's-3',
|
|
110
|
+
date: '2026-07-02',
|
|
111
|
+
startTime: '11:00',
|
|
112
|
+
endTime: '11:50',
|
|
113
|
+
personName: 'María González',
|
|
114
|
+
status: 'CONFIRMED',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { composeStories } from '@storybook/react-vite';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import '@testing-library/jest-dom';
|
|
7
|
+
import { format } from 'date-fns';
|
|
8
|
+
import * as stories from './ErkBookingPicker.stories';
|
|
9
|
+
const { Complete } = composeStories(stories);
|
|
10
|
+
function clickableToday() {
|
|
11
|
+
// El número de hoy puede aparecer también como día de un mes adyacente (deshabilitado).
|
|
12
|
+
const candidates = screen.getAllByRole('button', { name: format(new Date(), 'd') });
|
|
13
|
+
const enabled = candidates.find((b) => !b.disabled);
|
|
14
|
+
if (!enabled)
|
|
15
|
+
throw new Error('No selectable day found for today');
|
|
16
|
+
return enabled;
|
|
17
|
+
}
|
|
18
|
+
describe('ErkBookingPicker Complete story', () => {
|
|
19
|
+
it('opens the wizard in a modal, completes it, and confirming closes the modal leaving the summary', async () => {
|
|
20
|
+
const user = userEvent.setup();
|
|
21
|
+
render(_jsx(Complete, {}));
|
|
22
|
+
// Trigger → modal con el wizard
|
|
23
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
24
|
+
expect(await screen.findByRole('heading', { name: '¿Con quién quieres agendar?' })).toBeInTheDocument();
|
|
25
|
+
// Persona
|
|
26
|
+
await user.click(screen.getByText('María González'));
|
|
27
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
28
|
+
// Día
|
|
29
|
+
await screen.findByRole('heading', { name: 'Elige un día' });
|
|
30
|
+
await user.click(clickableToday());
|
|
31
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
32
|
+
// Hora (los slots del mock llegan con delay)
|
|
33
|
+
await screen.findByRole('heading', { name: 'Elige una hora' });
|
|
34
|
+
await user.click(await screen.findByRole('button', { name: '09:00' }));
|
|
35
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
36
|
+
// Confirmar
|
|
37
|
+
await screen.findByRole('heading', { name: 'Revisa y confirma tu cita' });
|
|
38
|
+
await user.type(screen.getByLabelText('Nombre'), 'Juan Pérez');
|
|
39
|
+
await user.type(screen.getByLabelText('Correo'), 'juan@correo.com');
|
|
40
|
+
await user.click(screen.getByRole('button', { name: 'Confirmar cita' }));
|
|
41
|
+
// El modal se cierra y queda el resumen compacto con opción de cambiar
|
|
42
|
+
await waitFor(() => expect(screen.queryByRole('heading', { name: 'Revisa y confirma tu cita' })).not.toBeInTheDocument());
|
|
43
|
+
expect(screen.getByText(/María González .* 09:00–09:50/)).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByRole('button', { name: 'Cambiar' })).toBeInTheDocument();
|
|
45
|
+
// "Cambiar" limpia el valor y vuelve al botón de agendar
|
|
46
|
+
await user.click(screen.getByRole('button', { name: 'Cambiar' }));
|
|
47
|
+
expect(screen.getByRole('button', { name: 'Agendar cita' })).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
import { format } from 'date-fns';
|
|
7
|
+
import ErkBookingPicker from './ErkBookingPicker';
|
|
8
|
+
import MaterialProviders from '../../Utils/MaterialProviders';
|
|
9
|
+
import InternalFormStyle from '../../constants/InternalFormStyle';
|
|
10
|
+
import { PersonSelectionMode } from '../../@Types/BookingFormStep';
|
|
11
|
+
const SLOTS = [
|
|
12
|
+
{ id: 's-1', startTime: '09:00', endTime: '09:50', available: true },
|
|
13
|
+
{ id: 's-2', startTime: '10:00', endTime: '10:50', available: false },
|
|
14
|
+
{ id: 's-3', startTime: '11:00', endTime: '11:50', available: true },
|
|
15
|
+
];
|
|
16
|
+
const PERSONS = [
|
|
17
|
+
{ id: 'p-1', name: 'María González', role: 'Asesora' },
|
|
18
|
+
{ id: 'p-2', name: 'Carlos Ruiz', role: 'Técnico' },
|
|
19
|
+
];
|
|
20
|
+
function clickableToday() {
|
|
21
|
+
// El número de hoy puede aparecer también como día de un mes adyacente (deshabilitado).
|
|
22
|
+
const candidates = screen.getAllByRole('button', { name: format(new Date(), 'd') });
|
|
23
|
+
const enabled = candidates.find((b) => !b.disabled);
|
|
24
|
+
if (!enabled)
|
|
25
|
+
throw new Error('No selectable day found for today');
|
|
26
|
+
return enabled;
|
|
27
|
+
}
|
|
28
|
+
describe('ErkBookingPicker (wizard)', () => {
|
|
29
|
+
it('drives the full flow: person → day → hour → confirm', async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
const onChange = vi.fn();
|
|
32
|
+
const fetchSlots = vi.fn(() => Promise.resolve(SLOTS));
|
|
33
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: onChange, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.OPTIONAL, persons: PERSONS, collectContact: true, fetchSlots: fetchSlots }) }));
|
|
34
|
+
// La tarjeta de entrada abre el wizard en un modal
|
|
35
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
36
|
+
// Paso 1: persona — Siguiente bloqueado hasta elegir
|
|
37
|
+
expect(await screen.findByRole('heading', { name: '¿Con quién quieres agendar?' })).toBeInTheDocument();
|
|
38
|
+
const next = screen.getByRole('button', { name: 'Siguiente' });
|
|
39
|
+
expect(next).toBeDisabled();
|
|
40
|
+
await user.click(screen.getByText('María González'));
|
|
41
|
+
expect(next).toBeEnabled();
|
|
42
|
+
await user.click(next);
|
|
43
|
+
// Paso 2: día
|
|
44
|
+
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeDisabled();
|
|
46
|
+
await user.click(clickableToday());
|
|
47
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
48
|
+
// Paso 3: hora — slots del fetch; ocupados deshabilitados
|
|
49
|
+
expect(await screen.findByRole('heading', { name: 'Elige una hora' })).toBeInTheDocument();
|
|
50
|
+
expect(fetchSlots).toHaveBeenCalledWith({ personId: 'p-1', date: format(new Date(), 'yyyy-MM-dd') });
|
|
51
|
+
const taken = await screen.findByRole('button', { name: '10:00' });
|
|
52
|
+
expect(taken).toBeDisabled();
|
|
53
|
+
await user.click(screen.getByRole('button', { name: '09:00' }));
|
|
54
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
55
|
+
// Paso 4: confirmar — datos de contacto en la barra y CTA
|
|
56
|
+
expect(await screen.findByRole('heading', { name: 'Revisa y confirma tu cita' })).toBeInTheDocument();
|
|
57
|
+
await user.type(screen.getByLabelText('Nombre'), 'Juan Pérez');
|
|
58
|
+
await user.type(screen.getByLabelText('Correo'), 'juan@correo.com');
|
|
59
|
+
await user.click(screen.getByRole('button', { name: 'Confirmar cita' }));
|
|
60
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
61
|
+
type: 'BOOKING_VALUE',
|
|
62
|
+
slotId: 's-1',
|
|
63
|
+
date: format(new Date(), 'yyyy-MM-dd'),
|
|
64
|
+
startTime: '09:00',
|
|
65
|
+
endTime: '09:50',
|
|
66
|
+
personId: 'p-1',
|
|
67
|
+
personName: 'María González',
|
|
68
|
+
contactName: 'Juan Pérez',
|
|
69
|
+
contactEmail: 'juan@correo.com',
|
|
70
|
+
status: 'PENDING',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('skips the person step when personSelectionMode is NONE', async () => {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: () => { }, label: "Agendar cita", durationMinutes: 30, personSelectionMode: PersonSelectionMode.NONE, fetchSlots: () => Promise.resolve(SLOTS) }) }));
|
|
76
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
77
|
+
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
78
|
+
expect(screen.queryByText('¿Con quién quieres agendar?')).not.toBeInTheDocument();
|
|
79
|
+
expect(screen.queryByText('Persona')).not.toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
it('allows navigating back through the breadcrumb without losing the selection', async () => {
|
|
82
|
+
const user = userEvent.setup();
|
|
83
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: () => { }, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.REQUIRED, persons: PERSONS, fetchSlots: () => Promise.resolve(SLOTS) }) }));
|
|
84
|
+
await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
|
|
85
|
+
await user.click(await screen.findByText('Carlos Ruiz'));
|
|
86
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
87
|
+
expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
|
|
88
|
+
// Volver por el breadcrumb: el paso "Persona" completado es clicable
|
|
89
|
+
await user.click(screen.getByRole('button', { name: /Persona/ }));
|
|
90
|
+
expect(await screen.findByRole('heading', { name: '¿Con quién quieres agendar?' })).toBeInTheDocument();
|
|
91
|
+
// La selección se mantiene y permite avanzar de nuevo
|
|
92
|
+
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeEnabled();
|
|
93
|
+
});
|
|
94
|
+
it('renders the compact summary when a value exists', () => {
|
|
95
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: {
|
|
96
|
+
type: 'BOOKING_VALUE',
|
|
97
|
+
slotId: 's-3',
|
|
98
|
+
date: '2026-07-02',
|
|
99
|
+
startTime: '11:00',
|
|
100
|
+
endTime: '11:50',
|
|
101
|
+
personName: 'María González',
|
|
102
|
+
status: 'CONFIRMED',
|
|
103
|
+
}, onChange: () => { }, label: "Cita confirmada", durationMinutes: 50 }) }));
|
|
104
|
+
expect(screen.getByText(/María González .* 11:00–11:50/)).toBeInTheDocument();
|
|
105
|
+
expect(screen.getByRole('button', { name: 'Cambiar' })).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CalendarEvent, CalendarEventCategory, CalendarEventTemplate } from '../../@Types/CalendarFormStep';
|
|
2
|
+
/** Color base por categoría (paleta del app; navy = primario, coral = acento). */
|
|
3
|
+
export declare const CATEGORY_COLORS: Record<CalendarEventCategory, string>;
|
|
4
|
+
export declare const CATEGORY_LABELS: Record<CalendarEventCategory, string>;
|
|
5
|
+
export declare function isAvailability(category: CalendarEventCategory): boolean;
|
|
6
|
+
export declare function parseTime(hhmm: string): {
|
|
7
|
+
h: number;
|
|
8
|
+
m: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function toMinutes(hhmm: string): number;
|
|
11
|
+
export declare function minutesToHHMM(mins: number): string;
|
|
12
|
+
/** "2026-07-14T09:30:00" → "09:30" */
|
|
13
|
+
export declare function isoToHHMM(iso: string): string;
|
|
14
|
+
/** Compone el ISO en hora local (sin corrimiento de zona horaria). */
|
|
15
|
+
export declare function dateAndTimeToISO(date: Date, hhmm: string): string;
|
|
16
|
+
/** Interpreta el ISO como hora local (sin corrimiento de zona horaria). */
|
|
17
|
+
export declare function isoToDate(iso: string): Date;
|
|
18
|
+
export declare function newEventId(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Si el evento ocurre en `day`, considerando su recurrencia RRULE (RFC 5545 —
|
|
21
|
+
* el mismo estándar de Google/Outlook/iCal). Soporta:
|
|
22
|
+
* - `FREQ=DAILY` (+ INTERVAL)
|
|
23
|
+
* - `FREQ=WEEKLY` (+ INTERVAL, BYDAY — ej: `BYDAY=MO,TU,WE,TH,FR` = días hábiles)
|
|
24
|
+
* - `FREQ=MONTHLY` (+ INTERVAL, mismo día del mes que el inicio)
|
|
25
|
+
*/
|
|
26
|
+
export declare function occursOnDay(event: CalendarEvent, day: Date): boolean;
|
|
27
|
+
export declare function eventColor(event: CalendarEvent, templates?: CalendarEventTemplate[]): string;
|
|
28
|
+
export declare function eventLabel(event: CalendarEvent, templates?: CalendarEventTemplate[]): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { differenceInCalendarDays, differenceInCalendarMonths, differenceInCalendarWeeks, format, isSameDay, startOfDay, } from 'date-fns';
|
|
2
|
+
import { CalendarEventCategory, } from '../../@Types/CalendarFormStep';
|
|
3
|
+
/** Color base por categoría (paleta del app; navy = primario, coral = acento). */
|
|
4
|
+
export const CATEGORY_COLORS = {
|
|
5
|
+
[CalendarEventCategory.WORKING]: '#3d5a7f',
|
|
6
|
+
[CalendarEventCategory.PHYSICAL]: '#2e9367',
|
|
7
|
+
[CalendarEventCategory.VIRTUAL]: '#7b5ea7',
|
|
8
|
+
[CalendarEventCategory.SPECIFIC]: '#ee6c4d',
|
|
9
|
+
[CalendarEventCategory.MEETING]: '#d9822b',
|
|
10
|
+
[CalendarEventCategory.BUSY]: '#98a2b3',
|
|
11
|
+
[CalendarEventCategory.CUSTOM]: '#607d8b',
|
|
12
|
+
};
|
|
13
|
+
export const CATEGORY_LABELS = {
|
|
14
|
+
[CalendarEventCategory.WORKING]: 'Laboral',
|
|
15
|
+
[CalendarEventCategory.PHYSICAL]: 'Presencial',
|
|
16
|
+
[CalendarEventCategory.VIRTUAL]: 'Virtual',
|
|
17
|
+
[CalendarEventCategory.SPECIFIC]: 'Específico',
|
|
18
|
+
[CalendarEventCategory.MEETING]: 'Reunión',
|
|
19
|
+
[CalendarEventCategory.BUSY]: 'Ocupado',
|
|
20
|
+
[CalendarEventCategory.CUSTOM]: 'Personalizado',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Categorías que definen ventanas de disponibilidad: se pintan como franja rayada
|
|
24
|
+
* de fondo (se puede agendar encima), no como bloques sólidos.
|
|
25
|
+
*/
|
|
26
|
+
const AVAILABILITY_CATEGORIES = new Set([
|
|
27
|
+
CalendarEventCategory.WORKING,
|
|
28
|
+
CalendarEventCategory.PHYSICAL,
|
|
29
|
+
CalendarEventCategory.VIRTUAL,
|
|
30
|
+
CalendarEventCategory.SPECIFIC,
|
|
31
|
+
]);
|
|
32
|
+
export function isAvailability(category) {
|
|
33
|
+
return AVAILABILITY_CATEGORIES.has(category);
|
|
34
|
+
}
|
|
35
|
+
export function parseTime(hhmm) {
|
|
36
|
+
const [h, m] = hhmm.split(':').map(Number);
|
|
37
|
+
return { h: h ?? 0, m: m ?? 0 };
|
|
38
|
+
}
|
|
39
|
+
export function toMinutes(hhmm) {
|
|
40
|
+
const { h, m } = parseTime(hhmm);
|
|
41
|
+
return h * 60 + m;
|
|
42
|
+
}
|
|
43
|
+
export function minutesToHHMM(mins) {
|
|
44
|
+
const h = Math.floor(mins / 60);
|
|
45
|
+
const m = mins % 60;
|
|
46
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
47
|
+
}
|
|
48
|
+
/** "2026-07-14T09:30:00" → "09:30" */
|
|
49
|
+
export function isoToHHMM(iso) {
|
|
50
|
+
return iso.slice(11, 16);
|
|
51
|
+
}
|
|
52
|
+
/** Compone el ISO en hora local (sin corrimiento de zona horaria). */
|
|
53
|
+
export function dateAndTimeToISO(date, hhmm) {
|
|
54
|
+
const { h, m } = parseTime(hhmm);
|
|
55
|
+
const d = new Date(date);
|
|
56
|
+
d.setHours(h, m, 0, 0);
|
|
57
|
+
return `${format(d, 'yyyy-MM-dd')}T${format(d, 'HH:mm:ss')}`;
|
|
58
|
+
}
|
|
59
|
+
/** Interpreta el ISO como hora local (sin corrimiento de zona horaria). */
|
|
60
|
+
export function isoToDate(iso) {
|
|
61
|
+
const [datePart, timePart] = iso.slice(0, 19).split('T');
|
|
62
|
+
const [y, mo, d] = datePart.split('-').map(Number);
|
|
63
|
+
const { h, m } = parseTime(timePart ?? '00:00');
|
|
64
|
+
return new Date(y, (mo ?? 1) - 1, d ?? 1, h, m, 0, 0);
|
|
65
|
+
}
|
|
66
|
+
export function newEventId() {
|
|
67
|
+
return `evt-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
68
|
+
}
|
|
69
|
+
/** Códigos de día RFC 5545 indexados por `Date.getDay()`. */
|
|
70
|
+
const RRULE_DAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
|
71
|
+
/**
|
|
72
|
+
* Si el evento ocurre en `day`, considerando su recurrencia RRULE (RFC 5545 —
|
|
73
|
+
* el mismo estándar de Google/Outlook/iCal). Soporta:
|
|
74
|
+
* - `FREQ=DAILY` (+ INTERVAL)
|
|
75
|
+
* - `FREQ=WEEKLY` (+ INTERVAL, BYDAY — ej: `BYDAY=MO,TU,WE,TH,FR` = días hábiles)
|
|
76
|
+
* - `FREQ=MONTHLY` (+ INTERVAL, mismo día del mes que el inicio)
|
|
77
|
+
*/
|
|
78
|
+
export function occursOnDay(event, day) {
|
|
79
|
+
const start = isoToDate(event.start);
|
|
80
|
+
if (isSameDay(start, day))
|
|
81
|
+
return true;
|
|
82
|
+
if (!event.recurrence)
|
|
83
|
+
return false;
|
|
84
|
+
if (startOfDay(day) < startOfDay(start))
|
|
85
|
+
return false;
|
|
86
|
+
const rule = event.recurrence;
|
|
87
|
+
const interval = /INTERVAL=(\d+)/.exec(rule);
|
|
88
|
+
const every = interval ? parseInt(interval[1], 10) : 1;
|
|
89
|
+
if (rule.includes('FREQ=DAILY')) {
|
|
90
|
+
return differenceInCalendarDays(day, start) % every === 0;
|
|
91
|
+
}
|
|
92
|
+
if (rule.includes('FREQ=WEEKLY')) {
|
|
93
|
+
const byday = /BYDAY=([A-Z,]+)/.exec(rule);
|
|
94
|
+
if (byday) {
|
|
95
|
+
if (!byday[1].split(',').includes(RRULE_DAY_CODES[day.getDay()]))
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
else if (day.getDay() !== start.getDay()) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return differenceInCalendarWeeks(day, start, { weekStartsOn: 1 }) % every === 0;
|
|
102
|
+
}
|
|
103
|
+
if (rule.includes('FREQ=MONTHLY')) {
|
|
104
|
+
if (day.getDate() !== start.getDate())
|
|
105
|
+
return false;
|
|
106
|
+
return differenceInCalendarMonths(day, start) % every === 0;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
export function eventColor(event, templates) {
|
|
111
|
+
if (event.color)
|
|
112
|
+
return event.color;
|
|
113
|
+
const template = templates?.find((t) => t.id === event.eventTemplateId);
|
|
114
|
+
return template?.color ?? CATEGORY_COLORS[event.category];
|
|
115
|
+
}
|
|
116
|
+
export function eventLabel(event, templates) {
|
|
117
|
+
if (event.title)
|
|
118
|
+
return event.title;
|
|
119
|
+
const template = templates?.find((t) => t.id === event.eventTemplateId);
|
|
120
|
+
return template?.name ?? CATEGORY_LABELS[event.category];
|
|
121
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CalendarEvent, CalendarEventCategory, CalendarEventTemplate } from '../../@Types/CalendarFormStep';
|
|
2
|
+
export interface CalendarCreateState {
|
|
3
|
+
anchorEl: HTMLElement;
|
|
4
|
+
date: Date;
|
|
5
|
+
/** Hora de inicio sugerida en formato HH:mm (según dónde se hizo clic). */
|
|
6
|
+
startHHMM: string;
|
|
7
|
+
/** Hora de fin sugerida en formato HH:mm. */
|
|
8
|
+
endHHMM: string;
|
|
9
|
+
}
|
|
10
|
+
interface CalendarCreatePopoverProps {
|
|
11
|
+
state: CalendarCreateState;
|
|
12
|
+
/** Categorías de disponibilidad permitidas (laboral/presencial/virtual). */
|
|
13
|
+
availabilityCategories: CalendarEventCategory[];
|
|
14
|
+
/** Categorías de evento permitidas (reunión/ocupado/personalizado). */
|
|
15
|
+
eventCategories: CalendarEventCategory[];
|
|
16
|
+
/** Plantillas de la organización: las `bookable` son tipos de espacio agendable. */
|
|
17
|
+
eventTemplates?: CalendarEventTemplate[];
|
|
18
|
+
allowRecurring: boolean;
|
|
19
|
+
allowAllDay: boolean;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
onCreate: (event: CalendarEvent) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Popover anclado al slot clickeado, con dos modos:
|
|
25
|
+
* - Disponibilidad: crea ventanas agendables — "estoy libre para que me agenden"
|
|
26
|
+
* (general/presencial/virtual, o un tipo de espacio de la organización).
|
|
27
|
+
* - Evento: reuniones, bloqueos y eventos personalizados.
|
|
28
|
+
*/
|
|
29
|
+
export declare function CalendarCreatePopover({ state, availabilityCategories, eventCategories, eventTemplates, allowRecurring, allowAllDay, onClose, onCreate, }: CalendarCreatePopoverProps): JSX.Element;
|
|
30
|
+
export interface CalendarDetailState {
|
|
31
|
+
anchorEl: HTMLElement;
|
|
32
|
+
event: CalendarEvent;
|
|
33
|
+
/** Si el evento es del calendario propio (editable) o de una fuente compartida. */
|
|
34
|
+
own: boolean;
|
|
35
|
+
}
|
|
36
|
+
interface CalendarDetailPopoverProps {
|
|
37
|
+
state: CalendarDetailState;
|
|
38
|
+
eventTemplates?: CalendarEventTemplate[];
|
|
39
|
+
readOnly: boolean;
|
|
40
|
+
onClose: () => void;
|
|
41
|
+
onDelete: (id: string) => void;
|
|
42
|
+
}
|
|
43
|
+
/** Detalle de un evento: cuándo, categoría, notas y acciones. */
|
|
44
|
+
export declare function CalendarDetailPopover({ state, eventTemplates, readOnly, onClose, onDelete, }: CalendarDetailPopoverProps): JSX.Element;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Popover } from '@mui/material';
|
|
4
|
+
import { format } from 'date-fns';
|
|
5
|
+
import { es } from 'date-fns/locale';
|
|
6
|
+
import { CalendarEventCategory, } from '../../@Types/CalendarFormStep';
|
|
7
|
+
import ErkButton from '../ErkButton/ErkButton';
|
|
8
|
+
import ErkTextField from '../ErkTextField/ErkTextField';
|
|
9
|
+
import { CATEGORY_COLORS, CATEGORY_LABELS, dateAndTimeToISO, eventColor, eventLabel, isoToDate, isoToHHMM, newEventId, } from './CalendarFunctions';
|
|
10
|
+
import styles from './ErkCalendar.module.css';
|
|
11
|
+
/** L–V: el patrón "días hábiles" en RRULE estándar. */
|
|
12
|
+
const BUSINESS_DAYS_RRULE = 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR';
|
|
13
|
+
const EVENT_RECURRENCE = [
|
|
14
|
+
{ label: 'No se repite' },
|
|
15
|
+
{ label: 'Cada semana', rrule: 'FREQ=WEEKLY' },
|
|
16
|
+
{ label: 'Cada 2 semanas', rrule: 'FREQ=WEEKLY;INTERVAL=2' },
|
|
17
|
+
];
|
|
18
|
+
const AVAILABILITY_RECURRENCE = [
|
|
19
|
+
{ label: 'Solo este día' },
|
|
20
|
+
{ label: 'Cada día hábil (L–V)', rrule: BUSINESS_DAYS_RRULE },
|
|
21
|
+
{ label: 'Cada semana', rrule: 'FREQ=WEEKLY' },
|
|
22
|
+
{ label: 'Cada 2 semanas', rrule: 'FREQ=WEEKLY;INTERVAL=2' },
|
|
23
|
+
];
|
|
24
|
+
/** Etiqueta del chip de disponibilidad general (WORKING se lee mejor como "General"). */
|
|
25
|
+
const AVAILABILITY_CHIP_LABELS = {
|
|
26
|
+
[CalendarEventCategory.WORKING]: 'General',
|
|
27
|
+
};
|
|
28
|
+
function capitalize(text) {
|
|
29
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
30
|
+
}
|
|
31
|
+
function toInvalidRange(start, end) {
|
|
32
|
+
return !start || !end || end <= start;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Popover anclado al slot clickeado, con dos modos:
|
|
36
|
+
* - Disponibilidad: crea ventanas agendables — "estoy libre para que me agenden"
|
|
37
|
+
* (general/presencial/virtual, o un tipo de espacio de la organización).
|
|
38
|
+
* - Evento: reuniones, bloqueos y eventos personalizados.
|
|
39
|
+
*/
|
|
40
|
+
export function CalendarCreatePopover({ state, availabilityCategories, eventCategories, eventTemplates, allowRecurring, allowAllDay, onClose, onCreate, }) {
|
|
41
|
+
const bookableTemplates = eventTemplates?.filter((t) => t.bookable) ?? [];
|
|
42
|
+
const otherTemplates = eventTemplates?.filter((t) => !t.bookable) ?? [];
|
|
43
|
+
const hasAvailability = availabilityCategories.length > 0 || bookableTemplates.length > 0;
|
|
44
|
+
const hasEvents = eventCategories.length > 0 || otherTemplates.length > 0;
|
|
45
|
+
const [mode, setMode] = useState(hasAvailability ? 'AVAILABILITY' : 'EVENT');
|
|
46
|
+
const [category, setCategory] = useState(availabilityCategories[0] ?? eventCategories[0] ?? CalendarEventCategory.WORKING);
|
|
47
|
+
const [templateId, setTemplateId] = useState(undefined);
|
|
48
|
+
const [title, setTitle] = useState('');
|
|
49
|
+
const [start, setStart] = useState(state.startHHMM);
|
|
50
|
+
const [end, setEnd] = useState(state.endHHMM);
|
|
51
|
+
const [allDay, setAllDay] = useState(false);
|
|
52
|
+
const [recurrence, setRecurrence] = useState(undefined);
|
|
53
|
+
const isAvailabilityMode = mode === 'AVAILABILITY';
|
|
54
|
+
const categoryOptions = isAvailabilityMode ? availabilityCategories : eventCategories;
|
|
55
|
+
const templateOptions = isAvailabilityMode ? bookableTemplates : otherTemplates;
|
|
56
|
+
const recurrenceOptions = isAvailabilityMode ? AVAILABILITY_RECURRENCE : EVENT_RECURRENCE;
|
|
57
|
+
const needsTitle = !isAvailabilityMode &&
|
|
58
|
+
!templateId &&
|
|
59
|
+
(category === CalendarEventCategory.MEETING || category === CalendarEventCategory.CUSTOM);
|
|
60
|
+
const activeColor = templateId
|
|
61
|
+
? (eventTemplates?.find((t) => t.id === templateId)?.color ?? CATEGORY_COLORS[category])
|
|
62
|
+
: CATEGORY_COLORS[category];
|
|
63
|
+
const switchMode = (next) => {
|
|
64
|
+
setMode(next);
|
|
65
|
+
setTemplateId(undefined);
|
|
66
|
+
setCategory(next === 'AVAILABILITY'
|
|
67
|
+
? (availabilityCategories[0] ?? CalendarEventCategory.WORKING)
|
|
68
|
+
: (eventCategories[0] ?? CalendarEventCategory.MEETING));
|
|
69
|
+
setRecurrence(undefined);
|
|
70
|
+
};
|
|
71
|
+
const pickCategory = (cat) => {
|
|
72
|
+
setCategory(cat);
|
|
73
|
+
setTemplateId(undefined);
|
|
74
|
+
};
|
|
75
|
+
const pickTemplate = (template) => {
|
|
76
|
+
setTemplateId(template.id);
|
|
77
|
+
setCategory(template.category);
|
|
78
|
+
};
|
|
79
|
+
const create = () => {
|
|
80
|
+
if (!allDay && toInvalidRange(start, end))
|
|
81
|
+
return;
|
|
82
|
+
onCreate({
|
|
83
|
+
id: newEventId(),
|
|
84
|
+
category,
|
|
85
|
+
eventTemplateId: templateId,
|
|
86
|
+
title: title || undefined,
|
|
87
|
+
start: dateAndTimeToISO(state.date, allDay ? '00:00' : start),
|
|
88
|
+
end: dateAndTimeToISO(state.date, allDay ? '23:59' : end),
|
|
89
|
+
isAllDay: allDay || undefined,
|
|
90
|
+
recurrence,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
return (_jsxs(Popover, { open: true, anchorEl: state.anchorEl, onClose: onClose, anchorOrigin: { vertical: 'top', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, slotProps: { paper: { className: styles.pop } }, children: [_jsxs("div", { className: styles.popHead, children: [_jsx("span", { className: styles.popBar, style: { backgroundColor: activeColor } }), _jsxs("div", { children: [_jsx("h4", { className: styles.popTitle, children: isAvailabilityMode ? 'Nueva disponibilidad' : 'Nuevo evento' }), _jsx("span", { className: styles.popWhen, children: capitalize(format(state.date, 'EEE d MMM', { locale: es })) })] })] }), hasAvailability && hasEvents && (_jsxs("div", { className: styles.tabs, children: [_jsx("button", { type: "button", className: isAvailabilityMode ? `${styles.tab} ${styles.tabOn}` : styles.tab, onClick: () => switchMode('AVAILABILITY'), children: "Disponibilidad" }), _jsx("button", { type: "button", className: !isAvailabilityMode ? `${styles.tab} ${styles.tabOn}` : styles.tab, onClick: () => switchMode('EVENT'), children: "Evento" })] })), _jsxs("div", { className: styles.popBody, children: [isAvailabilityMode && (_jsx("p", { className: styles.popHint, children: "Estos espacios quedan libres para que te agenden citas." })), _jsxs("div", { className: styles.field, children: [_jsx("span", { className: styles.fieldLabel, children: isAvailabilityMode ? 'Disponible para' : 'Categoría' }), _jsxs("div", { className: styles.catRow, children: [categoryOptions.map((cat) => (_jsxs("button", { type: "button", className: !templateId && category === cat
|
|
94
|
+
? `${styles.catChip} ${styles.catChipOn}`
|
|
95
|
+
: styles.catChip, onClick: () => pickCategory(cat), children: [_jsx("span", { className: styles.catDot, style: { backgroundColor: CATEGORY_COLORS[cat] } }), isAvailabilityMode
|
|
96
|
+
? (AVAILABILITY_CHIP_LABELS[cat] ?? CATEGORY_LABELS[cat])
|
|
97
|
+
: CATEGORY_LABELS[cat]] }, cat))), templateOptions.map((template) => (_jsxs("button", { type: "button", className: templateId === template.id
|
|
98
|
+
? `${styles.catChip} ${styles.catChipOn}`
|
|
99
|
+
: styles.catChip, onClick: () => pickTemplate(template), children: [_jsx("span", { className: styles.catDot, style: {
|
|
100
|
+
backgroundColor: template.color ?? CATEGORY_COLORS[template.category],
|
|
101
|
+
} }), template.name] }, template.id)))] })] }), !isAvailabilityMode && (_jsx(ErkTextField, { label: needsTitle ? 'Título' : 'Título (opcional)', value: title, onChange: setTitle, placeholder: needsTitle ? 'Ej: Standup, Visita cliente…' : CATEGORY_LABELS[category], autoFocus: needsTitle })), allowAllDay && (_jsxs("label", { className: styles.checkRow, children: [_jsx("input", { type: "checkbox", checked: allDay, onChange: (e) => setAllDay(e.target.checked) }), "Todo el d\u00EDa"] })), !allDay && (_jsxs("div", { className: styles.timeRow, children: [_jsxs("label", { className: styles.field, children: [_jsx("span", { className: styles.fieldLabel, children: "Inicio" }), _jsx("input", { type: "time", className: styles.timeInput, value: start, onChange: (e) => setStart(e.target.value) })] }), _jsx("span", { className: styles.timeDash, children: "\u2013" }), _jsxs("label", { className: styles.field, children: [_jsx("span", { className: styles.fieldLabel, children: "Fin" }), _jsx("input", { type: "time", className: styles.timeInput, value: end, onChange: (e) => setEnd(e.target.value) })] })] })), allowRecurring && (_jsxs("label", { className: styles.field, children: [_jsx("span", { className: styles.fieldLabel, children: "Repetir" }), _jsx("select", { className: styles.selectInput, value: recurrence ?? '', onChange: (e) => setRecurrence(e.target.value || undefined), children: recurrenceOptions.map((opt) => (_jsx("option", { value: opt.rrule ?? '', children: opt.label }, opt.label))) })] }))] }), _jsxs("div", { className: styles.popActions, children: [_jsx(ErkButton, { text: isAvailabilityMode ? 'Crear disponibilidad' : 'Crear evento', disabled: (!allDay && toInvalidRange(start, end)) || (needsTitle && !title), onClick: create }), _jsx(ErkButton, { text: "Cancelar", variant: "outlined", onClick: onClose })] })] }));
|
|
102
|
+
}
|
|
103
|
+
/** Detalle de un evento: cuándo, categoría, notas y acciones. */
|
|
104
|
+
export function CalendarDetailPopover({ state, eventTemplates, readOnly, onClose, onDelete, }) {
|
|
105
|
+
const { event } = state;
|
|
106
|
+
const day = isoToDate(event.start);
|
|
107
|
+
return (_jsxs(Popover, { open: true, anchorEl: state.anchorEl, onClose: onClose, anchorOrigin: { vertical: 'top', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, slotProps: { paper: { className: styles.pop } }, children: [_jsxs("div", { className: styles.popHead, children: [_jsx("span", { className: styles.popBar, style: { backgroundColor: eventColor(event, eventTemplates) } }), _jsxs("div", { children: [_jsx("h4", { className: styles.popTitle, children: eventLabel(event, eventTemplates) }), _jsxs("span", { className: styles.popWhen, children: [capitalize(format(day, 'EEE d MMM', { locale: es })), " \u00B7", ' ', event.isAllDay
|
|
108
|
+
? 'Todo el día'
|
|
109
|
+
: `${isoToHHMM(event.start)} – ${isoToHHMM(event.end)}`, event.recurrence ? ' · se repite' : ''] })] })] }), _jsxs("div", { className: styles.popBody, children: [_jsx("span", { className: styles.detailChip, style: { color: eventColor(event, eventTemplates) }, children: CATEGORY_LABELS[event.category] }), event.notes && _jsx("p", { className: styles.detailNotes, children: event.notes })] }), !readOnly && (_jsxs("div", { className: styles.popActions, children: [_jsx(ErkButton, { text: "Eliminar", color: "error", variant: "outlined", onClick: () => onDelete(event.id) }), _jsx(ErkButton, { text: "Cerrar", variant: "outlined", onClick: onClose })] }))] }));
|
|
110
|
+
}
|