@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,12 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
5
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
5
6
|
import { Tag } from "@open-mercato/ui/primitives/tag";
|
|
6
7
|
function fmt(n, currency = "USD") {
|
|
7
8
|
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n);
|
|
8
9
|
}
|
|
9
10
|
function ItemListStep({ config, onComplete, onBack, stepData, completedSteps, quoteId }) {
|
|
11
|
+
const t = useT();
|
|
10
12
|
const [lines, setLines] = useState([]);
|
|
11
13
|
const [loading, setLoading] = useState(true);
|
|
12
14
|
const parentLineSource = config.parentLineSource;
|
|
@@ -45,15 +47,15 @@ function ItemListStep({ config, onComplete, onBack, stepData, completedSteps, qu
|
|
|
45
47
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
46
48
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
47
49
|
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
48
|
-
lines.length,
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
maxItems < Infinity &&
|
|
50
|
+
lines.length !== 1 ? t("cpq.wizard.steps.itemList.count.other", "{count} items", { count: lines.length }) : t("cpq.wizard.steps.itemList.count.one", "{count} item", { count: lines.length }),
|
|
51
|
+
minItems > 0 && " ",
|
|
52
|
+
minItems > 0 && t("cpq.wizard.steps.itemList.min", "(min: {min})", { min: minItems }),
|
|
53
|
+
maxItems < Infinity && " ",
|
|
54
|
+
maxItems < Infinity && t("cpq.wizard.steps.itemList.max", "(max: {max})", { max: maxItems })
|
|
53
55
|
] }),
|
|
54
|
-
canAddMore && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Use the offering browser to add items to the quote, then return here." })
|
|
56
|
+
canAddMore && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.itemList.hint", "Use the offering browser to add items to the quote, then return here.") })
|
|
55
57
|
] }),
|
|
56
|
-
loading ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading items..." }) : /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
58
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("cpq.wizard.steps.itemList.loading", "Loading items...") }) : /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
57
59
|
lines.map((line) => /* @__PURE__ */ jsxs(
|
|
58
60
|
"div",
|
|
59
61
|
{
|
|
@@ -65,15 +67,10 @@ function ItemListStep({ config, onComplete, onBack, stepData, completedSteps, qu
|
|
|
65
67
|
"x",
|
|
66
68
|
line.quantity
|
|
67
69
|
] }),
|
|
68
|
-
!line.isConfigured && /* @__PURE__ */ jsx(Tag, { variant: "warning", className: "ml-2", children: "Needs configuration" })
|
|
70
|
+
!line.isConfigured && /* @__PURE__ */ jsx(Tag, { variant: "warning", className: "ml-2", children: t("cpq.wizard.steps.itemList.needsConfiguration", "Needs configuration") })
|
|
69
71
|
] }),
|
|
70
72
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
71
|
-
showPricing && /* @__PURE__ */
|
|
72
|
-
fmt(line.nrcTotal),
|
|
73
|
-
" NRC / ",
|
|
74
|
-
fmt(line.mrcTotal),
|
|
75
|
-
" MRC"
|
|
76
|
-
] }),
|
|
73
|
+
showPricing && /* @__PURE__ */ jsx("span", { className: "text-xs", children: t("cpq.wizard.linePricing", "{nrc} NRC / {mrc} MRC", { nrc: fmt(line.nrcTotal), mrc: fmt(line.mrcTotal) }) }),
|
|
77
74
|
/* @__PURE__ */ jsx(
|
|
78
75
|
Button,
|
|
79
76
|
{
|
|
@@ -81,7 +78,7 @@ function ItemListStep({ config, onComplete, onBack, stepData, completedSteps, qu
|
|
|
81
78
|
variant: "destructive-outline",
|
|
82
79
|
size: "sm",
|
|
83
80
|
onClick: () => handleRemove(line.lineId),
|
|
84
|
-
children: "Remove"
|
|
81
|
+
children: t("cpq.wizard.remove", "Remove")
|
|
85
82
|
}
|
|
86
83
|
)
|
|
87
84
|
] })
|
|
@@ -89,11 +86,11 @@ function ItemListStep({ config, onComplete, onBack, stepData, completedSteps, qu
|
|
|
89
86
|
},
|
|
90
87
|
line.lineId
|
|
91
88
|
)),
|
|
92
|
-
lines.length === 0 && /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: "No items added yet." })
|
|
89
|
+
lines.length === 0 && /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: t("cpq.wizard.steps.itemList.empty", "No items added yet.") })
|
|
93
90
|
] }),
|
|
94
91
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end pt-2", children: [
|
|
95
|
-
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" }),
|
|
96
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: loadLines, className: "inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted", children: "Refresh" }),
|
|
92
|
+
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") }),
|
|
93
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: loadLines, className: "inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted", children: t("cpq.wizard.steps.itemList.refresh", "Refresh") }),
|
|
97
94
|
/* @__PURE__ */ jsx(
|
|
98
95
|
"button",
|
|
99
96
|
{
|
|
@@ -103,7 +100,7 @@ function ItemListStep({ config, onComplete, onBack, stepData, completedSteps, qu
|
|
|
103
100
|
},
|
|
104
101
|
disabled: !canProceed,
|
|
105
102
|
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 disabled:cursor-not-allowed",
|
|
106
|
-
children: "Next"
|
|
103
|
+
children: t("cpq.wizard.next", "Next")
|
|
107
104
|
}
|
|
108
105
|
)
|
|
109
106
|
] })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/cpq/workflows/steps/ItemListStep.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport type { WizardStepProps } from '../types'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\n\ntype QuoteLine = {\n lineId: string\n offeringId: string | null\n offeringName: string\n quantity: number\n nrcTotal: number\n mrcTotal: number\n isConfigured: boolean\n parentLineId: string | null\n}\n\nfunction fmt(n: number, currency = 'USD') {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n}\n\nexport function ItemListStep({ config, onComplete, onBack, stepData, completedSteps, quoteId }: WizardStepProps) {\n const [lines, setLines] = useState<QuoteLine[]>([])\n const [loading, setLoading] = useState(true)\n\n const parentLineSource = config.parentLineSource as string | undefined\n const minItems = (config.minItems as number) ?? 0\n const maxItems = (config.maxItems as number) ?? Infinity\n const showPricing = config.showPricing !== false\n\n const parentLineId = parentLineSource\n ? (stepData[parentLineSource]?.lineId as string | undefined)\n : undefined\n\n const loadLines = 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 const data = await res.json()\n const allLines = (data.lines ?? []) as QuoteLine[]\n // Filter to children of the parent line (if parentLineId set), otherwise show all\n const filtered = parentLineId\n ? allLines.filter((l) => l.parentLineId === parentLineId)\n : allLines\n setLines(filtered)\n }\n } finally {\n setLoading(false)\n }\n }, [quoteId, parentLineId])\n\n useEffect(() => { loadLines() }, [loadLines])\n\n const handleRemove = useCallback(async (lineId: string) => {\n if (!quoteId) return\n await fetch(`/api/cpq/quotes/${quoteId}/items/${lineId}`, { method: 'DELETE' })\n await loadLines()\n }, [quoteId, loadLines])\n\n const canProceed = lines.length >= minItems\n const canAddMore = lines.length < maxItems\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs text-muted-foreground\">\n {lines.length}
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { WizardStepProps } from '../types'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\n\ntype QuoteLine = {\n lineId: string\n offeringId: string | null\n offeringName: string\n quantity: number\n nrcTotal: number\n mrcTotal: number\n isConfigured: boolean\n parentLineId: string | null\n}\n\nfunction fmt(n: number, currency = 'USD') {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n}\n\nexport function ItemListStep({ config, onComplete, onBack, stepData, completedSteps, quoteId }: WizardStepProps) {\n const t = useT()\n const [lines, setLines] = useState<QuoteLine[]>([])\n const [loading, setLoading] = useState(true)\n\n const parentLineSource = config.parentLineSource as string | undefined\n const minItems = (config.minItems as number) ?? 0\n const maxItems = (config.maxItems as number) ?? Infinity\n const showPricing = config.showPricing !== false\n\n const parentLineId = parentLineSource\n ? (stepData[parentLineSource]?.lineId as string | undefined)\n : undefined\n\n const loadLines = 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 const data = await res.json()\n const allLines = (data.lines ?? []) as QuoteLine[]\n // Filter to children of the parent line (if parentLineId set), otherwise show all\n const filtered = parentLineId\n ? allLines.filter((l) => l.parentLineId === parentLineId)\n : allLines\n setLines(filtered)\n }\n } finally {\n setLoading(false)\n }\n }, [quoteId, parentLineId])\n\n useEffect(() => { loadLines() }, [loadLines])\n\n const handleRemove = useCallback(async (lineId: string) => {\n if (!quoteId) return\n await fetch(`/api/cpq/quotes/${quoteId}/items/${lineId}`, { method: 'DELETE' })\n await loadLines()\n }, [quoteId, loadLines])\n\n const canProceed = lines.length >= minItems\n const canAddMore = lines.length < maxItems\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs text-muted-foreground\">\n {lines.length !== 1\n ? t('cpq.wizard.steps.itemList.count.other', '{count} items', { count: lines.length })\n : t('cpq.wizard.steps.itemList.count.one', '{count} item', { count: lines.length })}\n {minItems > 0 && ' '}\n {minItems > 0 && t('cpq.wizard.steps.itemList.min', '(min: {min})', { min: minItems })}\n {maxItems < Infinity && ' '}\n {maxItems < Infinity && t('cpq.wizard.steps.itemList.max', '(max: {max})', { max: maxItems })}\n </span>\n {canAddMore && (\n <span className=\"text-xs text-muted-foreground\">\n {t('cpq.wizard.steps.itemList.hint', 'Use the offering browser to add items to the quote, then return here.')}\n </span>\n )}\n </div>\n\n {loading ? (\n <div className=\"text-sm text-muted-foreground\">{t('cpq.wizard.steps.itemList.loading', 'Loading items...')}</div>\n ) : (\n <div className=\"space-y-2\">\n {lines.map((line) => (\n <div\n key={line.lineId}\n className=\"flex items-center justify-between rounded-lg border p-3\"\n >\n <div>\n <span className=\"text-sm font-medium\">{line.offeringName}</span>\n {line.quantity > 1 && <span className=\"ml-2 text-xs text-muted-foreground\">x{line.quantity}</span>}\n {!line.isConfigured && (\n <Tag variant=\"warning\" className=\"ml-2\">\n {t('cpq.wizard.steps.itemList.needsConfiguration', 'Needs configuration')}\n </Tag>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n {showPricing && (\n <span className=\"text-xs\">\n {t('cpq.wizard.linePricing', '{nrc} NRC / {mrc} MRC', { nrc: fmt(line.nrcTotal), mrc: fmt(line.mrcTotal) })}\n </span>\n )}\n <Button\n type=\"button\"\n variant=\"destructive-outline\"\n size=\"sm\"\n onClick={() => handleRemove(line.lineId)}\n >\n {t('cpq.wizard.remove', 'Remove')}\n </Button>\n </div>\n </div>\n ))}\n {lines.length === 0 && (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.wizard.steps.itemList.empty', 'No items added yet.')}\n </div>\n )}\n </div>\n )}\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 type=\"button\" onClick={loadLines} className=\"inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted\">\n {t('cpq.wizard.steps.itemList.refresh', 'Refresh')}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n onComplete({ lineCount: lines.length, lineIds: lines.map((l) => l.lineId) })\n }}\n disabled={!canProceed}\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 disabled:cursor-not-allowed\"\n >\n {t('cpq.wizard.next', 'Next')}\n </button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
5
|
import { NumberInput } from "../../components/NumberInput.js";
|
|
5
6
|
function ConfigPanel({
|
|
6
7
|
offering,
|
|
@@ -8,6 +9,7 @@ function ConfigPanel({
|
|
|
8
9
|
onChange,
|
|
9
10
|
onRemove
|
|
10
11
|
}) {
|
|
12
|
+
const t = useT();
|
|
11
13
|
const [attributes, setAttributes] = useState([]);
|
|
12
14
|
const [loading, setLoading] = useState(true);
|
|
13
15
|
const [configVersion, setConfigVersion] = useState(0);
|
|
@@ -49,7 +51,7 @@ function ConfigPanel({
|
|
|
49
51
|
const value = configuration[attr.code] ?? "";
|
|
50
52
|
if (attr.attributeType === "select" || attr.attributeType === "enum") {
|
|
51
53
|
return /* @__PURE__ */ jsxs("select", { value: String(value), onChange: (e) => handleChange(attr.code, e.target.value), className: "w-full rounded-md border px-3 py-2 text-sm", children: [
|
|
52
|
-
/* @__PURE__ */ jsx("option", { value: "", children: "Select..." }),
|
|
54
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t("cpq.wizard.selectPlaceholder", "Select...") }),
|
|
53
55
|
attr.options?.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
|
|
54
56
|
] });
|
|
55
57
|
}
|
|
@@ -73,9 +75,9 @@ function ConfigPanel({
|
|
|
73
75
|
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border p-4 space-y-3", children: [
|
|
74
76
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b pb-2", children: [
|
|
75
77
|
/* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: offering.name }),
|
|
76
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, className: "text-xs text-muted-foreground hover:text-foreground", children: "Remove" })
|
|
78
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, className: "text-xs text-muted-foreground hover:text-foreground", children: t("cpq.wizard.remove", "Remove") })
|
|
77
79
|
] }),
|
|
78
|
-
loading ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading attributes..." }) : attributes.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "No configurable attributes." }) : /* @__PURE__ */ jsx("div", { className: "space-y-3", children: attributes.map((attr) => /* @__PURE__ */ jsxs("div", { children: [
|
|
80
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("cpq.wizard.loadingAttributes", "Loading attributes...") }) : attributes.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("cpq.wizard.steps.offeringSelect.noAttributes", "No configurable attributes.") }) : /* @__PURE__ */ jsx("div", { className: "space-y-3", children: attributes.map((attr) => /* @__PURE__ */ jsxs("div", { children: [
|
|
79
81
|
/* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium mb-1", children: [
|
|
80
82
|
attr.name,
|
|
81
83
|
attr.isRequired && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
|
|
@@ -89,6 +91,7 @@ function BandwidthIndicator({
|
|
|
89
91
|
portCapacityMb,
|
|
90
92
|
items
|
|
91
93
|
}) {
|
|
94
|
+
const t = useT();
|
|
92
95
|
const totalBandwidth = items.reduce((sum, item) => {
|
|
93
96
|
const bw = Number(item.configuration.bandwidth_mb) || 0;
|
|
94
97
|
return sum + bw;
|
|
@@ -97,13 +100,8 @@ function BandwidthIndicator({
|
|
|
97
100
|
const overCapacity = totalBandwidth > portCapacityMb;
|
|
98
101
|
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border p-3 space-y-2", children: [
|
|
99
102
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
100
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: "Bandwidth Utilization" }),
|
|
101
|
-
/* @__PURE__ */
|
|
102
|
-
totalBandwidth,
|
|
103
|
-
" / ",
|
|
104
|
-
portCapacityMb,
|
|
105
|
-
" MB"
|
|
106
|
-
] })
|
|
103
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: t("cpq.wizard.steps.offeringSelect.bandwidth.title", "Bandwidth Utilization") }),
|
|
104
|
+
/* @__PURE__ */ jsx("span", { className: overCapacity ? "text-destructive font-medium" : "text-muted-foreground", children: t("cpq.wizard.steps.offeringSelect.bandwidth.usage", "{used} / {capacity} MB", { used: totalBandwidth, capacity: portCapacityMb }) })
|
|
107
105
|
] }),
|
|
108
106
|
/* @__PURE__ */ jsx("div", { className: "h-2 rounded-full bg-muted overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
109
107
|
"div",
|
|
@@ -112,15 +110,12 @@ function BandwidthIndicator({
|
|
|
112
110
|
style: { width: `${Math.min(100, pct)}%` }
|
|
113
111
|
}
|
|
114
112
|
) }),
|
|
115
|
-
overCapacity && /* @__PURE__ */
|
|
116
|
-
"Total bandwidth exceeds port capacity by ",
|
|
117
|
-
totalBandwidth - portCapacityMb,
|
|
118
|
-
" MB"
|
|
119
|
-
] })
|
|
113
|
+
overCapacity && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: t("cpq.wizard.steps.offeringSelect.bandwidth.exceeded", "Total bandwidth exceeds port capacity by {excess} MB", { excess: totalBandwidth - portCapacityMb }) })
|
|
120
114
|
] });
|
|
121
115
|
}
|
|
122
116
|
const PORT_SIZE_MB = { "1G": 1e3, "10G": 1e4, "100G": 1e5 };
|
|
123
117
|
function OfferingSelectStep({ config, onComplete, onBack, stepData, currentStepId, completedSteps, quoteId, customerId }) {
|
|
118
|
+
const t = useT();
|
|
124
119
|
const [offerings, setOfferings] = useState([]);
|
|
125
120
|
const [selectedItems, setSelectedItems] = useState([]);
|
|
126
121
|
const [loading, setLoading] = useState(true);
|
|
@@ -243,7 +238,7 @@ function OfferingSelectStep({ config, onComplete, onBack, stepData, currentStepI
|
|
|
243
238
|
}
|
|
244
239
|
}, [selectedItems, enableConfiguration, quoteId, resolvedCustomerId, onComplete]);
|
|
245
240
|
if (loading) {
|
|
246
|
-
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading offerings..." });
|
|
241
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("cpq.wizard.steps.offeringSelect.loading", "Loading offerings...") });
|
|
247
242
|
}
|
|
248
243
|
const availableOfferings = offerings.filter(
|
|
249
244
|
(o) => multiSelect ? true : !selectedItems.some((s) => s.offering.id === o.id)
|
|
@@ -264,26 +259,22 @@ function OfferingSelectStep({ config, onComplete, onBack, stepData, currentStepI
|
|
|
264
259
|
children: [
|
|
265
260
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
266
261
|
/* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: o.name }),
|
|
267
|
-
isAdded && /* @__PURE__ */ jsx("span", { className: "text-xs text-primary font-medium", children: "Added" })
|
|
262
|
+
isAdded && /* @__PURE__ */ jsx("span", { className: "text-xs text-primary font-medium", children: t("cpq.wizard.steps.offeringSelect.added", "Added") })
|
|
268
263
|
] }),
|
|
269
264
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1", children: o.description || o.code }),
|
|
270
|
-
!isAdded && /* @__PURE__ */ jsx("span", { className: "text-xs text-primary mt-2 inline-block", children: "+ Add" })
|
|
265
|
+
!isAdded && /* @__PURE__ */ jsx("span", { className: "text-xs text-primary mt-2 inline-block", children: t("cpq.wizard.steps.offeringSelect.add", "+ Add") })
|
|
271
266
|
]
|
|
272
267
|
},
|
|
273
268
|
o.id
|
|
274
269
|
);
|
|
275
270
|
}),
|
|
276
|
-
offerings.length === 0 && /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: "No offerings available." })
|
|
271
|
+
offerings.length === 0 && /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: t("cpq.wizard.steps.offeringSelect.empty", "No offerings available.") })
|
|
277
272
|
]
|
|
278
273
|
}
|
|
279
274
|
),
|
|
280
275
|
portCapacityMb > 0 && selectedItems.length > 0 && /* @__PURE__ */ jsx(BandwidthIndicator, { portCapacityMb, items: selectedItems }),
|
|
281
276
|
enableConfiguration && selectedItems.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
282
|
-
/* @__PURE__ */
|
|
283
|
-
"Selected Services (",
|
|
284
|
-
selectedItems.length,
|
|
285
|
-
")"
|
|
286
|
-
] }),
|
|
277
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: t("cpq.wizard.steps.offeringSelect.selectedServices", "Selected Services ({count})", { count: selectedItems.length }) }),
|
|
287
278
|
selectedItems.map((item, index) => /* @__PURE__ */ jsx(
|
|
288
279
|
ConfigPanel,
|
|
289
280
|
{
|
|
@@ -296,18 +287,14 @@ function OfferingSelectStep({ config, onComplete, onBack, stepData, currentStepI
|
|
|
296
287
|
))
|
|
297
288
|
] }),
|
|
298
289
|
!enableConfiguration && selectedItems.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
299
|
-
/* @__PURE__ */
|
|
300
|
-
"Selected (",
|
|
301
|
-
selectedItems.length,
|
|
302
|
-
")"
|
|
303
|
-
] }),
|
|
290
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: t("cpq.wizard.steps.offeringSelect.selected", "Selected ({count})", { count: selectedItems.length }) }),
|
|
304
291
|
selectedItems.map((item, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [
|
|
305
292
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: item.offering.name }),
|
|
306
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => removeItem(index), className: "text-xs text-muted-foreground hover:text-foreground", children: "Remove" })
|
|
293
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => removeItem(index), className: "text-xs text-muted-foreground hover:text-foreground", children: t("cpq.wizard.remove", "Remove") })
|
|
307
294
|
] }, `${item.offering.id}-${index}`))
|
|
308
295
|
] }),
|
|
309
296
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end pt-2", children: [
|
|
310
|
-
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" }),
|
|
297
|
+
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") }),
|
|
311
298
|
/* @__PURE__ */ jsx(
|
|
312
299
|
"button",
|
|
313
300
|
{
|
|
@@ -315,7 +302,7 @@ function OfferingSelectStep({ config, onComplete, onBack, stepData, currentStepI
|
|
|
315
302
|
onClick: handleNext,
|
|
316
303
|
disabled: selectedItems.length === 0 || saving,
|
|
317
304
|
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 disabled:cursor-not-allowed",
|
|
318
|
-
children: saving ? "Saving..." : "Next"
|
|
305
|
+
children: saving ? t("cpq.common.saving", "Saving...") : t("cpq.wizard.next", "Next")
|
|
319
306
|
}
|
|
320
307
|
)
|
|
321
308
|
] })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/cpq/workflows/steps/OfferingSelectStep.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport type { WizardStepProps } from '../types'\nimport { NumberInput } from '../../components/NumberInput'\n\ntype Offering = {\n id: string\n code: string\n name: string\n description: string | null\n specId: string\n isActive: boolean\n}\n\ntype ConstrainedAttribute = {\n code: string\n name: string\n attributeType: string\n isRequired: boolean\n options: Array<{ value: string; label: string }> | null\n constraints: Record<string, unknown> | null\n defaultValue: unknown | null\n helpText: string | null\n}\n\ntype SelectedItem = {\n offering: Offering\n configuration: Record<string, unknown>\n lineId?: string\n}\n\n// \u2500\u2500\u2500 Inline configuration panel for a single offering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction ConfigPanel({\n offering,\n configuration,\n onChange,\n onRemove,\n}: {\n offering: Offering\n configuration: Record<string, unknown>\n onChange: (code: string, value: unknown) => void\n onRemove: () => void\n}) {\n const [attributes, setAttributes] = useState<ConstrainedAttribute[]>([])\n const [loading, setLoading] = useState(true)\n const [configVersion, setConfigVersion] = useState(0)\n\n useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n try {\n const params = new URLSearchParams({ offeringId: offering.id })\n if (Object.keys(configuration).length > 0) {\n params.set('configuration', JSON.stringify(configuration))\n }\n const res = await fetch(`/api/cpq/quotes/attributes?${params}`)\n if (res.ok && !cancelled) {\n const data = await res.json()\n const attrs = (data.attributes ?? []) as ConstrainedAttribute[]\n setAttributes(attrs.filter((a: ConstrainedAttribute & { resolutionTime?: string }) => !a.resolutionTime || a.resolutionTime === 'run_time'))\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [offering.id, configVersion]) // eslint-disable-line react-hooks/exhaustive-deps\n\n const handleChange = useCallback((code: string, value: unknown) => {\n onChange(code, value)\n // Check for dependent attributes\n const hasDependents = attributes.some((a) => {\n const dep = a.constraints as Record<string, unknown> | null\n const depOn = dep?.dependsOn as { attributeCode?: string } | undefined\n const dynSrc = dep?.dynamicOptionsSource as { filterByDimension?: Record<string, string> } | undefined\n return depOn?.attributeCode === code ||\n (dynSrc?.filterByDimension && Object.values(dynSrc.filterByDimension).some((v) => v === `$${code}`))\n })\n if (hasDependents) setConfigVersion((v) => v + 1)\n }, [onChange, attributes])\n\n const renderAttribute = (attr: ConstrainedAttribute) => {\n const value = configuration[attr.code] ?? ''\n\n if (attr.attributeType === 'select' || attr.attributeType === 'enum') {\n return (\n <select value={String(value)} onChange={(e) => handleChange(attr.code, e.target.value)} className=\"w-full rounded-md border px-3 py-2 text-sm\">\n <option value=\"\">Select...</option>\n {attr.options?.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}\n </select>\n )\n }\n if (attr.attributeType === 'boolean') {\n return (\n <label className=\"flex items-center gap-2 text-sm\">\n <input type=\"checkbox\" checked={Boolean(value)} onChange={(e) => handleChange(attr.code, e.target.checked)} className=\"rounded border\" />\n {attr.name}\n </label>\n )\n }\n if (attr.attributeType === 'number') {\n return (\n <NumberInput\n value={value === '' || value == null ? null : Number(value)}\n onChange={(n) => handleChange(attr.code, n ?? '')}\n />\n )\n }\n return <input type=\"text\" value={String(value)} onChange={(e) => handleChange(attr.code, e.target.value)} className=\"w-full rounded-md border px-3 py-2 text-sm\" />\n }\n\n return (\n <div className=\"rounded-lg border p-4 space-y-3\">\n <div className=\"flex items-center justify-between border-b pb-2\">\n <h4 className=\"text-sm font-medium\">{offering.name}</h4>\n <button type=\"button\" onClick={onRemove} className=\"text-xs text-muted-foreground hover:text-foreground\">Remove</button>\n </div>\n {loading ? (\n <div className=\"text-sm text-muted-foreground\">Loading attributes...</div>\n ) : attributes.length === 0 ? (\n <div className=\"text-sm text-muted-foreground\">No configurable attributes.</div>\n ) : (\n <div className=\"space-y-3\">\n {attributes.map((attr) => (\n <div key={attr.code}>\n <label className=\"block text-sm font-medium mb-1\">\n {attr.name}\n {attr.isRequired && <span className=\"text-destructive ml-0.5\">*</span>}\n </label>\n {renderAttribute(attr)}\n {attr.helpText && <p className=\"text-xs text-muted-foreground mt-1\">{attr.helpText}</p>}\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Port capacity indicator \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction BandwidthIndicator({\n portCapacityMb,\n items,\n}: {\n portCapacityMb: number\n items: SelectedItem[]\n}) {\n const totalBandwidth = items.reduce((sum, item) => {\n const bw = Number(item.configuration.bandwidth_mb) || 0\n return sum + bw\n }, 0)\n\n const pct = portCapacityMb > 0 ? Math.min(100, (totalBandwidth / portCapacityMb) * 100) : 0\n const overCapacity = totalBandwidth > portCapacityMb\n\n return (\n <div className=\"rounded-lg border p-3 space-y-2\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"font-medium\">Bandwidth Utilization</span>\n <span className={overCapacity ? 'text-destructive font-medium' : 'text-muted-foreground'}>\n {totalBandwidth} / {portCapacityMb} MB\n </span>\n </div>\n <div className=\"h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${overCapacity ? 'bg-destructive' : 'bg-primary'}`}\n style={{ width: `${Math.min(100, pct)}%` }}\n />\n </div>\n {overCapacity && (\n <p className=\"text-xs text-destructive\">\n Total bandwidth exceeds port capacity by {totalBandwidth - portCapacityMb} MB\n </p>\n )}\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Main component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst PORT_SIZE_MB: Record<string, number> = { '1G': 1000, '10G': 10000, '100G': 100000 }\n\nexport function OfferingSelectStep({ config, onComplete, onBack, stepData, currentStepId, completedSteps, quoteId, customerId }: WizardStepProps) {\n const [offerings, setOfferings] = useState<Offering[]>([])\n const [selectedItems, setSelectedItems] = useState<SelectedItem[]>([])\n const [loading, setLoading] = useState(true)\n const [saving, setSaving] = useState(false)\n\n const enableConfiguration = config.enableConfiguration === true\n const multiSelect = config.multiSelect !== false // default true for offering_select\n const offeringFilter = config.offeringFilter as { specCodes?: string[]; offeringCodes?: string[] } | undefined\n const displayMode = (config.displayMode as string) ?? 'grid'\n\n // Resolve port capacity from prior step data\n const portCapacityMb = (() => {\n for (const data of Object.values(stepData)) {\n const cfg = data.configuration as Record<string, unknown> | undefined\n if (cfg?.port_size) return PORT_SIZE_MB[String(cfg.port_size)] ?? 0\n }\n return 0\n })()\n\n // Restore previous selections\n const previousData = stepData[currentStepId] as { items?: SelectedItem[] } | undefined\n\n useEffect(() => {\n async function load() {\n setLoading(true)\n try {\n const params = new URLSearchParams({ page: '1', pageSize: '100' })\n const res = await fetch(`/api/cpq/product-offerings?${params}`)\n if (res.ok) {\n const data = await res.json()\n let items = (data.items ?? []) as Offering[]\n if (offeringFilter?.offeringCodes?.length) {\n const codeOrder = offeringFilter.offeringCodes!\n items = items\n .filter((o) => codeOrder.includes(o.code))\n .sort((a, b) => codeOrder.indexOf(a.code) - codeOrder.indexOf(b.code))\n }\n items = items.filter((o) => o.isActive)\n setOfferings(items)\n\n // Restore previous selections\n if (previousData?.items?.length) {\n const restored = previousData.items\n .map((prev) => {\n const o = items.find((i) => i.id === prev.offering.id)\n return o ? { offering: o, configuration: prev.configuration, lineId: prev.lineId } : null\n })\n .filter(Boolean) as SelectedItem[]\n setSelectedItems(restored)\n }\n }\n } finally {\n setLoading(false)\n }\n }\n load()\n }, []) // eslint-disable-line react-hooks/exhaustive-deps\n\n const addItem = useCallback((offering: Offering) => {\n setSelectedItems((prev) => {\n if (!multiSelect) return [{ offering, configuration: {} }]\n return [...prev, { offering, configuration: {} }]\n })\n }, [multiSelect])\n\n const removeItem = useCallback((index: number) => {\n setSelectedItems((prev) => prev.filter((_, i) => i !== index))\n }, [])\n\n const updateItemConfig = useCallback((index: number, code: string, value: unknown) => {\n setSelectedItems((prev) => prev.map((item, i) =>\n i === index ? { ...item, configuration: { ...item.configuration, [code]: value } } : item\n ))\n }, [])\n\n // Resolve customerId from props or stepData\n const resolvedCustomerId = customerId ?? (() => {\n for (const data of Object.values(stepData)) {\n if (data.customerId) return data.customerId as string\n }\n return null\n })()\n\n const handleNext = useCallback(async () => {\n if (selectedItems.length === 0) return\n setSaving(true)\n try {\n let activeQuoteId = quoteId\n const output: Record<string, unknown> = { items: selectedItems }\n\n if (enableConfiguration) {\n // Auto-create quote if needed\n if (!activeQuoteId && resolvedCustomerId) {\n const createRes = await fetch('/api/cpq/quotes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ customerId: resolvedCustomerId }),\n })\n if (createRes.ok) {\n const quoteData = await createRes.json()\n activeQuoteId = quoteData.id\n output.quoteId = activeQuoteId\n }\n }\n\n // Add or update each selected item on the quote\n if (activeQuoteId) {\n for (const item of selectedItems) {\n if (item.lineId) {\n // Update existing line\n await fetch(`/api/cpq/quotes/${activeQuoteId}/items/${item.lineId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ configuration: item.configuration }),\n })\n } else {\n // Create new line\n const res = await fetch(`/api/cpq/quotes/${activeQuoteId}/items`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ offeringId: item.offering.id, configuration: item.configuration }),\n })\n if (res.ok) {\n const data = await res.json()\n const line = data.lines?.find((l: { offeringId: string }) => l.offeringId === item.offering.id)\n item.lineId = line?.lineId\n }\n }\n }\n output.quoteId = activeQuoteId\n }\n }\n\n // Also set single-item compat fields\n if (selectedItems.length === 1) {\n const first = selectedItems[0]\n output.offeringId = first.offering.id\n output.offeringCode = first.offering.code\n output.offeringName = first.offering.name\n output.specId = first.offering.specId\n if (enableConfiguration) output.configuration = first.configuration\n }\n\n onComplete(output)\n } finally {\n setSaving(false)\n }\n }, [selectedItems, enableConfiguration, quoteId, resolvedCustomerId, onComplete])\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">Loading offerings...</div>\n }\n\n // Offerings not yet added\n const availableOfferings = offerings.filter((o) =>\n multiSelect ? true : !selectedItems.some((s) => s.offering.id === o.id)\n )\n\n return (\n <div className=\"space-y-4\">\n {/* Offering cards */}\n <div\n className={\n displayMode === 'grid'\n ? 'grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3'\n : 'flex flex-col gap-2'\n }\n >\n {availableOfferings.map((o) => {\n const isAdded = selectedItems.some((s) => s.offering.id === o.id)\n return (\n <div\n key={o.id}\n onClick={() => !isAdded && addItem(o)}\n className={`rounded-lg border-2 p-4 transition-colors ${\n isAdded\n ? 'border-primary/30 bg-primary/5 opacity-60 cursor-default'\n : 'border-muted cursor-pointer hover:bg-muted/30'\n }`}\n >\n <div className=\"flex items-center justify-between\">\n <h4 className=\"text-sm font-medium\">{o.name}</h4>\n {isAdded && <span className=\"text-xs text-primary font-medium\">Added</span>}\n </div>\n <p className=\"text-xs text-muted-foreground mt-1\">{o.description || o.code}</p>\n {!isAdded && (\n <span className=\"text-xs text-primary mt-2 inline-block\">+ Add</span>\n )}\n </div>\n )\n })}\n {offerings.length === 0 && (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n No offerings available.\n </div>\n )}\n </div>\n\n {/* Bandwidth indicator */}\n {portCapacityMb > 0 && selectedItems.length > 0 && (\n <BandwidthIndicator portCapacityMb={portCapacityMb} items={selectedItems} />\n )}\n\n {/* Configuration panels for selected items */}\n {enableConfiguration && selectedItems.length > 0 && (\n <div className=\"space-y-3\">\n <h4 className=\"text-sm font-medium\">Selected Services ({selectedItems.length})</h4>\n {selectedItems.map((item, index) => (\n <ConfigPanel\n key={`${item.offering.id}-${index}`}\n offering={item.offering}\n configuration={item.configuration}\n onChange={(code, value) => updateItemConfig(index, code, value)}\n onRemove={() => removeItem(index)}\n />\n ))}\n </div>\n )}\n\n {/* No configuration \u2014 just show selected list */}\n {!enableConfiguration && selectedItems.length > 0 && (\n <div className=\"space-y-2\">\n <h4 className=\"text-sm font-medium\">Selected ({selectedItems.length})</h4>\n {selectedItems.map((item, index) => (\n <div key={`${item.offering.id}-${index}`} className=\"flex items-center justify-between rounded-md border p-3\">\n <span className=\"text-sm font-medium\">{item.offering.name}</span>\n <button type=\"button\" onClick={() => removeItem(index)} className=\"text-xs text-muted-foreground hover:text-foreground\">Remove</button>\n </div>\n ))}\n </div>\n )}\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={handleNext}\n disabled={selectedItems.length === 0 || saving}\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 disabled:cursor-not-allowed\"\n >\n {saving ? 'Saving...' : 'Next'}\n </button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { WizardStepProps } from '../types'\nimport { NumberInput } from '../../components/NumberInput'\n\ntype Offering = {\n id: string\n code: string\n name: string\n description: string | null\n specId: string\n isActive: boolean\n}\n\ntype ConstrainedAttribute = {\n code: string\n name: string\n attributeType: string\n isRequired: boolean\n options: Array<{ value: string; label: string }> | null\n constraints: Record<string, unknown> | null\n defaultValue: unknown | null\n helpText: string | null\n}\n\ntype SelectedItem = {\n offering: Offering\n configuration: Record<string, unknown>\n lineId?: string\n}\n\n// \u2500\u2500\u2500 Inline configuration panel for a single offering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction ConfigPanel({\n offering,\n configuration,\n onChange,\n onRemove,\n}: {\n offering: Offering\n configuration: Record<string, unknown>\n onChange: (code: string, value: unknown) => void\n onRemove: () => void\n}) {\n const t = useT()\n const [attributes, setAttributes] = useState<ConstrainedAttribute[]>([])\n const [loading, setLoading] = useState(true)\n const [configVersion, setConfigVersion] = useState(0)\n\n useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n try {\n const params = new URLSearchParams({ offeringId: offering.id })\n if (Object.keys(configuration).length > 0) {\n params.set('configuration', JSON.stringify(configuration))\n }\n const res = await fetch(`/api/cpq/quotes/attributes?${params}`)\n if (res.ok && !cancelled) {\n const data = await res.json()\n const attrs = (data.attributes ?? []) as ConstrainedAttribute[]\n setAttributes(attrs.filter((a: ConstrainedAttribute & { resolutionTime?: string }) => !a.resolutionTime || a.resolutionTime === 'run_time'))\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [offering.id, configVersion]) // eslint-disable-line react-hooks/exhaustive-deps\n\n const handleChange = useCallback((code: string, value: unknown) => {\n onChange(code, value)\n // Check for dependent attributes\n const hasDependents = attributes.some((a) => {\n const dep = a.constraints as Record<string, unknown> | null\n const depOn = dep?.dependsOn as { attributeCode?: string } | undefined\n const dynSrc = dep?.dynamicOptionsSource as { filterByDimension?: Record<string, string> } | undefined\n return depOn?.attributeCode === code ||\n (dynSrc?.filterByDimension && Object.values(dynSrc.filterByDimension).some((v) => v === `$${code}`))\n })\n if (hasDependents) setConfigVersion((v) => v + 1)\n }, [onChange, attributes])\n\n const renderAttribute = (attr: ConstrainedAttribute) => {\n const value = configuration[attr.code] ?? ''\n\n if (attr.attributeType === 'select' || attr.attributeType === 'enum') {\n return (\n <select value={String(value)} onChange={(e) => handleChange(attr.code, e.target.value)} className=\"w-full rounded-md border px-3 py-2 text-sm\">\n <option value=\"\">{t('cpq.wizard.selectPlaceholder', 'Select...')}</option>\n {attr.options?.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}\n </select>\n )\n }\n if (attr.attributeType === 'boolean') {\n return (\n <label className=\"flex items-center gap-2 text-sm\">\n <input type=\"checkbox\" checked={Boolean(value)} onChange={(e) => handleChange(attr.code, e.target.checked)} className=\"rounded border\" />\n {attr.name}\n </label>\n )\n }\n if (attr.attributeType === 'number') {\n return (\n <NumberInput\n value={value === '' || value == null ? null : Number(value)}\n onChange={(n) => handleChange(attr.code, n ?? '')}\n />\n )\n }\n return <input type=\"text\" value={String(value)} onChange={(e) => handleChange(attr.code, e.target.value)} className=\"w-full rounded-md border px-3 py-2 text-sm\" />\n }\n\n return (\n <div className=\"rounded-lg border p-4 space-y-3\">\n <div className=\"flex items-center justify-between border-b pb-2\">\n <h4 className=\"text-sm font-medium\">{offering.name}</h4>\n <button type=\"button\" onClick={onRemove} className=\"text-xs text-muted-foreground hover:text-foreground\">{t('cpq.wizard.remove', 'Remove')}</button>\n </div>\n {loading ? (\n <div className=\"text-sm text-muted-foreground\">{t('cpq.wizard.loadingAttributes', 'Loading attributes...')}</div>\n ) : attributes.length === 0 ? (\n <div className=\"text-sm text-muted-foreground\">{t('cpq.wizard.steps.offeringSelect.noAttributes', 'No configurable attributes.')}</div>\n ) : (\n <div className=\"space-y-3\">\n {attributes.map((attr) => (\n <div key={attr.code}>\n <label className=\"block text-sm font-medium mb-1\">\n {attr.name}\n {attr.isRequired && <span className=\"text-destructive ml-0.5\">*</span>}\n </label>\n {renderAttribute(attr)}\n {attr.helpText && <p className=\"text-xs text-muted-foreground mt-1\">{attr.helpText}</p>}\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Port capacity indicator \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction BandwidthIndicator({\n portCapacityMb,\n items,\n}: {\n portCapacityMb: number\n items: SelectedItem[]\n}) {\n const t = useT()\n const totalBandwidth = items.reduce((sum, item) => {\n const bw = Number(item.configuration.bandwidth_mb) || 0\n return sum + bw\n }, 0)\n\n const pct = portCapacityMb > 0 ? Math.min(100, (totalBandwidth / portCapacityMb) * 100) : 0\n const overCapacity = totalBandwidth > portCapacityMb\n\n return (\n <div className=\"rounded-lg border p-3 space-y-2\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"font-medium\">{t('cpq.wizard.steps.offeringSelect.bandwidth.title', 'Bandwidth Utilization')}</span>\n <span className={overCapacity ? 'text-destructive font-medium' : 'text-muted-foreground'}>\n {t('cpq.wizard.steps.offeringSelect.bandwidth.usage', '{used} / {capacity} MB', { used: totalBandwidth, capacity: portCapacityMb })}\n </span>\n </div>\n <div className=\"h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${overCapacity ? 'bg-destructive' : 'bg-primary'}`}\n style={{ width: `${Math.min(100, pct)}%` }}\n />\n </div>\n {overCapacity && (\n <p className=\"text-xs text-destructive\">\n {t('cpq.wizard.steps.offeringSelect.bandwidth.exceeded', 'Total bandwidth exceeds port capacity by {excess} MB', { excess: totalBandwidth - portCapacityMb })}\n </p>\n )}\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Main component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst PORT_SIZE_MB: Record<string, number> = { '1G': 1000, '10G': 10000, '100G': 100000 }\n\nexport function OfferingSelectStep({ config, onComplete, onBack, stepData, currentStepId, completedSteps, quoteId, customerId }: WizardStepProps) {\n const t = useT()\n const [offerings, setOfferings] = useState<Offering[]>([])\n const [selectedItems, setSelectedItems] = useState<SelectedItem[]>([])\n const [loading, setLoading] = useState(true)\n const [saving, setSaving] = useState(false)\n\n const enableConfiguration = config.enableConfiguration === true\n const multiSelect = config.multiSelect !== false // default true for offering_select\n const offeringFilter = config.offeringFilter as { specCodes?: string[]; offeringCodes?: string[] } | undefined\n const displayMode = (config.displayMode as string) ?? 'grid'\n\n // Resolve port capacity from prior step data\n const portCapacityMb = (() => {\n for (const data of Object.values(stepData)) {\n const cfg = data.configuration as Record<string, unknown> | undefined\n if (cfg?.port_size) return PORT_SIZE_MB[String(cfg.port_size)] ?? 0\n }\n return 0\n })()\n\n // Restore previous selections\n const previousData = stepData[currentStepId] as { items?: SelectedItem[] } | undefined\n\n useEffect(() => {\n async function load() {\n setLoading(true)\n try {\n const params = new URLSearchParams({ page: '1', pageSize: '100' })\n const res = await fetch(`/api/cpq/product-offerings?${params}`)\n if (res.ok) {\n const data = await res.json()\n let items = (data.items ?? []) as Offering[]\n if (offeringFilter?.offeringCodes?.length) {\n const codeOrder = offeringFilter.offeringCodes!\n items = items\n .filter((o) => codeOrder.includes(o.code))\n .sort((a, b) => codeOrder.indexOf(a.code) - codeOrder.indexOf(b.code))\n }\n items = items.filter((o) => o.isActive)\n setOfferings(items)\n\n // Restore previous selections\n if (previousData?.items?.length) {\n const restored = previousData.items\n .map((prev) => {\n const o = items.find((i) => i.id === prev.offering.id)\n return o ? { offering: o, configuration: prev.configuration, lineId: prev.lineId } : null\n })\n .filter(Boolean) as SelectedItem[]\n setSelectedItems(restored)\n }\n }\n } finally {\n setLoading(false)\n }\n }\n load()\n }, []) // eslint-disable-line react-hooks/exhaustive-deps\n\n const addItem = useCallback((offering: Offering) => {\n setSelectedItems((prev) => {\n if (!multiSelect) return [{ offering, configuration: {} }]\n return [...prev, { offering, configuration: {} }]\n })\n }, [multiSelect])\n\n const removeItem = useCallback((index: number) => {\n setSelectedItems((prev) => prev.filter((_, i) => i !== index))\n }, [])\n\n const updateItemConfig = useCallback((index: number, code: string, value: unknown) => {\n setSelectedItems((prev) => prev.map((item, i) =>\n i === index ? { ...item, configuration: { ...item.configuration, [code]: value } } : item\n ))\n }, [])\n\n // Resolve customerId from props or stepData\n const resolvedCustomerId = customerId ?? (() => {\n for (const data of Object.values(stepData)) {\n if (data.customerId) return data.customerId as string\n }\n return null\n })()\n\n const handleNext = useCallback(async () => {\n if (selectedItems.length === 0) return\n setSaving(true)\n try {\n let activeQuoteId = quoteId\n const output: Record<string, unknown> = { items: selectedItems }\n\n if (enableConfiguration) {\n // Auto-create quote if needed\n if (!activeQuoteId && resolvedCustomerId) {\n const createRes = await fetch('/api/cpq/quotes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ customerId: resolvedCustomerId }),\n })\n if (createRes.ok) {\n const quoteData = await createRes.json()\n activeQuoteId = quoteData.id\n output.quoteId = activeQuoteId\n }\n }\n\n // Add or update each selected item on the quote\n if (activeQuoteId) {\n for (const item of selectedItems) {\n if (item.lineId) {\n // Update existing line\n await fetch(`/api/cpq/quotes/${activeQuoteId}/items/${item.lineId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ configuration: item.configuration }),\n })\n } else {\n // Create new line\n const res = await fetch(`/api/cpq/quotes/${activeQuoteId}/items`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ offeringId: item.offering.id, configuration: item.configuration }),\n })\n if (res.ok) {\n const data = await res.json()\n const line = data.lines?.find((l: { offeringId: string }) => l.offeringId === item.offering.id)\n item.lineId = line?.lineId\n }\n }\n }\n output.quoteId = activeQuoteId\n }\n }\n\n // Also set single-item compat fields\n if (selectedItems.length === 1) {\n const first = selectedItems[0]\n output.offeringId = first.offering.id\n output.offeringCode = first.offering.code\n output.offeringName = first.offering.name\n output.specId = first.offering.specId\n if (enableConfiguration) output.configuration = first.configuration\n }\n\n onComplete(output)\n } finally {\n setSaving(false)\n }\n }, [selectedItems, enableConfiguration, quoteId, resolvedCustomerId, onComplete])\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">{t('cpq.wizard.steps.offeringSelect.loading', 'Loading offerings...')}</div>\n }\n\n // Offerings not yet added\n const availableOfferings = offerings.filter((o) =>\n multiSelect ? true : !selectedItems.some((s) => s.offering.id === o.id)\n )\n\n return (\n <div className=\"space-y-4\">\n {/* Offering cards */}\n <div\n className={\n displayMode === 'grid'\n ? 'grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3'\n : 'flex flex-col gap-2'\n }\n >\n {availableOfferings.map((o) => {\n const isAdded = selectedItems.some((s) => s.offering.id === o.id)\n return (\n <div\n key={o.id}\n onClick={() => !isAdded && addItem(o)}\n className={`rounded-lg border-2 p-4 transition-colors ${\n isAdded\n ? 'border-primary/30 bg-primary/5 opacity-60 cursor-default'\n : 'border-muted cursor-pointer hover:bg-muted/30'\n }`}\n >\n <div className=\"flex items-center justify-between\">\n <h4 className=\"text-sm font-medium\">{o.name}</h4>\n {isAdded && <span className=\"text-xs text-primary font-medium\">{t('cpq.wizard.steps.offeringSelect.added', 'Added')}</span>}\n </div>\n <p className=\"text-xs text-muted-foreground mt-1\">{o.description || o.code}</p>\n {!isAdded && (\n <span className=\"text-xs text-primary mt-2 inline-block\">{t('cpq.wizard.steps.offeringSelect.add', '+ Add')}</span>\n )}\n </div>\n )\n })}\n {offerings.length === 0 && (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.wizard.steps.offeringSelect.empty', 'No offerings available.')}\n </div>\n )}\n </div>\n\n {/* Bandwidth indicator */}\n {portCapacityMb > 0 && selectedItems.length > 0 && (\n <BandwidthIndicator portCapacityMb={portCapacityMb} items={selectedItems} />\n )}\n\n {/* Configuration panels for selected items */}\n {enableConfiguration && selectedItems.length > 0 && (\n <div className=\"space-y-3\">\n <h4 className=\"text-sm font-medium\">{t('cpq.wizard.steps.offeringSelect.selectedServices', 'Selected Services ({count})', { count: selectedItems.length })}</h4>\n {selectedItems.map((item, index) => (\n <ConfigPanel\n key={`${item.offering.id}-${index}`}\n offering={item.offering}\n configuration={item.configuration}\n onChange={(code, value) => updateItemConfig(index, code, value)}\n onRemove={() => removeItem(index)}\n />\n ))}\n </div>\n )}\n\n {/* No configuration \u2014 just show selected list */}\n {!enableConfiguration && selectedItems.length > 0 && (\n <div className=\"space-y-2\">\n <h4 className=\"text-sm font-medium\">{t('cpq.wizard.steps.offeringSelect.selected', 'Selected ({count})', { count: selectedItems.length })}</h4>\n {selectedItems.map((item, index) => (\n <div key={`${item.offering.id}-${index}`} className=\"flex items-center justify-between rounded-md border p-3\">\n <span className=\"text-sm font-medium\">{item.offering.name}</span>\n <button type=\"button\" onClick={() => removeItem(index)} className=\"text-xs text-muted-foreground hover:text-foreground\">{t('cpq.wizard.remove', 'Remove')}</button>\n </div>\n ))}\n </div>\n )}\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={handleNext}\n disabled={selectedItems.length === 0 || saving}\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 disabled:cursor-not-allowed\"\n >\n {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.wizard.next', 'Next')}\n </button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
5
|
import { NumberInput } from "../../components/NumberInput.js";
|
|
5
6
|
function resolveOfferingId(config, stepData) {
|
|
6
7
|
const source = config.offeringSource;
|
|
@@ -13,6 +14,7 @@ function resolveOfferingId(config, stepData) {
|
|
|
13
14
|
return null;
|
|
14
15
|
}
|
|
15
16
|
function ProductConfigureStep({ config, onComplete, onBack, stepData, currentStepId, completedSteps, quoteId, customerId }) {
|
|
17
|
+
const t = useT();
|
|
16
18
|
const [attributes, setAttributes] = useState([]);
|
|
17
19
|
const [values, setValues] = useState({});
|
|
18
20
|
const [loading, setLoading] = useState(true);
|
|
@@ -145,7 +147,7 @@ function ProductConfigureStep({ config, onComplete, onBack, stepData, currentSte
|
|
|
145
147
|
disabled: isReadonly,
|
|
146
148
|
className: "w-full rounded-md border px-3 py-2 text-sm disabled:opacity-50",
|
|
147
149
|
children: [
|
|
148
|
-
/* @__PURE__ */ jsx("option", { value: "", children: "Select..." }),
|
|
150
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t("cpq.wizard.selectPlaceholder", "Select...") }),
|
|
149
151
|
attr.options?.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
|
|
150
152
|
]
|
|
151
153
|
}
|
|
@@ -188,10 +190,10 @@ function ProductConfigureStep({ config, onComplete, onBack, stepData, currentSte
|
|
|
188
190
|
);
|
|
189
191
|
};
|
|
190
192
|
if (loading) {
|
|
191
|
-
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading attributes..." });
|
|
193
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("cpq.wizard.loadingAttributes", "Loading attributes...") });
|
|
192
194
|
}
|
|
193
195
|
if (!offeringId) {
|
|
194
|
-
return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: "No offering selected. Please go back and select an offering." });
|
|
196
|
+
return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: t("cpq.wizard.steps.productConfigure.noOffering", "No offering selected. Please go back and select an offering.") });
|
|
195
197
|
}
|
|
196
198
|
const renderGroup = (label, attrs) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
197
199
|
/* @__PURE__ */ jsx("h4", { className: "text-sm font-medium border-b pb-2", children: label }),
|
|
@@ -209,9 +211,9 @@ function ProductConfigureStep({ config, onComplete, onBack, stepData, currentSte
|
|
|
209
211
|
const groupAttrs = attributes.filter((a) => group.attributeCodes.includes(a.code));
|
|
210
212
|
if (groupAttrs.length === 0) return null;
|
|
211
213
|
return renderGroup(group.label, groupAttrs);
|
|
212
|
-
}) : renderGroup("Configuration", attributes),
|
|
214
|
+
}) : renderGroup(t("cpq.lines.configuration", "Configuration"), attributes),
|
|
213
215
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end pt-2", children: [
|
|
214
|
-
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" }),
|
|
216
|
+
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") }),
|
|
215
217
|
/* @__PURE__ */ jsx(
|
|
216
218
|
"button",
|
|
217
219
|
{
|
|
@@ -219,7 +221,7 @@ function ProductConfigureStep({ config, onComplete, onBack, stepData, currentSte
|
|
|
219
221
|
onClick: handleComplete,
|
|
220
222
|
disabled: saving,
|
|
221
223
|
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",
|
|
222
|
-
children: saving ? "Saving..." : "Next"
|
|
224
|
+
children: saving ? t("cpq.common.saving", "Saving...") : t("cpq.wizard.next", "Next")
|
|
223
225
|
}
|
|
224
226
|
)
|
|
225
227
|
] })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/cpq/workflows/steps/ProductConfigureStep.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport type { WizardStepProps } from '../types'\nimport { NumberInput } from '../../components/NumberInput'\n\ntype ConstrainedAttribute = {\n code: string\n name: string\n attributeType: string\n isRequired: boolean\n options: Array<{ value: string; label: string }> | null\n constraints: Record<string, unknown> | null\n defaultValue: unknown | null\n helpText: string | null\n}\n\ntype AttributeGroup = {\n label: string\n attributeCodes: string[]\n}\n\nfunction resolveOfferingId(config: Record<string, unknown>, stepData: Record<string, Record<string, unknown>>): string | null {\n // Check explicit source\n const source = config.offeringSource as string | undefined\n if (source && stepData[source]) {\n return (stepData[source].offeringId as string) ?? null\n }\n // Auto-detect: find first step data with offeringId\n for (const data of Object.values(stepData)) {\n if (data.offeringId) return data.offeringId as string\n }\n return null\n}\n\nexport function ProductConfigureStep({ config, onComplete, onBack, stepData, currentStepId, completedSteps, quoteId, customerId }: WizardStepProps) {\n const [attributes, setAttributes] = useState<ConstrainedAttribute[]>([])\n const [values, setValues] = useState<Record<string, unknown>>({})\n const [loading, setLoading] = useState(true)\n const [saving, setSaving] = useState(false)\n const [resolvedOfferingId, setResolvedOfferingId] = useState<string | null>(null)\n\n const directOfferingId = resolveOfferingId(config, stepData)\n const offeringCode = config.offeringCode as string | undefined\n const attributeGroups = config.attributeGroups as AttributeGroup[] | undefined\n\n // Resolve offering by code if no direct offeringId\n useEffect(() => {\n if (directOfferingId) {\n setResolvedOfferingId(directOfferingId)\n return\n }\n if (!offeringCode) return\n async function resolve() {\n try {\n const res = await fetch(`/api/cpq/product-offerings?code=${encodeURIComponent(offeringCode!)}`)\n if (res.ok) {\n const data = await res.json()\n const items = data.items ?? data\n if (Array.isArray(items) && items.length > 0) {\n setResolvedOfferingId(items[0].id)\n }\n }\n } catch { /* ignore */ }\n }\n resolve()\n }, [directOfferingId, offeringCode])\n\n const offeringId = resolvedOfferingId\n const hiddenAttributes = (config.hiddenAttributes as string[]) ?? []\n const readonlyAttributes = (config.readonlyAttributes as string[]) ?? []\n\n // Restore previous values\n const previousData = stepData[currentStepId] as { configuration?: Record<string, unknown> } | undefined\n\n useEffect(() => {\n async function loadAttributes() {\n if (!offeringId) { setLoading(false); return }\n setLoading(true)\n try {\n const params = new URLSearchParams({ offeringId })\n if (Object.keys(values).length > 0) {\n params.set('configuration', JSON.stringify(values))\n }\n const res = await fetch(`/api/cpq/quotes/attributes?${params}`)\n if (res.ok) {\n const data = await res.json()\n const attrs = (data.attributes ?? []) as ConstrainedAttribute[]\n setAttributes(attrs.filter((a) => !hiddenAttributes.includes(a.code)))\n\n // Set defaults on first load\n if (Object.keys(values).length === 0) {\n const defaults: Record<string, unknown> = previousData?.configuration ?? {}\n for (const attr of attrs) {\n if (defaults[attr.code] === undefined && attr.defaultValue != null) {\n defaults[attr.code] = attr.defaultValue\n }\n }\n setValues(defaults)\n }\n }\n } finally {\n setLoading(false)\n }\n }\n loadAttributes()\n }, [offeringId]) // eslint-disable-line react-hooks/exhaustive-deps\n\n const handleChange = useCallback((code: string, value: unknown) => {\n setValues((prev) => ({ ...prev, [code]: value }))\n }, [])\n\n // Resolve customerId from props or stepData\n const resolvedCustomerId = customerId ?? (() => {\n for (const data of Object.values(stepData)) {\n if (data.customerId) return data.customerId as string\n }\n return null\n })()\n\n const handleComplete = useCallback(async () => {\n setSaving(true)\n try {\n let activeQuoteId = quoteId\n\n // Auto-create a quote if none exists yet\n if (!activeQuoteId && resolvedCustomerId && offeringId) {\n const createRes = await fetch('/api/cpq/quotes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ customerId: resolvedCustomerId }),\n })\n if (createRes.ok) {\n const quoteData = await createRes.json()\n activeQuoteId = quoteData.id\n }\n }\n\n if (activeQuoteId && offeringId) {\n // Check if there's already a line for this offering\n const existingLineId = (stepData[currentStepId] as { lineId?: string })?.lineId\n\n if (existingLineId) {\n await fetch(`/api/cpq/quotes/${activeQuoteId}/items/${existingLineId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ configuration: values }),\n })\n onComplete({ configuration: values, offeringId, lineId: existingLineId, quoteId: activeQuoteId })\n } else {\n const res = await fetch(`/api/cpq/quotes/${activeQuoteId}/items`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ offeringId, configuration: values }),\n })\n if (res.ok) {\n const data = await res.json()\n const line = data.lines?.find((l: { offeringId: string }) => l.offeringId === offeringId)\n onComplete({ configuration: values, offeringId, lineId: line?.lineId, quoteId: activeQuoteId })\n } else {\n onComplete({ configuration: values, offeringId, quoteId: activeQuoteId })\n }\n }\n } else {\n onComplete({ configuration: values, offeringId })\n }\n } finally {\n setSaving(false)\n }\n }, [values, quoteId, offeringId, stepData, currentStepId, onComplete, resolvedCustomerId])\n\n const renderAttribute = (attr: ConstrainedAttribute) => {\n const isReadonly = readonlyAttributes.includes(attr.code)\n const value = values[attr.code] ?? ''\n\n if (attr.attributeType === 'select' || attr.attributeType === 'enum') {\n return (\n <select\n value={String(value)}\n onChange={(e) => handleChange(attr.code, e.target.value)}\n disabled={isReadonly}\n className=\"w-full rounded-md border px-3 py-2 text-sm disabled:opacity-50\"\n >\n <option value=\"\">Select...</option>\n {attr.options?.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )\n }\n\n if (attr.attributeType === 'boolean') {\n return (\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={Boolean(value)}\n onChange={(e) => handleChange(attr.code, e.target.checked)}\n disabled={isReadonly}\n className=\"rounded border\"\n />\n {attr.name}\n </label>\n )\n }\n\n if (attr.attributeType === 'number') {\n return (\n <NumberInput\n value={value === '' || value == null ? null : Number(value)}\n onChange={(n) => handleChange(attr.code, n ?? '')}\n disabled={isReadonly}\n />\n )\n }\n\n return (\n <input\n type=\"text\"\n value={String(value)}\n onChange={(e) => handleChange(attr.code, e.target.value)}\n disabled={isReadonly}\n className=\"w-full rounded-md border px-3 py-2 text-sm disabled:opacity-50\"\n />\n )\n }\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">Loading attributes...</div>\n }\n\n if (!offeringId) {\n return (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n No offering selected. Please go back and select an offering.\n </div>\n )\n }\n\n const renderGroup = (label: string, attrs: ConstrainedAttribute[]) => (\n <div key={label} className=\"space-y-3\">\n <h4 className=\"text-sm font-medium border-b pb-2\">{label}</h4>\n <div className=\"space-y-3\">\n {attrs.map((attr) => (\n <div key={attr.code}>\n <label className=\"block text-sm font-medium mb-1\">\n {attr.name}\n {attr.isRequired && <span className=\"text-destructive ml-0.5\">*</span>}\n </label>\n {renderAttribute(attr)}\n {attr.helpText && <p className=\"text-xs text-muted-foreground mt-1\">{attr.helpText}</p>}\n </div>\n ))}\n </div>\n </div>\n )\n\n return (\n <div className=\"space-y-4\">\n {attributeGroups?.length ? (\n attributeGroups.map((group) => {\n const groupAttrs = attributes.filter((a) => group.attributeCodes.includes(a.code))\n if (groupAttrs.length === 0) return null\n return renderGroup(group.label, groupAttrs)\n })\n ) : (\n renderGroup('Configuration', attributes)\n )}\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={handleComplete}\n disabled={saving}\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 {saving ? 'Saving...' : 'Next'}\n </button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { WizardStepProps } from '../types'\nimport { NumberInput } from '../../components/NumberInput'\n\ntype ConstrainedAttribute = {\n code: string\n name: string\n attributeType: string\n isRequired: boolean\n options: Array<{ value: string; label: string }> | null\n constraints: Record<string, unknown> | null\n defaultValue: unknown | null\n helpText: string | null\n}\n\ntype AttributeGroup = {\n label: string\n attributeCodes: string[]\n}\n\nfunction resolveOfferingId(config: Record<string, unknown>, stepData: Record<string, Record<string, unknown>>): string | null {\n // Check explicit source\n const source = config.offeringSource as string | undefined\n if (source && stepData[source]) {\n return (stepData[source].offeringId as string) ?? null\n }\n // Auto-detect: find first step data with offeringId\n for (const data of Object.values(stepData)) {\n if (data.offeringId) return data.offeringId as string\n }\n return null\n}\n\nexport function ProductConfigureStep({ config, onComplete, onBack, stepData, currentStepId, completedSteps, quoteId, customerId }: WizardStepProps) {\n const t = useT()\n const [attributes, setAttributes] = useState<ConstrainedAttribute[]>([])\n const [values, setValues] = useState<Record<string, unknown>>({})\n const [loading, setLoading] = useState(true)\n const [saving, setSaving] = useState(false)\n const [resolvedOfferingId, setResolvedOfferingId] = useState<string | null>(null)\n\n const directOfferingId = resolveOfferingId(config, stepData)\n const offeringCode = config.offeringCode as string | undefined\n const attributeGroups = config.attributeGroups as AttributeGroup[] | undefined\n\n // Resolve offering by code if no direct offeringId\n useEffect(() => {\n if (directOfferingId) {\n setResolvedOfferingId(directOfferingId)\n return\n }\n if (!offeringCode) return\n async function resolve() {\n try {\n const res = await fetch(`/api/cpq/product-offerings?code=${encodeURIComponent(offeringCode!)}`)\n if (res.ok) {\n const data = await res.json()\n const items = data.items ?? data\n if (Array.isArray(items) && items.length > 0) {\n setResolvedOfferingId(items[0].id)\n }\n }\n } catch { /* ignore */ }\n }\n resolve()\n }, [directOfferingId, offeringCode])\n\n const offeringId = resolvedOfferingId\n const hiddenAttributes = (config.hiddenAttributes as string[]) ?? []\n const readonlyAttributes = (config.readonlyAttributes as string[]) ?? []\n\n // Restore previous values\n const previousData = stepData[currentStepId] as { configuration?: Record<string, unknown> } | undefined\n\n useEffect(() => {\n async function loadAttributes() {\n if (!offeringId) { setLoading(false); return }\n setLoading(true)\n try {\n const params = new URLSearchParams({ offeringId })\n if (Object.keys(values).length > 0) {\n params.set('configuration', JSON.stringify(values))\n }\n const res = await fetch(`/api/cpq/quotes/attributes?${params}`)\n if (res.ok) {\n const data = await res.json()\n const attrs = (data.attributes ?? []) as ConstrainedAttribute[]\n setAttributes(attrs.filter((a) => !hiddenAttributes.includes(a.code)))\n\n // Set defaults on first load\n if (Object.keys(values).length === 0) {\n const defaults: Record<string, unknown> = previousData?.configuration ?? {}\n for (const attr of attrs) {\n if (defaults[attr.code] === undefined && attr.defaultValue != null) {\n defaults[attr.code] = attr.defaultValue\n }\n }\n setValues(defaults)\n }\n }\n } finally {\n setLoading(false)\n }\n }\n loadAttributes()\n }, [offeringId]) // eslint-disable-line react-hooks/exhaustive-deps\n\n const handleChange = useCallback((code: string, value: unknown) => {\n setValues((prev) => ({ ...prev, [code]: value }))\n }, [])\n\n // Resolve customerId from props or stepData\n const resolvedCustomerId = customerId ?? (() => {\n for (const data of Object.values(stepData)) {\n if (data.customerId) return data.customerId as string\n }\n return null\n })()\n\n const handleComplete = useCallback(async () => {\n setSaving(true)\n try {\n let activeQuoteId = quoteId\n\n // Auto-create a quote if none exists yet\n if (!activeQuoteId && resolvedCustomerId && offeringId) {\n const createRes = await fetch('/api/cpq/quotes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ customerId: resolvedCustomerId }),\n })\n if (createRes.ok) {\n const quoteData = await createRes.json()\n activeQuoteId = quoteData.id\n }\n }\n\n if (activeQuoteId && offeringId) {\n // Check if there's already a line for this offering\n const existingLineId = (stepData[currentStepId] as { lineId?: string })?.lineId\n\n if (existingLineId) {\n await fetch(`/api/cpq/quotes/${activeQuoteId}/items/${existingLineId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ configuration: values }),\n })\n onComplete({ configuration: values, offeringId, lineId: existingLineId, quoteId: activeQuoteId })\n } else {\n const res = await fetch(`/api/cpq/quotes/${activeQuoteId}/items`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ offeringId, configuration: values }),\n })\n if (res.ok) {\n const data = await res.json()\n const line = data.lines?.find((l: { offeringId: string }) => l.offeringId === offeringId)\n onComplete({ configuration: values, offeringId, lineId: line?.lineId, quoteId: activeQuoteId })\n } else {\n onComplete({ configuration: values, offeringId, quoteId: activeQuoteId })\n }\n }\n } else {\n onComplete({ configuration: values, offeringId })\n }\n } finally {\n setSaving(false)\n }\n }, [values, quoteId, offeringId, stepData, currentStepId, onComplete, resolvedCustomerId])\n\n const renderAttribute = (attr: ConstrainedAttribute) => {\n const isReadonly = readonlyAttributes.includes(attr.code)\n const value = values[attr.code] ?? ''\n\n if (attr.attributeType === 'select' || attr.attributeType === 'enum') {\n return (\n <select\n value={String(value)}\n onChange={(e) => handleChange(attr.code, e.target.value)}\n disabled={isReadonly}\n className=\"w-full rounded-md border px-3 py-2 text-sm disabled:opacity-50\"\n >\n <option value=\"\">{t('cpq.wizard.selectPlaceholder', 'Select...')}</option>\n {attr.options?.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )\n }\n\n if (attr.attributeType === 'boolean') {\n return (\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={Boolean(value)}\n onChange={(e) => handleChange(attr.code, e.target.checked)}\n disabled={isReadonly}\n className=\"rounded border\"\n />\n {attr.name}\n </label>\n )\n }\n\n if (attr.attributeType === 'number') {\n return (\n <NumberInput\n value={value === '' || value == null ? null : Number(value)}\n onChange={(n) => handleChange(attr.code, n ?? '')}\n disabled={isReadonly}\n />\n )\n }\n\n return (\n <input\n type=\"text\"\n value={String(value)}\n onChange={(e) => handleChange(attr.code, e.target.value)}\n disabled={isReadonly}\n className=\"w-full rounded-md border px-3 py-2 text-sm disabled:opacity-50\"\n />\n )\n }\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">{t('cpq.wizard.loadingAttributes', 'Loading attributes...')}</div>\n }\n\n if (!offeringId) {\n return (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.wizard.steps.productConfigure.noOffering', 'No offering selected. Please go back and select an offering.')}\n </div>\n )\n }\n\n const renderGroup = (label: string, attrs: ConstrainedAttribute[]) => (\n <div key={label} className=\"space-y-3\">\n <h4 className=\"text-sm font-medium border-b pb-2\">{label}</h4>\n <div className=\"space-y-3\">\n {attrs.map((attr) => (\n <div key={attr.code}>\n <label className=\"block text-sm font-medium mb-1\">\n {attr.name}\n {attr.isRequired && <span className=\"text-destructive ml-0.5\">*</span>}\n </label>\n {renderAttribute(attr)}\n {attr.helpText && <p className=\"text-xs text-muted-foreground mt-1\">{attr.helpText}</p>}\n </div>\n ))}\n </div>\n </div>\n )\n\n return (\n <div className=\"space-y-4\">\n {attributeGroups?.length ? (\n attributeGroups.map((group) => {\n const groupAttrs = attributes.filter((a) => group.attributeCodes.includes(a.code))\n if (groupAttrs.length === 0) return null\n return renderGroup(group.label, groupAttrs)\n })\n ) : (\n renderGroup(t('cpq.lines.configuration', 'Configuration'), attributes)\n )}\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={handleComplete}\n disabled={saving}\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 {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.wizard.next', 'Next')}\n </button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|