@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
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
3
  import { Alert } from '@open-mercato/ui/primitives/alert'
4
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
4
5
  import { NumberInput as BaseNumberInput } from '../../../../../components/NumberInput'
5
6
 
6
7
  // XD-250 — ARC configurator drawer.
@@ -59,19 +60,32 @@ export interface ArcQuoteConfiguratorProps {
59
60
  onChanged: () => void | Promise<void>
60
61
  }
61
62
 
62
- const REASON_CODES = [
63
- { value: 'upgrade', label: 'Upgrade' },
64
- { value: 'downgrade', label: 'Downgrade' },
65
- { value: 'config-change', label: 'Configuration change' },
66
- { value: 'price-adjustment', label: 'Price adjustment' },
67
- { value: 'term-extension', label: 'Term extension' },
68
- { value: 'term-reduction', label: 'Term reduction' },
69
- { value: 'consolidation', label: 'Consolidation' },
70
- { value: 'customer-request', label: 'Customer request' },
71
- { value: 'non-payment', label: 'Non-payment' },
72
- { value: 'contract-breach', label: 'Contract breach' },
73
- { value: 'other', label: 'Other' },
74
- ] as const
63
+ function buildReasonCodes(t: TranslateFn): Array<{ value: string; label: string }> {
64
+ return [
65
+ { value: 'upgrade', label: t('cpq.quotes.arc.drawer.reasons.upgrade', 'Upgrade') },
66
+ { value: 'downgrade', label: t('cpq.quotes.arc.drawer.reasons.downgrade', 'Downgrade') },
67
+ { value: 'config-change', label: t('cpq.quotes.arc.drawer.reasons.configChange', 'Configuration change') },
68
+ { value: 'price-adjustment', label: t('cpq.quotes.arc.drawer.reasons.priceAdjustment', 'Price adjustment') },
69
+ { value: 'term-extension', label: t('cpq.quotes.arc.drawer.reasons.termExtension', 'Term extension') },
70
+ { value: 'term-reduction', label: t('cpq.quotes.arc.drawer.reasons.termReduction', 'Term reduction') },
71
+ { value: 'consolidation', label: t('cpq.quotes.arc.drawer.reasons.consolidation', 'Consolidation') },
72
+ { value: 'customer-request', label: t('cpq.quotes.arc.drawer.reasons.customerRequest', 'Customer request') },
73
+ { value: 'non-payment', label: t('cpq.quotes.arc.drawer.reasons.nonPayment', 'Non-payment') },
74
+ { value: 'contract-breach', label: t('cpq.quotes.arc.drawer.reasons.contractBreach', 'Contract breach') },
75
+ { value: 'other', label: t('cpq.quotes.arc.drawer.reasons.other', 'Other') },
76
+ ]
77
+ }
78
+
79
+ function arcTypeLabel(t: TranslateFn, type: ArcType): string {
80
+ switch (type) {
81
+ case 'amend':
82
+ return t('cpq.quotes.type.amend', 'amend')
83
+ case 'renew':
84
+ return t('cpq.quotes.type.renew', 'renew')
85
+ case 'cancel':
86
+ return t('cpq.quotes.type.cancel', 'cancel')
87
+ }
88
+ }
75
89
 
76
90
  export default function ArcQuoteConfigurator(
77
91
  props: ArcQuoteConfiguratorProps,
@@ -88,6 +102,8 @@ export default function ArcQuoteConfigurator(
88
102
  onChanged,
89
103
  } = props
90
104
 
105
+ const t = useT()
106
+ const reasonCodes = React.useMemo(() => buildReasonCodes(t), [t])
91
107
  const [step, setStep] = React.useState<Step>(
92
108
  initialType === 'new' ? 'pick-action' : 'pick-targets',
93
109
  )
@@ -98,17 +114,17 @@ export default function ArcQuoteConfigurator(
98
114
  const [eligibleLoading, setEligibleLoading] = React.useState(false)
99
115
  const [picked, setPicked] = React.useState<Record<string, EligibleSub>>(() => {
100
116
  const seed: Record<string, EligibleSub> = {}
101
- for (const t of initialTargets) {
102
- if (t.subscription) {
103
- seed[t.subscriptionId] = {
104
- id: t.subscriptionId,
105
- code: t.subscription.code,
106
- name: t.subscription.name,
117
+ for (const target of initialTargets) {
118
+ if (target.subscription) {
119
+ seed[target.subscriptionId] = {
120
+ id: target.subscriptionId,
121
+ code: target.subscription.code,
122
+ name: target.subscription.name,
107
123
  status: 'active',
108
- currencyCode: t.subscription.currencyCode,
109
- billingCycle: t.subscription.billingCycle,
124
+ currencyCode: target.subscription.currencyCode,
125
+ billingCycle: target.subscription.billingCycle,
110
126
  currentTermEnd: null,
111
- mrcAmount: t.subscription.mrcAmount,
127
+ mrcAmount: target.subscription.mrcAmount,
112
128
  }
113
129
  }
114
130
  }
@@ -189,16 +205,16 @@ export default function ArcQuoteConfigurator(
189
205
  })
190
206
  if (!r.ok) {
191
207
  const body = await r.json().catch(() => ({}))
192
- throw new Error(body.error ?? `PATCH /quotes/${quoteId} failed`)
208
+ throw new Error(body.error ?? t('cpq.quotes.arc.drawer.errors.patchFailed', 'PATCH /quotes/{quoteId} failed', { quoteId }))
193
209
  }
194
210
  }
195
211
 
196
212
  // Step 2 — detach removed targets.
197
- const initialTargetIds = new Set(initialTargets.map((t) => t.subscriptionId))
213
+ const initialTargetIds = new Set(initialTargets.map((target) => target.subscriptionId))
198
214
  const pickedIds = new Set(pickedList.map((s) => s.id))
199
- for (const t of initialTargets) {
200
- if (!pickedIds.has(t.subscriptionId)) {
201
- await fetch(`/api/cpq/quotes/${quoteId}/target-subscriptions/${t.id}`, { method: 'DELETE' })
215
+ for (const target of initialTargets) {
216
+ if (!pickedIds.has(target.subscriptionId)) {
217
+ await fetch(`/api/cpq/quotes/${quoteId}/target-subscriptions/${target.id}`, { method: 'DELETE' })
202
218
  }
203
219
  }
204
220
 
@@ -224,7 +240,7 @@ export default function ArcQuoteConfigurator(
224
240
  })
225
241
  if (!r.ok) {
226
242
  const errBody = await r.json().catch(() => ({}))
227
- throw new Error(errBody.error ?? `POST /target-subscriptions failed`)
243
+ throw new Error(errBody.error ?? t('cpq.quotes.arc.drawer.errors.attachFailed', 'POST /target-subscriptions failed'))
228
244
  }
229
245
  }
230
246
  }
@@ -245,7 +261,7 @@ export default function ArcQuoteConfigurator(
245
261
  })
246
262
  if (!r.ok) {
247
263
  const errBody = await r.json().catch(() => ({}))
248
- throw new Error(errBody.error ?? `POST /merge-meta failed`)
264
+ throw new Error(errBody.error ?? t('cpq.quotes.arc.drawer.errors.mergeMetaFailed', 'POST /merge-meta failed'))
249
265
  }
250
266
  }
251
267
 
@@ -262,7 +278,7 @@ export default function ArcQuoteConfigurator(
262
278
  })
263
279
  if (!r.ok) {
264
280
  const errBody = await r.json().catch(() => ({}))
265
- throw new Error(errBody.error ?? `POST /cancel-meta failed`)
281
+ throw new Error(errBody.error ?? t('cpq.quotes.arc.drawer.errors.cancelMetaFailed', 'POST /cancel-meta failed'))
266
282
  }
267
283
  }
268
284
 
@@ -283,13 +299,14 @@ export default function ArcQuoteConfigurator(
283
299
  mergeTerm,
284
300
  cancelMeta,
285
301
  onChanged,
302
+ t,
286
303
  ])
287
304
 
288
305
  return (
289
306
  <div className="fixed inset-0 z-50 flex" role="dialog" aria-modal="true">
290
307
  {/* Overlay */}
291
308
  <button
292
- aria-label="Close drawer"
309
+ aria-label={t('cpq.quotes.arc.drawer.close', 'Close drawer')}
293
310
  className="absolute inset-0 bg-black/40"
294
311
  onClick={onClose}
295
312
  />
@@ -297,7 +314,7 @@ export default function ArcQuoteConfigurator(
297
314
  <div className="ml-auto h-full w-full max-w-2xl bg-card shadow-xl border-l overflow-y-auto relative flex flex-col">
298
315
  <header className="border-b px-5 py-4 flex items-start justify-between">
299
316
  <div>
300
- <h2 className="text-lg font-semibold">Modify subscription</h2>
317
+ <h2 className="text-lg font-semibold">{t('cpq.quotes.arc.drawer.title', 'Modify subscription')}</h2>
301
318
  <p className="text-xs text-muted-foreground mt-0.5">
302
319
  {customerName ?? customerId.slice(0, 12) + '…'} • {currencyCode}
303
320
  </p>
@@ -313,14 +330,14 @@ export default function ArcQuoteConfigurator(
313
330
  <div className="flex-1 px-5 py-4 space-y-4">
314
331
  {/* Step indicator */}
315
332
  <p className="text-xs text-muted-foreground">
316
- Step: <span className="font-mono">{step}</span>
317
- {type && <span> • {type}</span>}
318
- {pickedList.length > 0 && <span> • {pickedList.length} target(s)</span>}
333
+ {t('cpq.quotes.arc.drawer.stepLabel', 'Step:')} <span className="font-mono">{step}</span>
334
+ {type && <span> • {arcTypeLabel(t, type)}</span>}
335
+ {pickedList.length > 0 && <span> • {t('cpq.quotes.arc.drawer.targetCount', '{count} target(s)', { count: pickedList.length })}</span>}
319
336
  </p>
320
337
 
321
338
  {step === 'pick-action' && (
322
339
  <div className="space-y-3">
323
- <p className="text-sm">Pick the kind of change you want to make:</p>
340
+ <p className="text-sm">{t('cpq.quotes.arc.drawer.pickAction', 'Pick the kind of change you want to make:')}</p>
324
341
  {(['amend', 'renew', 'cancel'] as const).map((opt) => (
325
342
  <button
326
343
  key={opt}
@@ -330,11 +347,11 @@ export default function ArcQuoteConfigurator(
330
347
  }}
331
348
  className="block w-full text-left rounded-md border border-input px-4 py-3 hover:bg-muted"
332
349
  >
333
- <span className="font-medium capitalize">{opt}</span>
350
+ <span className="font-medium capitalize">{arcTypeLabel(t, opt)}</span>
334
351
  <span className="block text-xs text-muted-foreground mt-0.5">
335
- {opt === 'amend' && 'Add or remove items on existing subscriptions. Term untouched.'}
336
- {opt === 'renew' && 'Extend term. With 2+ targets, merges them into a new contract.'}
337
- {opt === 'cancel' && 'Terminate one or more subscriptions.'}
352
+ {opt === 'amend' && t('cpq.quotes.arc.drawer.actions.amendHint', 'Add or remove items on existing subscriptions. Term untouched.')}
353
+ {opt === 'renew' && t('cpq.quotes.arc.drawer.actions.renewHint', 'Extend term. With 2+ targets, merges them into a new contract.')}
354
+ {opt === 'cancel' && t('cpq.quotes.arc.drawer.actions.cancelHint', 'Terminate one or more subscriptions.')}
338
355
  </span>
339
356
  </button>
340
357
  ))}
@@ -344,9 +361,9 @@ export default function ArcQuoteConfigurator(
344
361
  {step === 'pick-targets' && type && (
345
362
  <div className="space-y-3">
346
363
  {eligibleLoading ? (
347
- <div className="text-sm text-muted-foreground">Loading subscriptions…</div>
364
+ <div className="text-sm text-muted-foreground">{t('cpq.quotes.arc.drawer.loadingSubscriptions', 'Loading subscriptions…')}</div>
348
365
  ) : eligible.length === 0 ? (
349
- <div className="text-sm text-muted-foreground">No subscriptions for this customer.</div>
366
+ <div className="text-sm text-muted-foreground">{t('cpq.quotes.arc.drawer.noSubscriptions', 'No subscriptions for this customer.')}</div>
350
367
  ) : (
351
368
  <div className="space-y-1">
352
369
  {eligible.map((s) => {
@@ -382,14 +399,15 @@ export default function ArcQuoteConfigurator(
382
399
  <span className="text-xs font-mono text-muted-foreground">{s.code}</span>
383
400
  </div>
384
401
  <div className="text-xs text-muted-foreground">
385
- {s.status} • {s.billingCycle} • MRC {s.mrcAmount.toFixed(2)} {s.currencyCode}
402
+ {s.status} • {s.billingCycle} • {t('cpq.lines.mrc', 'MRC')} {s.mrcAmount.toFixed(2)} {s.currencyCode}
403
+ {s.currentTermEnd ? ' • ' : ''}
386
404
  {s.currentTermEnd
387
- ? ` • Term ends ${new Date(s.currentTermEnd).toLocaleDateString()}`
405
+ ? t('cpq.quotes.arc.drawer.termEnds', 'Term ends {date}', { date: new Date(s.currentTermEnd).toLocaleDateString() })
388
406
  : ''}
389
407
  </div>
390
408
  {currencyMismatch && (
391
409
  <div className="text-xs text-status-warning-text mt-1">
392
- Currency mismatch ({s.currencyCode} vs {currencyCode}) — cannot include
410
+ {t('cpq.quotes.arc.drawer.currencyMismatch', 'Currency mismatch ({subscriptionCurrency} vs {quoteCurrency}) — cannot include', { subscriptionCurrency: s.currencyCode, quoteCurrency: currencyCode })}
393
411
  </div>
394
412
  )}
395
413
  </div>
@@ -400,12 +418,12 @@ export default function ArcQuoteConfigurator(
400
418
  )}
401
419
  {!cyclesMatch && (
402
420
  <Alert variant="warning" className="text-xs">
403
- Picked targets have different billing cycles — merge requires matching cycles.
421
+ {t('cpq.quotes.arc.drawer.cyclesMismatch', 'Picked targets have different billing cycles — merge requires matching cycles.')}
404
422
  </Alert>
405
423
  )}
406
424
  {isMergeRenew && (
407
425
  <Alert variant="warning" className="text-xs">
408
- Merging {pickedList.length} contracts into a new one — sources will be marked Merged at activation.
426
+ {t('cpq.quotes.arc.drawer.mergeWarning', 'Merging {count} contracts into a new one — sources will be marked Merged at activation.', { count: pickedList.length })}
409
427
  </Alert>
410
428
  )}
411
429
  </div>
@@ -413,15 +431,15 @@ export default function ArcQuoteConfigurator(
413
431
 
414
432
  {step === 'config-renew' && type === 'renew' && !isMergeRenew && (
415
433
  <div className="space-y-3">
416
- <p className="text-sm">New contract term for {pickedList[0]?.name}:</p>
434
+ <p className="text-sm">{t('cpq.quotes.arc.drawer.renew.singleTitle', 'New contract term for {name}:', { name: pickedList[0]?.name ?? '' })}</p>
417
435
  <p className="text-xs text-muted-foreground">
418
- Pre-filled with same length as the current term, starting at the
419
- current term end. Type a new value in <span className="font-medium">Months</span> to
420
- auto-shift the end date, or edit start/end directly.
436
+ {t('cpq.quotes.arc.drawer.renew.singleHintBefore', 'Pre-filled with same length as the current term, starting at the current term end. Type a new value in')}{' '}
437
+ <span className="font-medium">{t('cpq.quotes.arc.drawer.fields.months', 'Months')}</span>{' '}
438
+ {t('cpq.quotes.arc.drawer.renew.singleHintAfter', 'to auto-shift the end date, or edit start/end directly.')}
421
439
  </p>
422
440
  <div className="grid grid-cols-3 gap-3">
423
441
  <DateInput
424
- label="Start"
442
+ label={t('cpq.quotes.arc.drawer.fields.start', 'Start')}
425
443
  value={singleTerm.start}
426
444
  onChange={(v) => {
427
445
  // If months is set, slide end too so the contract length stays.
@@ -431,12 +449,12 @@ export default function ArcQuoteConfigurator(
431
449
  }}
432
450
  />
433
451
  <DateInput
434
- label="End"
452
+ label={t('cpq.quotes.arc.drawer.fields.end', 'End')}
435
453
  value={singleTerm.end}
436
454
  onChange={(v) => setSingleTerm({ ...singleTerm, end: v })}
437
455
  />
438
456
  <NumberInput
439
- label="Months"
457
+ label={t('cpq.quotes.arc.drawer.fields.months', 'Months')}
440
458
  value={singleTerm.months}
441
459
  onChange={(v) => {
442
460
  // Recompute end = start + months when both are present.
@@ -454,15 +472,16 @@ export default function ArcQuoteConfigurator(
454
472
  {step === 'config-renew' && type === 'renew' && isMergeRenew && (
455
473
  <div className="space-y-3">
456
474
  <p className="text-sm">
457
- New contract details (merge of {pickedList.length} subscriptions):
475
+ {t('cpq.quotes.arc.drawer.renew.mergeTitle', 'New contract details (merge of {count} subscriptions):', { count: pickedList.length })}
458
476
  </p>
459
477
  <p className="text-xs text-muted-foreground">
460
- Type a value into <span className="font-medium">Months</span> to
461
- auto-fill both dates — start defaults to today, end is start + months.
478
+ {t('cpq.quotes.arc.drawer.renew.mergeHintBefore', 'Type a value into')}{' '}
479
+ <span className="font-medium">{t('cpq.quotes.arc.drawer.fields.months', 'Months')}</span>{' '}
480
+ {t('cpq.quotes.arc.drawer.renew.mergeHintAfter', 'to auto-fill both dates — start defaults to today, end is start + months.')}
462
481
  </p>
463
482
  <div className="grid grid-cols-3 gap-3">
464
483
  <DateInput
465
- label="Start"
484
+ label={t('cpq.quotes.arc.drawer.fields.start', 'Start')}
466
485
  value={mergeTerm.start}
467
486
  onChange={(v) => {
468
487
  const months = mergeTerm.months ? Number(mergeTerm.months) : null
@@ -471,12 +490,12 @@ export default function ArcQuoteConfigurator(
471
490
  }}
472
491
  />
473
492
  <DateInput
474
- label="End"
493
+ label={t('cpq.quotes.arc.drawer.fields.end', 'End')}
475
494
  value={mergeTerm.end}
476
495
  onChange={(v) => setMergeTerm({ ...mergeTerm, end: v })}
477
496
  />
478
497
  <NumberInput
479
- label="Months"
498
+ label={t('cpq.quotes.arc.drawer.fields.months', 'Months')}
480
499
  value={mergeTerm.months}
481
500
  onChange={(v) => {
482
501
  const months = v ? Number(v) : null
@@ -488,12 +507,12 @@ export default function ArcQuoteConfigurator(
488
507
  </div>
489
508
  <div className="grid grid-cols-2 gap-3">
490
509
  <TextInput
491
- label="New contract code (optional — auto-generated if blank)"
510
+ label={t('cpq.quotes.arc.drawer.fields.newCode', 'New contract code (optional — auto-generated if blank)')}
492
511
  value={mergeTerm.code}
493
512
  onChange={(v) => setMergeTerm({ ...mergeTerm, code: v })}
494
513
  />
495
514
  <TextInput
496
- label="New contract name (optional)"
515
+ label={t('cpq.quotes.arc.drawer.fields.newName', 'New contract name (optional)')}
497
516
  value={mergeTerm.name}
498
517
  onChange={(v) => setMergeTerm({ ...mergeTerm, name: v })}
499
518
  />
@@ -503,16 +522,16 @@ export default function ArcQuoteConfigurator(
503
522
 
504
523
  {step === 'config-cancel' && type === 'cancel' && (
505
524
  <div className="space-y-3">
506
- <p className="text-sm">Reason for cancellation:</p>
525
+ <p className="text-sm">{t('cpq.quotes.arc.drawer.cancel.title', 'Reason for cancellation:')}</p>
507
526
  <div className="space-y-2">
508
- <label className="block text-xs text-muted-foreground">Reason code</label>
527
+ <label className="block text-xs text-muted-foreground">{t('cpq.quotes.arc.drawer.cancel.reasonCode', 'Reason code')}</label>
509
528
  <select
510
529
  value={cancelMeta.reasonCode}
511
530
  onChange={(e) => setCancelMeta({ ...cancelMeta, reasonCode: e.target.value })}
512
531
  className="w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm"
513
532
  >
514
- <option value="">— pick one —</option>
515
- {REASON_CODES.map((r) => (
533
+ <option value="">{t('cpq.quotes.arc.drawer.cancel.pickOne', '— pick one —')}</option>
534
+ {reasonCodes.map((r) => (
516
535
  <option key={r.value} value={r.value}>
517
536
  {r.label}
518
537
  </option>
@@ -520,18 +539,18 @@ export default function ArcQuoteConfigurator(
520
539
  </select>
521
540
  </div>
522
541
  <TextInput
523
- label="Free text (optional)"
542
+ label={t('cpq.quotes.arc.drawer.cancel.freeText', 'Free text (optional)')}
524
543
  value={cancelMeta.reasonText}
525
544
  onChange={(v) => setCancelMeta({ ...cancelMeta, reasonText: v })}
526
545
  />
527
546
  <div className="grid grid-cols-2 gap-3">
528
547
  <TextInput
529
- label="ETF amount (optional)"
548
+ label={t('cpq.quotes.arc.drawer.cancel.etfAmount', 'ETF amount (optional)')}
530
549
  value={cancelMeta.etfAmount}
531
550
  onChange={(v) => setCancelMeta({ ...cancelMeta, etfAmount: v })}
532
551
  />
533
552
  <TextInput
534
- label="ETF currency"
553
+ label={t('cpq.quotes.arc.drawer.cancel.etfCurrency', 'ETF currency')}
535
554
  value={cancelMeta.etfCurrency}
536
555
  onChange={(v) => setCancelMeta({ ...cancelMeta, etfCurrency: v })}
537
556
  />
@@ -541,10 +560,10 @@ export default function ArcQuoteConfigurator(
541
560
 
542
561
  {step === 'review' && (
543
562
  <div className="space-y-3">
544
- <p className="text-sm">Review and submit:</p>
563
+ <p className="text-sm">{t('cpq.quotes.arc.drawer.review.title', 'Review and submit:')}</p>
545
564
  <ul className="text-sm space-y-1 list-disc pl-5">
546
- <li>Action: {type}</li>
547
- <li>Targets: {pickedList.length}</li>
565
+ <li>{t('cpq.quotes.arc.drawer.review.action', 'Action: {type}', { type: type ? arcTypeLabel(t, type) : '' })}</li>
566
+ <li>{t('cpq.quotes.arc.drawer.review.targets', 'Targets: {count}', { count: pickedList.length })}</li>
548
567
  {pickedList.map((s) => (
549
568
  <li key={s.id} className="text-muted-foreground">
550
569
  {s.code} — {s.name}
@@ -552,28 +571,32 @@ export default function ArcQuoteConfigurator(
552
571
  ))}
553
572
  {type === 'renew' && !isMergeRenew && (
554
573
  <li>
555
- Term: {singleTerm.start} → {singleTerm.end}
556
- {singleTerm.months ? ` (${singleTerm.months}m)` : ''}
574
+ {singleTerm.months
575
+ ? t('cpq.quotes.arc.drawer.review.termWithMonths', 'Term: {start} → {end} ({months}m)', { start: singleTerm.start, end: singleTerm.end, months: singleTerm.months })
576
+ : t('cpq.quotes.arc.drawer.review.term', 'Term: {start} → {end}', { start: singleTerm.start, end: singleTerm.end })}
557
577
  </li>
558
578
  )}
559
579
  {type === 'renew' && isMergeRenew && (
560
580
  <li>
561
- Merge term: {mergeTerm.start} → {mergeTerm.end}
562
- {mergeTerm.code ? ` • code=${mergeTerm.code}` : ''}
563
- {mergeTerm.name ? ` • name=${mergeTerm.name}` : ''}
581
+ {t('cpq.quotes.arc.drawer.review.mergeTerm', 'Merge term: {start} → {end}', { start: mergeTerm.start, end: mergeTerm.end })}
582
+ {mergeTerm.code ? ' • ' : ''}
583
+ {mergeTerm.code ? t('cpq.quotes.arc.drawer.review.mergeCode', 'code={code}', { code: mergeTerm.code }) : ''}
584
+ {mergeTerm.name ? ' • ' : ''}
585
+ {mergeTerm.name ? t('cpq.quotes.arc.drawer.review.mergeName', 'name={name}', { name: mergeTerm.name }) : ''}
564
586
  </li>
565
587
  )}
566
588
  {type === 'cancel' && (
567
589
  <li>
568
- Reason: {cancelMeta.reasonCode}
590
+ {t('cpq.quotes.arc.drawer.review.reason', 'Reason: {reason}', { reason: cancelMeta.reasonCode })}
591
+ {cancelMeta.etfAmount ? ' • ' : ''}
569
592
  {cancelMeta.etfAmount
570
- ? ` • ETF ${cancelMeta.etfAmount} ${cancelMeta.etfCurrency}`
593
+ ? t('cpq.quotes.arc.drawer.review.etf', 'ETF {amount} {currency}', { amount: cancelMeta.etfAmount, currency: cancelMeta.etfCurrency })
571
594
  : ''}
572
595
  </li>
573
596
  )}
574
597
  </ul>
575
598
  <Alert variant="warning" className="text-xs">
576
- Submitting locks the quote type. Lines will be edited on the host page after this completes.
599
+ {t('cpq.quotes.arc.drawer.review.lockWarning', 'Submitting locks the quote type. Lines will be edited on the host page after this completes.')}
577
600
  </Alert>
578
601
  {submitError && (
579
602
  <Alert variant="destructive" className="text-xs">{submitError}</Alert>
@@ -582,12 +605,12 @@ export default function ArcQuoteConfigurator(
582
605
  )}
583
606
 
584
607
  {step === 'submitting' && (
585
- <div className="text-sm text-muted-foreground">Submitting…</div>
608
+ <div className="text-sm text-muted-foreground">{t('cpq.quotes.arc.drawer.submitting', 'Submitting…')}</div>
586
609
  )}
587
610
 
588
611
  {step === 'done' && (
589
612
  <div className="text-sm text-status-success-text">
590
- Done — close to return to the quote.
613
+ {t('cpq.quotes.arc.drawer.done', 'Done — close to return to the quote.')}
591
614
  </div>
592
615
  )}
593
616
  </div>
@@ -614,14 +637,14 @@ export default function ArcQuoteConfigurator(
614
637
  }
615
638
  className="text-sm text-muted-foreground hover:text-foreground disabled:opacity-30"
616
639
  >
617
- ← Back
640
+ {t('cpq.quotes.arc.drawer.back', '← Back')}
618
641
  </button>
619
642
  <div className="flex gap-2">
620
643
  <button
621
644
  onClick={onClose}
622
645
  className="rounded-md border border-input px-3 py-1.5 text-sm hover:bg-muted"
623
646
  >
624
- Cancel
647
+ {t('cpq.common.cancel', 'Cancel')}
625
648
  </button>
626
649
  {step === 'pick-targets' && (
627
650
  <button
@@ -633,7 +656,7 @@ export default function ArcQuoteConfigurator(
633
656
  }}
634
657
  className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
635
658
  >
636
- Next
659
+ {t('cpq.quotes.arc.drawer.next', 'Next')}
637
660
  </button>
638
661
  )}
639
662
  {(step === 'config-renew' || step === 'config-cancel') && (
@@ -645,7 +668,7 @@ export default function ArcQuoteConfigurator(
645
668
  onClick={() => setStep('review')}
646
669
  className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
647
670
  >
648
- Next
671
+ {t('cpq.quotes.arc.drawer.next', 'Next')}
649
672
  </button>
650
673
  )}
651
674
  {step === 'review' && (
@@ -654,7 +677,7 @@ export default function ArcQuoteConfigurator(
654
677
  onClick={submit}
655
678
  className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
656
679
  >
657
- Submit
680
+ {t('cpq.quotes.arc.drawer.submit', 'Submit')}
658
681
  </button>
659
682
  )}
660
683
  {step === 'done' && (
@@ -662,7 +685,7 @@ export default function ArcQuoteConfigurator(
662
685
  onClick={onClose}
663
686
  className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90"
664
687
  >
665
- Close
688
+ {t('cpq.quotes.arc.drawer.closeButton', 'Close')}
666
689
  </button>
667
690
  )}
668
691
  </div>
@@ -228,8 +228,8 @@ export function PriceAdjustmentDialog<Q extends PreviewQuote>(props: PriceAdjust
228
228
  )}
229
229
  {draftKey && !previewing && !previewCharge && previewLine && (
230
230
  <span>
231
- {t('cpq.priceAdjustments.dialog.previewLine', 'Line totals')}: MRC <span className="font-mono font-medium">{money(previewLine.mrcTotal, currency)}</span>
232
- {' · '}NRC <span className="font-mono font-medium">{money(previewLine.nrcTotal, currency)}</span>
231
+ {t('cpq.priceAdjustments.dialog.previewLine', 'Line totals')}: {t('cpq.lines.mrc', 'MRC')} <span className="font-mono font-medium">{money(previewLine.mrcTotal, currency)}</span>
232
+ {' · '}{t('cpq.lines.nrc', 'NRC')} <span className="font-mono font-medium">{money(previewLine.nrcTotal, currency)}</span>
233
233
  </span>
234
234
  )}
235
235
  </div>
@@ -250,7 +250,7 @@ export function PriceAdjustmentDialog<Q extends PreviewQuote>(props: PriceAdjust
250
250
 
251
251
  <DialogFooter>
252
252
  <Button variant="outline" onClick={onClose}>{t('cpq.priceAdjustments.dialog.cancel', 'Cancel')}</Button>
253
- <Button onClick={() => void save()} disabled={!canSave} title="Ctrl/Cmd+Enter">
253
+ <Button onClick={() => void save()} disabled={!canSave} title={t('cpq.priceAdjustments.dialog.saveShortcut', 'Ctrl/Cmd+Enter')}>
254
254
  {saving ? t('cpq.priceAdjustments.dialog.saving', 'Saving…') : t('cpq.priceAdjustments.dialog.save', 'Save')}
255
255
  </Button>
256
256
  </DialogFooter>