@dainamite/cpq 0.6.0 → 0.7.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 (164) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +3 -9
  3. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
  4. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
  5. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
  6. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
  7. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
  8. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
  9. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
  10. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +2 -2
  11. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
  12. package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
  13. package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
  14. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +154 -178
  15. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  16. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
  17. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
  18. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
  19. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
  20. package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
  21. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
  22. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +69 -66
  23. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  24. package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
  25. package/dist/modules/cpq/backend/cpq/orders/page.js.map +2 -2
  26. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +41 -36
  27. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
  28. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
  29. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
  30. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
  31. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
  32. package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
  33. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
  34. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
  35. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
  36. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
  37. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
  38. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
  39. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
  40. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
  41. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
  42. package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
  43. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
  44. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
  45. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
  46. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +6 -3
  47. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +2 -2
  48. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +129 -94
  49. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +2 -2
  50. package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
  51. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
  52. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +19 -15
  53. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
  54. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
  55. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
  56. package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
  57. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
  58. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
  59. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
  60. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
  61. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
  62. package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
  63. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
  64. package/dist/modules/cpq/components/ArcLineDiff.js +2 -2
  65. package/dist/modules/cpq/components/ArcLineDiff.js.map +2 -2
  66. package/dist/modules/cpq/components/ChargeBreakdownTable.js +4 -10
  67. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +2 -2
  68. package/dist/modules/cpq/components/PriceWaterfall.js +3 -1
  69. package/dist/modules/cpq/components/PriceWaterfall.js.map +2 -2
  70. package/dist/modules/cpq/components/PricingSummaryCard.js +1 -6
  71. package/dist/modules/cpq/components/PricingSummaryCard.js.map +2 -2
  72. package/dist/modules/cpq/components/StatusPath.js +5 -3
  73. package/dist/modules/cpq/components/StatusPath.js.map +2 -2
  74. package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
  75. package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
  76. package/dist/modules/cpq/i18n/de.json +1220 -0
  77. package/dist/modules/cpq/i18n/en.json +1220 -0
  78. package/dist/modules/cpq/i18n/es.json +1220 -0
  79. package/dist/modules/cpq/i18n/pl.json +1220 -0
  80. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
  81. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
  82. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
  83. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
  84. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
  85. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
  86. package/dist/modules/cpq/widgets/injection-table.js +2 -2
  87. package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
  88. package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
  89. package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
  90. package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
  91. package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
  92. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
  93. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
  94. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
  95. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
  96. package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
  97. package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
  98. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
  99. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
  100. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
  101. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
  102. package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
  103. package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
  104. package/dist/modules/cpq/workflows/steps/index.js +12 -0
  105. package/dist/modules/cpq/workflows/steps/index.js.map +2 -2
  106. package/package.json +1 -1
  107. package/src/modules/cpq/__tests__/i18n/dictionaries.test.ts +62 -0
  108. package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
  109. package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -0
  110. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +3 -9
  111. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
  112. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
  113. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
  114. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +2 -2
  115. package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
  116. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
  117. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +134 -112
  118. package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
  119. package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
  120. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +66 -43
  121. package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
  122. package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
  123. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
  124. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
  125. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
  126. package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
  127. package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
  128. package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
  129. package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
  130. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  131. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
  132. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +3 -3
  133. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +134 -89
  134. package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
  135. package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
  136. package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
  137. package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
  138. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
  139. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
  140. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  141. package/src/modules/cpq/components/ArcLineDiff.tsx +2 -2
  142. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +4 -2
  143. package/src/modules/cpq/components/PriceWaterfall.tsx +4 -2
  144. package/src/modules/cpq/components/PricingSummaryCard.tsx +1 -1
  145. package/src/modules/cpq/components/StatusPath.tsx +7 -4
  146. package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
  147. package/src/modules/cpq/i18n/de.json +1220 -0
  148. package/src/modules/cpq/i18n/en.json +1220 -0
  149. package/src/modules/cpq/i18n/es.json +1220 -0
  150. package/src/modules/cpq/i18n/pl.json +1220 -0
  151. package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
  152. package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
  153. package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
  154. package/src/modules/cpq/widgets/injection-table.ts +8 -2
  155. package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
  156. package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
  157. package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
  158. package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
  159. package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
  160. package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
  161. package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
  162. package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
  163. package/src/modules/cpq/workflows/steps/index.ts +12 -0
  164. package/src/modules/cpq/workflows/types.ts +4 -0
@@ -1,17 +1,31 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import React, { useState, useEffect, useCallback } from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
4
5
  function fmt(n, currency = "USD") {
5
6
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n);
6
7
  }
8
+ function chargeTypeLabel(t, chargeType) {
9
+ switch (chargeType) {
10
+ case "nrc":
11
+ return t("cpq.lines.chargeType.nrc", "nrc");
12
+ case "mrc":
13
+ return t("cpq.lines.chargeType.mrc", "mrc");
14
+ case "usage":
15
+ return t("cpq.lines.chargeType.usage", "usage");
16
+ default:
17
+ return chargeType;
18
+ }
19
+ }
7
20
  function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
21
+ const t = useT();
8
22
  const [quote, setQuote] = useState(null);
9
23
  const [loading, setLoading] = useState(true);
10
24
  const [submitting, setSubmitting] = useState(false);
11
25
  const showChargeBreakdown = config.showChargeBreakdown !== false;
12
26
  const showAdjustments = config.showAdjustments !== false;
13
27
  const submitAction = config.submitAction ?? "save";
14
- const submitLabel = config.submitLabel ?? "Complete";
28
+ const submitLabel = config.submitLabel ?? t("cpq.wizard.steps.review.complete", "Complete");
15
29
  const loadQuote = useCallback(async () => {
16
30
  if (!quoteId) {
17
31
  setLoading(false);
@@ -54,33 +68,33 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
54
68
  }
55
69
  }, [quoteId, submitAction, quote, onComplete]);
56
70
  if (loading) {
57
- return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading quote summary..." });
71
+ return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("cpq.wizard.steps.review.loading", "Loading quote summary...") });
58
72
  }
59
73
  if (!quote) {
60
- return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: "No quote data available. Please go back and configure items." });
74
+ return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: t("cpq.wizard.steps.review.noData", "No quote data available. Please go back and configure items.") });
61
75
  }
62
76
  const { pricingSummary, lines } = quote;
63
77
  return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
64
78
  /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 space-y-3", children: [
65
- /* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: "Quote Summary" }),
79
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: t("cpq.wizard.steps.review.summaryTitle", "Quote Summary") }),
66
80
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
67
81
  /* @__PURE__ */ jsxs("div", { children: [
68
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "NRC Total" }),
82
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.nrcTotal", "NRC Total") }),
69
83
  /* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: fmt(pricingSummary.nrcTotal, pricingSummary.currencyCode) })
70
84
  ] }),
71
85
  /* @__PURE__ */ jsxs("div", { children: [
72
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "MRC Total" }),
86
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.mrcTotal", "MRC Total") }),
73
87
  /* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: fmt(pricingSummary.mrcTotal, pricingSummary.currencyCode) })
74
88
  ] }),
75
89
  pricingSummary.discountTotal > 0 && /* @__PURE__ */ jsxs("div", { children: [
76
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Discounts" }),
90
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.discounts", "Discounts") }),
77
91
  /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
78
92
  "-",
79
93
  fmt(pricingSummary.discountTotal, pricingSummary.currencyCode)
80
94
  ] })
81
95
  ] }),
82
96
  pricingSummary.surchargeTotal > 0 && /* @__PURE__ */ jsxs("div", { children: [
83
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Surcharges" }),
97
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.surcharges", "Surcharges") }),
84
98
  /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
85
99
  "+",
86
100
  fmt(pricingSummary.surchargeTotal, pricingSummary.currencyCode)
@@ -89,36 +103,24 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
89
103
  ] })
90
104
  ] }),
91
105
  /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
92
- /* @__PURE__ */ jsxs("h4", { className: "text-sm font-medium", children: [
93
- "Items (",
94
- lines.length,
95
- ")"
96
- ] }),
106
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: t("cpq.wizard.steps.review.items", "Items ({count})", { count: lines.length }) }),
97
107
  /* @__PURE__ */ jsx("div", { className: "space-y-2", children: lines.map((line) => /* @__PURE__ */ jsxs("div", { className: "rounded-lg border p-3 space-y-2", children: [
98
108
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
99
109
  /* @__PURE__ */ jsx("strong", { className: "text-sm", children: line.offeringName }),
100
- /* @__PURE__ */ jsxs("span", { className: "text-sm", children: [
101
- fmt(line.nrcTotal, pricingSummary.currencyCode),
102
- " NRC / ",
103
- fmt(line.mrcTotal, pricingSummary.currencyCode),
104
- " MRC"
105
- ] })
106
- ] }),
107
- line.quantity > 1 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
108
- "Quantity: ",
109
- line.quantity
110
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: t("cpq.wizard.linePricing", "{nrc} NRC / {mrc} MRC", { nrc: fmt(line.nrcTotal, pricingSummary.currencyCode), mrc: fmt(line.mrcTotal, pricingSummary.currencyCode) }) })
110
111
  ] }),
112
+ line.quantity > 1 && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.quantity", "Quantity: {quantity}", { quantity: line.quantity }) }),
111
113
  showChargeBreakdown && line.charges?.length > 0 && /* @__PURE__ */ jsx("div", { className: "rounded border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
112
114
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b bg-muted/30", children: [
113
- /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: "Charge" }),
114
- /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: "Type" }),
115
- /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-right font-medium", children: "Price" })
115
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: t("cpq.wizard.steps.review.charges.charge", "Charge") }),
116
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: t("cpq.wizard.steps.review.charges.type", "Type") }),
117
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-right font-medium", children: t("cpq.wizard.steps.review.charges.price", "Price") })
116
118
  ] }) }),
117
119
  /* @__PURE__ */ jsx("tbody", { children: line.charges.map((charge) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
118
120
  /* @__PURE__ */ jsxs("tr", { className: "border-b last:border-0", children: [
119
121
  /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5", children: charge.chargeName }),
120
- /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 uppercase", children: charge.chargeType }),
121
- /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right", children: charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : `${fmt(charge.unitPrice, charge.currencyCode)}/unit` })
122
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 uppercase", children: chargeTypeLabel(t, charge.chargeType) }),
123
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right", children: charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : t("cpq.lines.perUnit", "{price}/unit", { price: fmt(charge.unitPrice, charge.currencyCode) }) })
122
124
  ] }),
123
125
  showAdjustments && charge.adjustments?.map((adj) => /* @__PURE__ */ jsxs("tr", { className: "text-[11px] text-muted-foreground", children: [
124
126
  /* @__PURE__ */ jsx("td", { colSpan: 2, className: "px-3 py-0.5 pl-6", children: adj.ruleName }),
@@ -132,7 +134,7 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
132
134
  ] }, line.lineId)) })
133
135
  ] }),
134
136
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end pt-2", children: [
135
- completedSteps.length > 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onBack, className: "inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted", children: "Back" }),
137
+ completedSteps.length > 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onBack, className: "inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted", children: t("cpq.common.back", "Back") }),
136
138
  /* @__PURE__ */ jsx(
137
139
  "button",
138
140
  {
@@ -140,7 +142,7 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
140
142
  onClick: handleSubmit,
141
143
  disabled: submitting,
142
144
  className: "inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
143
- children: submitting ? "Submitting..." : submitLabel
145
+ children: submitting ? t("cpq.wizard.steps.review.submitting", "Submitting...") : submitLabel
144
146
  }
145
147
  )
146
148
  ] })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/cpq/workflows/steps/ReviewStep.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport type { WizardStepProps } from '../types'\n\ntype ResolvedCharge = {\n chargeCode: string\n chargeName: string\n chargeType: string\n unitPrice: number\n totalPrice: number | null\n currencyCode: string\n adjustments?: Array<{\n ruleCode: string\n ruleName: string\n ruleType: string\n delta: number\n }> | null\n}\n\ntype QuoteLine = {\n lineId: string\n offeringName: string\n quantity: number\n nrcTotal: number\n mrcTotal: number\n charges: ResolvedCharge[]\n isConfigured: boolean\n}\n\ntype QuoteData = {\n id: string\n quoteNumber: string\n cpqStatus: string\n currencyCode: string\n pricingSummary: {\n nrcTotal: number\n mrcTotal: number\n discountTotal: number\n surchargeTotal: number\n currencyCode: string\n }\n lines: QuoteLine[]\n}\n\nfunction fmt(n: number, currency = 'USD') {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n}\n\nexport function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }: WizardStepProps) {\n const [quote, setQuote] = useState<QuoteData | null>(null)\n const [loading, setLoading] = useState(true)\n const [submitting, setSubmitting] = useState(false)\n\n const showChargeBreakdown = config.showChargeBreakdown !== false\n const showAdjustments = config.showAdjustments !== false\n const submitAction = (config.submitAction as string) ?? 'save'\n const submitLabel = (config.submitLabel as string) ?? 'Complete'\n\n const loadQuote = useCallback(async () => {\n if (!quoteId) { setLoading(false); return }\n setLoading(true)\n try {\n const res = await fetch(`/api/cpq/quotes/${quoteId}`)\n if (res.ok) {\n setQuote(await res.json())\n }\n } finally {\n setLoading(false)\n }\n }, [quoteId])\n\n useEffect(() => { loadQuote() }, [loadQuote])\n\n const handleSubmit = useCallback(async () => {\n setSubmitting(true)\n try {\n if (quoteId && submitAction !== 'none') {\n const targetStatus =\n submitAction === 'submit_for_approval' ? 'in_approval'\n : submitAction === 'send_to_customer' ? 'with_customer'\n : undefined\n\n if (targetStatus) {\n await fetch(`/api/cpq/quotes/${quoteId}/status`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ targetStatus }),\n })\n }\n }\n\n onComplete({\n quoteId,\n action: submitAction,\n nrcTotal: quote?.pricingSummary.nrcTotal ?? 0,\n mrcTotal: quote?.pricingSummary.mrcTotal ?? 0,\n })\n } finally {\n setSubmitting(false)\n }\n }, [quoteId, submitAction, quote, onComplete])\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">Loading quote summary...</div>\n }\n\n if (!quote) {\n return (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n No quote data available. Please go back and configure items.\n </div>\n )\n }\n\n const { pricingSummary, lines } = quote\n\n return (\n <div className=\"space-y-5\">\n {/* Quote summary */}\n <div className=\"rounded-lg border bg-muted/30 p-4 space-y-3\">\n <h4 className=\"text-sm font-medium\">Quote Summary</h4>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <span className=\"text-xs text-muted-foreground\">NRC Total</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.nrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n <div>\n <span className=\"text-xs text-muted-foreground\">MRC Total</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.mrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n {pricingSummary.discountTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">Discounts</span>\n <p className=\"text-sm\">-{fmt(pricingSummary.discountTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n {pricingSummary.surchargeTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">Surcharges</span>\n <p className=\"text-sm\">+{fmt(pricingSummary.surchargeTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n </div>\n </div>\n\n {/* Line items */}\n <div className=\"space-y-3\">\n <h4 className=\"text-sm font-medium\">Items ({lines.length})</h4>\n <div className=\"space-y-2\">\n {lines.map((line) => (\n <div key={line.lineId} className=\"rounded-lg border p-3 space-y-2\">\n <div className=\"flex items-center justify-between\">\n <strong className=\"text-sm\">{line.offeringName}</strong>\n <span className=\"text-sm\">\n {fmt(line.nrcTotal, pricingSummary.currencyCode)} NRC / {fmt(line.mrcTotal, pricingSummary.currencyCode)} MRC\n </span>\n </div>\n {line.quantity > 1 && (\n <span className=\"text-xs text-muted-foreground\">Quantity: {line.quantity}</span>\n )}\n\n {showChargeBreakdown && line.charges?.length > 0 && (\n <div className=\"rounded border\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"border-b bg-muted/30\">\n <th className=\"px-3 py-1.5 text-left font-medium\">Charge</th>\n <th className=\"px-3 py-1.5 text-left font-medium\">Type</th>\n <th className=\"px-3 py-1.5 text-right font-medium\">Price</th>\n </tr>\n </thead>\n <tbody>\n {line.charges.map((charge) => (\n <React.Fragment key={charge.chargeCode}>\n <tr className=\"border-b last:border-0\">\n <td className=\"px-3 py-1.5\">{charge.chargeName}</td>\n <td className=\"px-3 py-1.5 uppercase\">{charge.chargeType}</td>\n <td className=\"px-3 py-1.5 text-right\">\n {charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : `${fmt(charge.unitPrice, charge.currencyCode)}/unit`}\n </td>\n </tr>\n {showAdjustments && charge.adjustments?.map((adj) => (\n <tr key={adj.ruleCode} className=\"text-[11px] text-muted-foreground\">\n <td colSpan={2} className=\"px-3 py-0.5 pl-6\">{adj.ruleName}</td>\n <td className=\"px-3 py-0.5 text-right\">\n {adj.delta < 0 ? '' : '+'}{fmt(adj.delta, charge.currencyCode)}\n </td>\n </tr>\n ))}\n </React.Fragment>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n\n <div className=\"flex gap-2 justify-end pt-2\">\n {completedSteps.length > 0 && (\n <button type=\"button\" onClick={onBack} className=\"inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted\">\n Back\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={submitting}\n className=\"inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n >\n {submitting ? 'Submitting...' : submitLabel}\n </button>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { WizardStepProps } from '../types'\n\ntype ResolvedCharge = {\n chargeCode: string\n chargeName: string\n chargeType: string\n unitPrice: number\n totalPrice: number | null\n currencyCode: string\n adjustments?: Array<{\n ruleCode: string\n ruleName: string\n ruleType: string\n delta: number\n }> | null\n}\n\ntype QuoteLine = {\n lineId: string\n offeringName: string\n quantity: number\n nrcTotal: number\n mrcTotal: number\n charges: ResolvedCharge[]\n isConfigured: boolean\n}\n\ntype QuoteData = {\n id: string\n quoteNumber: string\n cpqStatus: string\n currencyCode: string\n pricingSummary: {\n nrcTotal: number\n mrcTotal: number\n discountTotal: number\n surchargeTotal: number\n currencyCode: string\n }\n lines: QuoteLine[]\n}\n\nfunction fmt(n: number, currency = 'USD') {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n}\n\nfunction chargeTypeLabel(t: TranslateFn, chargeType: string): string {\n switch (chargeType) {\n case 'nrc':\n return t('cpq.lines.chargeType.nrc', 'nrc')\n case 'mrc':\n return t('cpq.lines.chargeType.mrc', 'mrc')\n case 'usage':\n return t('cpq.lines.chargeType.usage', 'usage')\n default:\n return chargeType\n }\n}\n\nexport function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }: WizardStepProps) {\n const t = useT()\n const [quote, setQuote] = useState<QuoteData | null>(null)\n const [loading, setLoading] = useState(true)\n const [submitting, setSubmitting] = useState(false)\n\n const showChargeBreakdown = config.showChargeBreakdown !== false\n const showAdjustments = config.showAdjustments !== false\n const submitAction = (config.submitAction as string) ?? 'save'\n const submitLabel = (config.submitLabel as string) ?? t('cpq.wizard.steps.review.complete', 'Complete')\n\n const loadQuote = useCallback(async () => {\n if (!quoteId) { setLoading(false); return }\n setLoading(true)\n try {\n const res = await fetch(`/api/cpq/quotes/${quoteId}`)\n if (res.ok) {\n setQuote(await res.json())\n }\n } finally {\n setLoading(false)\n }\n }, [quoteId])\n\n useEffect(() => { loadQuote() }, [loadQuote])\n\n const handleSubmit = useCallback(async () => {\n setSubmitting(true)\n try {\n if (quoteId && submitAction !== 'none') {\n const targetStatus =\n submitAction === 'submit_for_approval' ? 'in_approval'\n : submitAction === 'send_to_customer' ? 'with_customer'\n : undefined\n\n if (targetStatus) {\n await fetch(`/api/cpq/quotes/${quoteId}/status`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ targetStatus }),\n })\n }\n }\n\n onComplete({\n quoteId,\n action: submitAction,\n nrcTotal: quote?.pricingSummary.nrcTotal ?? 0,\n mrcTotal: quote?.pricingSummary.mrcTotal ?? 0,\n })\n } finally {\n setSubmitting(false)\n }\n }, [quoteId, submitAction, quote, onComplete])\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">{t('cpq.wizard.steps.review.loading', 'Loading quote summary...')}</div>\n }\n\n if (!quote) {\n return (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.wizard.steps.review.noData', 'No quote data available. Please go back and configure items.')}\n </div>\n )\n }\n\n const { pricingSummary, lines } = quote\n\n return (\n <div className=\"space-y-5\">\n {/* Quote summary */}\n <div className=\"rounded-lg border bg-muted/30 p-4 space-y-3\">\n <h4 className=\"text-sm font-medium\">{t('cpq.wizard.steps.review.summaryTitle', 'Quote Summary')}</h4>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.nrcTotal', 'NRC Total')}</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.nrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.mrcTotal', 'MRC Total')}</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.mrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n {pricingSummary.discountTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.discounts', 'Discounts')}</span>\n <p className=\"text-sm\">-{fmt(pricingSummary.discountTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n {pricingSummary.surchargeTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.surcharges', 'Surcharges')}</span>\n <p className=\"text-sm\">+{fmt(pricingSummary.surchargeTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n </div>\n </div>\n\n {/* Line items */}\n <div className=\"space-y-3\">\n <h4 className=\"text-sm font-medium\">{t('cpq.wizard.steps.review.items', 'Items ({count})', { count: lines.length })}</h4>\n <div className=\"space-y-2\">\n {lines.map((line) => (\n <div key={line.lineId} className=\"rounded-lg border p-3 space-y-2\">\n <div className=\"flex items-center justify-between\">\n <strong className=\"text-sm\">{line.offeringName}</strong>\n <span className=\"text-sm\">\n {t('cpq.wizard.linePricing', '{nrc} NRC / {mrc} MRC', { nrc: fmt(line.nrcTotal, pricingSummary.currencyCode), mrc: fmt(line.mrcTotal, pricingSummary.currencyCode) })}\n </span>\n </div>\n {line.quantity > 1 && (\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.quantity', 'Quantity: {quantity}', { quantity: line.quantity })}</span>\n )}\n\n {showChargeBreakdown && line.charges?.length > 0 && (\n <div className=\"rounded border\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"border-b bg-muted/30\">\n <th className=\"px-3 py-1.5 text-left font-medium\">{t('cpq.wizard.steps.review.charges.charge', 'Charge')}</th>\n <th className=\"px-3 py-1.5 text-left font-medium\">{t('cpq.wizard.steps.review.charges.type', 'Type')}</th>\n <th className=\"px-3 py-1.5 text-right font-medium\">{t('cpq.wizard.steps.review.charges.price', 'Price')}</th>\n </tr>\n </thead>\n <tbody>\n {line.charges.map((charge) => (\n <React.Fragment key={charge.chargeCode}>\n <tr className=\"border-b last:border-0\">\n <td className=\"px-3 py-1.5\">{charge.chargeName}</td>\n <td className=\"px-3 py-1.5 uppercase\">{chargeTypeLabel(t, charge.chargeType)}</td>\n <td className=\"px-3 py-1.5 text-right\">\n {charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : t('cpq.lines.perUnit', '{price}/unit', { price: fmt(charge.unitPrice, charge.currencyCode) })}\n </td>\n </tr>\n {showAdjustments && charge.adjustments?.map((adj) => (\n <tr key={adj.ruleCode} className=\"text-[11px] text-muted-foreground\">\n <td colSpan={2} className=\"px-3 py-0.5 pl-6\">{adj.ruleName}</td>\n <td className=\"px-3 py-0.5 text-right\">\n {adj.delta < 0 ? '' : '+'}{fmt(adj.delta, charge.currencyCode)}\n </td>\n </tr>\n ))}\n </React.Fragment>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n\n <div className=\"flex gap-2 justify-end pt-2\">\n {completedSteps.length > 0 && (\n <button type=\"button\" onClick={onBack} className=\"inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted\">\n {t('cpq.common.back', 'Back')}\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={submitting}\n className=\"inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n >\n {submitting ? t('cpq.wizard.steps.review.submitting', 'Submitting...') : submitLabel}\n </button>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": ";AAuHW,cAmBD,YAnBC;AArHX,OAAO,SAAS,UAAU,WAAW,mBAAmB;AACxD,SAAS,YAA8B;AA2CvC,SAAS,IAAI,GAAW,WAAW,OAAO;AACxC,SAAO,IAAI,KAAK,aAAa,SAAS,EAAE,OAAO,YAAY,SAAS,CAAC,EAAE,OAAO,CAAC;AACjF;AAEA,SAAS,gBAAgB,GAAgB,YAA4B;AACnE,UAAQ,YAAY;AAAA,IAClB,KAAK;AACH,aAAO,EAAE,4BAA4B,KAAK;AAAA,IAC5C,KAAK;AACH,aAAO,EAAE,4BAA4B,KAAK;AAAA,IAC5C,KAAK;AACH,aAAO,EAAE,8BAA8B,OAAO;AAAA,IAChD;AACE,aAAO;AAAA,EACX;AACF;AAEO,SAAS,WAAW,EAAE,QAAQ,YAAY,QAAQ,gBAAgB,QAAQ,GAAoB;AACnG,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA2B,IAAI;AACzD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAElD,QAAM,sBAAsB,OAAO,wBAAwB;AAC3D,QAAM,kBAAkB,OAAO,oBAAoB;AACnD,QAAM,eAAgB,OAAO,gBAA2B;AACxD,QAAM,cAAe,OAAO,eAA0B,EAAE,oCAAoC,UAAU;AAEtG,QAAM,YAAY,YAAY,YAAY;AACxC,QAAI,CAAC,SAAS;AAAE,iBAAW,KAAK;AAAG;AAAA,IAAO;AAC1C,eAAW,IAAI;AACf,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,mBAAmB,OAAO,EAAE;AACpD,UAAI,IAAI,IAAI;AACV,iBAAS,MAAM,IAAI,KAAK,CAAC;AAAA,MAC3B;AAAA,IACF,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,YAAU,MAAM;AAAE,cAAU;AAAA,EAAE,GAAG,CAAC,SAAS,CAAC;AAE5C,QAAM,eAAe,YAAY,YAAY;AAC3C,kBAAc,IAAI;AAClB,QAAI;AACF,UAAI,WAAW,iBAAiB,QAAQ;AACtC,cAAM,eACJ,iBAAiB,wBAAwB,gBACvC,iBAAiB,qBAAqB,kBACtC;AAEJ,YAAI,cAAc;AAChB,gBAAM,MAAM,mBAAmB,OAAO,WAAW;AAAA,YAC/C,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,KAAK,UAAU,EAAE,aAAa,CAAC;AAAA,UACvC,CAAC;AAAA,QACH;AAAA,MACF;AAEA,iBAAW;AAAA,QACT;AAAA,QACA,QAAQ;AAAA,QACR,UAAU,OAAO,eAAe,YAAY;AAAA,QAC5C,UAAU,OAAO,eAAe,YAAY;AAAA,MAC9C,CAAC;AAAA,IACH,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,SAAS,cAAc,OAAO,UAAU,CAAC;AAE7C,MAAI,SAAS;AACX,WAAO,oBAAC,SAAI,WAAU,iCAAiC,YAAE,mCAAmC,0BAA0B,GAAE;AAAA,EAC1H;AAEA,MAAI,CAAC,OAAO;AACV,WACE,oBAAC,SAAI,WAAU,2EACZ,YAAE,kCAAkC,8DAA8D,GACrG;AAAA,EAEJ;AAEA,QAAM,EAAE,gBAAgB,MAAM,IAAI;AAElC,SACE,qBAAC,SAAI,WAAU,aAEb;AAAA,yBAAC,SAAI,WAAU,+CACb;AAAA,0BAAC,QAAG,WAAU,uBAAuB,YAAE,wCAAwC,eAAe,GAAE;AAAA,MAChG,qBAAC,SAAI,WAAU,0BACb;AAAA,6BAAC,SACC;AAAA,8BAAC,UAAK,WAAU,iCAAiC,YAAE,oCAAoC,WAAW,GAAE;AAAA,UACpG,oBAAC,OAAE,WAAU,yBAAyB,cAAI,eAAe,UAAU,eAAe,YAAY,GAAE;AAAA,WAClG;AAAA,QACA,qBAAC,SACC;AAAA,8BAAC,UAAK,WAAU,iCAAiC,YAAE,oCAAoC,WAAW,GAAE;AAAA,UACpG,oBAAC,OAAE,WAAU,yBAAyB,cAAI,eAAe,UAAU,eAAe,YAAY,GAAE;AAAA,WAClG;AAAA,QACC,eAAe,gBAAgB,KAC9B,qBAAC,SACC;AAAA,8BAAC,UAAK,WAAU,iCAAiC,YAAE,qCAAqC,WAAW,GAAE;AAAA,UACrG,qBAAC,OAAE,WAAU,WAAU;AAAA;AAAA,YAAE,IAAI,eAAe,eAAe,eAAe,YAAY;AAAA,aAAE;AAAA,WAC1F;AAAA,QAED,eAAe,iBAAiB,KAC/B,qBAAC,SACC;AAAA,8BAAC,UAAK,WAAU,iCAAiC,YAAE,sCAAsC,YAAY,GAAE;AAAA,UACvG,qBAAC,OAAE,WAAU,WAAU;AAAA;AAAA,YAAE,IAAI,eAAe,gBAAgB,eAAe,YAAY;AAAA,aAAE;AAAA,WAC3F;AAAA,SAEJ;AAAA,OACF;AAAA,IAGA,qBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,QAAG,WAAU,uBAAuB,YAAE,iCAAiC,mBAAmB,EAAE,OAAO,MAAM,OAAO,CAAC,GAAE;AAAA,MACpH,oBAAC,SAAI,WAAU,aACZ,gBAAM,IAAI,CAAC,SACV,qBAAC,SAAsB,WAAU,mCAC/B;AAAA,6BAAC,SAAI,WAAU,qCACb;AAAA,8BAAC,YAAO,WAAU,WAAW,eAAK,cAAa;AAAA,UAC/C,oBAAC,UAAK,WAAU,WACb,YAAE,0BAA0B,yBAAyB,EAAE,KAAK,IAAI,KAAK,UAAU,eAAe,YAAY,GAAG,KAAK,IAAI,KAAK,UAAU,eAAe,YAAY,EAAE,CAAC,GACtK;AAAA,WACF;AAAA,QACC,KAAK,WAAW,KACf,oBAAC,UAAK,WAAU,iCAAiC,YAAE,oCAAoC,wBAAwB,EAAE,UAAU,KAAK,SAAS,CAAC,GAAE;AAAA,QAG7I,uBAAuB,KAAK,SAAS,SAAS,KAC7C,oBAAC,SAAI,WAAU,kBACb,+BAAC,WAAM,WAAU,kBACf;AAAA,8BAAC,WACC,+BAAC,QAAG,WAAU,wBACZ;AAAA,gCAAC,QAAG,WAAU,qCAAqC,YAAE,0CAA0C,QAAQ,GAAE;AAAA,YACzG,oBAAC,QAAG,WAAU,qCAAqC,YAAE,wCAAwC,MAAM,GAAE;AAAA,YACrG,oBAAC,QAAG,WAAU,sCAAsC,YAAE,yCAAyC,OAAO,GAAE;AAAA,aAC1G,GACF;AAAA,UACA,oBAAC,WACE,eAAK,QAAQ,IAAI,CAAC,WACjB,qBAAC,MAAM,UAAN,EACC;AAAA,iCAAC,QAAG,WAAU,0BACZ;AAAA,kCAAC,QAAG,WAAU,eAAe,iBAAO,YAAW;AAAA,cAC/C,oBAAC,QAAG,WAAU,yBAAyB,0BAAgB,GAAG,OAAO,UAAU,GAAE;AAAA,cAC7E,oBAAC,QAAG,WAAU,0BACX,iBAAO,cAAc,OAAO,IAAI,OAAO,YAAY,OAAO,YAAY,IAAI,EAAE,qBAAqB,gBAAgB,EAAE,OAAO,IAAI,OAAO,WAAW,OAAO,YAAY,EAAE,CAAC,GACzK;AAAA,eACF;AAAA,YACC,mBAAmB,OAAO,aAAa,IAAI,CAAC,QAC3C,qBAAC,QAAsB,WAAU,qCAC/B;AAAA,kCAAC,QAAG,SAAS,GAAG,WAAU,oBAAoB,cAAI,UAAS;AAAA,cAC3D,qBAAC,QAAG,WAAU,0BACX;AAAA,oBAAI,QAAQ,IAAI,KAAK;AAAA,gBAAK,IAAI,IAAI,OAAO,OAAO,YAAY;AAAA,iBAC/D;AAAA,iBAJO,IAAI,QAKb,CACD;AAAA,eAfkB,OAAO,UAgB5B,CACD,GACH;AAAA,WACF,GACF;AAAA,WA3CM,KAAK,MA6Cf,CACD,GACH;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,+BACZ;AAAA,qBAAe,SAAS,KACvB,oBAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,WAAU,0GAC9C,YAAE,mBAAmB,MAAM,GAC9B;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,UAAU;AAAA,UACV,WAAU;AAAA,UAET,uBAAa,EAAE,sCAAsC,eAAe,IAAI;AAAA;AAAA,MAC3E;AAAA,OACF;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -8,36 +8,48 @@ import { InventorySelectStep } from "./InventorySelectStep.js";
8
8
  function registerBuiltInStepTypes() {
9
9
  registerStepType({
10
10
  type: "customer_select",
11
+ labelKey: "cpq.wizard.steps.customerSelect.label",
12
+ descriptionKey: "cpq.wizard.steps.customerSelect.description",
11
13
  label: "Customer Selection",
12
14
  description: "Search and select a customer for the quote.",
13
15
  component: CustomerSelectStep
14
16
  });
15
17
  registerStepType({
16
18
  type: "offering_select",
19
+ labelKey: "cpq.wizard.steps.offeringSelect.label",
20
+ descriptionKey: "cpq.wizard.steps.offeringSelect.description",
17
21
  label: "Offering Selection",
18
22
  description: "Browse and select a product offering from the catalog.",
19
23
  component: OfferingSelectStep
20
24
  });
21
25
  registerStepType({
22
26
  type: "product_configure",
27
+ labelKey: "cpq.wizard.steps.productConfigure.label",
28
+ descriptionKey: "cpq.wizard.steps.productConfigure.description",
23
29
  label: "Product Configuration",
24
30
  description: "Configure run-time attributes for the selected offering.",
25
31
  component: ProductConfigureStep
26
32
  });
27
33
  registerStepType({
28
34
  type: "item_list",
35
+ labelKey: "cpq.wizard.steps.itemList.label",
36
+ descriptionKey: "cpq.wizard.steps.itemList.description",
29
37
  label: "Item List",
30
38
  description: "View and manage child items on the quote.",
31
39
  component: ItemListStep
32
40
  });
33
41
  registerStepType({
34
42
  type: "review",
43
+ labelKey: "cpq.wizard.steps.review.label",
44
+ descriptionKey: "cpq.wizard.steps.review.description",
35
45
  label: "Review & Submit",
36
46
  description: "Review the quote summary and submit.",
37
47
  component: ReviewStep
38
48
  });
39
49
  registerStepType({
40
50
  type: "inventory_select",
51
+ labelKey: "cpq.wizard.steps.inventorySelect.label",
52
+ descriptionKey: "cpq.wizard.steps.inventorySelect.description",
41
53
  label: "Inventory Selection",
42
54
  description: "Select existing customer inventory items (stub \u2014 pending inventory domain).",
43
55
  component: InventorySelectStep
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/cpq/workflows/steps/index.ts"],
4
- "sourcesContent": ["import { registerStepType } from '../registry'\nimport { CustomerSelectStep } from './CustomerSelectStep'\nimport { OfferingSelectStep } from './OfferingSelectStep'\nimport { ProductConfigureStep } from './ProductConfigureStep'\nimport { ItemListStep } from './ItemListStep'\nimport { ReviewStep } from './ReviewStep'\nimport { InventorySelectStep } from './InventorySelectStep'\n\nexport function registerBuiltInStepTypes() {\n registerStepType({\n type: 'customer_select',\n label: 'Customer Selection',\n description: 'Search and select a customer for the quote.',\n component: CustomerSelectStep,\n })\n\n registerStepType({\n type: 'offering_select',\n label: 'Offering Selection',\n description: 'Browse and select a product offering from the catalog.',\n component: OfferingSelectStep,\n })\n\n registerStepType({\n type: 'product_configure',\n label: 'Product Configuration',\n description: 'Configure run-time attributes for the selected offering.',\n component: ProductConfigureStep,\n })\n\n registerStepType({\n type: 'item_list',\n label: 'Item List',\n description: 'View and manage child items on the quote.',\n component: ItemListStep,\n })\n\n registerStepType({\n type: 'review',\n label: 'Review & Submit',\n description: 'Review the quote summary and submit.',\n component: ReviewStep,\n })\n\n registerStepType({\n type: 'inventory_select',\n label: 'Inventory Selection',\n description: 'Select existing customer inventory items (stub \u2014 pending inventory domain).',\n component: InventorySelectStep,\n })\n}\n"],
5
- "mappings": "AAAA,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAE7B,SAAS,2BAA2B;AACzC,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AACH;",
4
+ "sourcesContent": ["import { registerStepType } from '../registry'\nimport { CustomerSelectStep } from './CustomerSelectStep'\nimport { OfferingSelectStep } from './OfferingSelectStep'\nimport { ProductConfigureStep } from './ProductConfigureStep'\nimport { ItemListStep } from './ItemListStep'\nimport { ReviewStep } from './ReviewStep'\nimport { InventorySelectStep } from './InventorySelectStep'\n\nexport function registerBuiltInStepTypes() {\n registerStepType({\n type: 'customer_select',\n labelKey: 'cpq.wizard.steps.customerSelect.label',\n descriptionKey: 'cpq.wizard.steps.customerSelect.description',\n label: 'Customer Selection',\n description: 'Search and select a customer for the quote.',\n component: CustomerSelectStep,\n })\n\n registerStepType({\n type: 'offering_select',\n labelKey: 'cpq.wizard.steps.offeringSelect.label',\n descriptionKey: 'cpq.wizard.steps.offeringSelect.description',\n label: 'Offering Selection',\n description: 'Browse and select a product offering from the catalog.',\n component: OfferingSelectStep,\n })\n\n registerStepType({\n type: 'product_configure',\n labelKey: 'cpq.wizard.steps.productConfigure.label',\n descriptionKey: 'cpq.wizard.steps.productConfigure.description',\n label: 'Product Configuration',\n description: 'Configure run-time attributes for the selected offering.',\n component: ProductConfigureStep,\n })\n\n registerStepType({\n type: 'item_list',\n labelKey: 'cpq.wizard.steps.itemList.label',\n descriptionKey: 'cpq.wizard.steps.itemList.description',\n label: 'Item List',\n description: 'View and manage child items on the quote.',\n component: ItemListStep,\n })\n\n registerStepType({\n type: 'review',\n labelKey: 'cpq.wizard.steps.review.label',\n descriptionKey: 'cpq.wizard.steps.review.description',\n label: 'Review & Submit',\n description: 'Review the quote summary and submit.',\n component: ReviewStep,\n })\n\n registerStepType({\n type: 'inventory_select',\n labelKey: 'cpq.wizard.steps.inventorySelect.label',\n descriptionKey: 'cpq.wizard.steps.inventorySelect.description',\n label: 'Inventory Selection',\n description: 'Select existing customer inventory items (stub \u2014 pending inventory domain).',\n component: InventorySelectStep,\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAE7B,SAAS,2BAA2B;AACzC,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AACH;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dainamite/cpq",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Dainamite CPQ — Configure, Price, Quote engine for Open Mercato (configurable products, multi-charge pricing tables, guided quoting, customer inventory, orders).",
5
5
  "type": "module",
6
6
  "main": "./dist/modules/cpq/index.js",
@@ -0,0 +1,62 @@
1
+ import { readdirSync, readFileSync } from 'fs'
2
+ import { join } from 'path'
3
+ import { MODULE_ROOT, collectTranslationKeys, listSourceFiles } from './sourceScan'
4
+
5
+ /**
6
+ * CPQ dictionary guardrails (spec 2026-11-15-cpq-multi-language):
7
+ * - every `i18n/<locale>.json` mirrors `en.json` key for key, keeps every
8
+ * `{placeholder}` and has no empty value;
9
+ * - every key the source uses exists in `en.json` (a missing key renders
10
+ * its English fallback in every language), and every dynamic key prefix
11
+ * has at least one entry;
12
+ * - `en.json` carries no orphaned keys.
13
+ */
14
+
15
+ const I18N_DIR = join(MODULE_ROOT, 'i18n')
16
+ const load = (locale: string) => JSON.parse(readFileSync(join(I18N_DIR, `${locale}.json`), 'utf8')) as Record<string, string>
17
+ const placeholders = (value: string) => [...value.matchAll(/\{(\w+)\}/g)].map((m) => m[1]).sort().join(',')
18
+
19
+ const locales = readdirSync(I18N_DIR).filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, '')).sort()
20
+ const en = load('en')
21
+ const enKeys = Object.keys(en)
22
+ const translated = locales.filter((l) => l !== 'en')
23
+ // `describe.each` rejects an empty table; en vs en is a trivially-true row.
24
+ const parityRows = translated.length ? translated : ['en']
25
+ const { keys: usedKeys, prefixes } = collectTranslationKeys(listSourceFiles(MODULE_ROOT))
26
+
27
+ describe('cpq i18n dictionaries', () => {
28
+ it('ships en plus the supported translations', () => {
29
+ expect(locales).toEqual(expect.arrayContaining(['en', 'es', 'de', 'pl']))
30
+ })
31
+
32
+ it('en.json is sorted by key and has no empty values', () => {
33
+ expect(enKeys).toEqual([...enKeys].sort())
34
+ expect(enKeys.filter((k) => !en[k]?.trim())).toEqual([])
35
+ })
36
+
37
+ it('every key used in source exists in en.json', () => {
38
+ expect([...usedKeys].filter((k) => !(k in en)).sort()).toEqual([])
39
+ })
40
+
41
+ it('every dynamic key prefix has entries in en.json', () => {
42
+ expect([...prefixes].filter((p) => !enKeys.some((k) => k.startsWith(p))).sort()).toEqual([])
43
+ })
44
+
45
+ it('en.json has no orphaned keys', () => {
46
+ const orphans = enKeys.filter((k) => !usedKeys.has(k) && ![...prefixes].some((p) => k.startsWith(p)))
47
+ expect(orphans).toEqual([])
48
+ })
49
+
50
+ describe.each(parityRows)('%s.json', (locale) => {
51
+ const dict = load(locale)
52
+ it('has exactly the keys of en.json, in the same order', () => {
53
+ expect(Object.keys(dict)).toEqual(enKeys)
54
+ })
55
+ it('keeps every {placeholder} of the English value', () => {
56
+ expect(enKeys.filter((k) => typeof dict[k] === 'string' && placeholders(dict[k]) !== placeholders(en[k]))).toEqual([])
57
+ })
58
+ it('has no empty values', () => {
59
+ expect(enKeys.filter((k) => typeof dict[k] !== 'string' || !dict[k].trim())).toEqual([])
60
+ })
61
+ })
62
+ })
@@ -0,0 +1,35 @@
1
+ import { join } from 'path'
2
+ import { MODULE_ROOT, UI_DIRS, collectHardcodedStrings, listSourceFiles } from './sourceScan'
3
+
4
+ /**
5
+ * No user-facing English literal may bypass `t()` in CPQ UI code
6
+ * (spec 2026-11-15-cpq-multi-language). Data (product / charge / customer
7
+ * names) is never a literal, so it is not affected.
8
+ *
9
+ * ALLOWLIST is for literals that are intentionally not translated; every
10
+ * entry carries a reason. Keyed by `<file>:<text>` so a moved line does not
11
+ * break it.
12
+ */
13
+ const ALLOWLIST: Record<string, string> = {
14
+ // Injection-widget metadata has no `titleKey` in the framework's widget
15
+ // contract; the host renders `metadata.title`. Tracked as a framework gap.
16
+ 'widgets/injection/order-cpq-tab/widget.ts:CPQ Order': 'framework: widget metadata has no titleKey',
17
+ 'widgets/injection/customer-inventory/widget.ts:Customer Inventory': 'framework: widget metadata has no titleKey',
18
+ 'widgets/injection/quote-configurator/widget.ts:CPQ Configuration': 'framework: widget metadata has no titleKey',
19
+ 'widgets/injection/order-cpq-tab/widget.ts:View the CPQ order associated with this sales order':
20
+ 'framework: widget metadata has no descriptionKey',
21
+ 'widgets/injection/customer-inventory/widget.ts:Products and services purchased by this customer':
22
+ 'framework: widget metadata has no descriptionKey',
23
+ 'widgets/injection/quote-configurator/widget.ts:Configure, price, and manage quote offerings':
24
+ 'framework: widget metadata has no descriptionKey',
25
+ }
26
+
27
+ describe('cpq UI has no hard-coded user-facing strings', () => {
28
+ it('routes every UI literal through t()', () => {
29
+ const files = UI_DIRS.flatMap((dir) => listSourceFiles(join(MODULE_ROOT, dir)))
30
+ const offenders = collectHardcodedStrings(files)
31
+ .filter((hit) => !(`${hit.file}:${hit.text}` in ALLOWLIST))
32
+ .map((hit) => `${hit.file}:${hit.line} ${hit.text}`)
33
+ expect(offenders).toEqual([])
34
+ })
35
+ })
@@ -0,0 +1,109 @@
1
+ import { readdirSync, readFileSync, statSync } from 'fs'
2
+ import { join, relative } from 'path'
3
+ import ts from 'typescript'
4
+
5
+ /**
6
+ * Static source scans backing the CPQ i18n guardrails
7
+ * (spec .ai/specs/2026-11-15-cpq-multi-language.md):
8
+ *
9
+ * - `collectTranslationKeys` — every literal `t('cpq.…')` key and every
10
+ * template-literal key prefix (`t(\`cpq.x.${v}\`)`) used in source;
11
+ * - `collectHardcodedStrings` — user-facing English literals that bypass
12
+ * `t()`: JSX text, string-valued UI attributes, and `label` / `title` /
13
+ * `header` / `placeholder` / `description` properties in UI files.
14
+ */
15
+
16
+ export const MODULE_ROOT = join(__dirname, '..', '..')
17
+ const SKIP_DIRS = new Set(['__tests__', '__integration__', 'migrations', 'i18n', 'node_modules'])
18
+ /** Directories whose files render UI. */
19
+ export const UI_DIRS = ['backend', 'components', 'widgets', 'workflows']
20
+
21
+ export function listSourceFiles(dir: string, exts = /\.(tsx?|mts)$/): string[] {
22
+ const out: string[] = []
23
+ for (const entry of readdirSync(dir)) {
24
+ const full = join(dir, entry)
25
+ if (statSync(full).isDirectory()) {
26
+ if (!SKIP_DIRS.has(entry)) out.push(...listSourceFiles(full, exts))
27
+ } else if (exts.test(entry) && !/\.test\.tsx?$/.test(entry)) {
28
+ out.push(full)
29
+ }
30
+ }
31
+ return out
32
+ }
33
+
34
+ function parse(file: string): ts.SourceFile {
35
+ return ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true, file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
36
+ }
37
+
38
+ function literalText(node: ts.Node | undefined): string | null {
39
+ if (!node) return null
40
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text
41
+ return null
42
+ }
43
+
44
+ const TRANSLATE_CALLEES = new Set(['t', 'translate', 'tr'])
45
+
46
+ export function collectTranslationKeys(files: string[]): { keys: Set<string>; prefixes: Set<string> } {
47
+ const keys = new Set<string>()
48
+ const prefixes = new Set<string>()
49
+ for (const file of files) {
50
+ const visit = (node: ts.Node) => {
51
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && TRANSLATE_CALLEES.has(node.expression.text)) {
52
+ const first = node.arguments[0]
53
+ const key = literalText(first)
54
+ if (key?.startsWith('cpq.')) keys.add(key)
55
+ if (first && ts.isTemplateExpression(first) && first.head.text.startsWith('cpq.')) prefixes.add(first.head.text)
56
+ }
57
+ // `*Key` props (page meta, breadcrumbs) and injection-table `groupLabel`s the host translates.
58
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && /(Key|^groupLabel)$/.test(node.name.text)) {
59
+ const key = literalText(node.initializer)
60
+ if (key?.startsWith('cpq.')) keys.add(key)
61
+ }
62
+ ts.forEachChild(node, visit)
63
+ }
64
+ visit(parse(file))
65
+ }
66
+ return { keys, prefixes }
67
+ }
68
+
69
+ const UI_ATTRIBUTES = new Set(['label', 'placeholder', 'title', 'aria-label', 'description', 'emptyLabel', 'emptyMessage', 'alt'])
70
+ const UI_PROPERTIES = new Set(['label', 'title', 'header', 'placeholder', 'description', 'emptyLabel', 'emptyMessage'])
71
+ /** Natural-language text: two letters in a row, not a code-ish token (snake_case, dotted, kebab, path, CSS). */
72
+ function isProse(text: string): boolean {
73
+ const value = text.trim()
74
+ if (!/[A-Za-z]{2}/.test(value)) return false
75
+ if (/^[a-z0-9]+([._/-][a-z0-9]+)+$/i.test(value) && !/\s/.test(value)) return false
76
+ return true
77
+ }
78
+
79
+ export type HardcodedHit = { file: string; line: number; text: string }
80
+
81
+ export function collectHardcodedStrings(files: string[]): HardcodedHit[] {
82
+ const hits: HardcodedHit[] = []
83
+ for (const file of files) {
84
+ const sf = parse(file)
85
+ const rel = relative(MODULE_ROOT, file)
86
+ const hit = (node: ts.Node, text: string) =>
87
+ hits.push({ file: rel, line: sf.getLineAndCharacterOfPosition(node.getStart()).line + 1, text: text.trim().slice(0, 80) })
88
+ const visit = (node: ts.Node) => {
89
+ if (ts.isJsxText(node) && isProse(node.text)) hit(node, node.text)
90
+ if (ts.isJsxAttribute(node) && UI_ATTRIBUTES.has(node.name.getText())) {
91
+ const init = node.initializer
92
+ const text = init && ts.isStringLiteral(init) ? init.text : init && ts.isJsxExpression(init) ? literalText(init.expression) : null
93
+ if (text && isProse(text)) hit(node, text)
94
+ }
95
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && UI_PROPERTIES.has(node.name.text)) {
96
+ const text = literalText(node.initializer)
97
+ // `{ label: 'X', labelKey: 'cpq.x' }` (page meta, breadcrumbs) is translated by the host.
98
+ const parent = node.parent
99
+ const hasKeySibling =
100
+ ts.isObjectLiteralExpression(parent) &&
101
+ parent.properties.some((p) => ts.isPropertyAssignment(p) && p.name.getText() === `${node.name.getText()}Key`)
102
+ if (text && isProse(text) && !hasKeySibling) hit(node, text)
103
+ }
104
+ ts.forEachChild(node, visit)
105
+ }
106
+ visit(sf)
107
+ }
108
+ return hits
109
+ }
@@ -8,7 +8,7 @@ import { RowActions } from '@open-mercato/ui/backend/RowActions'
8
8
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
9
9
  import { useCpqListData } from '../../../components/CpqListView'
10
10
  import { useCpqRowActions } from '../../../components/useCpqRowActions'
11
- import { StatusBadge, formatCurrency } from './shared'
11
+ import { StatusBadge, buildInventoryStatusOptions, formatCurrency } from './shared'
12
12
 
13
13
  type Asset = {
14
14
  id: string
@@ -23,13 +23,7 @@ type Asset = {
23
23
 
24
24
  const PAGE_SIZE = 50
25
25
 
26
- const ASSET_STATUS_OPTIONS = [
27
- { value: 'pending', label: 'Pending' },
28
- { value: 'delivered', label: 'Delivered' },
29
- { value: 'active', label: 'Active' },
30
- { value: 'returned', label: 'Returned' },
31
- { value: 'cancelled', label: 'Cancelled' },
32
- ] as const
26
+ const ASSET_STATUSES = ['pending', 'delivered', 'active', 'returned', 'cancelled'] as const
33
27
 
34
28
  function buildFilterParams(values: FilterValues, params: URLSearchParams) {
35
29
  if (typeof values.status === 'string' && values.status) {
@@ -67,7 +61,7 @@ export function AssetsTable() {
67
61
  id: 'status',
68
62
  label: t('cpq.inventory.filters.status', 'Status'),
69
63
  type: 'select',
70
- options: [...ASSET_STATUS_OPTIONS],
64
+ options: buildInventoryStatusOptions(t, ASSET_STATUSES),
71
65
  },
72
66
  {
73
67
  id: 'assetType',