@dainamite/cpq 0.5.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 (368) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/MIGRATION.md +74 -0
  3. package/dist/modules/cpq/acl.js +1 -0
  4. package/dist/modules/cpq/acl.js.map +2 -2
  5. package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js +45 -0
  6. package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js.map +7 -0
  7. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
  8. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
  9. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
  10. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
  11. package/dist/modules/cpq/api/orders/[orderId]/route.js +17 -1
  12. package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
  13. package/dist/modules/cpq/api/orders/route.js +47 -2
  14. package/dist/modules/cpq/api/orders/route.js.map +2 -2
  15. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
  16. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
  17. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +42 -2
  18. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
  19. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
  20. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
  21. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
  22. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
  23. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
  24. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
  25. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
  26. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
  27. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +32 -2
  28. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
  29. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
  30. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
  31. package/dist/modules/cpq/api/quotes/price/route.js +43 -3
  32. package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
  33. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +3 -9
  34. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
  35. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
  36. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
  37. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
  38. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
  39. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
  40. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
  41. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +3 -3
  42. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
  43. package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
  44. package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
  45. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +174 -184
  46. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  47. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
  48. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
  49. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
  50. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
  51. package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
  52. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
  53. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +140 -280
  54. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  55. package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
  56. package/dist/modules/cpq/backend/cpq/orders/page.js.map +2 -2
  57. package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
  58. package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
  59. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +41 -36
  60. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
  61. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
  62. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
  63. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
  64. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
  65. package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
  66. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
  67. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
  68. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
  69. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
  70. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
  71. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
  72. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
  73. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
  74. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
  75. package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
  76. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
  77. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
  78. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
  79. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +208 -0
  80. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
  81. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
  82. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
  83. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
  84. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
  85. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
  86. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
  87. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
  88. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
  89. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +269 -311
  90. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
  91. package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
  92. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
  93. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
  94. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
  95. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +19 -15
  96. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
  97. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
  98. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
  99. package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
  100. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
  101. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
  102. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
  103. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
  104. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
  105. package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
  106. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
  107. package/dist/modules/cpq/ce.js +8 -0
  108. package/dist/modules/cpq/ce.js.map +2 -2
  109. package/dist/modules/cpq/cli.js +34 -1
  110. package/dist/modules/cpq/cli.js.map +2 -2
  111. package/dist/modules/cpq/commands/price-adjustments.js +107 -0
  112. package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
  113. package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
  114. package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
  115. package/dist/modules/cpq/components/ChargeBreakdownTable.js +148 -0
  116. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
  117. package/dist/modules/cpq/components/PriceWaterfall.js +68 -0
  118. package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
  119. package/dist/modules/cpq/components/PricingSummaryCard.js +112 -0
  120. package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
  121. package/dist/modules/cpq/components/StatusPath.js +5 -3
  122. package/dist/modules/cpq/components/StatusPath.js.map +2 -2
  123. package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
  124. package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
  125. package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
  126. package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
  127. package/dist/modules/cpq/data/entities.js +97 -0
  128. package/dist/modules/cpq/data/entities.js.map +2 -2
  129. package/dist/modules/cpq/data/price-adjustments.js +70 -0
  130. package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
  131. package/dist/modules/cpq/data/validators.js +4 -0
  132. package/dist/modules/cpq/data/validators.js.map +2 -2
  133. package/dist/modules/cpq/di.js +6 -0
  134. package/dist/modules/cpq/di.js.map +2 -2
  135. package/dist/modules/cpq/events.js +47 -0
  136. package/dist/modules/cpq/events.js.map +2 -2
  137. package/dist/modules/cpq/i18n/de.json +1220 -0
  138. package/dist/modules/cpq/i18n/en.json +1220 -0
  139. package/dist/modules/cpq/i18n/es.json +1220 -0
  140. package/dist/modules/cpq/i18n/pl.json +1220 -0
  141. package/dist/modules/cpq/index.js +1 -1
  142. package/dist/modules/cpq/index.js.map +2 -2
  143. package/dist/modules/cpq/lib/auditActor.js +10 -0
  144. package/dist/modules/cpq/lib/auditActor.js.map +7 -0
  145. package/dist/modules/cpq/lib/formatMoney.js +7 -0
  146. package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
  147. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
  148. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
  149. package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
  150. package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
  151. package/dist/modules/cpq/lib/quotePermissions.js +13 -0
  152. package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
  153. package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
  154. package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
  155. package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
  156. package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
  157. package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
  158. package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
  159. package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
  160. package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
  161. package/dist/modules/cpq/services/cpqOrderService.js +25 -28
  162. package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
  163. package/dist/modules/cpq/services/cpqPricingService.js +7 -0
  164. package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
  165. package/dist/modules/cpq/services/cpqQuotingService.js +330 -109
  166. package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
  167. package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
  168. package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
  169. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
  170. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
  171. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
  172. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
  173. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
  174. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
  175. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
  176. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
  177. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
  178. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
  179. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
  180. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
  181. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
  182. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
  183. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
  184. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
  185. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
  186. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
  187. package/dist/modules/cpq/services/types.js.map +2 -2
  188. package/dist/modules/cpq/setup.js +6 -0
  189. package/dist/modules/cpq/setup.js.map +2 -2
  190. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
  191. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
  192. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
  193. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
  194. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
  195. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
  196. package/dist/modules/cpq/widgets/injection-table.js +2 -2
  197. package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
  198. package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
  199. package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
  200. package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
  201. package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
  202. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
  203. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
  204. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
  205. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
  206. package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
  207. package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
  208. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
  209. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
  210. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
  211. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
  212. package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
  213. package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
  214. package/dist/modules/cpq/workflows/steps/index.js +12 -0
  215. package/dist/modules/cpq/workflows/steps/index.js.map +2 -2
  216. package/package.json +1 -1
  217. package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
  218. package/src/modules/cpq/__tests__/i18n/dictionaries.test.ts +62 -0
  219. package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
  220. package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -0
  221. package/src/modules/cpq/acl.ts +1 -0
  222. package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
  223. package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
  224. package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
  225. package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
  226. package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
  227. package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
  228. package/src/modules/cpq/api/orders/[orderId]/route.ts +18 -0
  229. package/src/modules/cpq/api/orders/route.ts +51 -1
  230. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
  231. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +48 -1
  232. package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
  233. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
  234. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
  235. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
  236. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
  237. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +40 -1
  238. package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
  239. package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
  240. package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
  241. package/src/modules/cpq/api/quotes/price/route.ts +28 -2
  242. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +3 -9
  243. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
  244. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
  245. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
  246. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +3 -3
  247. package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
  248. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
  249. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +164 -120
  250. package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
  251. package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
  252. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +122 -308
  253. package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
  254. package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
  255. package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
  256. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
  257. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
  258. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
  259. package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
  260. package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
  261. package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
  262. package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
  263. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  264. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
  265. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
  266. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
  267. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
  268. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
  269. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
  270. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
  271. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +304 -348
  272. package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
  273. package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
  274. package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
  275. package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
  276. package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
  277. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
  278. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
  279. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  280. package/src/modules/cpq/ce.ts +8 -0
  281. package/src/modules/cpq/cli.ts +48 -1
  282. package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
  283. package/src/modules/cpq/commands/price-adjustments.ts +90 -0
  284. package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
  285. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +203 -0
  286. package/src/modules/cpq/components/PriceWaterfall.tsx +96 -0
  287. package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
  288. package/src/modules/cpq/components/StatusPath.tsx +7 -4
  289. package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
  290. package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
  291. package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
  292. package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
  293. package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
  294. package/src/modules/cpq/data/entities.ts +83 -0
  295. package/src/modules/cpq/data/price-adjustments.ts +67 -0
  296. package/src/modules/cpq/data/validators.ts +3 -0
  297. package/src/modules/cpq/di.ts +6 -0
  298. package/src/modules/cpq/events.ts +53 -2
  299. package/src/modules/cpq/i18n/de.json +1220 -0
  300. package/src/modules/cpq/i18n/en.json +1220 -0
  301. package/src/modules/cpq/i18n/es.json +1220 -0
  302. package/src/modules/cpq/i18n/pl.json +1220 -0
  303. package/src/modules/cpq/index.ts +1 -1
  304. package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
  305. package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
  306. package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
  307. package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
  308. package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
  309. package/src/modules/cpq/lib/auditActor.ts +13 -0
  310. package/src/modules/cpq/lib/formatMoney.ts +4 -0
  311. package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
  312. package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
  313. package/src/modules/cpq/lib/quotePermissions.ts +22 -0
  314. package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
  315. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
  316. package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
  317. package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
  318. package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
  319. package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
  320. package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
  321. package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
  322. package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
  323. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
  324. package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
  325. package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
  326. package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
  327. package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
  328. package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
  329. package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
  330. package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
  331. package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
  332. package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
  333. package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
  334. package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
  335. package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
  336. package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
  337. package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
  338. package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
  339. package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
  340. package/src/modules/cpq/services/cpqOrderService.ts +22 -28
  341. package/src/modules/cpq/services/cpqPricingService.ts +8 -0
  342. package/src/modules/cpq/services/cpqQuotingService.ts +374 -125
  343. package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
  344. package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
  345. package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
  346. package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
  347. package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
  348. package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
  349. package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
  350. package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
  351. package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
  352. package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
  353. package/src/modules/cpq/services/types.ts +34 -1
  354. package/src/modules/cpq/setup.ts +7 -1
  355. package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
  356. package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
  357. package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
  358. package/src/modules/cpq/widgets/injection-table.ts +8 -2
  359. package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
  360. package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
  361. package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
  362. package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
  363. package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
  364. package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
  365. package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
  366. package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
  367. package/src/modules/cpq/workflows/steps/index.ts +12 -0
  368. package/src/modules/cpq/workflows/types.ts +4 -0
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
- import { useT } from '@open-mercato/shared/lib/i18n/context'
3
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
4
4
  import { useParams, useRouter } from 'next/navigation'
5
5
  import { Alert } from '@open-mercato/ui/primitives/alert'
6
6
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -153,16 +153,47 @@ const EMPTY_CHARGE: EditingCharge = {
153
153
 
154
154
  // XD-297: the charge "shape" is two axes — Charge Model (how quantity drives
155
155
  // the charge) and Pricing Method (where the unit price comes from).
156
- const CHARGE_MODEL_LABELS: Record<string, string> = {
157
- flat: 'Flat Fee',
158
- per_unit: 'Per Unit',
159
- volume: 'Volume-based',
160
- tiered: 'Tiered',
156
+ function chargeModelLabel(t: TranslateFn, model: string): string {
157
+ switch (model) {
158
+ case 'flat':
159
+ return t('cpq.offerings.chargeModel.flat', 'Flat Fee')
160
+ case 'per_unit':
161
+ return t('cpq.offerings.chargeModel.perUnit', 'Per Unit')
162
+ case 'volume':
163
+ return t('cpq.offerings.chargeModel.volume', 'Volume-based')
164
+ case 'tiered':
165
+ return t('cpq.offerings.chargeModel.tiered', 'Tiered')
166
+ default:
167
+ return model
168
+ }
161
169
  }
162
170
 
163
- const PRICING_SOURCE_LABELS: Record<string, string> = {
164
- fixed: 'Fixed Price',
165
- table: 'Table Lookup',
171
+ function pricingSourceLabel(t: TranslateFn, source: string): string {
172
+ switch (source) {
173
+ case 'fixed':
174
+ return t('cpq.offerings.pricingMethod.fixed', 'Fixed Price')
175
+ case 'table':
176
+ return t('cpq.offerings.pricingMethod.table', 'Table Lookup')
177
+ default:
178
+ return source
179
+ }
180
+ }
181
+
182
+ type ChargeMissingField = 'fixedPrice' | 'currency' | 'quantityAttribute' | 'pricingTable' | 'priceColumn'
183
+
184
+ function chargeMissingFieldLabel(t: TranslateFn, field: ChargeMissingField): string {
185
+ switch (field) {
186
+ case 'fixedPrice':
187
+ return t('cpq.offerings.charge.fields.fixedPrice', 'Fixed Price')
188
+ case 'currency':
189
+ return t('cpq.offerings.charge.fields.currency', 'Currency')
190
+ case 'quantityAttribute':
191
+ return t('cpq.offerings.charge.fields.quantityAttribute', 'Quantity Attribute')
192
+ case 'pricingTable':
193
+ return t('cpq.offerings.charge.fields.pricingTable', 'Pricing Table')
194
+ case 'priceColumn':
195
+ return t('cpq.offerings.charge.fields.priceColumn', 'Price Column')
196
+ }
166
197
  }
167
198
 
168
199
  // V-CHG-1 in `validators.ts`: mirror the (chargeModel × pricingMethod) rules in
@@ -181,17 +212,17 @@ function isChargeComplete(charge: ChargeShape): boolean {
181
212
  return chargeMissingFields(charge).length === 0
182
213
  }
183
214
 
184
- function chargeMissingFields(charge: ChargeShape): string[] {
215
+ function chargeMissingFields(charge: ChargeShape): ChargeMissingField[] {
185
216
  const { model, source } = normalizeChargePricing(charge)
186
- const missing: string[] = []
217
+ const missing: ChargeMissingField[] = []
187
218
  if (source === 'fixed') {
188
- if (!charge.fixedPrice) missing.push('Fixed Price')
189
- if (!charge.currencyCode) missing.push('Currency')
190
- if (model === 'per_unit' && !charge.quantityAttributeCode) missing.push('Quantity Attribute')
219
+ if (!charge.fixedPrice) missing.push('fixedPrice')
220
+ if (!charge.currencyCode) missing.push('currency')
221
+ if (model === 'per_unit' && !charge.quantityAttributeCode) missing.push('quantityAttribute')
191
222
  } else {
192
- if (!charge.pricingTableId) missing.push('Pricing Table')
193
- if (!charge.priceColumnKey) missing.push('Price Column')
194
- if (model !== 'flat' && !charge.quantityAttributeCode) missing.push('Quantity Attribute')
223
+ if (!charge.pricingTableId) missing.push('pricingTable')
224
+ if (!charge.priceColumnKey) missing.push('priceColumn')
225
+ if (model !== 'flat' && !charge.quantityAttributeCode) missing.push('quantityAttribute')
195
226
  }
196
227
  return missing
197
228
  }
@@ -251,7 +282,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
251
282
  // Load offering detail
252
283
  const res = await fetch(`/api/cpq/product-offerings?id=${encodeURIComponent(offeringId)}`)
253
284
  if (!res.ok) {
254
- if (!cancelled) setError('Failed to load offering')
285
+ if (!cancelled) setError(t('cpq.offerings.detail.error.load', 'Failed to load offering'))
255
286
  return
256
287
  }
257
288
 
@@ -301,7 +332,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
301
332
  }
302
333
  } catch (err) {
303
334
  console.error('Failed to load', err)
304
- if (!cancelled) setError('Failed to load')
335
+ if (!cancelled) setError(t('cpq.offerings.detail.error.loadGeneric', 'Failed to load'))
305
336
  } finally {
306
337
  if (!cancelled) setLoading(false)
307
338
  }
@@ -343,7 +374,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
343
374
 
344
375
  if (!res.ok) {
345
376
  const d = await res.json()
346
- setError(d.error ?? 'Failed to save')
377
+ setError(d.error ?? t('cpq.offerings.detail.error.save', 'Failed to save'))
347
378
  return
348
379
  }
349
380
 
@@ -369,7 +400,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
369
400
  setGeneralMode('view')
370
401
  }
371
402
  } catch {
372
- setError('Failed to save')
403
+ setError(t('cpq.offerings.detail.error.save', 'Failed to save'))
373
404
  } finally {
374
405
  setSaving(false)
375
406
  }
@@ -387,10 +418,10 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
387
418
  router.push('/backend/cpq/offerings')
388
419
  } else {
389
420
  const d = await res.json()
390
- setError(d.error ?? 'Failed to delete')
421
+ setError(d.error ?? t('cpq.offerings.detail.error.delete', 'Failed to delete'))
391
422
  }
392
423
  } catch {
393
- setError('Failed to delete')
424
+ setError(t('cpq.offerings.detail.error.delete', 'Failed to delete'))
394
425
  }
395
426
  }
396
427
 
@@ -430,7 +461,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
430
461
  })
431
462
  if (!res.ok) {
432
463
  const d = await res.json()
433
- setError(d.error ?? 'Failed to save charge')
464
+ setError(d.error ?? t('cpq.offerings.detail.error.saveCharge', 'Failed to save charge'))
434
465
  return
435
466
  }
436
467
  // Reload offering to get updated charges
@@ -441,14 +472,14 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
441
472
  }
442
473
  setEditingCharge(null)
443
474
  } catch {
444
- setError('Failed to save charge')
475
+ setError(t('cpq.offerings.detail.error.saveCharge', 'Failed to save charge'))
445
476
  } finally {
446
477
  setSaving(false)
447
478
  }
448
479
  }
449
480
 
450
481
  const deleteCharge = async (chargeId: string) => {
451
- if (!confirm('Delete this charge?')) return
482
+ if (!confirm(t('cpq.offerings.detail.confirmDeleteCharge', 'Delete this charge?'))) return
452
483
  try {
453
484
  const res = await fetch('/api/cpq/product-charges', {
454
485
  method: 'DELETE',
@@ -457,7 +488,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
457
488
  })
458
489
  if (res.ok) setCharges((prev) => prev.filter((c) => c.id !== chargeId))
459
490
  } catch {
460
- setError('Failed to delete charge')
491
+ setError(t('cpq.offerings.detail.error.deleteCharge', 'Failed to delete charge'))
461
492
  }
462
493
  }
463
494
 
@@ -472,7 +503,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
472
503
  // ─── Render ─────────────────────────────────────────────────
473
504
 
474
505
  if (loading) {
475
- return <div className="text-sm text-muted-foreground">{t('common.loading', 'Loading...')}</div>
506
+ return <div className="text-sm text-muted-foreground">{t('cpq.common.loading', 'Loading...')}</div>
476
507
  }
477
508
 
478
509
  return (
@@ -484,7 +515,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
484
515
  onClick={() => router.push('/backend/cpq/offerings')}
485
516
  className="text-sm text-muted-foreground hover:text-foreground"
486
517
  >
487
- ← {t('common.back', 'Back')}
518
+ ← {t('cpq.common.back', 'Back')}
488
519
  </button>
489
520
  <h1 className="text-2xl font-bold">
490
521
  {isNew ? t('cpq.offerings.new', 'New Offering') : form.name}
@@ -496,7 +527,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
496
527
  )}
497
528
  {specDetail && (
498
529
  <span className="text-sm text-muted-foreground">
499
- Spec: <button onClick={() => router.push(`/backend/cpq/specifications/${specDetail.id}`)} className="text-primary hover:underline">{specDetail.name}</button>
530
+ {t('cpq.offerings.detail.specLabel', 'Spec:')} <button onClick={() => router.push(`/backend/cpq/specifications/${specDetail.id}`)} className="text-primary hover:underline">{specDetail.name}</button>
500
531
  </span>
501
532
  )}
502
533
  </div>
@@ -511,11 +542,11 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
511
542
  setGeneralMode('edit')
512
543
  }}
513
544
  >
514
- {t('common.edit', 'Edit')}
545
+ {t('cpq.common.edit', 'Edit')}
515
546
  </Button>
516
547
  )}
517
548
  <Button type="button" variant="destructive" onClick={deleteOffering}>
518
- {t('common.delete', 'Delete')}
549
+ {t('cpq.common.delete', 'Delete')}
519
550
  </Button>
520
551
  </div>
521
552
  )}
@@ -540,9 +571,9 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
540
571
  tab === t_ ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground'
541
572
  }`}
542
573
  >
543
- {t_ === 'general' && 'General'}
544
- {t_ === 'charges' && `Charges (${charges.length})`}
545
- {t_ === 'components' && `Components (${components.length})`}
574
+ {t_ === 'general' && t('cpq.offerings.detail.tabs.general', 'General')}
575
+ {t_ === 'charges' && t('cpq.offerings.detail.tabs.charges', 'Charges ({count})', { count: charges.length })}
576
+ {t_ === 'components' && t('cpq.offerings.detail.tabs.components', 'Components ({count})', { count: components.length })}
546
577
  </button>
547
578
  ))}
548
579
  </div>
@@ -553,7 +584,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
553
584
  <div className="rounded-lg border bg-card p-6 space-y-4">
554
585
  {isNew && (
555
586
  <div>
556
- <label className="block text-sm font-medium mb-1">Specification</label>
587
+ <label className="block text-sm font-medium mb-1">{t('cpq.offerings.detail.specification', 'Specification')}</label>
557
588
  <select
558
589
  value={form.specId}
559
590
  onChange={(e) => {
@@ -563,7 +594,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
563
594
  }}
564
595
  className="w-full rounded-md border px-3 py-2 text-sm"
565
596
  >
566
- <option value="">Select a specification...</option>
597
+ <option value="">{t('cpq.offerings.detail.selectSpecification', 'Select a specification...')}</option>
567
598
  {specs.map((s) => (
568
599
  <option key={s.id} value={s.id}>{s.name} ({s.code})</option>
569
600
  ))}
@@ -581,7 +612,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
581
612
  type="text"
582
613
  value={form.code}
583
614
  onChange={(e) => setForm({ ...form, code: e.target.value })}
584
- placeholder="e.g. gix-cloud-connect-aws"
615
+ placeholder={t('cpq.offerings.detail.codePlaceholder', 'e.g. gix-cloud-connect-aws')}
585
616
  className="w-full rounded-md border px-3 py-2 text-sm"
586
617
  />
587
618
  )}
@@ -613,9 +644,9 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
613
644
  onChange={(e) => setForm({ ...form, lifecycleStatus: e.target.value })}
614
645
  className="w-full rounded-md border px-3 py-2 text-sm"
615
646
  >
616
- <option value="draft">Draft</option>
617
- <option value="active">Active</option>
618
- <option value="deprecated">Deprecated</option>
647
+ <option value="draft">{t('cpq.offerings.lifecycle.draft', 'Draft')}</option>
648
+ <option value="active">{t('cpq.offerings.lifecycle.active', 'Active')}</option>
649
+ <option value="deprecated">{t('cpq.offerings.lifecycle.deprecated', 'Deprecated')}</option>
619
650
  </select>
620
651
  )}
621
652
  </div>
@@ -624,7 +655,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
624
655
  <div className="flex items-center h-[38px]">
625
656
  {generalMode === 'view' && !isNew ? (
626
657
  <Tag variant={form.isActive ? 'success' : 'neutral'} dot>
627
- {form.isActive ? t('common.yes', 'Yes') : t('common.no', 'No')}
658
+ {form.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
628
659
  </Tag>
629
660
  ) : (
630
661
  <input
@@ -654,7 +685,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
654
685
  {/* Design-time values */}
655
686
  {designTimeAttrs.length > 0 && (
656
687
  <div className="space-y-3">
657
- <label className="block text-sm font-medium">Design-Time Values</label>
688
+ <label className="block text-sm font-medium">{t('cpq.offerings.detail.designTimeValues', 'Design-Time Values')}</label>
658
689
  <div className="grid grid-cols-2 gap-3">
659
690
  {designTimeAttrs.map((attr) => {
660
691
  const value = designTimeValues[attr.code]
@@ -677,7 +708,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
677
708
  <span className="text-muted-foreground">—</span>
678
709
  ) : attr.attributeType === 'boolean' ? (
679
710
  <Tag variant={value ? 'success' : 'neutral'} dot>
680
- {value ? t('common.yes', 'Yes') : t('common.no', 'No')}
711
+ {value ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
681
712
  </Tag>
682
713
  ) : (
683
714
  optionLabel ?? String(value)
@@ -689,7 +720,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
689
720
  onChange={(e) => setDesignTimeValues({ ...designTimeValues, [attr.code]: e.target.value || null })}
690
721
  className="w-full rounded-md border px-2 py-1.5 text-sm"
691
722
  >
692
- <option value="">Select {attr.name}...</option>
723
+ <option value="">{t('cpq.offerings.detail.selectAttribute', 'Select {name}...', { name: attr.name })}</option>
693
724
  {attr.options.map((opt) => (
694
725
  <option key={opt.value} value={opt.value}>{opt.label}</option>
695
726
  ))}
@@ -738,7 +769,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
738
769
  onClick={saveOffering}
739
770
  disabled={saving || !form.code || !form.name || (isNew && !form.specId)}
740
771
  >
741
- {saving ? t('common.saving', 'Saving...') : t('common.save', 'Save')}
772
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
742
773
  </Button>
743
774
  <Button
744
775
  type="button"
@@ -755,7 +786,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
755
786
  setGeneralMode('view')
756
787
  }}
757
788
  >
758
- {t('common.cancel', 'Cancel')}
789
+ {t('cpq.common.cancel', 'Cancel')}
759
790
  </Button>
760
791
  </div>
761
792
  )}
@@ -778,27 +809,27 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
778
809
  {editingCharge && (
779
810
  <div className="rounded-lg border bg-card p-4 space-y-3">
780
811
  <h3 className="font-medium text-sm">
781
- {editingCharge.id ? 'Edit Charge' : 'New Charge'}
812
+ {editingCharge.id ? t('cpq.offerings.charge.form.editTitle', 'Edit Charge') : t('cpq.offerings.charge.form.newTitle', 'New Charge')}
782
813
  </h3>
783
814
  <div className="grid grid-cols-3 gap-3">
784
815
  <div>
785
- <label className="block text-xs font-medium mb-1">Code</label>
786
- <input type="text" value={editingCharge.code} onChange={(e) => setEditingCharge({ ...editingCharge, code: e.target.value })} placeholder="e.g. mrc-monthly" className="w-full rounded-md border px-2 py-1.5 text-sm" />
816
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.code', 'Code')}</label>
817
+ <input type="text" value={editingCharge.code} onChange={(e) => setEditingCharge({ ...editingCharge, code: e.target.value })} placeholder={t('cpq.offerings.charge.fields.codePlaceholder', 'e.g. mrc-monthly')} className="w-full rounded-md border px-2 py-1.5 text-sm" />
787
818
  </div>
788
819
  <div>
789
- <label className="block text-xs font-medium mb-1">Name</label>
820
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.name', 'Name')}</label>
790
821
  <input type="text" value={editingCharge.name} onChange={(e) => setEditingCharge({ ...editingCharge, name: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
791
822
  </div>
792
823
  <div>
793
- <label className="block text-xs font-medium mb-1">Charge Type</label>
824
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.chargeType', 'Charge Type')}</label>
794
825
  <select value={editingCharge.chargeType} onChange={(e) => setEditingCharge({ ...editingCharge, chargeType: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
795
- <option value="mrc">MRC (Monthly Recurring)</option>
796
- <option value="nrc">NRC (Non-Recurring)</option>
797
- <option value="usage">Usage</option>
826
+ <option value="mrc">{t('cpq.offerings.chargeType.mrc', 'MRC (Monthly Recurring)')}</option>
827
+ <option value="nrc">{t('cpq.offerings.chargeType.nrc', 'NRC (Non-Recurring)')}</option>
828
+ <option value="usage">{t('cpq.offerings.chargeType.usage', 'Usage')}</option>
798
829
  </select>
799
830
  </div>
800
831
  <div>
801
- <label className="block text-xs font-medium mb-1">Charge Model</label>
832
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.chargeModel', 'Charge Model')}</label>
802
833
  <select
803
834
  value={editingCharge.chargeModel ?? 'flat'}
804
835
  onChange={(e) => {
@@ -815,14 +846,14 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
815
846
  }}
816
847
  className="w-full rounded-md border px-2 py-1.5 text-sm"
817
848
  >
818
- <option value="flat">Flat Fee</option>
819
- <option value="per_unit">Per Unit</option>
820
- <option value="volume">Volume-based</option>
821
- <option value="tiered">Tiered</option>
849
+ <option value="flat">{chargeModelLabel(t, 'flat')}</option>
850
+ <option value="per_unit">{chargeModelLabel(t, 'per_unit')}</option>
851
+ <option value="volume">{chargeModelLabel(t, 'volume')}</option>
852
+ <option value="tiered">{chargeModelLabel(t, 'tiered')}</option>
822
853
  </select>
823
854
  </div>
824
855
  <div>
825
- <label className="block text-xs font-medium mb-1">Pricing Method</label>
856
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.pricingMethod', 'Pricing Method')}</label>
826
857
  <select
827
858
  value={editingCharge.pricingMethod}
828
859
  onChange={(e) => {
@@ -840,44 +871,44 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
840
871
  >
841
872
  {/* Volume / tiered are graduated over table ranges — a fixed price isn't applicable. */}
842
873
  {editingCharge.chargeModel !== 'volume' && editingCharge.chargeModel !== 'tiered' && (
843
- <option value="fixed">Fixed Price</option>
874
+ <option value="fixed">{pricingSourceLabel(t, 'fixed')}</option>
844
875
  )}
845
- <option value="table">Table Lookup</option>
876
+ <option value="table">{pricingSourceLabel(t, 'table')}</option>
846
877
  </select>
847
878
  </div>
848
879
  {editingCharge.pricingMethod === 'fixed' ? (
849
880
  <>
850
881
  <div>
851
- <label className="block text-xs font-medium mb-1">Fixed Price <span className="text-destructive">*</span></label>
882
+ <label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'fixedPrice')} <span className="text-destructive">*</span></label>
852
883
  <input type="text" value={editingCharge.fixedPrice ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, fixedPrice: e.target.value || null })} placeholder="0.00" className="w-full rounded-md border px-2 py-1.5 text-sm" />
853
884
  </div>
854
885
  <div>
855
- <label className="block text-xs font-medium mb-1">Currency <span className="text-destructive">*</span></label>
886
+ <label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'currency')} <span className="text-destructive">*</span></label>
856
887
  <input type="text" value={editingCharge.currencyCode ?? 'USD'} onChange={(e) => setEditingCharge({ ...editingCharge, currencyCode: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
857
888
  </div>
858
889
  </>
859
890
  ) : (
860
891
  <>
861
892
  <div>
862
- <label className="block text-xs font-medium mb-1">Pricing Table <span className="text-destructive">*</span></label>
893
+ <label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'pricingTable')} <span className="text-destructive">*</span></label>
863
894
  <select value={editingCharge.pricingTableId ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, pricingTableId: e.target.value || null, priceColumnKey: null })} className="w-full rounded-md border px-2 py-1.5 text-sm">
864
- <option value="">Select table...</option>
895
+ <option value="">{t('cpq.offerings.charge.fields.selectTable', 'Select table...')}</option>
865
896
  {pricingTables.map((pt) => (
866
897
  <option key={pt.id} value={pt.id}>{pt.name} ({pt.code})</option>
867
898
  ))}
868
899
  </select>
869
900
  {pricingTables.length === 0 && (
870
901
  <p className="text-xs text-warning mt-0.5">
871
- No pricing tables yet —{' '}
872
- <a href="/backend/cpq/pricing-tables" className="text-primary hover:underline">create one</a>{' '}
873
- before using Table Lookup pricing.
902
+ {t('cpq.offerings.charge.noPricingTables.prefix', 'No pricing tables yet —')}{' '}
903
+ <a href="/backend/cpq/pricing-tables" className="text-primary hover:underline">{t('cpq.offerings.charge.noPricingTables.link', 'create one')}</a>{' '}
904
+ {t('cpq.offerings.charge.noPricingTables.suffix', 'before using Table Lookup pricing.')}
874
905
  </p>
875
906
  )}
876
907
  </div>
877
908
  <div>
878
- <label className="block text-xs font-medium mb-1">Price Column <span className="text-destructive">*</span></label>
909
+ <label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'priceColumn')} <span className="text-destructive">*</span></label>
879
910
  <select value={editingCharge.priceColumnKey ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, priceColumnKey: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" disabled={!selectedPricingTable}>
880
- <option value="">Select column...</option>
911
+ <option value="">{t('cpq.offerings.charge.fields.selectColumn', 'Select column...')}</option>
881
912
  {selectedPricingTable?.priceColumns.map((col) => (
882
913
  <option key={col.key} value={col.key}>{col.label}</option>
883
914
  ))}
@@ -890,27 +921,27 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
890
921
  {((editingCharge.pricingMethod === 'table' && editingCharge.chargeModel !== 'flat') ||
891
922
  (editingCharge.pricingMethod === 'fixed' && editingCharge.chargeModel === 'per_unit')) && (
892
923
  <div>
893
- <label className="block text-xs font-medium mb-1">Quantity Attribute <span className="text-destructive">*</span></label>
924
+ <label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'quantityAttribute')} <span className="text-destructive">*</span></label>
894
925
  <select value={editingCharge.quantityAttributeCode ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, quantityAttributeCode: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm">
895
- <option value="">Select attribute...</option>
926
+ <option value="">{t('cpq.offerings.charge.fields.selectAttribute', 'Select attribute...')}</option>
896
927
  {attributes.filter((a) => a.attributeType === 'number').map((a) => (
897
928
  <option key={a.code} value={a.code}>{a.name} ({a.code})</option>
898
929
  ))}
899
930
  </select>
900
- <p className="text-xs text-muted-foreground mt-0.5">Attribute whose value drives the quantity for pricing</p>
931
+ <p className="text-xs text-muted-foreground mt-0.5">{t('cpq.offerings.charge.fields.quantityAttributeHelp', 'Attribute whose value drives the quantity for pricing')}</p>
901
932
  {attributes.filter((a) => a.attributeType === 'number').length === 0 && form.specId && (
902
933
  <p className="text-xs text-warning mt-0.5">
903
- Spec has no number attributes —{' '}
934
+ {t('cpq.offerings.charge.noNumberAttributes.prefix', 'Spec has no number attributes —')}{' '}
904
935
  <a href={`/backend/cpq/specifications/${form.specId}`} className="text-primary hover:underline">
905
- add one to the spec
936
+ {t('cpq.offerings.charge.noNumberAttributes.link', 'add one to the spec')}
906
937
  </a>{' '}
907
- first.
938
+ {t('cpq.offerings.charge.noNumberAttributes.suffix', 'first.')}
908
939
  </p>
909
940
  )}
910
941
  </div>
911
942
  )}
912
943
  <div>
913
- <label className="block text-xs font-medium mb-1">Sort Order</label>
944
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.sortOrder', 'Sort Order')}</label>
914
945
  <NumberInput
915
946
  integer
916
947
  value={editingCharge.sortOrder}
@@ -919,13 +950,13 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
919
950
  </div>
920
951
  </div>
921
952
  <div>
922
- <label className="block text-xs font-medium mb-1">Description</label>
953
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.description', 'Description')}</label>
923
954
  <input type="text" value={editingCharge.description ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, description: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
924
955
  </div>
925
956
  <div className="flex items-end gap-4">
926
957
  <label className="flex items-center gap-1.5 text-sm">
927
958
  <input type="checkbox" checked={editingCharge.isActive} onChange={(e) => setEditingCharge({ ...editingCharge, isActive: e.target.checked })} className="rounded border" />
928
- Active
959
+ {t('cpq.common.active', 'Active')}
929
960
  </label>
930
961
  </div>
931
962
  {(() => {
@@ -935,14 +966,22 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
935
966
  <>
936
967
  {missing.length > 0 && (
937
968
  <Alert variant="destructive">
938
- Missing required field{missing.length > 1 ? 's' : ''} for {editingCharge.pricingMethod} pricing: {missing.join(', ')}
969
+ {missing.length > 1
970
+ ? t('cpq.offerings.charge.missingFieldsPlural', 'Missing required fields for {method} pricing: {fields}', {
971
+ method: editingCharge.pricingMethod,
972
+ fields: missing.map((field) => chargeMissingFieldLabel(t, field)).join(', '),
973
+ })
974
+ : t('cpq.offerings.charge.missingFieldsSingular', 'Missing required field for {method} pricing: {fields}', {
975
+ method: editingCharge.pricingMethod,
976
+ fields: missing.map((field) => chargeMissingFieldLabel(t, field)).join(', '),
977
+ })}
939
978
  </Alert>
940
979
  )}
941
980
  <div className="flex gap-2">
942
981
  <button onClick={saveCharge} disabled={blocked} className="inline-flex items-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed">
943
- {saving ? 'Saving...' : 'Save'}
982
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
944
983
  </button>
945
- <button onClick={() => setEditingCharge(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
984
+ <button onClick={() => setEditingCharge(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">{t('cpq.common.cancel', 'Cancel')}</button>
946
985
  </div>
947
986
  </>
948
987
  )
@@ -953,20 +992,20 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
953
992
  {/* Charges table */}
954
993
  {charges.length === 0 ? (
955
994
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
956
- No charges defined. Add recurring, non-recurring, or usage charges to this offering.
995
+ {t('cpq.offerings.charges.empty', 'No charges defined. Add recurring, non-recurring, or usage charges to this offering.')}
957
996
  </div>
958
997
  ) : (
959
998
  <div className="rounded-lg border">
960
999
  <table className="w-full text-sm">
961
1000
  <thead>
962
1001
  <tr className="border-b bg-muted/50">
963
- <th className="px-4 py-3 text-left font-medium">Code</th>
964
- <th className="px-4 py-3 text-left font-medium">Name</th>
965
- <th className="px-4 py-3 text-left font-medium">Type</th>
966
- <th className="px-4 py-3 text-left font-medium">Pricing</th>
967
- <th className="px-4 py-3 text-left font-medium">Price</th>
968
- <th className="px-4 py-3 text-left font-medium">Active</th>
969
- <th className="px-4 py-3 text-right font-medium">Actions</th>
1002
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.code', 'Code')}</th>
1003
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.name', 'Name')}</th>
1004
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.type', 'Type')}</th>
1005
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.pricing', 'Pricing')}</th>
1006
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.price', 'Price')}</th>
1007
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.active', 'Active')}</th>
1008
+ <th className="px-4 py-3 text-right font-medium">{t('cpq.offerings.charges.columns.actions', 'Actions')}</th>
970
1009
  </tr>
971
1010
  </thead>
972
1011
  <tbody>
@@ -985,7 +1024,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
985
1024
  <td className="px-4 py-3">
986
1025
  {charge.name}
987
1026
  {incomplete && (
988
- <Tag variant="error" className="ml-2 px-2 text-[10px]" dot>Incomplete</Tag>
1027
+ <Tag variant="error" className="ml-2 px-2 text-[10px]" dot>{t('cpq.offerings.charges.incomplete', 'Incomplete')}</Tag>
989
1028
  )}
990
1029
  </td>
991
1030
  <td className="px-4 py-3">
@@ -994,8 +1033,8 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
994
1033
  </Tag>
995
1034
  </td>
996
1035
  <td className="px-4 py-3 text-xs">
997
- {CHARGE_MODEL_LABELS[model] ?? model}
998
- <span className="text-muted-foreground"> · {PRICING_SOURCE_LABELS[source] ?? source}</span>
1036
+ {chargeModelLabel(t, model)}
1037
+ <span className="text-muted-foreground"> · {pricingSourceLabel(t, source)}</span>
999
1038
  </td>
1000
1039
  <td className="px-4 py-3 text-xs">
1001
1040
  {source === 'fixed' ? (
@@ -1005,30 +1044,30 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1005
1044
  : <span className="text-muted-foreground">—</span>}
1006
1045
  {model === 'per_unit' && (
1007
1046
  <span>
1008
- <span className="text-muted-foreground">× Qty:</span>{' '}
1009
- {qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">missing</span>}
1047
+ <span className="text-muted-foreground">{t('cpq.offerings.charges.qtyLabel', '× Qty:')}</span>{' '}
1048
+ {qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">{t('cpq.offerings.charges.missing', 'missing')}</span>}
1010
1049
  </span>
1011
1050
  )}
1012
1051
  </div>
1013
1052
  ) : (
1014
1053
  <div className="flex flex-col gap-0.5">
1015
1054
  <span>
1016
- <span className="text-muted-foreground">Table:</span>{' '}
1017
- {tableRef ? `${tableRef.name} → ${charge.priceColumnKey ?? '?'}` : <span className="text-destructive">missing</span>}
1055
+ <span className="text-muted-foreground">{t('cpq.offerings.charges.tableLabel', 'Table:')}</span>{' '}
1056
+ {tableRef ? `${tableRef.name} → ${charge.priceColumnKey ?? '?'}` : <span className="text-destructive">{t('cpq.offerings.charges.missing', 'missing')}</span>}
1018
1057
  </span>
1019
1058
  {model !== 'flat' && (
1020
1059
  <span>
1021
- <span className="text-muted-foreground">× Qty:</span>{' '}
1022
- {qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">missing</span>}
1060
+ <span className="text-muted-foreground">{t('cpq.offerings.charges.qtyLabel', '× Qty:')}</span>{' '}
1061
+ {qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">{t('cpq.offerings.charges.missing', 'missing')}</span>}
1023
1062
  </span>
1024
1063
  )}
1025
1064
  </div>
1026
1065
  )}
1027
1066
  </td>
1028
- <td className="px-4 py-3">{charge.isActive ? 'Yes' : 'No'}</td>
1067
+ <td className="px-4 py-3">{charge.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}</td>
1029
1068
  <td className="px-4 py-3 text-right">
1030
- <button onClick={() => startEditCharge(charge)} className="text-xs text-primary hover:underline mr-2">Edit</button>
1031
- <button onClick={() => deleteCharge(charge.id)} className="text-xs text-destructive hover:underline">Delete</button>
1069
+ <button onClick={() => startEditCharge(charge)} className="text-xs text-primary hover:underline mr-2">{t('cpq.common.edit', 'Edit')}</button>
1070
+ <button onClick={() => deleteCharge(charge.id)} className="text-xs text-destructive hover:underline">{t('cpq.common.delete', 'Delete')}</button>
1032
1071
  </td>
1033
1072
  </tr>
1034
1073
  )
@@ -1045,7 +1084,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1045
1084
  <div className="space-y-4">
1046
1085
  {bundleSlots.length === 0 ? (
1047
1086
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
1048
- No bundle slots defined on this specification. Add slots on the specification first.
1087
+ {t('cpq.offerings.components.noSlots', 'No bundle slots defined on this specification. Add slots on the specification first.')}
1049
1088
  </div>
1050
1089
  ) : (
1051
1090
  <>
@@ -1080,7 +1119,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1080
1119
  }}
1081
1120
  className="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
1082
1121
  >
1083
- + Add Component
1122
+ {t('cpq.offerings.components.add', '+ Add Component')}
1084
1123
  </button>
1085
1124
  </div>
1086
1125
 
@@ -1088,20 +1127,20 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1088
1127
  <div className="p-4 border-b bg-muted/20 space-y-3">
1089
1128
  <div className="grid grid-cols-3 gap-3">
1090
1129
  <div className="col-span-2">
1091
- <label className="block text-xs font-medium mb-1">Component Offering</label>
1130
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.components.fields.componentOffering', 'Component Offering')}</label>
1092
1131
  <select
1093
1132
  value={addingComponent.childOfferingId}
1094
1133
  onChange={(e) => setAddingComponent({ ...addingComponent, childOfferingId: e.target.value })}
1095
1134
  className="w-full rounded-md border px-3 py-2 text-sm"
1096
1135
  >
1097
- <option value="">— Select offering —</option>
1136
+ <option value="">{t('cpq.offerings.components.fields.selectOffering', '— Select offering —')}</option>
1098
1137
  {(slotOfferings[slot.targetSpecId] ?? []).map((o) => (
1099
- <option key={o.id} value={o.id}>{o.name} ({o.code}){o.offeringType === 'bundle' ? ' [bundle]' : ''}</option>
1138
+ <option key={o.id} value={o.id}>{o.name} ({o.code}){o.offeringType === 'bundle' ? t('cpq.offerings.components.bundleSuffix', ' [bundle]') : ''}</option>
1100
1139
  ))}
1101
1140
  </select>
1102
1141
  </div>
1103
1142
  <div>
1104
- <label className="block text-xs font-medium mb-1">Default?</label>
1143
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.components.fields.isDefault', 'Default?')}</label>
1105
1144
  <div className="flex items-center h-[38px]">
1106
1145
  <input
1107
1146
  type="checkbox"
@@ -1109,7 +1148,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1109
1148
  onChange={(e) => setAddingComponent({ ...addingComponent, isDefault: e.target.checked })}
1110
1149
  className="rounded border"
1111
1150
  />
1112
- <span className="ml-2 text-xs text-muted-foreground">Pre-selected in quote</span>
1151
+ <span className="ml-2 text-xs text-muted-foreground">{t('cpq.offerings.components.fields.isDefaultHelp', 'Pre-selected in quote')}</span>
1113
1152
  </div>
1114
1153
  </div>
1115
1154
  </div>
@@ -1132,7 +1171,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1132
1171
  })
1133
1172
  if (!res.ok) {
1134
1173
  const d = await res.json()
1135
- setError(d.error ?? 'Failed to add component')
1174
+ setError(d.error ?? t('cpq.offerings.components.error.add', 'Failed to add component'))
1136
1175
  return
1137
1176
  }
1138
1177
  setAddingComponent(null)
@@ -1142,20 +1181,20 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1142
1181
  setComponents(d.items ?? [])
1143
1182
  }
1144
1183
  } catch {
1145
- setError('Failed to add component')
1184
+ setError(t('cpq.offerings.components.error.add', 'Failed to add component'))
1146
1185
  } finally {
1147
1186
  setSaving(false)
1148
1187
  }
1149
1188
  }}
1150
1189
  className="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
1151
1190
  >
1152
- {saving ? 'Adding...' : 'Add'}
1191
+ {saving ? t('cpq.offerings.components.adding', 'Adding...') : t('cpq.offerings.components.addSubmit', 'Add')}
1153
1192
  </button>
1154
1193
  <button
1155
1194
  onClick={() => setAddingComponent(null)}
1156
1195
  className="inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-xs font-medium hover:bg-muted"
1157
1196
  >
1158
- Cancel
1197
+ {t('cpq.common.cancel', 'Cancel')}
1159
1198
  </button>
1160
1199
  </div>
1161
1200
  </div>
@@ -1163,32 +1202,32 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1163
1202
 
1164
1203
  {slotComponents.length === 0 ? (
1165
1204
  <div className="p-4 text-center text-xs text-muted-foreground">
1166
- No components assigned to this slot
1205
+ {t('cpq.offerings.components.emptySlot', 'No components assigned to this slot')}
1167
1206
  {slot.cardinalityMin > 0 && (
1168
- <span className="ml-1 text-status-warning-text font-medium">(required: min {slot.cardinalityMin})</span>
1207
+ <span className="ml-1 text-status-warning-text font-medium">{t('cpq.offerings.components.requiredMin', '(required: min {min})', { min: slot.cardinalityMin })}</span>
1169
1208
  )}
1170
1209
  </div>
1171
1210
  ) : (
1172
1211
  <table className="w-full text-sm">
1173
1212
  <thead>
1174
1213
  <tr className="border-b bg-muted/30">
1175
- <th className="px-4 py-2 text-left text-xs font-medium">Offering</th>
1176
- <th className="px-4 py-2 text-left text-xs font-medium">Code</th>
1177
- <th className="px-4 py-2 text-left text-xs font-medium">Type</th>
1178
- <th className="px-4 py-2 text-left text-xs font-medium">Default</th>
1179
- <th className="px-4 py-2 text-right text-xs font-medium">Actions</th>
1214
+ <th className="px-4 py-2 text-left text-xs font-medium">{t('cpq.offerings.components.columns.offering', 'Offering')}</th>
1215
+ <th className="px-4 py-2 text-left text-xs font-medium">{t('cpq.offerings.components.columns.code', 'Code')}</th>
1216
+ <th className="px-4 py-2 text-left text-xs font-medium">{t('cpq.offerings.components.columns.type', 'Type')}</th>
1217
+ <th className="px-4 py-2 text-left text-xs font-medium">{t('cpq.offerings.components.columns.default', 'Default')}</th>
1218
+ <th className="px-4 py-2 text-right text-xs font-medium">{t('cpq.offerings.components.columns.actions', 'Actions')}</th>
1180
1219
  </tr>
1181
1220
  </thead>
1182
1221
  <tbody>
1183
1222
  {slotComponents.map((comp) => (
1184
1223
  <tr key={comp.id} className="border-b last:border-0">
1185
- <td className="px-4 py-2.5 font-medium">{comp.childOffering?.name ?? 'Unknown'}</td>
1224
+ <td className="px-4 py-2.5 font-medium">{comp.childOffering?.name ?? t('cpq.offerings.components.unknown', 'Unknown')}</td>
1186
1225
  <td className="px-4 py-2.5 text-muted-foreground text-xs">{comp.childOffering?.code ?? '—'}</td>
1187
1226
  <td className="px-4 py-2.5">
1188
1227
  {comp.childOffering?.offeringType === 'bundle' ? (
1189
- <Tag variant="brand" className="px-2 text-[10px]">bundle</Tag>
1228
+ <Tag variant="brand" className="px-2 text-[10px]">{t('cpq.offerings.components.tag.bundle', 'bundle')}</Tag>
1190
1229
  ) : (
1191
- <Tag variant="neutral" className="px-2 text-[10px]">simple</Tag>
1230
+ <Tag variant="neutral" className="px-2 text-[10px]">{t('cpq.offerings.components.tag.simple', 'simple')}</Tag>
1192
1231
  )}
1193
1232
  </td>
1194
1233
  <td className="px-4 py-2.5">
@@ -1208,14 +1247,14 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1208
1247
  className="cursor-pointer"
1209
1248
  >
1210
1249
  <Tag variant={comp.isDefault ? 'success' : 'neutral'} className="px-2 text-[10px]">
1211
- {comp.isDefault ? 'Yes' : 'No'}
1250
+ {comp.isDefault ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
1212
1251
  </Tag>
1213
1252
  </button>
1214
1253
  </td>
1215
1254
  <td className="px-4 py-2.5 text-right">
1216
1255
  <button
1217
1256
  onClick={async () => {
1218
- if (!confirm('Remove this component from the bundle?')) return
1257
+ if (!confirm(t('cpq.offerings.components.confirmRemove', 'Remove this component from the bundle?'))) return
1219
1258
  try {
1220
1259
  const res = await fetch('/api/cpq/offering-components', {
1221
1260
  method: 'DELETE',
@@ -1226,15 +1265,15 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
1226
1265
  setComponents((prev) => prev.filter((c) => c.id !== comp.id))
1227
1266
  } else {
1228
1267
  const d = await res.json()
1229
- setError(d.error ?? 'Failed to remove')
1268
+ setError(d.error ?? t('cpq.offerings.components.error.remove', 'Failed to remove'))
1230
1269
  }
1231
1270
  } catch {
1232
- setError('Failed to remove component')
1271
+ setError(t('cpq.offerings.components.error.removeComponent', 'Failed to remove component'))
1233
1272
  }
1234
1273
  }}
1235
1274
  className="text-xs text-destructive hover:underline"
1236
1275
  >
1237
- Remove
1276
+ {t('cpq.offerings.components.remove', 'Remove')}
1238
1277
  </button>
1239
1278
  </td>
1240
1279
  </tr>