@dainamite/cpq 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +3 -9
  3. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
  4. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
  5. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
  6. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
  7. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
  8. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
  9. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
  10. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +2 -2
  11. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
  12. package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
  13. package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
  14. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +154 -178
  15. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  16. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
  17. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
  18. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
  19. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
  20. package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
  21. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
  22. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +69 -66
  23. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  24. package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
  25. package/dist/modules/cpq/backend/cpq/orders/page.js.map +2 -2
  26. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +41 -36
  27. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
  28. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
  29. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
  30. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
  31. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
  32. package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
  33. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
  34. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
  35. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
  36. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
  37. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
  38. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
  39. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
  40. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
  41. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
  42. package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
  43. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
  44. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
  45. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
  46. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +6 -3
  47. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +2 -2
  48. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +129 -94
  49. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +2 -2
  50. package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
  51. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
  52. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +19 -15
  53. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
  54. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
  55. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
  56. package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
  57. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
  58. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
  59. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
  60. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
  61. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
  62. package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
  63. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
  64. package/dist/modules/cpq/components/ArcLineDiff.js +2 -2
  65. package/dist/modules/cpq/components/ArcLineDiff.js.map +2 -2
  66. package/dist/modules/cpq/components/ChargeBreakdownTable.js +4 -10
  67. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +2 -2
  68. package/dist/modules/cpq/components/PriceWaterfall.js +3 -1
  69. package/dist/modules/cpq/components/PriceWaterfall.js.map +2 -2
  70. package/dist/modules/cpq/components/PricingSummaryCard.js +1 -6
  71. package/dist/modules/cpq/components/PricingSummaryCard.js.map +2 -2
  72. package/dist/modules/cpq/components/StatusPath.js +5 -3
  73. package/dist/modules/cpq/components/StatusPath.js.map +2 -2
  74. package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
  75. package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
  76. package/dist/modules/cpq/i18n/de.json +1220 -0
  77. package/dist/modules/cpq/i18n/en.json +1220 -0
  78. package/dist/modules/cpq/i18n/es.json +1220 -0
  79. package/dist/modules/cpq/i18n/pl.json +1220 -0
  80. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
  81. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
  82. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
  83. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
  84. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
  85. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
  86. package/dist/modules/cpq/widgets/injection-table.js +2 -2
  87. package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
  88. package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
  89. package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
  90. package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
  91. package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
  92. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
  93. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
  94. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
  95. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
  96. package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
  97. package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
  98. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
  99. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
  100. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
  101. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
  102. package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
  103. package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
  104. package/dist/modules/cpq/workflows/steps/index.js +12 -0
  105. package/dist/modules/cpq/workflows/steps/index.js.map +2 -2
  106. package/package.json +1 -1
  107. package/src/modules/cpq/__tests__/i18n/dictionaries.test.ts +62 -0
  108. package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
  109. package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -0
  110. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +3 -9
  111. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
  112. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
  113. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
  114. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +2 -2
  115. package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
  116. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
  117. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +134 -112
  118. package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
  119. package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
  120. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +66 -43
  121. package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
  122. package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
  123. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
  124. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
  125. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
  126. package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
  127. package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
  128. package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
  129. package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
  130. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  131. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
  132. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +3 -3
  133. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +134 -89
  134. package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
  135. package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
  136. package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
  137. package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
  138. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
  139. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
  140. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  141. package/src/modules/cpq/components/ArcLineDiff.tsx +2 -2
  142. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +4 -2
  143. package/src/modules/cpq/components/PriceWaterfall.tsx +4 -2
  144. package/src/modules/cpq/components/PricingSummaryCard.tsx +1 -1
  145. package/src/modules/cpq/components/StatusPath.tsx +7 -4
  146. package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
  147. package/src/modules/cpq/i18n/de.json +1220 -0
  148. package/src/modules/cpq/i18n/en.json +1220 -0
  149. package/src/modules/cpq/i18n/es.json +1220 -0
  150. package/src/modules/cpq/i18n/pl.json +1220 -0
  151. package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
  152. package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
  153. package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
  154. package/src/modules/cpq/widgets/injection-table.ts +8 -2
  155. package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
  156. package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
  157. package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
  158. package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
  159. package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
  160. package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
  161. package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
  162. package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
  163. package/src/modules/cpq/workflows/steps/index.ts +12 -0
  164. package/src/modules/cpq/workflows/types.ts +4 -0
@@ -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) return 'At least one currency is required'
129
- const checkList = (items: Array<{ key: string; label: string }>, label: string): string | null => {
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 `${label} #${i + 1}: key is required`
134
- if (!item.label.trim()) return `${label} #${i + 1}: label is required`
135
- if (seen.has(item.key)) return `${label}: duplicate key "${item.key}"`
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 checkList(draft.dimensions, 'Dimension') ?? checkList(draft.priceColumns, 'Price Column')
141
- }, [draft])
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(body.error ?? `Failed to ${isNew ? 'create' : 'save'} pricing table`)
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="Code" required>
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="e.g. cloud-connect-pricing"
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="Name" required>
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="Cloud Connect Pricing"
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="Currencies (comma-separated)" required>
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="USD, EUR"
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="Active">
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="Dimensions"
406
- description="Lookup keys for matching entries (e.g. region, tier). Leave empty if you use tier ranges only."
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="Price Columns"
414
- description="Each price column becomes a numeric field on every entry. Charges reference one column by its key."
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 ? 'Saving...' : (isNew ? 'Create Pricing Table' : 'Save')}
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 {editing && <span className="text-destructive">*</span>}
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 {editing && <span className="text-destructive">*</span>}
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="Actions" />}
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="e.g. region"
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="Region"
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="Remove"
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
- + Add {title.slice(0, -1)}
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 { ok: false, error: err instanceof Error ? `Failed to read file: ${err.message}` : 'Failed to read file' }
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(`Ignored unknown column "${h}"`)
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: `Missing dimension column(s): ${missingDims.map((d) => d.label).join(', ')}` }
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: `Missing price column(s): ${missingPrices.map((c) => c.label).join(', ')}` }
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: `Row ${rowNumber}: missing currency_code` }
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: `Row ${rowNumber}: currency "${currency}" is not in the table's currency list (${table.currencyCodeList.join(', ')})` }
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: `Row ${rowNumber}: tier_number must be a number, got "${tierStr}"` }
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: `Row ${rowNumber}: price column "${header}" must be a number, got "${String(v)}"` }
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(`Row ${row.rowNumber}: id "${row.id}" not found in current entries — will be inserted as new`)
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 ?? `Failed to update entry ${before.id}` }
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 ?? `Failed to delete entry ${entry.id}` }
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
  },