@dainamite/cpq 0.4.0 → 0.6.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 (273) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/MIGRATION.md +101 -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/handleArcError.js +2 -2
  8. package/dist/modules/cpq/api/_helpers/handleArcError.js.map +2 -2
  9. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
  10. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
  11. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
  12. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
  13. package/dist/modules/cpq/api/orders/[orderId]/activate/route.js +2 -2
  14. package/dist/modules/cpq/api/orders/[orderId]/activate/route.js.map +2 -2
  15. package/dist/modules/cpq/api/orders/[orderId]/route.js +19 -3
  16. package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
  17. package/dist/modules/cpq/api/orders/[orderId]/status/route.js +2 -2
  18. package/dist/modules/cpq/api/orders/[orderId]/status/route.js.map +2 -2
  19. package/dist/modules/cpq/api/orders/route.js +49 -4
  20. package/dist/modules/cpq/api/orders/route.js.map +2 -2
  21. package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js +2 -2
  22. package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js.map +2 -2
  23. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
  24. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
  25. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +45 -5
  26. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
  27. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
  28. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
  29. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
  30. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
  31. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
  32. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
  33. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
  34. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
  35. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +34 -4
  36. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
  37. package/dist/modules/cpq/api/quotes/[quoteId]/route.js +2 -2
  38. package/dist/modules/cpq/api/quotes/[quoteId]/route.js.map +2 -2
  39. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
  40. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
  41. package/dist/modules/cpq/api/quotes/price/route.js +43 -3
  42. package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
  43. package/dist/modules/cpq/api/quotes/route.js +2 -2
  44. package/dist/modules/cpq/api/quotes/route.js.map +2 -2
  45. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +1 -1
  46. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +1 -1
  47. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +1 -1
  48. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +1 -1
  49. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +22 -8
  50. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  51. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +1 -1
  52. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +89 -232
  53. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  54. package/dist/modules/cpq/backend/cpq/orders/page.js.map +1 -1
  55. package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
  56. package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
  57. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +1 -1
  58. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +1 -1
  59. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +205 -0
  60. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
  61. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
  62. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
  63. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
  64. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
  65. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
  66. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
  67. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
  68. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
  69. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +144 -221
  70. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
  71. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +1 -1
  72. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
  73. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
  74. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +1 -1
  75. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +1 -1
  76. package/dist/modules/cpq/ce.js +8 -0
  77. package/dist/modules/cpq/ce.js.map +2 -2
  78. package/dist/modules/cpq/cli.js +34 -1
  79. package/dist/modules/cpq/cli.js.map +2 -2
  80. package/dist/modules/cpq/commands/price-adjustments.js +107 -0
  81. package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
  82. package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
  83. package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
  84. package/dist/modules/cpq/components/ChargeBreakdownTable.js +154 -0
  85. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
  86. package/dist/modules/cpq/components/CpqListView.js.map +2 -2
  87. package/dist/modules/cpq/components/PriceWaterfall.js +66 -0
  88. package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
  89. package/dist/modules/cpq/components/PricingSummaryCard.js +117 -0
  90. package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
  91. package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
  92. package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
  93. package/dist/modules/cpq/data/entities.js +97 -0
  94. package/dist/modules/cpq/data/entities.js.map +2 -2
  95. package/dist/modules/cpq/data/price-adjustments.js +70 -0
  96. package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
  97. package/dist/modules/cpq/data/validators.js +4 -0
  98. package/dist/modules/cpq/data/validators.js.map +2 -2
  99. package/dist/modules/cpq/di.js +6 -0
  100. package/dist/modules/cpq/di.js.map +2 -2
  101. package/dist/modules/cpq/events.js +47 -0
  102. package/dist/modules/cpq/events.js.map +2 -2
  103. package/dist/modules/cpq/index.js +1 -1
  104. package/dist/modules/cpq/index.js.map +2 -2
  105. package/dist/modules/cpq/lib/auditActor.js +10 -0
  106. package/dist/modules/cpq/lib/auditActor.js.map +7 -0
  107. package/dist/modules/cpq/lib/formatMoney.js +7 -0
  108. package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
  109. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
  110. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
  111. package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
  112. package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
  113. package/dist/modules/cpq/lib/quotePermissions.js +13 -0
  114. package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
  115. package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
  116. package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
  117. package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
  118. package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
  119. package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
  120. package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
  121. package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
  122. package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
  123. package/dist/modules/cpq/services/cpqOrderService.js +34 -30
  124. package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
  125. package/dist/modules/cpq/services/cpqPricingService.js +7 -0
  126. package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
  127. package/dist/modules/cpq/services/cpqQuotingService.js +339 -111
  128. package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
  129. package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
  130. package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
  131. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
  132. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
  133. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
  134. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
  135. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
  136. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
  137. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
  138. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
  139. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
  140. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
  141. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
  142. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
  143. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
  144. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
  145. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
  146. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
  147. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
  148. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
  149. package/dist/modules/cpq/services/types.js.map +2 -2
  150. package/dist/modules/cpq/setup.js +6 -0
  151. package/dist/modules/cpq/setup.js.map +2 -2
  152. package/package.json +9 -9
  153. package/src/modules/cpq/__integration__/TC-CPQ-002.spec.ts +8 -5
  154. package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
  155. package/src/modules/cpq/acl.ts +1 -0
  156. package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
  157. package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
  158. package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
  159. package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
  160. package/src/modules/cpq/api/_helpers/handleArcError.ts +12 -12
  161. package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
  162. package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
  163. package/src/modules/cpq/api/orders/[orderId]/activate/route.ts +2 -2
  164. package/src/modules/cpq/api/orders/[orderId]/route.ts +20 -2
  165. package/src/modules/cpq/api/orders/[orderId]/status/route.ts +2 -2
  166. package/src/modules/cpq/api/orders/route.ts +53 -3
  167. package/src/modules/cpq/api/quotes/[quoteId]/clone/route.ts +2 -2
  168. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
  169. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +51 -4
  170. package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
  171. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
  172. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
  173. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
  174. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
  175. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +42 -3
  176. package/src/modules/cpq/api/quotes/[quoteId]/route.ts +2 -2
  177. package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
  178. package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
  179. package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
  180. package/src/modules/cpq/api/quotes/price/route.ts +28 -2
  181. package/src/modules/cpq/api/quotes/route.ts +2 -2
  182. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +1 -1
  183. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +1 -1
  184. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +1 -1
  185. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +32 -10
  186. package/src/modules/cpq/backend/cpq/offerings/page.tsx +1 -1
  187. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +60 -269
  188. package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
  189. package/src/modules/cpq/backend/cpq/orders/page.tsx +1 -1
  190. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +1 -1
  191. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  192. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
  193. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
  194. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
  195. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
  196. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
  197. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
  198. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +178 -266
  199. package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
  200. package/src/modules/cpq/backend/cpq/quotes/page.tsx +1 -1
  201. package/src/modules/cpq/backend/cpq/specifications/page.tsx +1 -1
  202. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  203. package/src/modules/cpq/ce.ts +8 -0
  204. package/src/modules/cpq/cli.ts +48 -1
  205. package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
  206. package/src/modules/cpq/commands/price-adjustments.ts +90 -0
  207. package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
  208. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +201 -0
  209. package/src/modules/cpq/components/CpqListView.tsx +2 -1
  210. package/src/modules/cpq/components/PriceWaterfall.tsx +94 -0
  211. package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
  212. package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
  213. package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
  214. package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
  215. package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
  216. package/src/modules/cpq/data/entities.ts +83 -0
  217. package/src/modules/cpq/data/price-adjustments.ts +67 -0
  218. package/src/modules/cpq/data/validators.ts +3 -0
  219. package/src/modules/cpq/di.ts +6 -0
  220. package/src/modules/cpq/events.ts +53 -2
  221. package/src/modules/cpq/index.ts +1 -1
  222. package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
  223. package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
  224. package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
  225. package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
  226. package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
  227. package/src/modules/cpq/lib/auditActor.ts +13 -0
  228. package/src/modules/cpq/lib/formatMoney.ts +4 -0
  229. package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
  230. package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
  231. package/src/modules/cpq/lib/quotePermissions.ts +22 -0
  232. package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
  233. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
  234. package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
  235. package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
  236. package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
  237. package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
  238. package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
  239. package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
  240. package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
  241. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
  242. package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
  243. package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
  244. package/src/modules/cpq/services/__tests__/errorBrands.test.ts +72 -0
  245. package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
  246. package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
  247. package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
  248. package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
  249. package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
  250. package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
  251. package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
  252. package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
  253. package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
  254. package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
  255. package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
  256. package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
  257. package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
  258. package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
  259. package/src/modules/cpq/services/cpqOrderService.ts +32 -28
  260. package/src/modules/cpq/services/cpqPricingService.ts +8 -0
  261. package/src/modules/cpq/services/cpqQuotingService.ts +388 -125
  262. package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
  263. package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
  264. package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
  265. package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
  266. package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
  267. package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
  268. package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
  269. package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
  270. package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
  271. package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
  272. package/src/modules/cpq/services/types.ts +34 -1
  273. package/src/modules/cpq/setup.ts +7 -1
@@ -0,0 +1,154 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { Info } from "lucide-react";
6
+ import { SimpleTooltip } from "@open-mercato/ui/primitives/tooltip";
7
+ import { Tag } from "@open-mercato/ui/primitives/tag";
8
+ import { DepthBadge, PriceWaterfall } from "./PriceWaterfall.js";
9
+ import { ADJUSTMENT_ERROR_FALLBACKS } from "../lib/priceAdjustmentView.js";
10
+ import { formatMoney } from "../lib/formatMoney.js";
11
+ function PriceColumnHeader({ label, hint }) {
12
+ return /* @__PURE__ */ jsx("th", { className: "text-right py-1 font-medium", children: /* @__PURE__ */ jsx(SimpleTooltip, { content: hint, size: "sm", children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 cursor-help", tabIndex: 0, "aria-label": `${label}: ${hint}`, children: [
13
+ label,
14
+ /* @__PURE__ */ jsx(Info, { className: "h-3 w-3", "aria-hidden": true })
15
+ ] }) }) });
16
+ }
17
+ function ChargeBreakdownTable({
18
+ charges,
19
+ currency,
20
+ openDetailIndex,
21
+ onToggleDetail,
22
+ reasonLabels,
23
+ renderChargeActions,
24
+ renderManualActions
25
+ }) {
26
+ const t = useT();
27
+ return /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", "data-testid": "cpq-charge-breakdown", children: [
28
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-muted-foreground", children: [
29
+ /* @__PURE__ */ jsx("th", { className: "text-left py-1 font-medium", children: t("cpq.charges.charge", "Charge") }),
30
+ /* @__PURE__ */ jsx("th", { className: "text-left py-1 font-medium", children: t("cpq.charges.type", "Type") }),
31
+ /* @__PURE__ */ jsx(
32
+ PriceColumnHeader,
33
+ {
34
+ label: t("cpq.priceAdjustments.listUnit", "List"),
35
+ hint: t("cpq.priceAdjustments.listUnitHint", "Catalog unit price from the pricing table, before any price rules or manual adjustments.")
36
+ }
37
+ ),
38
+ /* @__PURE__ */ jsx(
39
+ PriceColumnHeader,
40
+ {
41
+ label: t("cpq.priceAdjustments.standardUnit", "Standard"),
42
+ hint: t("cpq.priceAdjustments.standardUnitHint", "Unit price after automatic price rules (e.g. volume or segment discounts), before manual adjustments.")
43
+ }
44
+ ),
45
+ /* @__PURE__ */ jsx(
46
+ PriceColumnHeader,
47
+ {
48
+ label: t("cpq.priceAdjustments.offerUnit", "Offer"),
49
+ hint: t("cpq.priceAdjustments.offerUnitHint", "Final unit price quoted to the customer, after price rules and manual adjustments. The badge shows the total change vs list.")
50
+ }
51
+ ),
52
+ /* @__PURE__ */ jsx("th", { className: "text-right py-1 font-medium", children: t("cpq.charges.qty", "Qty") }),
53
+ /* @__PURE__ */ jsx("th", { className: "text-right py-1 font-medium", children: t("cpq.charges.total", "Total") }),
54
+ /* @__PURE__ */ jsx("th", { className: "w-16" })
55
+ ] }) }),
56
+ /* @__PURE__ */ jsx("tbody", { children: charges.map((c, i) => {
57
+ const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length);
58
+ const isDetailOpen = openDetailIndex === i;
59
+ const listUnit = c.listUnitPrice ?? c.unitPrice;
60
+ const standardUnit = c.standardUnitPrice ?? c.unitPrice;
61
+ const errors = c.adjustmentErrors ?? [];
62
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
63
+ /* @__PURE__ */ jsxs("tr", { className: "border-t border-dashed border-border/50", children: [
64
+ /* @__PURE__ */ jsxs("td", { className: "py-1", children: [
65
+ /* @__PURE__ */ jsx("span", { children: c.chargeName }),
66
+ errors.length > 0 && /* @__PURE__ */ jsxs(Tag, { variant: "warning", className: "ml-1.5 px-1.5 text-xs", "data-testid": "cpq-adjustment-invalid", children: [
67
+ t("cpq.priceAdjustments.invalid", "Invalid"),
68
+ ": ",
69
+ t(`cpq.priceAdjustments.errors.${errors[0].code}`, ADJUSTMENT_ERROR_FALLBACKS[errors[0].code] ?? errors[0].code)
70
+ ] })
71
+ ] }),
72
+ /* @__PURE__ */ jsx("td", { className: "py-1 uppercase", children: c.chargeType }),
73
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right font-mono text-muted-foreground", children: formatMoney(listUnit, currency) }),
74
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right font-mono text-muted-foreground", children: formatMoney(standardUnit, currency) }),
75
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right font-mono", children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
76
+ /* @__PURE__ */ jsx(DepthBadge, { before: listUnit, after: c.unitPrice }),
77
+ formatMoney(c.unitPrice, currency)
78
+ ] }) }),
79
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right font-mono", children: c.quantity ?? "\u2014" }),
80
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right font-mono", children: c.totalPrice != null ? formatMoney(c.totalPrice, currency) : c.note ?? "\u2014" }),
81
+ /* @__PURE__ */ jsxs("td", { className: "py-1 text-right whitespace-nowrap", children: [
82
+ renderChargeActions?.(c),
83
+ hasDetail && /* @__PURE__ */ jsx(
84
+ "button",
85
+ {
86
+ type: "button",
87
+ onClick: () => onToggleDetail(i),
88
+ className: "inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors",
89
+ title: t("cpq.priceAdjustments.showBreakdown", "Show price breakdown"),
90
+ children: /* @__PURE__ */ jsx(Info, { className: "h-3.5 w-3.5" })
91
+ }
92
+ )
93
+ ] })
94
+ ] }),
95
+ isDetailOpen && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: 8, className: "pb-3 pt-1", children: /* @__PURE__ */ jsx(ChargePricingDetail, { charge: c, currency, reasonLabels, renderManualActions }) }) })
96
+ ] }, i);
97
+ }) })
98
+ ] });
99
+ }
100
+ function ChargePricingDetail({ charge: c, currency, reasonLabels, renderManualActions }) {
101
+ const t = useT();
102
+ const tiers = c.breakdown?.tiers ?? [];
103
+ return /* @__PURE__ */ jsxs("div", { className: "rounded border bg-background p-3 space-y-3 text-xs", children: [
104
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
105
+ /* @__PURE__ */ jsxs("span", { className: "font-semibold text-sm", children: [
106
+ t("cpq.charges.pricingDetails", "Pricing Details"),
107
+ " \u2014 ",
108
+ c.chargeName
109
+ ] }),
110
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: c.pricingMethod ?? "flat" })
111
+ ] }),
112
+ tiers.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
113
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-muted-foreground mb-1", children: t("cpq.charges.tierBreakdown", "Tier Breakdown") }),
114
+ /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
115
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-muted-foreground", children: [
116
+ /* @__PURE__ */ jsx("th", { className: "text-left py-0.5 font-medium", children: t("cpq.charges.tier", "Tier") }),
117
+ /* @__PURE__ */ jsx("th", { className: "text-right py-0.5 font-medium", children: t("cpq.charges.range", "Range") }),
118
+ /* @__PURE__ */ jsx("th", { className: "text-right py-0.5 font-medium", children: t("cpq.charges.pricePerUnit", "Price/Unit") }),
119
+ /* @__PURE__ */ jsx("th", { className: "text-right py-0.5 font-medium", children: t("cpq.charges.qty", "Qty") }),
120
+ /* @__PURE__ */ jsx("th", { className: "text-right py-0.5 font-medium", children: t("cpq.charges.subtotal", "Subtotal") })
121
+ ] }) }),
122
+ /* @__PURE__ */ jsxs("tbody", { children: [
123
+ tiers.map((tier) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/30", children: [
124
+ /* @__PURE__ */ jsx("td", { className: "py-0.5", children: tier.tier }),
125
+ /* @__PURE__ */ jsxs("td", { className: "py-0.5 text-right font-mono", children: [
126
+ tier.from,
127
+ "\u2013",
128
+ tier.to
129
+ ] }),
130
+ /* @__PURE__ */ jsx("td", { className: "py-0.5 text-right font-mono", children: formatMoney(tier.pricePerUnit, currency) }),
131
+ /* @__PURE__ */ jsx("td", { className: "py-0.5 text-right font-mono", children: tier.quantity }),
132
+ /* @__PURE__ */ jsx("td", { className: "py-0.5 text-right font-mono", children: formatMoney(tier.subtotal, currency) })
133
+ ] }, tier.tier)),
134
+ /* @__PURE__ */ jsxs("tr", { className: "border-t font-medium", children: [
135
+ /* @__PURE__ */ jsx("td", { colSpan: 4, className: "py-0.5 text-right", children: t("cpq.charges.tieredTotal", "Tiered Total") }),
136
+ /* @__PURE__ */ jsx("td", { className: "py-0.5 text-right font-mono", children: formatMoney(tiers.reduce((sum, tier) => sum + tier.subtotal, 0), currency) })
137
+ ] })
138
+ ] })
139
+ ] })
140
+ ] }),
141
+ /* @__PURE__ */ jsxs("div", { children: [
142
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-muted-foreground mb-1", children: t("cpq.priceAdjustments.waterfall.title", "Price waterfall") }),
143
+ /* @__PURE__ */ jsx(PriceWaterfall, { charge: c, currency, reasonLabels, renderManualActions })
144
+ ] }),
145
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pt-1 border-t text-sm font-semibold", children: [
146
+ /* @__PURE__ */ jsx("span", { children: t("cpq.charges.finalTotal", "Final Total") }),
147
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: c.totalPrice != null ? formatMoney(c.totalPrice, currency) : "\u2014" })
148
+ ] })
149
+ ] });
150
+ }
151
+ export {
152
+ ChargeBreakdownTable
153
+ };
154
+ //# sourceMappingURL=ChargeBreakdownTable.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/components/ChargeBreakdownTable.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Info } from 'lucide-react'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { DepthBadge, PriceWaterfall } from './PriceWaterfall'\nimport { ADJUSTMENT_ERROR_FALLBACKS, type WaterfallChargeInput } from '../lib/priceAdjustmentView'\nimport { formatMoney } from '../lib/formatMoney'\n\ntype TierBreakdownEntry = { tier: number; from: number; to: number; pricePerUnit: number; quantity: number; subtotal: number }\n\n/** One priced charge as stored on a quote or order line. */\nexport type ChargeRowView = WaterfallChargeInput & {\n chargeName: string\n quantity?: number | null\n note?: string | null\n breakdown?: { tiers: TierBreakdownEntry[] } | null\n adjustmentErrors?: Array<{ code: string }>\n}\n\n/** Right-aligned price column header with an explanatory hover tooltip. */\nfunction PriceColumnHeader({ label, hint }: { label: string; hint: string }) {\n return (\n <th className=\"text-right py-1 font-medium\">\n <SimpleTooltip content={hint} size=\"sm\">\n <span className=\"inline-flex items-center gap-1 cursor-help\" tabIndex={0} aria-label={`${label}: ${hint}`}>\n {label}\n <Info className=\"h-3 w-3\" aria-hidden />\n </span>\n </SimpleTooltip>\n </th>\n )\n}\n\n/**\n * Charge table shared by the quote and order detail pages: list / standard /\n * offer unit prices with depth badge, and a per-charge pricing detail (tiers +\n * price waterfall). Editing hooks are optional render props.\n */\nexport function ChargeBreakdownTable<C extends ChargeRowView>({\n charges,\n currency,\n openDetailIndex,\n onToggleDetail,\n reasonLabels,\n renderChargeActions,\n renderManualActions,\n}: {\n charges: C[]\n currency: string\n openDetailIndex: number | null\n onToggleDetail: (chargeIndex: number) => void\n reasonLabels?: Record<string, string>\n renderChargeActions?: (charge: C) => React.ReactNode\n renderManualActions?: (adjustmentId: string) => React.ReactNode\n}) {\n const t = useT()\n return (\n <table className=\"w-full text-xs\" data-testid=\"cpq-charge-breakdown\">\n <thead>\n <tr className=\"text-muted-foreground\">\n <th className=\"text-left py-1 font-medium\">{t('cpq.charges.charge', 'Charge')}</th>\n <th className=\"text-left py-1 font-medium\">{t('cpq.charges.type', 'Type')}</th>\n <PriceColumnHeader\n label={t('cpq.priceAdjustments.listUnit', 'List')}\n hint={t('cpq.priceAdjustments.listUnitHint', 'Catalog unit price from the pricing table, before any price rules or manual adjustments.')}\n />\n <PriceColumnHeader\n label={t('cpq.priceAdjustments.standardUnit', 'Standard')}\n hint={t('cpq.priceAdjustments.standardUnitHint', 'Unit price after automatic price rules (e.g. volume or segment discounts), before manual adjustments.')}\n />\n <PriceColumnHeader\n label={t('cpq.priceAdjustments.offerUnit', 'Offer')}\n hint={t('cpq.priceAdjustments.offerUnitHint', 'Final unit price quoted to the customer, after price rules and manual adjustments. The badge shows the total change vs list.')}\n />\n <th className=\"text-right py-1 font-medium\">{t('cpq.charges.qty', 'Qty')}</th>\n <th className=\"text-right py-1 font-medium\">{t('cpq.charges.total', 'Total')}</th>\n <th className=\"w-16\" />\n </tr>\n </thead>\n <tbody>\n {charges.map((c, i) => {\n const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length)\n const isDetailOpen = openDetailIndex === i\n const listUnit = c.listUnitPrice ?? c.unitPrice\n const standardUnit = c.standardUnitPrice ?? c.unitPrice\n const errors = c.adjustmentErrors ?? []\n return (\n <React.Fragment key={i}>\n <tr className=\"border-t border-dashed border-border/50\">\n <td className=\"py-1\">\n <span>{c.chargeName}</span>\n {errors.length > 0 && (\n <Tag variant=\"warning\" className=\"ml-1.5 px-1.5 text-xs\" data-testid=\"cpq-adjustment-invalid\">\n {t('cpq.priceAdjustments.invalid', 'Invalid')}: {t(`cpq.priceAdjustments.errors.${errors[0].code}`, ADJUSTMENT_ERROR_FALLBACKS[errors[0].code] ?? errors[0].code)}\n </Tag>\n )}\n </td>\n <td className=\"py-1 uppercase\">{c.chargeType}</td>\n <td className=\"py-1 text-right font-mono text-muted-foreground\">{formatMoney(listUnit, currency)}</td>\n <td className=\"py-1 text-right font-mono text-muted-foreground\">{formatMoney(standardUnit, currency)}</td>\n <td className=\"py-1 text-right font-mono\">\n <span className=\"inline-flex items-center gap-1\">\n <DepthBadge before={listUnit} after={c.unitPrice} />\n {formatMoney(c.unitPrice, currency)}\n </span>\n </td>\n <td className=\"py-1 text-right font-mono\">{c.quantity ?? '\u2014'}</td>\n <td className=\"py-1 text-right font-mono\">{c.totalPrice != null ? formatMoney(c.totalPrice, currency) : c.note ?? '\u2014'}</td>\n <td className=\"py-1 text-right whitespace-nowrap\">\n {renderChargeActions?.(c)}\n {hasDetail && (\n <button\n type=\"button\"\n onClick={() => onToggleDetail(i)}\n className=\"inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors\"\n title={t('cpq.priceAdjustments.showBreakdown', 'Show price breakdown')}\n >\n <Info className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </td>\n </tr>\n {isDetailOpen && (\n <tr>\n <td colSpan={8} className=\"pb-3 pt-1\">\n <ChargePricingDetail charge={c} currency={currency} reasonLabels={reasonLabels} renderManualActions={renderManualActions} />\n </td>\n </tr>\n )}\n </React.Fragment>\n )\n })}\n </tbody>\n </table>\n )\n}\n\nfunction ChargePricingDetail({ charge: c, currency, reasonLabels, renderManualActions }: {\n charge: ChargeRowView\n currency: string\n reasonLabels?: Record<string, string>\n renderManualActions?: (adjustmentId: string) => React.ReactNode\n}) {\n const t = useT()\n const tiers = c.breakdown?.tiers ?? []\n return (\n <div className=\"rounded border bg-background p-3 space-y-3 text-xs\">\n <div className=\"flex items-center justify-between\">\n <span className=\"font-semibold text-sm\">{t('cpq.charges.pricingDetails', 'Pricing Details')} \u2014 {c.chargeName}</span>\n <span className=\"text-muted-foreground\">{c.pricingMethod ?? 'flat'}</span>\n </div>\n\n {tiers.length > 0 && (\n <div>\n <div className=\"font-medium text-muted-foreground mb-1\">{t('cpq.charges.tierBreakdown', 'Tier Breakdown')}</div>\n <table className=\"w-full\">\n <thead>\n <tr className=\"text-muted-foreground\">\n <th className=\"text-left py-0.5 font-medium\">{t('cpq.charges.tier', 'Tier')}</th>\n <th className=\"text-right py-0.5 font-medium\">{t('cpq.charges.range', 'Range')}</th>\n <th className=\"text-right py-0.5 font-medium\">{t('cpq.charges.pricePerUnit', 'Price/Unit')}</th>\n <th className=\"text-right py-0.5 font-medium\">{t('cpq.charges.qty', 'Qty')}</th>\n <th className=\"text-right py-0.5 font-medium\">{t('cpq.charges.subtotal', 'Subtotal')}</th>\n </tr>\n </thead>\n <tbody>\n {tiers.map((tier) => (\n <tr key={tier.tier} className=\"border-t border-border/30\">\n <td className=\"py-0.5\">{tier.tier}</td>\n <td className=\"py-0.5 text-right font-mono\">{tier.from}\u2013{tier.to}</td>\n <td className=\"py-0.5 text-right font-mono\">{formatMoney(tier.pricePerUnit, currency)}</td>\n <td className=\"py-0.5 text-right font-mono\">{tier.quantity}</td>\n <td className=\"py-0.5 text-right font-mono\">{formatMoney(tier.subtotal, currency)}</td>\n </tr>\n ))}\n <tr className=\"border-t font-medium\">\n <td colSpan={4} className=\"py-0.5 text-right\">{t('cpq.charges.tieredTotal', 'Tiered Total')}</td>\n <td className=\"py-0.5 text-right font-mono\">\n {formatMoney(tiers.reduce((sum, tier) => sum + tier.subtotal, 0), currency)}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n )}\n\n <div>\n <div className=\"font-medium text-muted-foreground mb-1\">{t('cpq.priceAdjustments.waterfall.title', 'Price waterfall')}</div>\n <PriceWaterfall charge={c} currency={currency} reasonLabels={reasonLabels} renderManualActions={renderManualActions} />\n </div>\n\n <div className=\"flex items-center justify-between pt-1 border-t text-sm font-semibold\">\n <span>{t('cpq.charges.finalTotal', 'Final Total')}</span>\n <span className=\"font-mono\">{c.totalPrice != null ? formatMoney(c.totalPrice, currency) : '\u2014'}</span>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/cpq/components/CpqListView.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { DataTable, type BulkAction } from '@open-mercato/ui/backend/DataTable'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useCpqRowActions, type UseCpqRowActionsOptions } from './useCpqRowActions'\n\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype PaginatedResponse<T> = {\n items?: T[]\n total?: number\n totalPages?: number\n}\n\nexport type CpqListData<T> = {\n rows: T[]\n total: number\n totalPages: number\n page: number\n isLoading: boolean\n /** Last successful fetch duration in milliseconds. Surfaces in the\n * pagination footer as `... in 1.5s`, matching OM's standard list views. */\n durationMs: number | null\n search: string\n sorting: SortingState\n filterValues: FilterValues\n setPage: (page: number) => void\n setSearch: (value: string) => void\n setSorting: React.Dispatch<React.SetStateAction<SortingState>>\n setFilterValues: (values: FilterValues) => void\n clearFilters: () => void\n reload: () => void\n queryString: string\n}\n\nexport type UseCpqListDataOptions = {\n /** API endpoint without query string, e.g. `/api/cpq/product-offerings` */\n endpoint: string\n /** Default 50 \u2014 keep at or below 100. */\n pageSize?: number\n /** Initial sort state. Default `[{ id: 'createdAt', desc: true }]`. */\n initialSorting?: SortingState\n /** Map filter values to URL params (only call set on the params arg). */\n buildFilterParams?: (values: FilterValues, params: URLSearchParams) => void\n /** Override the flash message shown when the load fails. */\n loadErrorMessage?: string\n}\n\n/**\n * Server-side paginated list state for CPQ list views.\n *\n * Designed for 100k+ records:\n * - search / sort / filter all go to the server\n * - default pageSize stays at or below 100\n * - debouncing is handled by FilterBar's search input (1s)\n */\nexport function useCpqListData<T extends { id: string }>(\n options: UseCpqListDataOptions,\n): CpqListData<T> {\n const {\n endpoint,\n pageSize = 50,\n initialSorting = [{ id: 'createdAt', desc: true }],\n buildFilterParams,\n loadErrorMessage,\n } = options\n const t = useT()\n\n const [rows, setRows] = React.useState<T[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [page, setPageState] = React.useState(1)\n const [isLoading, setIsLoading] = React.useState(true)\n const [durationMs, setDurationMs] = React.useState<number | null>(null)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [search, setSearchState] = React.useState('')\n const [sorting, setSorting] = React.useState<SortingState>(initialSorting)\n const [filterValues, setFilterValuesState] = React.useState<FilterValues>({})\n\n const queryString = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const sort = sorting[0]\n if (sort?.id) {\n params.set('sortField', sort.id)\n params.set('sortDir', sort.desc ? 'desc' : 'asc')\n }\n if (buildFilterParams) buildFilterParams(filterValues, params)\n return params.toString()\n }, [buildFilterParams, filterValues, page, pageSize, search, sorting])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n // Use `performance.now()` so the wall-clock change during the request\n // (NTP, DST, manual clock skew) can never produce a negative duration.\n const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()\n try {\n const fallback: PaginatedResponse<T> = { items: [], total: 0, totalPages: 1 }\n const call = await apiCall<PaginatedResponse<T>>(\n `${endpoint}?${queryString}`,\n undefined,\n { fallback },\n )\n if (cancelled) return\n if (!call.ok) {\n flash(loadErrorMessage ?? t('cpq.list.error.load', 'Failed to load'), 'error')\n return\n }\n const payload = call.result ?? fallback\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items)\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)\n const finishedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()\n setDurationMs(Math.max(0, finishedAt - startedAt))\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [endpoint, loadErrorMessage, queryString, reloadToken, t])\n\n const setSearch = React.useCallback((value: string) => {\n setSearchState(value)\n setPageState(1)\n }, [])\n\n const setFilterValues = React.useCallback((values: FilterValues) => {\n setFilterValuesState(values)\n setPageState(1)\n }, [])\n\n const clearFilters = React.useCallback(() => {\n setFilterValuesState({})\n setPageState(1)\n }, [])\n\n const reload = React.useCallback(() => {\n setReloadToken((token) => token + 1)\n }, [])\n\n return {\n rows,\n total,\n totalPages,\n page,\n isLoading,\n durationMs,\n search,\n sorting,\n filterValues,\n setPage: setPageState,\n setSearch,\n setSorting,\n setFilterValues,\n clearFilters,\n reload,\n queryString,\n }\n}\n\n// \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Declarative shortcut for the standard CRUD-list row actions.\n *\n * When provided, CpqListView:\n * - registers `useCpqRowActions` internally\n * - renders Edit + Delete in every row's `\u22EF` menu (with optional\n * `extraRowItems` per row prepended / appended)\n * - adds a \"Delete selected\" bulk action (after any `bulkActions` you\n * pass explicitly, so module-specific bulk ops come first)\n * - mounts the confirm-dialog portal in the page footer\n *\n * Pass `rowActions` / `bulkActions` directly instead when the page needs\n * fully custom semantics (e.g. Quotes / Orders have no DELETE endpoint).\n */\nexport type CpqListViewCrudConfig<T extends { id: string }> = Omit<\n UseCpqRowActionsOptions<T>,\n 'onReload'\n> & {\n /**\n * Extra items added to each row's actions menu.\n * `prepend` renders before Edit, `append` between Edit and Delete.\n */\n extraRowItems?: (row: T) => { prepend?: RowActionItem[]; append?: RowActionItem[] }\n}\n\nexport type CpqListViewProps<T extends { id: string }> = {\n /** Page + table title */\n title: React.ReactNode\n /** Stable id for perspectives (e.g. `cpq.offerings.list`) */\n tableId: string\n /** Stable id for DataTable's selection state (defaults to tableId) */\n selectionScopeKey?: string\n /** List state \u2014 typically built from `useCpqListData` in the parent */\n data: CpqListData<T>\n /** Table columns */\n columns: ColumnDef<T>[]\n /** Filter definitions for the FilterBar overlay */\n filters?: FilterDef[]\n /** Search input placeholder */\n searchPlaceholder?: string\n /** Header actions (e.g. Create button) */\n actions?: React.ReactNode\n /** Bulk actions toolbar shown when rows are selected */\n bulkActions?: BulkAction<T>[]\n /** Per-row action menu */\n rowActions?: (row: T) => React.ReactNode\n /**\n * Declarative CRUD config. When set, CpqListView produces the standard\n * `\u22EF \u2192 Edit / Delete` menu, a \"Delete selected\" bulk action, and mounts\n * the confirm-dialog portal \u2014 replacing the ~30 lines of boilerplate\n * each list page used to repeat. Override `rowActions` / `bulkActions`\n * to take full control instead.\n */\n crud?: CpqListViewCrudConfig<T>\n /** Click handler for whole row (typically navigates to detail) */\n onRowClick?: (row: T) => void\n /** Custom empty state */\n emptyState?: React.ReactNode\n /** Pagination size (must match `useCpqListData` pageSize) */\n pageSize?: number\n /**\n * Custom content rendered ABOVE the DataTable inside the same Page wrapper.\n * Use for inline forms, customer pickers, tab switchers, alerts.\n */\n toolbarContent?: React.ReactNode\n /**\n * Custom content rendered BELOW the DataTable (e.g. confirm dialog portals).\n */\n footerContent?: React.ReactNode\n}\n\n/**\n * Generic CPQ list view. Wraps DataTable + FilterBar + Page in the\n * Open Mercato standard, with stronger row-hover affordance so users\n * always know what's clickable.\n */\nexport function CpqListView<T extends { id: string }>({\n title,\n tableId,\n selectionScopeKey,\n data,\n columns,\n filters,\n searchPlaceholder,\n actions,\n bulkActions,\n rowActions,\n crud,\n onRowClick,\n emptyState,\n pageSize = 50,\n toolbarContent,\n footerContent,\n}: CpqListViewProps<T>) {\n const t = useT()\n\n // Wire `crud` into RowActions + bulkActions + footer confirm dialog so the\n // call-site stays a single declarative config block.\n // The hook is always called (Rules of Hooks); the helpers below decide\n // whether to actually use its output based on the presence of `crud`.\n const crudActions = useCpqRowActions<T>({\n endpoint: crud?.endpoint ?? '',\n entityName: crud?.entityName ?? 'item',\n editHref: crud?.editHref ?? (() => '#'),\n onReload: data.reload,\n canDelete: crud?.canDelete,\n })\n\n const resolvedRowActions = React.useMemo(() => {\n if (rowActions) return rowActions\n if (!crud) return undefined\n return (row: T) => (\n <RowActions items={crudActions.buildItems(row, crud.extraRowItems?.(row))} />\n )\n }, [crud, crudActions, rowActions])\n\n const resolvedBulkActions = React.useMemo<BulkAction<T>[] | undefined>(() => {\n if (!crud) return bulkActions\n const deleteAction: BulkAction<T> = {\n id: 'delete',\n label: t('cpq.list.bulk.deleteSelected', 'Delete selected'),\n destructive: true,\n onExecute: crudActions.bulkDelete,\n }\n return bulkActions ? [...bulkActions, deleteAction] : [deleteAction]\n }, [bulkActions, crud, crudActions.bulkDelete, t])\n\n const resolvedFooter = crud ? (\n <>\n {footerContent}\n {crudActions.ConfirmDialogElement}\n </>\n ) : (\n footerContent\n )\n\n return (\n <Page>\n <PageBody className=\"cpq-list-view space-y-6\">\n <CpqInteractionStyles />\n {toolbarContent}\n <DataTable<T>\n title={title}\n actions={actions}\n refreshButton={{\n label: t('cpq.list.actions.refresh', 'Refresh'),\n onRefresh: data.reload,\n isRefreshing: data.isLoading,\n }}\n columns={columns}\n data={data.rows}\n searchValue={data.search}\n onSearchChange={data.setSearch}\n searchPlaceholder={searchPlaceholder ?? t('cpq.list.search.placeholder', 'Search...')}\n filters={filters}\n filterValues={data.filterValues}\n onFiltersApply={data.setFilterValues}\n onFiltersClear={data.clearFilters}\n sorting={data.sorting}\n onSortingChange={data.setSorting}\n bulkActions={resolvedBulkActions}\n selectionScopeKey={selectionScopeKey ?? tableId}\n onRowClick={onRowClick}\n rowActions={resolvedRowActions}\n perspective={{ tableId }}\n columnChooser={{ auto: true }}\n pagination={{\n page: data.page,\n pageSize,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: data.setPage,\n durationMs: data.durationMs,\n }}\n isLoading={data.isLoading}\n emptyState={emptyState}\n />\n {resolvedFooter}\n </PageBody>\n </Page>\n )\n}\n\n// \u2500\u2500\u2500 Shared interaction styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Inline style block applied inside `.cpq-list-view` (and any `.cpq-page`)\n * wrappers. Strengthens the OM hover affordance so users can clearly tell\n * which row their mouse is on, and forces `cursor: pointer` on every\n * clickable element inside CPQ pages.\n *\n * Kept inline to avoid coupling the package to a global stylesheet.\n */\nexport function CpqInteractionStyles() {\n return (\n <style>{`\n.cpq-list-view tbody tr.cursor-pointer,\n.cpq-page tbody tr.cursor-pointer {\n cursor: pointer;\n}\n.cpq-list-view tbody tr.cursor-pointer:hover,\n.cpq-page tbody tr.cursor-pointer:hover {\n background-color: rgba(0, 0, 0, 0.045);\n}\n.dark .cpq-list-view tbody tr.cursor-pointer:hover,\n.dark .cpq-page tbody tr.cursor-pointer:hover {\n background-color: rgba(255, 255, 255, 0.06);\n}\n.cpq-list-view button:not(:disabled),\n.cpq-list-view [role=\"button\"]:not([aria-disabled=\"true\"]),\n.cpq-list-view a[href],\n.cpq-list-view [data-clickable=\"true\"],\n.cpq-page button:not(:disabled),\n.cpq-page [role=\"button\"]:not([aria-disabled=\"true\"]),\n.cpq-page a[href],\n.cpq-page [data-clickable=\"true\"] {\n cursor: pointer;\n}\n/* Row actions \\`\u22EF\\` trigger: OM's hover:bg-accent is barely visible in\n the light theme. Make hover (and the open state) clearly distinct so\n users see what they're aiming at. */\n.cpq-list-view [aria-haspopup=\"menu\"]:hover,\n.cpq-page [aria-haspopup=\"menu\"]:hover,\n.cpq-list-view [aria-haspopup=\"menu\"][aria-expanded=\"true\"],\n.cpq-page [aria-haspopup=\"menu\"][aria-expanded=\"true\"] {\n background-color: rgba(0, 0, 0, 0.08);\n}\n.dark .cpq-list-view [aria-haspopup=\"menu\"]:hover,\n.dark .cpq-page [aria-haspopup=\"menu\"]:hover,\n.dark .cpq-list-view [aria-haspopup=\"menu\"][aria-expanded=\"true\"],\n.dark .cpq-page [aria-haspopup=\"menu\"][aria-expanded=\"true\"] {\n background-color: rgba(255, 255, 255, 0.1);\n}\n/* Items inside the row-actions dropdown menu \u2014 strengthen hover so the\n item under the pointer is unmistakable. The menu portal renders at the\n document body, so we can't scope it to .cpq-list-view; targeting the\n role attribute is enough. */\n[role=\"menu\"] [role=\"menuitem\"]:hover,\n[role=\"menu\"] [role=\"menuitem\"]:focus-visible {\n background-color: rgba(0, 0, 0, 0.06);\n}\n.dark [role=\"menu\"] [role=\"menuitem\"]:hover,\n.dark [role=\"menu\"] [role=\"menuitem\"]:focus-visible {\n background-color: rgba(255, 255, 255, 0.08);\n}\n/* Align padding between <a href> menu items (OM's px-2) and the <button>\n variant used for onSelect items (OM's Button size=sm = px-3). Without\n this the Delete label visibly indents past the Edit label. */\n[role=\"menu\"] button[role=\"menuitem\"] {\n padding-left: 0.5rem;\n padding-right: 0.5rem;\n}\n`}</style>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { SortingState } from '@tanstack/react-table'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable, type BulkAction } from '@open-mercato/ui/backend/DataTable'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useCpqRowActions, type UseCpqRowActionsOptions } from './useCpqRowActions'\n\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype PaginatedResponse<T> = {\n items?: T[]\n total?: number\n totalPages?: number\n}\n\nexport type CpqListData<T> = {\n rows: T[]\n total: number\n totalPages: number\n page: number\n isLoading: boolean\n /** Last successful fetch duration in milliseconds. Surfaces in the\n * pagination footer as `... in 1.5s`, matching OM's standard list views. */\n durationMs: number | null\n search: string\n sorting: SortingState\n filterValues: FilterValues\n setPage: (page: number) => void\n setSearch: (value: string) => void\n setSorting: React.Dispatch<React.SetStateAction<SortingState>>\n setFilterValues: (values: FilterValues) => void\n clearFilters: () => void\n reload: () => void\n queryString: string\n}\n\nexport type UseCpqListDataOptions = {\n /** API endpoint without query string, e.g. `/api/cpq/product-offerings` */\n endpoint: string\n /** Default 50 \u2014 keep at or below 100. */\n pageSize?: number\n /** Initial sort state. Default `[{ id: 'createdAt', desc: true }]`. */\n initialSorting?: SortingState\n /** Map filter values to URL params (only call set on the params arg). */\n buildFilterParams?: (values: FilterValues, params: URLSearchParams) => void\n /** Override the flash message shown when the load fails. */\n loadErrorMessage?: string\n}\n\n/**\n * Server-side paginated list state for CPQ list views.\n *\n * Designed for 100k+ records:\n * - search / sort / filter all go to the server\n * - default pageSize stays at or below 100\n * - debouncing is handled by FilterBar's search input (1s)\n */\nexport function useCpqListData<T extends { id: string }>(\n options: UseCpqListDataOptions,\n): CpqListData<T> {\n const {\n endpoint,\n pageSize = 50,\n initialSorting = [{ id: 'createdAt', desc: true }],\n buildFilterParams,\n loadErrorMessage,\n } = options\n const t = useT()\n\n const [rows, setRows] = React.useState<T[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [page, setPageState] = React.useState(1)\n const [isLoading, setIsLoading] = React.useState(true)\n const [durationMs, setDurationMs] = React.useState<number | null>(null)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [search, setSearchState] = React.useState('')\n const [sorting, setSorting] = React.useState<SortingState>(initialSorting)\n const [filterValues, setFilterValuesState] = React.useState<FilterValues>({})\n\n const queryString = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const sort = sorting[0]\n if (sort?.id) {\n params.set('sortField', sort.id)\n params.set('sortDir', sort.desc ? 'desc' : 'asc')\n }\n if (buildFilterParams) buildFilterParams(filterValues, params)\n return params.toString()\n }, [buildFilterParams, filterValues, page, pageSize, search, sorting])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n // Use `performance.now()` so the wall-clock change during the request\n // (NTP, DST, manual clock skew) can never produce a negative duration.\n const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()\n try {\n const fallback: PaginatedResponse<T> = { items: [], total: 0, totalPages: 1 }\n const call = await apiCall<PaginatedResponse<T>>(\n `${endpoint}?${queryString}`,\n undefined,\n { fallback },\n )\n if (cancelled) return\n if (!call.ok) {\n flash(loadErrorMessage ?? t('cpq.list.error.load', 'Failed to load'), 'error')\n return\n }\n const payload = call.result ?? fallback\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items)\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)\n const finishedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()\n setDurationMs(Math.max(0, finishedAt - startedAt))\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [endpoint, loadErrorMessage, queryString, reloadToken, t])\n\n const setSearch = React.useCallback((value: string) => {\n setSearchState(value)\n setPageState(1)\n }, [])\n\n const setFilterValues = React.useCallback((values: FilterValues) => {\n setFilterValuesState(values)\n setPageState(1)\n }, [])\n\n const clearFilters = React.useCallback(() => {\n setFilterValuesState({})\n setPageState(1)\n }, [])\n\n const reload = React.useCallback(() => {\n setReloadToken((token) => token + 1)\n }, [])\n\n return {\n rows,\n total,\n totalPages,\n page,\n isLoading,\n durationMs,\n search,\n sorting,\n filterValues,\n setPage: setPageState,\n setSearch,\n setSorting,\n setFilterValues,\n clearFilters,\n reload,\n queryString,\n }\n}\n\n// \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Declarative shortcut for the standard CRUD-list row actions.\n *\n * When provided, CpqListView:\n * - registers `useCpqRowActions` internally\n * - renders Edit + Delete in every row's `\u22EF` menu (with optional\n * `extraRowItems` per row prepended / appended)\n * - adds a \"Delete selected\" bulk action (after any `bulkActions` you\n * pass explicitly, so module-specific bulk ops come first)\n * - mounts the confirm-dialog portal in the page footer\n *\n * Pass `rowActions` / `bulkActions` directly instead when the page needs\n * fully custom semantics (e.g. Quotes / Orders have no DELETE endpoint).\n */\nexport type CpqListViewCrudConfig<T extends { id: string }> = Omit<\n UseCpqRowActionsOptions<T>,\n 'onReload'\n> & {\n /**\n * Extra items added to each row's actions menu.\n * `prepend` renders before Edit, `append` between Edit and Delete.\n */\n extraRowItems?: (row: T) => { prepend?: RowActionItem[]; append?: RowActionItem[] }\n}\n\nexport type CpqListViewProps<T extends { id: string }> = {\n /** Page + table title */\n title: React.ReactNode\n /** Stable id for perspectives (e.g. `cpq.offerings.list`) */\n tableId: string\n /** Stable id for DataTable's selection state (defaults to tableId) */\n selectionScopeKey?: string\n /** List state \u2014 typically built from `useCpqListData` in the parent */\n data: CpqListData<T>\n /** Table columns */\n columns: ColumnDef<T>[]\n /** Filter definitions for the FilterBar overlay */\n filters?: FilterDef[]\n /** Search input placeholder */\n searchPlaceholder?: string\n /** Header actions (e.g. Create button) */\n actions?: React.ReactNode\n /** Bulk actions toolbar shown when rows are selected */\n bulkActions?: BulkAction<T>[]\n /** Per-row action menu */\n rowActions?: (row: T) => React.ReactNode\n /**\n * Declarative CRUD config. When set, CpqListView produces the standard\n * `\u22EF \u2192 Edit / Delete` menu, a \"Delete selected\" bulk action, and mounts\n * the confirm-dialog portal \u2014 replacing the ~30 lines of boilerplate\n * each list page used to repeat. Override `rowActions` / `bulkActions`\n * to take full control instead.\n */\n crud?: CpqListViewCrudConfig<T>\n /** Click handler for whole row (typically navigates to detail) */\n onRowClick?: (row: T) => void\n /** Custom empty state */\n emptyState?: React.ReactNode\n /** Pagination size (must match `useCpqListData` pageSize) */\n pageSize?: number\n /**\n * Custom content rendered ABOVE the DataTable inside the same Page wrapper.\n * Use for inline forms, customer pickers, tab switchers, alerts.\n */\n toolbarContent?: React.ReactNode\n /**\n * Custom content rendered BELOW the DataTable (e.g. confirm dialog portals).\n */\n footerContent?: React.ReactNode\n}\n\n/**\n * Generic CPQ list view. Wraps DataTable + FilterBar + Page in the\n * Open Mercato standard, with stronger row-hover affordance so users\n * always know what's clickable.\n */\nexport function CpqListView<T extends { id: string }>({\n title,\n tableId,\n selectionScopeKey,\n data,\n columns,\n filters,\n searchPlaceholder,\n actions,\n bulkActions,\n rowActions,\n crud,\n onRowClick,\n emptyState,\n pageSize = 50,\n toolbarContent,\n footerContent,\n}: CpqListViewProps<T>) {\n const t = useT()\n\n // Wire `crud` into RowActions + bulkActions + footer confirm dialog so the\n // call-site stays a single declarative config block.\n // The hook is always called (Rules of Hooks); the helpers below decide\n // whether to actually use its output based on the presence of `crud`.\n const crudActions = useCpqRowActions<T>({\n endpoint: crud?.endpoint ?? '',\n entityName: crud?.entityName ?? 'item',\n editHref: crud?.editHref ?? (() => '#'),\n onReload: data.reload,\n canDelete: crud?.canDelete,\n })\n\n const resolvedRowActions = React.useMemo(() => {\n if (rowActions) return rowActions\n if (!crud) return undefined\n return (row: T) => (\n <RowActions items={crudActions.buildItems(row, crud.extraRowItems?.(row))} />\n )\n }, [crud, crudActions, rowActions])\n\n const resolvedBulkActions = React.useMemo<BulkAction<T>[] | undefined>(() => {\n if (!crud) return bulkActions\n const deleteAction: BulkAction<T> = {\n id: 'delete',\n label: t('cpq.list.bulk.deleteSelected', 'Delete selected'),\n destructive: true,\n onExecute: crudActions.bulkDelete,\n }\n return bulkActions ? [...bulkActions, deleteAction] : [deleteAction]\n }, [bulkActions, crud, crudActions.bulkDelete, t])\n\n const resolvedFooter = crud ? (\n <>\n {footerContent}\n {crudActions.ConfirmDialogElement}\n </>\n ) : (\n footerContent\n )\n\n return (\n <Page>\n <PageBody className=\"cpq-list-view space-y-6\">\n <CpqInteractionStyles />\n {toolbarContent}\n <DataTable<T>\n title={title}\n actions={actions}\n refreshButton={{\n label: t('cpq.list.actions.refresh', 'Refresh'),\n onRefresh: data.reload,\n isRefreshing: data.isLoading,\n }}\n columns={columns}\n data={data.rows}\n searchValue={data.search}\n onSearchChange={data.setSearch}\n searchPlaceholder={searchPlaceholder ?? t('cpq.list.search.placeholder', 'Search...')}\n filters={filters}\n filterValues={data.filterValues}\n onFiltersApply={data.setFilterValues}\n onFiltersClear={data.clearFilters}\n sorting={data.sorting}\n onSortingChange={data.setSorting}\n bulkActions={resolvedBulkActions}\n selectionScopeKey={selectionScopeKey ?? tableId}\n onRowClick={onRowClick}\n rowActions={resolvedRowActions}\n perspective={{ tableId }}\n columnChooser={{ auto: true }}\n pagination={{\n page: data.page,\n pageSize,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: data.setPage,\n durationMs: data.durationMs,\n }}\n isLoading={data.isLoading}\n emptyState={emptyState}\n />\n {resolvedFooter}\n </PageBody>\n </Page>\n )\n}\n\n// \u2500\u2500\u2500 Shared interaction styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Inline style block applied inside `.cpq-list-view` (and any `.cpq-page`)\n * wrappers. Strengthens the OM hover affordance so users can clearly tell\n * which row their mouse is on, and forces `cursor: pointer` on every\n * clickable element inside CPQ pages.\n *\n * Kept inline to avoid coupling the package to a global stylesheet.\n */\nexport function CpqInteractionStyles() {\n return (\n <style>{`\n.cpq-list-view tbody tr.cursor-pointer,\n.cpq-page tbody tr.cursor-pointer {\n cursor: pointer;\n}\n.cpq-list-view tbody tr.cursor-pointer:hover,\n.cpq-page tbody tr.cursor-pointer:hover {\n background-color: rgba(0, 0, 0, 0.045);\n}\n.dark .cpq-list-view tbody tr.cursor-pointer:hover,\n.dark .cpq-page tbody tr.cursor-pointer:hover {\n background-color: rgba(255, 255, 255, 0.06);\n}\n.cpq-list-view button:not(:disabled),\n.cpq-list-view [role=\"button\"]:not([aria-disabled=\"true\"]),\n.cpq-list-view a[href],\n.cpq-list-view [data-clickable=\"true\"],\n.cpq-page button:not(:disabled),\n.cpq-page [role=\"button\"]:not([aria-disabled=\"true\"]),\n.cpq-page a[href],\n.cpq-page [data-clickable=\"true\"] {\n cursor: pointer;\n}\n/* Row actions \\`\u22EF\\` trigger: OM's hover:bg-accent is barely visible in\n the light theme. Make hover (and the open state) clearly distinct so\n users see what they're aiming at. */\n.cpq-list-view [aria-haspopup=\"menu\"]:hover,\n.cpq-page [aria-haspopup=\"menu\"]:hover,\n.cpq-list-view [aria-haspopup=\"menu\"][aria-expanded=\"true\"],\n.cpq-page [aria-haspopup=\"menu\"][aria-expanded=\"true\"] {\n background-color: rgba(0, 0, 0, 0.08);\n}\n.dark .cpq-list-view [aria-haspopup=\"menu\"]:hover,\n.dark .cpq-page [aria-haspopup=\"menu\"]:hover,\n.dark .cpq-list-view [aria-haspopup=\"menu\"][aria-expanded=\"true\"],\n.dark .cpq-page [aria-haspopup=\"menu\"][aria-expanded=\"true\"] {\n background-color: rgba(255, 255, 255, 0.1);\n}\n/* Items inside the row-actions dropdown menu \u2014 strengthen hover so the\n item under the pointer is unmistakable. The menu portal renders at the\n document body, so we can't scope it to .cpq-list-view; targeting the\n role attribute is enough. */\n[role=\"menu\"] [role=\"menuitem\"]:hover,\n[role=\"menu\"] [role=\"menuitem\"]:focus-visible {\n background-color: rgba(0, 0, 0, 0.06);\n}\n.dark [role=\"menu\"] [role=\"menuitem\"]:hover,\n.dark [role=\"menu\"] [role=\"menuitem\"]:focus-visible {\n background-color: rgba(255, 255, 255, 0.08);\n}\n/* Align padding between <a href> menu items (OM's px-2) and the <button>\n variant used for onSelect items (OM's Button size=sm = px-3). Without\n this the Delete label visibly indents past the Edit label. */\n[role=\"menu\"] button[role=\"menuitem\"] {\n padding-left: 0.5rem;\n padding-right: 0.5rem;\n}\n`}</style>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
4
+ import { Tag } from "@open-mercato/ui/primitives/tag";
5
+ import {
6
+ discountDepth,
7
+ formatDepth,
8
+ waterfallRows
9
+ } from "../lib/priceAdjustmentView.js";
10
+ function money(amount, currency) {
11
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
12
+ }
13
+ const TYPE_FALLBACKS = {
14
+ discount_percent: "% discount",
15
+ discount_absolute: "amount off",
16
+ surcharge_percent: "% surcharge",
17
+ surcharge_absolute: "fixed surcharge",
18
+ price_override: "price override"
19
+ };
20
+ function DepthBadge({ before, after, title }) {
21
+ const label = formatDepth(discountDepth(before, after));
22
+ if (!label) return null;
23
+ return /* @__PURE__ */ jsx(Tag, { variant: "neutral", className: "px-1.5 text-xs font-mono", title, children: label });
24
+ }
25
+ function PriceWaterfall({ charge, currency, reasonLabels, renderManualActions }) {
26
+ const t = useT();
27
+ const rows = waterfallRows(charge);
28
+ const fmtValue = (ruleType, value) => ruleType.endsWith("_percent") ? `${value}%` : money(value, currency);
29
+ return /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", "data-testid": "cpq-price-waterfall", children: [
30
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-muted-foreground", children: [
31
+ /* @__PURE__ */ jsx("th", { className: "text-left py-0.5 font-medium", children: t("cpq.priceAdjustments.waterfall.step", "Step") }),
32
+ /* @__PURE__ */ jsx("th", { className: "text-left py-0.5 font-medium", children: t("cpq.priceAdjustments.waterfall.source", "Source") }),
33
+ /* @__PURE__ */ jsx("th", { className: "text-right py-0.5 font-medium", children: t("cpq.priceAdjustments.waterfall.value", "Value") }),
34
+ /* @__PURE__ */ jsx("th", { className: "text-right py-0.5 font-medium", children: t("cpq.priceAdjustments.waterfall.delta", "Delta") }),
35
+ /* @__PURE__ */ jsx("th", { className: "text-right py-0.5 font-medium", children: t("cpq.priceAdjustments.waterfall.unitPrice", "Unit price") }),
36
+ renderManualActions && /* @__PURE__ */ jsx("th", { className: "w-16" })
37
+ ] }) }),
38
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row, index) => {
39
+ if (row.kind === "list" || row.kind === "standard" || row.kind === "offer") {
40
+ const label = row.kind === "list" ? t("cpq.priceAdjustments.waterfall.list", "List price") : row.kind === "standard" ? t("cpq.priceAdjustments.waterfall.standard", "Standard price") : t("cpq.priceAdjustments.waterfall.offer", "Offer price");
41
+ return /* @__PURE__ */ jsxs("tr", { className: `border-t border-border/30 ${row.kind === "offer" ? "font-semibold" : "font-medium"}`, children: [
42
+ /* @__PURE__ */ jsx("td", { className: "py-0.5", colSpan: 4, children: label }),
43
+ /* @__PURE__ */ jsx("td", { className: "py-0.5 text-right font-mono", children: money(row.unitPrice, currency) }),
44
+ renderManualActions && /* @__PURE__ */ jsx("td", {})
45
+ ] }, `${row.kind}-${index}`);
46
+ }
47
+ const source = row.kind === "rule" ? `${t("cpq.priceAdjustments.waterfall.rule", "Price rule")}: ${row.label}` : `${t("cpq.priceAdjustments.waterfall.manual", "Negotiated")}: ${reasonLabels?.[row.reasonCode ?? ""] ?? row.reasonCode ?? row.label}`;
48
+ return /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/30", children: [
49
+ /* @__PURE__ */ jsx("td", { className: "py-0.5 pl-3 text-muted-foreground", children: t(`cpq.priceAdjustments.types.${row.ruleType}`, TYPE_FALLBACKS[row.ruleType] ?? row.ruleType) }),
50
+ /* @__PURE__ */ jsx("td", { className: "py-0.5", children: source }),
51
+ /* @__PURE__ */ jsx("td", { className: "py-0.5 text-right font-mono", children: fmtValue(row.ruleType, row.value) }),
52
+ /* @__PURE__ */ jsxs("td", { className: "py-0.5 text-right font-mono", children: [
53
+ row.delta > 0 ? "+" : "",
54
+ money(row.delta, currency)
55
+ ] }),
56
+ /* @__PURE__ */ jsx("td", { className: "py-0.5 text-right font-mono", children: money(row.unitPriceAfter, currency) }),
57
+ renderManualActions && /* @__PURE__ */ jsx("td", { className: "py-0.5 text-right", children: row.kind === "manual" && row.adjustmentId ? renderManualActions(row.adjustmentId) : null })
58
+ ] }, `${row.kind}-${index}`);
59
+ }) })
60
+ ] });
61
+ }
62
+ export {
63
+ DepthBadge,
64
+ PriceWaterfall
65
+ };
66
+ //# sourceMappingURL=PriceWaterfall.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/components/PriceWaterfall.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport {\n discountDepth,\n formatDepth,\n waterfallRows,\n type WaterfallChargeInput,\n} from '../lib/priceAdjustmentView'\n\nfunction money(amount: number, currency: string): string {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)\n}\n\nconst TYPE_FALLBACKS: Record<string, string> = {\n discount_percent: '% discount',\n discount_absolute: 'amount off',\n surcharge_percent: '% surcharge',\n surcharge_absolute: 'fixed surcharge',\n price_override: 'price override',\n}\n\n/** Signed depth badge: \"\u221220%\" for a discount, \"+5%\" for a surcharge; text, never colour only. */\nexport function DepthBadge({ before, after, title }: { before: number | null | undefined; after: number | null | undefined; title?: string }) {\n const label = formatDepth(discountDepth(before, after))\n if (!label) return null\n return <Tag variant=\"neutral\" className=\"px-1.5 text-xs font-mono\" title={title}>{label}</Tag>\n}\n\nexport type PriceWaterfallProps = {\n charge: WaterfallChargeInput & { chargeName?: string }\n currency: string\n /** Reason code \u2192 label; manual rows fall back to the code. */\n reasonLabels?: Record<string, string>\n /** Optional per-adjustment actions (edit / delete) rendered on manual rows. */\n renderManualActions?: (adjustmentId: string) => React.ReactNode\n}\n\n/** Read-only list \u2192 rule \u2192 standard \u2192 manual \u2192 offer breakdown for one charge (unit prices). */\nexport function PriceWaterfall({ charge, currency, reasonLabels, renderManualActions }: PriceWaterfallProps) {\n const t = useT()\n const rows = waterfallRows(charge)\n const fmtValue = (ruleType: string, value: number) =>\n ruleType.endsWith('_percent') ? `${value}%` : money(value, currency)\n return (\n <table className=\"w-full text-xs\" data-testid=\"cpq-price-waterfall\">\n <thead>\n <tr className=\"text-muted-foreground\">\n <th className=\"text-left py-0.5 font-medium\">{t('cpq.priceAdjustments.waterfall.step', 'Step')}</th>\n <th className=\"text-left py-0.5 font-medium\">{t('cpq.priceAdjustments.waterfall.source', 'Source')}</th>\n <th className=\"text-right py-0.5 font-medium\">{t('cpq.priceAdjustments.waterfall.value', 'Value')}</th>\n <th className=\"text-right py-0.5 font-medium\">{t('cpq.priceAdjustments.waterfall.delta', 'Delta')}</th>\n <th className=\"text-right py-0.5 font-medium\">{t('cpq.priceAdjustments.waterfall.unitPrice', 'Unit price')}</th>\n {renderManualActions && <th className=\"w-16\" />}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => {\n if (row.kind === 'list' || row.kind === 'standard' || row.kind === 'offer') {\n const label = row.kind === 'list'\n ? t('cpq.priceAdjustments.waterfall.list', 'List price')\n : row.kind === 'standard'\n ? t('cpq.priceAdjustments.waterfall.standard', 'Standard price')\n : t('cpq.priceAdjustments.waterfall.offer', 'Offer price')\n return (\n <tr key={`${row.kind}-${index}`} className={`border-t border-border/30 ${row.kind === 'offer' ? 'font-semibold' : 'font-medium'}`}>\n <td className=\"py-0.5\" colSpan={4}>{label}</td>\n <td className=\"py-0.5 text-right font-mono\">{money(row.unitPrice, currency)}</td>\n {renderManualActions && <td />}\n </tr>\n )\n }\n const source = row.kind === 'rule'\n ? `${t('cpq.priceAdjustments.waterfall.rule', 'Price rule')}: ${row.label}`\n : `${t('cpq.priceAdjustments.waterfall.manual', 'Negotiated')}: ${reasonLabels?.[row.reasonCode ?? ''] ?? row.reasonCode ?? row.label}`\n return (\n <tr key={`${row.kind}-${index}`} className=\"border-t border-border/30\">\n <td className=\"py-0.5 pl-3 text-muted-foreground\">{t(`cpq.priceAdjustments.types.${row.ruleType}`, TYPE_FALLBACKS[row.ruleType] ?? row.ruleType)}</td>\n <td className=\"py-0.5\">{source}</td>\n <td className=\"py-0.5 text-right font-mono\">{fmtValue(row.ruleType, row.value)}</td>\n <td className=\"py-0.5 text-right font-mono\">{row.delta > 0 ? '+' : ''}{money(row.delta, currency)}</td>\n <td className=\"py-0.5 text-right font-mono\">{money(row.unitPriceAfter, currency)}</td>\n {renderManualActions && (\n <td className=\"py-0.5 text-right\">{row.kind === 'manual' && row.adjustmentId ? renderManualActions(row.adjustmentId) : null}</td>\n )}\n </tr>\n )\n })}\n </tbody>\n </table>\n )\n}\n"],
5
+ "mappings": ";AA4BS,cAqBD,YArBC;AAzBT,SAAS,YAAY;AACrB,SAAS,WAAW;AACpB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,SAAS,MAAM,QAAgB,UAA0B;AACvD,SAAO,IAAI,KAAK,aAAa,SAAS,EAAE,OAAO,YAAY,SAAS,CAAC,EAAE,OAAO,MAAM;AACtF;AAEA,MAAM,iBAAyC;AAAA,EAC7C,kBAAkB;AAAA,EAClB,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,gBAAgB;AAClB;AAGO,SAAS,WAAW,EAAE,QAAQ,OAAO,MAAM,GAA4F;AAC5I,QAAM,QAAQ,YAAY,cAAc,QAAQ,KAAK,CAAC;AACtD,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,oBAAC,OAAI,SAAQ,WAAU,WAAU,4BAA2B,OAAe,iBAAM;AAC1F;AAYO,SAAS,eAAe,EAAE,QAAQ,UAAU,cAAc,oBAAoB,GAAwB;AAC3G,QAAM,IAAI,KAAK;AACf,QAAM,OAAO,cAAc,MAAM;AACjC,QAAM,WAAW,CAAC,UAAkB,UAClC,SAAS,SAAS,UAAU,IAAI,GAAG,KAAK,MAAM,MAAM,OAAO,QAAQ;AACrE,SACE,qBAAC,WAAM,WAAU,kBAAiB,eAAY,uBAC5C;AAAA,wBAAC,WACC,+BAAC,QAAG,WAAU,yBACZ;AAAA,0BAAC,QAAG,WAAU,gCAAgC,YAAE,uCAAuC,MAAM,GAAE;AAAA,MAC/F,oBAAC,QAAG,WAAU,gCAAgC,YAAE,yCAAyC,QAAQ,GAAE;AAAA,MACnG,oBAAC,QAAG,WAAU,iCAAiC,YAAE,wCAAwC,OAAO,GAAE;AAAA,MAClG,oBAAC,QAAG,WAAU,iCAAiC,YAAE,wCAAwC,OAAO,GAAE;AAAA,MAClG,oBAAC,QAAG,WAAU,iCAAiC,YAAE,4CAA4C,YAAY,GAAE;AAAA,MAC1G,uBAAuB,oBAAC,QAAG,WAAU,QAAO;AAAA,OAC/C,GACF;AAAA,IACA,oBAAC,WACE,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAI,IAAI,SAAS,UAAU,IAAI,SAAS,cAAc,IAAI,SAAS,SAAS;AAC1E,cAAM,QAAQ,IAAI,SAAS,SACvB,EAAE,uCAAuC,YAAY,IACrD,IAAI,SAAS,aACX,EAAE,2CAA2C,gBAAgB,IAC7D,EAAE,wCAAwC,aAAa;AAC7D,eACE,qBAAC,QAAgC,WAAW,6BAA6B,IAAI,SAAS,UAAU,kBAAkB,aAAa,IAC7H;AAAA,8BAAC,QAAG,WAAU,UAAS,SAAS,GAAI,iBAAM;AAAA,UAC1C,oBAAC,QAAG,WAAU,+BAA+B,gBAAM,IAAI,WAAW,QAAQ,GAAE;AAAA,UAC3E,uBAAuB,oBAAC,QAAG;AAAA,aAHrB,GAAG,IAAI,IAAI,IAAI,KAAK,EAI7B;AAAA,MAEJ;AACA,YAAM,SAAS,IAAI,SAAS,SACxB,GAAG,EAAE,uCAAuC,YAAY,CAAC,KAAK,IAAI,KAAK,KACvE,GAAG,EAAE,yCAAyC,YAAY,CAAC,KAAK,eAAe,IAAI,cAAc,EAAE,KAAK,IAAI,cAAc,IAAI,KAAK;AACvI,aACE,qBAAC,QAAgC,WAAU,6BACzC;AAAA,4BAAC,QAAG,WAAU,qCAAqC,YAAE,8BAA8B,IAAI,QAAQ,IAAI,eAAe,IAAI,QAAQ,KAAK,IAAI,QAAQ,GAAE;AAAA,QACjJ,oBAAC,QAAG,WAAU,UAAU,kBAAO;AAAA,QAC/B,oBAAC,QAAG,WAAU,+BAA+B,mBAAS,IAAI,UAAU,IAAI,KAAK,GAAE;AAAA,QAC/E,qBAAC,QAAG,WAAU,+BAA+B;AAAA,cAAI,QAAQ,IAAI,MAAM;AAAA,UAAI,MAAM,IAAI,OAAO,QAAQ;AAAA,WAAE;AAAA,QAClG,oBAAC,QAAG,WAAU,+BAA+B,gBAAM,IAAI,gBAAgB,QAAQ,GAAE;AAAA,QAChF,uBACC,oBAAC,QAAG,WAAU,qBAAqB,cAAI,SAAS,YAAY,IAAI,eAAe,oBAAoB,IAAI,YAAY,IAAI,MAAK;AAAA,WAPvH,GAAG,IAAI,IAAI,IAAI,KAAK,EAS7B;AAAA,IAEJ,CAAC,GACH;AAAA,KACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,117 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
4
+ import { formatDepth } from "../lib/priceAdjustmentView.js";
5
+ import { chargeTypeDepths, formatChangePct } from "../lib/quoteSummaryView.js";
6
+ import { formatMoney } from "../lib/formatMoney.js";
7
+ function SummaryFigure({
8
+ label,
9
+ value,
10
+ note,
11
+ title,
12
+ size = "lg",
13
+ valueClassName,
14
+ testId
15
+ }) {
16
+ return /* @__PURE__ */ jsxs("div", { "data-testid": testId, children: [
17
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", title, children: label }),
18
+ /* @__PURE__ */ jsxs("p", { className: `${size === "lg" ? "text-lg" : "text-sm"} font-semibold font-mono ${valueClassName ?? ""}`, children: [
19
+ value,
20
+ note && /* @__PURE__ */ jsxs("span", { className: "ml-1.5 text-xs font-normal text-muted-foreground", children: [
21
+ "(",
22
+ note,
23
+ ")"
24
+ ] })
25
+ ] })
26
+ ] });
27
+ }
28
+ function PricingSummaryCard({ summary, currency, arcDelta }) {
29
+ const t = useT();
30
+ const typeDepths = chargeTypeDepths(summary);
31
+ const usageCharges = summary.usageCharges ?? [];
32
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", "data-testid": "cpq-pricing-summary", children: [
33
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium mb-3", children: t("cpq.quotes.summary.title", "Pricing Summary") }),
34
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-4", children: [
35
+ /* @__PURE__ */ jsx(
36
+ SummaryFigure,
37
+ {
38
+ label: t("cpq.quotes.summary.nrc", "Non-Recurring (NRC)"),
39
+ value: formatMoney(summary.nrcTotal, currency),
40
+ note: formatDepth(typeDepths.nrc),
41
+ testId: "cpq-summary-nrc"
42
+ }
43
+ ),
44
+ /* @__PURE__ */ jsx(
45
+ SummaryFigure,
46
+ {
47
+ label: t("cpq.quotes.summary.mrc", "Monthly Recurring (MRC)"),
48
+ value: formatMoney(summary.mrcTotal, currency),
49
+ note: formatDepth(typeDepths.mrc),
50
+ testId: "cpq-summary-mrc"
51
+ }
52
+ ),
53
+ summary.dealValue && /* @__PURE__ */ jsx(
54
+ SummaryFigure,
55
+ {
56
+ label: t("cpq.priceAdjustments.summary.dealValue", "Deal value"),
57
+ title: t("cpq.priceAdjustments.summary.hint", "Deal value counts recurring charges over the term plus one-time charges; existing subscription items on change quotes are excluded."),
58
+ value: formatMoney(summary.dealValue.offer, currency),
59
+ note: formatDepth(summary.discountDepth?.total ?? null),
60
+ testId: "cpq-deal-value"
61
+ }
62
+ ),
63
+ summary.usageTotalEstimated != null && /* @__PURE__ */ jsx(
64
+ SummaryFigure,
65
+ {
66
+ label: t("cpq.quotes.summary.usageEstimated", "Usage (est.)"),
67
+ value: formatMoney(summary.usageTotalEstimated, currency)
68
+ }
69
+ ),
70
+ usageCharges.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
71
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("cpq.quotes.summary.usageCharges", "Usage Charges") }),
72
+ usageCharges.map((uc, i) => /* @__PURE__ */ jsxs("p", { className: "text-xs mt-0.5", children: [
73
+ uc.chargeName,
74
+ ": ",
75
+ formatMoney(uc.unitPrice, currency),
76
+ "/unit"
77
+ ] }, i))
78
+ ] })
79
+ ] }),
80
+ arcDelta && /* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-2 sm:grid-cols-4 gap-4 border-t pt-3", "data-testid": "cpq-arc-delta", children: [
81
+ /* @__PURE__ */ jsx(
82
+ SummaryFigure,
83
+ {
84
+ size: "sm",
85
+ testId: "cpq-arc-current-mrc",
86
+ label: t("cpq.quotes.summary.arc.currentMrc", "Current MRC"),
87
+ value: formatMoney(arcDelta.currentMrc, currency)
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsx(
91
+ SummaryFigure,
92
+ {
93
+ size: "sm",
94
+ testId: "cpq-arc-new-mrc",
95
+ label: t("cpq.quotes.summary.arc.newMrc", "New MRC"),
96
+ value: formatMoney(arcDelta.newMrc, currency)
97
+ }
98
+ ),
99
+ /* @__PURE__ */ jsx(
100
+ SummaryFigure,
101
+ {
102
+ size: "sm",
103
+ testId: "cpq-arc-mrc-change",
104
+ label: t("cpq.quotes.summary.arc.mrcChange", "MRC change"),
105
+ value: `${arcDelta.delta > 0 ? "+" : ""}${formatMoney(arcDelta.delta, currency)}`,
106
+ valueClassName: arcDelta.delta > 0 ? "text-status-success-text" : arcDelta.delta < 0 ? "text-destructive" : void 0,
107
+ note: formatChangePct(arcDelta.deltaPct)
108
+ }
109
+ )
110
+ ] })
111
+ ] });
112
+ }
113
+ export {
114
+ PricingSummaryCard,
115
+ SummaryFigure
116
+ };
117
+ //# sourceMappingURL=PricingSummaryCard.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/components/PricingSummaryCard.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatDepth } from '../lib/priceAdjustmentView'\nimport { chargeTypeDepths, formatChangePct, type ArcRecurringDelta } from '../lib/quoteSummaryView'\nimport { formatMoney } from '../lib/formatMoney'\n\n/** Pricing summary fields shared by quote and order results. Waterfall fields are absent on legacy snapshots. */\nexport type PricingSummaryView = {\n nrcTotal: number\n mrcTotal: number\n listNrcTotal?: number\n listMrcTotal?: number\n dealValue?: { list: number; standard: number; offer: number }\n discountDepth?: { total: number | null } | null\n usageTotalEstimated?: number | null\n usageCharges?: Array<{ chargeName: string; unitPrice: number }>\n}\n\nexport function SummaryFigure({\n label,\n value,\n note,\n title,\n size = 'lg',\n valueClassName,\n testId,\n}: {\n label: string\n value: string\n note?: string | null\n title?: string\n size?: 'lg' | 'sm'\n valueClassName?: string\n testId?: string\n}) {\n return (\n <div data-testid={testId}>\n <p className=\"text-xs text-muted-foreground\" title={title}>{label}</p>\n <p className={`${size === 'lg' ? 'text-lg' : 'text-sm'} font-semibold font-mono ${valueClassName ?? ''}`}>\n {value}\n {note && <span className=\"ml-1.5 text-xs font-normal text-muted-foreground\">({note})</span>}\n </p>\n </div>\n )\n}\n\n/**\n * NRC / MRC / deal value with discount depth in brackets, plus the ARC\n * current \u2192 new MRC row on Amend / Renew documents. Shared by the quote and\n * order detail pages so both read the same.\n */\nexport function PricingSummaryCard({ summary, currency, arcDelta }: {\n summary: PricingSummaryView\n currency: string\n arcDelta?: ArcRecurringDelta | null\n}) {\n const t = useT()\n const typeDepths = chargeTypeDepths(summary)\n const usageCharges = summary.usageCharges ?? []\n return (\n <div className=\"rounded-lg border bg-card p-4\" data-testid=\"cpq-pricing-summary\">\n <h3 className=\"text-sm font-medium mb-3\">{t('cpq.quotes.summary.title', 'Pricing Summary')}</h3>\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <SummaryFigure\n label={t('cpq.quotes.summary.nrc', 'Non-Recurring (NRC)')}\n value={formatMoney(summary.nrcTotal, currency)}\n note={formatDepth(typeDepths.nrc)}\n testId=\"cpq-summary-nrc\"\n />\n <SummaryFigure\n label={t('cpq.quotes.summary.mrc', 'Monthly Recurring (MRC)')}\n value={formatMoney(summary.mrcTotal, currency)}\n note={formatDepth(typeDepths.mrc)}\n testId=\"cpq-summary-mrc\"\n />\n {summary.dealValue && (\n <SummaryFigure\n label={t('cpq.priceAdjustments.summary.dealValue', 'Deal value')}\n title={t('cpq.priceAdjustments.summary.hint', 'Deal value counts recurring charges over the term plus one-time charges; existing subscription items on change quotes are excluded.')}\n value={formatMoney(summary.dealValue.offer, currency)}\n note={formatDepth(summary.discountDepth?.total ?? null)}\n testId=\"cpq-deal-value\"\n />\n )}\n {summary.usageTotalEstimated != null && (\n <SummaryFigure\n label={t('cpq.quotes.summary.usageEstimated', 'Usage (est.)')}\n value={formatMoney(summary.usageTotalEstimated, currency)}\n />\n )}\n {usageCharges.length > 0 && (\n <div>\n <p className=\"text-xs text-muted-foreground\">{t('cpq.quotes.summary.usageCharges', 'Usage Charges')}</p>\n {usageCharges.map((uc, i) => (\n <p key={i} className=\"text-xs mt-0.5\">{uc.chargeName}: {formatMoney(uc.unitPrice, currency)}/unit</p>\n ))}\n </div>\n )}\n </div>\n {arcDelta && (\n <div className=\"mt-4 grid grid-cols-2 sm:grid-cols-4 gap-4 border-t pt-3\" data-testid=\"cpq-arc-delta\">\n <SummaryFigure\n size=\"sm\"\n testId=\"cpq-arc-current-mrc\"\n label={t('cpq.quotes.summary.arc.currentMrc', 'Current MRC')}\n value={formatMoney(arcDelta.currentMrc, currency)}\n />\n <SummaryFigure\n size=\"sm\"\n testId=\"cpq-arc-new-mrc\"\n label={t('cpq.quotes.summary.arc.newMrc', 'New MRC')}\n value={formatMoney(arcDelta.newMrc, currency)}\n />\n <SummaryFigure\n size=\"sm\"\n testId=\"cpq-arc-mrc-change\"\n label={t('cpq.quotes.summary.arc.mrcChange', 'MRC change')}\n value={`${arcDelta.delta > 0 ? '+' : ''}${formatMoney(arcDelta.delta, currency)}`}\n valueClassName={arcDelta.delta > 0 ? 'text-status-success-text' : arcDelta.delta < 0 ? 'text-destructive' : undefined}\n note={formatChangePct(arcDelta.deltaPct)}\n />\n </div>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
4
+ function useGrantedFeatures(features) {
5
+ const [granted, setGranted] = React.useState([]);
6
+ const key = features.join("|");
7
+ React.useEffect(() => {
8
+ if (!key) return;
9
+ let cancelled = false;
10
+ void (async () => {
11
+ const call = await apiCall("/api/auth/feature-check", {
12
+ method: "POST",
13
+ headers: { "content-type": "application/json" },
14
+ body: JSON.stringify({ features: key.split("|") })
15
+ });
16
+ if (!cancelled && call.ok && call.result?.granted) setGranted(call.result.granted);
17
+ })();
18
+ return () => {
19
+ cancelled = true;
20
+ };
21
+ }, [key]);
22
+ return granted;
23
+ }
24
+ export {
25
+ useGrantedFeatures
26
+ };
27
+ //# sourceMappingURL=useGrantedFeatures.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/components/useGrantedFeatures.ts"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\n\ntype FeatureCheckResponse = { granted: string[] }\n\n/**\n * Resolve which of `features` the current user holds, through the framework's\n * `POST /api/auth/feature-check` endpoint (the same one the component-override\n * provider uses). Returns an empty list until the check completes, so callers\n * hide privileged actions by default.\n */\nexport function useGrantedFeatures(features: readonly string[]): readonly string[] {\n const [granted, setGranted] = React.useState<readonly string[]>([])\n const key = features.join('|')\n\n React.useEffect(() => {\n if (!key) return\n let cancelled = false\n void (async () => {\n const call = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: key.split('|') }),\n })\n if (!cancelled && call.ok && call.result?.granted) setGranted(call.result.granted)\n })()\n return () => {\n cancelled = true\n }\n }, [key])\n\n return granted\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,eAAe;AAUjB,SAAS,mBAAmB,UAAgD;AACjF,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAA4B,CAAC,CAAC;AAClE,QAAM,MAAM,SAAS,KAAK,GAAG;AAE7B,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,IAAK;AACV,QAAI,YAAY;AAChB,UAAM,YAAY;AAChB,YAAM,OAAO,MAAM,QAA8B,2BAA2B;AAAA,QAC1E,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,IAAI,MAAM,GAAG,EAAE,CAAC;AAAA,MACnD,CAAC;AACD,UAAI,CAAC,aAAa,KAAK,MAAM,KAAK,QAAQ,QAAS,YAAW,KAAK,OAAO,OAAO;AAAA,IACnF,GAAG;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,GAAG,CAAC;AAER,SAAO;AACT;",
6
+ "names": []
7
+ }