@arquimedes.co/eureka-forms 3.0.68-test → 3.0.69

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.
Files changed (138) hide show
  1. package/README +33 -1
  2. package/dist/@Types/AvailabilityFormStep.d.ts +48 -0
  3. package/dist/@Types/AvailabilityFormStep.js +24 -0
  4. package/dist/@Types/BankAccountFormStep.d.ts +67 -0
  5. package/dist/@Types/BankAccountFormStep.js +14 -0
  6. package/dist/@Types/BookingFormStep.d.ts +197 -0
  7. package/dist/@Types/BookingFormStep.js +44 -0
  8. package/dist/@Types/CalendarFormStep.d.ts +209 -0
  9. package/dist/@Types/CalendarFormStep.js +55 -0
  10. package/dist/@Types/CommunicationChannelFormStep.d.ts +51 -0
  11. package/dist/@Types/CommunicationChannelFormStep.js +15 -0
  12. package/dist/@Types/ConsentFormStep.d.ts +36 -0
  13. package/dist/@Types/ConsentFormStep.js +8 -0
  14. package/dist/@Types/EntityFormFormStep.d.ts +66 -0
  15. package/dist/@Types/EntityFormFormStep.js +20 -0
  16. package/dist/@Types/EventFormStep.d.ts +52 -0
  17. package/dist/@Types/EventFormStep.js +13 -0
  18. package/dist/@Types/ExternalReferenceFormStep.d.ts +74 -0
  19. package/dist/@Types/ExternalReferenceFormStep.js +20 -0
  20. package/dist/@Types/FormStep.d.ts +7 -2
  21. package/dist/@Types/FormStep.js +2 -1
  22. package/dist/@Types/LocationFormStep.d.ts +159 -0
  23. package/dist/@Types/LocationFormStep.js +46 -0
  24. package/dist/@Types/NumericFormSteps.d.ts +108 -0
  25. package/dist/@Types/NumericFormSteps.js +20 -0
  26. package/dist/@Types/PredefinedSelectorFormStep.d.ts +51 -0
  27. package/dist/@Types/PredefinedSelectorFormStep.js +39 -0
  28. package/dist/@Types/StepRef.d.ts +135 -0
  29. package/dist/@Types/StepRef.js +49 -0
  30. package/dist/App/App.stories.d.ts +10 -0
  31. package/dist/App/App.stories.js +189 -0
  32. package/dist/App/AppHooks.js +8 -0
  33. package/dist/App/AppHooks.test.js +8 -0
  34. package/dist/Form/Form.js +7 -6
  35. package/dist/Form/FormFunctions.d.ts +17 -0
  36. package/dist/Form/FormFunctions.js +22 -0
  37. package/dist/Form/FormFunctions.test.js +28 -1
  38. package/dist/FormSteps/AvailabilityStep/AvailabilityStep.d.ts +7 -0
  39. package/dist/FormSteps/AvailabilityStep/AvailabilityStep.js +14 -0
  40. package/dist/FormSteps/AvailabilityStep/MaterialAvailabilityStep/MaterialAvailabilityStep.d.ts +8 -0
  41. package/dist/FormSteps/AvailabilityStep/MaterialAvailabilityStep/MaterialAvailabilityStep.js +32 -0
  42. package/dist/FormSteps/BookingStep/BookingStep.d.ts +17 -0
  43. package/dist/FormSteps/BookingStep/BookingStep.js +14 -0
  44. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.d.ts +14 -0
  45. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +129 -0
  46. package/dist/FormSteps/CheckBoxStep/CheckBoxStep.mobileLabel.test.js +20 -12
  47. package/dist/FormSteps/CheckBoxStep/MaterialCheckBoxStep/MaterialCheckBoxStep.js +6 -13
  48. package/dist/FormSteps/CheckBoxStep/MaterialCheckBoxStep/MaterialCheckBoxStep.module.css +2 -13
  49. package/dist/FormSteps/ClassifierSelectorStep/ClassifierSelectorStep.lateClassifiers.test.d.ts +1 -0
  50. package/dist/FormSteps/ClassifierSelectorStep/ClassifierSelectorStep.lateClassifiers.test.js +70 -0
  51. package/dist/FormSteps/ClassifierSelectorStep/MaterialClassifierSelectorStep/MaterialClassifierSelectorStep.js +3 -1
  52. package/dist/FormSteps/EntityValueStep/MaterialEntityValuePickerStep/MaterialEntityValuePickerStep.js +26 -26
  53. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.d.ts +12 -0
  54. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.js +35 -0
  55. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.test.d.ts +1 -0
  56. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.test.js +31 -0
  57. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.d.ts +27 -0
  58. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.js +64 -0
  59. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.d.ts +1 -0
  60. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.js +95 -0
  61. package/dist/FormSteps/LocationStep/LocationStep.d.ts +10 -0
  62. package/dist/FormSteps/LocationStep/LocationStep.js +14 -0
  63. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.d.ts +3 -0
  64. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.js +54 -0
  65. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.module.css +18 -0
  66. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.d.ts +3 -0
  67. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.js +170 -0
  68. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.module.css +96 -0
  69. package/dist/FormSteps/NumberStep/NumberStep.d.ts +10 -0
  70. package/dist/FormSteps/NumberStep/NumberStep.js +14 -0
  71. package/dist/FormSteps/Step.js +16 -0
  72. package/dist/FormSteps/StepFunctions.js +14 -0
  73. package/dist/FormSteps/StepFunctions.test.js +54 -0
  74. package/dist/FormSteps/StepHooks.d.ts +21 -0
  75. package/dist/FormSteps/StepHooks.js +55 -8
  76. package/dist/FormSteps/StepHooks.test.d.ts +1 -0
  77. package/dist/FormSteps/StepHooks.test.js +92 -0
  78. package/dist/FormSteps/isStepHiddenByCondition.test.d.ts +1 -0
  79. package/dist/FormSteps/isStepHiddenByCondition.test.js +42 -0
  80. package/dist/Icons/AddIcon.d.ts +3 -0
  81. package/dist/Icons/AddIcon.js +7 -0
  82. package/dist/Icons/EditIcon.d.ts +3 -0
  83. package/dist/Icons/EditIcon.js +7 -0
  84. package/dist/Icons/RemoveIcon.d.ts +3 -0
  85. package/dist/Icons/RemoveIcon.js +7 -0
  86. package/dist/Icons/ViewDayIcon.d.ts +3 -0
  87. package/dist/Icons/ViewDayIcon.js +7 -0
  88. package/dist/Icons/ViewWeekIcon.d.ts +3 -0
  89. package/dist/Icons/ViewWeekIcon.js +7 -0
  90. package/dist/Services/BookingService.d.ts +79 -0
  91. package/dist/Services/BookingService.js +66 -0
  92. package/dist/Services/BookingService.test.d.ts +1 -0
  93. package/dist/Services/BookingService.test.js +57 -0
  94. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.d.ts +19 -0
  95. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.js +80 -0
  96. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.module.css +88 -0
  97. package/dist/Shared/ErkAddressAutocomplete/googlePlaces.d.ts +29 -0
  98. package/dist/Shared/ErkAddressAutocomplete/googlePlaces.js +139 -0
  99. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.d.ts +25 -0
  100. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.js +112 -0
  101. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.module.css +88 -0
  102. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.d.ts +1 -0
  103. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.js +73 -0
  104. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +52 -0
  105. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +292 -0
  106. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +608 -0
  107. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +28 -0
  108. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +117 -0
  109. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.d.ts +1 -0
  110. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +49 -0
  111. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.d.ts +1 -0
  112. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +157 -0
  113. package/dist/Shared/ErkDatePicker/ErkDatePicker.js +1 -3
  114. package/dist/Shared/ErkDateRangePicker/ErkDateRangePicker.d.ts +2 -3
  115. package/dist/Shared/ErkDateRangePicker/ErkDateRangePicker.js +1 -2
  116. package/dist/Shared/ErkIconButton/ErkIconButton.d.ts +17 -0
  117. package/dist/Shared/ErkIconButton/ErkIconButton.js +11 -0
  118. package/dist/Shared/ErkIconButton/ErkIconButton.module.css +41 -0
  119. package/dist/Shared/ErkLocationField/ErkLocationField.d.ts +24 -0
  120. package/dist/Shared/ErkLocationField/ErkLocationField.js +128 -0
  121. package/dist/Shared/ErkLocationField/ErkLocationField.module.css +164 -0
  122. package/dist/Shared/ErkLocationField/ErkLocationField.test.d.ts +1 -0
  123. package/dist/Shared/ErkLocationField/ErkLocationField.test.js +82 -0
  124. package/dist/Shared/ErkLocationField/addressFields.d.ts +12 -0
  125. package/dist/Shared/ErkLocationField/addressFields.js +34 -0
  126. package/dist/Shared/ErkLocationField/locationRestrictions.d.ts +6 -0
  127. package/dist/Shared/ErkLocationField/locationRestrictions.js +51 -0
  128. package/dist/Shared/ErkLocationInput/ErkLocationInput.d.ts +15 -0
  129. package/dist/Shared/ErkLocationInput/ErkLocationInput.js +502 -0
  130. package/dist/Shared/ErkLocationInput/ErkLocationInput.module.css +128 -0
  131. package/dist/Shared/ErkNumberField/ErkNumberField.d.ts +42 -0
  132. package/dist/Shared/ErkNumberField/ErkNumberField.js +169 -0
  133. package/dist/Utils/AxiosWidget.js +9 -1
  134. package/dist/constants/FormStepTypes.d.ts +5 -1
  135. package/dist/constants/FormStepTypes.js +4 -0
  136. package/dist/index.lib.d.ts +4 -2
  137. package/dist/index.lib.js +3 -1
  138. package/package.json +2 -1
@@ -0,0 +1,79 @@
1
+ import { BookingSlot } from '../@Types/BookingFormStep';
2
+ /**
3
+ * Cómo llegar al paso BOOKING de un formulario publicado. Misma regla que el envío del formulario:
4
+ * un form embebido o interno nombra su organización y sale por el dominio de forms; uno servido por
5
+ * el subdominio de la organización habla con `/api` en el mismo origen.
6
+ */
7
+ export interface BookingRoute {
8
+ apiKey: string;
9
+ idStep: string;
10
+ idOrganization?: string;
11
+ internal?: boolean;
12
+ }
13
+ /** Una persona agendable tal como la publica el backend: un handle opaco, nunca un id de agente. */
14
+ export interface BookingOfferPerson {
15
+ token: string;
16
+ name: string;
17
+ role?: string;
18
+ avatarUrl?: string;
19
+ description?: string;
20
+ }
21
+ /** Lo que el picker necesita para dibujarse: en qué zona horaria leer las horas y qué días tienen cupo. */
22
+ export interface BookingOffer {
23
+ timeZone: string;
24
+ durationMinutes: number;
25
+ bookingMode: string;
26
+ personSelection: 'NONE' | 'OPTIONAL' | 'REQUIRED';
27
+ persons?: BookingOfferPerson[];
28
+ /** Días con al menos un slot, `YYYY-MM-DD` en la zona horaria de la organización. */
29
+ days: string[];
30
+ from: string;
31
+ to: string;
32
+ }
33
+ /** Un slot tal como lo ofrece el backend: instantes ISO y el handle firmado que lo nombra. */
34
+ export interface BookingOfferSlot {
35
+ start: string;
36
+ end: string;
37
+ token: string;
38
+ }
39
+ /** Un slot retirado de la oferta mientras se termina el formulario. */
40
+ export interface BookingHold {
41
+ appointment: string;
42
+ hold: string;
43
+ lockedUntil: string;
44
+ }
45
+ export interface BookingContact {
46
+ name: string;
47
+ email?: string;
48
+ phone?: string;
49
+ }
50
+ /** La oferta del paso; con `person`, la oferta de esa persona (los días que ella tiene libres). */
51
+ export declare function fetchBookingOffer(route: BookingRoute, person?: string): Promise<BookingOffer>;
52
+ /** Los slots de un día, `date` en `YYYY-MM-DD` de la zona horaria de la organización. */
53
+ export declare function fetchBookingSlots(route: BookingRoute, date: string, person?: string): Promise<BookingOfferSlot[]>;
54
+ /**
55
+ * Bloquea un slot (o renueva un bloqueo propio si se envía su `hold`). El backend responde 409 cuando
56
+ * otra persona ya lo tomó, y 422 cuando el paso exige contacto y no llegó.
57
+ */
58
+ export declare function holdBookingSlot(route: BookingRoute, body: {
59
+ slot: string;
60
+ hold?: string;
61
+ contact?: BookingContact;
62
+ }): Promise<BookingHold>;
63
+ /** Devuelve un slot antes de que venza su bloqueo, para que se ofrezca de nuevo de inmediato. */
64
+ export declare function releaseBookingSlot(route: BookingRoute, body: {
65
+ appointment: string;
66
+ hold: string;
67
+ }): Promise<void>;
68
+ /**
69
+ * La hora `HH:mm` de un instante en la zona horaria de la organización. El picker muestra el reloj de
70
+ * quien recibe la cita, no el del navegador: un slot de las 09:00 en Bogotá es a las 09:00 aunque el
71
+ * que agenda esté en Madrid.
72
+ */
73
+ export declare function clockIn(iso: string, timeZone: string): string;
74
+ /** Un slot de la oferta como lo consume el picker: el handle es su id y las horas van en el reloj de la organización. */
75
+ export declare function toPickerSlot(slot: BookingOfferSlot, timeZone: string): BookingSlot;
76
+ /** Un texto opcional de un campo de contacto: vacío o solo espacios cuenta como no dado. */
77
+ export declare function nonEmpty(text?: string): string | undefined;
78
+ /** El motivo de un rechazo del backend, que llega como texto plano, o un mensaje genérico. */
79
+ export declare function bookingErrorMessage(error: unknown): string;
@@ -0,0 +1,66 @@
1
+ import axiosInstance from '../Utils/AxiosAPI';
2
+ import widgetInstance from '../Utils/AxiosWidget';
3
+ function client(route) {
4
+ return route.idOrganization || route.internal ? widgetInstance : axiosInstance;
5
+ }
6
+ function url(route, suffix, params = {}) {
7
+ const search = new URLSearchParams();
8
+ if (route.idOrganization || route.internal)
9
+ search.set('idOrganization', route.idOrganization ?? '');
10
+ for (const [key, value] of Object.entries(params)) {
11
+ if (value)
12
+ search.set(key, value);
13
+ }
14
+ const query = search.toString();
15
+ return `/booking/${route.apiKey}/${route.idStep}${suffix}${query ? `?${query}` : ''}`;
16
+ }
17
+ /** La oferta del paso; con `person`, la oferta de esa persona (los días que ella tiene libres). */
18
+ export async function fetchBookingOffer(route, person) {
19
+ const response = await client(route).get(url(route, '', { person }));
20
+ return response.data;
21
+ }
22
+ /** Los slots de un día, `date` en `YYYY-MM-DD` de la zona horaria de la organización. */
23
+ export async function fetchBookingSlots(route, date, person) {
24
+ const response = await client(route).get(url(route, '/slots', { date, person }));
25
+ return response.data.slots;
26
+ }
27
+ /**
28
+ * Bloquea un slot (o renueva un bloqueo propio si se envía su `hold`). El backend responde 409 cuando
29
+ * otra persona ya lo tomó, y 422 cuando el paso exige contacto y no llegó.
30
+ */
31
+ export async function holdBookingSlot(route, body) {
32
+ const response = await client(route).post(url(route, '/hold'), body);
33
+ return response.data;
34
+ }
35
+ /** Devuelve un slot antes de que venza su bloqueo, para que se ofrezca de nuevo de inmediato. */
36
+ export async function releaseBookingSlot(route, body) {
37
+ await client(route).post(url(route, '/release'), body);
38
+ }
39
+ /**
40
+ * La hora `HH:mm` de un instante en la zona horaria de la organización. El picker muestra el reloj de
41
+ * quien recibe la cita, no el del navegador: un slot de las 09:00 en Bogotá es a las 09:00 aunque el
42
+ * que agenda esté en Madrid.
43
+ */
44
+ export function clockIn(iso, timeZone) {
45
+ return new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23', timeZone }).format(new Date(iso));
46
+ }
47
+ /** Un slot de la oferta como lo consume el picker: el handle es su id y las horas van en el reloj de la organización. */
48
+ export function toPickerSlot(slot, timeZone) {
49
+ return {
50
+ id: slot.token,
51
+ startTime: clockIn(slot.start, timeZone),
52
+ endTime: clockIn(slot.end, timeZone),
53
+ available: true,
54
+ };
55
+ }
56
+ /** Un texto opcional de un campo de contacto: vacío o solo espacios cuenta como no dado. */
57
+ export function nonEmpty(text) {
58
+ return text?.trim() ? text : undefined;
59
+ }
60
+ /** El motivo de un rechazo del backend, que llega como texto plano, o un mensaje genérico. */
61
+ export function bookingErrorMessage(error) {
62
+ const data = error?.response?.data;
63
+ if (typeof data === 'string' && data.trim())
64
+ return data;
65
+ return 'No pudimos reservar ese horario. Intenta de nuevo.';
66
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method, @typescript-eslint/require-await, @typescript-eslint/explicit-function-return-type -- test file: vi.mocked()/expect() reference mocked axios methods as values, and the mock factory uses terse async/return types */
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import axiosInstance from '../Utils/AxiosAPI';
4
+ import widgetInstance from '../Utils/AxiosWidget';
5
+ import { bookingErrorMessage, clockIn, fetchBookingOffer, fetchBookingSlots, holdBookingSlot, releaseBookingSlot, toPickerSlot, } from './BookingService';
6
+ vi.mock('../Utils/AxiosWidget', () => ({
7
+ default: { get: vi.fn(), post: vi.fn() },
8
+ }));
9
+ vi.mock('../Utils/AxiosAPI', () => ({
10
+ default: { get: vi.fn(), post: vi.fn() },
11
+ axiosBaseQuery: () => async () => ({ data: undefined }),
12
+ }));
13
+ const ROUTE = { apiKey: 'form-key', idStep: 'BOOKING-1' };
14
+ describe('BookingService', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+ it('reads a slot on the organization’s clock, not the browser’s', () => {
19
+ // 14:00Z is 09:00 in Bogotá and 16:00 in Madrid; the picker shows the calendar's own hours.
20
+ expect(clockIn('2026-09-22T14:00:00.000Z', 'America/Bogota')).toBe('09:00');
21
+ expect(clockIn('2026-09-22T14:00:00.000Z', 'Europe/Madrid')).toBe('16:00');
22
+ expect(clockIn('2026-09-22T05:00:00.000Z', 'America/Bogota')).toBe('00:00');
23
+ expect(toPickerSlot({ start: '2026-09-22T14:00:00.000Z', end: '2026-09-22T14:30:00.000Z', token: 'tok' }, 'America/Bogota')).toEqual({ id: 'tok', startTime: '09:00', endTime: '09:30', available: true });
24
+ });
25
+ it('talks to the same-origin API for a form served under the organization', async () => {
26
+ vi.mocked(axiosInstance.get).mockResolvedValue({ data: { days: ['2026-09-22'] } });
27
+ await fetchBookingOffer(ROUTE);
28
+ expect(axiosInstance.get).toHaveBeenCalledWith('/booking/form-key/BOOKING-1');
29
+ await fetchBookingSlots(ROUTE, '2026-09-22', 'person-token');
30
+ expect(axiosInstance.get).toHaveBeenLastCalledWith('/booking/form-key/BOOKING-1/slots?date=2026-09-22&person=person-token');
31
+ expect(widgetInstance.get).not.toHaveBeenCalled();
32
+ });
33
+ it('names the organization and goes through the forms domain for an embedded form', async () => {
34
+ vi.mocked(widgetInstance.get).mockResolvedValue({ data: { days: [] } });
35
+ vi.mocked(widgetInstance.post).mockResolvedValue({ data: { appointment: 'a1', hold: 'h1', lockedUntil: 'x' } });
36
+ const route = { ...ROUTE, idOrganization: 'acme' };
37
+ await fetchBookingOffer(route, 'p');
38
+ expect(widgetInstance.get).toHaveBeenCalledWith('/booking/form-key/BOOKING-1?idOrganization=acme&person=p');
39
+ const held = await holdBookingSlot(route, { slot: 'slot-token', contact: { name: 'Ana' } });
40
+ expect(widgetInstance.post).toHaveBeenCalledWith('/booking/form-key/BOOKING-1/hold?idOrganization=acme', {
41
+ slot: 'slot-token',
42
+ contact: { name: 'Ana' },
43
+ });
44
+ expect(held).toEqual({ appointment: 'a1', hold: 'h1', lockedUntil: 'x' });
45
+ await releaseBookingSlot(route, { appointment: 'a1', hold: 'h1' });
46
+ expect(widgetInstance.post).toHaveBeenLastCalledWith('/booking/form-key/BOOKING-1/release?idOrganization=acme', {
47
+ appointment: 'a1',
48
+ hold: 'h1',
49
+ });
50
+ expect(axiosInstance.get).not.toHaveBeenCalled();
51
+ });
52
+ it('surfaces the edge’s plain-text refusal, and a generic message otherwise', () => {
53
+ expect(bookingErrorMessage({ response: { data: 'That slot is no longer available' } })).toBe('That slot is no longer available');
54
+ expect(bookingErrorMessage({ response: { data: { message: 'Forbidden' } } })).toBe('No pudimos reservar ese horario. Intenta de nuevo.');
55
+ expect(bookingErrorMessage(new Error('network'))).toBe('No pudimos reservar ese horario. Intenta de nuevo.');
56
+ });
57
+ });
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { LocationStepValue } from '../../@Types/LocationFormStep';
3
+ export interface ErkAddressAutocompleteProps {
4
+ value?: LocationStepValue | null;
5
+ onChange: (value: LocationStepValue) => void;
6
+ /** ISO-2 para acotar las sugerencias (ej. ['co']). */
7
+ regionCodes?: string[];
8
+ placeholder?: string;
9
+ error?: boolean;
10
+ required?: boolean;
11
+ readOnly?: boolean;
12
+ inputRef?: React.Ref<HTMLInputElement>;
13
+ }
14
+ /**
15
+ * Buscador de direcciones con el `Autocomplete` de Base UI + Google Places.
16
+ * Emite un `LocationStepValue` (source GOOGLE_PLACES) con el desglose estructurado.
17
+ */
18
+ declare function ErkAddressAutocomplete({ value, onChange, regionCodes, placeholder, error, required, readOnly, inputRef, }: ErkAddressAutocompleteProps): JSX.Element;
19
+ export default ErkAddressAutocomplete;
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Autocomplete } from '@base-ui/react/autocomplete';
4
+ import { fetchAddressSuggestions, loadPlaces, newSessionToken, resolvePrediction, } from './googlePlaces';
5
+ import styles from './ErkAddressAutocomplete.module.css';
6
+ /**
7
+ * Buscador de direcciones con el `Autocomplete` de Base UI + Google Places.
8
+ * Emite un `LocationStepValue` (source GOOGLE_PLACES) con el desglose estructurado.
9
+ */
10
+ function ErkAddressAutocomplete({ value, onChange, regionCodes = ['co'], placeholder = 'Buscar dirección…', error = false, required = false, readOnly = false, inputRef, }) {
11
+ const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? '';
12
+ const [ready, setReady] = useState(false);
13
+ const [inputValue, setInputValue] = useState(value?.address ?? value?.name ?? '');
14
+ const [options, setOptions] = useState([]);
15
+ const [loading, setLoading] = useState(false);
16
+ const [hasSearched, setHasSearched] = useState(false);
17
+ const debounceRef = useRef(undefined);
18
+ const sessionTokenRef = useRef(undefined);
19
+ useEffect(() => {
20
+ if (readOnly)
21
+ return;
22
+ let active = true;
23
+ void loadPlaces(apiKey)
24
+ .then(() => {
25
+ if (!active)
26
+ return;
27
+ sessionTokenRef.current = newSessionToken();
28
+ setReady(true);
29
+ })
30
+ .catch(() => {
31
+ if (active)
32
+ setReady(false);
33
+ });
34
+ return () => {
35
+ active = false;
36
+ };
37
+ }, [apiKey, readOnly]);
38
+ const handleInput = useCallback((text) => {
39
+ setInputValue(text);
40
+ if (debounceRef.current)
41
+ clearTimeout(debounceRef.current);
42
+ if (!ready || !text || text.length < 3) {
43
+ setOptions([]);
44
+ setHasSearched(false);
45
+ return;
46
+ }
47
+ setLoading(true);
48
+ debounceRef.current = setTimeout(() => {
49
+ void (async () => {
50
+ try {
51
+ setOptions(await fetchAddressSuggestions(text, {
52
+ sessionToken: sessionTokenRef.current,
53
+ regionCodes,
54
+ }));
55
+ }
56
+ catch {
57
+ setOptions([]);
58
+ }
59
+ finally {
60
+ setLoading(false);
61
+ setHasSearched(true);
62
+ }
63
+ })();
64
+ }, 300);
65
+ }, [ready, regionCodes]);
66
+ const handleSelect = useCallback(async (prediction) => {
67
+ try {
68
+ const stepValue = await resolvePrediction(prediction);
69
+ setInputValue(stepValue.address ?? stepValue.name ?? prediction.label);
70
+ setOptions([]);
71
+ onChange(stepValue);
72
+ }
73
+ finally {
74
+ // Cada selección cierra la sesión de billing de Google.
75
+ sessionTokenRef.current = newSessionToken();
76
+ }
77
+ }, [onChange]);
78
+ return (_jsxs(Autocomplete.Root, { items: options, mode: "none", value: inputValue, onValueChange: handleInput, disabled: readOnly, children: [_jsx(Autocomplete.Input, { ref: inputRef, placeholder: placeholder, required: required, readOnly: readOnly, className: error ? `${styles.input} ${styles.inputError}` : styles.input }), _jsx(Autocomplete.Portal, { children: _jsx(Autocomplete.Positioner, { className: styles.positioner, sideOffset: 4, children: _jsxs(Autocomplete.Popup, { className: styles.popup, children: [loading && _jsx("div", { className: styles.status, children: "Buscando\u2026" }), _jsx(Autocomplete.Empty, { className: styles.status, children: hasSearched ? 'No se encontraron resultados' : 'Escribe para buscar…' }), _jsx(Autocomplete.List, { className: styles.list, children: (option) => (_jsxs(Autocomplete.Item, { value: option, className: styles.item, onClick: () => void handleSelect(option), children: [_jsx("span", { className: styles.optionLabel, children: option.label }), option.description && (_jsx("span", { className: styles.optionDescription, children: option.description }))] }, option.placeId)) })] }) }) })] }));
79
+ }
80
+ export default ErkAddressAutocomplete;
@@ -0,0 +1,88 @@
1
+ .input {
2
+ width: 100%;
3
+ box-sizing: border-box;
4
+ height: 31px;
5
+ padding: 6px 12px;
6
+ font-size: 1rem;
7
+ font-family: inherit;
8
+ color: var(--eureka-text, #293241);
9
+ background-color: var(--eureka-bg, #fff);
10
+ border: 1px solid var(--eureka-outline, #b8b8b8);
11
+ border-radius: 10px;
12
+ outline: none;
13
+ }
14
+
15
+ .input::placeholder {
16
+ color: var(--eureka-text, #293241);
17
+ opacity: 0.5;
18
+ }
19
+
20
+ .input:hover:not(:disabled):not(:read-only) {
21
+ border-color: var(--eureka-primary, #3d5a7f);
22
+ }
23
+
24
+ .input:focus:not(:read-only) {
25
+ border-color: var(--eureka-primary, #3d5a7f);
26
+ border-width: 2px;
27
+ padding: 5px 11px;
28
+ }
29
+
30
+ .input:disabled {
31
+ opacity: 0.5;
32
+ }
33
+
34
+ .inputError {
35
+ border-color: var(--eureka-error, #e53935);
36
+ }
37
+
38
+ .positioner {
39
+ z-index: 1400;
40
+ width: var(--anchor-width);
41
+ }
42
+
43
+ .popup {
44
+ box-sizing: border-box;
45
+ max-height: 320px;
46
+ overflow-y: auto;
47
+ padding: 4px;
48
+ background-color: var(--eureka-bg, #fff);
49
+ border: 1px solid var(--eureka-outline, #b8b8b8);
50
+ border-radius: 10px;
51
+ box-shadow: 0 4px 16px rgb(0 0 0 / 12%);
52
+ }
53
+
54
+ .list {
55
+ display: flex;
56
+ flex-direction: column;
57
+ }
58
+
59
+ .status {
60
+ padding: 10px 12px;
61
+ font-size: 0.875rem;
62
+ color: var(--eureka-text, #666);
63
+ opacity: 0.7;
64
+ }
65
+
66
+ .item {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 2px;
70
+ padding: 8px 12px;
71
+ border-radius: 8px;
72
+ cursor: pointer;
73
+ }
74
+
75
+ .item[data-highlighted],
76
+ .item:hover {
77
+ background-color: var(--eureka-primary, #3d5a7f);
78
+ color: #fff;
79
+ }
80
+
81
+ .optionLabel {
82
+ font-size: 0.9375rem;
83
+ }
84
+
85
+ .optionDescription {
86
+ font-size: 0.8125rem;
87
+ opacity: 0.7;
88
+ }
@@ -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,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;