@servicetitan/dte-pdf-editor 1.35.0 → 1.36.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 (80) hide show
  1. package/README.md +28 -12
  2. package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
  3. package/dist/components/field-config-panel/advanced-settings.js +20 -10
  4. package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
  5. package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
  6. package/dist/components/field-config-panel/formula-generator.js +2 -1
  7. package/dist/components/field-config-panel/formula-generator.js.map +1 -1
  8. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
  9. package/dist/components/field-config-panel/formula-modal.js +47 -6
  10. package/dist/components/field-config-panel/formula-modal.js.map +1 -1
  11. package/dist/components/field-config-panel/formula-workspace.d.ts +1 -0
  12. package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
  13. package/dist/components/field-config-panel/formula-workspace.js +10 -4
  14. package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
  15. package/dist/components/field-config-panel/result-type-selector.d.ts.map +1 -1
  16. package/dist/components/field-config-panel/result-type-selector.js +1 -0
  17. package/dist/components/field-config-panel/result-type-selector.js.map +1 -1
  18. package/dist/components/pdf-view/pdf-view-calculated.d.ts +1 -0
  19. package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -1
  20. package/dist/components/pdf-view/pdf-view-calculated.js +20 -3
  21. package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -1
  22. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  23. package/dist/components/pdf-view/pdf-view-fillable.js +5 -2
  24. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  25. package/dist/components/pdf-view/pdf-view.d.ts +6 -0
  26. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  27. package/dist/components/pdf-view/pdf-view.js +2 -2
  28. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  29. package/dist/constants/calculated.constants.d.ts +2 -0
  30. package/dist/constants/calculated.constants.d.ts.map +1 -1
  31. package/dist/constants/calculated.constants.js +2 -0
  32. package/dist/constants/calculated.constants.js.map +1 -1
  33. package/dist/hooks/useFormulaEditor.d.ts +3 -0
  34. package/dist/hooks/useFormulaEditor.d.ts.map +1 -1
  35. package/dist/hooks/useFormulaEditor.js +24 -1
  36. package/dist/hooks/useFormulaEditor.js.map +1 -1
  37. package/dist/interface/types.d.ts +6 -1
  38. package/dist/interface/types.d.ts.map +1 -1
  39. package/dist/interface/types.js.map +1 -1
  40. package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
  41. package/dist/utils/data-model/extract-fields.utils.js +11 -1
  42. package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
  43. package/dist/utils/formula/evaluate-formula.utils.d.ts +10 -2
  44. package/dist/utils/formula/evaluate-formula.utils.d.ts.map +1 -1
  45. package/dist/utils/formula/evaluate-formula.utils.js +205 -4
  46. package/dist/utils/formula/evaluate-formula.utils.js.map +1 -1
  47. package/dist/utils/formula/format-calculated-result.utils.d.ts +1 -1
  48. package/dist/utils/formula/format-calculated-result.utils.d.ts.map +1 -1
  49. package/dist/utils/formula/format-calculated-result.utils.js +29 -8
  50. package/dist/utils/formula/format-calculated-result.utils.js.map +1 -1
  51. package/dist/utils/formula/validate-formula.utils.d.ts +1 -1
  52. package/dist/utils/formula/validate-formula.utils.d.ts.map +1 -1
  53. package/dist/utils/formula/validate-formula.utils.js +15 -1
  54. package/dist/utils/formula/validate-formula.utils.js.map +1 -1
  55. package/dist/utils/shared/date.utils.d.ts +2 -0
  56. package/dist/utils/shared/date.utils.d.ts.map +1 -0
  57. package/dist/utils/shared/date.utils.js +6 -0
  58. package/dist/utils/shared/date.utils.js.map +1 -0
  59. package/dist/utils/shared/index.d.ts +1 -0
  60. package/dist/utils/shared/index.d.ts.map +1 -1
  61. package/dist/utils/shared/index.js +1 -0
  62. package/dist/utils/shared/index.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/components/field-config-panel/advanced-settings.tsx +103 -77
  65. package/src/components/field-config-panel/formula-generator.tsx +2 -1
  66. package/src/components/field-config-panel/formula-modal.tsx +46 -6
  67. package/src/components/field-config-panel/formula-workspace.tsx +21 -7
  68. package/src/components/field-config-panel/result-type-selector.tsx +1 -0
  69. package/src/components/pdf-view/pdf-view-calculated.tsx +20 -3
  70. package/src/components/pdf-view/pdf-view-fillable.tsx +6 -3
  71. package/src/components/pdf-view/pdf-view.tsx +8 -0
  72. package/src/constants/calculated.constants.ts +4 -0
  73. package/src/hooks/useFormulaEditor.ts +32 -1
  74. package/src/interface/types.ts +7 -1
  75. package/src/utils/data-model/extract-fields.utils.ts +13 -1
  76. package/src/utils/formula/evaluate-formula.utils.ts +271 -5
  77. package/src/utils/formula/format-calculated-result.utils.ts +32 -10
  78. package/src/utils/formula/validate-formula.utils.ts +24 -0
  79. package/src/utils/shared/date.utils.ts +6 -0
  80. package/src/utils/shared/index.ts +1 -0
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_DATE_FORMAT } from '../../constants';
1
2
  function roundValue(value, decimals, mode) {
2
3
  const factor = 10 ** decimals;
3
4
  const scaled = value * factor;
@@ -24,27 +25,47 @@ function formatIntegerPart(s, thousandsSeparator, decimalSeparator) {
24
25
  }
25
26
  return parts.join(sep);
26
27
  }
28
+ const DATE_FORMAT_TOKENS = {
29
+ YYYY: d => String(d.getFullYear()),
30
+ YY: d => String(d.getFullYear()).slice(-2),
31
+ MMMM: d => d.toLocaleString('en-US', { month: 'long' }),
32
+ MMM: d => d.toLocaleString('en-US', { month: 'short' }),
33
+ MM: d => String(d.getMonth() + 1).padStart(2, '0'),
34
+ M: d => String(d.getMonth() + 1),
35
+ DD: d => String(d.getDate()).padStart(2, '0'),
36
+ D: d => String(d.getDate()),
37
+ };
38
+ const DATE_TOKEN_PATTERN = new RegExp(Object.keys(DATE_FORMAT_TOKENS).join('|'), 'g');
39
+ function formatDateValue(epochMs, dateFormat) {
40
+ const date = new Date(epochMs);
41
+ if (isNaN(date.getTime())) {
42
+ return '';
43
+ }
44
+ return dateFormat.replace(DATE_TOKEN_PATTERN, match => {
45
+ const fn = DATE_FORMAT_TOKENS[match];
46
+ return fn ? fn(date) : match;
47
+ });
48
+ }
27
49
  /**
28
50
  * Format a calculated formula result for display using advanced settings.
29
- * Applies rounding, decimal places, thousands/decimal separators, result type (number/currency/percent), and prefix/postfix.
51
+ * Applies rounding, decimal places, thousands/decimal separators, result type (number/currency/percent/date), and prefix/postfix.
30
52
  */
31
53
  export function formatCalculatedResult(value, format) {
54
+ var _a;
32
55
  if (format == null) {
33
56
  return String(value);
34
57
  }
35
- const { decimalSeparator, decimalSeparatorEnabled, decimals, postfixText, prefixText, resultType, roundingMode, thousandsSeparator, } = format;
36
- let num = value;
37
- if (resultType === 'percent') {
38
- num = value * 100;
58
+ if (format.resultType === 'date') {
59
+ return formatDateValue(value, (_a = format.dateFormat) !== null && _a !== void 0 ? _a : DEFAULT_DATE_FORMAT);
39
60
  }
40
- const rounded = roundValue(num, decimals, roundingMode);
61
+ const { decimalSeparator, decimalSeparatorEnabled, decimals, postfixText, prefixText, roundingMode, thousandsSeparator, } = format;
62
+ const rounded = roundValue(value, decimals, roundingMode);
41
63
  const fixed = rounded.toFixed(decimals);
42
64
  const [intPart, decPart] = fixed.split('.');
43
65
  const decimalSep = decimalSeparatorEnabled ? decimalSeparator : '.';
44
66
  const intFormatted = formatIntegerPart(intPart, thousandsSeparator, decimalSep);
45
67
  const decSuffix = decimals > 0 ? decimalSep + (decPart !== null && decPart !== void 0 ? decPart : '') : '';
46
68
  const numberStr = intFormatted + decSuffix;
47
- const suffix = resultType === 'percent' ? '%' : '';
48
- return `${prefixText}${numberStr}${suffix}${postfixText}`;
69
+ return `${prefixText}${numberStr}${postfixText}`;
49
70
  }
50
71
  //# sourceMappingURL=format-calculated-result.utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format-calculated-result.utils.js","sourceRoot":"","sources":["../../../src/utils/formula/format-calculated-result.utils.ts"],"names":[],"mappings":"AAEA,SAAS,UAAU,CACf,KAAa,EACb,QAAgB,EAChB,IAA2C;IAE3C,MAAM,MAAM,GAAG,EAAE,IAAI,QAAQ,CAAC;IAC9B,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;IAC9B,QAAQ,IAAI,EAAE,CAAC;QACX,KAAK,OAAO;YACR,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;QACvC,KAAK,MAAM;YACP,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;QACtC;YACI,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAC3C,CAAC;AACL,CAAC;AAED,SAAS,iBAAiB,CACtB,CAAS,EACT,kBAA2B,EAC3B,gBAA2B;IAE3B,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACtB,OAAO,CAAC,CAAC;IACb,CAAC;IACD,MAAM,GAAG,GAAG,gBAAgB,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,yBAAyB;IAC3E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IACjB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACjC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QACjC,CAAC,GAAG,KAAK,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAClC,KAAa,EACb,MAAyC;IAEzC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACjB,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IAED,MAAM,EACF,gBAAgB,EAChB,uBAAuB,EACvB,QAAQ,EACR,WAAW,EACX,UAAU,EACV,UAAU,EACV,YAAY,EACZ,kBAAkB,GACrB,GAAG,MAAM,CAAC;IAEX,IAAI,GAAG,GAAG,KAAK,CAAC;IAChB,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC3B,GAAG,GAAG,KAAK,GAAG,GAAG,CAAC;IACtB,CAAC;IAED,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE5C,MAAM,UAAU,GAAG,uBAAuB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC;IACpE,MAAM,YAAY,GAAG,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,EAAE,UAAU,CAAC,CAAC;IAChF,MAAM,SAAS,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEnE,MAAM,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;IAC3C,MAAM,MAAM,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAEnD,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,EAAE,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"format-calculated-result.utils.js","sourceRoot":"","sources":["../../../src/utils/formula/format-calculated-result.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGtD,SAAS,UAAU,CACf,KAAa,EACb,QAAgB,EAChB,IAA2C;IAE3C,MAAM,MAAM,GAAG,EAAE,IAAI,QAAQ,CAAC;IAC9B,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;IAC9B,QAAQ,IAAI,EAAE,CAAC;QACX,KAAK,OAAO;YACR,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;QACvC,KAAK,MAAM;YACP,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;QACtC;YACI,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAC3C,CAAC;AACL,CAAC;AAED,SAAS,iBAAiB,CACtB,CAAS,EACT,kBAA2B,EAC3B,gBAA2B;IAE3B,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACtB,OAAO,CAAC,CAAC;IACb,CAAC;IACD,MAAM,GAAG,GAAG,gBAAgB,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,yBAAyB;IAC3E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IACjB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACjC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QACjC,CAAC,GAAG,KAAK,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,kBAAkB,GAAwC;IAC5D,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAClC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACvD,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IACvD,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;IAClD,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAChC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;IAC7C,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;CAC9B,CAAC;AAEF,MAAM,kBAAkB,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;AAEtF,SAAS,eAAe,CAAC,OAAe,EAAE,UAAkB;IACxD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,CAAC;IACd,CAAC;IACD,OAAO,UAAU,CAAC,OAAO,CAAC,kBAAkB,EAAE,KAAK,CAAC,EAAE;QAClD,MAAM,EAAE,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACrC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACjC,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAClC,KAAa,EACb,MAAyC;;IAEzC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACjB,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IAED,IAAI,MAAM,CAAC,UAAU,KAAK,MAAM,EAAE,CAAC;QAC/B,OAAO,eAAe,CAAC,KAAK,EAAE,MAAA,MAAM,CAAC,UAAU,mCAAI,mBAAmB,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM,EACF,gBAAgB,EAChB,uBAAuB,EACvB,QAAQ,EACR,WAAW,EACX,UAAU,EACV,YAAY,EACZ,kBAAkB,GACrB,GAAG,MAAM,CAAC;IAEX,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;IAC1D,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE5C,MAAM,UAAU,GAAG,uBAAuB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC;IACpE,MAAM,YAAY,GAAG,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,EAAE,UAAU,CAAC,CAAC;IAChF,MAAM,SAAS,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEnE,MAAM,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;IAE3C,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,EAAE,CAAC;AACrD,CAAC"}
@@ -7,5 +7,5 @@ export interface FormulaValidationResult {
7
7
  * Validates a structured formula.
8
8
  * Rules: not empty, valid operator placement, balanced parentheses, supported operators only, all field paths exist.
9
9
  */
10
- export declare function validateFormula(formula: StructuredFormula | undefined | null, validPaths: Set<string>): FormulaValidationResult;
10
+ export declare function validateFormula(formula: StructuredFormula | undefined | null, validPaths: Set<string>, knownDateFields?: Set<string>): FormulaValidationResult;
11
11
  //# sourceMappingURL=validate-formula.utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-formula.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/formula/validate-formula.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAUD;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI,EAC7C,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GACxB,uBAAuB,CA0EzB"}
1
+ {"version":3,"file":"validate-formula.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/formula/validate-formula.utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAUD;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI,EAC7C,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAC9B,uBAAuB,CAgGzB"}
@@ -1,3 +1,4 @@
1
+ import { MAX_DATE_CALC_DAYS } from '../../constants';
1
2
  function isOperatorToken(t) {
2
3
  return t.type === 'operator';
3
4
  }
@@ -8,7 +9,7 @@ function isOperandToken(t) {
8
9
  * Validates a structured formula.
9
10
  * Rules: not empty, valid operator placement, balanced parentheses, supported operators only, all field paths exist.
10
11
  */
11
- export function validateFormula(formula, validPaths) {
12
+ export function validateFormula(formula, validPaths, knownDateFields) {
12
13
  var _a;
13
14
  const errors = [];
14
15
  if (!((_a = formula === null || formula === void 0 ? void 0 : formula.tokens) === null || _a === void 0 ? void 0 : _a.length)) {
@@ -78,6 +79,19 @@ export function validateFormula(formula, validPaths) {
78
79
  if (last && (last.type === 'operator' || last.type === 'lparen')) {
79
80
  errors.push('Formula must end with an operand or closing parenthesis');
80
81
  }
82
+ if (knownDateFields === null || knownDateFields === void 0 ? void 0 : knownDateFields.size) {
83
+ const hasDateField = tokens.some(t => t.type === 'field' && knownDateFields.has(t.path));
84
+ if (hasDateField) {
85
+ const hasMultiplyOrDivide = tokens.some(t => t.type === 'operator' && (t.value === '*' || t.value === '/'));
86
+ if (hasMultiplyOrDivide) {
87
+ errors.push('Multiply (*) and divide (/) operators cannot be used with date fields. Only +, -, and parentheses are allowed.');
88
+ }
89
+ const hasLargeNumber = tokens.some(t => t.type === 'number' && Math.abs(Number(t.value)) > MAX_DATE_CALC_DAYS);
90
+ if (hasLargeNumber) {
91
+ errors.push(`Number of days cannot exceed ${MAX_DATE_CALC_DAYS} in date calculations.`);
92
+ }
93
+ }
94
+ }
81
95
  return {
82
96
  valid: errors.length === 0,
83
97
  errors,
@@ -1 +1 @@
1
- {"version":3,"file":"validate-formula.utils.js","sourceRoot":"","sources":["../../../src/utils/formula/validate-formula.utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validate-formula.utils.js","sourceRoot":"","sources":["../../../src/utils/formula/validate-formula.utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const dateToUtcZero: (dateString: string) => string;
2
+ //# sourceMappingURL=date.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/shared/date.utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,GAAI,YAAY,MAAM,KAAG,MAKlD,CAAC"}
@@ -0,0 +1,6 @@
1
+ export const dateToUtcZero = (dateString) => {
2
+ const [year, month, day] = dateString.split('-').map(Number);
3
+ const utcDate = new Date(Date.UTC(year, month - 1, day));
4
+ return utcDate.toISOString();
5
+ };
6
+ //# sourceMappingURL=date.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date.utils.js","sourceRoot":"","sources":["../../../src/utils/shared/date.utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,UAAkB,EAAU,EAAE;IACxD,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7D,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAEzD,OAAO,OAAO,CAAC,WAAW,EAAE,CAAC;AACjC,CAAC,CAAC"}
@@ -1,2 +1,3 @@
1
+ export * from './date.utils';
1
2
  export * from './number.utils';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
@@ -1,2 +1,3 @@
1
+ export * from './date.utils';
1
2
  export * from './number.utils';
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/dte-pdf-editor",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "typings": "./dist/index.d.ts",
@@ -1,5 +1,6 @@
1
1
  import { Flex, SegmentedControl, Switch, Text } from '@servicetitan/anvil2';
2
2
  import { Dispatch, FC, SetStateAction } from 'react';
3
+ import { DEFAULT_DATE_FORMAT } from '../../constants';
3
4
  import { CalculatedFieldFormat } from '../../interface/types';
4
5
 
5
6
  interface AdvancedSettingsProps {
@@ -13,97 +14,122 @@ const ROUNDING_OPTIONS: { label: string; value: CalculatedFieldFormat['roundingM
13
14
  { label: 'Ceil', value: 'ceil' },
14
15
  ];
15
16
 
16
- export const AdvancedSettings: FC<AdvancedSettingsProps> = ({ format, setFormat }) => (
17
+ const DateFormatSettings: FC<AdvancedSettingsProps> = ({ format, setFormat }) => (
17
18
  <div className="dte-formula-advanced">
18
19
  <Text variant="body" size="small">
19
- Rounding mode
20
+ Date format
20
21
  </Text>
21
- <SegmentedControl
22
- size="medium"
23
- selected={format.roundingMode}
24
- onChange={(roundingMode: CalculatedFieldFormat['roundingMode']) =>
25
- setFormat(prev => ({ ...prev, roundingMode }))
26
- }
27
- fill
28
- >
29
- {ROUNDING_OPTIONS.map(opt => (
30
- <SegmentedControl.Segment key={opt.value} value={opt.value}>
31
- {opt.label}
32
- </SegmentedControl.Segment>
33
- ))}
34
- </SegmentedControl>
35
- <Flex alignItems="center" justifyContent="space-between">
36
- <Text variant="body" size="small">
37
- Decimal places
38
- </Text>
39
- <Text variant="body" size="small" className="dte-formula-advanced-value">
40
- {format.decimals}
41
- </Text>
42
- </Flex>
43
22
  <input
44
- type="range"
45
- min={0}
46
- max={10}
47
- value={format.decimals}
48
- onChange={e =>
49
- setFormat(prev => ({
50
- ...prev,
51
- decimals: Number(e.target.value),
52
- }))
53
- }
54
- />
55
- <Switch
56
- checked={format.thousandsSeparator}
57
- onChange={(_e, { checked }) => {
58
- setFormat(prev => ({
59
- ...prev,
60
- thousandsSeparator: checked,
61
- decimalSeparatorEnabled: checked,
62
- decimalSeparator: checked ? prev.decimalSeparator : '.',
63
- }));
64
- }}
65
- label="Thousands separator"
66
- value="switch-example"
23
+ type="text"
24
+ className="dte-formula-advanced-input"
25
+ style={{ width: '100%' }}
26
+ value={format.dateFormat ?? DEFAULT_DATE_FORMAT}
27
+ placeholder={DEFAULT_DATE_FORMAT}
28
+ onChange={e => setFormat(prev => ({ ...prev, dateFormat: e.target.value }))}
67
29
  />
30
+ <Text variant="body" size="small" color="tertiary">
31
+ Tokens: YYYY, YY, MMMM, MMM, MM, M, DD, D
32
+ </Text>
33
+ </div>
34
+ );
35
+
36
+ export const AdvancedSettings: FC<AdvancedSettingsProps> = ({ format, setFormat }) => {
37
+ if (format.resultType === 'date') {
38
+ return <DateFormatSettings format={format} setFormat={setFormat} />;
39
+ }
68
40
 
69
- {format.thousandsSeparator && (
41
+ return (
42
+ <div className="dte-formula-advanced">
43
+ <Text variant="body" size="small">
44
+ Rounding mode
45
+ </Text>
70
46
  <SegmentedControl
71
47
  size="medium"
72
- fill
73
- selected={format.decimalSeparator}
74
- onChange={(decimalSeparator: CalculatedFieldFormat['decimalSeparator']) =>
75
- setFormat(prev => ({ ...prev, decimalSeparator }))
48
+ selected={format.roundingMode}
49
+ onChange={(roundingMode: CalculatedFieldFormat['roundingMode']) =>
50
+ setFormat(prev => ({ ...prev, roundingMode }))
76
51
  }
52
+ fill
77
53
  >
78
- {['.', ','].map(decimalSeparator => (
79
- <SegmentedControl.Segment key={decimalSeparator} value={decimalSeparator}>
80
- {decimalSeparator}
54
+ {ROUNDING_OPTIONS.map(opt => (
55
+ <SegmentedControl.Segment key={opt.value} value={opt.value}>
56
+ {opt.label}
81
57
  </SegmentedControl.Segment>
82
58
  ))}
83
59
  </SegmentedControl>
84
- )}
85
- <Text variant="body" size="small">
86
- Result affixes
87
- </Text>
88
- <Flex gap="2" alignItems="center">
89
- <Text variant="body" size="small">
90
- Prefix
91
- </Text>
60
+ <Flex alignItems="center" justifyContent="space-between">
61
+ <Text variant="body" size="small">
62
+ Decimal places
63
+ </Text>
64
+ <Text variant="body" size="small" className="dte-formula-advanced-value">
65
+ {format.decimals}
66
+ </Text>
67
+ </Flex>
92
68
  <input
93
- type="text"
94
- className="dte-formula-advanced-input"
95
- value={format.prefixText}
96
- onChange={e => setFormat(prev => ({ ...prev, prefixText: e.target.value }))}
69
+ type="range"
70
+ min={0}
71
+ max={10}
72
+ value={format.decimals}
73
+ onChange={e =>
74
+ setFormat(prev => ({
75
+ ...prev,
76
+ decimals: Number(e.target.value),
77
+ }))
78
+ }
79
+ />
80
+ <Switch
81
+ checked={format.thousandsSeparator}
82
+ onChange={(_e, { checked }) => {
83
+ setFormat(prev => ({
84
+ ...prev,
85
+ thousandsSeparator: checked,
86
+ decimalSeparatorEnabled: checked,
87
+ decimalSeparator: checked ? prev.decimalSeparator : '.',
88
+ }));
89
+ }}
90
+ label="Thousands separator"
91
+ value="switch-example"
97
92
  />
93
+
94
+ {format.thousandsSeparator && (
95
+ <SegmentedControl
96
+ size="medium"
97
+ fill
98
+ selected={format.decimalSeparator}
99
+ onChange={(decimalSeparator: CalculatedFieldFormat['decimalSeparator']) =>
100
+ setFormat(prev => ({ ...prev, decimalSeparator }))
101
+ }
102
+ >
103
+ {['.', ','].map(decimalSeparator => (
104
+ <SegmentedControl.Segment key={decimalSeparator} value={decimalSeparator}>
105
+ {decimalSeparator}
106
+ </SegmentedControl.Segment>
107
+ ))}
108
+ </SegmentedControl>
109
+ )}
98
110
  <Text variant="body" size="small">
99
- Postfix
111
+ Result affixes
100
112
  </Text>
101
- <input
102
- type="text"
103
- className="dte-formula-advanced-input"
104
- value={format.postfixText}
105
- onChange={e => setFormat(prev => ({ ...prev, postfixText: e.target.value }))}
106
- />
107
- </Flex>
108
- </div>
109
- );
113
+ <Flex gap="2" alignItems="center">
114
+ <Text variant="body" size="small">
115
+ Prefix
116
+ </Text>
117
+ <input
118
+ type="text"
119
+ className="dte-formula-advanced-input"
120
+ value={format.prefixText}
121
+ onChange={e => setFormat(prev => ({ ...prev, prefixText: e.target.value }))}
122
+ />
123
+ <Text variant="body" size="small">
124
+ Postfix
125
+ </Text>
126
+ <input
127
+ type="text"
128
+ className="dte-formula-advanced-input"
129
+ value={format.postfixText}
130
+ onChange={e => setFormat(prev => ({ ...prev, postfixText: e.target.value }))}
131
+ />
132
+ </Flex>
133
+ </div>
134
+ );
135
+ };
@@ -32,6 +32,7 @@ function getFillableFilteredBuySubTypeFields(
32
32
  type: FieldTypeEnum.fillable,
33
33
  subType: f.subType,
34
34
  path: f.path,
35
+ fieldType: f.subType === 'date' ? 'date' : 'number',
35
36
  }));
36
37
  }
37
38
 
@@ -53,7 +54,7 @@ export const FormulaGenerator: FC<FormulaGeneratorProps> = ({
53
54
  [dataModel],
54
55
  );
55
56
  const fillableFieldsFromDocument = useMemo(
56
- () => getFillableFilteredBuySubTypeFields(documentFields, ['number']),
57
+ () => getFillableFilteredBuySubTypeFields(documentFields, ['number', 'date']),
57
58
  [documentFields],
58
59
  );
59
60
 
@@ -1,8 +1,8 @@
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 { FC, useCallback, useMemo, useState } from 'react';
5
- import { MODAL_CONTENT_MAX_HEIGHT } from '../../constants';
4
+ import { FC, useCallback, useEffect, useMemo, useState } from 'react';
5
+ import { DEFAULT_DATE_FORMAT, MODAL_CONTENT_MAX_HEIGHT } from '../../constants';
6
6
  import { useFormulaEditor } from '../../hooks';
7
7
  import {
8
8
  CalculatedFieldFormat,
@@ -92,6 +92,21 @@ export const FormulaModal: FC<FormulaModalProps> = ({
92
92
  return m;
93
93
  }, [allFields, fillableFieldsFromDocument]);
94
94
 
95
+ const knownDateFields = useMemo(() => {
96
+ const set = new Set<string>();
97
+ for (const f of allFields) {
98
+ if (f.fieldType === 'date' && f.path) {
99
+ set.add(f.path);
100
+ }
101
+ }
102
+ for (const f of fillableFieldsFromDocument) {
103
+ if (f.fieldType === 'date' && f.path) {
104
+ set.add(f.path);
105
+ }
106
+ }
107
+ return set;
108
+ }, [allFields, fillableFieldsFromDocument]);
109
+
95
110
  const currentExpression = useMemo(() => tokensToExpression(initialFormula), [initialFormula]);
96
111
 
97
112
  const [highlightElementPath, setHighlightElementPath] = useState('');
@@ -102,18 +117,35 @@ export const FormulaModal: FC<FormulaModalProps> = ({
102
117
 
103
118
  const formulaEditor = useFormulaEditor({
104
119
  currentExpression,
120
+ knownDateFields,
105
121
  opened: true,
106
122
  pathToLabel,
107
123
  });
108
124
 
125
+ useEffect(() => {
126
+ if (formulaEditor.expressionHasDateFields && format.resultType !== 'date') {
127
+ setFormat(prev => ({
128
+ ...prev,
129
+ resultType: 'date',
130
+ dateFormat: prev.dateFormat ?? DEFAULT_DATE_FORMAT,
131
+ }));
132
+ setAdvancedOpen(true);
133
+ } else if (!formulaEditor.expressionHasDateFields && format.resultType === 'date') {
134
+ setFormat(prev => ({
135
+ ...prev,
136
+ resultType: 'number',
137
+ }));
138
+ }
139
+ }, [formulaEditor.expressionHasDateFields, format.resultType]);
140
+
109
141
  const parsedTokens = useMemo(
110
142
  () => parseExpression(formulaEditor.draftExpression, validPaths, pathToLabel),
111
143
  [formulaEditor.draftExpression, validPaths, pathToLabel],
112
144
  );
113
145
 
114
146
  const formulaValidation = useMemo(
115
- () => validateFormula({ tokens: parsedTokens }, validPaths),
116
- [parsedTokens, validPaths],
147
+ () => validateFormula({ tokens: parsedTokens }, validPaths, knownDateFields),
148
+ [parsedTokens, validPaths, knownDateFields],
117
149
  );
118
150
 
119
151
  const unknownFieldErrors = useMemo(() => {
@@ -132,9 +164,16 @@ export const FormulaModal: FC<FormulaModalProps> = ({
132
164
  }, [onClose]);
133
165
 
134
166
  const handleSave = useCallback(() => {
135
- onSave({ tokens: parsedTokens }, format);
167
+ const usedDatePaths = parsedTokens
168
+ .filter(t => t.type === 'field' && knownDateFields.has(t.path))
169
+ .map(t => (t as { type: 'field'; path: string }).path);
170
+ const savedFormat: CalculatedFieldFormat = {
171
+ ...format,
172
+ dateFieldPaths: usedDatePaths.length > 0 ? [...new Set(usedDatePaths)] : undefined,
173
+ };
174
+ onSave({ tokens: parsedTokens }, savedFormat);
136
175
  onClose();
137
- }, [format, onClose, onSave, parsedTokens]);
176
+ }, [format, knownDateFields, onClose, onSave, parsedTokens]);
138
177
 
139
178
  const validationError = unknownFieldErrors[0] ?? formulaValidation.errors[0] ?? '';
140
179
  const isInvalid = !formulaValidation.valid || unknownFieldErrors.length > 0;
@@ -164,6 +203,7 @@ export const FormulaModal: FC<FormulaModalProps> = ({
164
203
  selectedPaths={formulaEditor.selectedFieldPaths}
165
204
  />
166
205
  <FormulaWorkspace
206
+ disabledOperators={formulaEditor.disabledOperators}
167
207
  editorRef={formulaEditor.editorRef}
168
208
  isInvalid={isInvalid && formulaEditor.isDirty}
169
209
  validationError={validationError}
@@ -17,6 +17,7 @@ import { ResultTypeSelector } from './result-type-selector';
17
17
  interface FormulaWorkspaceProps {
18
18
  actions: ReactNode;
19
19
  advancedOpen: boolean;
20
+ disabledOperators?: Set<string>;
20
21
  editorRef: RefObject<HTMLDivElement | null>;
21
22
  format: CalculatedFieldFormat;
22
23
  isInvalid: boolean;
@@ -36,6 +37,7 @@ interface FormulaWorkspaceProps {
36
37
  export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
37
38
  actions,
38
39
  advancedOpen,
40
+ disabledOperators,
39
41
  editorRef,
40
42
  format,
41
43
  isInvalid,
@@ -73,8 +75,11 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
73
75
  return (
74
76
  <Flex direction="column" flex={1} gap={2} style={{ padding: '12px' }}>
75
77
  <Text variant="body" size="small">
76
- Click a field on the left to add it. Use +, -, *, /, and parentheses to build
77
- formulas.
78
+ Click a field on the left to add it. Use +, -,{' '}
79
+ {disabledOperators?.has('*') ? '' : '*, /, and '}parentheses to build formulas.
80
+ {disabledOperators?.has('*')
81
+ ? ' Multiply and divide are not available with date fields.'
82
+ : ''}
78
83
  </Text>
79
84
  <Flex alignItems="center" justifyContent="space-between">
80
85
  <Text variant="body" size="small">
@@ -101,11 +106,20 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
101
106
  />
102
107
  {isInvalid && validationError && <Alert status="danger" title={validationError} />}
103
108
  <Flex gap={1}>
104
- {CALCULATED_OPERATIONS.map(op => (
105
- <Button key={op} onClick={() => onOperatorSelect(op)} size="small">
106
- {op}
107
- </Button>
108
- ))}
109
+ {CALCULATED_OPERATIONS.map(op => {
110
+ const isDisabled = disabledOperators?.has(op) ?? false;
111
+ return (
112
+ <Button
113
+ key={op}
114
+ onClick={() => onOperatorSelect(op)}
115
+ size="small"
116
+ disabled={isDisabled}
117
+ title={isDisabled ? 'Not available with date fields' : undefined}
118
+ >
119
+ {op}
120
+ </Button>
121
+ );
122
+ })}
109
123
  </Flex>
110
124
  <ResultTypeSelector format={format} onChange={onResultTypeChange} />
111
125
  <LinkButton appearance="primary" onClick={onToggleAdvanced}>
@@ -11,6 +11,7 @@ const RESULT_TYPE_OPTIONS: { label: string; value: CalculatedFieldFormat['result
11
11
  { label: 'Number', value: 'number' },
12
12
  { label: 'Currency', value: 'currency' },
13
13
  { label: 'Percent', value: 'percent' },
14
+ { label: 'Date', value: 'date' },
14
15
  ];
15
16
 
16
17
  export const ResultTypeSelector: FC<ResultTypeSelectorProps> = ({ format, onChange }) => (
@@ -5,19 +5,36 @@ import { evaluateFormula, formatCalculatedResult, resolvePdfDataValues } from '.
5
5
  interface PdfViewCalculatedProps {
6
6
  field: PdfField;
7
7
  data?: DataModelValues;
8
+ holidays?: string[];
8
9
  }
9
10
 
10
- export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({ data, field }) => {
11
+ export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({ data, field, holidays }) => {
12
+ const dateFieldPaths = useMemo(() => {
13
+ const paths = field.formulaFormat?.dateFieldPaths;
14
+ return paths?.length ? new Set(paths) : undefined;
15
+ }, [field.formulaFormat?.dateFieldPaths]);
16
+
11
17
  const displayValue = useMemo(() => {
12
18
  if (field.formula?.tokens?.length) {
13
- const value = evaluateFormula(field.formula, data);
19
+ const value = evaluateFormula(field.formula, data, dateFieldPaths, holidays);
20
+ if (value === null) {
21
+ return '';
22
+ }
14
23
  return formatCalculatedResult(value, field.formulaFormat);
15
24
  }
16
25
  if (field.path) {
17
26
  return resolvePdfDataValues(data, field.path) ?? '';
18
27
  }
19
28
  return field.label ?? '';
20
- }, [data, field.formula, field.formulaFormat, field.label, field.path]);
29
+ }, [
30
+ data,
31
+ dateFieldPaths,
32
+ field.formula,
33
+ field.formulaFormat,
34
+ field.label,
35
+ field.path,
36
+ holidays,
37
+ ]);
21
38
 
22
39
  return <div className="dte-pdf-field-value">{displayValue}</div>;
23
40
  };