@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,82 @@
|
|
|
1
|
+
.input {
|
|
2
|
+
width: 100%;
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
height: 40px;
|
|
5
|
+
padding: 6px 12px;
|
|
6
|
+
font-size: 1rem;
|
|
7
|
+
color: var(--eureka-text, #333);
|
|
8
|
+
background-color: var(--eureka-bg, #fff);
|
|
9
|
+
border: 1px solid var(--eureka-outline, #ccc);
|
|
10
|
+
border-radius: 10px;
|
|
11
|
+
outline: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.input:hover:not(:disabled) {
|
|
15
|
+
border-color: var(--eureka-primary, #1976d2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.input:focus {
|
|
19
|
+
border-color: var(--eureka-primary, #1976d2);
|
|
20
|
+
border-width: 2px;
|
|
21
|
+
padding: 5px 11px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.input:disabled {
|
|
25
|
+
opacity: 0.5;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.inputError {
|
|
29
|
+
border-color: var(--eureka-error, #e53935);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.positioner {
|
|
33
|
+
z-index: 1400;
|
|
34
|
+
width: var(--anchor-width);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.popup {
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
max-height: 320px;
|
|
40
|
+
overflow-y: auto;
|
|
41
|
+
padding: 4px;
|
|
42
|
+
background-color: var(--eureka-bg, #fff);
|
|
43
|
+
border: 1px solid var(--eureka-outline, #ccc);
|
|
44
|
+
border-radius: 10px;
|
|
45
|
+
box-shadow: 0 4px 16px rgb(0 0 0 / 12%);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.list {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.status {
|
|
54
|
+
padding: 10px 12px;
|
|
55
|
+
font-size: 0.875rem;
|
|
56
|
+
color: var(--eureka-text, #666);
|
|
57
|
+
opacity: 0.7;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.item {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 2px;
|
|
64
|
+
padding: 8px 12px;
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.item[data-highlighted],
|
|
70
|
+
.item:hover {
|
|
71
|
+
background-color: var(--eureka-primary, #1976d2);
|
|
72
|
+
color: #fff;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.optionLabel {
|
|
76
|
+
font-size: 0.9375rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.optionDescription {
|
|
80
|
+
font-size: 0.8125rem;
|
|
81
|
+
opacity: 0.7;
|
|
82
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LocationAddressDetail, LocationStepValue } from '../../@Types/LocationFormStep';
|
|
2
|
+
/** Asegura el script cargado y las clases de Places listas. Idempotente. */
|
|
3
|
+
export declare function loadPlaces(apiKey: string): Promise<void>;
|
|
4
|
+
/** Token de sesión nuevo (mejora el billing/relevancia de Google). */
|
|
5
|
+
export declare function newSessionToken(): any;
|
|
6
|
+
/** Reverse-geocoding para el modo GPS: coordenadas → dirección estructurada. */
|
|
7
|
+
export declare function reverseGeocode(apiKey: string, lat: number, lng: number): Promise<{
|
|
8
|
+
address?: string;
|
|
9
|
+
addressDetail: LocationAddressDetail;
|
|
10
|
+
}>;
|
|
11
|
+
export interface AddressPrediction {
|
|
12
|
+
placeId: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description: string;
|
|
15
|
+
toPlace: () => any;
|
|
16
|
+
}
|
|
17
|
+
export interface FetchSuggestionsOptions {
|
|
18
|
+
sessionToken?: any;
|
|
19
|
+
/** ISO-2, ej. ['co'] */
|
|
20
|
+
regionCodes?: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function fetchAddressSuggestions(input: string, options?: FetchSuggestionsOptions): Promise<AddressPrediction[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Descompone los `addressComponents` de un Place en el shape estructurado del step.
|
|
25
|
+
* Acepta el formato nuevo (`longText`/`shortText`) y el legacy (`long_name`/`short_name`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseAddressComponents(components: any[] | null | undefined): LocationAddressDetail;
|
|
28
|
+
/** Resuelve una predicción a un `LocationStepValue` completo (fetchFields + parse). */
|
|
29
|
+
export declare function resolvePrediction(prediction: AddressPrediction): Promise<LocationStepValue>;
|
|
@@ -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,31 @@
|
|
|
1
|
+
import { BookablePerson, BookingMode, BookingSlot, BookingValue, PersonSelectionMode } from '../../@Types/BookingFormStep';
|
|
2
|
+
export interface ErkBookingPickerProps {
|
|
3
|
+
value: BookingValue | null;
|
|
4
|
+
onChange: (value: BookingValue | null) => void;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string | null;
|
|
7
|
+
durationMinutes: number;
|
|
8
|
+
bufferMinutes?: number;
|
|
9
|
+
bookingMode?: BookingMode;
|
|
10
|
+
temporaryLockMinutes?: number;
|
|
11
|
+
personSelectionMode?: PersonSelectionMode;
|
|
12
|
+
persons?: BookablePerson[];
|
|
13
|
+
/** Recopila nombre/correo del que agenda. */
|
|
14
|
+
collectContact?: boolean;
|
|
15
|
+
/** Provee los slots disponibles para una persona + día. */
|
|
16
|
+
fetchSlots?: (args: {
|
|
17
|
+
personId?: string;
|
|
18
|
+
date: string;
|
|
19
|
+
}) => Promise<BookingSlot[]>;
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Selector de cita: una tarjeta de entrada ("Agendar cita") abre el wizard en un modal —
|
|
25
|
+
* una decisión por pantalla (persona → día → hora → confirmar), con panel izquierdo de
|
|
26
|
+
* resumen que se va llenando, breadcrumb navegable arriba y barra inferior Atrás / Siguiente.
|
|
27
|
+
* En el paso final el panel derecho desaparece y el resumen se expande en una columna
|
|
28
|
+
* centrada (cross-fade corto). Confirmar cierra el modal y deja el resumen compacto.
|
|
29
|
+
*/
|
|
30
|
+
declare function ErkBookingPicker({ value, onChange, label, description, durationMinutes, bufferMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, collectContact, fetchSlots, readOnly, error, }: ErkBookingPickerProps): JSX.Element;
|
|
31
|
+
export default ErkBookingPicker;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Dialog } from '@mui/material';
|
|
4
|
+
import { addDays, addMonths, format, isBefore, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek, } from 'date-fns';
|
|
5
|
+
import { es } from 'date-fns/locale';
|
|
6
|
+
import { BookingMode, PersonSelectionMode, } from '../../@Types/BookingFormStep';
|
|
7
|
+
import ErkButton from '../ErkButton/ErkButton';
|
|
8
|
+
import ErkTextField from '../ErkTextField/ErkTextField';
|
|
9
|
+
import styles from './ErkBookingPicker.module.css';
|
|
10
|
+
const WEEK_STARTS_ON = 1;
|
|
11
|
+
const DOW = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
|
|
12
|
+
const MODE_LABEL = {
|
|
13
|
+
[BookingMode.IN_PERSON]: 'Presencial',
|
|
14
|
+
[BookingMode.VIRTUAL]: 'Virtual',
|
|
15
|
+
[BookingMode.HYBRID]: 'Híbrida',
|
|
16
|
+
};
|
|
17
|
+
const STEP_LABEL = {
|
|
18
|
+
person: 'Persona',
|
|
19
|
+
day: 'Día',
|
|
20
|
+
hour: 'Hora',
|
|
21
|
+
confirm: 'Confirmar',
|
|
22
|
+
};
|
|
23
|
+
const STEP_HINT = {
|
|
24
|
+
person: 'Elige con quién agendar',
|
|
25
|
+
day: 'Elige un día',
|
|
26
|
+
hour: 'Elige una hora libre',
|
|
27
|
+
confirm: 'Confirma tu cita',
|
|
28
|
+
};
|
|
29
|
+
/** Duración del fade al reestructurarse el layout (entrar/salir del paso final). */
|
|
30
|
+
const XFADE_MS = 160;
|
|
31
|
+
/** El CTA de confirmar usa el acento del form (secondaryContrastColor vía --booking-accent). */
|
|
32
|
+
const ACCENT_SX = {
|
|
33
|
+
backgroundColor: 'var(--booking-accent, #ee6c4d)',
|
|
34
|
+
'&:hover': { backgroundColor: 'var(--booking-accent, #ee6c4d)', filter: 'brightness(0.95)' },
|
|
35
|
+
'&:disabled': { backgroundColor: 'var(--booking-accent, #ee6c4d)', color: '#fff' },
|
|
36
|
+
};
|
|
37
|
+
function fmtDate(date) {
|
|
38
|
+
return format(date, 'yyyy-MM-dd');
|
|
39
|
+
}
|
|
40
|
+
function longDate(date) {
|
|
41
|
+
return format(date, 'EEEE d MMM', { locale: es });
|
|
42
|
+
}
|
|
43
|
+
function initials(name) {
|
|
44
|
+
return name
|
|
45
|
+
.split(' ')
|
|
46
|
+
.map((p) => p[0])
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.slice(0, 2)
|
|
49
|
+
.join('')
|
|
50
|
+
.toUpperCase();
|
|
51
|
+
}
|
|
52
|
+
function capitalize(text) {
|
|
53
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Selector de cita: una tarjeta de entrada ("Agendar cita") abre el wizard en un modal —
|
|
57
|
+
* una decisión por pantalla (persona → día → hora → confirmar), con panel izquierdo de
|
|
58
|
+
* resumen que se va llenando, breadcrumb navegable arriba y barra inferior Atrás / Siguiente.
|
|
59
|
+
* En el paso final el panel derecho desaparece y el resumen se expande en una columna
|
|
60
|
+
* centrada (cross-fade corto). Confirmar cierra el modal y deja el resumen compacto.
|
|
61
|
+
*/
|
|
62
|
+
function ErkBookingPicker({ value, onChange, label, description, durationMinutes, bufferMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, collectContact = false, fetchSlots, readOnly = false, error, }) {
|
|
63
|
+
const needsPerson = !!personSelectionMode && personSelectionMode !== PersonSelectionMode.NONE;
|
|
64
|
+
const personsOptional = personSelectionMode === PersonSelectionMode.OPTIONAL;
|
|
65
|
+
const today = startOfDay(new Date());
|
|
66
|
+
const wizardKeys = useMemo(() => (needsPerson ? ['person', 'day', 'hour', 'confirm'] : ['day', 'hour', 'confirm']), [needsPerson]);
|
|
67
|
+
const finalIdx = wizardKeys.length - 1;
|
|
68
|
+
const [open, setOpen] = useState(false);
|
|
69
|
+
const [wizStep, setWizStep] = useState(0);
|
|
70
|
+
const [xfade, setXfade] = useState(false);
|
|
71
|
+
const [person, setPerson] = useState(undefined);
|
|
72
|
+
const [personTouched, setPersonTouched] = useState(false);
|
|
73
|
+
const [date, setDate] = useState(undefined);
|
|
74
|
+
const [viewMonth, setViewMonth] = useState(() => startOfMonth(today));
|
|
75
|
+
const [slot, setSlot] = useState(undefined);
|
|
76
|
+
const [slots, setSlots] = useState([]);
|
|
77
|
+
const [loading, setLoading] = useState(false);
|
|
78
|
+
const [contactName, setContactName] = useState('');
|
|
79
|
+
const [contactEmail, setContactEmail] = useState('');
|
|
80
|
+
const reduceMotion = useMemo(() => typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches, []);
|
|
81
|
+
const monthCells = useMemo(() => {
|
|
82
|
+
const first = startOfWeek(startOfMonth(viewMonth), { weekStartsOn: WEEK_STARTS_ON });
|
|
83
|
+
return Array.from({ length: 42 }, (_, i) => addDays(first, i));
|
|
84
|
+
}, [viewMonth]);
|
|
85
|
+
const loadSlots = useCallback(async (day, personId) => {
|
|
86
|
+
if (!fetchSlots)
|
|
87
|
+
return;
|
|
88
|
+
setLoading(true);
|
|
89
|
+
setSlots([]);
|
|
90
|
+
try {
|
|
91
|
+
setSlots(await fetchSlots({ personId, date: fmtDate(day) }));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
setSlots([]);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
setLoading(false);
|
|
98
|
+
}
|
|
99
|
+
}, [fetchSlots]);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (value || !date)
|
|
102
|
+
return;
|
|
103
|
+
void loadSlots(date, person?.id);
|
|
104
|
+
}, [value, date, person, loadSlots]);
|
|
105
|
+
// Al entrar/salir del paso final el layout se reestructura; el reacomodo
|
|
106
|
+
// ocurre mientras la tarjeta está invisible (fade corto en dos fases).
|
|
107
|
+
const goToStep = (target) => {
|
|
108
|
+
const crossesFinal = (wizStep === finalIdx) !== (target === finalIdx);
|
|
109
|
+
if (!crossesFinal || reduceMotion) {
|
|
110
|
+
setWizStep(target);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
setXfade(true);
|
|
114
|
+
window.setTimeout(() => {
|
|
115
|
+
setWizStep(target);
|
|
116
|
+
requestAnimationFrame(() => requestAnimationFrame(() => setXfade(false)));
|
|
117
|
+
}, XFADE_MS);
|
|
118
|
+
};
|
|
119
|
+
const currentKey = wizardKeys[wizStep];
|
|
120
|
+
const canAdvance = (() => {
|
|
121
|
+
switch (currentKey) {
|
|
122
|
+
case 'person':
|
|
123
|
+
return personsOptional ? personTouched : !!person;
|
|
124
|
+
case 'day':
|
|
125
|
+
return !!date;
|
|
126
|
+
case 'hour':
|
|
127
|
+
return !!slot;
|
|
128
|
+
default:
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
const confirm = () => {
|
|
133
|
+
if (!slot || !date)
|
|
134
|
+
return;
|
|
135
|
+
setOpen(false);
|
|
136
|
+
onChange({
|
|
137
|
+
type: 'BOOKING_VALUE',
|
|
138
|
+
slotId: slot.id,
|
|
139
|
+
date: fmtDate(date),
|
|
140
|
+
startTime: slot.startTime,
|
|
141
|
+
endTime: slot.endTime,
|
|
142
|
+
personId: person?.id,
|
|
143
|
+
personName: person?.name,
|
|
144
|
+
contactName: contactName || undefined,
|
|
145
|
+
contactEmail: contactEmail || undefined,
|
|
146
|
+
status: 'PENDING',
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
const reset = () => {
|
|
150
|
+
onChange(null);
|
|
151
|
+
setWizStep(0);
|
|
152
|
+
setPerson(undefined);
|
|
153
|
+
setPersonTouched(false);
|
|
154
|
+
setDate(undefined);
|
|
155
|
+
setSlot(undefined);
|
|
156
|
+
setContactName('');
|
|
157
|
+
setContactEmail('');
|
|
158
|
+
};
|
|
159
|
+
const pickPerson = (p) => {
|
|
160
|
+
setPerson(p);
|
|
161
|
+
setPersonTouched(true);
|
|
162
|
+
setSlot(undefined);
|
|
163
|
+
};
|
|
164
|
+
// Read-only / already booked → compact summary with the same card aesthetic
|
|
165
|
+
if (readOnly || value) {
|
|
166
|
+
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
|
|
167
|
+
? [
|
|
168
|
+
value.personName,
|
|
169
|
+
capitalize(longDate(new Date(value.date + 'T00:00:00'))),
|
|
170
|
+
`${value.startTime}–${value.endTime}`,
|
|
171
|
+
]
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.join(' · ')
|
|
174
|
+
: 'Sin cita' })] }), value && !readOnly && _jsx(ErkButton, { text: "Cambiar", variant: "outlined", onClick: reset })] }) }));
|
|
175
|
+
}
|
|
176
|
+
const isFinal = wizStep === finalIdx;
|
|
177
|
+
const personLabel = person?.name ?? (personTouched && personsOptional ? 'Cualquiera' : undefined);
|
|
178
|
+
const dayText = date ? capitalize(longDate(date)) : undefined;
|
|
179
|
+
const hourText = slot ? `${slot.startTime} – ${slot.endTime}` : undefined;
|
|
180
|
+
const summaryRows = [
|
|
181
|
+
...(needsPerson ? [{ key: 'person', placeholder: 'Persona', text: personLabel }] : []),
|
|
182
|
+
{ key: 'day', placeholder: 'Día', text: dayText },
|
|
183
|
+
{ key: 'hour', placeholder: 'Hora', text: hourText },
|
|
184
|
+
];
|
|
185
|
+
const navParts = [];
|
|
186
|
+
if (personLabel)
|
|
187
|
+
navParts.push(_jsx("b", { children: personLabel }, "p"));
|
|
188
|
+
if (dayText)
|
|
189
|
+
navParts.push(_jsx("span", { children: dayText }, "d"));
|
|
190
|
+
if (hourText)
|
|
191
|
+
navParts.push(_jsx("span", { className: styles.navHl, children: hourText }, "h"));
|
|
192
|
+
return (_jsxs("div", { className: styles.container, children: [_jsxs("div", { className: styles.entry, children: [_jsx("div", { className: styles.avatar, children: "\uD83D\uDCC5" }), _jsxs("div", { className: styles.entryInfo, children: [_jsx("span", { className: styles.entryLabel, children: label }), _jsx("span", { className: styles.entrySub, children: description ?? 'Agenda tu cita' })] }), _jsx(ErkButton, { text: "Agendar cita", onClick: () => setOpen(true) })] }), _jsx(Dialog, { open: open, onClose: () => setOpen(false), maxWidth: "md", fullWidth: true, slotProps: { paper: { style: { borderRadius: 16, background: 'transparent', boxShadow: 'none' } } }, children: _jsxs("div", { className: `${styles.card}${isFinal ? ` ${styles.cardFinal}` : ''}${xfade ? ` ${styles.xfade}` : ''}`, children: [_jsxs("div", { className: isFinal ? `${styles.book} ${styles.bookFinal}` : styles.book, children: [_jsxs("aside", { className: styles.who, children: [_jsxs("div", { className: styles.brand, children: [_jsx("div", { className: styles.avatar, children: person ? initials(person.name) : '📅' }), _jsxs("div", { children: [_jsx("h2", { className: styles.brandName, children: person?.name ?? label }), _jsx("p", { className: styles.brandRole, children: person?.role ?? description ?? 'Agenda tu cita' })] })] }), _jsxs("div", { className: styles.meta, children: [_jsxs("div", { className: styles.row, children: [_jsx("span", { className: styles.ic, children: "\u25F7" }), durationMinutes, " min", bufferMinutes ? ` · buffer ${bufferMinutes} min` : ''] }), bookingMode && (_jsxs("div", { className: styles.row, children: [_jsx("span", { className: styles.ic, children: "\u25F1" }), _jsx("span", { className: styles.pill, children: MODE_LABEL[bookingMode] })] }))] }), _jsx("div", { className: styles.sum, "aria-live": "polite", children: summaryRows.map((r) => (_jsxs("div", { className: r.text ? `${styles.srow} ${styles.srowOn}` : styles.srow, children: [_jsx("span", { className: styles.dot, children: "\u2713" }), _jsx("span", { children: r.text ?? r.placeholder })] }, r.key))) }), isFinal && collectContact && (_jsxs("div", { className: styles.cfields, children: [_jsx(ErkTextField, { label: "Nombre", value: contactName, onChange: setContactName }), _jsx(ErkTextField, { label: "Correo", value: contactEmail, onChange: setContactEmail })] })), !!temporaryLockMinutes && (_jsxs("div", { className: styles.lock, children: ["\u23F3 Reservaremos el horario ", _jsxs("b", { children: [temporaryLockMinutes, " min"] }), " mientras confirmas."] }))] }), _jsxs("div", { className: styles.right, children: [_jsx("nav", { className: styles.stepper, "aria-label": "Progreso", children: wizardKeys.map((key, i) => (_jsxs("span", { className: styles.stepGroup, children: [_jsxs("button", { type: "button", disabled: i >= wizStep, onClick: () => goToStep(i), className: i < wizStep
|
|
193
|
+
? `${styles.stepItem} ${styles.stepDone}`
|
|
194
|
+
: i === wizStep
|
|
195
|
+
? `${styles.stepItem} ${styles.stepOn}`
|
|
196
|
+
: styles.stepItem, children: [_jsx("span", { className: styles.stepNum, children: i < wizStep ? '✓' : i + 1 }), STEP_LABEL[key]] }), i < wizardKeys.length - 1 && _jsx("span", { className: styles.stepSep })] }, key))) }), currentKey === 'person' && (_jsxs("section", { className: styles.pane, children: [_jsx("h3", { className: styles.q, children: "\u00BFCon qui\u00E9n quieres agendar?" }), _jsx("p", { className: styles.qsub, children: personsOptional
|
|
197
|
+
? 'Elige una persona o deja que el sistema asigne a quien esté disponible.'
|
|
198
|
+
: '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 ? `${styles.pcard} ${styles.pcardOn}` : styles.pcard, onClick: () => pickPerson(p), children: [_jsx("span", { className: styles.pav, children: initials(p.name) }), _jsxs("span", { className: styles.pinfo, children: [_jsx("span", { className: styles.pn, children: p.name }), p.role && _jsx("span", { className: styles.pr, children: p.role })] }), _jsx("span", { className: styles.chk, children: "\u2713" })] }, p.id))), personsOptional && (_jsxs("button", { type: "button", className: personTouched && !person
|
|
199
|
+
? `${styles.pcard} ${styles.pcardAny} ${styles.pcardOn}`
|
|
200
|
+
: `${styles.pcard} ${styles.pcardAny}`, onClick: () => pickPerson(undefined), children: [_jsx("span", { className: styles.pav, children: "\u2726" }), _jsxs("span", { className: styles.pinfo, children: [_jsx("span", { className: styles.pn, children: "Cualquiera" }), _jsx("span", { className: styles.pr, children: "El sistema asigna" })] }), _jsx("span", { className: styles.chk, children: "\u2713" })] }))] })] }, "person")), currentKey === 'day' && (_jsxs("section", { className: styles.pane, children: [_jsx("h3", { className: styles.q, children: "Elige un d\u00EDa" }), _jsx("p", { className: styles.qsub, children: "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) => {
|
|
201
|
+
const outside = !isSameMonth(d, viewMonth);
|
|
202
|
+
const past = isBefore(d, today);
|
|
203
|
+
const selectable = !outside && !past;
|
|
204
|
+
const cls = date && isSameDay(d, date)
|
|
205
|
+
? `${styles.mday} ${styles.mdaySel}`
|
|
206
|
+
: selectable
|
|
207
|
+
? `${styles.mday} ${styles.mdayFree}`
|
|
208
|
+
: `${styles.mday} ${styles.mdayNone}`;
|
|
209
|
+
return (_jsx("button", { type: "button", disabled: !selectable, className: cls, onClick: () => {
|
|
210
|
+
setDate(d);
|
|
211
|
+
setSlot(undefined);
|
|
212
|
+
}, children: format(d, 'd') }, d.toISOString()));
|
|
213
|
+
})] })] })] }, "day")), currentKey === 'hour' && (_jsxs("section", { className: styles.pane, children: [_jsx("h3", { className: styles.q, children: "Elige una hora" }), _jsxs("p", { className: styles.qsub, children: [dayText, " \u00B7 citas de ", durationMinutes, " min"] }), loading ? (_jsx("div", { className: styles.empty, children: "Buscando horarios\u2026" })) : slots.length === 0 ? (_jsx("div", { className: styles.empty, children: "No hay horarios disponibles ese d\u00EDa." })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.slots, children: slots.map((s) => (_jsx("button", { type: "button", disabled: !s.available, className: slot?.id === s.id
|
|
214
|
+
? `${styles.slot} ${styles.slotSel}`
|
|
215
|
+
: s.available
|
|
216
|
+
? styles.slot
|
|
217
|
+
: `${styles.slot} ${styles.slotTaken}`, onClick: () => s.available && setSlot(s), children: s.startTime }, s.id))) }), _jsx("p", { className: styles.takenNote, children: "Tachados = ocupados por otro evento o cita." })] }))] }, "hour")), currentKey === 'confirm' && (_jsxs("section", { className: `${styles.pane} ${styles.paneFinal}`, children: [_jsx("h3", { className: styles.q, children: "Revisa y confirma tu cita" }), _jsx("p", { className: styles.qsub, children: collectContact
|
|
218
|
+
? 'Si todo está bien, completa tus datos y confirma.'
|
|
219
|
+
: 'Si todo está bien, confirma tu cita.' })] }, "confirm"))] })] }), _jsxs("div", { className: styles.navbar, children: [_jsx("div", { className: styles.navSummary, children: isFinal || !canAdvance || navParts.length === 0
|
|
220
|
+
? STEP_HINT[currentKey]
|
|
221
|
+
: 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: "Confirmar cita", sx: ACCENT_SX, disabled: !slot, onClick: confirm })) : (_jsx(ErkButton, { text: "Siguiente", disabled: !canAdvance, onClick: () => goToStep(wizStep + 1) }))] })] }) }), error && _jsx("div", { className: styles.err, children: error })] }));
|
|
222
|
+
}
|
|
223
|
+
export default ErkBookingPicker;
|