@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 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
import { addDays, addMonths, addWeeks, format, nextMonday } from 'date-fns';
|
|
7
|
+
import ErkCalendar from './ErkCalendar';
|
|
8
|
+
import { occursOnDay } from './CalendarFunctions';
|
|
9
|
+
import MaterialProviders from '../../Utils/MaterialProviders';
|
|
10
|
+
import InternalFormStyle from '../../constants/InternalFormStyle';
|
|
11
|
+
import { CalendarEventCategory, } from '../../@Types/CalendarFormStep';
|
|
12
|
+
function todayISO(hhmm) {
|
|
13
|
+
return `${format(new Date(), 'yyyy-MM-dd')}T${hhmm}:00`;
|
|
14
|
+
}
|
|
15
|
+
const MEETING = {
|
|
16
|
+
id: 'evt-1',
|
|
17
|
+
category: CalendarEventCategory.MEETING,
|
|
18
|
+
title: 'Standup equipo',
|
|
19
|
+
start: todayISO('09:00'),
|
|
20
|
+
end: todayISO('09:45'),
|
|
21
|
+
};
|
|
22
|
+
const VALUE = { type: 'CALENDAR_VALUE', events: [MEETING] };
|
|
23
|
+
function renderCalendar(props = {}) {
|
|
24
|
+
const onChange = vi.fn();
|
|
25
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkCalendar, { value: null, onChange: onChange, minTime: "07:00", maxTime: "18:00", ...props }) }));
|
|
26
|
+
return { onChange };
|
|
27
|
+
}
|
|
28
|
+
describe('ErkCalendar', () => {
|
|
29
|
+
it('creates an availability window through the "+ Nuevo evento" popover', async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
const { onChange } = renderCalendar();
|
|
32
|
+
await user.click(screen.getByRole('button', { name: '+ Nuevo evento' }));
|
|
33
|
+
// El modo por defecto es Disponibilidad (los espacios agendables)
|
|
34
|
+
expect(await screen.findByText('Nueva disponibilidad')).toBeInTheDocument();
|
|
35
|
+
await user.click(screen.getByRole('button', { name: 'Crear disponibilidad' }));
|
|
36
|
+
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({
|
|
37
|
+
type: 'CALENDAR_VALUE',
|
|
38
|
+
events: [
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
category: CalendarEventCategory.WORKING,
|
|
41
|
+
start: todayISO('09:00'),
|
|
42
|
+
end: todayISO('10:00'),
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
it('creates an all-day event through the Evento tab', async () => {
|
|
48
|
+
const user = userEvent.setup();
|
|
49
|
+
const { onChange } = renderCalendar();
|
|
50
|
+
await user.click(screen.getByRole('button', { name: '+ Nuevo evento' }));
|
|
51
|
+
await user.click(await screen.findByRole('button', { name: 'Evento' }));
|
|
52
|
+
await user.type(screen.getByLabelText('Título'), 'Capacitación anual');
|
|
53
|
+
await user.click(screen.getByLabelText('Todo el día'));
|
|
54
|
+
await user.click(screen.getByRole('button', { name: 'Crear evento' }));
|
|
55
|
+
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({
|
|
56
|
+
events: [
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
category: CalendarEventCategory.MEETING,
|
|
59
|
+
title: 'Capacitación anual',
|
|
60
|
+
isAllDay: true,
|
|
61
|
+
start: todayISO('00:00'),
|
|
62
|
+
end: todayISO('23:59'),
|
|
63
|
+
}),
|
|
64
|
+
],
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
it('deletes an event from its detail popover', async () => {
|
|
68
|
+
const user = userEvent.setup();
|
|
69
|
+
const { onChange } = renderCalendar({ value: VALUE });
|
|
70
|
+
await user.click(screen.getByText('Standup equipo'));
|
|
71
|
+
await user.click(await screen.findByRole('button', { name: 'Eliminar' }));
|
|
72
|
+
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ type: 'CALENDAR_VALUE', events: [] }));
|
|
73
|
+
});
|
|
74
|
+
it('toggles category visibility from the sidebar', async () => {
|
|
75
|
+
const user = userEvent.setup();
|
|
76
|
+
renderCalendar({ value: VALUE });
|
|
77
|
+
expect(screen.getByText('Standup equipo')).toBeInTheDocument();
|
|
78
|
+
await user.click(screen.getByRole('button', { name: 'Reunión' }));
|
|
79
|
+
expect(screen.queryByText('Standup equipo')).not.toBeInTheDocument();
|
|
80
|
+
await user.click(screen.getByRole('button', { name: 'Reunión' }));
|
|
81
|
+
expect(screen.getByText('Standup equipo')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
it('paints the business-days fallback when a day has no explicit availability', () => {
|
|
84
|
+
renderCalendar({
|
|
85
|
+
value: VALUE,
|
|
86
|
+
businessDays: { start: '08:00', end: '17:00', holidays: [] },
|
|
87
|
+
});
|
|
88
|
+
expect(screen.getAllByText('Días hábiles').length).toBeGreaterThan(0);
|
|
89
|
+
});
|
|
90
|
+
it('overlays shared sources read-only and toggles them from "Compartidos"', async () => {
|
|
91
|
+
const user = userEvent.setup();
|
|
92
|
+
renderCalendar({
|
|
93
|
+
value: VALUE,
|
|
94
|
+
sources: [
|
|
95
|
+
{
|
|
96
|
+
id: 'org',
|
|
97
|
+
name: 'Organización',
|
|
98
|
+
color: '#b7791f',
|
|
99
|
+
events: [
|
|
100
|
+
{
|
|
101
|
+
id: 'org-1',
|
|
102
|
+
category: CalendarEventCategory.MEETING,
|
|
103
|
+
title: 'Evento de organización',
|
|
104
|
+
start: todayISO('11:00'),
|
|
105
|
+
end: todayISO('12:00'),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
// El evento de la fuente se ve, pero su detalle no permite eliminar
|
|
112
|
+
await user.click(screen.getByText('Evento de organización'));
|
|
113
|
+
expect(await screen.findAllByText('Evento de organización')).toHaveLength(2);
|
|
114
|
+
expect(screen.queryByRole('button', { name: 'Eliminar' })).not.toBeInTheDocument();
|
|
115
|
+
await user.keyboard('{Escape}');
|
|
116
|
+
// Toggle de visibilidad en "Compartidos"
|
|
117
|
+
await user.click(screen.getByRole('button', { name: 'Organización' }));
|
|
118
|
+
expect(screen.queryByText('Evento de organización')).not.toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
it('hides creation and deletion when readOnly', async () => {
|
|
121
|
+
const user = userEvent.setup();
|
|
122
|
+
renderCalendar({ value: VALUE, readOnly: true });
|
|
123
|
+
expect(screen.queryByRole('button', { name: '+ Nuevo evento' })).not.toBeInTheDocument();
|
|
124
|
+
await user.click(screen.getByText('Standup equipo'));
|
|
125
|
+
// El popover de detalle abre (el título aparece en el bloque y en el popover)…
|
|
126
|
+
expect(await screen.findAllByText('Standup equipo')).toHaveLength(2);
|
|
127
|
+
// …pero sin acciones de edición
|
|
128
|
+
expect(screen.queryByRole('button', { name: 'Eliminar' })).not.toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('occursOnDay (RRULE)', () => {
|
|
132
|
+
const base = {
|
|
133
|
+
id: 'evt-r',
|
|
134
|
+
category: CalendarEventCategory.WORKING,
|
|
135
|
+
start: todayISO('08:00'),
|
|
136
|
+
end: todayISO('16:00'),
|
|
137
|
+
recurrence: 'FREQ=WEEKLY',
|
|
138
|
+
};
|
|
139
|
+
it('weekly: matches the same weekday on later weeks', () => {
|
|
140
|
+
expect(occursOnDay(base, new Date())).toBe(true);
|
|
141
|
+
expect(occursOnDay(base, addWeeks(new Date(), 1))).toBe(true);
|
|
142
|
+
expect(occursOnDay(base, addDays(new Date(), 1))).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
it('weekly: respects INTERVAL=2', () => {
|
|
145
|
+
const biweekly = { ...base, recurrence: 'FREQ=WEEKLY;INTERVAL=2' };
|
|
146
|
+
expect(occursOnDay(biweekly, addWeeks(new Date(), 1))).toBe(false);
|
|
147
|
+
expect(occursOnDay(biweekly, addWeeks(new Date(), 2))).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
it('weekly BYDAY: matches business days (L–V) regardless of the start weekday', () => {
|
|
150
|
+
const businessDays = { ...base, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' };
|
|
151
|
+
const monday = nextMonday(new Date());
|
|
152
|
+
expect(occursOnDay(businessDays, monday)).toBe(true);
|
|
153
|
+
expect(occursOnDay(businessDays, addDays(monday, 4))).toBe(true);
|
|
154
|
+
expect(occursOnDay(businessDays, addDays(monday, 5))).toBe(false); // sábado
|
|
155
|
+
});
|
|
156
|
+
it('daily: matches every day (with INTERVAL)', () => {
|
|
157
|
+
const daily = { ...base, recurrence: 'FREQ=DAILY' };
|
|
158
|
+
expect(occursOnDay(daily, addDays(new Date(), 1))).toBe(true);
|
|
159
|
+
const everyThree = { ...base, recurrence: 'FREQ=DAILY;INTERVAL=3' };
|
|
160
|
+
expect(occursOnDay(everyThree, addDays(new Date(), 3))).toBe(true);
|
|
161
|
+
expect(occursOnDay(everyThree, addDays(new Date(), 4))).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
it('monthly: matches the same day of month', () => {
|
|
164
|
+
const monthly = { ...base, recurrence: 'FREQ=MONTHLY' };
|
|
165
|
+
expect(occursOnDay(monthly, addMonths(new Date(), 1))).toBe(true);
|
|
166
|
+
expect(occursOnDay(monthly, addDays(addMonths(new Date(), 1), 1))).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('never matches before the event start', () => {
|
|
169
|
+
expect(occursOnDay(base, addWeeks(new Date(), -1))).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -2,11 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useMemo } from 'react';
|
|
3
3
|
import { format as dateFnsFormat, isValid } from 'date-fns';
|
|
4
4
|
import { InputAdornment, styled, Dialog as MuiDialog } from '@mui/material';
|
|
5
|
-
import { MobileDatePicker as MuiDatePicker, MobileDateTimePicker as MuiDateTimePicker, DatePickerToolbar as MuiDatePickerToolbar, DateTimePickerToolbar as MuiDateTimePickerToolbar, usePickerContext, } from '@mui/x-date-pickers';
|
|
6
|
-
import { PickerDay as MuiPickerDay } from '@mui/x-date-pickers/PickerDay';
|
|
5
|
+
import { MobileDatePicker as MuiDatePicker, MobileDateTimePicker as MuiDateTimePicker, DatePickerToolbar as MuiDatePickerToolbar, DateTimePickerToolbar as MuiDateTimePickerToolbar, usePickerContext, PickerDay as MuiPickerDay, renderTimeViewClock, } from '@mui/x-date-pickers';
|
|
7
6
|
import { StyledTextField } from '../ErkTextField/ErkTextField';
|
|
8
7
|
import CalendarIcon from '../../Icons/CalendarIcon';
|
|
9
|
-
import { renderTimeViewClock } from '@mui/x-date-pickers/timeViewRenderers';
|
|
10
8
|
const StyledDateTimePicker = styled(MuiDateTimePicker, {
|
|
11
9
|
shouldForwardProp: (propName) => propName !== 'size',
|
|
12
10
|
})(({ disabled, readOnly, size }) => ({
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { DateRange } from '@mui/x-date-pickers-pro';
|
|
4
|
-
import { PickersShortcutsItem } from '@mui/x-date-pickers/PickersShortcuts';
|
|
2
|
+
import { PickersShortcutsItem } from '@mui/x-date-pickers';
|
|
3
|
+
import { MobileDateRangePickerProps as MuiDateRangePickerProps, DateRange } from '@mui/x-date-pickers-pro';
|
|
5
4
|
import { ErkTextFieldProps } from '../ErkTextField/ErkTextField';
|
|
6
5
|
interface ErkDateRangePickerProps extends Omit<MuiDateRangePickerProps, 'ref' | 'onChange' | 'value' | 'slotProps'>, Pick<ErkTextFieldProps, 'error' | 'required' | 'helperText' | 'size'> {
|
|
7
6
|
onChange?: (dateRange: DateRange<Date>) => void;
|
|
@@ -2,8 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useMemo } from 'react';
|
|
3
3
|
import { InputAdornment, styled, Dialog as MuiDialog } from '@mui/material';
|
|
4
4
|
import { DatePickerToolbar as MuiDatePickerToolbar, usePickerContext, } from '@mui/x-date-pickers';
|
|
5
|
-
import { MobileDateRangePicker as MuiDateRangePicker, } from '@mui/x-date-pickers-pro
|
|
6
|
-
import { DateRangePickerDay as MuiDateRangePickerDay } from '@mui/x-date-pickers-pro';
|
|
5
|
+
import { MobileDateRangePicker as MuiDateRangePicker, DateRangePickerDay as MuiDateRangePickerDay, } from '@mui/x-date-pickers-pro';
|
|
7
6
|
import { format, isValid } from 'date-fns';
|
|
8
7
|
import { StyledTextField } from '../ErkTextField/ErkTextField';
|
|
9
8
|
import CalendarIcon from '../../Icons/CalendarIcon';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LocationInputMode, LocationStep, LocationStepValue } from '../../@Types/LocationFormStep';
|
|
3
|
+
export interface ErkLocationFieldProps {
|
|
4
|
+
value?: LocationStepValue | null;
|
|
5
|
+
onChange: (value: LocationStepValue | null) => void;
|
|
6
|
+
allowedModes: LocationInputMode[];
|
|
7
|
+
defaultMode?: LocationInputMode;
|
|
8
|
+
addressFields?: LocationStep['addressFields'];
|
|
9
|
+
/** ISO-2 para acotar el buscador (ej. ['co']). */
|
|
10
|
+
regionCodes?: string[];
|
|
11
|
+
error?: boolean;
|
|
12
|
+
helperText?: string | null;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
inputRef?: React.Ref<HTMLInputElement>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Campo de ubicación (rehecho con Base UI). Ofrece los modos habilitados
|
|
19
|
+
* (buscador de Google Places, GPS, entrada manual) y aplica la config por campo
|
|
20
|
+
* (fijo / omitir) al valor resultante.
|
|
21
|
+
*/
|
|
22
|
+
declare function ErkLocationField({ value, onChange, allowedModes, defaultMode, addressFields, regionCodes, error, helperText, required, readOnly, inputRef, }: ErkLocationFieldProps): JSX.Element;
|
|
23
|
+
export default ErkLocationField;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
import { AddressFieldMode, LocationInputMode, } from '../../@Types/LocationFormStep';
|
|
4
|
+
import ErkAddressAutocomplete from '../ErkAddressAutocomplete/ErkAddressAutocomplete';
|
|
5
|
+
import { reverseGeocode } from '../ErkAddressAutocomplete/googlePlaces';
|
|
6
|
+
import { ADDRESS_FIELDS, applyAddressFields, fieldConfig } from './addressFields';
|
|
7
|
+
import ErkTextField from '../ErkTextField/ErkTextField';
|
|
8
|
+
import ErkButton from '../ErkButton/ErkButton';
|
|
9
|
+
import LocationIcon from '../../Icons/LocationIcon';
|
|
10
|
+
import styles from './ErkLocationField.module.css';
|
|
11
|
+
const MODE_LABELS = {
|
|
12
|
+
[LocationInputMode.GOOGLE_PLACES]: 'Buscar',
|
|
13
|
+
[LocationInputMode.GPS]: 'Mi ubicación',
|
|
14
|
+
[LocationInputMode.MANUAL]: 'Manual',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Campo de ubicación (rehecho con Base UI). Ofrece los modos habilitados
|
|
18
|
+
* (buscador de Google Places, GPS, entrada manual) y aplica la config por campo
|
|
19
|
+
* (fijo / omitir) al valor resultante.
|
|
20
|
+
*/
|
|
21
|
+
function ErkLocationField({ value, onChange, allowedModes, defaultMode, addressFields, regionCodes = ['co'], error = false, helperText, required = false, readOnly = false, inputRef, }) {
|
|
22
|
+
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? '';
|
|
23
|
+
const initialMode = defaultMode && allowedModes.includes(defaultMode) ? defaultMode : allowedModes[0];
|
|
24
|
+
const [mode, setMode] = useState(initialMode);
|
|
25
|
+
const [gpsLoading, setGpsLoading] = useState(false);
|
|
26
|
+
const [gpsError, setGpsError] = useState(null);
|
|
27
|
+
const handleGoogle = useCallback((resolved) => {
|
|
28
|
+
onChange({ ...resolved, addressDetail: applyAddressFields(resolved.addressDetail ?? {}, addressFields) });
|
|
29
|
+
}, [onChange, addressFields]);
|
|
30
|
+
const handleGps = useCallback(() => {
|
|
31
|
+
if (!navigator.geolocation) {
|
|
32
|
+
setGpsError('La geolocalización no es compatible con este navegador.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setGpsLoading(true);
|
|
36
|
+
setGpsError(null);
|
|
37
|
+
navigator.geolocation.getCurrentPosition((position) => {
|
|
38
|
+
const lat = position.coords.latitude;
|
|
39
|
+
const lng = position.coords.longitude;
|
|
40
|
+
void reverseGeocode(apiKey, lat, lng)
|
|
41
|
+
.then(({ address, addressDetail }) => {
|
|
42
|
+
onChange({
|
|
43
|
+
source: LocationInputMode.GPS,
|
|
44
|
+
latitude: lat,
|
|
45
|
+
longitude: lng,
|
|
46
|
+
address: address ?? `${lat.toFixed(6)}, ${lng.toFixed(6)}`,
|
|
47
|
+
addressDetail: applyAddressFields(addressDetail, addressFields),
|
|
48
|
+
});
|
|
49
|
+
})
|
|
50
|
+
.finally(() => setGpsLoading(false));
|
|
51
|
+
}, () => {
|
|
52
|
+
setGpsError('No se pudo obtener la ubicación.');
|
|
53
|
+
setGpsLoading(false);
|
|
54
|
+
}, { enableHighAccuracy: false, timeout: 15000, maximumAge: 300000 });
|
|
55
|
+
}, [apiKey, onChange, addressFields]);
|
|
56
|
+
const updateManualField = useCallback((key, text) => {
|
|
57
|
+
const base = value?.source === LocationInputMode.MANUAL ? (value.addressDetail ?? {}) : {};
|
|
58
|
+
const nextDetail = applyAddressFields({ ...base, [key]: text || undefined }, addressFields);
|
|
59
|
+
const address = ADDRESS_FIELDS.map(({ key: k }) => nextDetail[k])
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(', ');
|
|
62
|
+
onChange({ source: LocationInputMode.MANUAL, address, addressDetail: nextDetail });
|
|
63
|
+
}, [value, onChange, addressFields]);
|
|
64
|
+
const manualDetail = value?.source === LocationInputMode.MANUAL ? (value.addressDetail ?? {}) : {};
|
|
65
|
+
return (_jsxs("div", { className: styles.container, children: [allowedModes.length > 1 && (_jsx("div", { className: styles.modeSwitch, children: allowedModes.map((m) => (_jsx(ErkButton, { color: m === mode ? 'primary' : 'secondary', disabled: readOnly, onClick: () => setMode(m), children: MODE_LABELS[m] }, m))) })), mode === LocationInputMode.GOOGLE_PLACES && (_jsx(ErkAddressAutocomplete, { value: value, onChange: handleGoogle, regionCodes: regionCodes, error: error, required: required, readOnly: readOnly, inputRef: inputRef })), mode === LocationInputMode.GPS && (_jsxs(ErkButton, { color: "primary", loading: gpsLoading, disabled: readOnly, onClick: handleGps, children: [_jsx(LocationIcon, { fill: "currentColor" }), " Usar mi ubicaci\u00F3n actual"] })), mode === LocationInputMode.MANUAL && (_jsx("div", { className: styles.manualGrid, children: ADDRESS_FIELDS.map(({ key, label }) => {
|
|
66
|
+
const config = fieldConfig(addressFields, key);
|
|
67
|
+
if (config.mode === AddressFieldMode.OMIT)
|
|
68
|
+
return null;
|
|
69
|
+
const fixed = config.mode === AddressFieldMode.FIXED;
|
|
70
|
+
return (_jsx(ErkTextField, { label: fixed ? `${label} (fijo)` : label, value: fixed ? (config.fixedValue ?? '') : (manualDetail[key] ?? ''), readOnly: fixed || readOnly, onChange: (text) => updateManualField(key, text) }, key));
|
|
71
|
+
}) })), (gpsError ?? helperText) && (_jsx("div", { className: error || gpsError ? styles.errorText : styles.helperText, children: gpsError ?? helperText }))] }));
|
|
72
|
+
}
|
|
73
|
+
export default ErkLocationField;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 10px;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.modeSwitch {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: 8px;
|
|
11
|
+
flex-wrap: wrap;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.manualGrid {
|
|
15
|
+
display: grid;
|
|
16
|
+
grid-template-columns: repeat(2, 1fr);
|
|
17
|
+
gap: 10px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.helperText {
|
|
21
|
+
font-size: 12px;
|
|
22
|
+
color: var(--eureka-text, #666);
|
|
23
|
+
opacity: 0.7;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.errorText {
|
|
27
|
+
font-size: 12px;
|
|
28
|
+
color: var(--eureka-error, #e53935);
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AddressFieldConfig, AddressFieldKey, LocationAddressDetail, LocationStep } from '../../@Types/LocationFormStep';
|
|
2
|
+
/** Campos de dirección en orden de render, con su etiqueta de UI. */
|
|
3
|
+
export declare const ADDRESS_FIELDS: {
|
|
4
|
+
key: AddressFieldKey;
|
|
5
|
+
label: string;
|
|
6
|
+
}[];
|
|
7
|
+
export declare function fieldConfig(addressFields: LocationStep['addressFields'], key: AddressFieldKey): AddressFieldConfig;
|
|
8
|
+
/**
|
|
9
|
+
* Aplica la config por campo a una dirección resuelta (Google/GPS):
|
|
10
|
+
* los `FIXED` se sobreescriben con el valor del diseñador y los `OMIT` se descartan.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyAddressFields(detail: LocationAddressDetail, addressFields: LocationStep['addressFields']): LocationAddressDetail;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AddressFieldMode, } from '../../@Types/LocationFormStep';
|
|
2
|
+
/** Campos de dirección en orden de render, con su etiqueta de UI. */
|
|
3
|
+
export const ADDRESS_FIELDS = [
|
|
4
|
+
{ key: 'street', label: 'Calle' },
|
|
5
|
+
{ key: 'streetNumber', label: 'Número' },
|
|
6
|
+
{ key: 'unit', label: 'Interior / Apto' },
|
|
7
|
+
{ key: 'neighborhood', label: 'Barrio' },
|
|
8
|
+
{ key: 'city', label: 'Ciudad' },
|
|
9
|
+
{ key: 'state', label: 'Departamento / Región' },
|
|
10
|
+
{ key: 'postalCode', label: 'Código postal' },
|
|
11
|
+
{ key: 'country', label: 'País' },
|
|
12
|
+
];
|
|
13
|
+
export function fieldConfig(addressFields, key) {
|
|
14
|
+
return addressFields?.[key] ?? { mode: AddressFieldMode.ASK };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Aplica la config por campo a una dirección resuelta (Google/GPS):
|
|
18
|
+
* los `FIXED` se sobreescriben con el valor del diseñador y los `OMIT` se descartan.
|
|
19
|
+
*/
|
|
20
|
+
export function applyAddressFields(detail, addressFields) {
|
|
21
|
+
if (!addressFields)
|
|
22
|
+
return detail;
|
|
23
|
+
const result = { ...detail };
|
|
24
|
+
for (const { key } of ADDRESS_FIELDS) {
|
|
25
|
+
const config = addressFields[key];
|
|
26
|
+
if (!config)
|
|
27
|
+
continue;
|
|
28
|
+
if (config.mode === AddressFieldMode.OMIT)
|
|
29
|
+
delete result[key];
|
|
30
|
+
else if (config.mode === AddressFieldMode.FIXED)
|
|
31
|
+
result[key] = config.fixedValue;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { LocationRestriction } from '../../@Types/LocationFormStep';
|
|
2
|
+
/**
|
|
3
|
+
* Valida una coordenada contra todas las restricciones (lógica AND).
|
|
4
|
+
* Devuelve el mensaje de error de la primera restricción que falla, o `null` si es válida.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateRestrictions(lat: number, lng: number, restrictions: LocationRestriction[] | undefined): string | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { LocationRestrictionBehavior, LocationRestrictionType, } from '../../@Types/LocationFormStep';
|
|
2
|
+
const EARTH_RADIUS_M = 6371000;
|
|
3
|
+
function toRad(deg) {
|
|
4
|
+
return (deg * Math.PI) / 180;
|
|
5
|
+
}
|
|
6
|
+
/** Distancia en metros entre dos coordenadas (haversine). */
|
|
7
|
+
function haversineMeters(aLat, aLng, bLat, bLng) {
|
|
8
|
+
const dLat = toRad(bLat - aLat);
|
|
9
|
+
const dLng = toRad(bLng - aLng);
|
|
10
|
+
const lat1 = toRad(aLat);
|
|
11
|
+
const lat2 = toRad(bLat);
|
|
12
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
|
|
13
|
+
return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
14
|
+
}
|
|
15
|
+
/** Punto dentro de polígono (ray casting). El polígono se cierra automáticamente. */
|
|
16
|
+
function pointInPolygon(lat, lng, polygon) {
|
|
17
|
+
let inside = false;
|
|
18
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
19
|
+
const yi = polygon[i].latitude;
|
|
20
|
+
const xi = polygon[i].longitude;
|
|
21
|
+
const yj = polygon[j].latitude;
|
|
22
|
+
const xj = polygon[j].longitude;
|
|
23
|
+
const intersects = yi > lat !== yj > lat && lng < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
|
|
24
|
+
if (intersects)
|
|
25
|
+
inside = !inside;
|
|
26
|
+
}
|
|
27
|
+
return inside;
|
|
28
|
+
}
|
|
29
|
+
function isInside(lat, lng, restriction) {
|
|
30
|
+
if (restriction.type === LocationRestrictionType.RADIUS) {
|
|
31
|
+
return (haversineMeters(lat, lng, restriction.center.latitude, restriction.center.longitude) <=
|
|
32
|
+
restriction.radiusMeters);
|
|
33
|
+
}
|
|
34
|
+
return pointInPolygon(lat, lng, restriction.polygon);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Valida una coordenada contra todas las restricciones (lógica AND).
|
|
38
|
+
* Devuelve el mensaje de error de la primera restricción que falla, o `null` si es válida.
|
|
39
|
+
*/
|
|
40
|
+
export function validateRestrictions(lat, lng, restrictions) {
|
|
41
|
+
for (const restriction of restrictions ?? []) {
|
|
42
|
+
const inside = isInside(lat, lng, restriction);
|
|
43
|
+
if (restriction.behavior === LocationRestrictionBehavior.ALLOW && !inside) {
|
|
44
|
+
return restriction.errorMessage ?? 'La ubicación está fuera de la zona permitida.';
|
|
45
|
+
}
|
|
46
|
+
if (restriction.behavior === LocationRestrictionBehavior.DENY && inside) {
|
|
47
|
+
return restriction.errorMessage ?? 'La ubicación está en una zona no permitida.';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LocationInputMode, LocationStepValue } from '../../@Types/LocationFormStep';
|
|
3
|
+
export interface ErkLocationInputProps {
|
|
4
|
+
value?: LocationStepValue | null;
|
|
5
|
+
onChange?: (value: LocationStepValue | null) => void;
|
|
6
|
+
allowedModes: LocationInputMode[];
|
|
7
|
+
defaultMode?: LocationInputMode;
|
|
8
|
+
label: string;
|
|
9
|
+
helperText?: string | null;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
error?: boolean;
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
inputRef?: React.Ref<any>;
|
|
14
|
+
}
|
|
15
|
+
export default function ErkLocationInput({ value, onChange, allowedModes, label, helperText, required, error, readOnly, inputRef, }: ErkLocationInputProps): JSX.Element;
|