@servicetitan/dte-pdf-editor 1.87.0 → 1.89.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 (81) hide show
  1. package/README.md +37 -30
  2. package/dist/components/display-conditions/condition-row.d.ts.map +1 -1
  3. package/dist/components/display-conditions/condition-row.js +51 -22
  4. package/dist/components/display-conditions/condition-row.js.map +1 -1
  5. package/dist/components/display-conditions/condition-value-control.d.ts.map +1 -1
  6. package/dist/components/display-conditions/condition-value-control.js +1 -2
  7. package/dist/components/display-conditions/condition-value-control.js.map +1 -1
  8. package/dist/components/display-conditions/display-condition-modal.d.ts.map +1 -1
  9. package/dist/components/display-conditions/display-condition-modal.js +6 -49
  10. package/dist/components/display-conditions/display-condition-modal.js.map +1 -1
  11. package/dist/components/field-config-panel/e-sign-date-rules-summary.d.ts +12 -0
  12. package/dist/components/field-config-panel/e-sign-date-rules-summary.d.ts.map +1 -0
  13. package/dist/components/field-config-panel/e-sign-date-rules-summary.js +37 -0
  14. package/dist/components/field-config-panel/e-sign-date-rules-summary.js.map +1 -0
  15. package/dist/components/field-config-panel/e-sign-date-signed-offset-modal.d.ts +16 -0
  16. package/dist/components/field-config-panel/e-sign-date-signed-offset-modal.d.ts.map +1 -0
  17. package/dist/components/field-config-panel/e-sign-date-signed-offset-modal.js +77 -0
  18. package/dist/components/field-config-panel/e-sign-date-signed-offset-modal.js.map +1 -0
  19. package/dist/components/field-config-panel/e-sign-offset-rule-row.d.ts +16 -0
  20. package/dist/components/field-config-panel/e-sign-offset-rule-row.d.ts.map +1 -0
  21. package/dist/components/field-config-panel/e-sign-offset-rule-row.js +25 -0
  22. package/dist/components/field-config-panel/e-sign-offset-rule-row.js.map +1 -0
  23. package/dist/components/field-config-panel/e-sign-options-modal.d.ts +15 -0
  24. package/dist/components/field-config-panel/e-sign-options-modal.d.ts.map +1 -0
  25. package/dist/components/field-config-panel/e-sign-options-modal.js +30 -0
  26. package/dist/components/field-config-panel/e-sign-options-modal.js.map +1 -0
  27. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  28. package/dist/components/field-config-panel/field-config-panel.js +30 -28
  29. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  30. package/dist/components/field-sidebar/e-sign-field-type-list.d.ts.map +1 -1
  31. package/dist/components/field-sidebar/e-sign-field-type-list.js +1 -6
  32. package/dist/components/field-sidebar/e-sign-field-type-list.js.map +1 -1
  33. package/dist/constants/field.constants.d.ts +1 -5
  34. package/dist/constants/field.constants.d.ts.map +1 -1
  35. package/dist/constants/field.constants.js +21 -7
  36. package/dist/constants/field.constants.js.map +1 -1
  37. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  38. package/dist/hooks/usePdfFieldDnD.js +13 -5
  39. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  40. package/dist/interface/types.d.ts +26 -1
  41. package/dist/interface/types.d.ts.map +1 -1
  42. package/dist/interface/types.js +1 -0
  43. package/dist/interface/types.js.map +1 -1
  44. package/dist/styles/field-config-panel.css +11 -0
  45. package/dist/styles/field-type.css +12 -0
  46. package/dist/styles/index.css +1 -0
  47. package/dist/styles/variables.css +2 -0
  48. package/dist/utils/conditions/condition-match-type.utils.d.ts +14 -0
  49. package/dist/utils/conditions/condition-match-type.utils.d.ts.map +1 -0
  50. package/dist/utils/conditions/condition-match-type.utils.js +50 -0
  51. package/dist/utils/conditions/condition-match-type.utils.js.map +1 -0
  52. package/dist/utils/conditions/e-sign-offset-rules.utils.d.ts +17 -0
  53. package/dist/utils/conditions/e-sign-offset-rules.utils.d.ts.map +1 -0
  54. package/dist/utils/conditions/e-sign-offset-rules.utils.js +34 -0
  55. package/dist/utils/conditions/e-sign-offset-rules.utils.js.map +1 -0
  56. package/dist/utils/conditions/index.d.ts +2 -0
  57. package/dist/utils/conditions/index.d.ts.map +1 -1
  58. package/dist/utils/conditions/index.js +2 -0
  59. package/dist/utils/conditions/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/components/display-conditions/condition-row.tsx +114 -131
  62. package/src/components/display-conditions/condition-value-control.tsx +1 -2
  63. package/src/components/display-conditions/display-condition-modal.tsx +13 -63
  64. package/src/components/field-config-panel/e-sign-date-rules-summary.tsx +127 -0
  65. package/src/components/field-config-panel/e-sign-date-signed-offset-modal.tsx +229 -0
  66. package/src/components/field-config-panel/e-sign-offset-rule-row.tsx +91 -0
  67. package/src/components/field-config-panel/e-sign-options-modal.tsx +118 -0
  68. package/src/components/field-config-panel/field-config-panel.tsx +70 -91
  69. package/src/components/field-sidebar/e-sign-field-type-list.tsx +11 -14
  70. package/src/constants/field.constants.ts +21 -8
  71. package/src/hooks/usePdfFieldDnD.ts +17 -2
  72. package/src/interface/types.ts +34 -1
  73. package/src/styles/field-config-panel.css +11 -0
  74. package/src/styles/field-type.css +12 -0
  75. package/src/styles/index.css +1 -0
  76. package/src/styles/variables.css +2 -0
  77. package/src/utils/conditions/__tests__/condition-match-type.utils.test.ts +104 -0
  78. package/src/utils/conditions/__tests__/e-sign-offset-rules.utils.test.ts +109 -0
  79. package/src/utils/conditions/condition-match-type.utils.ts +80 -0
  80. package/src/utils/conditions/e-sign-offset-rules.utils.ts +61 -0
  81. package/src/utils/conditions/index.ts +2 -0
@@ -9,48 +9,15 @@ import type {
9
9
  SchemaObject,
10
10
  } from '../../interface/types';
11
11
  import {
12
- getDisplayConditionFieldTypeForKey,
12
+ applyConditionMatchType,
13
+ type ConditionMatchType,
14
+ deriveConditionMatchType,
13
15
  getDocumentFieldsDataPointOptions,
14
16
  getSchemaDataPointOptions,
15
- isValidNumber,
17
+ isValidConditionGroups,
16
18
  } from '../../utils';
17
19
  import { ConditionGroupsSection } from './condition-groups-section';
18
20
 
19
- type ConditionMatchType = 'and' | 'or';
20
-
21
- const deriveConditionMatchType = (conditionState: DisplayConditionState): ConditionMatchType => {
22
- for (let g = 0; g < conditionState.groups.length; g++) {
23
- const group = conditionState.groups[g];
24
- if (g > 0 && group.logicalOperator === 'or') {
25
- return 'or';
26
- }
27
- for (let i = 1; i < group.conditions.length; i++) {
28
- if (group.conditions[i]?.logicalOperator === 'or') {
29
- return 'or';
30
- }
31
- }
32
- }
33
- return 'and';
34
- };
35
-
36
- const applyConditionMatchType = (
37
- conditionState: DisplayConditionState,
38
- matchType: ConditionMatchType,
39
- ): DisplayConditionState => {
40
- return {
41
- ...conditionState,
42
- groups: conditionState.groups.map((group, groupIndex) => ({
43
- ...group,
44
- ...(groupIndex > 0 ? { logicalOperator: matchType } : {}),
45
- conditions: group.conditions.map((c, i) =>
46
- i === 0
47
- ? { ...c, logicalOperator: undefined }
48
- : { ...c, logicalOperator: matchType },
49
- ),
50
- })),
51
- };
52
- };
53
-
54
21
  const BEHAVIOR_OPTIONS = [
55
22
  { label: 'Show', value: 'show' },
56
23
  { label: 'Hide', value: 'hide' },
@@ -159,32 +126,15 @@ export function DisplayConditionModal({
159
126
  setState(prev => applyConditionMatchType(prev, value));
160
127
  }, []);
161
128
 
162
- const canSave = useMemo(() => {
163
- for (const group of state.groups) {
164
- for (const condition of group.conditions) {
165
- if (!condition.dataPointKey) {
166
- return false;
167
- }
168
- if (condition.operator === 'is_empty' || condition.operator === 'is_not_empty') {
169
- continue;
170
- }
171
- const trimmedValue = condition.value.trim();
172
- if (!trimmedValue) {
173
- return false;
174
- }
175
- const fieldType = getDisplayConditionFieldTypeForKey(
176
- condition.dataPointKey,
177
- mergeTagOptions,
178
- fillableOptions,
179
- formFieldsByFormId ?? {},
180
- );
181
- if (fieldType === 'number' && !isValidNumber(trimmedValue)) {
182
- return false;
183
- }
184
- }
185
- }
186
- return state.groups.length > 0;
187
- }, [fillableOptions, formFieldsByFormId, mergeTagOptions, state.groups]);
129
+ const canSave = useMemo(
130
+ () =>
131
+ isValidConditionGroups(state.groups, {
132
+ fillableOptions,
133
+ formFieldsByFormId,
134
+ mergeTagOptions,
135
+ }),
136
+ [fillableOptions, formFieldsByFormId, mergeTagOptions, state.groups],
137
+ );
188
138
 
189
139
  return (
190
140
  <Dialog open onClose={handleClose} size="xlarge">
@@ -0,0 +1,127 @@
1
+ import { Button, Flex, Text } from '@servicetitan/anvil2';
2
+ import DeleteIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
3
+ import EditIcon from '@servicetitan/anvil2/assets/icons/material/round/edit.svg';
4
+ import { FC, useMemo } from 'react';
5
+ import {
6
+ CHECKBOX_OPERATORS,
7
+ type ConditionOperator,
8
+ type DisplayConditionSingle,
9
+ type ESignDateSignedOffsetSettingsRule,
10
+ NUMBER_OPERATORS,
11
+ type PdfField,
12
+ RADIO_OPERATORS,
13
+ type SchemaObject,
14
+ STRING_OPERATORS,
15
+ VALUE_LESS_OPERATORS,
16
+ } from '../../interface/types';
17
+ import { getDataPointOptions } from '../../utils';
18
+
19
+ const ALL_OPERATORS = [
20
+ ...NUMBER_OPERATORS,
21
+ ...STRING_OPERATORS,
22
+ ...RADIO_OPERATORS,
23
+ ...CHECKBOX_OPERATORS,
24
+ ];
25
+
26
+ const getOperatorSymbol = (operator: string): string =>
27
+ ALL_OPERATORS.find(op => op.value === operator)?.label.split(' (')[0] ?? operator;
28
+
29
+ export interface ESignDateRulesSummaryProps {
30
+ elseDays: number;
31
+ rulesWithConditions: ESignDateSignedOffsetSettingsRule[];
32
+ schema?: SchemaObject;
33
+ documentFields?: PdfField[];
34
+ onClear: () => void;
35
+ onEdit: () => void;
36
+ }
37
+
38
+ export const ESignDateRulesSummary: FC<ESignDateRulesSummaryProps> = ({
39
+ documentFields,
40
+ elseDays,
41
+ onClear,
42
+ onEdit,
43
+ rulesWithConditions,
44
+ schema,
45
+ }) => {
46
+ const dataPointOptions = useMemo(
47
+ () => getDataPointOptions(schema, documentFields),
48
+ [schema, documentFields],
49
+ );
50
+
51
+ const getConditionText = (condition: DisplayConditionSingle) => {
52
+ const dataPoint = dataPointOptions.find(
53
+ option => option.fullKey === condition.dataPointKey,
54
+ );
55
+ const label = dataPoint?.title ?? condition.dataPointKey;
56
+ const symbol = getOperatorSymbol(condition.operator).replace('==', '=');
57
+ const isValueLess =
58
+ dataPoint?.isCheckbox === true ||
59
+ VALUE_LESS_OPERATORS.includes(condition.operator as ConditionOperator);
60
+ const value = condition.valueLabel ?? condition.value;
61
+
62
+ return `${label} ${symbol}${isValueLess ? '' : ` ${value}`}`;
63
+ };
64
+
65
+ return (
66
+ <Flex direction="column" gap="1">
67
+ <Flex justifyContent="space-between" alignItems="center">
68
+ <Text variant="body" size="medium" style={{ fontWeight: 500 }}>
69
+ Calculated date rules
70
+ </Text>
71
+ <Flex gap="2">
72
+ <Button
73
+ appearance="ghost"
74
+ size="small"
75
+ icon={{ before: EditIcon }}
76
+ onClick={onEdit}
77
+ >
78
+ Edit
79
+ </Button>
80
+ <Button
81
+ appearance="ghost"
82
+ size="small"
83
+ icon={{ before: DeleteIcon }}
84
+ onClick={onClear}
85
+ >
86
+ Clear
87
+ </Button>
88
+ </Flex>
89
+ </Flex>
90
+ <Flex direction="column" gap="2" className="dte-rule-card --active">
91
+ {rulesWithConditions.map((rule, index) => {
92
+ const condition = rule.condition.groups[0]?.conditions[0];
93
+ return (
94
+ <Flex key={rule.condition.groups[0]?.id ?? index} direction="column">
95
+ <Text variant="body">{rule.days} After Signing When:</Text>
96
+ {condition && (
97
+ <Text
98
+ variant="body"
99
+ size="small"
100
+ style={{
101
+ color: '#422799',
102
+ }}
103
+ >
104
+ {getConditionText(condition)}
105
+ </Text>
106
+ )}
107
+ </Flex>
108
+ );
109
+ })}
110
+ <Flex direction="column">
111
+ <Text variant="body" size="medium">
112
+ {elseDays} After Signing When:
113
+ </Text>
114
+ <Text
115
+ variant="body"
116
+ size="small"
117
+ style={{
118
+ color: '#422799',
119
+ }}
120
+ >
121
+ ELSE
122
+ </Text>
123
+ </Flex>
124
+ </Flex>
125
+ </Flex>
126
+ );
127
+ };
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Why not `DisplayConditionModal`:
3
+ *
4
+ * `DisplayConditionModal` manages exactly one `DisplayConditionState` (a show/hide behavior
5
+ * toggle plus a list of condition groups) for a single field. `ESignDateSignedOffsetSettings` is
6
+ * a different shape entirely: a *list* of independent `{ days, condition }` rules, with no
7
+ * show/hide concept at all (`ESignDateSignedOffsetSettingsRule.condition` is "only show" by
8
+ * contract). The trailing, condition-less "else" row required by that settings type is owned by
9
+ * `ESignOptionsModal` (its own always-visible "Day offset" field) — this modal only ever manages
10
+ * the conditioned rules that precede it.
11
+ *
12
+ * Instead, this modal follows the same Dialog/Header/Content/Footer structure and reuses the same
13
+ * building blocks `DisplayConditionModal` is built from: `ConditionGroup`/`ConditionRow` for the
14
+ * condition-editing UI, `getSchemaDataPointOptions`/`getDocumentFieldsDataPointOptions` for
15
+ * data-point resolution, and `isValidConditionGroups` extracted from `DisplayConditionModal`.
16
+ * Nothing ESign-specific was added to any of those shared pieces.
17
+ */
18
+ import { Button, Chip, Dialog, Flex, NumberField, Text } from '@servicetitan/anvil2';
19
+ import PlusCircleIcon from '@servicetitan/anvil2/assets/icons/material/round/add_circle.svg';
20
+ import { FC, useCallback, useMemo, useState } from 'react';
21
+ import { MODAL_CONTENT_MAX_HEIGHT } from '../../constants';
22
+ import type {
23
+ ESignDateSignedOffsetSettingsFinalRule,
24
+ ESignDateSignedOffsetSettingsRule,
25
+ FormFieldsByFormIdI,
26
+ FormInfo,
27
+ PdfField,
28
+ SchemaObject,
29
+ } from '../../interface/types';
30
+ import {
31
+ addESignOffsetRule,
32
+ canAddESignOffsetRule,
33
+ createInitialESignOffsetRules,
34
+ getDocumentFieldsDataPointOptions,
35
+ getESignOffsetRulesForSave,
36
+ getSchemaDataPointOptions,
37
+ isValidConditionGroups,
38
+ removeESignOffsetRule,
39
+ updateESignOffsetRule,
40
+ } from '../../utils';
41
+ import { ESignOffsetRuleRow } from './e-sign-offset-rule-row';
42
+
43
+ export interface ESignDateSignedOffsetModalProps {
44
+ onClose: () => void;
45
+ onSave: (
46
+ state: ESignDateSignedOffsetSettingsRule[],
47
+ finalRule: ESignDateSignedOffsetSettingsFinalRule,
48
+ ) => void;
49
+ initialState: ESignDateSignedOffsetSettingsRule[];
50
+ finalRule: ESignDateSignedOffsetSettingsFinalRule;
51
+ schema?: SchemaObject;
52
+ documentFields?: PdfField[];
53
+ forms?: FormInfo[];
54
+ formFieldsByFormId?: FormFieldsByFormIdI;
55
+ onRequestFormFields?: (formId: number) => void;
56
+ formFieldsLoadingFormId?: number | null;
57
+ }
58
+
59
+ export const ESignDateSignedOffsetModal: FC<ESignDateSignedOffsetModalProps> = ({
60
+ documentFields,
61
+ finalRule,
62
+ formFieldsByFormId,
63
+ formFieldsLoadingFormId,
64
+ forms,
65
+ initialState,
66
+ onClose,
67
+ onRequestFormFields,
68
+ onSave,
69
+ schema,
70
+ }) => {
71
+ const [rules, setRules] = useState<ESignDateSignedOffsetSettingsRule[]>(
72
+ initialState.length === 0 ? createInitialESignOffsetRules() : initialState,
73
+ );
74
+ const [finalRuleDays, setFinalRuleDays] = useState<number>(finalRule.days);
75
+ const [hasClearedRules, setHasClearedRules] = useState(false);
76
+
77
+ const mergeTagOptions = useMemo(() => getSchemaDataPointOptions(schema), [schema]);
78
+
79
+ const fillableOptions = useMemo(
80
+ () => getDocumentFieldsDataPointOptions(documentFields),
81
+ [documentFields],
82
+ );
83
+
84
+ const conditionValidationDeps = useMemo(
85
+ () => ({
86
+ fillableOptions,
87
+ formFieldsByFormId,
88
+ mergeTagOptions,
89
+ }),
90
+ [fillableOptions, formFieldsByFormId, mergeTagOptions],
91
+ );
92
+
93
+ const hasConfiguredRules = useMemo(
94
+ () =>
95
+ rules.some(rule =>
96
+ isValidConditionGroups(rule.condition.groups, conditionValidationDeps),
97
+ ),
98
+ [conditionValidationDeps, rules],
99
+ );
100
+
101
+ const showClearRules = hasConfiguredRules || rules.length > 1;
102
+
103
+ const handleClose = useCallback(() => onClose(), [onClose]);
104
+
105
+ const handleSave = useCallback(() => {
106
+ const rulesToSave = getESignOffsetRulesForSave(rules, hasClearedRules, hasConfiguredRules);
107
+ onSave(rulesToSave, { days: finalRuleDays });
108
+ onClose();
109
+ }, [finalRuleDays, hasClearedRules, hasConfiguredRules, onClose, onSave, rules]);
110
+
111
+ const handleFinalRuleDaysChange = useCallback((days: number | null) => {
112
+ setFinalRuleDays(Math.max(Math.min(365, days ?? 0), 0));
113
+ }, []);
114
+
115
+ const handleAddRule = useCallback(() => {
116
+ setRules(prev => addESignOffsetRule(prev));
117
+ }, []);
118
+
119
+ const handleRemoveRule = useCallback((index: number) => {
120
+ setRules(prev => removeESignOffsetRule(prev, index));
121
+ }, []);
122
+
123
+ const handleClearRules = useCallback(() => {
124
+ setRules(createInitialESignOffsetRules());
125
+ setFinalRuleDays(0);
126
+ setHasClearedRules(true);
127
+ }, []);
128
+
129
+ const handleUpdateRule = useCallback(
130
+ (index: number, rule: ESignDateSignedOffsetSettingsRule) => {
131
+ setRules(prev => updateESignOffsetRule(prev, index, rule));
132
+ },
133
+ [],
134
+ );
135
+
136
+ const canSave = useMemo(() => {
137
+ if (hasClearedRules && !hasConfiguredRules) {
138
+ return true;
139
+ }
140
+
141
+ return rules.every(rule =>
142
+ isValidConditionGroups(rule.condition.groups, conditionValidationDeps),
143
+ );
144
+ }, [conditionValidationDeps, hasClearedRules, hasConfiguredRules, rules]);
145
+
146
+ return (
147
+ <Dialog open onClose={handleClose} size="xlarge">
148
+ <Dialog.Header>Date Signed Offset Rules</Dialog.Header>
149
+ <Dialog.Content>
150
+ <Flex
151
+ direction="column"
152
+ gap="4"
153
+ flex={1}
154
+ style={{
155
+ maxHeight: MODAL_CONTENT_MAX_HEIGHT,
156
+ overflowY: 'auto',
157
+ }}
158
+ >
159
+ {rules.length === 0 && (
160
+ <Flex justifyContent="center" alignItems="center">
161
+ <Text variant="body" size="xlarge" subdued>
162
+ Rule not configured yet.
163
+ </Text>
164
+ </Flex>
165
+ )}
166
+
167
+ {rules.map((rule, index) => (
168
+ <ESignOffsetRuleRow
169
+ key={rule.condition.groups[0]?.id ?? index}
170
+ onDelete={() => handleRemoveRule(index)}
171
+ fillableOptions={fillableOptions}
172
+ formFieldsByFormId={formFieldsByFormId}
173
+ formFieldsLoadingFormId={formFieldsLoadingFormId}
174
+ forms={forms}
175
+ mergeTagOptions={mergeTagOptions}
176
+ onChange={next => handleUpdateRule(index, next)}
177
+ onRequestFormFields={onRequestFormFields}
178
+ rule={rule}
179
+ canRemove
180
+ />
181
+ ))}
182
+ <Button
183
+ size="small"
184
+ appearance="ghost"
185
+ icon={{ before: PlusCircleIcon }}
186
+ onClick={handleAddRule}
187
+ disabled={!canAddESignOffsetRule(rules)}
188
+ >
189
+ Add Else Condition
190
+ </Button>
191
+
192
+ <Flex className="dte-rule-card">
193
+ <Flex
194
+ alignItems="center"
195
+ style={{
196
+ padding: '8px 12px',
197
+ }}
198
+ >
199
+ <Chip label="THEN" size="medium" />
200
+ </Flex>
201
+ <Flex direction="column" gap="1">
202
+ <NumberField
203
+ size="small"
204
+ label="Number of days"
205
+ value={finalRuleDays}
206
+ onChange={handleFinalRuleDaysChange}
207
+ suffix="days"
208
+ />
209
+ <Text variant="body" size="small" subdued>
210
+ days range up to 365
211
+ </Text>
212
+ </Flex>
213
+ </Flex>
214
+ </Flex>
215
+ </Dialog.Content>
216
+ <Dialog.Footer sticky>
217
+ {showClearRules && (
218
+ <Button appearance="ghost" onClick={handleClearRules}>
219
+ Clear Rules
220
+ </Button>
221
+ )}
222
+ <Dialog.CancelButton onClick={handleClose}>Cancel</Dialog.CancelButton>
223
+ <Button appearance="primary" disabled={!canSave} onClick={handleSave}>
224
+ Save Changes
225
+ </Button>
226
+ </Dialog.Footer>
227
+ </Dialog>
228
+ );
229
+ };
@@ -0,0 +1,91 @@
1
+ import { Chip, Flex, NumberField, Text } from '@servicetitan/anvil2';
2
+ import { FC } from 'react';
3
+ import { defaultCondition, defaultGroup } from '../../constants';
4
+ import type {
5
+ DataPointOption,
6
+ ESignDateSignedOffsetSettingsRule,
7
+ FormFieldsByFormIdI,
8
+ FormInfo,
9
+ } from '../../interface/types';
10
+ import { ConditionRow } from '../display-conditions/condition-row';
11
+
12
+ export interface ESignOffsetRuleRowProps {
13
+ canRemove: boolean;
14
+ rule: ESignDateSignedOffsetSettingsRule;
15
+ onChange: (rule: ESignDateSignedOffsetSettingsRule) => void;
16
+ onDelete: () => void;
17
+ fillableOptions: DataPointOption[];
18
+ mergeTagOptions: DataPointOption[];
19
+ forms?: FormInfo[];
20
+ formFieldsByFormId?: FormFieldsByFormIdI;
21
+ onRequestFormFields?: (formId: number) => void;
22
+ formFieldsLoadingFormId?: number | null;
23
+ }
24
+
25
+ export const ESignOffsetRuleRow: FC<ESignOffsetRuleRowProps> = ({
26
+ canRemove,
27
+ fillableOptions,
28
+ formFieldsByFormId,
29
+ formFieldsLoadingFormId,
30
+ forms,
31
+ mergeTagOptions,
32
+ onChange,
33
+ onDelete,
34
+ onRequestFormFields,
35
+ rule,
36
+ }) => {
37
+ const handleDaysChange = (days: number | null) => {
38
+ onChange({ ...rule, days: days ?? 0 });
39
+ };
40
+
41
+ const group = rule.condition.groups[0];
42
+ const condition = group?.conditions[0] ?? defaultCondition();
43
+
44
+ const handleConditionChange = (nextCondition: typeof condition) => {
45
+ onChange({
46
+ ...rule,
47
+ condition: {
48
+ ...rule.condition,
49
+ groups: [{ ...(group ?? defaultGroup()), conditions: [nextCondition] }],
50
+ },
51
+ });
52
+ };
53
+
54
+ return (
55
+ <Flex direction="column" gap="3" className="dte-rule-card">
56
+ <ConditionRow
57
+ canRemove={canRemove}
58
+ condition={condition}
59
+ fillableOptions={fillableOptions}
60
+ formFieldsByFormId={formFieldsByFormId}
61
+ formFieldsLoadingFormId={formFieldsLoadingFormId}
62
+ forms={forms}
63
+ mergeTagOptions={mergeTagOptions}
64
+ onChange={handleConditionChange}
65
+ onRemove={onDelete}
66
+ onRequestFormFields={onRequestFormFields}
67
+ />
68
+ <Flex>
69
+ <Flex
70
+ alignItems="center"
71
+ style={{
72
+ padding: '8px 12px',
73
+ }}
74
+ >
75
+ <Chip label="THEN" size="medium" />
76
+ </Flex>
77
+ <Flex direction="column" gap="1">
78
+ <NumberField
79
+ label="Number of days"
80
+ value={rule.days}
81
+ onChange={handleDaysChange}
82
+ suffix="days"
83
+ />
84
+ <Text variant="body" size="small" subdued>
85
+ days range up to 365
86
+ </Text>
87
+ </Flex>
88
+ </Flex>
89
+ </Flex>
90
+ );
91
+ };
@@ -0,0 +1,118 @@
1
+ import { Button, Flex, NumberField } from '@servicetitan/anvil2';
2
+ import AddIcon from '@servicetitan/anvil2/assets/icons/material/round/add_circle.svg';
3
+ import { FC, Fragment, useState } from 'react';
4
+ import {
5
+ ESignDateSignedOffsetSettings,
6
+ ESignDateSignedOffsetSettingsFinalRule,
7
+ ESignDateSignedOffsetSettingsRule,
8
+ FormFieldsByFormIdI,
9
+ FormInfo,
10
+ PdfField,
11
+ SchemaObject,
12
+ } from '../../interface/types';
13
+ import { createClearedESignOffsetSettings } from '../../utils';
14
+ import { ESignDateRulesSummary } from './e-sign-date-rules-summary';
15
+ import { ESignDateSignedOffsetModal } from './e-sign-date-signed-offset-modal';
16
+
17
+ interface ESignOptionsModalProps {
18
+ initialState: ESignDateSignedOffsetSettings;
19
+ onSave: (state: ESignDateSignedOffsetSettings) => void;
20
+ schema?: SchemaObject;
21
+ documentFields?: PdfField[];
22
+ forms?: FormInfo[];
23
+ formFieldsByFormId?: FormFieldsByFormIdI;
24
+ onRequestFormFields?: (formId: number) => void;
25
+ formFieldsLoadingFormId?: number | null;
26
+ }
27
+
28
+ export const ESignOptionsModal: FC<ESignOptionsModalProps> = ({
29
+ documentFields,
30
+ formFieldsByFormId,
31
+ formFieldsLoadingFormId,
32
+ forms,
33
+ initialState,
34
+ onRequestFormFields,
35
+ onSave,
36
+ schema,
37
+ }) => {
38
+ const [open, setOpen] = useState(false);
39
+ /*
40
+ * `rules` is a tuple: zero or more conditioned rules followed by exactly one condition-less
41
+ * final rule (enforced by `ESignDateSignedOffsetSettings['rules']`). Slicing/indexing a
42
+ * variadic tuple widens to the member union, so the split needs one explicit, deliberate cast.
43
+ */
44
+ const ruleWithOutCondition = initialState.rules[
45
+ initialState.rules.length - 1
46
+ ] as ESignDateSignedOffsetSettingsFinalRule;
47
+ const rulesWithConditions = initialState.rules.slice(
48
+ 0,
49
+ -1,
50
+ ) as ESignDateSignedOffsetSettingsRule[];
51
+
52
+ const handleDaysChange = (days: number | null) => {
53
+ onSave({
54
+ rules: [...rulesWithConditions, { days: Math.max(Math.min(365, days ?? 0), 0) }],
55
+ });
56
+ };
57
+
58
+ const handleSaveRules = (
59
+ rules: ESignDateSignedOffsetSettingsRule[],
60
+ finalRule: ESignDateSignedOffsetSettingsFinalRule,
61
+ ) => {
62
+ onSave({ rules: [...rules, finalRule] });
63
+ };
64
+
65
+ const handleClearRules = () => {
66
+ onSave(createClearedESignOffsetSettings());
67
+ };
68
+
69
+ return (
70
+ <Fragment>
71
+ {rulesWithConditions.length === 0 ? (
72
+ <Flex gap={1} direction="column" justifyContent="space-between">
73
+ <NumberField
74
+ size="small"
75
+ label="Number of days"
76
+ value={ruleWithOutCondition.days}
77
+ onChange={handleDaysChange}
78
+ suffix="days"
79
+ width="100%"
80
+ />
81
+
82
+ <Button
83
+ appearance="ghost"
84
+ size="small"
85
+ icon={AddIcon}
86
+ onClick={() => setOpen(true)}
87
+ style={{ color: '#0265DC' }}
88
+ >
89
+ Set Date Rule
90
+ </Button>
91
+ </Flex>
92
+ ) : (
93
+ <ESignDateRulesSummary
94
+ documentFields={documentFields}
95
+ onClear={handleClearRules}
96
+ onEdit={() => setOpen(true)}
97
+ rulesWithConditions={rulesWithConditions}
98
+ schema={schema}
99
+ elseDays={ruleWithOutCondition.days}
100
+ />
101
+ )}
102
+ {open && (
103
+ <ESignDateSignedOffsetModal
104
+ onClose={() => setOpen(false)}
105
+ onSave={handleSaveRules}
106
+ documentFields={documentFields}
107
+ finalRule={ruleWithOutCondition}
108
+ formFieldsByFormId={formFieldsByFormId}
109
+ formFieldsLoadingFormId={formFieldsLoadingFormId}
110
+ forms={forms}
111
+ initialState={rulesWithConditions}
112
+ onRequestFormFields={onRequestFormFields}
113
+ schema={schema}
114
+ />
115
+ )}
116
+ </Fragment>
117
+ );
118
+ };