@servicetitan/dte-pdf-editor 1.94.0 → 1.96.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 (145) hide show
  1. package/README.md +121 -4
  2. package/dist/components/field-config-panel/advanced-settings.d.ts +2 -0
  3. package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
  4. package/dist/components/field-config-panel/advanced-settings.js +5 -4
  5. package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
  6. package/dist/components/field-config-panel/e-sign-options-modal.d.ts +3 -0
  7. package/dist/components/field-config-panel/e-sign-options-modal.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/e-sign-options-modal.js +6 -4
  9. package/dist/components/field-config-panel/e-sign-options-modal.js.map +1 -1
  10. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +14 -1
  11. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  12. package/dist/components/field-config-panel/field-config-panel-overlay.js +41 -2
  13. package/dist/components/field-config-panel/field-config-panel-overlay.js.map +1 -1
  14. package/dist/components/field-config-panel/field-config-panel.d.ts +6 -0
  15. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  16. package/dist/components/field-config-panel/field-config-panel.js +17 -3
  17. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  18. package/dist/components/field-config-panel/formula-generator.d.ts +4 -0
  19. package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
  20. package/dist/components/field-config-panel/formula-generator.js +2 -2
  21. package/dist/components/field-config-panel/formula-generator.js.map +1 -1
  22. package/dist/components/field-config-panel/formula-modal.d.ts +5 -0
  23. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
  24. package/dist/components/field-config-panel/formula-modal.js +13 -6
  25. package/dist/components/field-config-panel/formula-modal.js.map +1 -1
  26. package/dist/components/field-config-panel/formula-workspace.d.ts +2 -0
  27. package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
  28. package/dist/components/field-config-panel/formula-workspace.js +6 -4
  29. package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
  30. package/dist/components/pdf-editor/pdf-editor.d.ts +52 -1
  31. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  32. package/dist/components/pdf-editor/pdf-editor.js +2 -2
  33. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  34. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
  35. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +4 -6
  36. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
  37. package/dist/components/pdf-view/pdf-view-calculated.d.ts +3 -1
  38. package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -1
  39. package/dist/components/pdf-view/pdf-view-calculated.js +14 -4
  40. package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -1
  41. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  42. package/dist/components/pdf-view/pdf-view-fillable.js +27 -4
  43. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  44. package/dist/components/pdf-view/pdf-view.d.ts +18 -4
  45. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  46. package/dist/components/pdf-view/pdf-view.js +2 -2
  47. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  48. package/dist/components/shared/char-limit-indicator.d.ts +8 -0
  49. package/dist/components/shared/char-limit-indicator.d.ts.map +1 -0
  50. package/dist/components/shared/char-limit-indicator.js +9 -0
  51. package/dist/components/shared/char-limit-indicator.js.map +1 -0
  52. package/dist/components/shared/index.d.ts +1 -0
  53. package/dist/components/shared/index.d.ts.map +1 -1
  54. package/dist/components/shared/index.js +1 -0
  55. package/dist/components/shared/index.js.map +1 -1
  56. package/dist/constants/calculated.constants.d.ts +6 -0
  57. package/dist/constants/calculated.constants.d.ts.map +1 -1
  58. package/dist/constants/calculated.constants.js +6 -0
  59. package/dist/constants/calculated.constants.js.map +1 -1
  60. package/dist/hooks/usePinchZoom.d.ts.map +1 -1
  61. package/dist/hooks/usePinchZoom.js +1 -1
  62. package/dist/hooks/usePinchZoom.js.map +1 -1
  63. package/dist/interface/types.d.ts +37 -1
  64. package/dist/interface/types.d.ts.map +1 -1
  65. package/dist/interface/types.js.map +1 -1
  66. package/dist/styles/pdf-field-overlay.css +9 -2
  67. package/dist/styles/pdf-field.css +22 -1
  68. package/dist/utils/field/field-text-box.utils.d.ts +31 -0
  69. package/dist/utils/field/field-text-box.utils.d.ts.map +1 -0
  70. package/dist/utils/field/field-text-box.utils.js +102 -0
  71. package/dist/utils/field/field-text-box.utils.js.map +1 -0
  72. package/dist/utils/field/fit-field-text.utils.d.ts +13 -0
  73. package/dist/utils/field/fit-field-text.utils.d.ts.map +1 -0
  74. package/dist/utils/field/fit-field-text.utils.js +44 -0
  75. package/dist/utils/field/fit-field-text.utils.js.map +1 -0
  76. package/dist/utils/field/get-field-max-length.utils.d.ts +38 -0
  77. package/dist/utils/field/get-field-max-length.utils.d.ts.map +1 -0
  78. package/dist/utils/field/get-field-max-length.utils.js +166 -0
  79. package/dist/utils/field/get-field-max-length.utils.js.map +1 -0
  80. package/dist/utils/field/index.d.ts +2 -0
  81. package/dist/utils/field/index.d.ts.map +1 -1
  82. package/dist/utils/field/index.js +2 -0
  83. package/dist/utils/field/index.js.map +1 -1
  84. package/dist/utils/field/text-wrap.utils.d.ts +26 -0
  85. package/dist/utils/field/text-wrap.utils.d.ts.map +1 -0
  86. package/dist/utils/field/text-wrap.utils.js +53 -0
  87. package/dist/utils/field/text-wrap.utils.js.map +1 -0
  88. package/dist/utils/formula/business-days.utils.d.ts +32 -0
  89. package/dist/utils/formula/business-days.utils.d.ts.map +1 -0
  90. package/dist/utils/formula/business-days.utils.js +148 -0
  91. package/dist/utils/formula/business-days.utils.js.map +1 -0
  92. package/dist/utils/formula/caret.utils.js +1 -1
  93. package/dist/utils/formula/caret.utils.js.map +1 -1
  94. package/dist/utils/formula/evaluate-formula.utils.d.ts +25 -6
  95. package/dist/utils/formula/evaluate-formula.utils.d.ts.map +1 -1
  96. package/dist/utils/formula/evaluate-formula.utils.js +61 -101
  97. package/dist/utils/formula/evaluate-formula.utils.js.map +1 -1
  98. package/dist/utils/formula/index.d.ts +1 -0
  99. package/dist/utils/formula/index.d.ts.map +1 -1
  100. package/dist/utils/formula/index.js +1 -0
  101. package/dist/utils/formula/index.js.map +1 -1
  102. package/dist/utils/formula/schedule-bindings.utils.d.ts +12 -0
  103. package/dist/utils/formula/schedule-bindings.utils.d.ts.map +1 -0
  104. package/dist/utils/formula/schedule-bindings.utils.js +17 -0
  105. package/dist/utils/formula/schedule-bindings.utils.js.map +1 -0
  106. package/package.json +2 -1
  107. package/src/__tests__/field-types/date-fields.test.ts +29 -3
  108. package/src/__tests__/field-types/e-sign-fields.test.ts +2 -2
  109. package/src/__tests__/field-types/fillable-fields.test.ts +2 -2
  110. package/src/components/field-config-panel/advanced-settings.tsx +16 -3
  111. package/src/components/field-config-panel/e-sign-options-modal.tsx +13 -2
  112. package/src/components/field-config-panel/field-config-panel-overlay.tsx +97 -1
  113. package/src/components/field-config-panel/field-config-panel.tsx +28 -0
  114. package/src/components/field-config-panel/formula-generator.tsx +8 -1
  115. package/src/components/field-config-panel/formula-modal.tsx +19 -4
  116. package/src/components/field-config-panel/formula-workspace.tsx +31 -6
  117. package/src/components/pdf-editor/pdf-editor.tsx +82 -0
  118. package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +41 -28
  119. package/src/components/pdf-view/pdf-view-calculated.tsx +28 -3
  120. package/src/components/pdf-view/pdf-view-fillable.tsx +82 -36
  121. package/src/components/pdf-view/pdf-view.tsx +23 -3
  122. package/src/components/shared/char-limit-indicator.tsx +23 -0
  123. package/src/components/shared/index.ts +1 -0
  124. package/src/constants/calculated.constants.ts +7 -0
  125. package/src/hooks/usePinchZoom.ts +1 -6
  126. package/src/interface/types.ts +47 -0
  127. package/src/styles/pdf-field-overlay.css +9 -2
  128. package/src/styles/pdf-field.css +22 -1
  129. package/src/utils/field/__tests__/field-background-color.utils.test.ts +8 -8
  130. package/src/utils/field/__tests__/fit-field-text.utils.test.ts +224 -0
  131. package/src/utils/field/__tests__/get-field-bounds-style.utils.test.ts +16 -0
  132. package/src/utils/field/__tests__/get-field-max-length.utils.test.ts +751 -0
  133. package/src/utils/field/__tests__/text-wrap.utils.test.ts +181 -0
  134. package/src/utils/field/field-text-box.utils.ts +143 -0
  135. package/src/utils/field/fit-field-text.utils.ts +61 -0
  136. package/src/utils/field/get-field-max-length.utils.ts +211 -0
  137. package/src/utils/field/index.ts +2 -0
  138. package/src/utils/field/text-wrap.utils.ts +78 -0
  139. package/src/utils/formula/__tests__/evaluate-formula.utils.test.ts +198 -16
  140. package/src/utils/formula/__tests__/schedule-bindings.utils.test.ts +73 -0
  141. package/src/utils/formula/business-days.utils.ts +188 -0
  142. package/src/utils/formula/caret.utils.ts +1 -1
  143. package/src/utils/formula/evaluate-formula.utils.ts +94 -114
  144. package/src/utils/formula/index.ts +1 -0
  145. package/src/utils/formula/schedule-bindings.utils.ts +27 -0
@@ -1,13 +1,20 @@
1
1
  import { Button, Flex, Text } from '@servicetitan/anvil2';
2
2
  import IconClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
3
- import { FC } from 'react';
3
+ import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
4
+ import { FC, useCallback, useMemo } from 'react';
4
5
  import {
6
+ type BusinessCalendar,
7
+ type BusinessDaySchedule,
8
+ type FieldScheduleBinding,
5
9
  type FormFieldsByFormIdI,
6
10
  type FormInfo,
11
+ type HolidayCatalog,
7
12
  type PdfField,
8
13
  type RecipientInfo,
14
+ type SaveScheduleRequest,
9
15
  type SchemaObject,
10
16
  } from '../../interface/types';
17
+ import { getFieldDateCalcOptions, setFieldSchedule, setFieldScheduleEnabled } from '../../utils';
11
18
  import { FieldConfigPanel } from './field-config-panel';
12
19
 
13
20
  interface FieldConfigPanelOverlayProps {
@@ -19,6 +26,19 @@ interface FieldConfigPanelOverlayProps {
19
26
  selectedField: PdfField;
20
27
  recipients: RecipientInfo[];
21
28
  hideConditionalLogic?: boolean;
29
+ holidays?: string[];
30
+ fieldCalendars?: Record<string, BusinessCalendar>;
31
+ scheduleBindings?: FieldScheduleBinding[];
32
+ holidayCatalog?: HolidayCatalog;
33
+ holidayCatalogLoading?: boolean;
34
+ holidayCatalogError?: string;
35
+ schedules?: BusinessDaySchedule[];
36
+ schedulesLoading?: boolean;
37
+ schedulesError?: string;
38
+ onScheduleBindingsChange?(bindings: FieldScheduleBinding[]): void;
39
+ onCreateSchedule?(request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
40
+ onUpdateSchedule?(id: number, request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
41
+ onDeleteSchedule?(id: number): Promise<void>;
22
42
  onFieldConfigChange(updates: Partial<PdfField>): void;
23
43
  onMatchingFieldsConfigChange?(
24
44
  predicate: (field: PdfField) => boolean,
@@ -32,18 +52,92 @@ interface FieldConfigPanelOverlayProps {
32
52
  export const FieldConfigPanelOverlay: FC<FieldConfigPanelOverlayProps> = ({
33
53
  dataModel,
34
54
  documentFields = [],
55
+ fieldCalendars,
35
56
  formFieldsByFormId,
36
57
  forms,
37
58
  formulaLoadingFormId,
38
59
  hideConditionalLogic,
60
+ holidayCatalog,
61
+ holidayCatalogError,
62
+ holidayCatalogLoading,
63
+ holidays,
64
+ onCreateSchedule,
39
65
  onDeleteField,
66
+ onDeleteSchedule,
40
67
  onDeselectField,
41
68
  onFieldConfigChange,
42
69
  onMatchingFieldsConfigChange,
43
70
  onRequestFormFields,
71
+ onScheduleBindingsChange,
72
+ onUpdateSchedule,
44
73
  recipients,
74
+ scheduleBindings,
75
+ schedules,
76
+ schedulesError,
77
+ schedulesLoading,
45
78
  selectedField,
46
79
  }) => {
80
+ const dateCalcOptions = useMemo(
81
+ () => getFieldDateCalcOptions(selectedField.id, scheduleBindings, holidays, fieldCalendars),
82
+ [selectedField.id, scheduleBindings, holidays, fieldCalendars],
83
+ );
84
+
85
+ const selectedFieldId = selectedField.id;
86
+ const selectedBinding = useMemo(
87
+ () => scheduleBindings?.find(b => b.fieldKey === selectedFieldId),
88
+ [scheduleBindings, selectedFieldId],
89
+ );
90
+
91
+ const handleToggle = useCallback(
92
+ (enabled: boolean) => {
93
+ onScheduleBindingsChange?.(
94
+ setFieldScheduleEnabled(scheduleBindings ?? [], selectedFieldId, enabled),
95
+ );
96
+ },
97
+ [scheduleBindings, selectedFieldId, onScheduleBindingsChange],
98
+ );
99
+
100
+ const handleSelectSchedule = useCallback(
101
+ (scheduleId: number | undefined) => {
102
+ onScheduleBindingsChange?.(
103
+ setFieldSchedule(scheduleBindings ?? [], selectedFieldId, scheduleId),
104
+ );
105
+ },
106
+ [scheduleBindings, selectedFieldId, onScheduleBindingsChange],
107
+ );
108
+
109
+ const businessCalendar: ScheduleEditorContext = useMemo(
110
+ () => ({
111
+ enabled: !!selectedBinding,
112
+ selectedScheduleId: selectedBinding?.scheduleId,
113
+ onToggle: handleToggle,
114
+ onSelectSchedule: handleSelectSchedule,
115
+ holidayCatalog,
116
+ holidayCatalogLoading,
117
+ holidayCatalogError,
118
+ schedules,
119
+ schedulesLoading,
120
+ schedulesError,
121
+ onCreateSchedule,
122
+ onUpdateSchedule,
123
+ onDeleteSchedule,
124
+ }),
125
+ [
126
+ selectedBinding,
127
+ handleToggle,
128
+ handleSelectSchedule,
129
+ holidayCatalog,
130
+ holidayCatalogLoading,
131
+ holidayCatalogError,
132
+ schedules,
133
+ schedulesLoading,
134
+ schedulesError,
135
+ onCreateSchedule,
136
+ onUpdateSchedule,
137
+ onDeleteSchedule,
138
+ ],
139
+ );
140
+
47
141
  return (
48
142
  <Flex className="dte-field-config-panel-overlay" onClick={e => e.stopPropagation()}>
49
143
  <Flex
@@ -75,6 +169,8 @@ export const FieldConfigPanelOverlay: FC<FieldConfigPanelOverlayProps> = ({
75
169
  onMatchingFieldsConfigChange={onMatchingFieldsConfigChange}
76
170
  onDeleteField={onDeleteField}
77
171
  hideConditionalLogic={hideConditionalLogic}
172
+ dateCalcOptions={dateCalcOptions}
173
+ businessCalendar={businessCalendar}
78
174
  onRequestFormFields={onRequestFormFields}
79
175
  />
80
176
  </div>
@@ -1,4 +1,5 @@
1
1
  import { Button, Checkbox, Flex, SelectFieldSync, Textarea, TextField } from '@servicetitan/anvil2';
2
+ import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
2
3
  import { FC, useMemo } from 'react';
3
4
  import { FIELD_FONT_SIZE_OPTIONS } from '../../constants';
4
5
  import {
@@ -12,6 +13,7 @@ import {
12
13
  type SchemaObject,
13
14
  } from '../../interface/types';
14
15
  import {
16
+ type DateCalcOptions,
15
17
  generateESignPath,
16
18
  generateFillablePath,
17
19
  getFieldFontSize,
@@ -35,6 +37,10 @@ interface FieldConfigPanelProps {
35
37
  formulaLoadingFormId?: number | null;
36
38
  recipients: RecipientInfo[];
37
39
  hideConditionalLogic?: boolean;
40
+ /** The calendar of this field's formula preview. */
41
+ dateCalcOptions?: DateCalcOptions;
42
+ /** The business-calendar toggle/picker/drawer state and callbacks for this field. */
43
+ businessCalendar?: ScheduleEditorContext;
38
44
  onDeleteField(): void;
39
45
  onFieldConfigChange(updates: Partial<PdfField>): void;
40
46
  onMatchingFieldsConfigChange?(
@@ -45,7 +51,9 @@ interface FieldConfigPanelProps {
45
51
  }
46
52
 
47
53
  export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
54
+ businessCalendar,
48
55
  dataModel,
56
+ dateCalcOptions,
49
57
  documentFields = [],
50
58
  field,
51
59
  formFieldsByFormId,
@@ -88,6 +96,23 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
88
96
  onFieldConfigChange({ data });
89
97
  };
90
98
 
99
+ /*
100
+ * Reuses the catalog/CRUD half of `businessCalendar` (shared tenant schedules, same as a
101
+ * calculated field's), but overrides the enabled/selected-schedule half to read and write
102
+ * `ESignDateSignedOffsetSettings` instead of a `scheduleBindings` entry, since the toggle here
103
+ * applies to every `dateSignedOffset` field in the document, same as `rules` does.
104
+ */
105
+ const dateSignedOffsetData = field.data as ESignDateSignedOffsetSettings;
106
+ const eSignBusinessCalendar: ScheduleEditorContext | undefined = businessCalendar && {
107
+ ...businessCalendar,
108
+ enabled: !!dateSignedOffsetData?.businessCalendarEnabled,
109
+ selectedScheduleId: dateSignedOffsetData?.scheduleId,
110
+ onToggle: businessCalendarEnabled =>
111
+ handleDateSignedOffsetSave({ ...dateSignedOffsetData, businessCalendarEnabled }),
112
+ onSelectSchedule: scheduleId =>
113
+ handleDateSignedOffsetSave({ ...dateSignedOffsetData, scheduleId }),
114
+ };
115
+
91
116
  const handleRecipientChange = (item: { id: string; label: string } | null) => {
92
117
  if (!item) {
93
118
  return;
@@ -245,7 +270,9 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
245
270
  )}
246
271
  {field.type === FieldTypeEnum.calculated && (
247
272
  <FormulaGenerator
273
+ businessCalendar={businessCalendar}
248
274
  dataModel={dataModel}
275
+ dateCalcOptions={dateCalcOptions}
249
276
  documentFields={documentFields}
250
277
  formFieldsByFormId={formFieldsByFormId}
251
278
  forms={forms}
@@ -281,6 +308,7 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
281
308
  <ESignOptionsModal
282
309
  initialState={field.data as ESignDateSignedOffsetSettings}
283
310
  onSave={handleDateSignedOffsetSave}
311
+ businessCalendar={eSignBusinessCalendar}
284
312
  documentFields={documentFields}
285
313
  formFieldsByFormId={formFieldsByFormId}
286
314
  formFieldsLoadingFormId={formulaLoadingFormId}
@@ -1,5 +1,6 @@
1
1
  import { Button } from '@servicetitan/anvil2';
2
2
  import EditIcon from '@servicetitan/anvil2/assets/icons/material/round/edit.svg';
3
+ import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
3
4
  import { FC, Fragment, useMemo, useState } from 'react';
4
5
  import {
5
6
  type CalculatedFieldFormat,
@@ -12,11 +13,13 @@ import {
12
13
  type SchemaObject,
13
14
  type StructuredFormula,
14
15
  } from '../../interface/types';
15
- import { extractGroupedFieldsFromDataModel } from '../../utils';
16
+ import { type DateCalcOptions, extractGroupedFieldsFromDataModel } from '../../utils';
16
17
  import { FormulaModal } from './formula-modal';
17
18
 
18
19
  export interface FormulaGeneratorProps {
20
+ businessCalendar?: ScheduleEditorContext;
19
21
  dataModel?: SchemaObject;
22
+ dateCalcOptions?: DateCalcOptions;
20
23
  documentFields?: PdfField[];
21
24
  formFieldsByFormId?: FormFieldsByFormIdI;
22
25
  forms: FormInfo[];
@@ -43,7 +46,9 @@ function getFillableFilteredBuySubTypeFields(
43
46
  }
44
47
 
45
48
  export const FormulaGenerator: FC<FormulaGeneratorProps> = ({
49
+ businessCalendar,
46
50
  dataModel,
51
+ dateCalcOptions,
47
52
  documentFields = [],
48
53
  formFieldsByFormId,
49
54
  forms,
@@ -87,7 +92,9 @@ export const FormulaGenerator: FC<FormulaGeneratorProps> = ({
87
92
  <FormulaModal
88
93
  initialFormula={formula}
89
94
  initialFormat={formulaFormat}
95
+ businessCalendar={businessCalendar}
90
96
  dataModelGroups={dataModelGroups}
97
+ dateCalcOptions={dateCalcOptions}
91
98
  fillableFieldsFromDocument={fillableFieldsFromDocument}
92
99
  formFieldsByFormId={formFieldsByFormId}
93
100
  forms={forms}
@@ -1,6 +1,7 @@
1
1
  import { Button, Dialog, Divider, Flex } from '@servicetitan/anvil2';
2
2
  import IconRedo from '@servicetitan/anvil2/assets/icons/material/round/redo.svg';
3
3
  import IconUndo from '@servicetitan/anvil2/assets/icons/material/round/undo.svg';
4
+ import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
4
5
  import { FC, useCallback, useEffect, useMemo, useState } from 'react';
5
6
  import {
6
7
  DEFAULT_DATE_CALCULATED_FORMAT,
@@ -18,6 +19,7 @@ import {
18
19
  type StructuredFormula,
19
20
  } from '../../interface/types';
20
21
  import {
22
+ type DateCalcOptions,
21
23
  evaluateFormula,
22
24
  formatCalculatedResult,
23
25
  formFieldInfosToCalculationOptions,
@@ -68,7 +70,10 @@ function setSampleValueAtPath(target: DataModelValues, path: string, value: unkn
68
70
  export interface FormulaModalProps {
69
71
  initialFormula: StructuredFormula | undefined;
70
72
  initialFormat?: CalculatedFieldFormat;
73
+ businessCalendar?: ScheduleEditorContext;
71
74
  dataModelGroups: DataModelFieldGroup[];
75
+ /** The calendar the preview uses: the field's own, or the default one. */
76
+ dateCalcOptions?: DateCalcOptions;
72
77
  fillableFieldsFromDocument?: FieldTypeOption[];
73
78
  formFieldsByFormId?: FormFieldsByFormIdI;
74
79
  forms: FormInfo[];
@@ -79,7 +84,9 @@ export interface FormulaModalProps {
79
84
  }
80
85
 
81
86
  export const FormulaModal: FC<FormulaModalProps> = ({
87
+ businessCalendar,
82
88
  dataModelGroups,
89
+ dateCalcOptions,
83
90
  fillableFieldsFromDocument = [],
84
91
  formFieldsByFormId = {},
85
92
  forms,
@@ -255,21 +262,28 @@ export const FormulaModal: FC<FormulaModalProps> = ({
255
262
  if (parsedTokens.length === 0 || !formulaValidation.valid) {
256
263
  return '';
257
264
  }
258
- return hasDivisionByZero({ tokens: parsedTokens }, sampleData)
265
+ return hasDivisionByZero({ tokens: parsedTokens }, sampleData, dateCalcOptions)
259
266
  ? 'Division by zero is not allowed'
260
267
  : '';
261
- }, [parsedTokens, formulaValidation.valid, sampleData]);
268
+ }, [parsedTokens, formulaValidation.valid, sampleData, dateCalcOptions]);
262
269
 
263
270
  const previewValue = useMemo(() => {
264
271
  if (parsedTokens.length === 0 || !formulaValidation.valid || divisionByZeroError) {
265
272
  return '';
266
273
  }
267
- const computed = evaluateFormula({ tokens: parsedTokens }, sampleData);
274
+ const computed = evaluateFormula({ tokens: parsedTokens }, sampleData, dateCalcOptions);
268
275
  if (computed === null) {
269
276
  return '';
270
277
  }
271
278
  return formatCalculatedResult(computed, format);
272
- }, [parsedTokens, formulaValidation.valid, divisionByZeroError, sampleData, format]);
279
+ }, [
280
+ parsedTokens,
281
+ formulaValidation.valid,
282
+ divisionByZeroError,
283
+ sampleData,
284
+ format,
285
+ dateCalcOptions,
286
+ ]);
273
287
 
274
288
  const handleClose = useCallback(() => {
275
289
  onClose();
@@ -317,6 +331,7 @@ export const FormulaModal: FC<FormulaModalProps> = ({
317
331
  selectedPaths={formulaEditor.selectedFieldPaths}
318
332
  />
319
333
  <FormulaWorkspace
334
+ businessCalendar={businessCalendar}
320
335
  disabledOperators={formulaEditor.disabledOperators}
321
336
  editorRef={formulaEditor.editorRef}
322
337
  isInvalid={isInvalid && formulaEditor.isDirty}
@@ -1,4 +1,8 @@
1
- import { Alert, Button, Flex, LinkButton, Text } from '@servicetitan/anvil2';
1
+ import { Alert, Button, Flex, Text } from '@servicetitan/anvil2';
2
+ import IconExpandMore from '@servicetitan/anvil2/assets/icons/material/round/expand_more.svg';
3
+ import IconArrowForward from '@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_right.svg';
4
+
5
+ import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
2
6
  import {
3
7
  Dispatch,
4
8
  FC,
@@ -17,6 +21,7 @@ import { ResultTypeSelector } from './result-type-selector';
17
21
  interface FormulaWorkspaceProps {
18
22
  actions: ReactNode;
19
23
  advancedOpen: boolean;
24
+ businessCalendar?: ScheduleEditorContext;
20
25
  disabledOperators?: Set<string>;
21
26
  editorRef: RefObject<HTMLDivElement | null>;
22
27
  format: CalculatedFieldFormat;
@@ -38,6 +43,7 @@ interface FormulaWorkspaceProps {
38
43
  export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
39
44
  actions,
40
45
  advancedOpen,
46
+ businessCalendar,
41
47
  disabledOperators,
42
48
  editorRef,
43
49
  format,
@@ -75,7 +81,7 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
75
81
  );
76
82
 
77
83
  return (
78
- <Flex direction="column" flex={1} gap={2} style={{ padding: '12px' }}>
84
+ <Flex direction="column" flex={1} gap={2} style={{ padding: '12px', overflowY: 'auto' }}>
79
85
  <Flex direction="column" gap={1}>
80
86
  <Text variant="body" size="small">
81
87
  Preview (uses demo sample data)
@@ -132,10 +138,29 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
132
138
  })}
133
139
  </Flex>
134
140
  <ResultTypeSelector format={format} onChange={onResultTypeChange} />
135
- <LinkButton appearance="primary" onClick={onToggleAdvanced}>
136
- {advancedOpen ? 'Hide advanced settings' : 'Show advanced settings'}
137
- </LinkButton>
138
- {advancedOpen && <AdvancedSettings format={format} setFormat={setFormat} />}
141
+ <Flex
142
+ role="button"
143
+ onClick={onToggleAdvanced}
144
+ alignItems="center"
145
+ gap={1}
146
+ style={{ cursor: 'pointer', marginTop: 16 }}
147
+ >
148
+ <Text variant="eyebrow" size="small">
149
+ Advanced settings
150
+ </Text>
151
+ {advancedOpen ? (
152
+ <IconExpandMore fontSize="small" />
153
+ ) : (
154
+ <IconArrowForward fontSize="small" />
155
+ )}
156
+ </Flex>
157
+ {advancedOpen && (
158
+ <AdvancedSettings
159
+ format={format}
160
+ setFormat={setFormat}
161
+ businessCalendar={businessCalendar}
162
+ />
163
+ )}
139
164
  </Flex>
140
165
  );
141
166
  };
@@ -2,11 +2,16 @@ import { Flex } from '@servicetitan/anvil2';
2
2
  import { FC, ReactNode, useCallback, useMemo, useRef } from 'react';
3
3
  import { useFormFields, usePdfFieldDnD, usePdfFieldSelection } from '../../hooks';
4
4
  import {
5
+ type BusinessCalendar,
6
+ type BusinessDaySchedule,
5
7
  FieldTypeEnum,
8
+ type FieldScheduleBinding,
6
9
  type FormFieldsByFormIdI,
7
10
  type FormInfo,
11
+ type HolidayCatalog,
8
12
  type PdfField,
9
13
  type RecipientInfo,
14
+ type SaveScheduleRequest,
10
15
  type SchemaObject,
11
16
  } from '../../interface/types';
12
17
  import { extractGroupedFieldsFromDataModel, mapColorsToRecipients } from '../../utils';
@@ -30,10 +35,61 @@ export interface PdfEditorProps {
30
35
  hideConditionalLogic?: boolean;
31
36
  forms?: FormInfo[];
32
37
  zoomProps?: PdfCanvasProps['zoomProps'];
38
+ /**
39
+ * Holidays of the default business-day calendar, as `PdfView`'s `holidays`: the formula
40
+ * builder's preview of a date calculated field without its own calendar skips Saturday,
41
+ * Sunday and these days.
42
+ */
43
+ holidays?: string[];
44
+ /** Calendars of the date calculated fields bound to a schedule, by field id, as `PdfView`'s. */
45
+ fieldCalendars?: Record<string, BusinessCalendar>;
46
+ /**
47
+ * Date calculated fields whose custom business-day exclusion toggle is on, and the schedule
48
+ * each uses (`PdfField.id` as `fieldKey`), as `PdfView`'s. Also drives the Formula Builder's
49
+ * toggle/picker for the selected field.
50
+ */
51
+ scheduleBindings?: FieldScheduleBinding[];
52
+ /** The tenant's known holidays, for the schedule editor's checklist. */
53
+ holidayCatalog?: HolidayCatalog;
54
+ holidayCatalogLoading?: boolean;
55
+ holidayCatalogError?: string;
56
+ /** The schedules the tenant can pick from the Formula Builder's schedule picker. */
57
+ schedules?: BusinessDaySchedule[];
58
+ schedulesLoading?: boolean;
59
+ schedulesError?: string;
33
60
  onFormSelect?: (
34
61
  formIds: number[],
35
62
  sendFormFields: (formFieldsByFormId: FormFieldsByFormIdI) => void,
36
63
  ) => void;
64
+ /**
65
+ * Called with the complete desired `scheduleBindings` whenever the Formula Builder's toggle
66
+ * or schedule picker changes the selected field's binding (full replacement, mirroring DTE's
67
+ * `PUT api/holidays/templates/{id}/bindings`). `PdfEditor` does not persist this itself —
68
+ * the host is expected to save it (and the bindings are typically saved separately from the
69
+ * template's own fields).
70
+ */
71
+ onScheduleBindingsChange?(bindings: FieldScheduleBinding[]): void;
72
+ /**
73
+ * Creates a tenant schedule from the "Create New Schedule" drawer. Always creates a new
74
+ * schedule — it never overwrites a selected one. On success the created schedule is
75
+ * auto-selected in the picker. A rejected promise's `message` is shown as the drawer's error
76
+ * (for example a duplicate-name conflict).
77
+ */
78
+ onCreateSchedule?(request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
79
+ /**
80
+ * Replaces a tenant schedule's name and rules in place, from the picker's "Edit" action
81
+ * (DTE's `PUT api/holidays/schedules/{id}`). Applies from the next conversion of every
82
+ * template using it. On success the updated schedule stays selected.
83
+ */
84
+ onUpdateSchedule?(id: number, request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
85
+ /**
86
+ * Permanently deletes a tenant schedule, from the picker's "Delete" action (DTE's
87
+ * `DELETE api/holidays/schedules/{id}`). Fields still bound to it fall back to the default
88
+ * schedule — `PdfEditor` selects the default schedule for the current field on success, but
89
+ * does not itself update other fields' `scheduleBindings`; the host should drop bindings to
90
+ * a deleted schedule when it next saves them.
91
+ */
92
+ onDeleteSchedule?(id: number): Promise<void>;
37
93
  onLoadSuccess?(numPages: number): void;
38
94
  onFieldsChange(fields: PdfField[]): void;
39
95
  }
@@ -42,17 +98,30 @@ export const PdfEditor: FC<PdfEditorProps> = ({
42
98
  dataModel,
43
99
  errorPlaceholder,
44
100
  errors = {},
101
+ fieldCalendars,
45
102
  fields = [],
46
103
  forms,
47
104
  hideConditionalLogic,
48
105
  hideFields,
106
+ holidayCatalog,
107
+ holidayCatalogError,
108
+ holidayCatalogLoading,
109
+ holidays,
49
110
  loading = false,
50
111
  loadingPlaceholder,
112
+ onCreateSchedule,
113
+ onDeleteSchedule,
51
114
  onFieldsChange,
52
115
  onFormSelect,
53
116
  onLoadSuccess,
117
+ onScheduleBindingsChange,
118
+ onUpdateSchedule,
54
119
  pdfUrl,
55
120
  recipients = [],
121
+ scheduleBindings,
122
+ schedules,
123
+ schedulesError,
124
+ schedulesLoading,
56
125
  zoomProps,
57
126
  zoomable = true,
58
127
  }) => {
@@ -99,6 +168,19 @@ export const PdfEditor: FC<PdfEditorProps> = ({
99
168
  recipients={recipients}
100
169
  onDeselectField={pdfFieldSelection.deselectField}
101
170
  hideConditionalLogic={hideConditionalLogic}
171
+ holidays={holidays}
172
+ fieldCalendars={fieldCalendars}
173
+ scheduleBindings={scheduleBindings}
174
+ onScheduleBindingsChange={onScheduleBindingsChange}
175
+ holidayCatalog={holidayCatalog}
176
+ holidayCatalogLoading={holidayCatalogLoading}
177
+ holidayCatalogError={holidayCatalogError}
178
+ schedules={schedules}
179
+ schedulesLoading={schedulesLoading}
180
+ schedulesError={schedulesError}
181
+ onCreateSchedule={onCreateSchedule}
182
+ onUpdateSchedule={onUpdateSchedule}
183
+ onDeleteSchedule={onDeleteSchedule}
102
184
  onRequestFormFields={formFields.requestFormFieldsById}
103
185
  />
104
186
  )}
@@ -3,12 +3,14 @@ import { v4 as uuidv4 } from 'uuid';
3
3
  import { type PdfField } from '../../interface/types';
4
4
  import {
5
5
  getDropdownFieldItems,
6
+ getFieldMaxLength,
6
7
  getFieldPlaceholderText,
7
8
  getFieldTextClipClassName,
8
9
  getFillablePaintedLabel,
9
10
  getTodayDateInputValue,
10
11
  isRadioDefaultSelected,
11
12
  } from '../../utils';
13
+ import { CharLimitIndicator } from '../shared';
12
14
  import { AddHandle } from './add-handle';
13
15
 
14
16
  interface PdfOverlayFieldFillableProps {
@@ -130,14 +132,20 @@ export const PdfOverlayFieldFillable: FC<PdfOverlayFieldFillableProps> = ({
130
132
  case 'number': {
131
133
  const defaultNumberValue = field.default?.type === 'number' ? field.default.value : '';
132
134
  return (
133
- <input
134
- key={defaultNumberValue}
135
- type="number"
136
- readOnly
137
- defaultValue={defaultNumberValue}
138
- className={`dte-pdf-field-fillable ${clipClassName}`}
139
- placeholder={placeholderText}
140
- />
135
+ <Fragment>
136
+ <input
137
+ key={defaultNumberValue}
138
+ type="number"
139
+ readOnly
140
+ defaultValue={defaultNumberValue}
141
+ className={`dte-pdf-field-fillable ${clipClassName}`}
142
+ placeholder={placeholderText}
143
+ />
144
+ <CharLimitIndicator
145
+ maxLength={getFieldMaxLength(field, String(defaultNumberValue))}
146
+ value={String(defaultNumberValue)}
147
+ />
148
+ </Fragment>
141
149
  );
142
150
  }
143
151
 
@@ -166,27 +174,32 @@ export const PdfOverlayFieldFillable: FC<PdfOverlayFieldFillableProps> = ({
166
174
 
167
175
  default: {
168
176
  const defaultTextValue = field.default?.type === 'text' ? field.default.value : '';
169
- if (field.multiline) {
170
- return (
171
- <textarea
172
- key={defaultTextValue}
173
- readOnly
174
- defaultValue={defaultTextValue}
175
- style={{ font: 'inherit', lineHeight: 'inherit', outline: 'none' }}
176
- placeholder={placeholderText}
177
- className={`dte-pdf-field-fillable ${clipClassName}`}
178
- />
179
- );
180
- }
181
177
  return (
182
- <input
183
- key={defaultTextValue}
184
- type="text"
185
- readOnly
186
- defaultValue={defaultTextValue}
187
- placeholder={placeholderText}
188
- className={`dte-pdf-field-fillable ${clipClassName}`}
189
- />
178
+ <Fragment>
179
+ {field.multiline ? (
180
+ <textarea
181
+ key={defaultTextValue}
182
+ readOnly
183
+ defaultValue={defaultTextValue}
184
+ style={{ font: 'inherit', lineHeight: 'inherit', outline: 'none' }}
185
+ placeholder={placeholderText}
186
+ className={`dte-pdf-field-fillable ${clipClassName}`}
187
+ />
188
+ ) : (
189
+ <input
190
+ key={defaultTextValue}
191
+ type="text"
192
+ readOnly
193
+ defaultValue={defaultTextValue}
194
+ placeholder={placeholderText}
195
+ className={`dte-pdf-field-fillable ${clipClassName}`}
196
+ />
197
+ )}
198
+ <CharLimitIndicator
199
+ maxLength={getFieldMaxLength(field, defaultTextValue)}
200
+ value={defaultTextValue}
201
+ />
202
+ </Fragment>
190
203
  );
191
204
  }
192
205
  }