@arquimedes.co/eureka-forms 3.0.71-test → 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/README +33 -1
- 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 +249 -0
- package/dist/@Types/BookingFormStep.js +65 -0
- package/dist/@Types/CalendarFormStep.d.ts +209 -0
- package/dist/@Types/CalendarFormStep.js +55 -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/App/App.stories.d.ts +10 -0
- package/dist/App/App.stories.js +189 -0
- package/dist/App/App.submitError.test.d.ts +1 -0
- package/dist/App/App.submitError.test.js +53 -0
- package/dist/App/AppHooks.js +8 -0
- package/dist/App/AppHooks.test.js +8 -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 +16 -8
- package/dist/Form/FormFunctions.d.ts +17 -0
- package/dist/Form/FormFunctions.js +22 -0
- package/dist/Form/FormFunctions.test.js +28 -1
- 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/AvailabilityStep/AvailabilityStep.d.ts +7 -0
- package/dist/FormSteps/AvailabilityStep/AvailabilityStep.js +14 -0
- package/dist/FormSteps/AvailabilityStep/MaterialAvailabilityStep/MaterialAvailabilityStep.d.ts +8 -0
- package/dist/FormSteps/AvailabilityStep/MaterialAvailabilityStep/MaterialAvailabilityStep.js +32 -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 +21 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +289 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.d.ts +1 -0
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +275 -0
- package/dist/FormSteps/CheckBoxStep/CheckBoxStep.mobileLabel.test.js +20 -12
- package/dist/FormSteps/CheckBoxStep/MaterialCheckBoxStep/MaterialCheckBoxStep.js +6 -13
- package/dist/FormSteps/CheckBoxStep/MaterialCheckBoxStep/MaterialCheckBoxStep.module.css +2 -13
- package/dist/FormSteps/ClassifierSelectorStep/ClassifierSelectorStep.lateClassifiers.test.d.ts +1 -0
- package/dist/FormSteps/ClassifierSelectorStep/ClassifierSelectorStep.lateClassifiers.test.js +70 -0
- package/dist/FormSteps/ClassifierSelectorStep/MaterialClassifierSelectorStep/MaterialClassifierSelectorStep.js +3 -1
- package/dist/FormSteps/EntityValueStep/MaterialEntityValuePickerStep/MaterialEntityValuePickerStep.js +26 -26
- package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.d.ts +12 -0
- package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.js +35 -0
- package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.test.d.ts +1 -0
- package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.test.js +31 -0
- package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.d.ts +27 -0
- package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.js +64 -0
- package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.d.ts +1 -0
- package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.js +95 -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 +54 -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 +170 -0
- package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.module.css +96 -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/FormSteps/StepHooks.d.ts +21 -0
- package/dist/FormSteps/StepHooks.js +55 -8
- package/dist/FormSteps/StepHooks.test.d.ts +1 -0
- package/dist/FormSteps/StepHooks.test.js +92 -0
- package/dist/FormSteps/isStepHiddenByCondition.test.d.ts +1 -0
- package/dist/FormSteps/isStepHiddenByCondition.test.js +42 -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/Services/BookingService.d.ts +226 -0
- package/dist/Services/BookingService.js +264 -0
- package/dist/Services/BookingService.test.d.ts +1 -0
- package/dist/Services/BookingService.test.js +178 -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 +88 -0
- package/dist/Shared/ErkAddressAutocomplete/googlePlaces.d.ts +29 -0
- package/dist/Shared/ErkAddressAutocomplete/googlePlaces.js +139 -0
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.d.ts +25 -0
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.js +112 -0
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.module.css +88 -0
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.d.ts +1 -0
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.js +73 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +105 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +399 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +646 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +36 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +177 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.d.ts +1 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +47 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.d.ts +1 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +440 -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/ErkIconButton/ErkIconButton.d.ts +17 -0
- package/dist/Shared/ErkIconButton/ErkIconButton.js +11 -0
- package/dist/Shared/ErkIconButton/ErkIconButton.module.css +41 -0
- package/dist/Shared/ErkLocationField/ErkLocationField.d.ts +24 -0
- package/dist/Shared/ErkLocationField/ErkLocationField.js +128 -0
- package/dist/Shared/ErkLocationField/ErkLocationField.module.css +164 -0
- package/dist/Shared/ErkLocationField/ErkLocationField.test.d.ts +1 -0
- package/dist/Shared/ErkLocationField/ErkLocationField.test.js +82 -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 +128 -0
- package/dist/Shared/ErkNumberField/ErkNumberField.d.ts +42 -0
- package/dist/Shared/ErkNumberField/ErkNumberField.js +169 -0
- package/dist/Utils/AxiosWidget.js +9 -1
- 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/dist/main.js +4 -1
- package/package.json +2 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { LocationInputMode } from '../../@Types/LocationFormStep';
|
|
2
|
+
/**
|
|
3
|
+
* Carga y consulta de Google Places (nueva API `AutocompleteSuggestion`).
|
|
4
|
+
* Encapsula el script loader + importLibrary que antes vivía inline en ErkLocationInput,
|
|
5
|
+
* para reutilizarlo desde el autocomplete de Base UI sin duplicar lógica.
|
|
6
|
+
*/
|
|
7
|
+
// Google's new Places classes (AutocompleteSuggestion/SessionToken) are untyped in the
|
|
8
|
+
// installed @types/google.maps, so they're kept as `any` — same approach as ErkLocationInput.
|
|
9
|
+
let loadPromise = null;
|
|
10
|
+
let suggestionClass = null;
|
|
11
|
+
let sessionTokenClass = null;
|
|
12
|
+
let geocoderClass = null;
|
|
13
|
+
function loadScript(apiKey) {
|
|
14
|
+
if (typeof window === 'undefined')
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
const win = window;
|
|
17
|
+
if (win.google?.maps?.importLibrary)
|
|
18
|
+
return Promise.resolve();
|
|
19
|
+
if (loadPromise)
|
|
20
|
+
return loadPromise;
|
|
21
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
22
|
+
const script = document.createElement('script');
|
|
23
|
+
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&loading=async`;
|
|
24
|
+
script.async = true;
|
|
25
|
+
script.defer = true;
|
|
26
|
+
script.onload = () => resolve();
|
|
27
|
+
script.onerror = () => {
|
|
28
|
+
loadPromise = null;
|
|
29
|
+
reject(new Error('Google Maps script failed to load.'));
|
|
30
|
+
};
|
|
31
|
+
document.head.appendChild(script);
|
|
32
|
+
});
|
|
33
|
+
return loadPromise;
|
|
34
|
+
}
|
|
35
|
+
/** Asegura el script cargado y las clases de Places listas. Idempotente. */
|
|
36
|
+
export async function loadPlaces(apiKey) {
|
|
37
|
+
await loadScript(apiKey);
|
|
38
|
+
const win = window;
|
|
39
|
+
if (win.google?.maps?.importLibrary && !suggestionClass) {
|
|
40
|
+
const places = await win.google.maps.importLibrary('places');
|
|
41
|
+
suggestionClass = places.AutocompleteSuggestion;
|
|
42
|
+
sessionTokenClass = places.AutocompleteSessionToken;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Token de sesión nuevo (mejora el billing/relevancia de Google). */
|
|
46
|
+
export function newSessionToken() {
|
|
47
|
+
return sessionTokenClass ? new sessionTokenClass() : undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Reverse-geocoding para el modo GPS: coordenadas → dirección estructurada. */
|
|
50
|
+
export async function reverseGeocode(apiKey, lat, lng) {
|
|
51
|
+
await loadScript(apiKey);
|
|
52
|
+
const win = window;
|
|
53
|
+
if (win.google?.maps?.importLibrary && !geocoderClass) {
|
|
54
|
+
const geocoding = await win.google.maps.importLibrary('geocoding');
|
|
55
|
+
geocoderClass = geocoding.Geocoder;
|
|
56
|
+
}
|
|
57
|
+
else if (!geocoderClass && win.google?.maps?.Geocoder) {
|
|
58
|
+
geocoderClass = win.google.maps.Geocoder;
|
|
59
|
+
}
|
|
60
|
+
if (!geocoderClass)
|
|
61
|
+
return { addressDetail: {} };
|
|
62
|
+
const geocoder = new geocoderClass();
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
geocoder.geocode({ location: { lat, lng } }, (results, status) => {
|
|
65
|
+
if (status === 'OK' && results?.[0]) {
|
|
66
|
+
resolve({
|
|
67
|
+
address: results[0].formatted_address,
|
|
68
|
+
addressDetail: parseAddressComponents(results[0].address_components),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
resolve({ addressDetail: {} });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export async function fetchAddressSuggestions(input, options) {
|
|
78
|
+
if (!suggestionClass)
|
|
79
|
+
return [];
|
|
80
|
+
const request = { input };
|
|
81
|
+
if (options?.sessionToken)
|
|
82
|
+
request.sessionToken = options.sessionToken;
|
|
83
|
+
if (options?.regionCodes)
|
|
84
|
+
request.includedRegionCodes = options.regionCodes;
|
|
85
|
+
const { suggestions } = await suggestionClass.fetchAutocompleteSuggestions(request);
|
|
86
|
+
return suggestions
|
|
87
|
+
.filter((s) => s.placePrediction)
|
|
88
|
+
.map((s) => ({
|
|
89
|
+
placeId: s.placePrediction.placeId,
|
|
90
|
+
label: s.placePrediction.mainText?.text ?? s.placePrediction.text.text,
|
|
91
|
+
description: s.placePrediction.secondaryText?.text ?? s.placePrediction.text.text,
|
|
92
|
+
toPlace: () => s.placePrediction.toPlace(),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Descompone los `addressComponents` de un Place en el shape estructurado del step.
|
|
97
|
+
* Acepta el formato nuevo (`longText`/`shortText`) y el legacy (`long_name`/`short_name`).
|
|
98
|
+
*/
|
|
99
|
+
export function parseAddressComponents(components) {
|
|
100
|
+
const detail = {};
|
|
101
|
+
for (const component of components ?? []) {
|
|
102
|
+
const types = component.types;
|
|
103
|
+
const longName = component.longText ?? component.long_name;
|
|
104
|
+
const shortName = component.shortText ?? component.short_name;
|
|
105
|
+
if (types.includes('route'))
|
|
106
|
+
detail.street = longName;
|
|
107
|
+
else if (types.includes('street_number'))
|
|
108
|
+
detail.streetNumber = longName;
|
|
109
|
+
else if (types.includes('subpremise'))
|
|
110
|
+
detail.unit = longName;
|
|
111
|
+
else if (types.includes('neighborhood') || types.includes('sublocality'))
|
|
112
|
+
detail.neighborhood = longName;
|
|
113
|
+
else if (types.includes('locality'))
|
|
114
|
+
detail.city = longName;
|
|
115
|
+
else if (types.includes('administrative_area_level_1'))
|
|
116
|
+
detail.state = longName;
|
|
117
|
+
else if (types.includes('country')) {
|
|
118
|
+
detail.country = longName;
|
|
119
|
+
detail.countryCode = shortName;
|
|
120
|
+
}
|
|
121
|
+
else if (types.includes('postal_code'))
|
|
122
|
+
detail.postalCode = longName;
|
|
123
|
+
}
|
|
124
|
+
return detail;
|
|
125
|
+
}
|
|
126
|
+
/** Resuelve una predicción a un `LocationStepValue` completo (fetchFields + parse). */
|
|
127
|
+
export async function resolvePrediction(prediction) {
|
|
128
|
+
const place = prediction.toPlace();
|
|
129
|
+
await place.fetchFields({ fields: ['displayName', 'formattedAddress', 'location', 'addressComponents'] });
|
|
130
|
+
return {
|
|
131
|
+
source: LocationInputMode.GOOGLE_PLACES,
|
|
132
|
+
latitude: place.location?.lat(),
|
|
133
|
+
longitude: place.location?.lng(),
|
|
134
|
+
name: place.displayName ?? prediction.label,
|
|
135
|
+
address: place.formattedAddress ?? undefined,
|
|
136
|
+
placeId: prediction.placeId,
|
|
137
|
+
addressDetail: parseAddressComponents(place.addressComponents),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CalendarValue } from '../../@Types/CalendarFormStep';
|
|
2
|
+
export interface ErkAvailabilityPickerProps {
|
|
3
|
+
value: CalendarValue | null;
|
|
4
|
+
onChange: (value: CalendarValue | null) => void;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string | null;
|
|
7
|
+
/** Hora mínima del grid (HH:mm). @default "07:00" */
|
|
8
|
+
minTime?: string;
|
|
9
|
+
/** Hora máxima del grid (HH:mm). @default "19:00" */
|
|
10
|
+
maxTime?: string;
|
|
11
|
+
/** Muestra sábado y domingo. @default true */
|
|
12
|
+
showWeekends?: boolean;
|
|
13
|
+
/** Zona horaria que se guarda junto a los eventos. */
|
|
14
|
+
timezone?: string;
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Picker compacto de disponibilidad para el paso CALENDAR/Availability del form:
|
|
20
|
+
* un grid semanal pequeño (día × hora) donde quien responde marca sus ventanas
|
|
21
|
+
* disponibles. Produce ventanas WORKING con recurrencia semanal (RRULE), las que
|
|
22
|
+
* un paso BOOKING puede usar como disponibilidad.
|
|
23
|
+
*/
|
|
24
|
+
declare function ErkAvailabilityPicker({ value, onChange, label, description, minTime, maxTime, showWeekends, timezone, readOnly, error, }: ErkAvailabilityPickerProps): JSX.Element;
|
|
25
|
+
export default ErkAvailabilityPicker;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { addDays, format, startOfWeek } from 'date-fns';
|
|
4
|
+
import { CalendarEventCategory } from '../../@Types/CalendarFormStep';
|
|
5
|
+
import styles from './ErkAvailabilityPicker.module.css';
|
|
6
|
+
const DOW_LABELS = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
|
|
7
|
+
const DOW_NAMES = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
|
|
8
|
+
function parseHour(hhmm) {
|
|
9
|
+
return Number(hhmm.split(':')[0]) || 0;
|
|
10
|
+
}
|
|
11
|
+
/** Ancla del día `dayIdx` (0=Lun … 6=Dom) en la semana actual. */
|
|
12
|
+
function anchorDate(dayIdx) {
|
|
13
|
+
return addDays(startOfWeek(new Date(), { weekStartsOn: 1 }), dayIdx);
|
|
14
|
+
}
|
|
15
|
+
function cellISO(dayIdx, hour) {
|
|
16
|
+
const day = anchorDate(dayIdx);
|
|
17
|
+
return `${format(day, 'yyyy-MM-dd')}T${String(hour).padStart(2, '0')}:00:00`;
|
|
18
|
+
}
|
|
19
|
+
/** `Date.getDay()` → índice de columna (0=Lun … 6=Dom). */
|
|
20
|
+
function dayToIdx(day) {
|
|
21
|
+
return (day + 6) % 7;
|
|
22
|
+
}
|
|
23
|
+
const RRULE_DAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
|
24
|
+
/**
|
|
25
|
+
* Expande el value a celdas marcadas. Soporta las ventanas que el propio picker
|
|
26
|
+
* genera (semanales por día) y BYDAY (ej: L–V) por si el valor viene de otra fuente.
|
|
27
|
+
*/
|
|
28
|
+
function valueToCells(value) {
|
|
29
|
+
const cells = new Set();
|
|
30
|
+
for (const event of value?.events ?? []) {
|
|
31
|
+
const startHour = parseHour(event.start.slice(11, 16));
|
|
32
|
+
const endHour = parseHour(event.end.slice(11, 16)) || 24;
|
|
33
|
+
const byday = /BYDAY=([A-Z,]+)/.exec(event.recurrence ?? '');
|
|
34
|
+
const dayIdxs = byday
|
|
35
|
+
? byday[1]
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((code) => RRULE_DAY_CODES.indexOf(code))
|
|
38
|
+
.filter((d) => d >= 0)
|
|
39
|
+
.map(dayToIdx)
|
|
40
|
+
: [dayToIdx(new Date(`${event.start.slice(0, 10)}T00:00:00`).getDay())];
|
|
41
|
+
for (const dayIdx of dayIdxs) {
|
|
42
|
+
for (let h = startHour; h < endHour; h++)
|
|
43
|
+
cells.add(`${dayIdx}:${h}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return cells;
|
|
47
|
+
}
|
|
48
|
+
/** Convierte las celdas marcadas en ventanas semanales contiguas por día. */
|
|
49
|
+
function cellsToEvents(cells, hours) {
|
|
50
|
+
const events = [];
|
|
51
|
+
for (let dayIdx = 0; dayIdx < 7; dayIdx++) {
|
|
52
|
+
let windowStart = null;
|
|
53
|
+
for (const h of hours) {
|
|
54
|
+
const marked = cells.has(`${dayIdx}:${h}`);
|
|
55
|
+
if (marked && windowStart === null)
|
|
56
|
+
windowStart = h;
|
|
57
|
+
if (!marked && windowStart !== null) {
|
|
58
|
+
events.push(windowEvent(dayIdx, windowStart, h));
|
|
59
|
+
windowStart = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (windowStart !== null) {
|
|
63
|
+
events.push(windowEvent(dayIdx, windowStart, hours[hours.length - 1] + 1));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return events;
|
|
67
|
+
}
|
|
68
|
+
function windowEvent(dayIdx, startHour, endHour) {
|
|
69
|
+
return {
|
|
70
|
+
id: `avail-${dayIdx}-${startHour}-${endHour}`,
|
|
71
|
+
category: CalendarEventCategory.WORKING,
|
|
72
|
+
start: cellISO(dayIdx, startHour),
|
|
73
|
+
end: cellISO(dayIdx, endHour),
|
|
74
|
+
recurrence: 'FREQ=WEEKLY',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Picker compacto de disponibilidad para el paso CALENDAR/Availability del form:
|
|
79
|
+
* un grid semanal pequeño (día × hora) donde quien responde marca sus ventanas
|
|
80
|
+
* disponibles. Produce ventanas WORKING con recurrencia semanal (RRULE), las que
|
|
81
|
+
* un paso BOOKING puede usar como disponibilidad.
|
|
82
|
+
*/
|
|
83
|
+
function ErkAvailabilityPicker({ value, onChange, label, description, minTime = '07:00', maxTime = '19:00', showWeekends = true, timezone, readOnly = false, error, }) {
|
|
84
|
+
const minHour = parseHour(minTime);
|
|
85
|
+
const maxHour = parseHour(maxTime);
|
|
86
|
+
const hours = useMemo(() => Array.from({ length: Math.max(maxHour - minHour, 1) }, (_, i) => minHour + i), [minHour, maxHour]);
|
|
87
|
+
const dayIdxs = showWeekends ? [0, 1, 2, 3, 4, 5, 6] : [0, 1, 2, 3, 4];
|
|
88
|
+
const cells = useMemo(() => valueToCells(value), [value]);
|
|
89
|
+
const toggle = (dayIdx, hour) => {
|
|
90
|
+
if (readOnly)
|
|
91
|
+
return;
|
|
92
|
+
const next = new Set(cells);
|
|
93
|
+
const key = `${dayIdx}:${hour}`;
|
|
94
|
+
if (next.has(key))
|
|
95
|
+
next.delete(key);
|
|
96
|
+
else
|
|
97
|
+
next.add(key);
|
|
98
|
+
onChange({
|
|
99
|
+
type: 'CALENDAR_VALUE',
|
|
100
|
+
events: cellsToEvents(next, hours),
|
|
101
|
+
timezone: value?.timezone ?? timezone,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const markedCount = cells.size;
|
|
105
|
+
return (_jsxs("div", { className: styles.container, children: [_jsx("span", { className: styles.label, children: label }), description && _jsx("p", { className: styles.description, children: description }), _jsxs("div", { className: styles.card, children: [_jsxs("div", { className: styles.grid, style: { gridTemplateColumns: `44px repeat(${dayIdxs.length}, 1fr)` }, children: [_jsx("span", { className: styles.corner }), dayIdxs.map((d) => (_jsx("span", { className: styles.dow, children: DOW_LABELS[d] }, d))), hours.map((h) => (_jsxs("div", { className: styles.row, children: [_jsxs("span", { className: styles.hour, children: [h, ":00"] }), dayIdxs.map((d) => {
|
|
106
|
+
const on = cells.has(`${d}:${h}`);
|
|
107
|
+
return (_jsx("button", { type: "button", disabled: readOnly, "aria-pressed": on, "aria-label": `${DOW_NAMES[d]} ${h}:00`, className: on ? `${styles.cell} ${styles.cellOn}` : styles.cell, onClick: () => toggle(d, h) }, d));
|
|
108
|
+
})] }, h)))] }), _jsx("div", { className: styles.footer, children: markedCount > 0
|
|
109
|
+
? 'Disponible cada semana en las horas marcadas.'
|
|
110
|
+
: 'Marca las horas en las que estás disponible cada semana.' })] }), error && _jsx("div", { className: styles.err, children: error })] }));
|
|
111
|
+
}
|
|
112
|
+
export default ErkAvailabilityPicker;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
max-width: 100%;
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 4px;
|
|
7
|
+
}
|
|
8
|
+
.label {
|
|
9
|
+
font-size: 14.5px;
|
|
10
|
+
font-weight: 700;
|
|
11
|
+
color: var(--eureka-text, #293241);
|
|
12
|
+
}
|
|
13
|
+
.description {
|
|
14
|
+
margin: 0 0 4px;
|
|
15
|
+
font-size: 12.5px;
|
|
16
|
+
color: #787878;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.card {
|
|
20
|
+
border: 1px solid var(--eureka-outline, #d9dee6);
|
|
21
|
+
border-radius: 12px;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
background: var(--eureka-bg, #fff);
|
|
24
|
+
max-width: 460px;
|
|
25
|
+
}
|
|
26
|
+
.grid {
|
|
27
|
+
display: grid;
|
|
28
|
+
padding: 10px 12px 4px;
|
|
29
|
+
gap: 2px;
|
|
30
|
+
font-variant-numeric: tabular-nums;
|
|
31
|
+
}
|
|
32
|
+
.corner {
|
|
33
|
+
grid-column: 1;
|
|
34
|
+
}
|
|
35
|
+
.dow {
|
|
36
|
+
text-align: center;
|
|
37
|
+
font-size: 10.5px;
|
|
38
|
+
font-weight: 700;
|
|
39
|
+
color: #787878;
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
padding-bottom: 2px;
|
|
42
|
+
}
|
|
43
|
+
.row {
|
|
44
|
+
display: contents;
|
|
45
|
+
}
|
|
46
|
+
.hour {
|
|
47
|
+
font-size: 10.5px;
|
|
48
|
+
color: #787878;
|
|
49
|
+
text-align: right;
|
|
50
|
+
padding-right: 6px;
|
|
51
|
+
align-self: center;
|
|
52
|
+
}
|
|
53
|
+
.cell {
|
|
54
|
+
height: 26px;
|
|
55
|
+
border: 1px solid var(--eureka-outline, #eceff3);
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
background: #fbfcfd;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
padding: 0;
|
|
60
|
+
}
|
|
61
|
+
.cell:hover {
|
|
62
|
+
border-color: var(--eureka-primary, #3d5a7f);
|
|
63
|
+
}
|
|
64
|
+
.cell:disabled {
|
|
65
|
+
cursor: default;
|
|
66
|
+
}
|
|
67
|
+
.cellOn {
|
|
68
|
+
background: repeating-linear-gradient(
|
|
69
|
+
-45deg,
|
|
70
|
+
color-mix(in srgb, var(--eureka-primary, #3d5a7f) 30%, #fff),
|
|
71
|
+
color-mix(in srgb, var(--eureka-primary, #3d5a7f) 30%, #fff) 5px,
|
|
72
|
+
color-mix(in srgb, var(--eureka-primary, #3d5a7f) 18%, #fff) 5px,
|
|
73
|
+
color-mix(in srgb, var(--eureka-primary, #3d5a7f) 18%, #fff) 10px
|
|
74
|
+
);
|
|
75
|
+
border-color: var(--eureka-primary, #3d5a7f);
|
|
76
|
+
}
|
|
77
|
+
.footer {
|
|
78
|
+
padding: 8px 12px 10px;
|
|
79
|
+
font-size: 11.5px;
|
|
80
|
+
color: #787878;
|
|
81
|
+
border-top: 1px solid var(--eureka-outline, #eceff3);
|
|
82
|
+
background: color-mix(in srgb, var(--eureka-primary, #3d5a7f) 2%, #fff);
|
|
83
|
+
}
|
|
84
|
+
.err {
|
|
85
|
+
font-size: 12px;
|
|
86
|
+
color: #d0473f;
|
|
87
|
+
margin-top: 2px;
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,73 @@
|
|
|
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 { addDays, format, startOfWeek } from 'date-fns';
|
|
7
|
+
import ErkAvailabilityPicker from './ErkAvailabilityPicker';
|
|
8
|
+
import MaterialProviders from '../../Utils/MaterialProviders';
|
|
9
|
+
import InternalFormStyle from '../../constants/InternalFormStyle';
|
|
10
|
+
import { CalendarEventCategory } from '../../@Types/CalendarFormStep';
|
|
11
|
+
function iso(dayIdx, hhmm) {
|
|
12
|
+
const day = addDays(startOfWeek(new Date(), { weekStartsOn: 1 }), dayIdx);
|
|
13
|
+
return `${format(day, 'yyyy-MM-dd')}T${hhmm}:00`;
|
|
14
|
+
}
|
|
15
|
+
describe('ErkAvailabilityPicker', () => {
|
|
16
|
+
it('marks a cell and emits a weekly WORKING window', async () => {
|
|
17
|
+
const user = userEvent.setup();
|
|
18
|
+
const onChange = vi.fn();
|
|
19
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkAvailabilityPicker, { value: null, onChange: onChange, label: "Disponibilidad", minTime: "08:00", maxTime: "12:00" }) }));
|
|
20
|
+
await user.click(screen.getByRole('button', { name: 'Lunes 9:00' }));
|
|
21
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
22
|
+
type: 'CALENDAR_VALUE',
|
|
23
|
+
events: [
|
|
24
|
+
expect.objectContaining({
|
|
25
|
+
category: CalendarEventCategory.WORKING,
|
|
26
|
+
start: iso(0, '09:00'),
|
|
27
|
+
end: iso(0, '10:00'),
|
|
28
|
+
recurrence: 'FREQ=WEEKLY',
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
timezone: undefined,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('merges contiguous marked hours into a single window', async () => {
|
|
35
|
+
const user = userEvent.setup();
|
|
36
|
+
const onChange = vi.fn();
|
|
37
|
+
const value = {
|
|
38
|
+
type: 'CALENDAR_VALUE',
|
|
39
|
+
events: [
|
|
40
|
+
{
|
|
41
|
+
id: 'avail-0-9-10',
|
|
42
|
+
category: CalendarEventCategory.WORKING,
|
|
43
|
+
start: iso(0, '09:00'),
|
|
44
|
+
end: iso(0, '10:00'),
|
|
45
|
+
recurrence: 'FREQ=WEEKLY',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkAvailabilityPicker, { value: value, onChange: onChange, label: "Disponibilidad", minTime: "08:00", maxTime: "12:00" }) }));
|
|
50
|
+
await user.click(screen.getByRole('button', { name: 'Lunes 10:00' }));
|
|
51
|
+
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({
|
|
52
|
+
events: [expect.objectContaining({ start: iso(0, '09:00'), end: iso(0, '11:00') })],
|
|
53
|
+
}));
|
|
54
|
+
});
|
|
55
|
+
it('renders existing windows as pressed cells and blocks toggling when readOnly', () => {
|
|
56
|
+
const value = {
|
|
57
|
+
type: 'CALENDAR_VALUE',
|
|
58
|
+
events: [
|
|
59
|
+
{
|
|
60
|
+
id: 'avail-2-14-16',
|
|
61
|
+
category: CalendarEventCategory.WORKING,
|
|
62
|
+
start: iso(2, '14:00'),
|
|
63
|
+
end: iso(2, '16:00'),
|
|
64
|
+
recurrence: 'FREQ=WEEKLY',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkAvailabilityPicker, { value: value, onChange: () => { }, label: "Disponibilidad", minTime: "13:00", maxTime: "17:00", readOnly: true }) }));
|
|
69
|
+
expect(screen.getByRole('button', { name: 'Miércoles 14:00', pressed: true })).toBeDisabled();
|
|
70
|
+
expect(screen.getByRole('button', { name: 'Miércoles 15:00', pressed: true })).toBeInTheDocument();
|
|
71
|
+
expect(screen.getByRole('button', { name: 'Miércoles 16:00', pressed: false })).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
}
|
|
18
|
+
export interface ErkBookingPickerProps {
|
|
19
|
+
value: BookingValue | null;
|
|
20
|
+
onChange: (value: BookingValue | null) => void;
|
|
21
|
+
label: string;
|
|
22
|
+
description?: string | null;
|
|
23
|
+
durationMinutes: number;
|
|
24
|
+
bookingMode?: BookingMode;
|
|
25
|
+
temporaryLockMinutes?: number;
|
|
26
|
+
personSelectionMode?: PersonSelectionMode;
|
|
27
|
+
persons?: BookablePerson[];
|
|
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). */
|
|
55
|
+
fetchSlots?: (args: {
|
|
56
|
+
personId?: string;
|
|
57
|
+
date: string;
|
|
58
|
+
eventTemplate?: string;
|
|
59
|
+
}) => Promise<BookingSlot[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Los días (`YYYY-MM-DD`) con algún horario para la persona elegida. Sin él, cualquier día desde
|
|
62
|
+
* hoy es elegible.
|
|
63
|
+
*/
|
|
64
|
+
fetchDays?: (args: {
|
|
65
|
+
personId?: string;
|
|
66
|
+
eventTemplate?: string;
|
|
67
|
+
}) => Promise<string[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Retira el slot de la oferta al confirmar. Lo que devuelve se guarda en el valor junto con la
|
|
70
|
+
* selección; si rechaza, el wizard vuelve a la hora con el motivo y recarga los horarios.
|
|
71
|
+
*/
|
|
72
|
+
holdSlot?: (args: {
|
|
73
|
+
slot: BookingSlot;
|
|
74
|
+
personId?: string;
|
|
75
|
+
eventTemplate?: string;
|
|
76
|
+
contact: {
|
|
77
|
+
name: string;
|
|
78
|
+
email?: string;
|
|
79
|
+
};
|
|
80
|
+
}) => Promise<Partial<BookingValue>>;
|
|
81
|
+
/** Deshabilita la tarjeta de entrada mientras el host termina de cargar la oferta. */
|
|
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>;
|
|
87
|
+
readOnly?: boolean;
|
|
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;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Selector de cita: una tarjeta de entrada ("Agendar cita") abre el wizard en un modal —
|
|
98
|
+
* una decisión por pantalla (persona → día → hora → confirmar), con panel izquierdo de
|
|
99
|
+
* resumen que se va llenando, breadcrumb navegable arriba y barra inferior Atrás / Siguiente.
|
|
100
|
+
* En el paso final el panel derecho desaparece y el resumen se expande en una columna
|
|
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.
|
|
103
|
+
*/
|
|
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;
|
|
105
|
+
export default ErkBookingPicker;
|