@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
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
6
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
6
7
|
import {
|
|
7
8
|
assetStatusMap,
|
|
8
|
-
formatStatusLabel,
|
|
9
9
|
subscriptionStatusMap,
|
|
10
10
|
type AssetStatus,
|
|
11
11
|
type SubscriptionStatus,
|
|
12
12
|
} from '../../../components/statusMaps'
|
|
13
|
+
import { inventoryStatusLabel } from '../../../backend/cpq/inventory/shared'
|
|
13
14
|
|
|
14
15
|
// ─── Types ───────────────────────────────────────────────────────
|
|
15
16
|
|
|
@@ -79,13 +80,14 @@ function fmt(amount: number, currency: string): string {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function StatusBadge({ status }: { status: string }) {
|
|
83
|
+
const t = useT()
|
|
82
84
|
const variant =
|
|
83
85
|
subscriptionStatusMap[status as SubscriptionStatus] ??
|
|
84
86
|
assetStatusMap[status as AssetStatus] ??
|
|
85
87
|
'neutral'
|
|
86
88
|
return (
|
|
87
89
|
<Tag variant={variant} dot>
|
|
88
|
-
{
|
|
90
|
+
{inventoryStatusLabel(t, status)}
|
|
89
91
|
</Tag>
|
|
90
92
|
)
|
|
91
93
|
}
|
|
@@ -93,6 +95,7 @@ function StatusBadge({ status }: { status: string }) {
|
|
|
93
95
|
// ─── Main Widget ─────────────────────────────────────────────────
|
|
94
96
|
|
|
95
97
|
export default function CustomerInventoryWidget({ data, context }: InjectionWidgetComponentProps & { context?: Record<string, unknown> }) {
|
|
98
|
+
const t = useT()
|
|
96
99
|
const router = useRouter()
|
|
97
100
|
const record = data as Record<string, unknown> | null
|
|
98
101
|
const companyRecord = record?.company as Record<string, unknown> | undefined
|
|
@@ -121,12 +124,12 @@ export default function CustomerInventoryWidget({ data, context }: InjectionWidg
|
|
|
121
124
|
return () => { cancelled = true }
|
|
122
125
|
}, [customerId])
|
|
123
126
|
|
|
124
|
-
if (!customerId) return <div className="p-4 text-sm text-muted-foreground">No customer context available
|
|
127
|
+
if (!customerId) return <div className="p-4 text-sm text-muted-foreground">{t('cpq.widgets.customerInventory.noContext', 'No customer context available.')}</div>
|
|
125
128
|
|
|
126
129
|
if (loading) {
|
|
127
130
|
return (
|
|
128
131
|
<div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
|
|
129
|
-
<Spinner /> Loading inventory…
|
|
132
|
+
<Spinner /> {t('cpq.widgets.customerInventory.loading', 'Loading inventory…')}
|
|
130
133
|
</div>
|
|
131
134
|
)
|
|
132
135
|
}
|
|
@@ -139,7 +142,7 @@ export default function CustomerInventoryWidget({ data, context }: InjectionWidg
|
|
|
139
142
|
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
|
140
143
|
</svg>
|
|
141
144
|
</div>
|
|
142
|
-
<p className="text-sm text-muted-foreground">No inventory records found
|
|
145
|
+
<p className="text-sm text-muted-foreground">{t('cpq.widgets.customerInventory.empty', 'No inventory records found.')}</p>
|
|
143
146
|
</div>
|
|
144
147
|
)
|
|
145
148
|
}
|
|
@@ -148,19 +151,19 @@ export default function CustomerInventoryWidget({ data, context }: InjectionWidg
|
|
|
148
151
|
const currency = summary.currencyCode || 'USD'
|
|
149
152
|
|
|
150
153
|
const tabs: { key: TabKey; label: string; count: number }[] = [
|
|
151
|
-
{ key: 'subscriptions', label: 'Subscriptions', count: inventory.subscriptions.length },
|
|
152
|
-
{ key: 'items', label: 'Subscription Items', count: inventory.subscriptionItems.length },
|
|
153
|
-
{ key: 'assets', label: 'Assets', count: inventory.assets.length },
|
|
154
|
+
{ key: 'subscriptions', label: t('cpq.widgets.customerInventory.tabs.subscriptions', 'Subscriptions'), count: inventory.subscriptions.length },
|
|
155
|
+
{ key: 'items', label: t('cpq.widgets.customerInventory.tabs.items', 'Subscription Items'), count: inventory.subscriptionItems.length },
|
|
156
|
+
{ key: 'assets', label: t('cpq.widgets.customerInventory.tabs.assets', 'Assets'), count: inventory.assets.length },
|
|
154
157
|
]
|
|
155
158
|
|
|
156
159
|
return (
|
|
157
160
|
<div className="space-y-4">
|
|
158
161
|
{/* Summary Cards */}
|
|
159
162
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
160
|
-
<SummaryCard label=
|
|
161
|
-
<SummaryCard label=
|
|
162
|
-
<SummaryCard label=
|
|
163
|
-
<SummaryCard label=
|
|
163
|
+
<SummaryCard label={t('cpq.widgets.customerInventory.summary.activeSubscriptions', 'Active Subscriptions')} value={String(summary.activeSubscriptions)} />
|
|
164
|
+
<SummaryCard label={t('cpq.widgets.customerInventory.summary.totalMrc', 'Total MRC')} value={fmt(summary.totalMrc, currency)} mono />
|
|
165
|
+
<SummaryCard label={t('cpq.widgets.customerInventory.summary.activeAssets', 'Active Assets')} value={String(summary.activeAssets)} />
|
|
166
|
+
<SummaryCard label={t('cpq.widgets.customerInventory.summary.pendingItems', 'Pending Items')} value={String(summary.pendingItems)} />
|
|
164
167
|
</div>
|
|
165
168
|
|
|
166
169
|
{/* Tab Switcher */}
|
|
@@ -189,7 +192,7 @@ export default function CustomerInventoryWidget({ data, context }: InjectionWidg
|
|
|
189
192
|
{/* Link to full page */}
|
|
190
193
|
<div className="pt-2 text-right">
|
|
191
194
|
<a href="/backend/cpq/inventory" className="text-sm text-primary hover:underline">
|
|
192
|
-
View full inventory →
|
|
195
|
+
{t('cpq.widgets.customerInventory.viewFull', 'View full inventory →')}
|
|
193
196
|
</a>
|
|
194
197
|
</div>
|
|
195
198
|
</div>
|
|
@@ -210,18 +213,19 @@ function SummaryCard({ label, value, mono }: { label: string; value: string; mon
|
|
|
210
213
|
// ─── Subscriptions Table ─────────────────────────────────────────
|
|
211
214
|
|
|
212
215
|
function SubscriptionsTable({ items, currency, onOpen }: { items: Subscription[]; currency: string; onOpen: (id: string) => void }) {
|
|
216
|
+
const t = useT()
|
|
213
217
|
if (items.length === 0) return <EmptyTab />
|
|
214
218
|
return (
|
|
215
219
|
<div className="rounded-lg border bg-card overflow-hidden">
|
|
216
220
|
<table className="w-full text-sm">
|
|
217
221
|
<thead>
|
|
218
222
|
<tr className="border-b border-border bg-muted/40 text-muted-foreground">
|
|
219
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Name</th>
|
|
220
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Status</th>
|
|
221
|
-
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">MRC</th>
|
|
222
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Billing</th>
|
|
223
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Start Date</th>
|
|
224
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Term End</th>
|
|
223
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.name', 'Name')}</th>
|
|
224
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.status', 'Status')}</th>
|
|
225
|
+
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.mrc', 'MRC')}</th>
|
|
226
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.billing', 'Billing')}</th>
|
|
227
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.startDate', 'Start Date')}</th>
|
|
228
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.termEnd', 'Term End')}</th>
|
|
225
229
|
</tr>
|
|
226
230
|
</thead>
|
|
227
231
|
<tbody>
|
|
@@ -248,17 +252,18 @@ function SubscriptionsTable({ items, currency, onOpen }: { items: Subscription[]
|
|
|
248
252
|
// ─── Subscription Items Table ────────────────────────────────────
|
|
249
253
|
|
|
250
254
|
function SubscriptionItemsTable({ items, currency, onOpen }: { items: SubscriptionItem[]; currency: string; onOpen: (subscriptionId: string) => void }) {
|
|
255
|
+
const t = useT()
|
|
251
256
|
if (items.length === 0) return <EmptyTab />
|
|
252
257
|
return (
|
|
253
258
|
<div className="rounded-lg border bg-card overflow-hidden">
|
|
254
259
|
<table className="w-full text-sm">
|
|
255
260
|
<thead>
|
|
256
261
|
<tr className="border-b border-border bg-muted/40 text-muted-foreground">
|
|
257
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Name</th>
|
|
258
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Status</th>
|
|
259
|
-
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">MRC</th>
|
|
260
|
-
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">NRC</th>
|
|
261
|
-
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">Qty</th>
|
|
262
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.name', 'Name')}</th>
|
|
263
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.status', 'Status')}</th>
|
|
264
|
+
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.mrc', 'MRC')}</th>
|
|
265
|
+
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.nrc', 'NRC')}</th>
|
|
266
|
+
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.qty', 'Qty')}</th>
|
|
262
267
|
</tr>
|
|
263
268
|
</thead>
|
|
264
269
|
<tbody>
|
|
@@ -284,17 +289,18 @@ function SubscriptionItemsTable({ items, currency, onOpen }: { items: Subscripti
|
|
|
284
289
|
// ─── Assets Table ────────────────────────────────────────────────
|
|
285
290
|
|
|
286
291
|
function AssetsTable({ items, currency, onOpen }: { items: Asset[]; currency: string; onOpen: (id: string) => void }) {
|
|
292
|
+
const t = useT()
|
|
287
293
|
if (items.length === 0) return <EmptyTab />
|
|
288
294
|
return (
|
|
289
295
|
<div className="rounded-lg border bg-card overflow-hidden">
|
|
290
296
|
<table className="w-full text-sm">
|
|
291
297
|
<thead>
|
|
292
298
|
<tr className="border-b border-border bg-muted/40 text-muted-foreground">
|
|
293
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Name</th>
|
|
294
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Type</th>
|
|
295
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Status</th>
|
|
296
|
-
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">Price</th>
|
|
297
|
-
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Linked To</th>
|
|
299
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.name', 'Name')}</th>
|
|
300
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.type', 'Type')}</th>
|
|
301
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.status', 'Status')}</th>
|
|
302
|
+
<th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.price', 'Price')}</th>
|
|
303
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.linkedTo', 'Linked To')}</th>
|
|
298
304
|
</tr>
|
|
299
305
|
</thead>
|
|
300
306
|
<tbody>
|
|
@@ -309,7 +315,7 @@ function AssetsTable({ items, currency, onOpen }: { items: Asset[]; currency: st
|
|
|
309
315
|
<td className="px-4 py-3"><StatusBadge status={a.status} /></td>
|
|
310
316
|
<td className="px-4 py-3 text-right font-mono">{fmt(a.purchasePrice, a.currencyCode || currency)}</td>
|
|
311
317
|
<td className="px-4 py-3 text-muted-foreground">
|
|
312
|
-
{a.subscriptionId ?
|
|
318
|
+
{a.subscriptionId ? t('cpq.widgets.customerInventory.table.linkedSubscription', 'Sub {id}…', { id: a.subscriptionId.slice(0, 8) }) : '—'}
|
|
313
319
|
</td>
|
|
314
320
|
</tr>
|
|
315
321
|
))}
|
|
@@ -322,9 +328,10 @@ function AssetsTable({ items, currency, onOpen }: { items: Asset[]; currency: st
|
|
|
322
328
|
// ─── Shared ──────────────────────────────────────────────────────
|
|
323
329
|
|
|
324
330
|
function EmptyTab() {
|
|
331
|
+
const t = useT()
|
|
325
332
|
return (
|
|
326
333
|
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
|
|
327
|
-
No records in this category.
|
|
334
|
+
{t('cpq.widgets.customerInventory.emptyTab', 'No records in this category.')}
|
|
328
335
|
</div>
|
|
329
336
|
)
|
|
330
337
|
}
|
|
@@ -3,6 +3,7 @@ import * as React from 'react'
|
|
|
3
3
|
import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
5
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
7
|
import {
|
|
7
8
|
formatStatusLabel,
|
|
8
9
|
orderCpqStatusMap,
|
|
@@ -28,6 +29,7 @@ function fmt(amount: number | undefined | null, currency: string): string {
|
|
|
28
29
|
|
|
29
30
|
export default function OrderCpqTabWidget({ data }: InjectionWidgetComponentProps<any, any>) {
|
|
30
31
|
const router = useRouter()
|
|
32
|
+
const t = useT()
|
|
31
33
|
const salesOrderId = data?.id as string | undefined
|
|
32
34
|
const [cpqOrders, setCpqOrders] = React.useState<CpqOrderSummary[]>([])
|
|
33
35
|
const [loading, setLoading] = React.useState(true)
|
|
@@ -52,7 +54,7 @@ export default function OrderCpqTabWidget({ data }: InjectionWidgetComponentProp
|
|
|
52
54
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
53
55
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
|
54
56
|
</svg>
|
|
55
|
-
Loading CPQ orders…
|
|
57
|
+
{t('cpq.widgets.orderCpqTab.loading', 'Loading CPQ orders…')}
|
|
56
58
|
</div>
|
|
57
59
|
)
|
|
58
60
|
}
|
|
@@ -60,7 +62,7 @@ export default function OrderCpqTabWidget({ data }: InjectionWidgetComponentProp
|
|
|
60
62
|
if (cpqOrders.length === 0) {
|
|
61
63
|
return (
|
|
62
64
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
63
|
-
No CPQ orders linked to this sales order.
|
|
65
|
+
{t('cpq.widgets.orderCpqTab.empty', 'No CPQ orders linked to this sales order.')}
|
|
64
66
|
</div>
|
|
65
67
|
)
|
|
66
68
|
}
|
|
@@ -82,12 +84,12 @@ export default function OrderCpqTabWidget({ data }: InjectionWidgetComponentProp
|
|
|
82
84
|
<div className="flex items-center gap-4 text-xs">
|
|
83
85
|
{o.pricingSummary && (
|
|
84
86
|
<>
|
|
85
|
-
<span><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(o.pricingSummary.nrcTotal, o.currencyCode)}</span></span>
|
|
86
|
-
<span><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(o.pricingSummary.mrcTotal, o.currencyCode)}</span></span>
|
|
87
|
+
<span><span className="text-muted-foreground">{t('cpq.lines.nrc', 'NRC')} </span><span className="font-mono font-medium">{fmt(o.pricingSummary.nrcTotal, o.currencyCode)}</span></span>
|
|
88
|
+
<span><span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span><span className="font-mono font-medium">{fmt(o.pricingSummary.mrcTotal, o.currencyCode)}</span></span>
|
|
87
89
|
</>
|
|
88
90
|
)}
|
|
89
91
|
<span className="text-muted-foreground">{new Date(o.createdAt).toLocaleDateString()}</span>
|
|
90
|
-
<span className="text-primary font-medium">Open
|
|
92
|
+
<span className="text-primary font-medium">{t('cpq.widgets.orderCpqTab.open', 'Open →')}</span>
|
|
91
93
|
</div>
|
|
92
94
|
</div>
|
|
93
95
|
))}
|
|
@@ -3,6 +3,7 @@ import * as React from 'react'
|
|
|
3
3
|
import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
5
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
6
|
+
import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
6
7
|
import {
|
|
7
8
|
formatStatusLabel,
|
|
8
9
|
quoteCpqStatusMap,
|
|
@@ -58,10 +59,24 @@ function fmt(amount: number, currency: string): string {
|
|
|
58
59
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
function chargeTypeLabel(t: TranslateFn, chargeType: ResolvedCharge['chargeType']): string {
|
|
63
|
+
switch (chargeType) {
|
|
64
|
+
case 'nrc':
|
|
65
|
+
return t('cpq.lines.chargeType.nrc', 'nrc')
|
|
66
|
+
case 'mrc':
|
|
67
|
+
return t('cpq.lines.chargeType.mrc', 'mrc')
|
|
68
|
+
case 'usage':
|
|
69
|
+
return t('cpq.lines.chargeType.usage', 'usage')
|
|
70
|
+
default:
|
|
71
|
+
return chargeType
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
// ─── Main Widget (read-only) ─────────────────────────────────────
|
|
62
76
|
|
|
63
77
|
export default function QuoteConfiguratorWidget({ data }: InjectionWidgetComponentProps) {
|
|
64
78
|
const router = useRouter()
|
|
79
|
+
const t = useT()
|
|
65
80
|
const record = data as Record<string, unknown> | null
|
|
66
81
|
const quoteId = (record?.id as string) ?? null
|
|
67
82
|
|
|
@@ -83,12 +98,12 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
83
98
|
return () => { cancelled = true }
|
|
84
99
|
}, [quoteId])
|
|
85
100
|
|
|
86
|
-
if (!quoteId) return <div className="p-4 text-sm text-muted-foreground">No quote context available
|
|
101
|
+
if (!quoteId) return <div className="p-4 text-sm text-muted-foreground">{t('cpq.widgets.quoteConfigurator.noContext', 'No quote context available.')}</div>
|
|
87
102
|
|
|
88
103
|
if (loading) {
|
|
89
104
|
return (
|
|
90
105
|
<div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
|
|
91
|
-
<Spinner /> Loading CPQ configuration…
|
|
106
|
+
<Spinner /> {t('cpq.widgets.quoteConfigurator.loading', 'Loading CPQ configuration…')}
|
|
92
107
|
</div>
|
|
93
108
|
)
|
|
94
109
|
}
|
|
@@ -105,7 +120,7 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
105
120
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
106
121
|
</svg>
|
|
107
122
|
</div>
|
|
108
|
-
<p className="text-sm text-muted-foreground mb-4">This quote has no CPQ configuration yet
|
|
123
|
+
<p className="text-sm text-muted-foreground mb-4">{t('cpq.widgets.quoteConfigurator.noConfig', 'This quote has no CPQ configuration yet.')}</p>
|
|
109
124
|
<button
|
|
110
125
|
onClick={() => router.push(`/backend/cpq/quotes/new?quoteId=${quoteId}&customerId=${custId}`)}
|
|
111
126
|
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
|
@@ -113,9 +128,9 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
113
128
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
114
129
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
115
130
|
</svg>
|
|
116
|
-
Create CPQ Quote
|
|
131
|
+
{t('cpq.widgets.quoteConfigurator.create', 'Create CPQ Quote')}
|
|
117
132
|
</button>
|
|
118
|
-
{!custId && <p className="mt-2 text-xs text-status-warning-text">Assign a customer to this quote first
|
|
133
|
+
{!custId && <p className="mt-2 text-xs text-status-warning-text">{t('cpq.widgets.quoteConfigurator.assignCustomer', 'Assign a customer to this quote first.')}</p>}
|
|
119
134
|
</div>
|
|
120
135
|
)
|
|
121
136
|
}
|
|
@@ -146,7 +161,7 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
146
161
|
onClick={() => router.push(`/backend/cpq/quotes/${cpqQuote.id}`)}
|
|
147
162
|
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
|
148
163
|
>
|
|
149
|
-
Open CPQ Configurator
|
|
164
|
+
{t('cpq.widgets.quoteConfigurator.open', 'Open CPQ Configurator')}
|
|
150
165
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
151
166
|
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
152
167
|
</svg>
|
|
@@ -156,7 +171,7 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
156
171
|
{/* Lines */}
|
|
157
172
|
{cpqQuote.lines.length === 0 ? (
|
|
158
173
|
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
|
|
159
|
-
No items configured yet.
|
|
174
|
+
{t('cpq.widgets.quoteConfigurator.noLines', 'No items configured yet.')}
|
|
160
175
|
</div>
|
|
161
176
|
) : (
|
|
162
177
|
<div className="rounded-lg border bg-card divide-y">
|
|
@@ -178,9 +193,9 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
178
193
|
<span className="font-medium text-sm">{line.offeringName}</span>
|
|
179
194
|
{line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
|
|
180
195
|
{line.isConfigured ? (
|
|
181
|
-
<Tag variant="success" className="px-1.5 text-xs">configured</Tag>
|
|
196
|
+
<Tag variant="success" className="px-1.5 text-xs">{t('cpq.quotes.detail.lineConfigured', 'configured')}</Tag>
|
|
182
197
|
) : (
|
|
183
|
-
<Tag variant="warning" className="px-1.5 text-xs">incomplete</Tag>
|
|
198
|
+
<Tag variant="warning" className="px-1.5 text-xs">{t('cpq.quotes.detail.lineIncomplete', 'incomplete')}</Tag>
|
|
184
199
|
)}
|
|
185
200
|
</div>
|
|
186
201
|
{line.validationErrors && line.validationErrors.length > 0 && (
|
|
@@ -188,8 +203,8 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
188
203
|
)}
|
|
189
204
|
</div>
|
|
190
205
|
<div className="shrink-0 text-right text-xs space-y-0.5">
|
|
191
|
-
{line.nrcTotal > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
|
|
192
|
-
{line.mrcTotal > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
|
|
206
|
+
{line.nrcTotal > 0 && <div><span className="text-muted-foreground">{t('cpq.lines.nrc', 'NRC')} </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
|
|
207
|
+
{line.mrcTotal > 0 && <div><span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
|
|
193
208
|
</div>
|
|
194
209
|
</div>
|
|
195
210
|
|
|
@@ -199,18 +214,18 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
199
214
|
<table className="w-full text-xs">
|
|
200
215
|
<thead>
|
|
201
216
|
<tr className="text-muted-foreground">
|
|
202
|
-
<th className="text-left py-1 font-medium">Charge</th>
|
|
203
|
-
<th className="text-left py-1 font-medium">Type</th>
|
|
204
|
-
<th className="text-right py-1 font-medium">Unit Price</th>
|
|
205
|
-
<th className="text-right py-1 font-medium">Qty</th>
|
|
206
|
-
<th className="text-right py-1 font-medium">Total</th>
|
|
217
|
+
<th className="text-left py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.charge', 'Charge')}</th>
|
|
218
|
+
<th className="text-left py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.type', 'Type')}</th>
|
|
219
|
+
<th className="text-right py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.unitPrice', 'Unit Price')}</th>
|
|
220
|
+
<th className="text-right py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.qty', 'Qty')}</th>
|
|
221
|
+
<th className="text-right py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.total', 'Total')}</th>
|
|
207
222
|
</tr>
|
|
208
223
|
</thead>
|
|
209
224
|
<tbody>
|
|
210
225
|
{line.charges.map((c, i) => (
|
|
211
226
|
<tr key={i} className="border-t border-dashed border-border/50">
|
|
212
227
|
<td className="py-1">{c.chargeName}</td>
|
|
213
|
-
<td className="py-1 uppercase">{c.chargeType}</td>
|
|
228
|
+
<td className="py-1 uppercase">{chargeTypeLabel(t, c.chargeType)}</td>
|
|
214
229
|
<td className="py-1 text-right font-mono">{fmt(c.unitPrice, currency)}</td>
|
|
215
230
|
<td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
|
|
216
231
|
<td className="py-1 text-right font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : c.note ?? '—'}</td>
|
|
@@ -231,19 +246,19 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
|
|
|
231
246
|
<div className="rounded-lg border bg-card p-4">
|
|
232
247
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
|
233
248
|
<div>
|
|
234
|
-
<p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
|
|
249
|
+
<p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.nrc', 'Non-Recurring (NRC)')}</p>
|
|
235
250
|
<p className="text-lg font-semibold font-mono">{fmt(summary.nrcTotal, currency)}</p>
|
|
236
251
|
</div>
|
|
237
252
|
<div>
|
|
238
|
-
<p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
|
|
253
|
+
<p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.mrc', 'Monthly Recurring (MRC)')}</p>
|
|
239
254
|
<p className="text-lg font-semibold font-mono">{fmt(summary.mrcTotal, currency)}</p>
|
|
240
255
|
</div>
|
|
241
256
|
{summary.usageCharges.length > 0 && (
|
|
242
257
|
<div>
|
|
243
|
-
<p className="text-xs text-muted-foreground">Usage Charges</p>
|
|
258
|
+
<p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.usageCharges', 'Usage Charges')}</p>
|
|
244
259
|
<div className="mt-1 space-y-0.5">
|
|
245
260
|
{summary.usageCharges.map((uc, i) => (
|
|
246
|
-
<p key={i} className="text-xs">{uc.chargeName}: {fmt(uc.unitPrice, currency)}
|
|
261
|
+
<p key={i} className="text-xs">{uc.chargeName}: {t('cpq.lines.perUnit', '{price}/unit', { price: fmt(uc.unitPrice, currency) })}</p>
|
|
247
262
|
))}
|
|
248
263
|
</div>
|
|
249
264
|
</div>
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
2
|
|
|
3
|
+
// Tab labels: the sales document page translates `groupLabel` as an i18n key
|
|
4
|
+
// (`t(groupLabel, metadata.title)`), so the sales tabs use CPQ keys. The
|
|
5
|
+
// customers detail pages (legacy v1) render `groupLabel` verbatim, so the
|
|
6
|
+
// customer-inventory tab keeps plain English until the host translates it
|
|
7
|
+
// (spec 2026-11-15-cpq-multi-language, Risks).
|
|
8
|
+
|
|
3
9
|
export const injectionTable: ModuleInjectionTable = {
|
|
4
10
|
'sales.document.detail.quote:tabs': [
|
|
5
11
|
{
|
|
6
12
|
widgetId: 'cpq.injection.quote-configurator',
|
|
7
13
|
kind: 'tab',
|
|
8
|
-
groupLabel: '
|
|
14
|
+
groupLabel: 'cpq.widgets.quoteConfigurator.tab',
|
|
9
15
|
groupDescription: 'Configure, price, and manage quote offerings',
|
|
10
16
|
priority: 70,
|
|
11
17
|
},
|
|
@@ -14,7 +20,7 @@ export const injectionTable: ModuleInjectionTable = {
|
|
|
14
20
|
{
|
|
15
21
|
widgetId: 'cpq.injection.order-cpq-tab',
|
|
16
22
|
kind: 'tab',
|
|
17
|
-
groupLabel: '
|
|
23
|
+
groupLabel: 'cpq.widgets.orderCpqTab.tab',
|
|
18
24
|
groupDescription: 'View the CPQ order associated with this sales order',
|
|
19
25
|
priority: 60,
|
|
20
26
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
4
5
|
import { WizardEngineProvider, useWizardEngine } from './engine'
|
|
5
6
|
import { getStepType } from './registry'
|
|
6
7
|
import type { WizardDefinitionResult, WizardStepProps } from './types'
|
|
@@ -9,6 +10,7 @@ import type { TenantScope } from '../services/types'
|
|
|
9
10
|
// ─── Step renderer ──────────────────────────────────────────────
|
|
10
11
|
|
|
11
12
|
function StepRenderer({ scope }: { scope: TenantScope }) {
|
|
13
|
+
const t = useT()
|
|
12
14
|
const { state, completeStep, goBack, skipStep } = useWizardEngine()
|
|
13
15
|
const { currentStep, stepData, completedSteps, currentStepId, params, quoteId, customerId } = state
|
|
14
16
|
|
|
@@ -33,18 +35,18 @@ function StepRenderer({ scope }: { scope: TenantScope }) {
|
|
|
33
35
|
return (
|
|
34
36
|
<div className="rounded-lg border bg-card p-6 text-center space-y-3">
|
|
35
37
|
<p className="text-sm text-destructive">
|
|
36
|
-
Unknown step type: <strong>{currentStep.type}</strong>
|
|
38
|
+
{t('cpq.wizard.runner.unknownStepType', 'Unknown step type:')} <strong>{currentStep.type}</strong>
|
|
37
39
|
</p>
|
|
38
|
-
<p className="text-sm text-muted-foreground">This step type is not registered. Register it via <code className="font-mono text-xs bg-muted px-1 py-0.5 rounded">registerStepType()</code>.</p>
|
|
40
|
+
<p className="text-sm text-muted-foreground">{t('cpq.wizard.runner.notRegistered', 'This step type is not registered. Register it via')} <code className="font-mono text-xs bg-muted px-1 py-0.5 rounded">{'registerStepType()'}</code>.</p>
|
|
39
41
|
<div className="flex gap-2 justify-center pt-2">
|
|
40
42
|
{state.canGoBack && (
|
|
41
43
|
<button type="button" onClick={goBack} className="inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted">
|
|
42
|
-
Back
|
|
44
|
+
{t('cpq.common.back', 'Back')}
|
|
43
45
|
</button>
|
|
44
46
|
)}
|
|
45
47
|
{state.canSkip && (
|
|
46
48
|
<button type="button" onClick={skipStep} className="inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted">
|
|
47
|
-
Skip
|
|
49
|
+
{t('cpq.wizard.skip', 'Skip')}
|
|
48
50
|
</button>
|
|
49
51
|
)}
|
|
50
52
|
</div>
|
|
@@ -98,6 +100,7 @@ function WizardProgress() {
|
|
|
98
100
|
// ─── Header ─────────────────────────────────────────────────────
|
|
99
101
|
|
|
100
102
|
function WizardHeader() {
|
|
103
|
+
const t = useT()
|
|
101
104
|
const { state } = useWizardEngine()
|
|
102
105
|
|
|
103
106
|
return (
|
|
@@ -105,7 +108,7 @@ function WizardHeader() {
|
|
|
105
108
|
<div>
|
|
106
109
|
<h2 className="text-lg font-semibold">{state.definition.name}</h2>
|
|
107
110
|
<p className="text-sm text-muted-foreground">
|
|
108
|
-
Step: {state.currentStep.title}
|
|
111
|
+
{t('cpq.wizard.runner.currentStep', 'Step: {title}', { title: state.currentStep.title })}
|
|
109
112
|
</p>
|
|
110
113
|
</div>
|
|
111
114
|
</div>
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useCallback, useState } from 'react'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
5
6
|
import type { WizardDefinitionResult } from './types'
|
|
6
7
|
|
|
7
8
|
// ─── API helper ─────────────────────────────────────────────────
|
|
8
9
|
|
|
9
|
-
async function apiJson<T>(url: string): Promise<T> {
|
|
10
|
+
async function apiJson<T>(t: TranslateFn, url: string): Promise<T> {
|
|
10
11
|
const res = await fetch(url)
|
|
11
12
|
if (!res.ok) {
|
|
12
13
|
const body = await res.json().catch(() => ({}))
|
|
13
|
-
throw new Error(body.error ??
|
|
14
|
+
throw new Error(body.error ?? t('cpq.wizard.trigger.apiError', 'API error {status}', { status: res.status }))
|
|
14
15
|
}
|
|
15
16
|
return res.json() as Promise<T>
|
|
16
17
|
}
|
|
@@ -43,13 +44,16 @@ export function WizardTrigger({
|
|
|
43
44
|
offeringCode,
|
|
44
45
|
params: wizardParams = {},
|
|
45
46
|
surface = 'backend',
|
|
46
|
-
label
|
|
47
|
+
label,
|
|
47
48
|
variant = 'button',
|
|
48
49
|
className,
|
|
49
50
|
basePath,
|
|
50
51
|
onStart,
|
|
51
52
|
}: WizardTriggerProps) {
|
|
52
53
|
const router = useRouter()
|
|
54
|
+
const t = useT()
|
|
55
|
+
const buttonLabel = label ?? t('cpq.wizard.trigger.start', 'Start Wizard')
|
|
56
|
+
const startingLabel = t('cpq.wizard.trigger.starting', 'Starting...')
|
|
53
57
|
const [loading, setLoading] = useState(false)
|
|
54
58
|
const [error, setError] = useState<string | null>(null)
|
|
55
59
|
|
|
@@ -63,18 +67,20 @@ export function WizardTrigger({
|
|
|
63
67
|
|
|
64
68
|
if (wizardCode) {
|
|
65
69
|
definition = await apiJson<WizardDefinitionResult>(
|
|
70
|
+
t,
|
|
66
71
|
`/api/cpq/wizards?code=${encodeURIComponent(wizardCode)}`,
|
|
67
72
|
)
|
|
68
73
|
} else {
|
|
69
74
|
const resolveParams = new URLSearchParams({ surface })
|
|
70
75
|
if (offeringCode) resolveParams.set('offeringCode', offeringCode)
|
|
71
76
|
definition = await apiJson<WizardDefinitionResult>(
|
|
77
|
+
t,
|
|
72
78
|
`/api/cpq/wizards/resolve?${resolveParams}`,
|
|
73
79
|
)
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
if (!definition) {
|
|
77
|
-
setError('No matching wizard found')
|
|
83
|
+
setError(t('cpq.wizard.trigger.notFound', 'No matching wizard found'))
|
|
78
84
|
return
|
|
79
85
|
}
|
|
80
86
|
|
|
@@ -90,11 +96,11 @@ export function WizardTrigger({
|
|
|
90
96
|
router.push(`${path}/${definition.code}${qs ? `?${qs}` : ''}`)
|
|
91
97
|
}
|
|
92
98
|
} catch (err) {
|
|
93
|
-
setError(err instanceof Error ? err.message : 'Failed to start wizard')
|
|
99
|
+
setError(err instanceof Error ? err.message : t('cpq.wizard.trigger.failed', 'Failed to start wizard'))
|
|
94
100
|
} finally {
|
|
95
101
|
setLoading(false)
|
|
96
102
|
}
|
|
97
|
-
}, [wizardCode, offeringCode, wizardParams, surface, basePath, onStart, router])
|
|
103
|
+
}, [wizardCode, offeringCode, wizardParams, surface, basePath, onStart, router, t])
|
|
98
104
|
|
|
99
105
|
if (variant === 'link') {
|
|
100
106
|
return (
|
|
@@ -106,7 +112,7 @@ export function WizardTrigger({
|
|
|
106
112
|
handleClick()
|
|
107
113
|
}}
|
|
108
114
|
>
|
|
109
|
-
{loading ?
|
|
115
|
+
{loading ? startingLabel : buttonLabel}
|
|
110
116
|
{error && <span className="ml-2 text-xs text-destructive">{error}</span>}
|
|
111
117
|
</a>
|
|
112
118
|
)
|
|
@@ -121,7 +127,7 @@ export function WizardTrigger({
|
|
|
121
127
|
tabIndex={0}
|
|
122
128
|
onKeyDown={(e) => { if (e.key === 'Enter') handleClick() }}
|
|
123
129
|
>
|
|
124
|
-
<p className="font-semibold text-sm">{
|
|
130
|
+
<p className="font-semibold text-sm">{buttonLabel}</p>
|
|
125
131
|
{error && <p className="mt-1 text-xs text-destructive">{error}</p>}
|
|
126
132
|
</div>
|
|
127
133
|
)
|
|
@@ -135,7 +141,7 @@ export function WizardTrigger({
|
|
|
135
141
|
onClick={handleClick}
|
|
136
142
|
disabled={loading}
|
|
137
143
|
>
|
|
138
|
-
{loading ?
|
|
144
|
+
{loading ? startingLabel : buttonLabel}
|
|
139
145
|
{error && <span className="ml-2 text-xs text-destructive/70">{error}</span>}
|
|
140
146
|
</button>
|
|
141
147
|
)
|