@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.
- package/CHANGELOG.md +16 -0
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +3 -9
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
- package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +154 -178
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
- package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +69 -66
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
- package/dist/modules/cpq/backend/cpq/orders/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +41 -36
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
- package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
- package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +6 -3
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +129 -94
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
- package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +19 -15
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
- package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
- package/dist/modules/cpq/components/ArcLineDiff.js +2 -2
- package/dist/modules/cpq/components/ArcLineDiff.js.map +2 -2
- package/dist/modules/cpq/components/ChargeBreakdownTable.js +4 -10
- package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +2 -2
- package/dist/modules/cpq/components/PriceWaterfall.js +3 -1
- package/dist/modules/cpq/components/PriceWaterfall.js.map +2 -2
- package/dist/modules/cpq/components/PricingSummaryCard.js +1 -6
- package/dist/modules/cpq/components/PricingSummaryCard.js.map +2 -2
- package/dist/modules/cpq/components/StatusPath.js +5 -3
- package/dist/modules/cpq/components/StatusPath.js.map +2 -2
- package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
- package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
- package/dist/modules/cpq/i18n/de.json +1220 -0
- package/dist/modules/cpq/i18n/en.json +1220 -0
- package/dist/modules/cpq/i18n/es.json +1220 -0
- package/dist/modules/cpq/i18n/pl.json +1220 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection-table.js +2 -2
- package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
- package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
- package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
- package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
- package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
- package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
- package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/index.js +12 -0
- package/dist/modules/cpq/workflows/steps/index.js.map +2 -2
- package/package.json +1 -1
- package/src/modules/cpq/__tests__/i18n/dictionaries.test.ts +62 -0
- package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
- package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -0
- package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +3 -9
- package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
- package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +2 -2
- package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +134 -112
- package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
- package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
- package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +66 -43
- package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
- package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
- package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
- package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
- package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
- package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
- package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
- package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +3 -3
- package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +134 -89
- package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
- package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
- package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
- package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
- package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
- package/src/modules/cpq/components/ArcLineDiff.tsx +2 -2
- package/src/modules/cpq/components/ChargeBreakdownTable.tsx +4 -2
- package/src/modules/cpq/components/PriceWaterfall.tsx +4 -2
- package/src/modules/cpq/components/PricingSummaryCard.tsx +1 -1
- package/src/modules/cpq/components/StatusPath.tsx +7 -4
- package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
- package/src/modules/cpq/i18n/de.json +1220 -0
- package/src/modules/cpq/i18n/en.json +1220 -0
- package/src/modules/cpq/i18n/es.json +1220 -0
- package/src/modules/cpq/i18n/pl.json +1220 -0
- package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
- package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
- package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
- package/src/modules/cpq/widgets/injection-table.ts +8 -2
- package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
- package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
- package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
- package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
- package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
- package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
- package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
- package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
- package/src/modules/cpq/workflows/steps/index.ts +12 -0
- 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__ */
|
|
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__ */
|
|
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) :
|
|
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
|
|
5
|
-
"mappings": ";
|
|
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": "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",
|
|
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.
|
|
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
|
|
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:
|
|
64
|
+
options: buildInventoryStatusOptions(t, ASSET_STATUSES),
|
|
71
65
|
},
|
|
72
66
|
{
|
|
73
67
|
id: 'assetType',
|