@servicetitan/dte-unlayer 0.157.0 → 0.159.0

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 (166) hide show
  1. package/dist/api-core.js +22 -29
  2. package/dist/api-core.js.map +1 -1
  3. package/dist/api-custom-tools.js +23 -29
  4. package/dist/api-custom-tools.js.map +1 -1
  5. package/dist/business-calendar/BusinessCalendarDrawer.d.ts +19 -0
  6. package/dist/business-calendar/BusinessCalendarDrawer.d.ts.map +1 -0
  7. package/dist/business-calendar/BusinessCalendarDrawer.js +41 -0
  8. package/dist/business-calendar/BusinessCalendarDrawer.js.map +1 -0
  9. package/dist/business-calendar/business-calendar-requests.d.ts +25 -0
  10. package/dist/business-calendar/business-calendar-requests.d.ts.map +1 -0
  11. package/dist/business-calendar/business-calendar-requests.js +46 -0
  12. package/dist/business-calendar/business-calendar-requests.js.map +1 -0
  13. package/dist/business-calendar/business-calendar-state.d.ts +22 -0
  14. package/dist/business-calendar/business-calendar-state.d.ts.map +1 -0
  15. package/dist/business-calendar/business-calendar-state.js +74 -0
  16. package/dist/business-calendar/business-calendar-state.js.map +1 -0
  17. package/dist/business-calendar/useBusinessCalendarRequests.d.ts +15 -0
  18. package/dist/business-calendar/useBusinessCalendarRequests.d.ts.map +1 -0
  19. package/dist/business-calendar/useBusinessCalendarRequests.js +30 -0
  20. package/dist/business-calendar/useBusinessCalendarRequests.js.map +1 -0
  21. package/dist/display-conditions/ConditionGroup.js +15 -89
  22. package/dist/display-conditions/ConditionGroup.js.map +1 -1
  23. package/dist/display-conditions/ConditionGroupsSection.js +2 -70
  24. package/dist/display-conditions/ConditionGroupsSection.js.map +1 -1
  25. package/dist/display-conditions/ConditionRow.d.ts.map +1 -1
  26. package/dist/display-conditions/ConditionRow.js +71 -296
  27. package/dist/display-conditions/ConditionRow.js.map +1 -1
  28. package/dist/display-conditions/DisplayConditionModal.js +40 -190
  29. package/dist/display-conditions/DisplayConditionModal.js.map +1 -1
  30. package/dist/display-conditions/LookupValueControl.d.ts.map +1 -1
  31. package/dist/display-conditions/LookupValueControl.js +42 -109
  32. package/dist/display-conditions/LookupValueControl.js.map +1 -1
  33. package/dist/display-conditions/SeparatorWithChip.js +1 -10
  34. package/dist/display-conditions/SeparatorWithChip.js.map +1 -1
  35. package/dist/display-conditions/constants.js +15 -20
  36. package/dist/display-conditions/constants.js.map +1 -1
  37. package/dist/display-conditions/displayConditionController.js +5 -8
  38. package/dist/display-conditions/displayConditionController.js.map +1 -1
  39. package/dist/display-conditions/lookupOptionsController.js +16 -16
  40. package/dist/display-conditions/lookupOptionsController.js.map +1 -1
  41. package/dist/display-conditions/nunjucks.js +98 -116
  42. package/dist/display-conditions/nunjucks.js.map +1 -1
  43. package/dist/display-conditions/schemaDataPoints.js +13 -11
  44. package/dist/display-conditions/schemaDataPoints.js.map +1 -1
  45. package/dist/display-conditions/types.js +23 -67
  46. package/dist/display-conditions/types.js.map +1 -1
  47. package/dist/display-conditions/useConditionForms.js +38 -56
  48. package/dist/display-conditions/useConditionForms.js.map +1 -1
  49. package/dist/display-conditions/useLookupOptions.js +24 -44
  50. package/dist/display-conditions/useLookupOptions.js.map +1 -1
  51. package/dist/e-sign-offset/ESignDateSignedOffsetModal.d.ts.map +1 -1
  52. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js +48 -156
  53. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js.map +1 -1
  54. package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts +3 -1
  55. package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts.map +1 -1
  56. package/dist/e-sign-offset/ESignOffsetRuleRow.js +12 -71
  57. package/dist/e-sign-offset/ESignOffsetRuleRow.js.map +1 -1
  58. package/dist/e-sign-offset/eSignOffsetController.js +5 -8
  59. package/dist/e-sign-offset/eSignOffsetController.js.map +1 -1
  60. package/dist/editor-core-source.d.ts +2 -2
  61. package/dist/editor-core-source.d.ts.map +1 -1
  62. package/dist/editor-core-source.js +3 -3
  63. package/dist/editor-core-source.js.map +1 -1
  64. package/dist/editor-core.js +2 -2
  65. package/dist/editor-core.js.map +1 -1
  66. package/dist/editor.d.ts.map +1 -1
  67. package/dist/editor.js +56 -118
  68. package/dist/editor.js.map +1 -1
  69. package/dist/image-upload-error.js +3 -3
  70. package/dist/image-upload-error.js.map +1 -1
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +0 -1
  74. package/dist/index.js.map +1 -1
  75. package/dist/loadScript.js +7 -8
  76. package/dist/loadScript.js.map +1 -1
  77. package/dist/log.js +2 -3
  78. package/dist/log.js.map +1 -1
  79. package/dist/merge-tags.js +18 -17
  80. package/dist/merge-tags.js.map +1 -1
  81. package/dist/shared/configs.js +1 -2
  82. package/dist/shared/configs.js.map +1 -1
  83. package/dist/shared/const.d.ts +72 -0
  84. package/dist/shared/const.d.ts.map +1 -1
  85. package/dist/shared/const.js +13 -4
  86. package/dist/shared/const.js.map +1 -1
  87. package/dist/shared/date-calc.d.ts +32 -13
  88. package/dist/shared/date-calc.d.ts.map +1 -1
  89. package/dist/shared/date-calc.js +130 -76
  90. package/dist/shared/date-calc.js.map +1 -1
  91. package/dist/shared/e-sign-offset.js +46 -63
  92. package/dist/shared/e-sign-offset.js.map +1 -1
  93. package/dist/shared/edit-icon.js +0 -1
  94. package/dist/shared/edit-icon.js.map +1 -1
  95. package/dist/shared/fonts.js +16 -17
  96. package/dist/shared/fonts.js.map +1 -1
  97. package/dist/shared/forms.js +16 -38
  98. package/dist/shared/forms.js.map +1 -1
  99. package/dist/shared/instance-id.d.ts +3 -0
  100. package/dist/shared/instance-id.d.ts.map +1 -1
  101. package/dist/shared/instance-id.js +10 -7
  102. package/dist/shared/instance-id.js.map +1 -1
  103. package/dist/shared/nunjucks-helpers.js +2 -5
  104. package/dist/shared/nunjucks-helpers.js.map +1 -1
  105. package/dist/shared/schema.js +52 -96
  106. package/dist/shared/schema.js.map +1 -1
  107. package/dist/shared/tools.js +6 -13
  108. package/dist/shared/tools.js.map +1 -1
  109. package/dist/store.d.ts +15 -1
  110. package/dist/store.d.ts.map +1 -1
  111. package/dist/store.js +851 -547
  112. package/dist/store.js.map +1 -1
  113. package/dist/tools.js +133 -143
  114. package/dist/tools.js.map +1 -1
  115. package/dist/unlayer-interface.d.ts +25 -1
  116. package/dist/unlayer-interface.d.ts.map +1 -1
  117. package/dist/unlayer-interface.js +1 -2
  118. package/dist/unlayer-interface.js.map +1 -1
  119. package/dist/unlayer.js +86 -113
  120. package/dist/unlayer.js.map +1 -1
  121. package/dist/vendor/dte-shared/business-calendar-validation.utils.d.ts +25 -0
  122. package/dist/vendor/dte-shared/business-calendar-validation.utils.js +80 -0
  123. package/dist/vendor/dte-shared/components/business-calendar-section.d.ts +15 -0
  124. package/dist/vendor/dte-shared/components/business-calendar-section.js +14 -0
  125. package/dist/vendor/dte-shared/components/schedule-drawer.d.ts +18 -0
  126. package/dist/vendor/dte-shared/components/schedule-drawer.js +119 -0
  127. package/dist/vendor/dte-shared/components/schedule-picker.d.ts +8 -0
  128. package/dist/vendor/dte-shared/components/schedule-picker.js +88 -0
  129. package/dist/vendor/dte-shared/constants.d.ts +31 -0
  130. package/dist/vendor/dte-shared/constants.js +34 -0
  131. package/dist/vendor/dte-shared/field-schedule-editor-context.d.ts +31 -0
  132. package/dist/vendor/dte-shared/field-schedule-editor-context.js +110 -0
  133. package/dist/vendor/dte-shared/index.d.ts +8 -0
  134. package/dist/vendor/dte-shared/index.js +7 -0
  135. package/dist/vendor/dte-shared/schedule-bindings.utils.d.ts +22 -0
  136. package/dist/vendor/dte-shared/schedule-bindings.utils.js +31 -0
  137. package/dist/vendor/dte-shared/styles/schedule.css +19 -0
  138. package/dist/vendor/dte-shared/types.d.ts +129 -0
  139. package/dist/vendor/dte-shared/types.js +1 -0
  140. package/package.json +7 -1
  141. package/src/__tests__/business-calendar-drawer.test.tsx +183 -0
  142. package/src/__tests__/business-calendar-requests.test.ts +166 -0
  143. package/src/__tests__/business-calendar-state.test.tsx +142 -0
  144. package/src/__tests__/condition-row.test.tsx +131 -0
  145. package/src/__tests__/date-calc.test.ts +145 -0
  146. package/src/__tests__/e-sign-offset-modal.test.tsx +124 -0
  147. package/src/__tests__/instance-id.test.ts +49 -0
  148. package/src/__tests__/store.business-calendar.test.ts +185 -0
  149. package/src/__tests__/unlayer.e-sign-types.test.ts +30 -0
  150. package/src/business-calendar/BusinessCalendarDrawer.tsx +76 -0
  151. package/src/business-calendar/business-calendar-requests.ts +80 -0
  152. package/src/business-calendar/business-calendar-state.ts +113 -0
  153. package/src/business-calendar/useBusinessCalendarRequests.ts +49 -0
  154. package/src/display-conditions/ConditionRow.tsx +102 -175
  155. package/src/display-conditions/LookupValueControl.tsx +1 -12
  156. package/src/e-sign-offset/ESignDateSignedOffsetModal.tsx +20 -12
  157. package/src/e-sign-offset/ESignOffsetRuleRow.tsx +7 -2
  158. package/src/editor-core-source.ts +2 -2
  159. package/src/editor.tsx +15 -0
  160. package/src/index.ts +10 -0
  161. package/src/shared/const.ts +61 -0
  162. package/src/shared/date-calc.ts +155 -78
  163. package/src/shared/instance-id.ts +6 -1
  164. package/src/store.ts +68 -5
  165. package/src/unlayer-interface.tsx +25 -1
  166. package/src/unlayer.tsx +5 -5
@@ -0,0 +1,80 @@
1
+ import { CUSTOM_DATE_MONTH_LENGTHS, CUSTOM_DATE_PATTERN, MAX_SCHEDULE_NAME_LENGTH, RESERVED_SCHEDULE_NAME, } from './constants';
2
+ /** Whether `value` is a valid `MM-DD` custom date (`02-29` included, as a recurring rule). */
3
+ export function isValidCustomDate(value) {
4
+ const match = CUSTOM_DATE_PATTERN.exec(value);
5
+ if (!match) {
6
+ return false;
7
+ }
8
+ const month = Number(match[1]);
9
+ const day = Number(match[2]);
10
+ return day <= CUSTOM_DATE_MONTH_LENGTHS[month - 1];
11
+ }
12
+ /** Converts a complete `{ month, day }` to `MM-DD`, or null while the field is incomplete/invalid. */
13
+ export function yearlessDateToCustomDate(value) {
14
+ var _a, _b;
15
+ const month = (_a = value === null || value === void 0 ? void 0 : value.month) !== null && _a !== void 0 ? _a : null;
16
+ const day = (_b = value === null || value === void 0 ? void 0 : value.day) !== null && _b !== void 0 ? _b : null;
17
+ if (month === null || day === null) {
18
+ return null;
19
+ }
20
+ const candidate = `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
21
+ return isValidCustomDate(candidate) ? candidate : null;
22
+ }
23
+ /** Converts a stored `MM-DD` custom date back to `DateFieldYearless`'s `{ month, day }` value. */
24
+ export function customDateToYearlessDate(value) {
25
+ const match = CUSTOM_DATE_PATTERN.exec(value);
26
+ return match ? { month: Number(match[1]), day: Number(match[2]) } : null;
27
+ }
28
+ const CUSTOM_DATE_MONTH_ABBREVIATIONS = [
29
+ 'Jan',
30
+ 'Feb',
31
+ 'Mar',
32
+ 'Apr',
33
+ 'May',
34
+ 'Jun',
35
+ 'Jul',
36
+ 'Aug',
37
+ 'Sep',
38
+ 'Oct',
39
+ 'Nov',
40
+ 'Dec',
41
+ ];
42
+ /** Formats a stored `MM-DD` custom date for display, e.g. `03-03` → `Mar - 03`. */
43
+ export function formatCustomDateLabel(value) {
44
+ const match = CUSTOM_DATE_PATTERN.exec(value);
45
+ if (!match) {
46
+ return value;
47
+ }
48
+ const [, month, day] = match;
49
+ return `${CUSTOM_DATE_MONTH_ABBREVIATIONS[Number(month) - 1]} - ${day}`;
50
+ }
51
+ /** How the schedule picker and drawer name a known holiday, e.g. `Independence Day · July 4`. */
52
+ export function formatKnownHolidayLabel(holiday) {
53
+ return `${holiday.displayName} · ${holiday.rule}`;
54
+ }
55
+ /** Whether `name` is the reserved default-schedule name, ignoring case and surrounding whitespace. */
56
+ export function isReservedScheduleName(name) {
57
+ return name.trim().toLowerCase() === RESERVED_SCHEDULE_NAME.toLowerCase();
58
+ }
59
+ /**
60
+ * Client-side schedule name checks mirroring DTE's `SaveDateScheduleRequest.Name` contract: a
61
+ * required, non-reserved name up to 255 characters, unique per tenant ignoring case. `existingNames`
62
+ * should exclude the schedule being edited, if any. The server is still authoritative — a duplicate
63
+ * it rejects despite passing this check is a 409, not a bug here.
64
+ */
65
+ export function validateScheduleName(name, existingNames) {
66
+ const trimmed = name.trim();
67
+ if (!trimmed) {
68
+ return 'Schedule name is required';
69
+ }
70
+ if (trimmed.length > MAX_SCHEDULE_NAME_LENGTH) {
71
+ return `Schedule name must be ${MAX_SCHEDULE_NAME_LENGTH} characters or fewer`;
72
+ }
73
+ if (isReservedScheduleName(trimmed)) {
74
+ return `"${RESERVED_SCHEDULE_NAME}" is reserved for the default schedule`;
75
+ }
76
+ if (existingNames.some(existing => existing.trim().toLowerCase() === trimmed.toLowerCase())) {
77
+ return 'A schedule with this name already exists';
78
+ }
79
+ return undefined;
80
+ }
@@ -0,0 +1,15 @@
1
+ import { FC } from 'react';
2
+ import '../styles/schedule.css';
3
+ import { type ScheduleEditorContext } from '../types';
4
+ interface BusinessCalendarSectionProps {
5
+ businessCalendar: ScheduleEditorContext;
6
+ /** @default "Exclude weekends & holidays from the calculation" */
7
+ label?: string;
8
+ }
9
+ /**
10
+ * A business-day exclusion toggle, shown for a date calculated field (Formula Builder's Advanced
11
+ * Settings, date result type only) and an e-sign `dateSignedOffset` field's day-offset settings.
12
+ * On reveals the schedule picker; what off means for the date arithmetic is up to the editor.
13
+ */
14
+ export declare const BusinessCalendarSection: FC<BusinessCalendarSectionProps>;
15
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Flex, Switch } from '@servicetitan/anvil2';
3
+ import { BUSINESS_CALENDAR_TOGGLE_LABEL } from '../constants';
4
+ import '../styles/schedule.css';
5
+ import { SchedulePicker } from './schedule-picker';
6
+ /**
7
+ * A business-day exclusion toggle, shown for a date calculated field (Formula Builder's Advanced
8
+ * Settings, date result type only) and an e-sign `dateSignedOffset` field's day-offset settings.
9
+ * On reveals the schedule picker; what off means for the date arithmetic is up to the editor.
10
+ */
11
+ export const BusinessCalendarSection = ({ businessCalendar, label = BUSINESS_CALENDAR_TOGGLE_LABEL, }) => {
12
+ const { enabled, onToggle } = businessCalendar;
13
+ return (_jsx("div", { className: "dte-business-calendar-section", children: _jsxs(Flex, { direction: "column", gap: "4", children: [_jsx(Switch, { checked: enabled, onChange: (_e, state) => onToggle(state.checked), label: label, value: "business-calendar-exclusion" }), enabled && _jsx(SchedulePicker, { businessCalendar: businessCalendar })] }) }));
14
+ };
@@ -0,0 +1,18 @@
1
+ import { FC } from 'react';
2
+ import { type BusinessDaySchedule, type ScheduleEditorContext } from '../types';
3
+ interface ScheduleDrawerProps {
4
+ businessCalendar: ScheduleEditorContext;
5
+ /** When set, edits this schedule in place (PUT) instead of creating a new one (POST). */
6
+ editingSchedule?: BusinessDaySchedule;
7
+ onClose(): void;
8
+ onSaved(schedule: BusinessDaySchedule): void;
9
+ }
10
+ /**
11
+ * "Create New Schedule" / "Edit Schedule" drawer. With `businessCalendar.editorContainer` set, the
12
+ * same form is portaled into that element instead (its Close button calls `onClose`; there is no
13
+ * backdrop or Escape key). Creating starts from the default schedule's rules
14
+ * (DEFAULT_SCHEDULE_WEEKEND_DAYS, every catalog holiday); editing starts from `editingSchedule`'s
15
+ * own rules and replaces it in place on save.
16
+ */
17
+ export declare const ScheduleDrawer: FC<ScheduleDrawerProps>;
18
+ export {};
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Alert, Button, ButtonToggle, DateFieldYearless, Drawer, Flex, MultiSelectFieldSync, Text, TextField, } from '@servicetitan/anvil2';
3
+ import IconAddCircle from '@servicetitan/anvil2/assets/icons/material/round/add_circle.svg';
4
+ import IconClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
5
+ import DeleteIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
6
+ import { useMemo, useRef, useState } from 'react';
7
+ import { createPortal } from 'react-dom';
8
+ import { customDateToYearlessDate, formatKnownHolidayLabel, validateScheduleName, yearlessDateToCustomDate, } from '../business-calendar-validation.utils';
9
+ import { DEFAULT_SCHEDULE_WEEKEND_DAYS } from '../constants';
10
+ const EMPTY_DATE = { month: null, day: null };
11
+ const isEmptyDate = (value) => value.month === null && value.day === null;
12
+ /**
13
+ * "Create New Schedule" / "Edit Schedule" drawer. With `businessCalendar.editorContainer` set, the
14
+ * same form is portaled into that element instead (its Close button calls `onClose`; there is no
15
+ * backdrop or Escape key). Creating starts from the default schedule's rules
16
+ * (DEFAULT_SCHEDULE_WEEKEND_DAYS, every catalog holiday); editing starts from `editingSchedule`'s
17
+ * own rules and replaces it in place on save.
18
+ */
19
+ export const ScheduleDrawer = ({ businessCalendar, editingSchedule, onClose, onSaved, }) => {
20
+ var _a;
21
+ const { editorContainer, holidayCatalog, holidayCatalogError, holidayCatalogLoading, onCreateSchedule, onUpdateSchedule, schedules, } = businessCalendar;
22
+ const [name, setName] = useState((_a = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.name) !== null && _a !== void 0 ? _a : '');
23
+ const [weekendDays, setWeekendDays] = useState(() => { var _a; return new Set((_a = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.calendar.weekendDays) !== null && _a !== void 0 ? _a : DEFAULT_SCHEDULE_WEEKEND_DAYS); });
24
+ const [knownHolidays, setKnownHolidays] = useState(() => {
25
+ var _a, _b;
26
+ return new Set((_b = (_a = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.calendar.knownHolidays) !== null && _a !== void 0 ? _a : holidayCatalog === null || holidayCatalog === void 0 ? void 0 : holidayCatalog.holidays.map(h => h.code)) !== null && _b !== void 0 ? _b : []);
27
+ });
28
+ const [customDates, setCustomDates] = useState(() => {
29
+ var _a;
30
+ return ((_a = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.calendar.customDates) !== null && _a !== void 0 ? _a : []).map((date, index) => {
31
+ var _a;
32
+ return ({
33
+ id: index,
34
+ value: (_a = customDateToYearlessDate(date)) !== null && _a !== void 0 ? _a : EMPTY_DATE,
35
+ });
36
+ });
37
+ });
38
+ const nextRowId = useRef(customDates.length);
39
+ const [nameTouched, setNameTouched] = useState(false);
40
+ const [submitError, setSubmitError] = useState();
41
+ const [submitting, setSubmitting] = useState(false);
42
+ const holidayOptions = useMemo(() => {
43
+ var _a;
44
+ return ((_a = holidayCatalog === null || holidayCatalog === void 0 ? void 0 : holidayCatalog.holidays) !== null && _a !== void 0 ? _a : []).map(h => ({
45
+ id: h.code,
46
+ label: formatKnownHolidayLabel(h),
47
+ }));
48
+ }, [holidayCatalog]);
49
+ const selectedHolidayOptions = useMemo(() => holidayOptions.filter(o => knownHolidays.has(String(o.id))), [holidayOptions, knownHolidays]);
50
+ const existingNames = useMemo(() => (schedules !== null && schedules !== void 0 ? schedules : [])
51
+ .filter(s => !s.isDefault && s.id !== (editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.id))
52
+ .map(s => s.name), [schedules, editingSchedule]);
53
+ const nameError = validateScheduleName(name, existingNames);
54
+ const toggleWeekendDay = (day) => {
55
+ setWeekendDays(prev => {
56
+ const next = new Set(prev);
57
+ if (next.has(day)) {
58
+ next.delete(day);
59
+ }
60
+ else {
61
+ next.add(day);
62
+ }
63
+ return next;
64
+ });
65
+ };
66
+ const handleAddDate = () => setCustomDates(prev => [...prev, { id: nextRowId.current++, value: EMPTY_DATE }]);
67
+ const handleRemoveDate = (id) => setCustomDates(prev => prev.filter(row => row.id !== id));
68
+ const handleCustomDateChange = (id, value) => setCustomDates(prev => prev.map(row => (row.id === id ? { ...row, value: value !== null && value !== void 0 ? value : EMPTY_DATE } : row)));
69
+ /** Resolved `MM-DD` per non-empty row, or `null` for an incomplete/invalid one (blocks submit). */
70
+ const resolvedCustomDates = customDates.map(row => isEmptyDate(row.value) ? '' : yearlessDateToCustomDate(row.value));
71
+ const hasInvalidCustomDate = resolvedCustomDates.some((d, i) => d === null && !isEmptyDate(customDates[i].value));
72
+ /*
73
+ * Editing is only offered for a saved tenant schedule, which always has an id (only the
74
+ * read-only default schedule — never editable — lacks one).
75
+ */
76
+ const editingId = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.id;
77
+ const isEditing = editingId !== undefined;
78
+ const canSave = isEditing ? !!onUpdateSchedule : !!onCreateSchedule;
79
+ const canSubmit = !nameError && !hasInvalidCustomDate && !submitting && canSave;
80
+ const handleSubmit = async () => {
81
+ setNameTouched(true);
82
+ if (nameError || hasInvalidCustomDate || !canSave) {
83
+ return;
84
+ }
85
+ const request = {
86
+ name: name.trim(),
87
+ weekendDays: Array.from(weekendDays),
88
+ knownHolidays: Array.from(knownHolidays),
89
+ customDates: Array.from(new Set(resolvedCustomDates.filter((d) => !!d))),
90
+ };
91
+ setSubmitting(true);
92
+ setSubmitError(undefined);
93
+ try {
94
+ if (isEditing && onUpdateSchedule) {
95
+ onSaved(await onUpdateSchedule(editingId, request));
96
+ }
97
+ else if (!isEditing && onCreateSchedule) {
98
+ onSaved(await onCreateSchedule(request));
99
+ }
100
+ }
101
+ catch (e) {
102
+ setSubmitError(e instanceof Error
103
+ ? e.message
104
+ : `Failed to ${isEditing ? 'save' : 'create'} schedule`);
105
+ }
106
+ finally {
107
+ setSubmitting(false);
108
+ }
109
+ };
110
+ const title = isEditing ? 'Edit Schedule' : 'Create New Schedule';
111
+ const form = (_jsxs(Flex, { direction: "column", gap: "5", style: { height: '100%', overflowY: 'auto' }, children: [_jsx(TextField, { required: true, label: "Schedule Name", placeholder: "New Schedule", value: name, onChange: e => setName(e.target.value), onBlur: () => setNameTouched(true), error: nameTouched ? nameError : undefined }), _jsxs(Flex, { direction: "column", gap: "3", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: "Exclude Weekends" }), _jsxs(Flex, { gap: "3", children: [_jsx(ButtonToggle, { checked: weekendDays.has(6), onClick: () => toggleWeekendDay(6), children: "Saturday" }), _jsx(ButtonToggle, { checked: weekendDays.has(0), onClick: () => toggleWeekendDay(0), children: "Sunday" })] })] }), _jsxs(Flex, { direction: "column", gap: "3", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: "Exclude Holidays" }), holidayCatalogLoading && _jsx(Text, { variant: "body", children: "Loading holidays\u2026" }), holidayCatalogError && _jsx(Alert, { status: "danger", title: holidayCatalogError }), _jsx(MultiSelectFieldSync, { label: holidayCatalog ? `${holidayCatalog.jurisdiction} Holidays` : 'Holidays', disabled: holidayCatalogLoading, options: holidayOptions, value: selectedHolidayOptions, onSelectedOptionsChange: options => setKnownHolidays(new Set(options.map(o => String(o.id)))), description: "Select the holidays you want to exclude", filter: { keys: ['label'] } })] }), _jsxs(Flex, { direction: "column", gap: "2", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: "Custom Dates" }), customDates.map((row, index) => {
112
+ const invalid = resolvedCustomDates[index] === null && !isEmptyDate(row.value);
113
+ return (_jsxs(Flex, { gap: "2", children: [_jsx(DateFieldYearless, { label: `Custom date ${index + 1}`, hideLabel: true, value: row.value, error: invalid ? 'Enter a valid date' : undefined, onChange: change => handleCustomDateChange(row.id, change.value), flex: 1 }), _jsx(Button, { icon: DeleteIcon, appearance: "ghost", "aria-label": "Remove date", onClick: () => handleRemoveDate(row.id) })] }, row.id));
114
+ }), _jsx(Button, { appearance: "ghost", icon: IconAddCircle, onClick: handleAddDate, alignSelf: "end", children: "Add Date" })] }), submitError && _jsx(Alert, { status: "danger", title: submitError })] }));
115
+ if (editorContainer) {
116
+ return createPortal(_jsxs(Flex, { direction: "column", gap: "5", style: { height: '100%' }, children: [_jsxs(Flex, { alignItems: "center", justifyContent: "space-between", children: [_jsx(Text, { variant: "headline", el: "h6", size: "small", children: title }), _jsx(Button, { appearance: "ghost", size: "small", onClick: onClose, "aria-label": "Close", icon: IconClose })] }), form, _jsx(Button, { size: "small", appearance: "primary", onClick: handleSubmit, loading: submitting, disabled: !canSubmit, style: { width: '100%' }, children: isEditing ? 'Save' : 'Create' })] }), editorContainer);
117
+ }
118
+ return (_jsxs(Drawer, { open: true, onClose: onClose, size: "medium", children: [_jsx(Drawer.Header, { children: title }), _jsx(Drawer.Content, { children: form }), _jsxs(Drawer.Footer, { sticky: true, children: [_jsx(Drawer.CancelButton, { appearance: "secondary", children: "Cancel" }), _jsx(Button, { appearance: "primary", onClick: handleSubmit, loading: submitting, disabled: !canSubmit, children: isEditing ? 'Save' : 'Create' })] })] }));
119
+ };
@@ -0,0 +1,8 @@
1
+ import { FC } from 'react';
2
+ import { type ScheduleEditorContext } from '../types';
3
+ interface SchedulePickerProps {
4
+ businessCalendar: ScheduleEditorContext;
5
+ }
6
+ /** "Select Schedule or create new" picker plus a collapsible preview of the selected schedule's rules. */
7
+ export declare const SchedulePicker: FC<SchedulePickerProps>;
8
+ export {};
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Alert, Button, Chip, Flex, SelectFieldSync, Text } from '@servicetitan/anvil2';
3
+ import DeleteIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
4
+ import EditIcon from '@servicetitan/anvil2/assets/icons/material/round/edit.svg';
5
+ import IconExpandLess from '@servicetitan/anvil2/assets/icons/material/round/expand_less.svg';
6
+ import IconExpandMore from '@servicetitan/anvil2/assets/icons/material/round/expand_more.svg';
7
+ import { Fragment, useMemo, useState } from 'react';
8
+ import { formatCustomDateLabel, formatKnownHolidayLabel, } from '../business-calendar-validation.utils';
9
+ import { SCHEDULE_PICKER_TEXT, WEEKEND_DAY_LABELS } from '../constants';
10
+ import { findSelectedSchedule } from '../schedule-bindings.utils';
11
+ import { ScheduleDrawer } from './schedule-drawer';
12
+ const DEFAULT_OPTION_ID = 'default';
13
+ /** "Select Schedule or create new" picker plus a collapsible preview of the selected schedule's rules. */
14
+ export const SchedulePicker = ({ businessCalendar }) => {
15
+ var _a;
16
+ const { holidayCatalog, holidayCatalogError, holidayCatalogLoading, onDeleteSchedule, onSelectSchedule, schedules, schedulesError, schedulesLoading, selectedScheduleId, } = businessCalendar;
17
+ const [drawerOpen, setDrawerOpen] = useState(false);
18
+ const [editingSchedule, setEditingSchedule] = useState();
19
+ const [previewOpen, setPreviewOpen] = useState(true);
20
+ const [deleting, setDeleting] = useState(false);
21
+ const [deleteError, setDeleteError] = useState();
22
+ const options = useMemo(() => (schedules !== null && schedules !== void 0 ? schedules : []).map(s => { var _a; return ({ id: (_a = s.id) !== null && _a !== void 0 ? _a : DEFAULT_OPTION_ID, label: s.name }); }), [schedules]);
23
+ const selectedOptionId = selectedScheduleId !== null && selectedScheduleId !== void 0 ? selectedScheduleId : DEFAULT_OPTION_ID;
24
+ const selectedOption = (_a = options.find(o => o.id === selectedOptionId)) !== null && _a !== void 0 ? _a : null;
25
+ const selectedSchedule = findSelectedSchedule(schedules, selectedScheduleId);
26
+ const holidayByCode = useMemo(() => {
27
+ var _a;
28
+ const map = new Map();
29
+ for (const h of (_a = holidayCatalog === null || holidayCatalog === void 0 ? void 0 : holidayCatalog.holidays) !== null && _a !== void 0 ? _a : []) {
30
+ map.set(h.code, h);
31
+ }
32
+ return map;
33
+ }, [holidayCatalog]);
34
+ const handleSelectedOptionChange = (option) => {
35
+ if (!option) {
36
+ return;
37
+ }
38
+ onSelectSchedule(option.id === DEFAULT_OPTION_ID ? undefined : option.id);
39
+ };
40
+ const handleSaved = (schedule) => {
41
+ onSelectSchedule(schedule.id);
42
+ setDrawerOpen(false);
43
+ setEditingSchedule(undefined);
44
+ };
45
+ const handleOpenCreate = () => {
46
+ setEditingSchedule(undefined);
47
+ setDrawerOpen(true);
48
+ };
49
+ const handleOpenEdit = () => {
50
+ if (!selectedSchedule) {
51
+ return;
52
+ }
53
+ setEditingSchedule(selectedSchedule);
54
+ setDrawerOpen(true);
55
+ };
56
+ const handleCloseDrawer = () => {
57
+ setDrawerOpen(false);
58
+ setEditingSchedule(undefined);
59
+ };
60
+ const handleDelete = async () => {
61
+ if (!(selectedSchedule === null || selectedSchedule === void 0 ? void 0 : selectedSchedule.id) || !onDeleteSchedule) {
62
+ return;
63
+ }
64
+ setDeleting(true);
65
+ setDeleteError(undefined);
66
+ try {
67
+ await onDeleteSchedule(selectedSchedule.id);
68
+ onSelectSchedule(undefined);
69
+ }
70
+ catch (e) {
71
+ setDeleteError(e instanceof Error ? e.message : SCHEDULE_PICKER_TEXT.deleteFailed);
72
+ }
73
+ finally {
74
+ setDeleting(false);
75
+ }
76
+ };
77
+ return (_jsxs(Fragment, { children: [_jsx(SelectFieldSync, { label: SCHEDULE_PICKER_TEXT.selectLabel, options: options, value: selectedOption, disableClearButton: true, disabled: schedulesLoading, onSelectedOptionChange: handleSelectedOptionChange, onAddNewItem: handleOpenCreate, addItemLabel: SCHEDULE_PICKER_TEXT.createNew, filter: { keys: ['label'] } }), schedulesLoading && (_jsx(Text, { variant: "body", children: SCHEDULE_PICKER_TEXT.loadingSchedules })), schedulesError && _jsx(Alert, { status: "danger", title: schedulesError }), selectedSchedule && (_jsxs("div", { className: "dte-business-calendar-preview", children: [_jsxs(Flex, { alignItems: "center", justifyContent: "space-between", gap: "2", className: "dte-business-calendar-preview-header", children: [_jsxs(Flex, { alignItems: "center", gap: "2", onClick: () => setPreviewOpen(prev => !prev), children: [_jsx(Button, { appearance: "ghost", size: "small", icon: previewOpen ? IconExpandLess : IconExpandMore, "aria-label": previewOpen
78
+ ? 'Collapse schedule preview'
79
+ : 'Expand schedule preview' }), _jsx(Text, { variant: "body", size: "small", style: { fontWeight: 'bold' }, children: selectedSchedule.name })] }), !selectedSchedule.isDefault && (_jsxs(Flex, { alignItems: "center", gap: "2", children: [_jsx(Button, { icon: EditIcon, size: "small", appearance: "ghost", onClick: handleOpenEdit, disabled: deleting }), _jsx(Button, { icon: DeleteIcon, size: "small", appearance: "ghost", "aria-label": "Delete schedule", onClick: handleDelete, loading: deleting, disabled: !onDeleteSchedule })] }))] }), deleteError && _jsx(Alert, { status: "danger", title: deleteError }), previewOpen && (_jsxs(Flex, { direction: "column", gap: "5", className: "dte-business-calendar-preview-content", children: [_jsxs(Flex, { direction: "column", gap: "4", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: SCHEDULE_PICKER_TEXT.excludedWeekends }), _jsx(Flex, { gap: "2", style: { flexWrap: 'wrap' }, children: selectedSchedule.calendar.weekendDays.map(day => {
80
+ var _a;
81
+ return (_jsx(Chip, { label: (_a = WEEKEND_DAY_LABELS[day]) !== null && _a !== void 0 ? _a : String(day), size: "medium" }, day));
82
+ }) })] }), _jsxs(Flex, { direction: "column", gap: "4", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: SCHEDULE_PICKER_TEXT.excludedHolidays }), holidayCatalogLoading && (_jsx(Text, { variant: "body", children: SCHEDULE_PICKER_TEXT.loadingHolidays })), holidayCatalogError && (_jsx(Alert, { status: "danger", title: holidayCatalogError })), _jsx(Flex, { gap: "2", style: { flexWrap: 'wrap' }, children: selectedSchedule.calendar.knownHolidays.map(code => {
83
+ const holiday = holidayByCode.get(code);
84
+ return (_jsx(Chip, { label: holiday
85
+ ? formatKnownHolidayLabel(holiday)
86
+ : code, size: "medium" }, code));
87
+ }) })] }), selectedSchedule.calendar.customDates.length > 0 && (_jsxs(Flex, { direction: "column", gap: "4", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: SCHEDULE_PICKER_TEXT.customDates }), _jsx(Flex, { gap: "2", style: { flexWrap: 'wrap' }, children: selectedSchedule.calendar.customDates.map(date => (_jsx(Chip, { label: formatCustomDateLabel(date), size: "medium" }, date))) })] }))] }))] })), drawerOpen && (_jsx(ScheduleDrawer, { businessCalendar: businessCalendar, editingSchedule: editingSchedule, onClose: handleCloseDrawer, onSaved: handleSaved }))] }));
88
+ };
@@ -0,0 +1,31 @@
1
+ /** DTE's `SaveDateScheduleRequest.Name` limit. */
2
+ export declare const MAX_SCHEDULE_NAME_LENGTH = 255;
3
+ /** The read-only default schedule's name; a tenant schedule may not reuse it (case-insensitive). */
4
+ export declare const RESERVED_SCHEDULE_NAME = "Default";
5
+ /** `MM-DD`, 01-12 / 01-31, as DTE's `CalendarSpecModel.customDates` (`02-29` is a valid leap day). */
6
+ export declare const CUSTOM_DATE_PATTERN: RegExp;
7
+ /** Days per month for an `MM-DD` custom date, Feb including the leap day. */
8
+ export declare const CUSTOM_DATE_MONTH_LENGTHS: number[];
9
+ /** The default schedule's weekend: Sunday and Saturday, DTE's `Date.getUTCDay()` numbering. */
10
+ export declare const DEFAULT_SCHEDULE_WEEKEND_DAYS: [0, 6];
11
+ /**
12
+ * `BusinessCalendarSection`'s default toggle label, used for a date calculated field and an e-sign
13
+ * `dateSignedOffset` field alike.
14
+ */
15
+ export declare const BUSINESS_CALENDAR_TOGGLE_LABEL = "Exclude weekends & holidays from the calculation";
16
+ /** How a schedule's preview names `BusinessCalendarRules.weekendDays`. */
17
+ export declare const WEEKEND_DAY_LABELS: Readonly<Record<number, string>>;
18
+ /**
19
+ * `SchedulePicker`'s text, also used by an editor that renders its own copy of the picker
20
+ * (Unlayer's Formula Builder, inside an iframe without Anvil2), so both read the same.
21
+ */
22
+ export declare const SCHEDULE_PICKER_TEXT: {
23
+ readonly selectLabel: "Select Schedule or create new";
24
+ readonly createNew: "Create new schedule";
25
+ readonly loadingSchedules: "Loading schedules…";
26
+ readonly loadingHolidays: "Loading holidays…";
27
+ readonly excludedWeekends: "Excluded Weekends";
28
+ readonly excludedHolidays: "Excluded Holidays";
29
+ readonly customDates: "Custom Dates";
30
+ readonly deleteFailed: "Failed to delete schedule";
31
+ };
@@ -0,0 +1,34 @@
1
+ /** DTE's `SaveDateScheduleRequest.Name` limit. */
2
+ export const MAX_SCHEDULE_NAME_LENGTH = 255;
3
+ /** The read-only default schedule's name; a tenant schedule may not reuse it (case-insensitive). */
4
+ export const RESERVED_SCHEDULE_NAME = 'Default';
5
+ /** `MM-DD`, 01-12 / 01-31, as DTE's `CalendarSpecModel.customDates` (`02-29` is a valid leap day). */
6
+ export const CUSTOM_DATE_PATTERN = /^(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
7
+ /** Days per month for an `MM-DD` custom date, Feb including the leap day. */
8
+ export const CUSTOM_DATE_MONTH_LENGTHS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
9
+ /** The default schedule's weekend: Sunday and Saturday, DTE's `Date.getUTCDay()` numbering. */
10
+ export const DEFAULT_SCHEDULE_WEEKEND_DAYS = [0, 6];
11
+ /**
12
+ * `BusinessCalendarSection`'s default toggle label, used for a date calculated field and an e-sign
13
+ * `dateSignedOffset` field alike.
14
+ */
15
+ export const BUSINESS_CALENDAR_TOGGLE_LABEL = 'Exclude weekends & holidays from the calculation';
16
+ /** How a schedule's preview names `BusinessCalendarRules.weekendDays`. */
17
+ export const WEEKEND_DAY_LABELS = {
18
+ 0: 'Sundays',
19
+ 6: 'Saturdays',
20
+ };
21
+ /**
22
+ * `SchedulePicker`'s text, also used by an editor that renders its own copy of the picker
23
+ * (Unlayer's Formula Builder, inside an iframe without Anvil2), so both read the same.
24
+ */
25
+ export const SCHEDULE_PICKER_TEXT = {
26
+ selectLabel: 'Select Schedule or create new',
27
+ createNew: 'Create new schedule',
28
+ loadingSchedules: 'Loading schedules…',
29
+ loadingHolidays: 'Loading holidays…',
30
+ excludedWeekends: 'Excluded Weekends',
31
+ excludedHolidays: 'Excluded Holidays',
32
+ customDates: 'Custom Dates',
33
+ deleteFailed: 'Failed to delete schedule',
34
+ };
@@ -0,0 +1,31 @@
1
+ import { type BusinessCalendarHostProps, type FieldScheduleBinding, type ScheduleEditorContext } from './types';
2
+ /** Where a field's `ScheduleEditorContext` reads the current bindings and sends the next ones. */
3
+ export interface FieldScheduleBindingsAccess {
4
+ get(): FieldScheduleBinding[];
5
+ set(bindings: FieldScheduleBinding[]): void;
6
+ }
7
+ /**
8
+ * The `ScheduleEditorContext` of the field `fieldKey`, built from an editor's schedule props: the
9
+ * toggle is on while the field has a binding, and the toggle, the picker and a schedule delete
10
+ * replace the bindings through `bindings.set`.
11
+ *
12
+ * Every change reads the bindings through `bindings.get` when it happens, not when the context was
13
+ * built, so a callback kept from an earlier render can't write stale bindings back. A pick changes
14
+ * only a field whose toggle is on, and only to a different schedule: the picker shows only while
15
+ * the toggle is on, and its select of the default schedule right after a delete (which already
16
+ * moved the field there) must neither resend the bindings nor turn back on a toggle switched off
17
+ * while the delete was running. Without a `fieldKey` the toggle is off and does nothing.
18
+ */
19
+ export declare function buildFieldScheduleEditorContext(fieldKey: string | undefined, host: BusinessCalendarHostProps, bindings: FieldScheduleBindingsAccess): ScheduleEditorContext;
20
+ /**
21
+ * A stable {@link FieldScheduleBindingsAccess} over an editor's `scheduleBindings`: changes go to the
22
+ * latest `onScheduleBindingsChange`, even from a callback kept from an earlier render, and until the
23
+ * host passes them back the next change builds on the last one sent.
24
+ */
25
+ export declare function useFieldScheduleBindingsAccess(scheduleBindings: FieldScheduleBinding[] | undefined, onScheduleBindingsChange: ((bindings: FieldScheduleBinding[]) => void) | undefined): FieldScheduleBindingsAccess;
26
+ /**
27
+ * {@link buildFieldScheduleEditorContext} for a component: the field's context from the editor's
28
+ * schedule props, rebuilt only when one of them changes, with
29
+ * {@link useFieldScheduleBindingsAccess} for the bindings.
30
+ */
31
+ export declare function useFieldScheduleEditorContext(fieldKey: string | undefined, host: BusinessCalendarHostProps): ScheduleEditorContext;
@@ -0,0 +1,110 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { clearDeletedSchedule, setFieldSchedule, setFieldScheduleEnabled, } from './schedule-bindings.utils';
3
+ /**
4
+ * The `ScheduleEditorContext` of the field `fieldKey`, built from an editor's schedule props: the
5
+ * toggle is on while the field has a binding, and the toggle, the picker and a schedule delete
6
+ * replace the bindings through `bindings.set`.
7
+ *
8
+ * Every change reads the bindings through `bindings.get` when it happens, not when the context was
9
+ * built, so a callback kept from an earlier render can't write stale bindings back. A pick changes
10
+ * only a field whose toggle is on, and only to a different schedule: the picker shows only while
11
+ * the toggle is on, and its select of the default schedule right after a delete (which already
12
+ * moved the field there) must neither resend the bindings nor turn back on a toggle switched off
13
+ * while the delete was running. Without a `fieldKey` the toggle is off and does nothing.
14
+ */
15
+ export function buildFieldScheduleEditorContext(fieldKey, host, bindings) {
16
+ var _a;
17
+ const binding = fieldKey
18
+ ? (_a = host.scheduleBindings) === null || _a === void 0 ? void 0 : _a.find(b => b.fieldKey === fieldKey)
19
+ : undefined;
20
+ const { onDeleteSchedule } = host;
21
+ return {
22
+ enabled: !!binding,
23
+ selectedScheduleId: binding === null || binding === void 0 ? void 0 : binding.scheduleId,
24
+ onToggle: enabled => {
25
+ if (fieldKey) {
26
+ bindings.set(setFieldScheduleEnabled(bindings.get(), fieldKey, enabled));
27
+ }
28
+ },
29
+ onSelectSchedule: scheduleId => {
30
+ const current = bindings.get();
31
+ const fieldBinding = fieldKey ? current.find(b => b.fieldKey === fieldKey) : undefined;
32
+ if (fieldKey && fieldBinding && fieldBinding.scheduleId !== scheduleId) {
33
+ bindings.set(setFieldSchedule(current, fieldKey, scheduleId));
34
+ }
35
+ },
36
+ holidayCatalog: host.holidayCatalog,
37
+ holidayCatalogLoading: host.holidayCatalogLoading,
38
+ holidayCatalogError: host.holidayCatalogError,
39
+ schedules: host.schedules,
40
+ schedulesLoading: host.schedulesLoading,
41
+ schedulesError: host.schedulesError,
42
+ onCreateSchedule: host.onCreateSchedule,
43
+ onUpdateSchedule: host.onUpdateSchedule,
44
+ onDeleteSchedule: onDeleteSchedule &&
45
+ (async (id) => {
46
+ await onDeleteSchedule(id);
47
+ const current = bindings.get();
48
+ if (current.some(b => b.scheduleId === id)) {
49
+ bindings.set(clearDeletedSchedule(current, id));
50
+ }
51
+ }),
52
+ };
53
+ }
54
+ /**
55
+ * A stable {@link FieldScheduleBindingsAccess} over an editor's `scheduleBindings`: changes go to the
56
+ * latest `onScheduleBindingsChange`, even from a callback kept from an earlier render, and until the
57
+ * host passes them back the next change builds on the last one sent.
58
+ */
59
+ export function useFieldScheduleBindingsAccess(scheduleBindings, onScheduleBindingsChange) {
60
+ const latestBindings = useRef(scheduleBindings !== null && scheduleBindings !== void 0 ? scheduleBindings : []);
61
+ const latestOnChange = useRef(onScheduleBindingsChange);
62
+ useEffect(() => {
63
+ latestBindings.current = scheduleBindings !== null && scheduleBindings !== void 0 ? scheduleBindings : [];
64
+ }, [scheduleBindings]);
65
+ useEffect(() => {
66
+ latestOnChange.current = onScheduleBindingsChange;
67
+ }, [onScheduleBindingsChange]);
68
+ return useMemo(() => ({
69
+ get: () => latestBindings.current,
70
+ set: next => {
71
+ var _a;
72
+ latestBindings.current = next;
73
+ (_a = latestOnChange.current) === null || _a === void 0 ? void 0 : _a.call(latestOnChange, next);
74
+ },
75
+ }), []);
76
+ }
77
+ /**
78
+ * {@link buildFieldScheduleEditorContext} for a component: the field's context from the editor's
79
+ * schedule props, rebuilt only when one of them changes, with
80
+ * {@link useFieldScheduleBindingsAccess} for the bindings.
81
+ */
82
+ export function useFieldScheduleEditorContext(fieldKey, host) {
83
+ const { holidayCatalog, holidayCatalogError, holidayCatalogLoading, onCreateSchedule, onDeleteSchedule, onScheduleBindingsChange, onUpdateSchedule, scheduleBindings, schedules, schedulesError, schedulesLoading, } = host;
84
+ const bindings = useFieldScheduleBindingsAccess(scheduleBindings, onScheduleBindingsChange);
85
+ return useMemo(() => buildFieldScheduleEditorContext(fieldKey, {
86
+ holidayCatalog,
87
+ holidayCatalogError,
88
+ holidayCatalogLoading,
89
+ onCreateSchedule,
90
+ onDeleteSchedule,
91
+ onUpdateSchedule,
92
+ scheduleBindings,
93
+ schedules,
94
+ schedulesError,
95
+ schedulesLoading,
96
+ }, bindings), [
97
+ bindings,
98
+ fieldKey,
99
+ holidayCatalog,
100
+ holidayCatalogError,
101
+ holidayCatalogLoading,
102
+ onCreateSchedule,
103
+ onDeleteSchedule,
104
+ onUpdateSchedule,
105
+ scheduleBindings,
106
+ schedules,
107
+ schedulesError,
108
+ schedulesLoading,
109
+ ]);
110
+ }
@@ -0,0 +1,8 @@
1
+ export { BusinessCalendarSection } from './components/business-calendar-section';
2
+ export { SchedulePicker } from './components/schedule-picker';
3
+ export { ScheduleDrawer } from './components/schedule-drawer';
4
+ export type * from './types';
5
+ export * from './constants';
6
+ export * from './business-calendar-validation.utils';
7
+ export * from './schedule-bindings.utils';
8
+ export * from './field-schedule-editor-context';
@@ -0,0 +1,7 @@
1
+ export { BusinessCalendarSection } from './components/business-calendar-section';
2
+ export { SchedulePicker } from './components/schedule-picker';
3
+ export { ScheduleDrawer } from './components/schedule-drawer';
4
+ export * from './constants';
5
+ export * from './business-calendar-validation.utils';
6
+ export * from './schedule-bindings.utils';
7
+ export * from './field-schedule-editor-context';
@@ -0,0 +1,22 @@
1
+ import { type BusinessDaySchedule, type FieldScheduleBinding } from './types';
2
+ /**
3
+ * The schedule a field's picker shows for its `scheduleId` (undefined for the default schedule),
4
+ * or undefined when it isn't listed (for example a deleted one).
5
+ */
6
+ export declare function findSelectedSchedule(schedules: BusinessDaySchedule[] | undefined, scheduleId: number | undefined): BusinessDaySchedule | undefined;
7
+ /**
8
+ * Replaces `fieldKey`'s binding: enabling adds it (default schedule, `scheduleId` undefined);
9
+ * disabling removes it. Every other field's binding is returned unchanged.
10
+ */
11
+ export declare function setFieldScheduleEnabled(bindings: FieldScheduleBinding[], fieldKey: string, enabled: boolean): FieldScheduleBinding[];
12
+ /**
13
+ * Sets `fieldKey`'s selected schedule (`scheduleId` undefined for the default schedule),
14
+ * enabling its toggle if it wasn't already on. Every other field's binding is returned unchanged.
15
+ */
16
+ export declare function setFieldSchedule(bindings: FieldScheduleBinding[], fieldKey: string, scheduleId: number | undefined): FieldScheduleBinding[];
17
+ /**
18
+ * Moves every field bound to the deleted schedule `scheduleId` to the default schedule, keeping its
19
+ * toggle on, as DTE falls back for a deleted schedule (and DTE refuses to save a binding to a
20
+ * schedule that no longer exists). Every other binding is returned unchanged.
21
+ */
22
+ export declare function clearDeletedSchedule(bindings: FieldScheduleBinding[], scheduleId: number): FieldScheduleBinding[];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The schedule a field's picker shows for its `scheduleId` (undefined for the default schedule),
3
+ * or undefined when it isn't listed (for example a deleted one).
4
+ */
5
+ export function findSelectedSchedule(schedules, scheduleId) {
6
+ return (schedules !== null && schedules !== void 0 ? schedules : []).find(s => scheduleId === undefined ? s.id === undefined : s.id === scheduleId);
7
+ }
8
+ /**
9
+ * Replaces `fieldKey`'s binding: enabling adds it (default schedule, `scheduleId` undefined);
10
+ * disabling removes it. Every other field's binding is returned unchanged.
11
+ */
12
+ export function setFieldScheduleEnabled(bindings, fieldKey, enabled) {
13
+ const withoutField = bindings.filter(b => b.fieldKey !== fieldKey);
14
+ return enabled ? [...withoutField, { fieldKey }] : withoutField;
15
+ }
16
+ /**
17
+ * Sets `fieldKey`'s selected schedule (`scheduleId` undefined for the default schedule),
18
+ * enabling its toggle if it wasn't already on. Every other field's binding is returned unchanged.
19
+ */
20
+ export function setFieldSchedule(bindings, fieldKey, scheduleId) {
21
+ const withoutField = bindings.filter(b => b.fieldKey !== fieldKey);
22
+ return [...withoutField, { fieldKey, scheduleId }];
23
+ }
24
+ /**
25
+ * Moves every field bound to the deleted schedule `scheduleId` to the default schedule, keeping its
26
+ * toggle on, as DTE falls back for a deleted schedule (and DTE refuses to save a binding to a
27
+ * schedule that no longer exists). Every other binding is returned unchanged.
28
+ */
29
+ export function clearDeletedSchedule(bindings, scheduleId) {
30
+ return bindings.map(b => (b.scheduleId === scheduleId ? { fieldKey: b.fieldKey } : b));
31
+ }