@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
@@ -300,7 +300,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
300
300
  // Load spec detail with nested data
301
301
  const res = await fetch(`/api/cpq/product-specifications?id=${encodeURIComponent(specId)}`)
302
302
  if (!res.ok) {
303
- if (!cancelled) setError('Failed to load specification')
303
+ if (!cancelled) setError(t('cpq.specifications.detail.error.load', 'Failed to load specification'))
304
304
  return
305
305
  }
306
306
 
@@ -325,7 +325,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
325
325
  }
326
326
  } catch (err) {
327
327
  console.error('Failed to load', err)
328
- if (!cancelled) setError('Failed to load')
328
+ if (!cancelled) setError(t('cpq.specifications.detail.error.loadGeneric', 'Failed to load'))
329
329
  } finally {
330
330
  if (!cancelled) setLoading(false)
331
331
  }
@@ -354,7 +354,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
354
354
 
355
355
  if (!res.ok) {
356
356
  const d = await res.json()
357
- setError(d.error ?? 'Failed to save')
357
+ setError(d.error ?? t('cpq.specifications.detail.error.save', 'Failed to save'))
358
358
  return
359
359
  }
360
360
 
@@ -379,7 +379,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
379
379
  setGeneralMode('view')
380
380
  }
381
381
  } catch {
382
- setError('Failed to save')
382
+ setError(t('cpq.specifications.detail.error.save', 'Failed to save'))
383
383
  } finally {
384
384
  setSaving(false)
385
385
  }
@@ -397,10 +397,10 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
397
397
  router.push('/backend/cpq/specifications')
398
398
  } else {
399
399
  const d = await res.json()
400
- setError(d.error ?? 'Failed to delete')
400
+ setError(d.error ?? t('cpq.specifications.detail.error.delete', 'Failed to delete'))
401
401
  }
402
402
  } catch {
403
- setError('Failed to delete')
403
+ setError(t('cpq.specifications.detail.error.delete', 'Failed to delete'))
404
404
  }
405
405
  }
406
406
 
@@ -438,7 +438,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
438
438
  })
439
439
  if (!res.ok) {
440
440
  const d = await res.json()
441
- setError(d.error ?? 'Failed to save attribute')
441
+ setError(d.error ?? t('cpq.specifications.attributes.error.save', 'Failed to save attribute'))
442
442
  return
443
443
  }
444
444
  const saved = await res.json()
@@ -449,14 +449,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
449
449
  }
450
450
  setEditingAttr(null)
451
451
  } catch {
452
- setError('Failed to save attribute')
452
+ setError(t('cpq.specifications.attributes.error.save', 'Failed to save attribute'))
453
453
  } finally {
454
454
  setSaving(false)
455
455
  }
456
456
  }
457
457
 
458
458
  const deleteAttribute = async (attrId: string) => {
459
- if (!confirm('Delete this attribute?')) return
459
+ if (!confirm(t('cpq.specifications.attributes.confirmDelete', 'Delete this attribute?'))) return
460
460
  try {
461
461
  const res = await fetch('/api/cpq/product-attributes', {
462
462
  method: 'DELETE',
@@ -465,7 +465,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
465
465
  })
466
466
  if (res.ok) setAttributes((prev) => prev.filter((a) => a.id !== attrId))
467
467
  } catch {
468
- setError('Failed to delete attribute')
468
+ setError(t('cpq.specifications.attributes.error.delete', 'Failed to delete attribute'))
469
469
  }
470
470
  }
471
471
 
@@ -502,7 +502,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
502
502
  })
503
503
  if (!res.ok) {
504
504
  const d = await res.json()
505
- setError(d.error ?? 'Failed to save offering')
505
+ setError(d.error ?? t('cpq.specifications.offerings.error.save', 'Failed to save offering'))
506
506
  return
507
507
  }
508
508
  const saved = await res.json()
@@ -513,14 +513,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
513
513
  }
514
514
  setEditingOffering(null)
515
515
  } catch {
516
- setError('Failed to save offering')
516
+ setError(t('cpq.specifications.offerings.error.save', 'Failed to save offering'))
517
517
  } finally {
518
518
  setSaving(false)
519
519
  }
520
520
  }
521
521
 
522
522
  const deleteOffering = async (offeringId: string) => {
523
- if (!confirm('Delete this offering?')) return
523
+ if (!confirm(t('cpq.specifications.offerings.confirmDelete', 'Delete this offering?'))) return
524
524
  try {
525
525
  const res = await fetch('/api/cpq/product-offerings', {
526
526
  method: 'DELETE',
@@ -529,7 +529,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
529
529
  })
530
530
  if (res.ok) setOfferings((prev) => prev.filter((o) => o.id !== offeringId))
531
531
  } catch {
532
- setError('Failed to delete offering')
532
+ setError(t('cpq.specifications.offerings.error.delete', 'Failed to delete offering'))
533
533
  }
534
534
  }
535
535
 
@@ -623,21 +623,21 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
623
623
  })
624
624
  if (!res.ok) {
625
625
  const d = await res.json()
626
- setError(d.error ?? `Failed to save charge for ${offering.name}`)
626
+ setError(d.error ?? t('cpq.specifications.charges.error.saveFor', 'Failed to save charge for {name}', { name: offering.name }))
627
627
  break
628
628
  }
629
629
  }
630
630
  setChargeFormOpen(false)
631
631
  await loadAllOfferingCharges()
632
632
  } catch {
633
- setError('Failed to save charges')
633
+ setError(t('cpq.specifications.charges.error.saveAll', 'Failed to save charges'))
634
634
  } finally {
635
635
  setSaving(false)
636
636
  }
637
637
  }
638
638
 
639
639
  const deleteCharge = async (offeringId: string, chargeId: string) => {
640
- if (!confirm('Delete this charge?')) return
640
+ if (!confirm(t('cpq.specifications.charges.confirmDelete', 'Delete this charge?'))) return
641
641
  try {
642
642
  const res = await fetch('/api/cpq/product-charges', {
643
643
  method: 'DELETE',
@@ -651,7 +651,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
651
651
  }))
652
652
  }
653
653
  } catch {
654
- setError('Failed to delete charge')
654
+ setError(t('cpq.specifications.charges.error.delete', 'Failed to delete charge'))
655
655
  }
656
656
  }
657
657
 
@@ -682,7 +682,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
682
682
  })
683
683
  if (!res.ok) {
684
684
  const d = await res.json()
685
- setError(d.error ?? 'Failed to save relationship')
685
+ setError(d.error ?? t('cpq.specifications.relationships.error.save', 'Failed to save relationship'))
686
686
  return
687
687
  }
688
688
  const saved = await res.json()
@@ -693,14 +693,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
693
693
  }
694
694
  setEditingRel(null)
695
695
  } catch {
696
- setError('Failed to save relationship')
696
+ setError(t('cpq.specifications.relationships.error.save', 'Failed to save relationship'))
697
697
  } finally {
698
698
  setSaving(false)
699
699
  }
700
700
  }
701
701
 
702
702
  const deleteRelationship = async (relId: string) => {
703
- if (!confirm('Delete this relationship?')) return
703
+ if (!confirm(t('cpq.specifications.relationships.confirmDelete', 'Delete this relationship?'))) return
704
704
  try {
705
705
  const res = await fetch('/api/cpq/product-relationships', {
706
706
  method: 'DELETE',
@@ -709,7 +709,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
709
709
  })
710
710
  if (res.ok) setRelationships((prev) => prev.filter((r) => r.id !== relId))
711
711
  } catch {
712
- setError('Failed to delete relationship')
712
+ setError(t('cpq.specifications.relationships.error.delete', 'Failed to delete relationship'))
713
713
  }
714
714
  }
715
715
 
@@ -726,7 +726,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
726
726
  // ─── Render ─────────────────────────────────────────────────
727
727
 
728
728
  if (loading) {
729
- return <div className="text-sm text-muted-foreground">{t('common.loading', 'Loading...')}</div>
729
+ return <div className="text-sm text-muted-foreground">{t('cpq.common.loading', 'Loading...')}</div>
730
730
  }
731
731
 
732
732
  const designTimeAttrs = attributes.filter((a) => a.resolutionTime === 'design_time')
@@ -740,7 +740,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
740
740
  onClick={() => router.push('/backend/cpq/specifications')}
741
741
  className="text-sm text-muted-foreground hover:text-foreground"
742
742
  >
743
- ← {t('common.back', 'Back')}
743
+ ← {t('cpq.common.back', 'Back')}
744
744
  </button>
745
745
  <h1 className="text-2xl font-bold">
746
746
  {isNew ? t('cpq.specifications.new', 'New Specification') : form.name}
@@ -764,11 +764,11 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
764
764
  setGeneralMode('edit')
765
765
  }}
766
766
  >
767
- {t('common.edit', 'Edit')}
767
+ {t('cpq.common.edit', 'Edit')}
768
768
  </Button>
769
769
  )}
770
770
  <Button type="button" variant="destructive" onClick={deleteSpec}>
771
- {t('common.delete', 'Delete')}
771
+ {t('cpq.common.delete', 'Delete')}
772
772
  </Button>
773
773
  </div>
774
774
  )}
@@ -795,11 +795,11 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
795
795
  tab === t_ ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground'
796
796
  }`}
797
797
  >
798
- {t_ === 'general' && 'General'}
799
- {t_ === 'attributes' && `Attributes (${attributes.length})`}
800
- {t_ === 'offerings' && `Offerings (${offerings.length})`}
801
- {t_ === 'relationships' && `Relationships (${relationships.length})`}
802
- {t_ === 'slots' && `Bundle Slots (${bundleSlots.length})`}
798
+ {t_ === 'general' && t('cpq.specifications.detail.tabs.general', 'General')}
799
+ {t_ === 'attributes' && t('cpq.specifications.detail.tabs.attributes', 'Attributes ({count})', { count: attributes.length })}
800
+ {t_ === 'offerings' && t('cpq.specifications.detail.tabs.offerings', 'Offerings ({count})', { count: offerings.length })}
801
+ {t_ === 'relationships' && t('cpq.specifications.detail.tabs.relationships', 'Relationships ({count})', { count: relationships.length })}
802
+ {t_ === 'slots' && t('cpq.specifications.detail.tabs.slots', 'Bundle Slots ({count})', { count: bundleSlots.length })}
803
803
  </button>
804
804
  ))}
805
805
  </div>
@@ -824,7 +824,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
824
824
  type="text"
825
825
  value={form.code}
826
826
  onChange={(e) => setForm({ ...form, code: e.target.value })}
827
- placeholder="e.g. gix-access-port"
827
+ placeholder={t('cpq.specifications.detail.codePlaceholder', 'e.g. gix-access-port')}
828
828
  className="w-full rounded-md border px-3 py-2 text-sm"
829
829
  />
830
830
  )}
@@ -856,9 +856,9 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
856
856
  onChange={(e) => setForm({ ...form, lifecycleStatus: e.target.value })}
857
857
  className="w-full rounded-md border px-3 py-2 text-sm"
858
858
  >
859
- <option value="draft">Draft</option>
860
- <option value="active">Active</option>
861
- <option value="deprecated">Deprecated</option>
859
+ <option value="draft">{t('cpq.specifications.lifecycle.draft', 'Draft')}</option>
860
+ <option value="active">{t('cpq.specifications.lifecycle.active', 'Active')}</option>
861
+ <option value="deprecated">{t('cpq.specifications.lifecycle.deprecated', 'Deprecated')}</option>
862
862
  </select>
863
863
  )}
864
864
  </div>
@@ -867,7 +867,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
867
867
  <div className="flex items-center h-[38px]">
868
868
  {isViewing ? (
869
869
  <Tag variant={form.isActive ? 'success' : 'neutral'} dot>
870
- {form.isActive ? t('common.yes', 'Yes') : t('common.no', 'No')}
870
+ {form.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
871
871
  </Tag>
872
872
  ) : (
873
873
  <input
@@ -880,9 +880,9 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
880
880
  </div>
881
881
  </div>
882
882
  <div>
883
- <label className="block text-sm font-medium mb-1">Spec Type</label>
883
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.detail.specType', 'Spec Type')}</label>
884
884
  {isViewing ? (
885
- <p className="text-sm py-2">{form.specType === 'bundle' ? 'Bundle' : 'Simple'}</p>
885
+ <p className="text-sm py-2">{form.specType === 'bundle' ? t('cpq.specifications.type.bundle', 'Bundle') : t('cpq.specifications.type.simple', 'Simple')}</p>
886
886
  ) : (
887
887
  <>
888
888
  <select
@@ -891,21 +891,21 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
891
891
  disabled={specTypeLocked}
892
892
  className="w-full rounded-md border px-3 py-2 text-sm disabled:opacity-50"
893
893
  >
894
- <option value="simple">Simple</option>
895
- <option value="bundle">Bundle</option>
894
+ <option value="simple">{t('cpq.specifications.type.simple', 'Simple')}</option>
895
+ <option value="bundle">{t('cpq.specifications.type.bundle', 'Bundle')}</option>
896
896
  </select>
897
897
  {specTypeLocked && (
898
- <p className="text-xs text-muted-foreground mt-1">Cannot change after offerings or slots exist</p>
898
+ <p className="text-xs text-muted-foreground mt-1">{t('cpq.specifications.detail.specTypeLocked', 'Cannot change after offerings or slots exist')}</p>
899
899
  )}
900
900
  </>
901
901
  )}
902
902
  </div>
903
903
  <div>
904
- <label className="block text-sm font-medium mb-1">Is Assetizable?</label>
904
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.detail.isAssetizable', 'Is Assetizable?')}</label>
905
905
  <div className="flex items-center h-[38px]">
906
906
  {isViewing ? (
907
907
  <Tag variant={form.isAssetizable ? 'warning' : 'neutral'} dot>
908
- {form.isAssetizable ? t('common.yes', 'Yes') : t('common.no', 'No')}
908
+ {form.isAssetizable ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
909
909
  </Tag>
910
910
  ) : (
911
911
  <>
@@ -915,7 +915,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
915
915
  onChange={(e) => setForm({ ...form, isAssetizable: e.target.checked })}
916
916
  className="rounded border"
917
917
  />
918
- <span className="ml-2 text-xs text-muted-foreground">Creates asset records during fulfilment</span>
918
+ <span className="ml-2 text-xs text-muted-foreground">{t('cpq.specifications.detail.isAssetizableHelp', 'Creates asset records during fulfilment')}</span>
919
919
  </>
920
920
  )}
921
921
  </div>
@@ -935,7 +935,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
935
935
  )}
936
936
  </div>
937
937
  {!isNew && (
938
- <span className="text-xs text-muted-foreground">Version {form.version}</span>
938
+ <span className="text-xs text-muted-foreground">{t('cpq.specifications.detail.versionLabel', 'Version {version}', { version: form.version })}</span>
939
939
  )}
940
940
  {/* In view mode the Edit button lives in the page header next to
941
941
  Delete (OM standard). Here we only render the Save/Cancel
@@ -947,7 +947,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
947
947
  onClick={saveSpec}
948
948
  disabled={saving || !form.code || !form.name}
949
949
  >
950
- {saving ? t('common.saving', 'Saving...') : t('common.save', 'Save')}
950
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
951
951
  </Button>
952
952
  <Button
953
953
  type="button"
@@ -963,7 +963,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
963
963
  setGeneralMode('view')
964
964
  }}
965
965
  >
966
- {t('common.cancel', 'Cancel')}
966
+ {t('cpq.common.cancel', 'Cancel')}
967
967
  </Button>
968
968
  </div>
969
969
  )}
@@ -986,40 +986,40 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
986
986
  {editingAttr && (
987
987
  <div className="rounded-lg border bg-card p-4 space-y-3">
988
988
  <h3 className="font-medium text-sm">
989
- {editingAttr.id ? 'Edit Attribute' : 'New Attribute'}
989
+ {editingAttr.id ? t('cpq.specifications.attributes.form.editTitle', 'Edit Attribute') : t('cpq.specifications.attributes.form.newTitle', 'New Attribute')}
990
990
  </h3>
991
991
  <div className="grid grid-cols-3 gap-3">
992
992
  <div>
993
- <label className="block text-xs font-medium mb-1">Code</label>
993
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.code', 'Code')}</label>
994
994
  <input type="text" value={editingAttr.code} onChange={(e) => setEditingAttr({ ...editingAttr, code: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
995
995
  </div>
996
996
  <div>
997
- <label className="block text-xs font-medium mb-1">Name</label>
997
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.name', 'Name')}</label>
998
998
  <input type="text" value={editingAttr.name} onChange={(e) => setEditingAttr({ ...editingAttr, name: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
999
999
  </div>
1000
1000
  <div>
1001
- <label className="block text-xs font-medium mb-1">Type</label>
1001
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.type', 'Type')}</label>
1002
1002
  {/* Reset the default when the type changes — a default kept from a
1003
1003
  different type (e.g. a numeric default on a now-boolean attr)
1004
1004
  would be invalid and confusing. */}
1005
1005
  <select value={editingAttr.attributeType} onChange={(e) => setEditingAttr({ ...editingAttr, attributeType: e.target.value, defaultValue: null })} className="w-full rounded-md border px-2 py-1.5 text-sm">
1006
- <option value="text">Text</option>
1007
- <option value="number">Number</option>
1008
- <option value="boolean">Boolean</option>
1009
- <option value="select">Select</option>
1010
- <option value="multi-select">Multi-Select</option>
1011
- <option value="reference">Reference</option>
1006
+ <option value="text">{t('cpq.specifications.attributeType.text', 'Text')}</option>
1007
+ <option value="number">{t('cpq.specifications.attributeType.number', 'Number')}</option>
1008
+ <option value="boolean">{t('cpq.specifications.attributeType.boolean', 'Boolean')}</option>
1009
+ <option value="select">{t('cpq.specifications.attributeType.select', 'Select')}</option>
1010
+ <option value="multi-select">{t('cpq.specifications.attributeType.multiSelect', 'Multi-Select')}</option>
1011
+ <option value="reference">{t('cpq.specifications.attributeType.reference', 'Reference')}</option>
1012
1012
  </select>
1013
1013
  </div>
1014
1014
  <div>
1015
- <label className="block text-xs font-medium mb-1">Resolution Time</label>
1015
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.resolutionTime', 'Resolution Time')}</label>
1016
1016
  <select value={editingAttr.resolutionTime} onChange={(e) => setEditingAttr({ ...editingAttr, resolutionTime: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
1017
- <option value="run_time">Run-time (quoting)</option>
1018
- <option value="design_time">Design-time (offering)</option>
1017
+ <option value="run_time">{t('cpq.specifications.resolutionTime.runTime', 'Run-time (quoting)')}</option>
1018
+ <option value="design_time">{t('cpq.specifications.resolutionTime.designTime', 'Design-time (offering)')}</option>
1019
1019
  </select>
1020
1020
  </div>
1021
1021
  <div>
1022
- <label className="block text-xs font-medium mb-1">Sort Order</label>
1022
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.sortOrder', 'Sort Order')}</label>
1023
1023
  <NumberInput
1024
1024
  integer
1025
1025
  value={editingAttr.sortOrder}
@@ -1027,7 +1027,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1027
1027
  />
1028
1028
  </div>
1029
1029
  <div>
1030
- <label className="block text-xs font-medium mb-1">Help Text</label>
1030
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.helpText', 'Help Text')}</label>
1031
1031
  <input type="text" value={editingAttr.helpText ?? ''} onChange={(e) => setEditingAttr({ ...editingAttr, helpText: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
1032
1032
  </div>
1033
1033
  {/* Reference attributes resolve to an FK UUID at run-time, so a
@@ -1102,25 +1102,25 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1102
1102
  {editingAttr.attributeType === 'reference' && (
1103
1103
  <>
1104
1104
  <div>
1105
- <label className="block text-xs font-medium mb-1">Reference Entity</label>
1106
- <input type="text" value={editingAttr.referenceEntity ?? ''} onChange={(e) => setEditingAttr({ ...editingAttr, referenceEntity: e.target.value || null })} placeholder="e.g. CatalogProduct" className="w-full rounded-md border px-2 py-1.5 text-sm" />
1105
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.referenceEntity', 'Reference Entity')}</label>
1106
+ <input type="text" value={editingAttr.referenceEntity ?? ''} onChange={(e) => setEditingAttr({ ...editingAttr, referenceEntity: e.target.value || null })} placeholder={t('cpq.specifications.attributes.fields.referenceEntityPlaceholder', 'e.g. CatalogProduct')} className="w-full rounded-md border px-2 py-1.5 text-sm" />
1107
1107
  </div>
1108
1108
  </>
1109
1109
  )}
1110
1110
  {(editingAttr.attributeType === 'select' || editingAttr.attributeType === 'multi-select') && (
1111
1111
  <div className="col-span-3 space-y-2">
1112
1112
  <div className="flex items-center justify-between">
1113
- <label className="block text-xs font-medium">Options</label>
1113
+ <label className="block text-xs font-medium">{t('cpq.specifications.attributes.fields.options', 'Options')}</label>
1114
1114
  <button
1115
1115
  type="button"
1116
1116
  onClick={() => setEditingOptions([...editingOptions, { value: '', label: '' }])}
1117
1117
  className="text-xs text-primary hover:underline"
1118
1118
  >
1119
- + Add option
1119
+ {t('cpq.specifications.attributes.addOption', '+ Add option')}
1120
1120
  </button>
1121
1121
  </div>
1122
1122
  {editingOptions.length === 0 ? (
1123
- <div className="text-xs text-muted-foreground py-2">No options defined. Click &quot;Add option&quot; to start.</div>
1123
+ <div className="text-xs text-muted-foreground py-2">{t('cpq.specifications.attributes.noOptions', 'No options defined. Click "Add option" to start.')}</div>
1124
1124
  ) : (
1125
1125
  <div className="space-y-1.5">
1126
1126
  {editingOptions.map((opt, idx) => (
@@ -1133,7 +1133,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1133
1133
  next[idx] = { ...next[idx], value: e.target.value }
1134
1134
  setEditingOptions(next)
1135
1135
  }}
1136
- placeholder="Value (e.g. aws)"
1136
+ placeholder={t('cpq.specifications.attributes.optionValuePlaceholder', 'Value (e.g. aws)')}
1137
1137
  className="flex-1 rounded-md border px-2 py-1.5 text-sm"
1138
1138
  />
1139
1139
  <input
@@ -1144,14 +1144,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1144
1144
  next[idx] = { ...next[idx], label: e.target.value }
1145
1145
  setEditingOptions(next)
1146
1146
  }}
1147
- placeholder="Label (e.g. Amazon Web Services)"
1147
+ placeholder={t('cpq.specifications.attributes.optionLabelPlaceholder', 'Label (e.g. Amazon Web Services)')}
1148
1148
  className="flex-1 rounded-md border px-2 py-1.5 text-sm"
1149
1149
  />
1150
1150
  <button
1151
1151
  type="button"
1152
1152
  onClick={() => setEditingOptions(editingOptions.filter((_, i) => i !== idx))}
1153
1153
  className="text-destructive hover:text-destructive/80 text-sm px-1"
1154
- title="Remove"
1154
+ title={t('cpq.specifications.attributes.removeOption', 'Remove')}
1155
1155
  >
1156
1156
  ×
1157
1157
  </button>
@@ -1169,7 +1169,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1169
1169
  onChange={(e) => setDependsOnEnabled(e.target.checked)}
1170
1170
  className="rounded border"
1171
1171
  />
1172
- Depends on another attribute
1172
+ {t('cpq.specifications.attributes.dependsOn.enable', 'Depends on another attribute')}
1173
1173
  </label>
1174
1174
  {dependsOnEnabled && (() => {
1175
1175
  const parentAttr = attributes.find((a) => a.code === dependsOnAttrCode)
@@ -1177,7 +1177,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1177
1177
  <div className="space-y-3">
1178
1178
  <div className="grid grid-cols-2 gap-3">
1179
1179
  <div>
1180
- <label className="block text-xs font-medium mb-1">Parent Attribute Code</label>
1180
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.dependsOn.parentAttribute', 'Parent Attribute Code')}</label>
1181
1181
  <select
1182
1182
  value={dependsOnAttrCode}
1183
1183
  onChange={(e) => {
@@ -1190,14 +1190,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1190
1190
  }}
1191
1191
  className="w-full rounded-md border px-2 py-1.5 text-sm"
1192
1192
  >
1193
- <option value="">Select attribute...</option>
1193
+ <option value="">{t('cpq.specifications.attributes.dependsOn.selectAttribute', 'Select attribute...')}</option>
1194
1194
  {attributes.filter((a) => editingAttr && a.code !== editingAttr.code).map((a) => (
1195
1195
  <option key={a.code} value={a.code}>{a.name} ({a.code})</option>
1196
1196
  ))}
1197
1197
  </select>
1198
1198
  </div>
1199
1199
  <div>
1200
- <label className="block text-xs font-medium mb-1">Effect</label>
1200
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.dependsOn.effect', 'Effect')}</label>
1201
1201
  <select
1202
1202
  value={dependsOnEffect}
1203
1203
  onChange={(e) => {
@@ -1209,14 +1209,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1209
1209
  }}
1210
1210
  className="w-full rounded-md border px-2 py-1.5 text-sm"
1211
1211
  >
1212
- <option value="filter_options">Filter options</option>
1213
- <option value="set_value">Set value</option>
1214
- <option value="toggle_required">Toggle required</option>
1212
+ <option value="filter_options">{t('cpq.specifications.attributes.dependsOn.effects.filterOptions', 'Filter options')}</option>
1213
+ <option value="set_value">{t('cpq.specifications.attributes.dependsOn.effects.setValue', 'Set value')}</option>
1214
+ <option value="toggle_required">{t('cpq.specifications.attributes.dependsOn.effects.toggleRequired', 'Toggle required')}</option>
1215
1215
  </select>
1216
1216
  </div>
1217
1217
  </div>
1218
1218
  <div>
1219
- <label className="block text-xs font-medium mb-2">Rules</label>
1219
+ <label className="block text-xs font-medium mb-2">{t('cpq.specifications.attributes.dependsOn.rules', 'Rules')}</label>
1220
1220
  <DependsOnRulesEditor
1221
1221
  effect={dependsOnEffect}
1222
1222
  rules={dependsOnRules}
@@ -1232,39 +1232,39 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1232
1232
  <div className="flex items-end gap-4">
1233
1233
  <label className="flex items-center gap-1.5 text-sm">
1234
1234
  <input type="checkbox" checked={editingAttr.isRequired} onChange={(e) => setEditingAttr({ ...editingAttr, isRequired: e.target.checked })} className="rounded border" />
1235
- Required
1235
+ {t('cpq.specifications.attributes.fields.isRequired', 'Required')}
1236
1236
  </label>
1237
1237
  <label className="flex items-center gap-1.5 text-sm">
1238
1238
  <input type="checkbox" checked={editingAttr.isActive} onChange={(e) => setEditingAttr({ ...editingAttr, isActive: e.target.checked })} className="rounded border" />
1239
- Active
1239
+ {t('cpq.common.active', 'Active')}
1240
1240
  </label>
1241
1241
  </div>
1242
1242
  </div>
1243
1243
  <div className="flex gap-2">
1244
1244
  <button onClick={saveAttribute} disabled={saving} 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">
1245
- {saving ? 'Saving...' : 'Save'}
1245
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
1246
1246
  </button>
1247
- <button onClick={() => setEditingAttr(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
1247
+ <button onClick={() => setEditingAttr(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>
1248
1248
  </div>
1249
1249
  </div>
1250
1250
  )}
1251
1251
 
1252
1252
  {attributes.length === 0 ? (
1253
1253
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
1254
- No attributes defined. Add design-time and run-time attributes for this specification.
1254
+ {t('cpq.specifications.attributes.empty', 'No attributes defined. Add design-time and run-time attributes for this specification.')}
1255
1255
  </div>
1256
1256
  ) : (
1257
1257
  <div className="rounded-lg border">
1258
1258
  <table className="w-full text-sm">
1259
1259
  <thead>
1260
1260
  <tr className="border-b bg-muted/50">
1261
- <th className="px-4 py-3 text-left font-medium">Code</th>
1262
- <th className="px-4 py-3 text-left font-medium">Name</th>
1263
- <th className="px-4 py-3 text-left font-medium">Type</th>
1264
- <th className="px-4 py-3 text-left font-medium">Resolution</th>
1265
- <th className="px-4 py-3 text-left font-medium">Options</th>
1266
- <th className="px-4 py-3 text-left font-medium">Required</th>
1267
- <th className="px-4 py-3 text-right font-medium">Actions</th>
1261
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.code', 'Code')}</th>
1262
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.name', 'Name')}</th>
1263
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.type', 'Type')}</th>
1264
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.resolution', 'Resolution')}</th>
1265
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.options', 'Options')}</th>
1266
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.required', 'Required')}</th>
1267
+ <th className="px-4 py-3 text-right font-medium">{t('cpq.specifications.attributes.columns.actions', 'Actions')}</th>
1268
1268
  </tr>
1269
1269
  </thead>
1270
1270
  <tbody>
@@ -1275,14 +1275,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1275
1275
  <td className="px-4 py-3">{attr.attributeType}</td>
1276
1276
  <td className="px-4 py-3">
1277
1277
  <Tag variant={attr.resolutionTime === 'design_time' ? 'info' : 'neutral'} className="px-2 text-xs">
1278
- {attr.resolutionTime === 'design_time' ? 'design' : 'run'}
1278
+ {attr.resolutionTime === 'design_time' ? t('cpq.specifications.attributes.resolutionShort.design', 'design') : t('cpq.specifications.attributes.resolutionShort.run', 'run')}
1279
1279
  </Tag>
1280
1280
  </td>
1281
- <td className="px-4 py-3 text-muted-foreground">{attr.options ? `${attr.options.length} options` : '—'}</td>
1282
- <td className="px-4 py-3">{attr.isRequired ? 'Yes' : 'No'}</td>
1281
+ <td className="px-4 py-3 text-muted-foreground">{attr.options ? t('cpq.specifications.attributes.optionsCount', '{count} options', { count: attr.options.length }) : '—'}</td>
1282
+ <td className="px-4 py-3">{attr.isRequired ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}</td>
1283
1283
  <td className="px-4 py-3 text-right">
1284
- <button onClick={() => startEditAttr(attr)} className="text-xs text-primary hover:underline mr-2">Edit</button>
1285
- <button onClick={() => deleteAttribute(attr.id)} className="text-xs text-destructive hover:underline">Delete</button>
1284
+ <button onClick={() => startEditAttr(attr)} className="text-xs text-primary hover:underline mr-2">{t('cpq.common.edit', 'Edit')}</button>
1285
+ <button onClick={() => deleteAttribute(attr.id)} className="text-xs text-destructive hover:underline">{t('cpq.common.delete', 'Delete')}</button>
1286
1286
  </td>
1287
1287
  </tr>
1288
1288
  ))}
@@ -1308,39 +1308,39 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1308
1308
  {editingOffering && (
1309
1309
  <div className="rounded-lg border bg-card p-4 space-y-3">
1310
1310
  <h3 className="font-medium text-sm">
1311
- {editingOffering.id ? 'Edit Offering' : 'New Offering'}
1311
+ {editingOffering.id ? t('cpq.specifications.offerings.form.editTitle', 'Edit Offering') : t('cpq.specifications.offerings.form.newTitle', 'New Offering')}
1312
1312
  </h3>
1313
1313
  <div className="grid grid-cols-4 gap-3">
1314
1314
  <div>
1315
- <label className="block text-xs font-medium mb-1">Code</label>
1316
- <input type="text" value={editingOffering.code} onChange={(e) => setEditingOffering({ ...editingOffering, code: e.target.value })} placeholder="e.g. gix-cloud-connect-aws" className="w-full rounded-md border px-2 py-1.5 text-sm" />
1315
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.code', 'Code')}</label>
1316
+ <input type="text" value={editingOffering.code} onChange={(e) => setEditingOffering({ ...editingOffering, code: e.target.value })} placeholder={t('cpq.specifications.offerings.fields.codePlaceholder', 'e.g. gix-cloud-connect-aws')} className="w-full rounded-md border px-2 py-1.5 text-sm" />
1317
1317
  </div>
1318
1318
  <div>
1319
- <label className="block text-xs font-medium mb-1">Name</label>
1319
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.name', 'Name')}</label>
1320
1320
  <input type="text" value={editingOffering.name} onChange={(e) => setEditingOffering({ ...editingOffering, name: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
1321
1321
  </div>
1322
1322
  <div>
1323
- <label className="block text-xs font-medium mb-1">Lifecycle Status</label>
1323
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.lifecycleStatus', 'Lifecycle Status')}</label>
1324
1324
  <select value={editingOffering.lifecycleStatus} onChange={(e) => setEditingOffering({ ...editingOffering, lifecycleStatus: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
1325
- <option value="draft">Draft</option>
1326
- <option value="active">Active</option>
1327
- <option value="deprecated">Deprecated</option>
1325
+ <option value="draft">{t('cpq.offerings.lifecycle.draft', 'Draft')}</option>
1326
+ <option value="active">{t('cpq.offerings.lifecycle.active', 'Active')}</option>
1327
+ <option value="deprecated">{t('cpq.offerings.lifecycle.deprecated', 'Deprecated')}</option>
1328
1328
  </select>
1329
1329
  </div>
1330
1330
  <div>
1331
- <label className="block text-xs font-medium mb-1">Is Active?</label>
1331
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.isActive', 'Is Active?')}</label>
1332
1332
  <div className="flex items-center h-[30px]">
1333
1333
  <input type="checkbox" checked={editingOffering.isActive} onChange={(e) => setEditingOffering({ ...editingOffering, isActive: e.target.checked })} className="rounded border" />
1334
1334
  </div>
1335
1335
  </div>
1336
1336
  </div>
1337
1337
  <div>
1338
- <label className="block text-xs font-medium mb-1">Description</label>
1338
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.description', 'Description')}</label>
1339
1339
  <input type="text" value={editingOffering.description ?? ''} onChange={(e) => setEditingOffering({ ...editingOffering, description: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
1340
1340
  </div>
1341
1341
  {designTimeAttrs.length > 0 ? (
1342
1342
  <div className="space-y-3">
1343
- <label className="block text-xs font-medium">Design-Time Values</label>
1343
+ <label className="block text-xs font-medium">{t('cpq.specifications.offerings.fields.designTimeValues', 'Design-Time Values')}</label>
1344
1344
  <div className="grid grid-cols-2 gap-3">
1345
1345
  {designTimeAttrs.map((attr) => (
1346
1346
  <div key={attr.code}>
@@ -1354,7 +1354,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1354
1354
  onChange={(e) => setEditingDtValues({ ...editingDtValues, [attr.code]: e.target.value || null })}
1355
1355
  className="w-full rounded-md border px-2 py-1.5 text-sm"
1356
1356
  >
1357
- <option value="">Select {attr.name}...</option>
1357
+ <option value="">{t('cpq.specifications.offerings.fields.selectAttribute', 'Select {name}...', { name: attr.name })}</option>
1358
1358
  {attr.options.map((opt) => (
1359
1359
  <option key={opt.value} value={opt.value}>{opt.label}</option>
1360
1360
  ))}
@@ -1392,32 +1392,32 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1392
1392
  </div>
1393
1393
  ) : (
1394
1394
  <div className="text-xs text-muted-foreground py-1">
1395
- No design-time attributes defined on this specification. All attributes are run-time.
1395
+ {t('cpq.specifications.offerings.noDesignTimeAttributes', 'No design-time attributes defined on this specification. All attributes are run-time.')}
1396
1396
  </div>
1397
1397
  )}
1398
1398
  <div className="flex gap-2">
1399
1399
  <button onClick={saveOffering} disabled={saving} 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">
1400
- {saving ? 'Saving...' : 'Save'}
1400
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
1401
1401
  </button>
1402
- <button onClick={() => setEditingOffering(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
1402
+ <button onClick={() => setEditingOffering(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>
1403
1403
  </div>
1404
1404
  </div>
1405
1405
  )}
1406
1406
 
1407
1407
  {offerings.length === 0 ? (
1408
1408
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
1409
- No offerings created. An offering is a sellable instance of this specification with fixed design-time values and attached charges.
1409
+ {t('cpq.specifications.offerings.empty', 'No offerings created. An offering is a sellable instance of this specification with fixed design-time values and attached charges.')}
1410
1410
  </div>
1411
1411
  ) : (
1412
1412
  <div className="rounded-lg border">
1413
1413
  <table className="w-full text-sm">
1414
1414
  <thead>
1415
1415
  <tr className="border-b bg-muted/50">
1416
- <th className="px-3 py-3 text-left font-medium">Code</th>
1417
- <th className="px-3 py-3 text-left font-medium">Name</th>
1418
- <th className="px-3 py-3 text-left font-medium">Status</th>
1419
- <th className="px-3 py-3 text-left font-medium">Design-Time Values</th>
1420
- <th className="px-3 py-3 text-right font-medium">Actions</th>
1416
+ <th className="px-3 py-3 text-left font-medium">{t('cpq.specifications.offerings.columns.code', 'Code')}</th>
1417
+ <th className="px-3 py-3 text-left font-medium">{t('cpq.specifications.offerings.columns.name', 'Name')}</th>
1418
+ <th className="px-3 py-3 text-left font-medium">{t('cpq.specifications.offerings.columns.status', 'Status')}</th>
1419
+ <th className="px-3 py-3 text-left font-medium">{t('cpq.specifications.offerings.columns.designTimeValues', 'Design-Time Values')}</th>
1420
+ <th className="px-3 py-3 text-right font-medium">{t('cpq.specifications.offerings.columns.actions', 'Actions')}</th>
1421
1421
  </tr>
1422
1422
  </thead>
1423
1423
  <tbody>
@@ -1440,8 +1440,8 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1440
1440
  : '—'}
1441
1441
  </td>
1442
1442
  <td className="px-3 py-3 text-right">
1443
- <button onClick={(e) => { e.stopPropagation(); startEditOffering(o) }} className="text-xs text-primary hover:underline mr-2">Edit</button>
1444
- <button onClick={(e) => { e.stopPropagation(); deleteOffering(o.id) }} className="text-xs text-destructive hover:underline">Delete</button>
1443
+ <button onClick={(e) => { e.stopPropagation(); startEditOffering(o) }} className="text-xs text-primary hover:underline mr-2">{t('cpq.common.edit', 'Edit')}</button>
1444
+ <button onClick={(e) => { e.stopPropagation(); deleteOffering(o.id) }} className="text-xs text-destructive hover:underline">{t('cpq.common.delete', 'Delete')}</button>
1445
1445
  </td>
1446
1446
  </tr>
1447
1447
  ))}
@@ -1467,21 +1467,21 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1467
1467
  {editingRel && (
1468
1468
  <div className="rounded-lg border bg-card p-4 space-y-3">
1469
1469
  <h3 className="font-medium text-sm">
1470
- {editingRel.id ? 'Edit Relationship' : 'New Relationship'}
1470
+ {editingRel.id ? t('cpq.specifications.relationships.form.editTitle', 'Edit Relationship') : t('cpq.specifications.relationships.form.newTitle', 'New Relationship')}
1471
1471
  </h3>
1472
1472
  <div className="grid grid-cols-3 gap-3">
1473
1473
  <div>
1474
- <label className="block text-xs font-medium mb-1">Type</label>
1474
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.type', 'Type')}</label>
1475
1475
  <select value={editingRel.relationshipType} onChange={(e) => setEditingRel({ ...editingRel, relationshipType: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
1476
- <option value="parent_child">Parent / Child</option>
1477
- <option value="requires">Requires</option>
1478
- <option value="excludes">Excludes</option>
1476
+ <option value="parent_child">{t('cpq.specifications.relationships.type.parentChild', 'Parent / Child')}</option>
1477
+ <option value="requires">{t('cpq.specifications.relationships.type.requires', 'Requires')}</option>
1478
+ <option value="excludes">{t('cpq.specifications.relationships.type.excludes', 'Excludes')}</option>
1479
1479
  </select>
1480
1480
  </div>
1481
1481
  <div>
1482
- <label className="block text-xs font-medium mb-1">Target Specification</label>
1482
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.targetSpecification', 'Target Specification')}</label>
1483
1483
  <select value={editingRel.targetSpecId} onChange={(e) => setEditingRel({ ...editingRel, targetSpecId: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
1484
- <option value="">Select...</option>
1484
+ <option value="">{t('cpq.specifications.relationships.fields.selectTarget', 'Select...')}</option>
1485
1485
  {allSpecs.filter((s) => s.id !== specId).map((s) => (
1486
1486
  <option key={s.id} value={s.id}>{s.name} ({s.code})</option>
1487
1487
  ))}
@@ -1489,7 +1489,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1489
1489
  </div>
1490
1490
  <div className="grid grid-cols-2 gap-2">
1491
1491
  <div>
1492
- <label className="block text-xs font-medium mb-1">Min</label>
1492
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.min', 'Min')}</label>
1493
1493
  <NumberInput
1494
1494
  integer
1495
1495
  min={0}
@@ -1498,7 +1498,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1498
1498
  />
1499
1499
  </div>
1500
1500
  <div>
1501
- <label className="block text-xs font-medium mb-1">Max</label>
1501
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.max', 'Max')}</label>
1502
1502
  <NumberInput
1503
1503
  integer
1504
1504
  min={0}
@@ -1510,33 +1510,33 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1510
1510
  </div>
1511
1511
  </div>
1512
1512
  <div>
1513
- <label className="block text-xs font-medium mb-1">Condition (JSON)</label>
1514
- <textarea value={conditionJson} onChange={(e) => setConditionJson(e.target.value)} rows={3} className="w-full rounded-md border px-2 py-1.5 text-sm font-mono" placeholder='null or {"attribute":"direction","operator":"in","value":["inbound"]}' />
1513
+ <label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.condition', 'Condition (JSON)')}</label>
1514
+ <textarea value={conditionJson} onChange={(e) => setConditionJson(e.target.value)} rows={3} className="w-full rounded-md border px-2 py-1.5 text-sm font-mono" placeholder={t('cpq.specifications.relationships.fields.conditionPlaceholder', 'null or {"attribute":"direction","operator":"in","value":["inbound"]}')} />
1515
1515
  </div>
1516
1516
  <div className="flex gap-2">
1517
1517
  <button onClick={saveRelationship} disabled={saving} 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">
1518
- {saving ? 'Saving...' : 'Save'}
1518
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
1519
1519
  </button>
1520
- <button onClick={() => setEditingRel(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
1520
+ <button onClick={() => setEditingRel(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>
1521
1521
  </div>
1522
1522
  </div>
1523
1523
  )}
1524
1524
 
1525
1525
  {relationships.length === 0 ? (
1526
1526
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
1527
- No relationships defined. Declare parent/child, requires, or excludes relationships with other specifications.
1527
+ {t('cpq.specifications.relationships.empty', 'No relationships defined. Declare parent/child, requires, or excludes relationships with other specifications.')}
1528
1528
  </div>
1529
1529
  ) : (
1530
1530
  <div className="rounded-lg border">
1531
1531
  <table className="w-full text-sm">
1532
1532
  <thead>
1533
1533
  <tr className="border-b bg-muted/50">
1534
- <th className="px-4 py-3 text-left font-medium">Type</th>
1535
- <th className="px-4 py-3 text-left font-medium">Source</th>
1536
- <th className="px-4 py-3 text-left font-medium">Target</th>
1537
- <th className="px-4 py-3 text-left font-medium">Cardinality</th>
1538
- <th className="px-4 py-3 text-left font-medium">Condition</th>
1539
- <th className="px-4 py-3 text-right font-medium">Actions</th>
1534
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.type', 'Type')}</th>
1535
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.source', 'Source')}</th>
1536
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.target', 'Target')}</th>
1537
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.cardinality', 'Cardinality')}</th>
1538
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.condition', 'Condition')}</th>
1539
+ <th className="px-4 py-3 text-right font-medium">{t('cpq.specifications.relationships.columns.actions', 'Actions')}</th>
1540
1540
  </tr>
1541
1541
  </thead>
1542
1542
  <tbody>
@@ -1558,8 +1558,8 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1558
1558
  <td className="px-4 py-3 text-muted-foreground">{rel.cardinalityMin}..{rel.cardinalityMax ?? '∞'}</td>
1559
1559
  <td className="px-4 py-3 text-muted-foreground text-xs">{rel.condition ? JSON.stringify(rel.condition) : '—'}</td>
1560
1560
  <td className="px-4 py-3 text-right">
1561
- <button onClick={() => startEditRel(rel)} className="text-xs text-primary hover:underline mr-2">Edit</button>
1562
- <button onClick={() => deleteRelationship(rel.id)} className="text-xs text-destructive hover:underline">Delete</button>
1561
+ <button onClick={() => startEditRel(rel)} className="text-xs text-primary hover:underline mr-2">{t('cpq.common.edit', 'Edit')}</button>
1562
+ <button onClick={() => deleteRelationship(rel.id)} className="text-xs text-destructive hover:underline">{t('cpq.common.delete', 'Delete')}</button>
1563
1563
  </td>
1564
1564
  </tr>
1565
1565
  ))}
@@ -1578,49 +1578,49 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1578
1578
  onClick={() => setEditingSlot({ ...EMPTY_SLOT })}
1579
1579
  className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
1580
1580
  >
1581
- + Add Slot
1581
+ {t('cpq.specifications.slots.add', '+ Add Slot')}
1582
1582
  </button>
1583
1583
  </div>
1584
1584
 
1585
1585
  {editingSlot && (
1586
1586
  <div className="rounded-lg border bg-card p-6 space-y-4">
1587
- <h3 className="text-sm font-semibold">{editingSlot.id ? 'Edit Bundle Slot' : 'New Bundle Slot'}</h3>
1587
+ <h3 className="text-sm font-semibold">{editingSlot.id ? t('cpq.specifications.slots.form.editTitle', 'Edit Bundle Slot') : t('cpq.specifications.slots.form.newTitle', 'New Bundle Slot')}</h3>
1588
1588
  <div className="grid grid-cols-2 gap-4">
1589
1589
  <div>
1590
- <label className="block text-sm font-medium mb-1">Name</label>
1590
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.name', 'Name')}</label>
1591
1591
  <input
1592
1592
  type="text"
1593
1593
  value={editingSlot.name}
1594
1594
  onChange={(e) => setEditingSlot({ ...editingSlot, name: e.target.value })}
1595
- placeholder="e.g. Internet Component"
1595
+ placeholder={t('cpq.specifications.slots.fields.namePlaceholder', 'e.g. Internet Component')}
1596
1596
  className="w-full rounded-md border px-3 py-2 text-sm"
1597
1597
  />
1598
1598
  </div>
1599
1599
  <div>
1600
- <label className="block text-sm font-medium mb-1">Component Group</label>
1600
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.componentGroup', 'Component Group')}</label>
1601
1601
  <input
1602
1602
  type="text"
1603
1603
  value={editingSlot.componentGroup}
1604
1604
  onChange={(e) => setEditingSlot({ ...editingSlot, componentGroup: e.target.value })}
1605
- placeholder="e.g. internet, tv, phone"
1605
+ placeholder={t('cpq.specifications.slots.fields.componentGroupPlaceholder', 'e.g. internet, tv, phone')}
1606
1606
  className="w-full rounded-md border px-3 py-2 text-sm"
1607
1607
  />
1608
1608
  </div>
1609
1609
  <div>
1610
- <label className="block text-sm font-medium mb-1">Target Specification</label>
1610
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.targetSpecification', 'Target Specification')}</label>
1611
1611
  <select
1612
1612
  value={editingSlot.targetSpecId}
1613
1613
  onChange={(e) => setEditingSlot({ ...editingSlot, targetSpecId: e.target.value })}
1614
1614
  className="w-full rounded-md border px-3 py-2 text-sm"
1615
1615
  >
1616
- <option value="">— Select specification —</option>
1616
+ <option value="">{t('cpq.specifications.slots.fields.selectSpecification', '— Select specification —')}</option>
1617
1617
  {allSpecs.filter((s) => s.id !== specId).map((s) => (
1618
1618
  <option key={s.id} value={s.id}>{s.name} ({s.code})</option>
1619
1619
  ))}
1620
1620
  </select>
1621
1621
  </div>
1622
1622
  <div>
1623
- <label className="block text-sm font-medium mb-1">Sort Order</label>
1623
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.sortOrder', 'Sort Order')}</label>
1624
1624
  <NumberInput
1625
1625
  integer
1626
1626
  value={editingSlot.sortOrder}
@@ -1628,7 +1628,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1628
1628
  />
1629
1629
  </div>
1630
1630
  <div>
1631
- <label className="block text-sm font-medium mb-1">Cardinality Min</label>
1631
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.cardinalityMin', 'Cardinality Min')}</label>
1632
1632
  <NumberInput
1633
1633
  integer
1634
1634
  min={0}
@@ -1637,18 +1637,18 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1637
1637
  />
1638
1638
  </div>
1639
1639
  <div>
1640
- <label className="block text-sm font-medium mb-1">Cardinality Max</label>
1640
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.cardinalityMax', 'Cardinality Max')}</label>
1641
1641
  <NumberInput
1642
1642
  integer
1643
1643
  min={0}
1644
- placeholder="∞ (unlimited)"
1644
+ placeholder={t('cpq.specifications.slots.fields.cardinalityMaxPlaceholder', '∞ (unlimited)')}
1645
1645
  value={editingSlot.cardinalityMax}
1646
1646
  onChange={(n) => setEditingSlot({ ...editingSlot, cardinalityMax: n })}
1647
1647
  />
1648
1648
  </div>
1649
1649
  </div>
1650
1650
  <div>
1651
- <label className="block text-sm font-medium mb-1">Description</label>
1651
+ <label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.description', 'Description')}</label>
1652
1652
  <textarea
1653
1653
  value={editingSlot.description ?? ''}
1654
1654
  onChange={(e) => setEditingSlot({ ...editingSlot, description: e.target.value })}
@@ -1673,7 +1673,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1673
1673
  })
1674
1674
  if (!res.ok) {
1675
1675
  const d = await res.json()
1676
- setError(d.error ?? 'Failed to save slot')
1676
+ setError(d.error ?? t('cpq.specifications.slots.error.save', 'Failed to save slot'))
1677
1677
  return
1678
1678
  }
1679
1679
  setEditingSlot(null)
@@ -1683,7 +1683,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1683
1683
  setBundleSlots(d.items ?? [])
1684
1684
  }
1685
1685
  } catch {
1686
- setError('Failed to save slot')
1686
+ setError(t('cpq.specifications.slots.error.save', 'Failed to save slot'))
1687
1687
  } finally {
1688
1688
  setSaving(false)
1689
1689
  }
@@ -1691,13 +1691,13 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1691
1691
  disabled={saving || !editingSlot.name || !editingSlot.componentGroup || !editingSlot.targetSpecId}
1692
1692
  className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
1693
1693
  >
1694
- {saving ? 'Saving...' : (editingSlot.id ? 'Update' : 'Create')}
1694
+ {saving ? t('cpq.common.saving', 'Saving...') : (editingSlot.id ? t('cpq.specifications.slots.form.update', 'Update') : t('cpq.specifications.slots.form.create', 'Create'))}
1695
1695
  </button>
1696
1696
  <button
1697
1697
  onClick={() => setEditingSlot(null)}
1698
1698
  className="inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted"
1699
1699
  >
1700
- Cancel
1700
+ {t('cpq.common.cancel', 'Cancel')}
1701
1701
  </button>
1702
1702
  </div>
1703
1703
  </div>
@@ -1705,19 +1705,19 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1705
1705
 
1706
1706
  {bundleSlots.length === 0 && !editingSlot ? (
1707
1707
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
1708
- No bundle slots defined yet. Add slots to define what component specifications can fill this bundle.
1708
+ {t('cpq.specifications.slots.empty', 'No bundle slots defined yet. Add slots to define what component specifications can fill this bundle.')}
1709
1709
  </div>
1710
1710
  ) : bundleSlots.length > 0 && (
1711
1711
  <div className="rounded-lg border overflow-hidden">
1712
1712
  <table className="w-full text-sm">
1713
1713
  <thead>
1714
1714
  <tr className="border-b bg-muted/50">
1715
- <th className="px-4 py-3 text-left font-medium">Name</th>
1716
- <th className="px-4 py-3 text-left font-medium">Group</th>
1717
- <th className="px-4 py-3 text-left font-medium">Target Spec</th>
1718
- <th className="px-4 py-3 text-left font-medium">Cardinality</th>
1719
- <th className="px-4 py-3 text-left font-medium">Order</th>
1720
- <th className="px-4 py-3 text-right font-medium">Actions</th>
1715
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.name', 'Name')}</th>
1716
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.group', 'Group')}</th>
1717
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.targetSpec', 'Target Spec')}</th>
1718
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.cardinality', 'Cardinality')}</th>
1719
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.order', 'Order')}</th>
1720
+ <th className="px-4 py-3 text-right font-medium">{t('cpq.specifications.slots.columns.actions', 'Actions')}</th>
1721
1721
  </tr>
1722
1722
  </thead>
1723
1723
  <tbody>
@@ -1732,11 +1732,11 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1732
1732
  <span>
1733
1733
  {slot.targetSpec.name}
1734
1734
  {slot.targetSpec.specType === 'bundle' && (
1735
- <Tag variant="brand" className="ml-1 px-1.5 text-[10px]">bundle</Tag>
1735
+ <Tag variant="brand" className="ml-1 px-1.5 text-[10px]">{t('cpq.specifications.slots.tag.bundle', 'bundle')}</Tag>
1736
1736
  )}
1737
1737
  </span>
1738
1738
  ) : (
1739
- <span className="text-muted-foreground">Unknown</span>
1739
+ <span className="text-muted-foreground">{t('cpq.specifications.slots.unknown', 'Unknown')}</span>
1740
1740
  )}
1741
1741
  </td>
1742
1742
  <td className="px-4 py-3 text-muted-foreground">{slot.cardinalityMin}..{slot.cardinalityMax ?? '∞'}</td>
@@ -1755,11 +1755,11 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1755
1755
  })}
1756
1756
  className="text-xs text-primary hover:underline mr-2"
1757
1757
  >
1758
- Edit
1758
+ {t('cpq.common.edit', 'Edit')}
1759
1759
  </button>
1760
1760
  <button
1761
1761
  onClick={async () => {
1762
- if (!confirm('Delete this bundle slot?')) return
1762
+ if (!confirm(t('cpq.specifications.slots.confirmDelete', 'Delete this bundle slot?'))) return
1763
1763
  try {
1764
1764
  const res = await fetch('/api/cpq/bundle-slots', {
1765
1765
  method: 'DELETE',
@@ -1770,15 +1770,15 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
1770
1770
  setBundleSlots((prev) => prev.filter((s) => s.id !== slot.id))
1771
1771
  } else {
1772
1772
  const d = await res.json()
1773
- setError(d.error ?? 'Failed to delete slot')
1773
+ setError(d.error ?? t('cpq.specifications.slots.error.delete', 'Failed to delete slot'))
1774
1774
  }
1775
1775
  } catch {
1776
- setError('Failed to delete slot')
1776
+ setError(t('cpq.specifications.slots.error.delete', 'Failed to delete slot'))
1777
1777
  }
1778
1778
  }}
1779
1779
  className="text-xs text-destructive hover:underline"
1780
1780
  >
1781
- Delete
1781
+ {t('cpq.common.delete', 'Delete')}
1782
1782
  </button>
1783
1783
  </td>
1784
1784
  </tr>