@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
|
@@ -66,7 +66,7 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
66
66
|
const load = React.useCallback(async () => {
|
|
67
67
|
if (isNew) return
|
|
68
68
|
if (!id || !UUID_RE.test(id)) {
|
|
69
|
-
setError('Invalid pricing table ID')
|
|
69
|
+
setError(t('cpq.pricing.detail.errors.invalidId', 'Invalid pricing table ID'))
|
|
70
70
|
setLoading(false)
|
|
71
71
|
return
|
|
72
72
|
}
|
|
@@ -74,7 +74,7 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
74
74
|
try {
|
|
75
75
|
const res = await fetch(`/api/cpq/pricing-tables?id=${encodeURIComponent(id)}`)
|
|
76
76
|
if (!res.ok) {
|
|
77
|
-
setError('Pricing table not found')
|
|
77
|
+
setError(t('cpq.pricing.detail.errors.notFound', 'Pricing table not found'))
|
|
78
78
|
return
|
|
79
79
|
}
|
|
80
80
|
const data = await res.json()
|
|
@@ -92,11 +92,11 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
92
92
|
setEntries(data.entries ?? [])
|
|
93
93
|
} catch (err) {
|
|
94
94
|
console.error('Failed to load pricing table', err)
|
|
95
|
-
setError('Failed to load')
|
|
95
|
+
setError(t('cpq.pricing.detail.errors.loadFailed', 'Failed to load'))
|
|
96
96
|
} finally {
|
|
97
97
|
setLoading(false)
|
|
98
98
|
}
|
|
99
|
-
}, [id, isNew])
|
|
99
|
+
}, [id, isNew, t])
|
|
100
100
|
|
|
101
101
|
React.useEffect(() => {
|
|
102
102
|
void load()
|
|
@@ -123,22 +123,46 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
123
123
|
// Mirror V-PT-1 from validators.ts so the user gets feedback before the
|
|
124
124
|
// server rejects the payload.
|
|
125
125
|
const configValidationError = React.useMemo(() => {
|
|
126
|
-
if (!draft.code.trim()) return 'Code is required'
|
|
127
|
-
if (!draft.name.trim()) return 'Name is required'
|
|
128
|
-
if (draft.currencyCodeList.length === 0)
|
|
129
|
-
|
|
126
|
+
if (!draft.code.trim()) return t('cpq.pricing.validation.codeRequired', 'Code is required')
|
|
127
|
+
if (!draft.name.trim()) return t('cpq.pricing.validation.nameRequired', 'Name is required')
|
|
128
|
+
if (draft.currencyCodeList.length === 0) {
|
|
129
|
+
return t('cpq.pricing.validation.currencyRequired', 'At least one currency is required')
|
|
130
|
+
}
|
|
131
|
+
type ListMessages = {
|
|
132
|
+
keyRequired: (index: number) => string
|
|
133
|
+
labelRequired: (index: number) => string
|
|
134
|
+
duplicateKey: (key: string) => string
|
|
135
|
+
}
|
|
136
|
+
const checkList = (items: Array<{ key: string; label: string }>, messages: ListMessages): string | null => {
|
|
130
137
|
const seen = new Set<string>()
|
|
131
138
|
for (let i = 0; i < items.length; i++) {
|
|
132
139
|
const item = items[i]
|
|
133
|
-
if (!item.key.trim()) return
|
|
134
|
-
if (!item.label.trim()) return
|
|
135
|
-
if (seen.has(item.key)) return
|
|
140
|
+
if (!item.key.trim()) return messages.keyRequired(i + 1)
|
|
141
|
+
if (!item.label.trim()) return messages.labelRequired(i + 1)
|
|
142
|
+
if (seen.has(item.key)) return messages.duplicateKey(item.key)
|
|
136
143
|
seen.add(item.key)
|
|
137
144
|
}
|
|
138
145
|
return null
|
|
139
146
|
}
|
|
140
|
-
return
|
|
141
|
-
|
|
147
|
+
return (
|
|
148
|
+
checkList(draft.dimensions, {
|
|
149
|
+
keyRequired: (index) =>
|
|
150
|
+
t('cpq.pricing.validation.dimensionKeyRequired', 'Dimension #{index}: key is required', { index }),
|
|
151
|
+
labelRequired: (index) =>
|
|
152
|
+
t('cpq.pricing.validation.dimensionLabelRequired', 'Dimension #{index}: label is required', { index }),
|
|
153
|
+
duplicateKey: (key) =>
|
|
154
|
+
t('cpq.pricing.validation.dimensionDuplicateKey', 'Dimension: duplicate key "{key}"', { key }),
|
|
155
|
+
}) ??
|
|
156
|
+
checkList(draft.priceColumns, {
|
|
157
|
+
keyRequired: (index) =>
|
|
158
|
+
t('cpq.pricing.validation.priceColumnKeyRequired', 'Price Column #{index}: key is required', { index }),
|
|
159
|
+
labelRequired: (index) =>
|
|
160
|
+
t('cpq.pricing.validation.priceColumnLabelRequired', 'Price Column #{index}: label is required', { index }),
|
|
161
|
+
duplicateKey: (key) =>
|
|
162
|
+
t('cpq.pricing.validation.priceColumnDuplicateKey', 'Price Column: duplicate key "{key}"', { key }),
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
}, [draft, t])
|
|
142
166
|
|
|
143
167
|
const saveConfig = async () => {
|
|
144
168
|
if (configValidationError) {
|
|
@@ -163,7 +187,12 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
163
187
|
})
|
|
164
188
|
if (!res.ok) {
|
|
165
189
|
const body = await res.json().catch(() => ({}))
|
|
166
|
-
setError(
|
|
190
|
+
setError(
|
|
191
|
+
body.error ??
|
|
192
|
+
(isNew
|
|
193
|
+
? t('cpq.pricing.detail.errors.createFailed', 'Failed to create pricing table')
|
|
194
|
+
: t('cpq.pricing.detail.errors.saveFailed', 'Failed to save pricing table')),
|
|
195
|
+
)
|
|
167
196
|
return
|
|
168
197
|
}
|
|
169
198
|
const saved = await res.json()
|
|
@@ -175,7 +204,7 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
175
204
|
}
|
|
176
205
|
} catch (err) {
|
|
177
206
|
console.error('Failed to save', err)
|
|
178
|
-
setError('Failed to save')
|
|
207
|
+
setError(t('cpq.pricing.detail.errors.saveFailedGeneric', 'Failed to save'))
|
|
179
208
|
} finally {
|
|
180
209
|
setSaving(false)
|
|
181
210
|
}
|
|
@@ -193,6 +222,7 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
193
222
|
currencyCodeList: table.currencyCodeList,
|
|
194
223
|
},
|
|
195
224
|
entries,
|
|
225
|
+
t,
|
|
196
226
|
)
|
|
197
227
|
if (!r.ok) {
|
|
198
228
|
setError(r.error)
|
|
@@ -206,7 +236,7 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
206
236
|
setImportApplying(true)
|
|
207
237
|
setError(null)
|
|
208
238
|
try {
|
|
209
|
-
const r = await commitImportDiff(table.id, importDiff)
|
|
239
|
+
const r = await commitImportDiff(table.id, importDiff, t)
|
|
210
240
|
if (!r.ok) {
|
|
211
241
|
setError(r.error)
|
|
212
242
|
return
|
|
@@ -235,17 +265,17 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
235
265
|
router.push('/backend/cpq/pricing')
|
|
236
266
|
} else {
|
|
237
267
|
const body = await res.json().catch(() => ({}))
|
|
238
|
-
setError(body.error ?? 'Failed to delete')
|
|
268
|
+
setError(body.error ?? t('cpq.pricing.detail.errors.deleteFailed', 'Failed to delete'))
|
|
239
269
|
}
|
|
240
270
|
} catch {
|
|
241
|
-
setError('Failed to delete')
|
|
271
|
+
setError(t('cpq.pricing.detail.errors.deleteFailed', 'Failed to delete'))
|
|
242
272
|
}
|
|
243
273
|
}
|
|
244
274
|
|
|
245
275
|
// ─── Render ────────────────────────────────────────────────
|
|
246
276
|
|
|
247
277
|
if (loading) {
|
|
248
|
-
return <div className="text-sm text-muted-foreground">{t('common.loading', 'Loading...')}</div>
|
|
278
|
+
return <div className="text-sm text-muted-foreground">{t('cpq.common.loading', 'Loading...')}</div>
|
|
249
279
|
}
|
|
250
280
|
|
|
251
281
|
if (error && !isNew && !table.id) {
|
|
@@ -255,7 +285,7 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
255
285
|
onClick={() => router.push('/backend/cpq/pricing')}
|
|
256
286
|
className="text-sm text-muted-foreground hover:text-foreground"
|
|
257
287
|
>
|
|
258
|
-
← {t('common.back', 'Back')}
|
|
288
|
+
← {t('cpq.common.back', 'Back')}
|
|
259
289
|
</button>
|
|
260
290
|
<Alert variant="destructive">{error}</Alert>
|
|
261
291
|
</div>
|
|
@@ -274,14 +304,14 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
274
304
|
onClick={() => router.push('/backend/cpq/pricing')}
|
|
275
305
|
className="text-sm text-muted-foreground hover:text-foreground"
|
|
276
306
|
>
|
|
277
|
-
← {t('common.back', 'Back')}
|
|
307
|
+
← {t('cpq.common.back', 'Back')}
|
|
278
308
|
</button>
|
|
279
309
|
<h1 className="text-2xl font-bold">
|
|
280
310
|
{isNew ? t('cpq.pricing.new', 'New Pricing Table') : table.name}
|
|
281
311
|
</h1>
|
|
282
312
|
{!isNew && (
|
|
283
313
|
<Tag variant={table.isActive ? 'success' : 'neutral'} dot>
|
|
284
|
-
{table.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}
|
|
314
|
+
{table.isActive ? t('cpq.common.active', 'Active') : t('cpq.common.inactive', 'Inactive')}
|
|
285
315
|
</Tag>
|
|
286
316
|
)}
|
|
287
317
|
</div>
|
|
@@ -318,9 +348,9 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
318
348
|
e.target.value = ''
|
|
319
349
|
}}
|
|
320
350
|
/>
|
|
321
|
-
<Button type="button" onClick={startEdit}>{t('common.edit', 'Edit')}</Button>
|
|
351
|
+
<Button type="button" onClick={startEdit}>{t('cpq.common.edit', 'Edit')}</Button>
|
|
322
352
|
<Button type="button" variant="destructive" onClick={deleteTable}>
|
|
323
|
-
{t('common.delete', 'Delete')}
|
|
353
|
+
{t('cpq.common.delete', 'Delete')}
|
|
324
354
|
</Button>
|
|
325
355
|
</div>
|
|
326
356
|
)}
|
|
@@ -340,32 +370,32 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
340
370
|
|
|
341
371
|
{/* Configuration section */}
|
|
342
372
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
343
|
-
<h2 className="text-lg font-semibold">Configuration</h2>
|
|
373
|
+
<h2 className="text-lg font-semibold">{t('cpq.pricing.detail.configuration', 'Configuration')}</h2>
|
|
344
374
|
|
|
345
375
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
346
|
-
<ConfigField label=
|
|
376
|
+
<ConfigField label={t('cpq.pricing.code', 'Code')} required>
|
|
347
377
|
{editing ? (
|
|
348
378
|
<Input
|
|
349
379
|
value={view.code}
|
|
350
380
|
onChange={(e) => setDraft({ ...draft, code: e.target.value })}
|
|
351
|
-
placeholder=
|
|
381
|
+
placeholder={t('cpq.pricing.detail.codePlaceholder', 'e.g. cloud-connect-pricing')}
|
|
352
382
|
/>
|
|
353
383
|
) : (
|
|
354
384
|
<span className="font-mono text-sm">{view.code}</span>
|
|
355
385
|
)}
|
|
356
386
|
</ConfigField>
|
|
357
|
-
<ConfigField label=
|
|
387
|
+
<ConfigField label={t('cpq.pricing.name', 'Name')} required>
|
|
358
388
|
{editing ? (
|
|
359
389
|
<Input
|
|
360
390
|
value={view.name}
|
|
361
391
|
onChange={(e) => setDraft({ ...draft, name: e.target.value })}
|
|
362
|
-
placeholder=
|
|
392
|
+
placeholder={t('cpq.pricing.detail.namePlaceholder', 'Cloud Connect Pricing')}
|
|
363
393
|
/>
|
|
364
394
|
) : (
|
|
365
395
|
<span className="font-medium text-sm">{view.name}</span>
|
|
366
396
|
)}
|
|
367
397
|
</ConfigField>
|
|
368
|
-
<ConfigField label=
|
|
398
|
+
<ConfigField label={t('cpq.pricing.detail.currenciesCommaSeparated', 'Currencies (comma-separated)')} required>
|
|
369
399
|
{editing ? (
|
|
370
400
|
<Input
|
|
371
401
|
value={view.currencyCodeList.join(', ')}
|
|
@@ -378,13 +408,13 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
378
408
|
.filter(Boolean),
|
|
379
409
|
})
|
|
380
410
|
}
|
|
381
|
-
placeholder=
|
|
411
|
+
placeholder={t('cpq.pricing.detail.currenciesPlaceholder', 'USD, EUR')}
|
|
382
412
|
/>
|
|
383
413
|
) : (
|
|
384
414
|
<span className="text-sm">{view.currencyCodeList.join(', ') || '—'}</span>
|
|
385
415
|
)}
|
|
386
416
|
</ConfigField>
|
|
387
|
-
<ConfigField label=
|
|
417
|
+
<ConfigField label={t('cpq.common.active', 'Active')}>
|
|
388
418
|
{editing ? (
|
|
389
419
|
<label className="flex items-center gap-2 text-sm">
|
|
390
420
|
<input
|
|
@@ -393,25 +423,33 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
393
423
|
onChange={(e) => setDraft({ ...draft, isActive: e.target.checked })}
|
|
394
424
|
className="rounded border"
|
|
395
425
|
/>
|
|
396
|
-
{view.isActive ? 'Yes' : 'No'}
|
|
426
|
+
{view.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
|
|
397
427
|
</label>
|
|
398
428
|
) : (
|
|
399
|
-
<span className="text-sm">{view.isActive ? 'Yes' : 'No'}</span>
|
|
429
|
+
<span className="text-sm">{view.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}</span>
|
|
400
430
|
)}
|
|
401
431
|
</ConfigField>
|
|
402
432
|
</div>
|
|
403
433
|
|
|
404
434
|
<KeyLabelListEditor
|
|
405
|
-
title=
|
|
406
|
-
description=
|
|
435
|
+
title={t('cpq.pricing.detail.dimensions.title', 'Dimensions')}
|
|
436
|
+
description={t(
|
|
437
|
+
'cpq.pricing.detail.dimensions.description',
|
|
438
|
+
'Lookup keys for matching entries (e.g. region, tier). Leave empty if you use tier ranges only.',
|
|
439
|
+
)}
|
|
440
|
+
addLabel={t('cpq.pricing.detail.dimensions.add', '+ Add Dimension')}
|
|
407
441
|
items={view.dimensions}
|
|
408
442
|
editing={editing}
|
|
409
443
|
onChange={(dimensions) => setDraft({ ...draft, dimensions })}
|
|
410
444
|
/>
|
|
411
445
|
|
|
412
446
|
<KeyLabelListEditor
|
|
413
|
-
title=
|
|
414
|
-
description=
|
|
447
|
+
title={t('cpq.pricing.priceColumns', 'Price Columns')}
|
|
448
|
+
description={t(
|
|
449
|
+
'cpq.pricing.detail.priceColumns.description',
|
|
450
|
+
'Each price column becomes a numeric field on every entry. Charges reference one column by its key.',
|
|
451
|
+
)}
|
|
452
|
+
addLabel={t('cpq.pricing.detail.priceColumns.add', '+ Add Price Column')}
|
|
415
453
|
items={view.priceColumns}
|
|
416
454
|
editing={editing}
|
|
417
455
|
onChange={(priceColumns) => setDraft({ ...draft, priceColumns })}
|
|
@@ -428,10 +466,14 @@ export default function PricingTableDetailPage(props: { params?: { id?: string }
|
|
|
428
466
|
onClick={saveConfig}
|
|
429
467
|
disabled={saving || configValidationError !== null}
|
|
430
468
|
>
|
|
431
|
-
{saving
|
|
469
|
+
{saving
|
|
470
|
+
? t('cpq.common.saving', 'Saving...')
|
|
471
|
+
: isNew
|
|
472
|
+
? t('cpq.pricing.detail.create', 'Create Pricing Table')
|
|
473
|
+
: t('cpq.common.save', 'Save')}
|
|
432
474
|
</Button>
|
|
433
475
|
<Button type="button" variant="outline" onClick={cancelEdit} disabled={saving}>
|
|
434
|
-
Cancel
|
|
476
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
435
477
|
</Button>
|
|
436
478
|
</div>
|
|
437
479
|
</>
|
|
@@ -484,16 +526,20 @@ function ConfigField({
|
|
|
484
526
|
function KeyLabelListEditor({
|
|
485
527
|
title,
|
|
486
528
|
description,
|
|
529
|
+
addLabel,
|
|
487
530
|
items,
|
|
488
531
|
editing,
|
|
489
532
|
onChange,
|
|
490
533
|
}: {
|
|
491
534
|
title: string
|
|
492
535
|
description: string
|
|
536
|
+
/** Translated "+ Add …" button text, supplied by the caller. */
|
|
537
|
+
addLabel: string
|
|
493
538
|
items: Array<{ key: string; label: string }>
|
|
494
539
|
editing: boolean
|
|
495
540
|
onChange: (next: Array<{ key: string; label: string }>) => void
|
|
496
541
|
}) {
|
|
542
|
+
const t = useT()
|
|
497
543
|
return (
|
|
498
544
|
<div className="space-y-2">
|
|
499
545
|
<div>
|
|
@@ -511,12 +557,14 @@ function KeyLabelListEditor({
|
|
|
511
557
|
<thead>
|
|
512
558
|
<tr className="border-b bg-muted/30 text-xs">
|
|
513
559
|
<th className="px-3 py-2 text-left font-medium">
|
|
514
|
-
Key
|
|
560
|
+
{t('cpq.pricing.detail.keyLabelList.key', 'Key')}{' '}
|
|
561
|
+
{editing && <span className="text-destructive">*</span>}
|
|
515
562
|
</th>
|
|
516
563
|
<th className="px-3 py-2 text-left font-medium">
|
|
517
|
-
Label
|
|
564
|
+
{t('cpq.pricing.detail.keyLabelList.label', 'Label')}{' '}
|
|
565
|
+
{editing && <span className="text-destructive">*</span>}
|
|
518
566
|
</th>
|
|
519
|
-
{editing && <th className="px-3 py-2 w-8" aria-label=
|
|
567
|
+
{editing && <th className="px-3 py-2 w-8" aria-label={t('cpq.pricing.detail.keyLabelList.actions', 'Actions')} />}
|
|
520
568
|
</tr>
|
|
521
569
|
</thead>
|
|
522
570
|
<tbody>
|
|
@@ -531,7 +579,7 @@ function KeyLabelListEditor({
|
|
|
531
579
|
next[idx] = { ...item, key: e.target.value }
|
|
532
580
|
onChange(next)
|
|
533
581
|
}}
|
|
534
|
-
placeholder=
|
|
582
|
+
placeholder={t('cpq.pricing.detail.keyLabelList.keyPlaceholder', 'e.g. region')}
|
|
535
583
|
className="h-8"
|
|
536
584
|
/>
|
|
537
585
|
) : (
|
|
@@ -547,7 +595,7 @@ function KeyLabelListEditor({
|
|
|
547
595
|
next[idx] = { ...item, label: e.target.value }
|
|
548
596
|
onChange(next)
|
|
549
597
|
}}
|
|
550
|
-
placeholder=
|
|
598
|
+
placeholder={t('cpq.pricing.detail.keyLabelList.labelPlaceholder', 'Region')}
|
|
551
599
|
className="h-8"
|
|
552
600
|
/>
|
|
553
601
|
) : (
|
|
@@ -560,7 +608,7 @@ function KeyLabelListEditor({
|
|
|
560
608
|
type="button"
|
|
561
609
|
onClick={() => onChange(items.filter((_, i) => i !== idx))}
|
|
562
610
|
className="text-destructive hover:bg-destructive/10 rounded p-1"
|
|
563
|
-
aria-label=
|
|
611
|
+
aria-label={t('cpq.pricing.detail.keyLabelList.remove', 'Remove')}
|
|
564
612
|
>
|
|
565
613
|
<Trash2 className="h-4 w-4" />
|
|
566
614
|
</button>
|
|
@@ -580,7 +628,7 @@ function KeyLabelListEditor({
|
|
|
580
628
|
size="sm"
|
|
581
629
|
onClick={() => onChange([...items, { key: '', label: '' }])}
|
|
582
630
|
>
|
|
583
|
-
|
|
631
|
+
{addLabel}
|
|
584
632
|
</Button>
|
|
585
633
|
)}
|
|
586
634
|
</div>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// absent from the file are treated as DELETES.
|
|
8
8
|
|
|
9
9
|
import * as XLSX from 'xlsx'
|
|
10
|
+
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
10
11
|
|
|
11
12
|
type Dimension = { key: string; label: string }
|
|
12
13
|
type PriceColumn = { key: string; label: string }
|
|
@@ -54,17 +55,23 @@ export async function parsePricingTableXlsx(
|
|
|
54
55
|
file: File,
|
|
55
56
|
table: TableShape,
|
|
56
57
|
existingEntries: ImportEntry[],
|
|
58
|
+
t: TranslateFn,
|
|
57
59
|
): Promise<ImportResult> {
|
|
58
60
|
let workbook: XLSX.WorkBook
|
|
59
61
|
try {
|
|
60
62
|
const buffer = await file.arrayBuffer()
|
|
61
63
|
workbook = XLSX.read(buffer, { type: 'array' })
|
|
62
64
|
} catch (err) {
|
|
63
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: err instanceof Error
|
|
68
|
+
? t('cpq.pricing.import.errors.readFailedWithReason', 'Failed to read file: {reason}', { reason: err.message })
|
|
69
|
+
: t('cpq.pricing.import.errors.readFailed', 'Failed to read file'),
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
const sheetName = workbook.SheetNames[0]
|
|
67
|
-
if (!sheetName) return { ok: false, error: 'Workbook has no sheets' }
|
|
74
|
+
if (!sheetName) return { ok: false, error: t('cpq.pricing.import.errors.noSheets', 'Workbook has no sheets') }
|
|
68
75
|
const sheet = workbook.Sheets[sheetName]
|
|
69
76
|
|
|
70
77
|
// Two-row header strategy: row 1 is human labels (from export), row 2+ is
|
|
@@ -78,7 +85,7 @@ export async function parsePricingTableXlsx(
|
|
|
78
85
|
})
|
|
79
86
|
|
|
80
87
|
if (rows.length === 0) {
|
|
81
|
-
return { ok: false, error: 'Sheet has no data rows' }
|
|
88
|
+
return { ok: false, error: t('cpq.pricing.import.errors.noDataRows', 'Sheet has no data rows') }
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
// Build label→key map for dimensions and price columns so we can match
|
|
@@ -131,20 +138,20 @@ export async function parsePricingTableXlsx(
|
|
|
131
138
|
...priceHeaders.map((p) => p.header),
|
|
132
139
|
].filter((h): h is string => h !== null))
|
|
133
140
|
for (const h of headers) {
|
|
134
|
-
if (!knownHeaders.has(h)) warnings.push(
|
|
141
|
+
if (!knownHeaders.has(h)) warnings.push(t('cpq.pricing.import.warnings.unknownColumn', 'Ignored unknown column "{column}"', { column: h }))
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
// Missing required columns from the table → error.
|
|
138
145
|
const missingDims = table.dimensions.filter((d) => !dimensionHeaders.some((h) => h.key === d.key))
|
|
139
146
|
const missingPrices = table.priceColumns.filter((c) => !priceHeaders.some((h) => h.key === c.key))
|
|
140
147
|
if (missingDims.length > 0) {
|
|
141
|
-
return { ok: false, error:
|
|
148
|
+
return { ok: false, error: t('cpq.pricing.import.errors.missingDimensionColumns', 'Missing dimension column(s): {columns}', { columns: missingDims.map((d) => d.label).join(', ') }) }
|
|
142
149
|
}
|
|
143
150
|
if (missingPrices.length > 0) {
|
|
144
|
-
return { ok: false, error:
|
|
151
|
+
return { ok: false, error: t('cpq.pricing.import.errors.missingPriceColumns', 'Missing price column(s): {columns}', { columns: missingPrices.map((c) => c.label).join(', ') }) }
|
|
145
152
|
}
|
|
146
153
|
if (!currencyHeader) {
|
|
147
|
-
return { ok: false, error: 'Missing "currency_code" column' }
|
|
154
|
+
return { ok: false, error: t('cpq.pricing.import.errors.missingCurrencyColumn', 'Missing "currency_code" column') }
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
// ─── Parse rows ──────────────────────────────────────────────
|
|
@@ -161,15 +168,15 @@ export async function parsePricingTableXlsx(
|
|
|
161
168
|
const currency = toStringOrNull(raw[currencyHeader]) ?? ''
|
|
162
169
|
|
|
163
170
|
if (!currency) {
|
|
164
|
-
return { ok: false, error:
|
|
171
|
+
return { ok: false, error: t('cpq.pricing.import.errors.rowMissingCurrency', 'Row {row}: missing currency_code', { row: rowNumber }) }
|
|
165
172
|
}
|
|
166
173
|
if (!table.currencyCodeList.includes(currency.toUpperCase())) {
|
|
167
|
-
return { ok: false, error:
|
|
174
|
+
return { ok: false, error: t('cpq.pricing.import.errors.rowCurrencyNotAllowed', 'Row {row}: currency "{currency}" is not in the table\'s currency list ({currencies})', { row: rowNumber, currency, currencies: table.currencyCodeList.join(', ') }) }
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
const tierNumber = tierStr ? Number(tierStr) : null
|
|
171
178
|
if (tierNumber !== null && Number.isNaN(tierNumber)) {
|
|
172
|
-
return { ok: false, error:
|
|
179
|
+
return { ok: false, error: t('cpq.pricing.import.errors.rowTierNotNumber', 'Row {row}: tier_number must be a number, got "{value}"', { row: rowNumber, value: tierStr ?? '' }) }
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
const dimensionValues: Record<string, string> = {}
|
|
@@ -186,7 +193,7 @@ export async function parsePricingTableXlsx(
|
|
|
186
193
|
}
|
|
187
194
|
const num = Number(v)
|
|
188
195
|
if (Number.isNaN(num)) {
|
|
189
|
-
return { ok: false, error:
|
|
196
|
+
return { ok: false, error: t('cpq.pricing.import.errors.rowPriceNotNumber', 'Row {row}: price column "{column}" must be a number, got "{value}"', { row: rowNumber, column: header, value: String(v) }) }
|
|
190
197
|
}
|
|
191
198
|
prices[key] = num
|
|
192
199
|
}
|
|
@@ -219,7 +226,7 @@ export async function parsePricingTableXlsx(
|
|
|
219
226
|
}
|
|
220
227
|
const existing = existingById.get(row.id)
|
|
221
228
|
if (!existing) {
|
|
222
|
-
warnings.push(
|
|
229
|
+
warnings.push(t('cpq.pricing.import.warnings.idNotFound', 'Row {row}: id "{id}" not found in current entries — will be inserted as new', { row: row.rowNumber, id: row.id }))
|
|
223
230
|
inserts.push({ ...row, id: null })
|
|
224
231
|
continue
|
|
225
232
|
}
|
|
@@ -243,6 +250,7 @@ export async function parsePricingTableXlsx(
|
|
|
243
250
|
export async function commitImportDiff(
|
|
244
251
|
pricingTableId: string,
|
|
245
252
|
diff: ImportDiff,
|
|
253
|
+
t: TranslateFn,
|
|
246
254
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
247
255
|
// INSERTS (batch — route accepts arrays)
|
|
248
256
|
if (diff.inserts.length > 0) {
|
|
@@ -262,7 +270,7 @@ export async function commitImportDiff(
|
|
|
262
270
|
})
|
|
263
271
|
if (!res.ok) {
|
|
264
272
|
const body = await res.json().catch(() => ({}))
|
|
265
|
-
return { ok: false, error: body.error ?? 'Failed to insert entries' }
|
|
273
|
+
return { ok: false, error: body.error ?? t('cpq.pricing.import.errors.insertFailed', 'Failed to insert entries') }
|
|
266
274
|
}
|
|
267
275
|
}
|
|
268
276
|
|
|
@@ -283,7 +291,7 @@ export async function commitImportDiff(
|
|
|
283
291
|
})
|
|
284
292
|
if (!res.ok) {
|
|
285
293
|
const body = await res.json().catch(() => ({}))
|
|
286
|
-
return { ok: false, error: body.error ??
|
|
294
|
+
return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.updateFailed', 'Failed to update entry {id}', { id: before.id }) }
|
|
287
295
|
}
|
|
288
296
|
}
|
|
289
297
|
|
|
@@ -296,7 +304,7 @@ export async function commitImportDiff(
|
|
|
296
304
|
})
|
|
297
305
|
if (!res.ok) {
|
|
298
306
|
const body = await res.json().catch(() => ({}))
|
|
299
|
-
return { ok: false, error: body.error ??
|
|
307
|
+
return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.deleteFailed', 'Failed to delete entry {id}', { id: entry.id }) }
|
|
300
308
|
}
|
|
301
309
|
}
|
|
302
310
|
|
|
@@ -73,7 +73,7 @@ export default function PricingTablesPage() {
|
|
|
73
73
|
header: t('cpq.pricing.status', 'Status'),
|
|
74
74
|
cell: ({ row }) => (
|
|
75
75
|
<Tag variant={row.original.isActive ? 'success' : 'neutral'} dot>
|
|
76
|
-
{row.original.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}
|
|
76
|
+
{row.original.isActive ? t('cpq.common.active', 'Active') : t('cpq.common.inactive', 'Inactive')}
|
|
77
77
|
</Tag>
|
|
78
78
|
),
|
|
79
79
|
},
|