@arquimedes.co/eureka-forms 3.0.53-new-steps → 3.0.55-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/CalendarFormStep.d.ts +20 -0
- package/dist/@Types/CalendarFormStep.js +10 -0
- package/dist/FormSteps/CalendarStep/MaterialCalendarStep/MaterialCalendarStep.d.ts +3 -2
- package/dist/FormSteps/CalendarStep/MaterialCalendarStep/MaterialCalendarStep.js +6 -5
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.d.ts +25 -0
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.js +112 -0
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.module.css +88 -0
- package/dist/Shared/ErkAvailabilityPicker/ErkAvailabilityPicker.test.js +75 -0
- package/dist/index.lib.d.ts +3 -3
- package/dist/index.lib.js +2 -2
- package/package.json +1 -1
- package/dist/Shared/ErkCalendar/CalendarFunctions.d.ts +0 -28
- package/dist/Shared/ErkCalendar/CalendarFunctions.js +0 -121
- package/dist/Shared/ErkCalendar/CalendarPopovers.d.ts +0 -45
- package/dist/Shared/ErkCalendar/CalendarPopovers.js +0 -110
- package/dist/Shared/ErkCalendar/ErkCalendar.d.ts +0 -73
- package/dist/Shared/ErkCalendar/ErkCalendar.js +0 -293
- package/dist/Shared/ErkCalendar/ErkCalendar.module.css +0 -775
- package/dist/Shared/ErkCalendar/ErkCalendar.test.js +0 -171
- /package/dist/Shared/{ErkCalendar/ErkCalendar.test.d.ts → ErkAvailabilityPicker/ErkAvailabilityPicker.test.d.ts} +0 -0
|
@@ -43,6 +43,26 @@ export declare enum CalendarDefaultView {
|
|
|
43
43
|
MONTH = "month",
|
|
44
44
|
AGENDA = "agenda"
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Quién puede ver el detalle del evento.
|
|
48
|
+
*/
|
|
49
|
+
export declare enum CalendarEventVisibility {
|
|
50
|
+
/** Solo el dueño del calendario ve el detalle. */
|
|
51
|
+
PERSONAL = "PERSONAL",
|
|
52
|
+
/** Visible para toda la organización. */
|
|
53
|
+
ORGANIZATION = "ORGANIZATION"
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Invitado / implicado en un evento (como los invitados de Google Calendar).
|
|
57
|
+
*/
|
|
58
|
+
export interface CalendarAttendee {
|
|
59
|
+
/** ID de la persona en el sistema (si es un agente interno). */
|
|
60
|
+
id?: string;
|
|
61
|
+
/** Nombre visible. */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Correo (invitados externos o para notificar). */
|
|
64
|
+
email?: string;
|
|
65
|
+
}
|
|
46
66
|
/**
|
|
47
67
|
* Plantilla de tipo de evento que el usuario puede seleccionar al crear un bloque.
|
|
48
68
|
* Equivale a las "categorías de evento" visibles en la UI del scheduler.
|
|
@@ -46,3 +46,13 @@ export var CalendarDefaultView;
|
|
|
46
46
|
CalendarDefaultView["MONTH"] = "month";
|
|
47
47
|
CalendarDefaultView["AGENDA"] = "agenda";
|
|
48
48
|
})(CalendarDefaultView || (CalendarDefaultView = {}));
|
|
49
|
+
/**
|
|
50
|
+
* Quién puede ver el detalle del evento.
|
|
51
|
+
*/
|
|
52
|
+
export var CalendarEventVisibility;
|
|
53
|
+
(function (CalendarEventVisibility) {
|
|
54
|
+
/** Solo el dueño del calendario ve el detalle. */
|
|
55
|
+
CalendarEventVisibility["PERSONAL"] = "PERSONAL";
|
|
56
|
+
/** Visible para toda la organización. */
|
|
57
|
+
CalendarEventVisibility["ORGANIZATION"] = "ORGANIZATION";
|
|
58
|
+
})(CalendarEventVisibility || (CalendarEventVisibility = {}));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CalendarStepProps } from '../CalendarStep';
|
|
2
2
|
/**
|
|
3
|
-
* Conector del paso CALENDAR:
|
|
4
|
-
*
|
|
3
|
+
* Conector del paso CALENDAR/Availability: pregunta de disponibilidad compacta
|
|
4
|
+
* (grid semanal día × hora). El calendario completo vive en el admin; el form
|
|
5
|
+
* solo captura ventanas disponibles que un paso BOOKING puede consumir.
|
|
5
6
|
*/
|
|
6
7
|
declare function MaterialCalendarStep({ step, editable }: CalendarStepProps): JSX.Element;
|
|
7
8
|
export default MaterialCalendarStep;
|
|
@@ -4,10 +4,11 @@ import { useFormStep } from '../../StepHooks';
|
|
|
4
4
|
import FormContext from '../../../Contexts/FormContext';
|
|
5
5
|
import { selectBreakPoint, useAppSelector } from '../../../hooks';
|
|
6
6
|
import { calcStepWidth } from '../../StepFunctions';
|
|
7
|
-
import
|
|
7
|
+
import ErkAvailabilityPicker from '../../../Shared/ErkAvailabilityPicker/ErkAvailabilityPicker';
|
|
8
8
|
/**
|
|
9
|
-
* Conector del paso CALENDAR:
|
|
10
|
-
*
|
|
9
|
+
* Conector del paso CALENDAR/Availability: pregunta de disponibilidad compacta
|
|
10
|
+
* (grid semanal día × hora). El calendario completo vive en el admin; el form
|
|
11
|
+
* solo captura ventanas disponibles que un paso BOOKING puede consumir.
|
|
11
12
|
*/
|
|
12
13
|
function MaterialCalendarStep({ step, editable }) {
|
|
13
14
|
const currentBreakPoint = useAppSelector(selectBreakPoint);
|
|
@@ -24,8 +25,8 @@ function MaterialCalendarStep({ step, editable }) {
|
|
|
24
25
|
};
|
|
25
26
|
const { ref, value, onChange, error } = useFormStep(step, {
|
|
26
27
|
defaultValue: null,
|
|
27
|
-
rules: { required: step.required ? '
|
|
28
|
+
rules: { required: step.required ? 'Marca tu disponibilidad' : undefined },
|
|
28
29
|
});
|
|
29
|
-
return (_jsx("div", { ref: ref, style: cssVars, children: _jsx(
|
|
30
|
+
return (_jsx("div", { ref: ref, style: cssVars, children: _jsx(ErkAvailabilityPicker, { value: value, onChange: onChange, label: step.label, description: step.description, minTime: step.minTime, maxTime: step.maxTime, showWeekends: step.showWeekends, timezone: step.timezone, readOnly: isReadOnly, error: error?.message }) }));
|
|
30
31
|
}
|
|
31
32
|
export default MaterialCalendarStep;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CalendarValue } from '../../@Types/CalendarFormStep';
|
|
2
|
+
export interface ErkAvailabilityPickerProps {
|
|
3
|
+
value: CalendarValue | null;
|
|
4
|
+
onChange: (value: CalendarValue | null) => void;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string | null;
|
|
7
|
+
/** Hora mínima del grid (HH:mm). @default "07:00" */
|
|
8
|
+
minTime?: string;
|
|
9
|
+
/** Hora máxima del grid (HH:mm). @default "19:00" */
|
|
10
|
+
maxTime?: string;
|
|
11
|
+
/** Muestra sábado y domingo. @default true */
|
|
12
|
+
showWeekends?: boolean;
|
|
13
|
+
/** Zona horaria que se guarda junto a los eventos. */
|
|
14
|
+
timezone?: string;
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Picker compacto de disponibilidad para el paso CALENDAR/Availability del form:
|
|
20
|
+
* un grid semanal pequeño (día × hora) donde quien responde marca sus ventanas
|
|
21
|
+
* disponibles. Produce ventanas WORKING con recurrencia semanal (RRULE), las que
|
|
22
|
+
* un paso BOOKING puede usar como disponibilidad.
|
|
23
|
+
*/
|
|
24
|
+
declare function ErkAvailabilityPicker({ value, onChange, label, description, minTime, maxTime, showWeekends, timezone, readOnly, error, }: ErkAvailabilityPickerProps): JSX.Element;
|
|
25
|
+
export default ErkAvailabilityPicker;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { addDays, format, startOfWeek } from 'date-fns';
|
|
4
|
+
import { CalendarEventCategory, } from '../../@Types/CalendarFormStep';
|
|
5
|
+
import styles from './ErkAvailabilityPicker.module.css';
|
|
6
|
+
const DOW_LABELS = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
|
|
7
|
+
const DOW_NAMES = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
|
|
8
|
+
function parseHour(hhmm) {
|
|
9
|
+
return Number(hhmm.split(':')[0]) || 0;
|
|
10
|
+
}
|
|
11
|
+
/** Ancla del día `dayIdx` (0=Lun … 6=Dom) en la semana actual. */
|
|
12
|
+
function anchorDate(dayIdx) {
|
|
13
|
+
return addDays(startOfWeek(new Date(), { weekStartsOn: 1 }), dayIdx);
|
|
14
|
+
}
|
|
15
|
+
function cellISO(dayIdx, hour) {
|
|
16
|
+
const day = anchorDate(dayIdx);
|
|
17
|
+
return `${format(day, 'yyyy-MM-dd')}T${String(hour).padStart(2, '0')}:00:00`;
|
|
18
|
+
}
|
|
19
|
+
/** `Date.getDay()` → índice de columna (0=Lun … 6=Dom). */
|
|
20
|
+
function dayToIdx(day) {
|
|
21
|
+
return (day + 6) % 7;
|
|
22
|
+
}
|
|
23
|
+
const RRULE_DAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
|
24
|
+
/**
|
|
25
|
+
* Expande el value a celdas marcadas. Soporta las ventanas que el propio picker
|
|
26
|
+
* genera (semanales por día) y BYDAY (ej: L–V) por si el valor viene de otra fuente.
|
|
27
|
+
*/
|
|
28
|
+
function valueToCells(value) {
|
|
29
|
+
const cells = new Set();
|
|
30
|
+
for (const event of value?.events ?? []) {
|
|
31
|
+
const startHour = parseHour(event.start.slice(11, 16));
|
|
32
|
+
const endHour = parseHour(event.end.slice(11, 16)) || 24;
|
|
33
|
+
const byday = /BYDAY=([A-Z,]+)/.exec(event.recurrence ?? '');
|
|
34
|
+
const dayIdxs = byday
|
|
35
|
+
? byday[1]
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((code) => RRULE_DAY_CODES.indexOf(code))
|
|
38
|
+
.filter((d) => d >= 0)
|
|
39
|
+
.map(dayToIdx)
|
|
40
|
+
: [dayToIdx(new Date(`${event.start.slice(0, 10)}T00:00:00`).getDay())];
|
|
41
|
+
for (const dayIdx of dayIdxs) {
|
|
42
|
+
for (let h = startHour; h < endHour; h++)
|
|
43
|
+
cells.add(`${dayIdx}:${h}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return cells;
|
|
47
|
+
}
|
|
48
|
+
/** Convierte las celdas marcadas en ventanas semanales contiguas por día. */
|
|
49
|
+
function cellsToEvents(cells, hours) {
|
|
50
|
+
const events = [];
|
|
51
|
+
for (let dayIdx = 0; dayIdx < 7; dayIdx++) {
|
|
52
|
+
let windowStart = null;
|
|
53
|
+
for (const h of hours) {
|
|
54
|
+
const marked = cells.has(`${dayIdx}:${h}`);
|
|
55
|
+
if (marked && windowStart === null)
|
|
56
|
+
windowStart = h;
|
|
57
|
+
if (!marked && windowStart !== null) {
|
|
58
|
+
events.push(windowEvent(dayIdx, windowStart, h));
|
|
59
|
+
windowStart = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (windowStart !== null) {
|
|
63
|
+
events.push(windowEvent(dayIdx, windowStart, hours[hours.length - 1] + 1));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return events;
|
|
67
|
+
}
|
|
68
|
+
function windowEvent(dayIdx, startHour, endHour) {
|
|
69
|
+
return {
|
|
70
|
+
id: `avail-${dayIdx}-${startHour}-${endHour}`,
|
|
71
|
+
category: CalendarEventCategory.WORKING,
|
|
72
|
+
start: cellISO(dayIdx, startHour),
|
|
73
|
+
end: cellISO(dayIdx, endHour),
|
|
74
|
+
recurrence: 'FREQ=WEEKLY',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Picker compacto de disponibilidad para el paso CALENDAR/Availability del form:
|
|
79
|
+
* un grid semanal pequeño (día × hora) donde quien responde marca sus ventanas
|
|
80
|
+
* disponibles. Produce ventanas WORKING con recurrencia semanal (RRULE), las que
|
|
81
|
+
* un paso BOOKING puede usar como disponibilidad.
|
|
82
|
+
*/
|
|
83
|
+
function ErkAvailabilityPicker({ value, onChange, label, description, minTime = '07:00', maxTime = '19:00', showWeekends = true, timezone, readOnly = false, error, }) {
|
|
84
|
+
const minHour = parseHour(minTime);
|
|
85
|
+
const maxHour = parseHour(maxTime);
|
|
86
|
+
const hours = useMemo(() => Array.from({ length: Math.max(maxHour - minHour, 1) }, (_, i) => minHour + i), [minHour, maxHour]);
|
|
87
|
+
const dayIdxs = showWeekends ? [0, 1, 2, 3, 4, 5, 6] : [0, 1, 2, 3, 4];
|
|
88
|
+
const cells = useMemo(() => valueToCells(value), [value]);
|
|
89
|
+
const toggle = (dayIdx, hour) => {
|
|
90
|
+
if (readOnly)
|
|
91
|
+
return;
|
|
92
|
+
const next = new Set(cells);
|
|
93
|
+
const key = `${dayIdx}:${hour}`;
|
|
94
|
+
if (next.has(key))
|
|
95
|
+
next.delete(key);
|
|
96
|
+
else
|
|
97
|
+
next.add(key);
|
|
98
|
+
onChange({
|
|
99
|
+
type: 'CALENDAR_VALUE',
|
|
100
|
+
events: cellsToEvents(next, hours),
|
|
101
|
+
timezone: value?.timezone ?? timezone,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const markedCount = cells.size;
|
|
105
|
+
return (_jsxs("div", { className: styles.container, children: [_jsx("span", { className: styles.label, children: label }), description && _jsx("p", { className: styles.description, children: description }), _jsxs("div", { className: styles.card, children: [_jsxs("div", { className: styles.grid, style: { gridTemplateColumns: `44px repeat(${dayIdxs.length}, 1fr)` }, children: [_jsx("span", { className: styles.corner }), dayIdxs.map((d) => (_jsx("span", { className: styles.dow, children: DOW_LABELS[d] }, d))), hours.map((h) => (_jsxs("div", { className: styles.row, children: [_jsxs("span", { className: styles.hour, children: [h, ":00"] }), dayIdxs.map((d) => {
|
|
106
|
+
const on = cells.has(`${d}:${h}`);
|
|
107
|
+
return (_jsx("button", { type: "button", disabled: readOnly, "aria-pressed": on, "aria-label": `${DOW_NAMES[d]} ${h}:00`, className: on ? `${styles.cell} ${styles.cellOn}` : styles.cell, onClick: () => toggle(d, h) }, d));
|
|
108
|
+
})] }, h)))] }), _jsx("div", { className: styles.footer, children: markedCount > 0
|
|
109
|
+
? 'Disponible cada semana en las horas marcadas.'
|
|
110
|
+
: 'Marca las horas en las que estás disponible cada semana.' })] }), error && _jsx("div", { className: styles.err, children: error })] }));
|
|
111
|
+
}
|
|
112
|
+
export default ErkAvailabilityPicker;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
max-width: 100%;
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 4px;
|
|
7
|
+
}
|
|
8
|
+
.label {
|
|
9
|
+
font-size: 14.5px;
|
|
10
|
+
font-weight: 700;
|
|
11
|
+
color: var(--eureka-text, #293241);
|
|
12
|
+
}
|
|
13
|
+
.description {
|
|
14
|
+
margin: 0 0 4px;
|
|
15
|
+
font-size: 12.5px;
|
|
16
|
+
color: #787878;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.card {
|
|
20
|
+
border: 1px solid var(--eureka-outline, #d9dee6);
|
|
21
|
+
border-radius: 12px;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
background: var(--eureka-bg, #fff);
|
|
24
|
+
max-width: 460px;
|
|
25
|
+
}
|
|
26
|
+
.grid {
|
|
27
|
+
display: grid;
|
|
28
|
+
padding: 10px 12px 4px;
|
|
29
|
+
gap: 2px;
|
|
30
|
+
font-variant-numeric: tabular-nums;
|
|
31
|
+
}
|
|
32
|
+
.corner {
|
|
33
|
+
grid-column: 1;
|
|
34
|
+
}
|
|
35
|
+
.dow {
|
|
36
|
+
text-align: center;
|
|
37
|
+
font-size: 10.5px;
|
|
38
|
+
font-weight: 700;
|
|
39
|
+
color: #787878;
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
padding-bottom: 2px;
|
|
42
|
+
}
|
|
43
|
+
.row {
|
|
44
|
+
display: contents;
|
|
45
|
+
}
|
|
46
|
+
.hour {
|
|
47
|
+
font-size: 10.5px;
|
|
48
|
+
color: #787878;
|
|
49
|
+
text-align: right;
|
|
50
|
+
padding-right: 6px;
|
|
51
|
+
align-self: center;
|
|
52
|
+
}
|
|
53
|
+
.cell {
|
|
54
|
+
height: 26px;
|
|
55
|
+
border: 1px solid var(--eureka-outline, #eceff3);
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
background: #fbfcfd;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
padding: 0;
|
|
60
|
+
}
|
|
61
|
+
.cell:hover {
|
|
62
|
+
border-color: var(--eureka-primary, #3d5a7f);
|
|
63
|
+
}
|
|
64
|
+
.cell:disabled {
|
|
65
|
+
cursor: default;
|
|
66
|
+
}
|
|
67
|
+
.cellOn {
|
|
68
|
+
background: repeating-linear-gradient(
|
|
69
|
+
-45deg,
|
|
70
|
+
color-mix(in srgb, var(--eureka-primary, #3d5a7f) 30%, #fff),
|
|
71
|
+
color-mix(in srgb, var(--eureka-primary, #3d5a7f) 30%, #fff) 5px,
|
|
72
|
+
color-mix(in srgb, var(--eureka-primary, #3d5a7f) 18%, #fff) 5px,
|
|
73
|
+
color-mix(in srgb, var(--eureka-primary, #3d5a7f) 18%, #fff) 10px
|
|
74
|
+
);
|
|
75
|
+
border-color: var(--eureka-primary, #3d5a7f);
|
|
76
|
+
}
|
|
77
|
+
.footer {
|
|
78
|
+
padding: 8px 12px 10px;
|
|
79
|
+
font-size: 11.5px;
|
|
80
|
+
color: #787878;
|
|
81
|
+
border-top: 1px solid var(--eureka-outline, #eceff3);
|
|
82
|
+
background: color-mix(in srgb, var(--eureka-primary, #3d5a7f) 2%, #fff);
|
|
83
|
+
}
|
|
84
|
+
.err {
|
|
85
|
+
font-size: 12px;
|
|
86
|
+
color: #d0473f;
|
|
87
|
+
margin-top: 2px;
|
|
88
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
import { addDays, format, startOfWeek } from 'date-fns';
|
|
7
|
+
import ErkAvailabilityPicker from './ErkAvailabilityPicker';
|
|
8
|
+
import MaterialProviders from '../../Utils/MaterialProviders';
|
|
9
|
+
import InternalFormStyle from '../../constants/InternalFormStyle';
|
|
10
|
+
import { CalendarEventCategory } from '../../@Types/CalendarFormStep';
|
|
11
|
+
function iso(dayIdx, hhmm) {
|
|
12
|
+
const day = addDays(startOfWeek(new Date(), { weekStartsOn: 1 }), dayIdx);
|
|
13
|
+
return `${format(day, 'yyyy-MM-dd')}T${hhmm}:00`;
|
|
14
|
+
}
|
|
15
|
+
describe('ErkAvailabilityPicker', () => {
|
|
16
|
+
it('marks a cell and emits a weekly WORKING window', async () => {
|
|
17
|
+
const user = userEvent.setup();
|
|
18
|
+
const onChange = vi.fn();
|
|
19
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkAvailabilityPicker, { value: null, onChange: onChange, label: "Disponibilidad", minTime: "08:00", maxTime: "12:00" }) }));
|
|
20
|
+
await user.click(screen.getByRole('button', { name: 'Lunes 9:00' }));
|
|
21
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
22
|
+
type: 'CALENDAR_VALUE',
|
|
23
|
+
events: [
|
|
24
|
+
expect.objectContaining({
|
|
25
|
+
category: CalendarEventCategory.WORKING,
|
|
26
|
+
start: iso(0, '09:00'),
|
|
27
|
+
end: iso(0, '10:00'),
|
|
28
|
+
recurrence: 'FREQ=WEEKLY',
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
timezone: undefined,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('merges contiguous marked hours into a single window', async () => {
|
|
35
|
+
const user = userEvent.setup();
|
|
36
|
+
const onChange = vi.fn();
|
|
37
|
+
const value = {
|
|
38
|
+
type: 'CALENDAR_VALUE',
|
|
39
|
+
events: [
|
|
40
|
+
{
|
|
41
|
+
id: 'avail-0-9-10',
|
|
42
|
+
category: CalendarEventCategory.WORKING,
|
|
43
|
+
start: iso(0, '09:00'),
|
|
44
|
+
end: iso(0, '10:00'),
|
|
45
|
+
recurrence: 'FREQ=WEEKLY',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkAvailabilityPicker, { value: value, onChange: onChange, label: "Disponibilidad", minTime: "08:00", maxTime: "12:00" }) }));
|
|
50
|
+
await user.click(screen.getByRole('button', { name: 'Lunes 10:00' }));
|
|
51
|
+
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({
|
|
52
|
+
events: [
|
|
53
|
+
expect.objectContaining({ start: iso(0, '09:00'), end: iso(0, '11:00') }),
|
|
54
|
+
],
|
|
55
|
+
}));
|
|
56
|
+
});
|
|
57
|
+
it('renders existing windows as pressed cells and blocks toggling when readOnly', () => {
|
|
58
|
+
const value = {
|
|
59
|
+
type: 'CALENDAR_VALUE',
|
|
60
|
+
events: [
|
|
61
|
+
{
|
|
62
|
+
id: 'avail-2-14-16',
|
|
63
|
+
category: CalendarEventCategory.WORKING,
|
|
64
|
+
start: iso(2, '14:00'),
|
|
65
|
+
end: iso(2, '16:00'),
|
|
66
|
+
recurrence: 'FREQ=WEEKLY',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkAvailabilityPicker, { value: value, onChange: () => { }, label: "Disponibilidad", minTime: "13:00", maxTime: "17:00", readOnly: true }) }));
|
|
71
|
+
expect(screen.getByRole('button', { name: 'Miércoles 14:00', pressed: true })).toBeDisabled();
|
|
72
|
+
expect(screen.getByRole('button', { name: 'Miércoles 15:00', pressed: true })).toBeInTheDocument();
|
|
73
|
+
expect(screen.getByRole('button', { name: 'Miércoles 16:00', pressed: false })).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
});
|
package/dist/index.lib.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import EurekaForm, { AppProps as EurekaFormProps } from './App/App';
|
|
2
2
|
import { StepProps } from './FormSteps/Step';
|
|
3
3
|
import * as StepFunctions from './FormSteps/StepFunctions';
|
|
4
|
-
import ErkCalendar, { BusinessDaysConfig, ErkCalendarProps } from './Shared/ErkCalendar/ErkCalendar';
|
|
5
4
|
import ErkBookingPicker, { ErkBookingPickerProps } from './Shared/ErkBookingPicker/ErkBookingPicker';
|
|
6
|
-
|
|
7
|
-
export {
|
|
5
|
+
import ErkAvailabilityPicker, { ErkAvailabilityPickerProps } from './Shared/ErkAvailabilityPicker/ErkAvailabilityPicker';
|
|
6
|
+
export type { StepProps, EurekaFormProps, ErkBookingPickerProps, ErkAvailabilityPickerProps };
|
|
7
|
+
export { EurekaForm, StepFunctions, ErkBookingPicker, ErkAvailabilityPicker };
|
|
8
8
|
export default EurekaForm;
|
package/dist/index.lib.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EurekaForm from './App/App';
|
|
2
2
|
import * as StepFunctions from './FormSteps/StepFunctions';
|
|
3
|
-
import ErkCalendar from './Shared/ErkCalendar/ErkCalendar';
|
|
4
3
|
import ErkBookingPicker from './Shared/ErkBookingPicker/ErkBookingPicker';
|
|
5
|
-
|
|
4
|
+
import ErkAvailabilityPicker from './Shared/ErkAvailabilityPicker/ErkAvailabilityPicker';
|
|
5
|
+
export { EurekaForm, StepFunctions, ErkBookingPicker, ErkAvailabilityPicker };
|
|
6
6
|
export default EurekaForm;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arquimedes.co/eureka-forms",
|
|
3
3
|
"repository": "git://github.com/Arquimede5/Eureka-Forms.git",
|
|
4
|
-
"version":"3.0.
|
|
4
|
+
"version":"3.0.55-new-steps",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"watch": "tsgo --noEmit --watch --project tsconfig.app.json",
|
|
7
7
|
"start": "vite",
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { CalendarEvent, CalendarEventCategory, CalendarEventTemplate } from '../../@Types/CalendarFormStep';
|
|
2
|
-
/** Color base por categoría (paleta del app; navy = primario, coral = acento). */
|
|
3
|
-
export declare const CATEGORY_COLORS: Record<CalendarEventCategory, string>;
|
|
4
|
-
export declare const CATEGORY_LABELS: Record<CalendarEventCategory, string>;
|
|
5
|
-
export declare function isAvailability(category: CalendarEventCategory): boolean;
|
|
6
|
-
export declare function parseTime(hhmm: string): {
|
|
7
|
-
h: number;
|
|
8
|
-
m: number;
|
|
9
|
-
};
|
|
10
|
-
export declare function toMinutes(hhmm: string): number;
|
|
11
|
-
export declare function minutesToHHMM(mins: number): string;
|
|
12
|
-
/** "2026-07-14T09:30:00" → "09:30" */
|
|
13
|
-
export declare function isoToHHMM(iso: string): string;
|
|
14
|
-
/** Compone el ISO en hora local (sin corrimiento de zona horaria). */
|
|
15
|
-
export declare function dateAndTimeToISO(date: Date, hhmm: string): string;
|
|
16
|
-
/** Interpreta el ISO como hora local (sin corrimiento de zona horaria). */
|
|
17
|
-
export declare function isoToDate(iso: string): Date;
|
|
18
|
-
export declare function newEventId(): string;
|
|
19
|
-
/**
|
|
20
|
-
* Si el evento ocurre en `day`, considerando su recurrencia RRULE (RFC 5545 —
|
|
21
|
-
* el mismo estándar de Google/Outlook/iCal). Soporta:
|
|
22
|
-
* - `FREQ=DAILY` (+ INTERVAL)
|
|
23
|
-
* - `FREQ=WEEKLY` (+ INTERVAL, BYDAY — ej: `BYDAY=MO,TU,WE,TH,FR` = días hábiles)
|
|
24
|
-
* - `FREQ=MONTHLY` (+ INTERVAL, mismo día del mes que el inicio)
|
|
25
|
-
*/
|
|
26
|
-
export declare function occursOnDay(event: CalendarEvent, day: Date): boolean;
|
|
27
|
-
export declare function eventColor(event: CalendarEvent, templates?: CalendarEventTemplate[]): string;
|
|
28
|
-
export declare function eventLabel(event: CalendarEvent, templates?: CalendarEventTemplate[]): string;
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { differenceInCalendarDays, differenceInCalendarMonths, differenceInCalendarWeeks, format, isSameDay, startOfDay, } from 'date-fns';
|
|
2
|
-
import { CalendarEventCategory, } from '../../@Types/CalendarFormStep';
|
|
3
|
-
/** Color base por categoría (paleta del app; navy = primario, coral = acento). */
|
|
4
|
-
export const CATEGORY_COLORS = {
|
|
5
|
-
[CalendarEventCategory.WORKING]: '#3d5a7f',
|
|
6
|
-
[CalendarEventCategory.PHYSICAL]: '#2e9367',
|
|
7
|
-
[CalendarEventCategory.VIRTUAL]: '#7b5ea7',
|
|
8
|
-
[CalendarEventCategory.SPECIFIC]: '#ee6c4d',
|
|
9
|
-
[CalendarEventCategory.MEETING]: '#d9822b',
|
|
10
|
-
[CalendarEventCategory.BUSY]: '#98a2b3',
|
|
11
|
-
[CalendarEventCategory.CUSTOM]: '#607d8b',
|
|
12
|
-
};
|
|
13
|
-
export const CATEGORY_LABELS = {
|
|
14
|
-
[CalendarEventCategory.WORKING]: 'Laboral',
|
|
15
|
-
[CalendarEventCategory.PHYSICAL]: 'Presencial',
|
|
16
|
-
[CalendarEventCategory.VIRTUAL]: 'Virtual',
|
|
17
|
-
[CalendarEventCategory.SPECIFIC]: 'Específico',
|
|
18
|
-
[CalendarEventCategory.MEETING]: 'Reunión',
|
|
19
|
-
[CalendarEventCategory.BUSY]: 'Ocupado',
|
|
20
|
-
[CalendarEventCategory.CUSTOM]: 'Personalizado',
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Categorías que definen ventanas de disponibilidad: se pintan como franja rayada
|
|
24
|
-
* de fondo (se puede agendar encima), no como bloques sólidos.
|
|
25
|
-
*/
|
|
26
|
-
const AVAILABILITY_CATEGORIES = new Set([
|
|
27
|
-
CalendarEventCategory.WORKING,
|
|
28
|
-
CalendarEventCategory.PHYSICAL,
|
|
29
|
-
CalendarEventCategory.VIRTUAL,
|
|
30
|
-
CalendarEventCategory.SPECIFIC,
|
|
31
|
-
]);
|
|
32
|
-
export function isAvailability(category) {
|
|
33
|
-
return AVAILABILITY_CATEGORIES.has(category);
|
|
34
|
-
}
|
|
35
|
-
export function parseTime(hhmm) {
|
|
36
|
-
const [h, m] = hhmm.split(':').map(Number);
|
|
37
|
-
return { h: h ?? 0, m: m ?? 0 };
|
|
38
|
-
}
|
|
39
|
-
export function toMinutes(hhmm) {
|
|
40
|
-
const { h, m } = parseTime(hhmm);
|
|
41
|
-
return h * 60 + m;
|
|
42
|
-
}
|
|
43
|
-
export function minutesToHHMM(mins) {
|
|
44
|
-
const h = Math.floor(mins / 60);
|
|
45
|
-
const m = mins % 60;
|
|
46
|
-
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
47
|
-
}
|
|
48
|
-
/** "2026-07-14T09:30:00" → "09:30" */
|
|
49
|
-
export function isoToHHMM(iso) {
|
|
50
|
-
return iso.slice(11, 16);
|
|
51
|
-
}
|
|
52
|
-
/** Compone el ISO en hora local (sin corrimiento de zona horaria). */
|
|
53
|
-
export function dateAndTimeToISO(date, hhmm) {
|
|
54
|
-
const { h, m } = parseTime(hhmm);
|
|
55
|
-
const d = new Date(date);
|
|
56
|
-
d.setHours(h, m, 0, 0);
|
|
57
|
-
return `${format(d, 'yyyy-MM-dd')}T${format(d, 'HH:mm:ss')}`;
|
|
58
|
-
}
|
|
59
|
-
/** Interpreta el ISO como hora local (sin corrimiento de zona horaria). */
|
|
60
|
-
export function isoToDate(iso) {
|
|
61
|
-
const [datePart, timePart] = iso.slice(0, 19).split('T');
|
|
62
|
-
const [y, mo, d] = datePart.split('-').map(Number);
|
|
63
|
-
const { h, m } = parseTime(timePart ?? '00:00');
|
|
64
|
-
return new Date(y, (mo ?? 1) - 1, d ?? 1, h, m, 0, 0);
|
|
65
|
-
}
|
|
66
|
-
export function newEventId() {
|
|
67
|
-
return `evt-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
68
|
-
}
|
|
69
|
-
/** Códigos de día RFC 5545 indexados por `Date.getDay()`. */
|
|
70
|
-
const RRULE_DAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
|
71
|
-
/**
|
|
72
|
-
* Si el evento ocurre en `day`, considerando su recurrencia RRULE (RFC 5545 —
|
|
73
|
-
* el mismo estándar de Google/Outlook/iCal). Soporta:
|
|
74
|
-
* - `FREQ=DAILY` (+ INTERVAL)
|
|
75
|
-
* - `FREQ=WEEKLY` (+ INTERVAL, BYDAY — ej: `BYDAY=MO,TU,WE,TH,FR` = días hábiles)
|
|
76
|
-
* - `FREQ=MONTHLY` (+ INTERVAL, mismo día del mes que el inicio)
|
|
77
|
-
*/
|
|
78
|
-
export function occursOnDay(event, day) {
|
|
79
|
-
const start = isoToDate(event.start);
|
|
80
|
-
if (isSameDay(start, day))
|
|
81
|
-
return true;
|
|
82
|
-
if (!event.recurrence)
|
|
83
|
-
return false;
|
|
84
|
-
if (startOfDay(day) < startOfDay(start))
|
|
85
|
-
return false;
|
|
86
|
-
const rule = event.recurrence;
|
|
87
|
-
const interval = /INTERVAL=(\d+)/.exec(rule);
|
|
88
|
-
const every = interval ? parseInt(interval[1], 10) : 1;
|
|
89
|
-
if (rule.includes('FREQ=DAILY')) {
|
|
90
|
-
return differenceInCalendarDays(day, start) % every === 0;
|
|
91
|
-
}
|
|
92
|
-
if (rule.includes('FREQ=WEEKLY')) {
|
|
93
|
-
const byday = /BYDAY=([A-Z,]+)/.exec(rule);
|
|
94
|
-
if (byday) {
|
|
95
|
-
if (!byday[1].split(',').includes(RRULE_DAY_CODES[day.getDay()]))
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
else if (day.getDay() !== start.getDay()) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
return differenceInCalendarWeeks(day, start, { weekStartsOn: 1 }) % every === 0;
|
|
102
|
-
}
|
|
103
|
-
if (rule.includes('FREQ=MONTHLY')) {
|
|
104
|
-
if (day.getDate() !== start.getDate())
|
|
105
|
-
return false;
|
|
106
|
-
return differenceInCalendarMonths(day, start) % every === 0;
|
|
107
|
-
}
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
export function eventColor(event, templates) {
|
|
111
|
-
if (event.color)
|
|
112
|
-
return event.color;
|
|
113
|
-
const template = templates?.find((t) => t.id === event.eventTemplateId);
|
|
114
|
-
return template?.color ?? CATEGORY_COLORS[event.category];
|
|
115
|
-
}
|
|
116
|
-
export function eventLabel(event, templates) {
|
|
117
|
-
if (event.title)
|
|
118
|
-
return event.title;
|
|
119
|
-
const template = templates?.find((t) => t.id === event.eventTemplateId);
|
|
120
|
-
return template?.name ?? CATEGORY_LABELS[event.category];
|
|
121
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { CalendarEvent, CalendarEventCategory, CalendarEventTemplate } from '../../@Types/CalendarFormStep';
|
|
2
|
-
export interface CalendarCreateState {
|
|
3
|
-
anchorEl: HTMLElement;
|
|
4
|
-
date: Date;
|
|
5
|
-
/** Hora de inicio sugerida en formato HH:mm (según dónde se hizo clic). */
|
|
6
|
-
startHHMM: string;
|
|
7
|
-
/** Hora de fin sugerida en formato HH:mm. */
|
|
8
|
-
endHHMM: string;
|
|
9
|
-
}
|
|
10
|
-
interface CalendarCreatePopoverProps {
|
|
11
|
-
state: CalendarCreateState;
|
|
12
|
-
/** Categorías de disponibilidad permitidas (laboral/presencial/virtual). */
|
|
13
|
-
availabilityCategories: CalendarEventCategory[];
|
|
14
|
-
/** Categorías de evento permitidas (reunión/ocupado/personalizado). */
|
|
15
|
-
eventCategories: CalendarEventCategory[];
|
|
16
|
-
/** Plantillas de la organización: las `bookable` son tipos de espacio agendable. */
|
|
17
|
-
eventTemplates?: CalendarEventTemplate[];
|
|
18
|
-
allowRecurring: boolean;
|
|
19
|
-
allowAllDay: boolean;
|
|
20
|
-
onClose: () => void;
|
|
21
|
-
onCreate: (event: CalendarEvent) => void;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Popover anclado al slot clickeado, con dos modos:
|
|
25
|
-
* - Disponibilidad: crea ventanas agendables — "estoy libre para que me agenden"
|
|
26
|
-
* (general/presencial/virtual, o un tipo de espacio de la organización).
|
|
27
|
-
* - Evento: reuniones, bloqueos y eventos personalizados.
|
|
28
|
-
*/
|
|
29
|
-
export declare function CalendarCreatePopover({ state, availabilityCategories, eventCategories, eventTemplates, allowRecurring, allowAllDay, onClose, onCreate, }: CalendarCreatePopoverProps): JSX.Element;
|
|
30
|
-
export interface CalendarDetailState {
|
|
31
|
-
anchorEl: HTMLElement;
|
|
32
|
-
event: CalendarEvent;
|
|
33
|
-
/** Si el evento es del calendario propio (editable) o de una fuente compartida. */
|
|
34
|
-
own: boolean;
|
|
35
|
-
}
|
|
36
|
-
interface CalendarDetailPopoverProps {
|
|
37
|
-
state: CalendarDetailState;
|
|
38
|
-
eventTemplates?: CalendarEventTemplate[];
|
|
39
|
-
readOnly: boolean;
|
|
40
|
-
onClose: () => void;
|
|
41
|
-
onDelete: (id: string) => void;
|
|
42
|
-
}
|
|
43
|
-
/** Detalle de un evento: cuándo, categoría, notas y acciones. */
|
|
44
|
-
export declare function CalendarDetailPopover({ state, eventTemplates, readOnly, onClose, onDelete, }: CalendarDetailPopoverProps): JSX.Element;
|
|
45
|
-
export {};
|