@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
@@ -12,6 +12,12 @@ import {
12
12
  type OrderCpqStatus,
13
13
  } from '../../../../components/statusMaps'
14
14
  import { StatusPath } from '../../../../components/StatusPath'
15
+ import { ChargeBreakdownTable, type ChargeRowView } from '../../../../components/ChargeBreakdownTable'
16
+ import { PricingSummaryCard, type PricingSummaryView } from '../../../../components/PricingSummaryCard'
17
+ import { ArcLineDiff, LineActionBadge } from '../../../../components/ArcLineDiff'
18
+ import { arcRecurringDelta } from '../../../../lib/quoteSummaryView'
19
+ import { isArcCancelledLine } from '../../../../services/arcLineRemoval'
20
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
15
21
 
16
22
  // ─── Types ───────────────────────────────────────────────────────
17
23
 
@@ -24,7 +30,7 @@ type ArcLineSource = {
24
30
  }
25
31
 
26
32
  type TierBreakdownEntry = { tier: number; from: number; to: number; pricePerUnit: number; quantity: number; subtotal: number }
27
- type RuleAdjustment = { ruleCode: string; ruleName: string; ruleType: string; value: number; unitPriceBefore: number; unitPriceAfter: number; delta: number }
33
+ type RuleAdjustment = { ruleCode: string; ruleName: string; ruleType: string; value: number; unitPriceBefore: number; unitPriceAfter: number; delta: number; origin?: 'rule' | 'manual'; adjustmentId?: string; reasonCode?: string }
28
34
 
29
35
  type ResolvedCharge = {
30
36
  chargeName?: string
@@ -36,6 +42,15 @@ type ResolvedCharge = {
36
42
  note?: string | null
37
43
  breakdown?: { tiers: TierBreakdownEntry[] } | null
38
44
  adjustments?: RuleAdjustment[] | null
45
+ listUnitPrice?: number
46
+ listTotalPrice?: number | null
47
+ standardUnitPrice?: number
48
+ standardTotalPrice?: number | null
49
+ }
50
+
51
+ /** Order lines carry the quote's stored waterfall verbatim (no recalculation at conversion). */
52
+ function toChargeRow(c: ResolvedCharge): ChargeRowView {
53
+ return { ...c, chargeName: c.chargeName ?? '—', chargeType: (c.chargeType ?? 'mrc') as 'nrc' | 'mrc' | 'usage', unitPrice: c.unitPrice ?? 0, totalPrice: c.totalPrice ?? null }
39
54
  }
40
55
 
41
56
  type OrderLineResult = {
@@ -68,11 +83,7 @@ type OrderResult = {
68
83
  sourceQuoteId: string | null
69
84
  quoteType: string
70
85
  currencyCode: string
71
- pricingSummary: {
72
- nrcTotal: number
73
- mrcTotal: number
74
- currencyCode: string
75
- }
86
+ pricingSummary: PricingSummaryView & { currencyCode: string }
76
87
  activatedAt: string | null
77
88
  createdAt: string
78
89
  lines: OrderLineResult[]
@@ -84,19 +95,19 @@ const ARC_BADGE_VARIANTS: Record<string, 'brand' | 'success' | 'error'> = {
84
95
  cancel: 'error',
85
96
  }
86
97
 
87
- const RULE_TYPE_LABELS: Record<string, string> = {
88
- discount_percent: '% discount',
89
- discount_absolute: 'fixed discount',
90
- surcharge_percent: '% surcharge',
91
- surcharge_absolute: 'fixed surcharge',
92
- price_override: 'override',
98
+ function quoteTypeLabel(t: TranslateFn, quoteType: string): string {
99
+ switch (quoteType) {
100
+ case 'amend':
101
+ return t('cpq.quotes.type.amend', 'amend')
102
+ case 'renew':
103
+ return t('cpq.quotes.type.renew', 'renew')
104
+ case 'cancel':
105
+ return t('cpq.quotes.type.cancel', 'cancel')
106
+ default:
107
+ return quoteType
108
+ }
93
109
  }
94
110
 
95
- function fmtRuleValue(adj: RuleAdjustment, currency?: string): string {
96
- if (adj.ruleType === 'discount_percent' || adj.ruleType === 'surcharge_percent') return `${adj.value}%`
97
- if (currency) return fmt(adj.value, currency)
98
- return String(adj.value)
99
- }
100
111
 
101
112
  // ─── Constants ───────────────────────────────────────────────────
102
113
 
@@ -104,10 +115,10 @@ function fmt(amount: number, currency: string): string {
104
115
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
105
116
  }
106
117
 
107
- async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
118
+ async function apiJson<T>(t: TranslateFn, url: string, init?: RequestInit): Promise<T> {
108
119
  const res = await fetch(url, init)
109
120
  const json = await res.json()
110
- if (!res.ok) throw new Error(json?.error ?? `Request failed (${res.status})`)
121
+ if (!res.ok) throw new Error(json?.error ?? t('cpq.common.requestFailed', 'Request failed ({status})', { status: res.status }))
111
122
  return json as T
112
123
  }
113
124
 
@@ -115,6 +126,7 @@ async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
115
126
 
116
127
  export default function CpqOrderDetailPage(props: { params?: { id?: string } }) {
117
128
  const router = useRouter()
129
+ const t = useT()
118
130
  const urlParams = useParams()
119
131
  const cpqOrderId = (props.params?.id ?? urlParams?.id) as string
120
132
 
@@ -153,8 +165,8 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
153
165
  .then(async (d) => {
154
166
  const targets: Array<{ subscriptionId: string }> = d?.items ?? []
155
167
  const fetched = await Promise.all(
156
- targets.map((t) =>
157
- fetch(`/api/cpq/inventory/subscriptions?id=${t.subscriptionId}`)
168
+ targets.map((target) =>
169
+ fetch(`/api/cpq/inventory/subscriptions?id=${target.subscriptionId}`)
158
170
  .then((r) => (r.ok ? r.json() : null))
159
171
  .catch(() => null),
160
172
  ),
@@ -183,7 +195,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
183
195
  let cancelled = false
184
196
  async function load() {
185
197
  try {
186
- const result = await apiJson<OrderResult>(`/api/cpq/orders/${cpqOrderId}`)
198
+ const result = await apiJson<OrderResult>(t, `/api/cpq/orders/${cpqOrderId}`)
187
199
  if (!cancelled) {
188
200
  setOrder(result)
189
201
  fetch(`/api/customers/companies?id=${result.customerId}&pageSize=1`)
@@ -206,14 +218,14 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
206
218
  }
207
219
  load()
208
220
  return () => { cancelled = true }
209
- }, [cpqOrderId, fetchLinkedSubscriptions])
221
+ }, [cpqOrderId, fetchLinkedSubscriptions, t])
210
222
 
211
223
  const activateOrder = async () => {
212
224
  if (!order) return
213
225
  try {
214
226
  setActivating(true)
215
227
  setError(null)
216
- const result = await apiJson<OrderResult>(`/api/cpq/orders/${order.id}/activate`, {
228
+ const result = await apiJson<OrderResult>(t, `/api/cpq/orders/${order.id}/activate`, {
217
229
  method: 'POST',
218
230
  headers: { 'Content-Type': 'application/json' },
219
231
  body: JSON.stringify({}),
@@ -231,7 +243,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
231
243
  if (!order) return
232
244
  try {
233
245
  setError(null)
234
- const result = await apiJson<OrderResult>(`/api/cpq/orders/${order.id}/status`, {
246
+ const result = await apiJson<OrderResult>(t, `/api/cpq/orders/${order.id}/status`, {
235
247
  method: 'POST',
236
248
  headers: { 'Content-Type': 'application/json' },
237
249
  body: JSON.stringify({ targetStatus: 'cancelled' }),
@@ -251,7 +263,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
251
263
  if (loading) {
252
264
  return (
253
265
  <div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
254
- <Spinner /> Loading…
266
+ <Spinner /> {t('cpq.orders.detail.loading', 'Loading…')}
255
267
  </div>
256
268
  )
257
269
  }
@@ -259,9 +271,9 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
259
271
  if (!order) {
260
272
  return (
261
273
  <div className="space-y-4">
262
- <button onClick={() => router.push('/backend/cpq/orders')} className="text-sm text-muted-foreground hover:text-foreground">← Back to Orders</button>
274
+ <button onClick={() => router.push('/backend/cpq/orders')} className="text-sm text-muted-foreground hover:text-foreground">{t('cpq.orders.detail.backToOrders', '← Back to Orders')}</button>
263
275
  {error && <ErrorBanner message={error} />}
264
- <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Order not found.</div>
276
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">{t('cpq.orders.detail.notFound', 'Order not found.')}</div>
265
277
  </div>
266
278
  )
267
279
  }
@@ -288,8 +300,8 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
288
300
  <FormHeader
289
301
  mode="detail"
290
302
  backHref="/backend/cpq/orders"
291
- backLabel="Back to CPQ Orders"
292
- entityTypeLabel="CPQ Order"
303
+ backLabel={t('cpq.orders.detail.backLabel', 'Back to CPQ Orders')}
304
+ entityTypeLabel={t('cpq.orders.detail.entityType', 'CPQ Order')}
293
305
  title={order.orderNumber || order.orderId.slice(0, 8)}
294
306
  statusBadge={
295
307
  <div className="flex flex-wrap items-center gap-2 pt-1">
@@ -298,7 +310,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
298
310
  </Tag>
299
311
  {order.quoteType && order.quoteType !== 'new' && (
300
312
  <Tag variant={ARC_BADGE_VARIANTS[order.quoteType] ?? 'neutral'} className="uppercase">
301
- {order.quoteType}
313
+ {quoteTypeLabel(t, order.quoteType)}
302
314
  </Tag>
303
315
  )}
304
316
  </div>
@@ -311,7 +323,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
311
323
  onClick={() => router.push(`/backend/sales/orders/${order.orderId}`)}
312
324
  >
313
325
  <ExternalLink className="h-3.5 w-3.5" />
314
- View Sales Order
326
+ {t('cpq.orders.detail.viewSalesOrder', 'View Sales Order')}
315
327
  </Button>
316
328
  {order.sourceQuoteId && (
317
329
  <Button
@@ -319,19 +331,19 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
319
331
  variant="outline"
320
332
  onClick={() => router.push(`/backend/cpq/quotes/${order.sourceQuoteId}`)}
321
333
  >
322
- Source Quote
334
+ {t('cpq.orders.detail.sourceQuote', 'Source Quote')}
323
335
  </Button>
324
336
  )}
325
337
  {canActivate && (
326
338
  <Button type="button" onClick={activateOrder} disabled={activating}>
327
339
  {activating ? <Spinner /> : <Play className="h-4 w-4" />}
328
- Activate
340
+ {t('cpq.orders.detail.activate', 'Activate')}
329
341
  </Button>
330
342
  )}
331
343
  {canCancel && (
332
344
  <Button type="button" variant="destructive-outline" onClick={cancelOrder}>
333
345
  <X className="h-4 w-4" />
334
- Cancel
346
+ {t('cpq.common.cancel', 'Cancel')}
335
347
  </Button>
336
348
  )}
337
349
  </>
@@ -350,7 +362,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
350
362
  path={['draft', 'pending_activation', 'active', 'fulfilled']}
351
363
  terminals={['cancelled']}
352
364
  statusMap={orderCpqStatusMap}
353
- ariaLabel="Order status path"
365
+ ariaLabel={t('cpq.orders.detail.statusPath', 'Order status path')}
354
366
  />
355
367
  </div>
356
368
 
@@ -370,7 +382,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
370
382
  <div className="flex-1 min-w-0 space-y-1">
371
383
  <div className="flex items-center gap-2 flex-wrap">
372
384
  <span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
373
- Linked Subscription
385
+ {t('cpq.orders.detail.linkedSubscription', 'Linked Subscription')}
374
386
  </span>
375
387
  <Tag
376
388
  variant={sub.status === 'active' ? 'success' : 'info'}
@@ -386,7 +398,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
386
398
  </div>
387
399
  {order.activatedAt && (
388
400
  <div className="text-xs text-muted-foreground">
389
- Activated on {new Date(order.activatedAt).toLocaleString()}
401
+ {t('cpq.orders.detail.activatedOn', 'Activated on {date}', { date: new Date(order.activatedAt).toLocaleString() })}
390
402
  </div>
391
403
  )}
392
404
  </div>
@@ -396,7 +408,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
396
408
  onClick={() => router.push(`/backend/cpq/inventory/subscriptions/${sub.id}`)}
397
409
  >
398
410
  <ArrowRight className="h-3.5 w-3.5" />
399
- Open
411
+ {t('cpq.orders.actions.open', 'Open')}
400
412
  </Button>
401
413
  </div>
402
414
  ))}
@@ -405,7 +417,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
405
417
 
406
418
  {/* Meta cards — secondary reference info, matches the quote layout. */}
407
419
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
408
- <MetaCard label="Customer">
420
+ <MetaCard label={t('cpq.orders.detail.meta.customer', 'Customer')}>
409
421
  {customerName ? (
410
422
  <span className="font-medium">{customerName}</span>
411
423
  ) : (
@@ -414,32 +426,26 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
414
426
  </span>
415
427
  )}
416
428
  </MetaCard>
417
- <MetaCard label="Created">
429
+ <MetaCard label={t('cpq.orders.detail.meta.created', 'Created')}>
418
430
  <span className="font-medium">{new Date(order.createdAt).toLocaleDateString()}</span>
419
431
  </MetaCard>
420
- <MetaCard label="Currency">
432
+ <MetaCard label={t('cpq.orders.detail.meta.currency', 'Currency')}>
421
433
  <span className="font-medium">{currency}</span>
422
434
  </MetaCard>
423
435
  </div>
424
436
 
425
- {/* Pricing Summary Card */}
426
- <div className="rounded-lg border bg-card p-4">
427
- <h3 className="text-sm font-medium mb-3">Pricing Summary</h3>
428
- <div className="grid grid-cols-2 gap-4">
429
- <div>
430
- <p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
431
- <p className="text-lg font-semibold font-mono">{fmt(order.pricingSummary.nrcTotal, currency)}</p>
432
- </div>
433
- <div>
434
- <p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
435
- <p className="text-lg font-semibold font-mono">{fmt(order.pricingSummary.mrcTotal, currency)}</p>
436
- </div>
437
- </div>
438
- </div>
437
+ <PricingSummaryCard
438
+ summary={order.pricingSummary}
439
+ currency={currency}
440
+ arcDelta={order.quoteType === 'amend' || order.quoteType === 'renew'
441
+ ? arcRecurringDelta(order.lines.map((l) => ({ ...l, action: l.action as 'add' | 'modify' | 'cancel' })))
442
+ : null}
443
+ />
439
444
 
440
445
  {/* Order Lines */}
441
446
  <OrderLineTree
442
447
  lines={order.lines}
448
+ quoteType={order.quoteType}
443
449
  currency={currency}
444
450
  expandedLines={expandedLines}
445
451
  pricingDetail={pricingDetail}
@@ -465,6 +471,7 @@ function MetaCard({ label, children }: { label: string; children: React.ReactNod
465
471
 
466
472
  function OrderLineTree({
467
473
  lines,
474
+ quoteType,
468
475
  currency,
469
476
  expandedLines,
470
477
  pricingDetail,
@@ -472,12 +479,14 @@ function OrderLineTree({
472
479
  onTogglePricingDetail,
473
480
  }: {
474
481
  lines: OrderLineResult[]
482
+ quoteType: string
475
483
  currency: string
476
484
  expandedLines: Set<string>
477
485
  pricingDetail: { lineIdx: number; chargeIdx: number } | null
478
486
  onToggleLine: (id: string) => void
479
487
  onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
480
488
  }) {
489
+ const t = useT()
481
490
  const rootLines = lines.filter((l) => !l.parentLineId)
482
491
  const childrenByParent = new Map<string, OrderLineResult[]>()
483
492
  for (const line of lines) {
@@ -495,10 +504,10 @@ function OrderLineTree({
495
504
 
496
505
  return (
497
506
  <div>
498
- <h3 className="text-sm font-medium mb-2 px-1">Line Items ({lines.length})</h3>
507
+ <h3 className="text-sm font-medium mb-2 px-1">{t('cpq.orders.detail.lineItems', 'Line Items ({count})', { count: lines.length })}</h3>
499
508
  {lines.length === 0 ? (
500
509
  <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
501
- No line items.
510
+ {t('cpq.orders.detail.noLineItems', 'No line items.')}
502
511
  </div>
503
512
  ) : (
504
513
  <div className="rounded-lg border bg-card divide-y">
@@ -512,6 +521,7 @@ function OrderLineTree({
512
521
  <OrderLineRow
513
522
  line={line}
514
523
  lineIdx={lineIdx}
524
+ quoteType={quoteType}
515
525
  currency={currency}
516
526
  isExpanded={isExpanded}
517
527
  isBundle={isBundle}
@@ -524,7 +534,7 @@ function OrderLineTree({
524
534
  {isBundle && isExpanded && children.length > 0 && (
525
535
  <div className="border-t border-dashed border-brand-violet/30 bg-brand-violet/5">
526
536
  <div className="px-6 py-1.5">
527
- <span className="text-[10px] font-medium text-brand-violet uppercase tracking-wider">Bundle Components ({children.length})</span>
537
+ <span className="text-[10px] font-medium text-brand-violet uppercase tracking-wider">{t('cpq.lines.bundleComponents', 'Bundle Components ({count})', { count: children.length })}</span>
528
538
  </div>
529
539
  {children.map((child) => {
530
540
  const childIdx = indexOf.get(child.lineId) ?? 0
@@ -533,6 +543,7 @@ function OrderLineTree({
533
543
  key={child.lineId}
534
544
  line={child}
535
545
  lineIdx={childIdx}
546
+ quoteType={quoteType}
536
547
  currency={currency}
537
548
  isExpanded={expandedLines.has(child.lineId)}
538
549
  isBundle={false}
@@ -555,20 +566,26 @@ function OrderLineTree({
555
566
  )
556
567
  }
557
568
 
558
- function OrderLineRow({ line, lineIdx, currency, isExpanded, isBundle, childCount, indent, pricingDetail, onToggle, onTogglePricingDetail }: {
559
- line: OrderLineResult; lineIdx: number; currency: string; isExpanded: boolean; isBundle: boolean; childCount: number; indent: number
569
+ function OrderLineRow({ line, lineIdx, quoteType, currency, isExpanded, isBundle, childCount, indent, pricingDetail, onToggle, onTogglePricingDetail }: {
570
+ line: OrderLineResult; lineIdx: number; quoteType: string; currency: string; isExpanded: boolean; isBundle: boolean; childCount: number; indent: number
560
571
  pricingDetail: { lineIdx: number; chargeIdx: number } | null
561
572
  onToggle: () => void
562
573
  onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
563
574
  }) {
575
+ const t = useT()
564
576
  const paddingLeft = indent > 0 ? `${indent * 2 + 1}rem` : undefined
577
+ // Same presentation as the quote page: an existing item removed on an
578
+ // Amend / Renew quote reads as "remove", struck through.
579
+ const isRemoved = isArcCancelledLine(line, quoteType)
565
580
 
566
581
  return (
567
582
  <div>
568
583
  <div
569
- className="flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors cursor-pointer"
584
+ className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors cursor-pointer ${isRemoved ? 'bg-muted/20' : ''}`}
570
585
  style={paddingLeft ? { paddingLeft } : undefined}
571
586
  onClick={onToggle}
587
+ data-testid="cpq-order-line"
588
+ data-removed={isRemoved ? 'true' : undefined}
572
589
  >
573
590
  <button className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground">
574
591
  <svg className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
@@ -580,23 +597,39 @@ function OrderLineRow({ line, lineIdx, currency, isExpanded, isBundle, childCoun
580
597
  <div className="flex items-center gap-2">
581
598
  {indent > 0 && <span className="text-muted-foreground text-xs">└</span>}
582
599
  {isBundle && (
583
- <Tag variant="brand" className="px-1.5 text-xs">bundle</Tag>
600
+ <Tag variant="brand" className="px-1.5 text-xs">{t('cpq.lines.bundle', 'bundle')}</Tag>
584
601
  )}
585
- <span className="font-medium text-sm">{line.offeringName}</span>
602
+ <span className={`font-medium text-sm ${isRemoved ? 'text-muted-foreground line-through' : ''}`}>{line.offeringName}</span>
586
603
  {line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
587
- <Tag variant="info" className="px-1.5 text-xs">{line.action}</Tag>
588
- {isBundle && <span className="text-xs text-muted-foreground">{childCount} component{childCount !== 1 ? 's' : ''}</span>}
604
+ <LineActionBadge
605
+ action={line.action}
606
+ label={isRemoved ? t('cpq.quotes.arc.lineRemoved', 'remove') : undefined}
607
+ title={isRemoved ? t('cpq.quotes.arc.lineRemovedHint', 'This item is cancelled on the subscription when the order is activated') : undefined}
608
+ />
609
+ {isBundle && (
610
+ <span className="text-xs text-muted-foreground">
611
+ {childCount !== 1
612
+ ? t('cpq.lines.componentCount.other', '{count} components', { count: childCount })
613
+ : t('cpq.lines.componentCount.one', '{count} component', { count: childCount })}
614
+ </span>
615
+ )}
589
616
  </div>
590
617
  </div>
591
618
 
592
619
  <div className="shrink-0 text-right text-xs space-y-0.5">
593
- {line.nrcTotal > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
594
- {line.mrcTotal > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
620
+ {isRemoved && line.arcSource && line.arcSource.mrcAmount > 0 && (
621
+ <div>
622
+ <span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span>
623
+ <span className="font-mono text-muted-foreground line-through">{fmt(line.arcSource.mrcAmount, currency)}</span>
624
+ </div>
625
+ )}
626
+ {line.nrcTotal > 0 && <div><span className="text-muted-foreground">{t('cpq.lines.nrc', 'NRC')} </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
627
+ {line.mrcTotal > 0 && <div><span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
595
628
  </div>
596
629
  </div>
597
630
 
598
631
  {isExpanded && (() => {
599
- const internalKeys = new Set(['_arcMirroredName', '_arcMirroredFromItemId', 'offeringName'])
632
+ const internalKeys = new Set(['_arcMirroredName', '_arcMirroredFromItemId', '_arcPriceFrozen', '_arcRepriced', 'offeringName'])
600
633
  const visibleConfigEntries = Object.entries(line.configuration).filter(
601
634
  ([key]) => !internalKeys.has(key),
602
635
  )
@@ -612,22 +645,20 @@ function OrderLineRow({ line, lineIdx, currency, isExpanded, isBundle, childCoun
612
645
  className="border-t bg-muted/10 px-12 py-3 space-y-3"
613
646
  style={paddingLeft ? { paddingLeft: `calc(${paddingLeft} + 2rem)` } : undefined}
614
647
  >
615
- {hasArcDiff && (
616
- <ArcLineDiff line={line} currency={currency} />
617
- )}
618
648
  {isEmpty ? (
619
649
  <div className="text-xs text-muted-foreground space-y-1">
620
650
  <div>
621
- No detailed breakdown for this line.
622
- {mirroredFromItemId && ' Mirrored from existing subscription item.'}
651
+ {t('cpq.orders.detail.noBreakdown', 'No detailed breakdown for this line.')}
652
+ {mirroredFromItemId && ' '}
653
+ {mirroredFromItemId && t('cpq.orders.detail.mirroredItem', 'Mirrored from existing subscription item.')}
623
654
  </div>
624
655
  {(line.nrcTotal > 0 || line.mrcTotal > 0) && (
625
656
  <div className="flex gap-4">
626
657
  {line.nrcTotal > 0 && (
627
- <span>NRC <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
658
+ <span>{t('cpq.lines.nrc', 'NRC')} <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
628
659
  )}
629
660
  {line.mrcTotal > 0 && (
630
- <span>MRC <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
661
+ <span>{t('cpq.lines.mrc', 'MRC')} <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
631
662
  )}
632
663
  </div>
633
664
  )}
@@ -636,7 +667,7 @@ function OrderLineRow({ line, lineIdx, currency, isExpanded, isBundle, childCoun
636
667
  <>
637
668
  {hasConfig && (
638
669
  <div>
639
- <p className="text-xs font-medium text-muted-foreground mb-1">Configuration</p>
670
+ <p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.lines.configuration', 'Configuration')}</p>
640
671
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
641
672
  {visibleConfigEntries.map(([key, value]) => (
642
673
  <div key={key} className="text-xs">
@@ -649,25 +680,24 @@ function OrderLineRow({ line, lineIdx, currency, isExpanded, isBundle, childCoun
649
680
  )}
650
681
 
651
682
  {hasCharges && (
652
- <div>
653
- <p className="text-xs font-medium text-muted-foreground mb-1">Charges</p>
654
- <ChargeBreakdown
655
- line={line}
656
- lineIdx={lineIdx}
657
- currency={currency}
658
- pricingDetail={pricingDetail}
659
- onTogglePricingDetail={onTogglePricingDetail}
660
- />
661
- </div>
683
+ <ChargeBreakdownTable
684
+ charges={line.charges.map(toChargeRow)}
685
+ currency={currency}
686
+ openDetailIndex={pricingDetail?.lineIdx === lineIdx ? pricingDetail.chargeIdx : null}
687
+ onToggleDetail={(chargeIdx) => onTogglePricingDetail(lineIdx, chargeIdx)}
688
+ />
662
689
  )}
663
690
 
664
691
  {hasTerm && (
665
692
  <div className="flex gap-4 text-xs text-muted-foreground">
666
- {line.startDate && <span>Start: {new Date(line.startDate).toLocaleDateString()}</span>}
667
- {line.termMonths && <span>Term: {line.termMonths}mo</span>}
668
- {line.endDate && <span>End: {new Date(line.endDate).toLocaleDateString()}</span>}
693
+ {line.startDate && <span>{t('cpq.lines.term.start', 'Start: {date}', { date: new Date(line.startDate).toLocaleDateString() })}</span>}
694
+ {line.termMonths && <span>{t('cpq.lines.term.months', 'Term: {months}mo', { months: line.termMonths })}</span>}
695
+ {line.endDate && <span>{t('cpq.lines.term.end', 'End: {date}', { date: new Date(line.endDate).toLocaleDateString() })}</span>}
669
696
  </div>
670
697
  )}
698
+
699
+ {/* XD-250 ARC: before/after preview below the charges, as on the quote. */}
700
+ {hasArcDiff && <ArcLineDiff line={line} currency={currency} />}
671
701
  </>
672
702
  )}
673
703
  </div>
@@ -677,144 +707,6 @@ function OrderLineRow({ line, lineIdx, currency, isExpanded, isBundle, childCoun
677
707
  )
678
708
  }
679
709
 
680
- // Charge breakdown table with the waterfall ("show pricing details")
681
- // toggle. Same shape as the quote page so a charge looks identical on
682
- // both surfaces, including tier-by-tier subtotals and any price-rule
683
- // adjustments that fired during calculation.
684
- function ChargeBreakdown({ line, lineIdx, currency, pricingDetail, onTogglePricingDetail }: {
685
- line: OrderLineResult; lineIdx: number; currency: string
686
- pricingDetail: { lineIdx: number; chargeIdx: number } | null
687
- onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
688
- }) {
689
- return (
690
- <table className="w-full text-xs">
691
- <thead>
692
- <tr className="text-muted-foreground">
693
- <th className="text-left py-1 font-medium">Charge</th>
694
- <th className="text-left py-1 font-medium">Type</th>
695
- <th className="text-right py-1 font-medium">Unit Price</th>
696
- <th className="text-right py-1 font-medium">Qty</th>
697
- <th className="text-right py-1 font-medium">Total</th>
698
- <th className="w-8" />
699
- </tr>
700
- </thead>
701
- <tbody>
702
- {line.charges.map((c, i) => {
703
- const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length)
704
- const isDetailOpen = pricingDetail?.lineIdx === lineIdx && pricingDetail?.chargeIdx === i
705
- return (
706
- <React.Fragment key={i}>
707
- <tr className="border-t border-dashed border-border/50">
708
- <td className="py-1">{c.chargeName ?? '—'}</td>
709
- <td className="py-1 uppercase">{c.chargeType ?? '—'}</td>
710
- <td className="py-1 text-right font-mono">{c.unitPrice != null ? fmt(c.unitPrice, currency) : '—'}</td>
711
- <td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
712
- <td className="py-1 text-right font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : c.note ?? '—'}</td>
713
- <td className="py-1 text-center">
714
- {hasDetail && (
715
- <button
716
- onClick={() => onTogglePricingDetail(lineIdx, i)}
717
- className="inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
718
- title="Show pricing breakdown"
719
- >
720
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-3.5 h-3.5">
721
- <path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
722
- </svg>
723
- </button>
724
- )}
725
- </td>
726
- </tr>
727
- {isDetailOpen && (
728
- <tr>
729
- <td colSpan={6} className="pb-3 pt-1">
730
- <div className="rounded border bg-background p-3 space-y-3 text-xs">
731
- <div className="flex items-center justify-between">
732
- <span className="font-semibold text-sm">Pricing Details — {c.chargeName}</span>
733
- <span className="text-muted-foreground">{c.pricingMethod ?? 'flat'}</span>
734
- </div>
735
-
736
- {c.breakdown?.tiers && c.breakdown.tiers.length > 0 && (
737
- <div>
738
- <div className="font-medium text-muted-foreground mb-1">Tier Breakdown</div>
739
- <table className="w-full">
740
- <thead>
741
- <tr className="text-muted-foreground">
742
- <th className="text-left py-0.5 font-medium">Tier</th>
743
- <th className="text-right py-0.5 font-medium">Range</th>
744
- <th className="text-right py-0.5 font-medium">Price/Unit</th>
745
- <th className="text-right py-0.5 font-medium">Qty</th>
746
- <th className="text-right py-0.5 font-medium">Subtotal</th>
747
- </tr>
748
- </thead>
749
- <tbody>
750
- {c.breakdown.tiers.map((t) => (
751
- <tr key={t.tier} className="border-t border-border/30">
752
- <td className="py-0.5">{t.tier}</td>
753
- <td className="py-0.5 text-right font-mono">{t.from}–{t.to}</td>
754
- <td className="py-0.5 text-right font-mono">{fmt(t.pricePerUnit, currency)}</td>
755
- <td className="py-0.5 text-right font-mono">{t.quantity}</td>
756
- <td className="py-0.5 text-right font-mono">{fmt(t.subtotal, currency)}</td>
757
- </tr>
758
- ))}
759
- <tr className="border-t font-medium">
760
- <td colSpan={4} className="py-0.5 text-right">Tiered Total</td>
761
- <td className="py-0.5 text-right font-mono">
762
- {fmt(c.breakdown.tiers.reduce((s, t) => s + t.subtotal, 0), currency)}
763
- </td>
764
- </tr>
765
- </tbody>
766
- </table>
767
- </div>
768
- )}
769
-
770
- {c.adjustments && c.adjustments.length > 0 && (
771
- <div>
772
- <div className="font-medium text-muted-foreground mb-1">Applied Price Rules</div>
773
- <table className="w-full">
774
- <thead>
775
- <tr className="text-muted-foreground">
776
- <th className="text-left py-0.5 font-medium">Rule</th>
777
- <th className="text-left py-0.5 font-medium">Type</th>
778
- <th className="text-right py-0.5 font-medium">Value</th>
779
- <th className="text-right py-0.5 font-medium">Before</th>
780
- <th className="text-right py-0.5 font-medium">After</th>
781
- <th className="text-right py-0.5 font-medium">Delta</th>
782
- </tr>
783
- </thead>
784
- <tbody>
785
- {c.adjustments.map((adj, ai) => (
786
- <tr key={ai} className="border-t border-border/30">
787
- <td className="py-0.5">{adj.ruleName}</td>
788
- <td className="py-0.5">{RULE_TYPE_LABELS[adj.ruleType] ?? adj.ruleType}</td>
789
- <td className="py-0.5 text-right font-mono">{fmtRuleValue(adj, currency)}</td>
790
- <td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceBefore, currency)}</td>
791
- <td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceAfter, currency)}</td>
792
- <td className={`py-0.5 text-right font-mono ${adj.delta < 0 ? 'text-destructive' : adj.delta > 0 ? 'text-status-success-text' : ''}`}>
793
- {adj.delta > 0 ? '+' : ''}{fmt(adj.delta, currency)}
794
- </td>
795
- </tr>
796
- ))}
797
- </tbody>
798
- </table>
799
- </div>
800
- )}
801
-
802
- <div className="flex items-center justify-between pt-1 border-t text-sm font-semibold">
803
- <span>Final Total</span>
804
- <span className="font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : '—'}</span>
805
- </div>
806
- </div>
807
- </td>
808
- </tr>
809
- )}
810
- </React.Fragment>
811
- )
812
- })}
813
- </tbody>
814
- </table>
815
- )
816
- }
817
-
818
710
  function Spinner() {
819
711
  return (
820
712
  <svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
@@ -825,6 +717,7 @@ function Spinner() {
825
717
  }
826
718
 
827
719
  function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () => void }) {
720
+ const t = useT()
828
721
  return (
829
722
  <Alert variant="destructive" className="flex items-start gap-2">
830
723
  <span className="flex-1">{message}</span>
@@ -832,7 +725,7 @@ function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () =
832
725
  <button
833
726
  onClick={onDismiss}
834
727
  className="text-destructive/70 hover:text-destructive text-sm font-bold"
835
- aria-label="Dismiss"
728
+ aria-label={t('cpq.common.dismiss', 'Dismiss')}
836
729
  >
837
730
  ×
838
731
  </button>
@@ -840,82 +733,3 @@ function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () =
840
733
  </Alert>
841
734
  )
842
735
  }
843
-
844
- // XD-250 ARC: side-by-side "Before / After" snapshot for a quote / order line
845
- // that mirrors an existing subscription item. "Before" comes from the live
846
- // CpqInventorySubscriptionItem at render time; "After" is what the line will
847
- // apply on activation. For 'cancel' lines the after column is greyed out.
848
- function ArcLineDiff({ line, currency }: { line: OrderLineResult; currency: string }) {
849
- const src = line.arcSource
850
- if (!src) return null
851
- const isCancel = line.action === 'cancel'
852
-
853
- const beforeMrc = src.mrcAmount
854
- const beforeNrc = src.nrcAmount
855
- const beforeQty = src.quantity
856
- const afterMrc = isCancel ? 0 : line.mrcTotal
857
- const afterNrc = isCancel ? 0 : line.nrcTotal
858
- const afterQty = isCancel ? 0 : line.quantity
859
- const mrcDelta = afterMrc - beforeMrc
860
- const nrcDelta = afterNrc - beforeNrc
861
- const qtyDelta = afterQty - beforeQty
862
-
863
- return (
864
- <div>
865
- <p className="text-xs font-medium text-muted-foreground mb-1.5">Change Preview</p>
866
- <div className="rounded border bg-background overflow-hidden">
867
- <table className="w-full text-xs">
868
- <thead className="bg-muted/40">
869
- <tr className="text-muted-foreground">
870
- <th className="text-left px-3 py-1.5 font-medium" />
871
- <th className="text-right px-3 py-1.5 font-medium">Before</th>
872
- <th className="text-right px-3 py-1.5 font-medium">After</th>
873
- <th className="text-right px-3 py-1.5 font-medium">Δ</th>
874
- </tr>
875
- </thead>
876
- <tbody>
877
- <ArcDiffRow label="Quantity" before={beforeQty} after={afterQty} delta={qtyDelta} currency={null} dimAfter={isCancel} />
878
- <ArcDiffRow label="MRC" before={beforeMrc} after={afterMrc} delta={mrcDelta} currency={currency} dimAfter={isCancel} />
879
- <ArcDiffRow label="NRC" before={beforeNrc} after={afterNrc} delta={nrcDelta} currency={currency} dimAfter={isCancel} />
880
- </tbody>
881
- </table>
882
- </div>
883
- {isCancel && (
884
- <p className="text-xs text-destructive mt-1 italic">
885
- Cancellation — item will be terminated on activation.
886
- </p>
887
- )}
888
- </div>
889
- )
890
- }
891
-
892
- function ArcDiffRow({
893
- label,
894
- before,
895
- after,
896
- delta,
897
- currency,
898
- dimAfter,
899
- }: {
900
- label: string
901
- before: number
902
- after: number
903
- delta: number
904
- currency: string | null
905
- dimAfter: boolean
906
- }) {
907
- const fmtVal = (v: number) => (currency ? fmt(v, currency) : String(v))
908
- const showDelta = delta !== 0
909
- return (
910
- <tr className="border-t border-dashed border-border/40">
911
- <td className="px-3 py-1 text-muted-foreground">{label}</td>
912
- <td className="px-3 py-1 text-right font-mono">{fmtVal(before)}</td>
913
- <td className={`px-3 py-1 text-right font-mono ${dimAfter ? 'text-muted-foreground line-through' : ''}`}>
914
- {fmtVal(after)}
915
- </td>
916
- <td className={`px-3 py-1 text-right font-mono font-medium ${showDelta ? (delta > 0 ? 'text-status-success-text' : 'text-destructive') : 'text-muted-foreground'}`}>
917
- {showDelta ? `${delta > 0 ? '+' : ''}${fmtVal(delta)}` : '—'}
918
- </td>
919
- </tr>
920
- )
921
- }