@arquimedes.co/eureka-forms 3.0.71-test → 3.0.72

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 (167) 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 +249 -0
  7. package/dist/@Types/BookingFormStep.js +65 -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/App.submitError.test.d.ts +1 -0
  33. package/dist/App/App.submitError.test.js +53 -0
  34. package/dist/App/AppHooks.js +8 -0
  35. package/dist/App/AppHooks.test.js +8 -0
  36. package/dist/BookingManage/AppointmentText.d.ts +5 -0
  37. package/dist/BookingManage/AppointmentText.js +20 -0
  38. package/dist/BookingManage/BookingManagePage.d.ts +17 -0
  39. package/dist/BookingManage/BookingManagePage.js +177 -0
  40. package/dist/BookingManage/BookingManagePage.module.css +115 -0
  41. package/dist/BookingManage/BookingManagePage.test.d.ts +1 -0
  42. package/dist/BookingManage/BookingManagePage.test.js +149 -0
  43. package/dist/Form/ConfirmationDialog/ConfirmationDialog.d.ts +14 -5
  44. package/dist/Form/ConfirmationDialog/ConfirmationDialog.js +8 -1
  45. package/dist/Form/ConfirmationDialog/ConfirmationDialog.module.css +12 -0
  46. package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.d.ts +1 -0
  47. package/dist/Form/ConfirmationDialog/ConfirmationDialog.test.js +21 -0
  48. package/dist/Form/Form.d.ts +3 -0
  49. package/dist/Form/Form.js +16 -8
  50. package/dist/Form/FormFunctions.d.ts +17 -0
  51. package/dist/Form/FormFunctions.js +22 -0
  52. package/dist/Form/FormFunctions.test.js +28 -1
  53. package/dist/Form/FormTypes/ColumnForm/ColumnForm.d.ts +1 -1
  54. package/dist/Form/FormTypes/ColumnForm/ColumnForm.js +3 -2
  55. package/dist/Form/FormTypes/StepperForm/StepperForm.d.ts +1 -1
  56. package/dist/Form/FormTypes/StepperForm/StepperForm.js +3 -2
  57. package/dist/Form/SubmitError/SubmitError.d.ts +11 -0
  58. package/dist/Form/SubmitError/SubmitError.js +9 -0
  59. package/dist/Form/SubmitError/SubmitError.module.css +25 -0
  60. package/dist/Form/SubmitError/SubmitError.test.d.ts +1 -0
  61. package/dist/Form/SubmitError/SubmitError.test.js +34 -0
  62. package/dist/Form/SubmitError/SubmitErrorReason.d.ts +13 -0
  63. package/dist/Form/SubmitError/SubmitErrorReason.js +20 -0
  64. package/dist/FormSteps/AvailabilityStep/AvailabilityStep.d.ts +7 -0
  65. package/dist/FormSteps/AvailabilityStep/AvailabilityStep.js +14 -0
  66. package/dist/FormSteps/AvailabilityStep/MaterialAvailabilityStep/MaterialAvailabilityStep.d.ts +8 -0
  67. package/dist/FormSteps/AvailabilityStep/MaterialAvailabilityStep/MaterialAvailabilityStep.js +32 -0
  68. package/dist/FormSteps/BookingStep/BookingStep.d.ts +17 -0
  69. package/dist/FormSteps/BookingStep/BookingStep.js +14 -0
  70. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.d.ts +21 -0
  71. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +289 -0
  72. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.d.ts +1 -0
  73. package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +275 -0
  74. package/dist/FormSteps/CheckBoxStep/CheckBoxStep.mobileLabel.test.js +20 -12
  75. package/dist/FormSteps/CheckBoxStep/MaterialCheckBoxStep/MaterialCheckBoxStep.js +6 -13
  76. package/dist/FormSteps/CheckBoxStep/MaterialCheckBoxStep/MaterialCheckBoxStep.module.css +2 -13
  77. package/dist/FormSteps/ClassifierSelectorStep/ClassifierSelectorStep.lateClassifiers.test.d.ts +1 -0
  78. package/dist/FormSteps/ClassifierSelectorStep/ClassifierSelectorStep.lateClassifiers.test.js +70 -0
  79. package/dist/FormSteps/ClassifierSelectorStep/MaterialClassifierSelectorStep/MaterialClassifierSelectorStep.js +3 -1
  80. package/dist/FormSteps/EntityValueStep/MaterialEntityValuePickerStep/MaterialEntityValuePickerStep.js +26 -26
  81. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.d.ts +12 -0
  82. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.js +35 -0
  83. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.test.d.ts +1 -0
  84. package/dist/FormSteps/EntityValueStep/entityValueDependencyValue.test.js +31 -0
  85. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.d.ts +27 -0
  86. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.js +64 -0
  87. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.d.ts +1 -0
  88. package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.js +95 -0
  89. package/dist/FormSteps/LocationStep/LocationStep.d.ts +10 -0
  90. package/dist/FormSteps/LocationStep/LocationStep.js +14 -0
  91. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.d.ts +3 -0
  92. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.js +54 -0
  93. package/dist/FormSteps/LocationStep/MaterialLocationStep/MaterialLocationStep.module.css +18 -0
  94. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.d.ts +3 -0
  95. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.js +170 -0
  96. package/dist/FormSteps/NumberStep/MaterialNumberStep/MaterialNumberStep.module.css +96 -0
  97. package/dist/FormSteps/NumberStep/NumberStep.d.ts +10 -0
  98. package/dist/FormSteps/NumberStep/NumberStep.js +14 -0
  99. package/dist/FormSteps/Step.js +16 -0
  100. package/dist/FormSteps/StepFunctions.js +14 -0
  101. package/dist/FormSteps/StepFunctions.test.js +54 -0
  102. package/dist/FormSteps/StepHooks.d.ts +21 -0
  103. package/dist/FormSteps/StepHooks.js +55 -8
  104. package/dist/FormSteps/StepHooks.test.d.ts +1 -0
  105. package/dist/FormSteps/StepHooks.test.js +92 -0
  106. package/dist/FormSteps/isStepHiddenByCondition.test.d.ts +1 -0
  107. package/dist/FormSteps/isStepHiddenByCondition.test.js +42 -0
  108. package/dist/Icons/AddIcon.d.ts +3 -0
  109. package/dist/Icons/AddIcon.js +7 -0
  110. package/dist/Icons/EditIcon.d.ts +3 -0
  111. package/dist/Icons/EditIcon.js +7 -0
  112. package/dist/Icons/RemoveIcon.d.ts +3 -0
  113. package/dist/Icons/RemoveIcon.js +7 -0
  114. package/dist/Icons/ViewDayIcon.d.ts +3 -0
  115. package/dist/Icons/ViewDayIcon.js +7 -0
  116. package/dist/Icons/ViewWeekIcon.d.ts +3 -0
  117. package/dist/Icons/ViewWeekIcon.js +7 -0
  118. package/dist/Services/BookingService.d.ts +226 -0
  119. package/dist/Services/BookingService.js +264 -0
  120. package/dist/Services/BookingService.test.d.ts +1 -0
  121. package/dist/Services/BookingService.test.js +178 -0
  122. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.d.ts +19 -0
  123. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.js +80 -0
  124. package/dist/Shared/ErkAddressAutocomplete/ErkAddressAutocomplete.module.css +88 -0
  125. package/dist/Shared/ErkAddressAutocomplete/googlePlaces.d.ts +29 -0
  126. package/dist/Shared/ErkAddressAutocomplete/googlePlaces.js +139 -0
  127. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.d.ts +25 -0
  128. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.js +112 -0
  129. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.module.css +88 -0
  130. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.d.ts +1 -0
  131. package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.js +73 -0
  132. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +105 -0
  133. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +399 -0
  134. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +646 -0
  135. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +36 -0
  136. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +177 -0
  137. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.d.ts +1 -0
  138. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +47 -0
  139. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.d.ts +1 -0
  140. package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +440 -0
  141. package/dist/Shared/ErkDatePicker/ErkDatePicker.js +1 -3
  142. package/dist/Shared/ErkDateRangePicker/ErkDateRangePicker.d.ts +2 -3
  143. package/dist/Shared/ErkDateRangePicker/ErkDateRangePicker.js +1 -2
  144. package/dist/Shared/ErkIconButton/ErkIconButton.d.ts +17 -0
  145. package/dist/Shared/ErkIconButton/ErkIconButton.js +11 -0
  146. package/dist/Shared/ErkIconButton/ErkIconButton.module.css +41 -0
  147. package/dist/Shared/ErkLocationField/ErkLocationField.d.ts +24 -0
  148. package/dist/Shared/ErkLocationField/ErkLocationField.js +128 -0
  149. package/dist/Shared/ErkLocationField/ErkLocationField.module.css +164 -0
  150. package/dist/Shared/ErkLocationField/ErkLocationField.test.d.ts +1 -0
  151. package/dist/Shared/ErkLocationField/ErkLocationField.test.js +82 -0
  152. package/dist/Shared/ErkLocationField/addressFields.d.ts +12 -0
  153. package/dist/Shared/ErkLocationField/addressFields.js +34 -0
  154. package/dist/Shared/ErkLocationField/locationRestrictions.d.ts +6 -0
  155. package/dist/Shared/ErkLocationField/locationRestrictions.js +51 -0
  156. package/dist/Shared/ErkLocationInput/ErkLocationInput.d.ts +15 -0
  157. package/dist/Shared/ErkLocationInput/ErkLocationInput.js +502 -0
  158. package/dist/Shared/ErkLocationInput/ErkLocationInput.module.css +128 -0
  159. package/dist/Shared/ErkNumberField/ErkNumberField.d.ts +42 -0
  160. package/dist/Shared/ErkNumberField/ErkNumberField.js +169 -0
  161. package/dist/Utils/AxiosWidget.js +9 -1
  162. package/dist/constants/FormStepTypes.d.ts +5 -1
  163. package/dist/constants/FormStepTypes.js +4 -0
  164. package/dist/index.lib.d.ts +4 -2
  165. package/dist/index.lib.js +3 -1
  166. package/dist/main.js +4 -1
  167. package/package.json +2 -1
@@ -0,0 +1,264 @@
1
+ import axiosInstance from '../Utils/AxiosAPI';
2
+ import widgetInstance from '../Utils/AxiosWidget';
3
+ /** Los códigos con los que el borde de agendamiento explica un rechazo. */
4
+ export var BookingErrorCodes;
5
+ (function (BookingErrorCodes) {
6
+ BookingErrorCodes["INVALID_REQUEST"] = "INVALID_REQUEST";
7
+ BookingErrorCodes["NOT_FOUND"] = "NOT_FOUND";
8
+ BookingErrorCodes["FORBIDDEN"] = "FORBIDDEN";
9
+ /** Un token de persona o de cupo venció: hay que recargar la oferta (o las horas). */
10
+ BookingErrorCodes["STALE_OFFER"] = "STALE_OFFER";
11
+ BookingErrorCodes["CONTACT_REQUIRED"] = "CONTACT_REQUIRED";
12
+ BookingErrorCodes["SLOT_TAKEN"] = "SLOT_TAKEN";
13
+ /** La renovación llegó tarde: la reserva ya venció. */
14
+ BookingErrorCodes["HOLD_EXPIRED"] = "HOLD_EXPIRED";
15
+ /** El enlace de la cita venció, fue alterado o ya no nombra ninguna cita. */
16
+ BookingErrorCodes["INVALID_LINK"] = "INVALID_LINK";
17
+ /** La cita ya no se puede cancelar ni mover: está cancelada o su paso ya no existe. */
18
+ BookingErrorCodes["APPOINTMENT_CLOSED"] = "APPOINTMENT_CLOSED";
19
+ BookingErrorCodes["RATE_LIMITED"] = "RATE_LIMITED";
20
+ BookingErrorCodes["METHOD_NOT_ALLOWED"] = "METHOD_NOT_ALLOWED";
21
+ BookingErrorCodes["INTERNAL"] = "INTERNAL";
22
+ })(BookingErrorCodes || (BookingErrorCodes = {}));
23
+ /** Un rechazo del borde ya leído: el mensaje es apto para mostrarse a quien agenda. */
24
+ export class BookingEdgeError extends Error {
25
+ constructor(message, code, status) {
26
+ super(message);
27
+ Object.defineProperty(this, "code", {
28
+ enumerable: true,
29
+ configurable: true,
30
+ writable: true,
31
+ value: void 0
32
+ });
33
+ Object.defineProperty(this, "status", {
34
+ enumerable: true,
35
+ configurable: true,
36
+ writable: true,
37
+ value: void 0
38
+ });
39
+ this.name = 'BookingEdgeError';
40
+ this.code = code;
41
+ this.status = status;
42
+ }
43
+ }
44
+ /** El texto que muestra el picker cuando la renovación de la reserva falla para siempre. */
45
+ export const HOLD_LAPSED_MESSAGE = 'Tu reserva venció; elige la hora de nuevo.';
46
+ function client(route) {
47
+ return route.idOrganization || route.internal ? widgetInstance : axiosInstance;
48
+ }
49
+ function url(route, suffix, params = {}) {
50
+ const search = new URLSearchParams();
51
+ if (route.idOrganization || route.internal)
52
+ search.set('idOrganization', route.idOrganization ?? '');
53
+ for (const [key, value] of Object.entries(params)) {
54
+ if (value)
55
+ search.set(key, value);
56
+ }
57
+ const query = search.toString();
58
+ return `/booking/${route.apiKey}/${route.idStep}${suffix}${query ? `?${query}` : ''}`;
59
+ }
60
+ function appointmentUrl(token, idOrganization, suffix = '') {
61
+ const query = idOrganization ? `?${new URLSearchParams({ idOrganization }).toString()}` : '';
62
+ return `/booking/appointment/${encodeURIComponent(token)}${suffix}${query}`;
63
+ }
64
+ function appointmentClient(idOrganization) {
65
+ return idOrganization ? widgetInstance : axiosInstance;
66
+ }
67
+ /** La cita que nombra el enlace. Un enlace vencido o alterado responde 404 `INVALID_LINK`. */
68
+ export async function fetchBookingAppointment(token, idOrganization) {
69
+ const response = await appointmentClient(idOrganization).get(appointmentUrl(token, idOrganization));
70
+ return response.data;
71
+ }
72
+ /** Cancela la cita; responde la cita ya cancelada. Una cita que ya no está vigente responde 409 `APPOINTMENT_CLOSED`. */
73
+ export async function cancelBookingAppointment(token, idOrganization) {
74
+ const response = await appointmentClient(idOrganization).post(appointmentUrl(token, idOrganization, '/cancel'));
75
+ return response.data;
76
+ }
77
+ /**
78
+ * Mueve la cita al cupo `slot` (un handle de la oferta del mismo paso y la misma persona). Responde la
79
+ * cita movida con su handle nuevo; 409 `SLOT_TAKEN` si alguien tomó la hora primero.
80
+ */
81
+ export async function moveBookingAppointment(token, slot, idOrganization) {
82
+ const response = await appointmentClient(idOrganization).post(appointmentUrl(token, idOrganization, '/move'), { slot });
83
+ return response.data;
84
+ }
85
+ /**
86
+ * La oferta del paso; con `person`, la oferta de esa persona (los días que ella tiene libres), y con
87
+ * `eventTemplate`, los días de ese tipo de atención con su duración.
88
+ */
89
+ export async function fetchBookingOffer(route, query = {}) {
90
+ const response = await client(route).get(url(route, '', { person: query.person, eventTemplate: query.eventTemplate }));
91
+ return response.data;
92
+ }
93
+ /** Los slots de un día, `date` en `YYYY-MM-DD` de la zona horaria de la organización. */
94
+ export async function fetchBookingSlots(route, date, query = {}) {
95
+ const response = await client(route).get(url(route, '/slots', { date, person: query.person, eventTemplate: query.eventTemplate }));
96
+ return response.data.slots;
97
+ }
98
+ /**
99
+ * Bloquea un slot a nombre de quien agenda. El borde responde 409 `SLOT_TAKEN` cuando nadie quedó
100
+ * libre a esa hora y 422 `CONTACT_REQUIRED` sin nombre o sin correo válido. `eventTemplate` is the
101
+ * handle of the type the requester picked; the appointment takes that type's duration.
102
+ */
103
+ export async function holdBookingSlot(route, body) {
104
+ const response = await client(route).post(url(route, '/hold'), body);
105
+ return response.data;
106
+ }
107
+ /**
108
+ * Extiende una reserva propia con el `renew` de la última respuesta. No depende del token del cupo,
109
+ * así que una reserva renovada a tiempo vive lo que tarde el formulario. Una reserva ya vencida
110
+ * responde 409 `HOLD_EXPIRED`.
111
+ */
112
+ export async function renewBookingHold(route, body) {
113
+ const response = await client(route).post(url(route, '/hold'), body);
114
+ return response.data;
115
+ }
116
+ /** Devuelve un slot antes de que venza su bloqueo, para que se ofrezca de nuevo de inmediato. */
117
+ export async function releaseBookingSlot(route, body) {
118
+ await client(route).post(url(route, '/release'), body);
119
+ }
120
+ /**
121
+ * La hora `HH:mm` de un instante en la zona horaria de la organización. El picker muestra el reloj de
122
+ * quien recibe la cita, no el del navegador: un slot de las 09:00 en Bogotá es a las 09:00 aunque el
123
+ * que agenda esté en Madrid.
124
+ */
125
+ export function clockIn(iso, timeZone) {
126
+ return new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23', timeZone }).format(new Date(iso));
127
+ }
128
+ /** El día `YYYY-MM-DD` de un instante en una zona horaria. */
129
+ export function dayIn(iso, timeZone) {
130
+ return new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone }).format(new Date(iso));
131
+ }
132
+ /** La zona horaria del navegador, o `undefined` si no se puede leer. */
133
+ export function browserTimeZone() {
134
+ try {
135
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
136
+ }
137
+ catch {
138
+ return undefined;
139
+ }
140
+ }
141
+ /** Ciudades de zonas IANA cuyo nombre en español lleva tilde o cambia. */
142
+ const CITY_NAMES = {
143
+ Bogota: 'Bogotá',
144
+ Mexico_City: 'Ciudad de México',
145
+ Panama: 'Panamá',
146
+ Asuncion: 'Asunción',
147
+ Sao_Paulo: 'São Paulo',
148
+ New_York: 'Nueva York',
149
+ Los_Angeles: 'Los Ángeles',
150
+ Merida: 'Mérida',
151
+ Cancun: 'Cancún',
152
+ };
153
+ /**
154
+ * Cómo se nombra la zona de la organización junto a las horas: "hora de Bogotá (GMT-5)". El desfase
155
+ * se calcula en `at`, porque en las zonas con horario de verano cambia según el día.
156
+ */
157
+ export function zoneLabel(timeZone, at = new Date()) {
158
+ const id = timeZone.split('/').pop() ?? timeZone;
159
+ const city = CITY_NAMES[id] ?? id.replace(/_/g, ' ');
160
+ let offset;
161
+ try {
162
+ offset = new Intl.DateTimeFormat('en-US', { timeZone, timeZoneName: 'shortOffset' })
163
+ .formatToParts(at)
164
+ .find((part) => part.type === 'timeZoneName')?.value;
165
+ }
166
+ catch {
167
+ offset = undefined;
168
+ }
169
+ return offset ? `hora de ${city} (${offset})` : `hora de ${city}`;
170
+ }
171
+ /** 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. */
172
+ export function toPickerSlot(slot, timeZone) {
173
+ return {
174
+ id: slot.token,
175
+ startTime: clockIn(slot.start, timeZone),
176
+ endTime: clockIn(slot.end, timeZone),
177
+ available: true,
178
+ start: slot.start,
179
+ };
180
+ }
181
+ /**
182
+ * Las horas de un día como las muestra el picker: una por instante y en orden. El borde ya agrupa por
183
+ * instante; si llegara un instante repetido, gana el primero y el servidor elige a la persona al reservar.
184
+ */
185
+ export function toPickerSlots(slots, timeZone) {
186
+ const seen = new Set();
187
+ return [...slots]
188
+ .sort((a, b) => a.start.localeCompare(b.start))
189
+ .filter((slot) => {
190
+ if (seen.has(slot.start))
191
+ return false;
192
+ seen.add(slot.start);
193
+ return true;
194
+ })
195
+ .map((slot) => toPickerSlot(slot, timeZone));
196
+ }
197
+ /**
198
+ * Lo que la reserva aporta al valor del paso: día y horas reales en el reloj de la organización, la
199
+ * persona asignada y el tipo de atención elegido.
200
+ */
201
+ export function holdToValue(held, timeZone) {
202
+ return {
203
+ appointment: held.appointment,
204
+ hold: held.hold,
205
+ lockedUntil: held.lockedUntil,
206
+ date: dayIn(held.start, timeZone),
207
+ startTime: clockIn(held.start, timeZone),
208
+ endTime: clockIn(held.end, timeZone),
209
+ ...(held.person ? { personId: held.person.token, personName: held.person.name } : {}),
210
+ ...(held.eventTemplate
211
+ ? { eventTemplate: { name: held.eventTemplate.name, durationMinutes: held.eventTemplate.durationMinutes } }
212
+ : {}),
213
+ };
214
+ }
215
+ /** Un texto opcional de un campo de contacto: vacío o solo espacios cuenta como no dado. */
216
+ export function nonEmpty(text) {
217
+ return text?.trim() ? text.trim() : undefined;
218
+ }
219
+ /** Si un texto es un correo con forma válida (lo mismo que exige el borde al reservar). */
220
+ export function isValidEmail(text) {
221
+ return !!text && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text.trim());
222
+ }
223
+ /** El código HTTP de un error del cliente HTTP, si lo hay. */
224
+ export function statusOf(error) {
225
+ return error?.response?.status;
226
+ }
227
+ /**
228
+ * Lee un rechazo del borde. El borde responde `{ code, message }` con el mensaje en español; los
229
+ * errores que el gateway responde antes de la lambda siguen llegando como texto plano.
230
+ */
231
+ export function toBookingError(error, fallback = 'No pudimos reservar ese horario. Intenta de nuevo.') {
232
+ if (error instanceof BookingEdgeError)
233
+ return error;
234
+ const status = statusOf(error);
235
+ const data = error?.response?.data;
236
+ if (data && typeof data === 'object') {
237
+ const { code, message } = data;
238
+ if (typeof message === 'string' && message.trim()) {
239
+ return new BookingEdgeError(message, typeof code === 'string' ? code : undefined, status);
240
+ }
241
+ }
242
+ if (typeof data === 'string' && data.trim())
243
+ return new BookingEdgeError(data, undefined, status);
244
+ if (status === 429) {
245
+ return new BookingEdgeError('Hay muchas consultas en este momento. Intenta de nuevo en unos segundos.', BookingErrorCodes.RATE_LIMITED, status);
246
+ }
247
+ return new BookingEdgeError(fallback, undefined, status);
248
+ }
249
+ /** El motivo de un rechazo del borde, o un mensaje genérico. */
250
+ export function bookingErrorMessage(error, fallback) {
251
+ return toBookingError(error, fallback).message;
252
+ }
253
+ /**
254
+ * Si una renovación fallida significa que la reserva ya no existe: el borde lo dice (`HOLD_EXPIRED`,
255
+ * o el token venció), o el plazo pasó sin una renovación que llegara. Un corte de red o un 429 antes
256
+ * del plazo se reintenta en la siguiente vuelta.
257
+ */
258
+ export function holdLapsed(error, lockedUntil, now = Date.now()) {
259
+ if (error.code === BookingErrorCodes.HOLD_EXPIRED || error.code === BookingErrorCodes.STALE_OFFER)
260
+ return true;
261
+ if (error.code === BookingErrorCodes.NOT_FOUND || error.code === BookingErrorCodes.INVALID_REQUEST)
262
+ return true;
263
+ return !!lockedUntil && new Date(lockedUntil).getTime() <= now;
264
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,178 @@
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 { BookingEdgeError, BookingErrorCodes, bookingErrorMessage, cancelBookingAppointment, clockIn, dayIn, fetchBookingAppointment, fetchBookingOffer, fetchBookingSlots, holdBookingSlot, holdLapsed, holdToValue, isValidEmail, moveBookingAppointment, releaseBookingSlot, renewBookingHold, toBookingError, toPickerSlot, toPickerSlots, zoneLabel, } 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({
24
+ id: 'tok',
25
+ startTime: '09:00',
26
+ endTime: '09:30',
27
+ available: true,
28
+ start: '2026-09-22T14:00:00.000Z',
29
+ });
30
+ });
31
+ it('shows one hour per instant, in order', () => {
32
+ const slots = toPickerSlots([
33
+ { start: '2026-09-22T14:15:00.000Z', end: '2026-09-22T14:45:00.000Z', token: 'b' },
34
+ { start: '2026-09-22T14:00:00.000Z', end: '2026-09-22T14:30:00.000Z', token: 'a' },
35
+ { start: '2026-09-22T14:00:00.000Z', end: '2026-09-22T14:30:00.000Z', token: 'a2' },
36
+ ], 'America/Bogota');
37
+ expect(slots.map((slot) => [slot.id, slot.startTime])).toEqual([
38
+ ['a', '09:00'],
39
+ ['b', '09:15'],
40
+ ]);
41
+ });
42
+ it('names the organization clock with its offset on the given day', () => {
43
+ expect(zoneLabel('America/Bogota', new Date('2026-10-14T12:00:00Z'))).toBe('hora de Bogotá (GMT-5)');
44
+ expect(zoneLabel('Europe/Madrid', new Date('2026-07-01T12:00:00Z'))).toBe('hora de Madrid (GMT+2)');
45
+ expect(zoneLabel('Europe/Madrid', new Date('2026-12-01T12:00:00Z'))).toBe('hora de Madrid (GMT+1)');
46
+ });
47
+ it('takes the day, hours and assigned person of a hold from the hold itself', () => {
48
+ // 04:30Z is still the previous day in Bogotá.
49
+ expect(dayIn('2026-10-15T04:30:00.000Z', 'America/Bogota')).toBe('2026-10-14');
50
+ expect(holdToValue({
51
+ appointment: 'a1',
52
+ hold: 'h1',
53
+ lockedUntil: '2026-10-14T15:30:00.000Z',
54
+ renew: 'r1',
55
+ start: '2026-10-15T14:00:00.000Z',
56
+ end: '2026-10-15T14:30:00.000Z',
57
+ person: { token: 'pt', name: 'Bruno Pérez', role: 'Asesor' },
58
+ }, 'America/Bogota')).toEqual({
59
+ appointment: 'a1',
60
+ hold: 'h1',
61
+ lockedUntil: '2026-10-14T15:30:00.000Z',
62
+ date: '2026-10-15',
63
+ startTime: '09:00',
64
+ endTime: '09:30',
65
+ personId: 'pt',
66
+ personName: 'Bruno Pérez',
67
+ });
68
+ });
69
+ it('keeps the event template the hold was made with', () => {
70
+ expect(holdToValue({
71
+ appointment: 'a1',
72
+ hold: 'h1',
73
+ lockedUntil: '2026-10-14T15:30:00.000Z',
74
+ renew: 'r1',
75
+ start: '2026-10-15T14:00:00.000Z',
76
+ end: '2026-10-15T14:45:00.000Z',
77
+ eventTemplate: { name: 'Asesoría de crédito', durationMinutes: 45 },
78
+ }, 'America/Bogota').eventTemplate).toEqual({ name: 'Asesoría de crédito', durationMinutes: 45 });
79
+ });
80
+ it('narrows the offer, the slots and the hold to the picked event template', async () => {
81
+ vi.mocked(axiosInstance.get).mockResolvedValue({ data: { days: [], slots: [] } });
82
+ vi.mocked(axiosInstance.post).mockResolvedValue({ data: {} });
83
+ await fetchBookingOffer(ROUTE, { eventTemplate: 'tt-1' });
84
+ expect(axiosInstance.get).toHaveBeenLastCalledWith('/booking/form-key/BOOKING-1?eventTemplate=tt-1');
85
+ await fetchBookingSlots(ROUTE, '2026-09-22', { person: 'p', eventTemplate: 'tt-1' });
86
+ expect(axiosInstance.get).toHaveBeenLastCalledWith('/booking/form-key/BOOKING-1/slots?date=2026-09-22&person=p&eventTemplate=tt-1');
87
+ await holdBookingSlot(ROUTE, {
88
+ slot: 's',
89
+ contact: { name: 'Ana', email: 'ana@correo.com' },
90
+ eventTemplate: 'tt-1',
91
+ });
92
+ expect(axiosInstance.post).toHaveBeenLastCalledWith('/booking/form-key/BOOKING-1/hold', {
93
+ slot: 's',
94
+ contact: { name: 'Ana', email: 'ana@correo.com' },
95
+ eventTemplate: 'tt-1',
96
+ });
97
+ });
98
+ it('accepts only well-formed emails', () => {
99
+ expect(isValidEmail('juan@correo.com')).toBe(true);
100
+ expect(isValidEmail(' juan@correo.com ')).toBe(true);
101
+ expect(isValidEmail('juan@')).toBe(false);
102
+ expect(isValidEmail('juan correo.com')).toBe(false);
103
+ expect(isValidEmail(undefined)).toBe(false);
104
+ });
105
+ it('talks to the same-origin API for a form served under the organization', async () => {
106
+ vi.mocked(axiosInstance.get).mockResolvedValue({ data: { days: ['2026-09-22'] } });
107
+ await fetchBookingOffer(ROUTE);
108
+ expect(axiosInstance.get).toHaveBeenCalledWith('/booking/form-key/BOOKING-1');
109
+ await fetchBookingSlots(ROUTE, '2026-09-22', { person: 'person-token' });
110
+ expect(axiosInstance.get).toHaveBeenLastCalledWith('/booking/form-key/BOOKING-1/slots?date=2026-09-22&person=person-token');
111
+ expect(widgetInstance.get).not.toHaveBeenCalled();
112
+ });
113
+ it('names the organization and goes through the forms domain for an embedded form', async () => {
114
+ vi.mocked(widgetInstance.get).mockResolvedValue({ data: { days: [] } });
115
+ vi.mocked(widgetInstance.post).mockResolvedValue({ data: { appointment: 'a1', hold: 'h1', lockedUntil: 'x' } });
116
+ const route = { ...ROUTE, idOrganization: 'acme' };
117
+ await fetchBookingOffer(route, { person: 'p' });
118
+ expect(widgetInstance.get).toHaveBeenCalledWith('/booking/form-key/BOOKING-1?idOrganization=acme&person=p');
119
+ const held = await holdBookingSlot(route, {
120
+ slot: 'slot-token',
121
+ contact: { name: 'Ana', email: 'ana@correo.com' },
122
+ });
123
+ expect(widgetInstance.post).toHaveBeenCalledWith('/booking/form-key/BOOKING-1/hold?idOrganization=acme', {
124
+ slot: 'slot-token',
125
+ contact: { name: 'Ana', email: 'ana@correo.com' },
126
+ });
127
+ expect(held).toEqual({ appointment: 'a1', hold: 'h1', lockedUntil: 'x' });
128
+ // A renewal carries only the renewal token and the hold secret: no slot, no contact.
129
+ await renewBookingHold(route, { renew: 'r1', hold: 'h1' });
130
+ expect(widgetInstance.post).toHaveBeenLastCalledWith('/booking/form-key/BOOKING-1/hold?idOrganization=acme', {
131
+ renew: 'r1',
132
+ hold: 'h1',
133
+ });
134
+ await releaseBookingSlot(route, { appointment: 'a1', hold: 'h1' });
135
+ expect(widgetInstance.post).toHaveBeenLastCalledWith('/booking/form-key/BOOKING-1/release?idOrganization=acme', {
136
+ appointment: 'a1',
137
+ hold: 'h1',
138
+ });
139
+ expect(axiosInstance.get).not.toHaveBeenCalled();
140
+ });
141
+ it('talks about an appointment by its link, on the same origin or naming the organization', async () => {
142
+ vi.mocked(axiosInstance.get).mockResolvedValue({ data: { token: 't1' } });
143
+ vi.mocked(widgetInstance.post).mockResolvedValue({ data: { token: 't2' } });
144
+ await expect(fetchBookingAppointment('a.b')).resolves.toEqual({ token: 't1' });
145
+ expect(axiosInstance.get).toHaveBeenCalledWith('/booking/appointment/a.b');
146
+ await expect(cancelBookingAppointment('a.b', 'acme')).resolves.toEqual({ token: 't2' });
147
+ expect(widgetInstance.post).toHaveBeenCalledWith('/booking/appointment/a.b/cancel?idOrganization=acme');
148
+ await moveBookingAppointment('a.b', 'slot-1', 'acme');
149
+ expect(widgetInstance.post).toHaveBeenLastCalledWith('/booking/appointment/a.b/move?idOrganization=acme', {
150
+ slot: 'slot-1',
151
+ });
152
+ });
153
+ it('reads the edge’s { code, message } refusal, plain text, and a generic message otherwise', () => {
154
+ const stale = toBookingError({
155
+ response: { status: 403, data: { code: 'STALE_OFFER', message: 'Esa persona ya no está disponible' } },
156
+ });
157
+ expect(stale).toBeInstanceOf(BookingEdgeError);
158
+ expect(stale.code).toBe(BookingErrorCodes.STALE_OFFER);
159
+ expect(stale.status).toBe(403);
160
+ expect(stale.message).toBe('Esa persona ya no está disponible');
161
+ expect(bookingErrorMessage({ response: { status: 404, data: 'Organization not found' } })).toBe('Organization not found');
162
+ expect(toBookingError({ response: { status: 429, data: '' } }).code).toBe(BookingErrorCodes.RATE_LIMITED);
163
+ expect(bookingErrorMessage(new Error('network'))).toBe('No pudimos reservar ese horario. Intenta de nuevo.');
164
+ expect(bookingErrorMessage(new Error('network'), 'No pudimos cargar los horarios.')).toBe('No pudimos cargar los horarios.');
165
+ });
166
+ it('gives a hold up only when the edge says so or its time ran out', () => {
167
+ const now = new Date('2026-10-14T15:00:00.000Z').getTime();
168
+ const later = '2026-10-14T15:10:00.000Z';
169
+ const earlier = '2026-10-14T14:59:00.000Z';
170
+ const refusal = (code) => new BookingEdgeError('x', code);
171
+ expect(holdLapsed(refusal(BookingErrorCodes.HOLD_EXPIRED), later, now)).toBe(true);
172
+ expect(holdLapsed(refusal(BookingErrorCodes.STALE_OFFER), later, now)).toBe(true);
173
+ // A network failure or a rate limit is retried on the next round, while the lock still runs.
174
+ expect(holdLapsed(refusal(), later, now)).toBe(false);
175
+ expect(holdLapsed(refusal(BookingErrorCodes.RATE_LIMITED), later, now)).toBe(false);
176
+ expect(holdLapsed(refusal(), earlier, now)).toBe(true);
177
+ });
178
+ });
@@ -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>;