@dainamite/cpq 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/MIGRATION.md +74 -0
- package/dist/modules/cpq/acl.js +1 -0
- package/dist/modules/cpq/acl.js.map +2 -2
- package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js +45 -0
- package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js.map +7 -0
- package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
- package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
- package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
- package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
- package/dist/modules/cpq/api/orders/[orderId]/route.js +17 -1
- package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
- package/dist/modules/cpq/api/orders/route.js +47 -2
- package/dist/modules/cpq/api/orders/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +42 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +32 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/price/route.js +43 -3
- package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +3 -9
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +3 -3
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
- package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +174 -184
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
- package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +140 -280
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
- package/dist/modules/cpq/backend/cpq/orders/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +41 -36
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
- package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
- package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +208 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +269 -311
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
- package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
- package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +19 -15
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
- package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
- package/dist/modules/cpq/ce.js +8 -0
- package/dist/modules/cpq/ce.js.map +2 -2
- package/dist/modules/cpq/cli.js +34 -1
- package/dist/modules/cpq/cli.js.map +2 -2
- package/dist/modules/cpq/commands/price-adjustments.js +107 -0
- package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
- package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
- package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
- package/dist/modules/cpq/components/ChargeBreakdownTable.js +148 -0
- package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
- package/dist/modules/cpq/components/PriceWaterfall.js +68 -0
- package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
- package/dist/modules/cpq/components/PricingSummaryCard.js +112 -0
- package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
- package/dist/modules/cpq/components/StatusPath.js +5 -3
- package/dist/modules/cpq/components/StatusPath.js.map +2 -2
- package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
- package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
- package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
- package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
- package/dist/modules/cpq/data/entities.js +97 -0
- package/dist/modules/cpq/data/entities.js.map +2 -2
- package/dist/modules/cpq/data/price-adjustments.js +70 -0
- package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
- package/dist/modules/cpq/data/validators.js +4 -0
- package/dist/modules/cpq/data/validators.js.map +2 -2
- package/dist/modules/cpq/di.js +6 -0
- package/dist/modules/cpq/di.js.map +2 -2
- package/dist/modules/cpq/events.js +47 -0
- package/dist/modules/cpq/events.js.map +2 -2
- package/dist/modules/cpq/i18n/de.json +1220 -0
- package/dist/modules/cpq/i18n/en.json +1220 -0
- package/dist/modules/cpq/i18n/es.json +1220 -0
- package/dist/modules/cpq/i18n/pl.json +1220 -0
- package/dist/modules/cpq/index.js +1 -1
- package/dist/modules/cpq/index.js.map +2 -2
- package/dist/modules/cpq/lib/auditActor.js +10 -0
- package/dist/modules/cpq/lib/auditActor.js.map +7 -0
- package/dist/modules/cpq/lib/formatMoney.js +7 -0
- package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
- package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
- package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
- package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
- package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
- package/dist/modules/cpq/lib/quotePermissions.js +13 -0
- package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
- package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
- package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
- package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
- package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
- package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
- package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
- package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
- package/dist/modules/cpq/services/cpqOrderService.js +25 -28
- package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
- package/dist/modules/cpq/services/cpqPricingService.js +7 -0
- package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
- package/dist/modules/cpq/services/cpqQuotingService.js +330 -109
- package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
- package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
- package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
- package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
- package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
- package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
- package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
- package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
- package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
- package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
- package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
- package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
- package/dist/modules/cpq/services/types.js.map +2 -2
- package/dist/modules/cpq/setup.js +6 -0
- package/dist/modules/cpq/setup.js.map +2 -2
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection-table.js +2 -2
- package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
- package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
- package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
- package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
- package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
- package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
- package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/index.js +12 -0
- package/dist/modules/cpq/workflows/steps/index.js.map +2 -2
- package/package.json +1 -1
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
- package/src/modules/cpq/__tests__/i18n/dictionaries.test.ts +62 -0
- package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
- package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -0
- package/src/modules/cpq/acl.ts +1 -0
- package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
- package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
- package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
- package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
- package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
- package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
- package/src/modules/cpq/api/orders/[orderId]/route.ts +18 -0
- package/src/modules/cpq/api/orders/route.ts +51 -1
- package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
- package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +48 -1
- package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
- package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
- package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
- package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
- package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
- package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +40 -1
- package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
- package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
- package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
- package/src/modules/cpq/api/quotes/price/route.ts +28 -2
- package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +3 -9
- package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
- package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +3 -3
- package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +164 -120
- package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
- package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
- package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +122 -308
- package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
- package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
- package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
- package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
- package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
- package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
- package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
- package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +304 -348
- package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
- package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
- package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
- package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
- package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
- package/src/modules/cpq/ce.ts +8 -0
- package/src/modules/cpq/cli.ts +48 -1
- package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
- package/src/modules/cpq/commands/price-adjustments.ts +90 -0
- package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
- package/src/modules/cpq/components/ChargeBreakdownTable.tsx +203 -0
- package/src/modules/cpq/components/PriceWaterfall.tsx +96 -0
- package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
- package/src/modules/cpq/components/StatusPath.tsx +7 -4
- package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
- package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
- package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
- package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
- package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
- package/src/modules/cpq/data/entities.ts +83 -0
- package/src/modules/cpq/data/price-adjustments.ts +67 -0
- package/src/modules/cpq/data/validators.ts +3 -0
- package/src/modules/cpq/di.ts +6 -0
- package/src/modules/cpq/events.ts +53 -2
- package/src/modules/cpq/i18n/de.json +1220 -0
- package/src/modules/cpq/i18n/en.json +1220 -0
- package/src/modules/cpq/i18n/es.json +1220 -0
- package/src/modules/cpq/i18n/pl.json +1220 -0
- package/src/modules/cpq/index.ts +1 -1
- package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
- package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
- package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
- package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
- package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
- package/src/modules/cpq/lib/auditActor.ts +13 -0
- package/src/modules/cpq/lib/formatMoney.ts +4 -0
- package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
- package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
- package/src/modules/cpq/lib/quotePermissions.ts +22 -0
- package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
- package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
- package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
- package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
- package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
- package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
- package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
- package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
- package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
- package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
- package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
- package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
- package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
- package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
- package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
- package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
- package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
- package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
- package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
- package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
- package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
- package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
- package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
- package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
- package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
- package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
- package/src/modules/cpq/services/cpqOrderService.ts +22 -28
- package/src/modules/cpq/services/cpqPricingService.ts +8 -0
- package/src/modules/cpq/services/cpqQuotingService.ts +374 -125
- package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
- package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
- package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
- package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
- package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
- package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
- package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
- package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
- package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
- package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
- package/src/modules/cpq/services/types.ts +34 -1
- package/src/modules/cpq/setup.ts +7 -1
- package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
- package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
- package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
- package/src/modules/cpq/widgets/injection-table.ts +8 -2
- package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
- package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
- package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
- package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
- package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
- package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
- package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
- package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
- package/src/modules/cpq/workflows/steps/index.ts +12 -0
- package/src/modules/cpq/workflows/types.ts +4 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { resolveAuditActor } from '../lib/auditActor'
|
|
3
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
4
|
+
import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
5
|
+
import { registerCommand, type CommandHandler, type CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
6
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
|
+
import { cpqPriceAdjustmentInputSchema, cpqPriceAdjustmentPatchSchema } from '../data/price-adjustments'
|
|
8
|
+
import { QuotingError } from '../services/cpqQuotingService'
|
|
9
|
+
import type { CpqAdjustmentService, AdjustmentWriteResult, AdjustmentSnapshot } from '../services/priceAdjustments/adjustmentService'
|
|
10
|
+
|
|
11
|
+
const id = z.string().uuid()
|
|
12
|
+
const createSchema = z.object({ quoteId: id, adjustment: cpqPriceAdjustmentInputSchema }).strict()
|
|
13
|
+
const updateSchema = z.object({ quoteId: id, adjustmentId: id, patch: cpqPriceAdjustmentPatchSchema }).strict()
|
|
14
|
+
const deleteSchema = z.object({ quoteId: id, adjustmentId: id, expectedRevision: z.number().int().min(0).optional() }).strict()
|
|
15
|
+
// Undo data is parsed independently of the original request and never trusted
|
|
16
|
+
// to supply actor/scope. Its target is checked again under the aggregate lock.
|
|
17
|
+
const snapshotSchema = z.object({
|
|
18
|
+
id, quoteConfigurationId: id, quoteId: id, tenantId: id, organizationId: id,
|
|
19
|
+
createdByUserId: id, createdAt: z.string().datetime(), updatedAt: z.string().datetime(),
|
|
20
|
+
deletedAt: z.string().datetime().nullable(), isActive: z.boolean(),
|
|
21
|
+
level: z.enum(['charge', 'line']), quoteLineId: id, chargeCode: z.string().nullish(),
|
|
22
|
+
chargeTypeFilter: z.enum(['nrc', 'mrc']).nullish(), adjustmentType: z.enum(['discount_percent', 'discount_absolute', 'price_override']),
|
|
23
|
+
value: z.number(), currencyCode: z.string().nullish(), reasonCode: z.string(), reasonNote: z.string().nullish(),
|
|
24
|
+
})
|
|
25
|
+
const undoSchema = z.object({ quoteId: id, before: snapshotSchema.nullable(), after: snapshotSchema })
|
|
26
|
+
|
|
27
|
+
async function context(ctx: CommandRuntimeContext) {
|
|
28
|
+
// RBAC is checked for the authenticated subject (user or API key); the
|
|
29
|
+
// stored author must be a user id (uuid columns).
|
|
30
|
+
const subject = ctx.auth?.sub
|
|
31
|
+
const actor = resolveAuditActor(ctx.auth)
|
|
32
|
+
const tenantId = ctx.organizationScope?.tenantId ?? ctx.auth?.tenantId
|
|
33
|
+
const organizationId = ctx.selectedOrganizationId
|
|
34
|
+
if (!subject || !tenantId || !organizationId) throw new QuotingError(403, 'An authenticated tenant and organization are required')
|
|
35
|
+
if (!actor) throw new QuotingError(403, 'Price adjustments require a user or a user-bound API key')
|
|
36
|
+
const scope = { tenantId, organizationId }
|
|
37
|
+
const rbac = ctx.container.resolve<RbacService>('rbacService')
|
|
38
|
+
if (!(await rbac.userHasAllFeatures(subject, ['cpq.quotes.manage', 'cpq.quotes.adjust_price'], scope))) throw new QuotingError(403, 'Price adjustment permission required')
|
|
39
|
+
return { actor, scope, service: ctx.container.resolve<CpqAdjustmentService>('cpqAdjustmentService') }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function buildLog(quoteId: string, result: AdjustmentWriteResult) {
|
|
43
|
+
const { translate } = await resolveTranslations()
|
|
44
|
+
return { actionLabel: translate('cpq.priceAdjustments.audit.adjust', 'Adjust quote price'), resourceKind: 'cpq.quote_price_adjustment', resourceId: result.adjustment.id,
|
|
45
|
+
tenantId: result.adjustment.tenantId, organizationId: result.adjustment.organizationId,
|
|
46
|
+
snapshotBefore: result.before, snapshotAfter: result.adjustment,
|
|
47
|
+
payload: { undo: { quoteId, before: result.before, after: result.adjustment } } }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function undo({ logEntry, ctx }: Parameters<NonNullable<CommandHandler['undo']>>[0]) {
|
|
51
|
+
const envelope = undoSchema.safeParse(extractUndoPayload<unknown>(logEntry))
|
|
52
|
+
if (!envelope.success) throw new QuotingError(422, 'Invalid adjustment undo data')
|
|
53
|
+
const { actor, scope, service } = await context(ctx)
|
|
54
|
+
const { quoteId, before, after } = envelope.data
|
|
55
|
+
await service.undo(quoteId, after.id, before as AdjustmentSnapshot | null, after as AdjustmentSnapshot, actor, scope)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const createAdjustmentCommand: CommandHandler<z.infer<typeof createSchema>, AdjustmentWriteResult> = {
|
|
59
|
+
id: 'cpq.quote_price_adjustment.create', isUndoable: true,
|
|
60
|
+
async execute(input, ctx) {
|
|
61
|
+
const parsed = createSchema.parse(input)
|
|
62
|
+
const { actor, scope, service } = await context(ctx)
|
|
63
|
+
return service.create(parsed.quoteId, parsed.adjustment, actor, scope)
|
|
64
|
+
},
|
|
65
|
+
buildLog: ({ input, result }) => buildLog(input.quoteId, result),
|
|
66
|
+
undo,
|
|
67
|
+
}
|
|
68
|
+
export const updateAdjustmentCommand: CommandHandler<z.infer<typeof updateSchema>, AdjustmentWriteResult> = {
|
|
69
|
+
id: 'cpq.quote_price_adjustment.update', isUndoable: true,
|
|
70
|
+
async execute(input, ctx) {
|
|
71
|
+
const parsed = updateSchema.parse(input)
|
|
72
|
+
const { actor, scope, service } = await context(ctx)
|
|
73
|
+
return service.update(parsed.quoteId, parsed.adjustmentId, parsed.patch, actor, scope)
|
|
74
|
+
},
|
|
75
|
+
buildLog: ({ input, result }) => buildLog(input.quoteId, result),
|
|
76
|
+
undo,
|
|
77
|
+
}
|
|
78
|
+
export const deleteAdjustmentCommand: CommandHandler<z.infer<typeof deleteSchema>, AdjustmentWriteResult> = {
|
|
79
|
+
id: 'cpq.quote_price_adjustment.delete', isUndoable: true,
|
|
80
|
+
async execute(input, ctx) {
|
|
81
|
+
const parsed = deleteSchema.parse(input)
|
|
82
|
+
const { actor, scope, service } = await context(ctx)
|
|
83
|
+
return service.remove(parsed.quoteId, parsed.adjustmentId, actor, scope, parsed.expectedRevision)
|
|
84
|
+
},
|
|
85
|
+
buildLog: ({ input, result }) => buildLog(input.quoteId, result),
|
|
86
|
+
undo,
|
|
87
|
+
}
|
|
88
|
+
registerCommand(createAdjustmentCommand)
|
|
89
|
+
registerCommand(updateAdjustmentCommand)
|
|
90
|
+
registerCommand(deleteAdjustmentCommand)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
6
|
+
import { formatMoney } from '../lib/formatMoney'
|
|
7
|
+
|
|
8
|
+
export type LineAction = 'add' | 'modify' | 'cancel'
|
|
9
|
+
|
|
10
|
+
const ACTION_VARIANTS: Record<LineAction, 'info' | 'warning' | 'error'> = {
|
|
11
|
+
add: 'info',
|
|
12
|
+
modify: 'warning',
|
|
13
|
+
cancel: 'error',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Coloured add / modify / cancel tag on quote and order lines. */
|
|
17
|
+
export function LineActionBadge({ action, label, title }: { action: string; label?: string; title?: string }) {
|
|
18
|
+
const variant = ACTION_VARIANTS[action as LineAction] ?? 'info'
|
|
19
|
+
return (
|
|
20
|
+
<Tag variant={variant} className="px-1.5 text-xs" title={title}>
|
|
21
|
+
{label ?? action}
|
|
22
|
+
</Tag>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ArcDiffLine = {
|
|
27
|
+
action: string
|
|
28
|
+
quantity: number
|
|
29
|
+
nrcTotal: number
|
|
30
|
+
mrcTotal: number
|
|
31
|
+
arcSource: { mrcAmount: number; nrcAmount: number; quantity: number } | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Before / after / Δ preview for a line that changes an existing subscription item. */
|
|
35
|
+
export function ArcLineDiff({ line, currency }: { line: ArcDiffLine; currency: string }) {
|
|
36
|
+
const t = useT()
|
|
37
|
+
const src = line.arcSource
|
|
38
|
+
if (!src) return null
|
|
39
|
+
const isCancel = line.action === 'cancel'
|
|
40
|
+
const rows: Array<{ label: string; before: number; after: number; asCurrency: boolean }> = [
|
|
41
|
+
{ label: t('cpq.arc.diff.quantity', 'Quantity'), before: src.quantity, after: isCancel ? 0 : line.quantity, asCurrency: false },
|
|
42
|
+
{ label: t('cpq.arc.diff.mrc', 'MRC'), before: src.mrcAmount, after: isCancel ? 0 : line.mrcTotal, asCurrency: true },
|
|
43
|
+
{ label: t('cpq.arc.diff.nrc', 'NRC'), before: src.nrcAmount, after: isCancel ? 0 : line.nrcTotal, asCurrency: true },
|
|
44
|
+
]
|
|
45
|
+
const fmtVal = (value: number, asCurrency: boolean) => (asCurrency ? formatMoney(value, currency) : String(value))
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="space-y-1.5" data-testid="cpq-arc-line-diff">
|
|
49
|
+
<p className="text-xs font-medium text-muted-foreground">{t('cpq.arc.diff.title', 'Change Preview')}</p>
|
|
50
|
+
<div className="rounded border bg-background overflow-hidden">
|
|
51
|
+
<table className="w-full text-xs">
|
|
52
|
+
<thead className="bg-muted/40">
|
|
53
|
+
<tr className="text-muted-foreground">
|
|
54
|
+
<th className="text-left px-3 py-1.5 font-medium" />
|
|
55
|
+
<th className="text-right px-3 py-1.5 font-medium">{t('cpq.arc.diff.before', 'Before')}</th>
|
|
56
|
+
<th className="text-right px-3 py-1.5 font-medium">{t('cpq.arc.diff.after', 'After')}</th>
|
|
57
|
+
<th className="text-right px-3 py-1.5 font-medium">Δ</th>
|
|
58
|
+
</tr>
|
|
59
|
+
</thead>
|
|
60
|
+
<tbody>
|
|
61
|
+
{rows.map((r) => {
|
|
62
|
+
const delta = r.after - r.before
|
|
63
|
+
const showDelta = delta !== 0
|
|
64
|
+
return (
|
|
65
|
+
<tr key={r.label} className="border-t border-dashed border-border/40">
|
|
66
|
+
<td className="px-3 py-1 text-muted-foreground">{r.label}</td>
|
|
67
|
+
<td className="px-3 py-1 text-right font-mono">{fmtVal(r.before, r.asCurrency)}</td>
|
|
68
|
+
<td className={`px-3 py-1 text-right font-mono ${isCancel ? 'text-muted-foreground line-through' : ''}`}>
|
|
69
|
+
{fmtVal(r.after, r.asCurrency)}
|
|
70
|
+
</td>
|
|
71
|
+
<td className={`px-3 py-1 text-right font-mono font-medium ${showDelta ? (delta > 0 ? 'text-status-success-text' : 'text-destructive') : 'text-muted-foreground'}`}>
|
|
72
|
+
{showDelta ? `${delta > 0 ? '+' : ''}${fmtVal(delta, r.asCurrency)}` : '—'}
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
)
|
|
76
|
+
})}
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
</div>
|
|
80
|
+
{isCancel && (
|
|
81
|
+
<p className="text-xs text-destructive italic">
|
|
82
|
+
{t('cpq.arc.diff.cancelNote', 'Cancellation — item will be terminated on activation.')}
|
|
83
|
+
</p>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
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'
|
|
9
|
+
import { ADJUSTMENT_ERROR_FALLBACKS, type WaterfallChargeInput } from '../lib/priceAdjustmentView'
|
|
10
|
+
import { formatMoney } from '../lib/formatMoney'
|
|
11
|
+
|
|
12
|
+
type TierBreakdownEntry = { tier: number; from: number; to: number; pricePerUnit: number; quantity: number; subtotal: number }
|
|
13
|
+
|
|
14
|
+
/** One priced charge as stored on a quote or order line. */
|
|
15
|
+
export type ChargeRowView = WaterfallChargeInput & {
|
|
16
|
+
chargeName: string
|
|
17
|
+
quantity?: number | null
|
|
18
|
+
note?: string | null
|
|
19
|
+
breakdown?: { tiers: TierBreakdownEntry[] } | null
|
|
20
|
+
adjustmentErrors?: Array<{ code: string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Right-aligned price column header with an explanatory hover tooltip. */
|
|
24
|
+
function PriceColumnHeader({ label, hint }: { label: string; hint: string }) {
|
|
25
|
+
return (
|
|
26
|
+
<th className="text-right py-1 font-medium">
|
|
27
|
+
<SimpleTooltip content={hint} size="sm">
|
|
28
|
+
<span className="inline-flex items-center gap-1 cursor-help" tabIndex={0} aria-label={`${label}: ${hint}`}>
|
|
29
|
+
{label}
|
|
30
|
+
<Info className="h-3 w-3" aria-hidden />
|
|
31
|
+
</span>
|
|
32
|
+
</SimpleTooltip>
|
|
33
|
+
</th>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Charge table shared by the quote and order detail pages: list / standard /
|
|
39
|
+
* offer unit prices with depth badge, and a per-charge pricing detail (tiers +
|
|
40
|
+
* price waterfall). Editing hooks are optional render props.
|
|
41
|
+
*/
|
|
42
|
+
export function ChargeBreakdownTable<C extends ChargeRowView>({
|
|
43
|
+
charges,
|
|
44
|
+
currency,
|
|
45
|
+
openDetailIndex,
|
|
46
|
+
onToggleDetail,
|
|
47
|
+
reasonLabels,
|
|
48
|
+
renderChargeActions,
|
|
49
|
+
renderManualActions,
|
|
50
|
+
}: {
|
|
51
|
+
charges: C[]
|
|
52
|
+
currency: string
|
|
53
|
+
openDetailIndex: number | null
|
|
54
|
+
onToggleDetail: (chargeIndex: number) => void
|
|
55
|
+
reasonLabels?: Record<string, string>
|
|
56
|
+
renderChargeActions?: (charge: C) => React.ReactNode
|
|
57
|
+
renderManualActions?: (adjustmentId: string) => React.ReactNode
|
|
58
|
+
}) {
|
|
59
|
+
const t = useT()
|
|
60
|
+
return (
|
|
61
|
+
<table className="w-full text-xs" data-testid="cpq-charge-breakdown">
|
|
62
|
+
<thead>
|
|
63
|
+
<tr className="text-muted-foreground">
|
|
64
|
+
<th className="text-left py-1 font-medium">{t('cpq.charges.charge', 'Charge')}</th>
|
|
65
|
+
<th className="text-left py-1 font-medium">{t('cpq.charges.type', 'Type')}</th>
|
|
66
|
+
<PriceColumnHeader
|
|
67
|
+
label={t('cpq.priceAdjustments.listUnit', 'List')}
|
|
68
|
+
hint={t('cpq.priceAdjustments.listUnitHint', 'Catalog unit price from the pricing table, before any price rules or manual adjustments.')}
|
|
69
|
+
/>
|
|
70
|
+
<PriceColumnHeader
|
|
71
|
+
label={t('cpq.priceAdjustments.standardUnit', 'Standard')}
|
|
72
|
+
hint={t('cpq.priceAdjustments.standardUnitHint', 'Unit price after automatic price rules (e.g. volume or segment discounts), before manual adjustments.')}
|
|
73
|
+
/>
|
|
74
|
+
<PriceColumnHeader
|
|
75
|
+
label={t('cpq.priceAdjustments.offerUnit', 'Offer')}
|
|
76
|
+
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.')}
|
|
77
|
+
/>
|
|
78
|
+
<th className="text-right py-1 font-medium">{t('cpq.charges.qty', 'Qty')}</th>
|
|
79
|
+
<th className="text-right py-1 font-medium">{t('cpq.charges.total', 'Total')}</th>
|
|
80
|
+
<th className="w-16" />
|
|
81
|
+
</tr>
|
|
82
|
+
</thead>
|
|
83
|
+
<tbody>
|
|
84
|
+
{charges.map((c, i) => {
|
|
85
|
+
const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length)
|
|
86
|
+
const isDetailOpen = openDetailIndex === i
|
|
87
|
+
const listUnit = c.listUnitPrice ?? c.unitPrice
|
|
88
|
+
const standardUnit = c.standardUnitPrice ?? c.unitPrice
|
|
89
|
+
const errors = c.adjustmentErrors ?? []
|
|
90
|
+
return (
|
|
91
|
+
<React.Fragment key={i}>
|
|
92
|
+
<tr className="border-t border-dashed border-border/50">
|
|
93
|
+
<td className="py-1">
|
|
94
|
+
<span>{c.chargeName}</span>
|
|
95
|
+
{errors.length > 0 && (
|
|
96
|
+
<Tag variant="warning" className="ml-1.5 px-1.5 text-xs" data-testid="cpq-adjustment-invalid">
|
|
97
|
+
{t('cpq.priceAdjustments.invalidReason', 'Invalid: {reason}', {
|
|
98
|
+
reason: t(`cpq.priceAdjustments.errors.${errors[0].code}`, ADJUSTMENT_ERROR_FALLBACKS[errors[0].code] ?? errors[0].code),
|
|
99
|
+
})}
|
|
100
|
+
</Tag>
|
|
101
|
+
)}
|
|
102
|
+
</td>
|
|
103
|
+
<td className="py-1 uppercase">{c.chargeType}</td>
|
|
104
|
+
<td className="py-1 text-right font-mono text-muted-foreground">{formatMoney(listUnit, currency)}</td>
|
|
105
|
+
<td className="py-1 text-right font-mono text-muted-foreground">{formatMoney(standardUnit, currency)}</td>
|
|
106
|
+
<td className="py-1 text-right font-mono">
|
|
107
|
+
<span className="inline-flex items-center gap-1">
|
|
108
|
+
<DepthBadge before={listUnit} after={c.unitPrice} />
|
|
109
|
+
{formatMoney(c.unitPrice, currency)}
|
|
110
|
+
</span>
|
|
111
|
+
</td>
|
|
112
|
+
<td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
|
|
113
|
+
<td className="py-1 text-right font-mono">{c.totalPrice != null ? formatMoney(c.totalPrice, currency) : c.note ?? '—'}</td>
|
|
114
|
+
<td className="py-1 text-right whitespace-nowrap">
|
|
115
|
+
{renderChargeActions?.(c)}
|
|
116
|
+
{hasDetail && (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={() => onToggleDetail(i)}
|
|
120
|
+
className="inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
|
121
|
+
title={t('cpq.priceAdjustments.showBreakdown', 'Show price breakdown')}
|
|
122
|
+
>
|
|
123
|
+
<Info className="h-3.5 w-3.5" />
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
</td>
|
|
127
|
+
</tr>
|
|
128
|
+
{isDetailOpen && (
|
|
129
|
+
<tr>
|
|
130
|
+
<td colSpan={8} className="pb-3 pt-1">
|
|
131
|
+
<ChargePricingDetail charge={c} currency={currency} reasonLabels={reasonLabels} renderManualActions={renderManualActions} />
|
|
132
|
+
</td>
|
|
133
|
+
</tr>
|
|
134
|
+
)}
|
|
135
|
+
</React.Fragment>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
138
|
+
</tbody>
|
|
139
|
+
</table>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ChargePricingDetail({ charge: c, currency, reasonLabels, renderManualActions }: {
|
|
144
|
+
charge: ChargeRowView
|
|
145
|
+
currency: string
|
|
146
|
+
reasonLabels?: Record<string, string>
|
|
147
|
+
renderManualActions?: (adjustmentId: string) => React.ReactNode
|
|
148
|
+
}) {
|
|
149
|
+
const t = useT()
|
|
150
|
+
const tiers = c.breakdown?.tiers ?? []
|
|
151
|
+
return (
|
|
152
|
+
<div className="rounded border bg-background p-3 space-y-3 text-xs">
|
|
153
|
+
<div className="flex items-center justify-between">
|
|
154
|
+
<span className="font-semibold text-sm">{t('cpq.charges.pricingDetailsFor', 'Pricing Details — {name}', { name: c.chargeName })}</span>
|
|
155
|
+
<span className="text-muted-foreground">{c.pricingMethod ?? 'flat'}</span>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{tiers.length > 0 && (
|
|
159
|
+
<div>
|
|
160
|
+
<div className="font-medium text-muted-foreground mb-1">{t('cpq.charges.tierBreakdown', 'Tier Breakdown')}</div>
|
|
161
|
+
<table className="w-full">
|
|
162
|
+
<thead>
|
|
163
|
+
<tr className="text-muted-foreground">
|
|
164
|
+
<th className="text-left py-0.5 font-medium">{t('cpq.charges.tier', 'Tier')}</th>
|
|
165
|
+
<th className="text-right py-0.5 font-medium">{t('cpq.charges.range', 'Range')}</th>
|
|
166
|
+
<th className="text-right py-0.5 font-medium">{t('cpq.charges.pricePerUnit', 'Price/Unit')}</th>
|
|
167
|
+
<th className="text-right py-0.5 font-medium">{t('cpq.charges.qty', 'Qty')}</th>
|
|
168
|
+
<th className="text-right py-0.5 font-medium">{t('cpq.charges.subtotal', 'Subtotal')}</th>
|
|
169
|
+
</tr>
|
|
170
|
+
</thead>
|
|
171
|
+
<tbody>
|
|
172
|
+
{tiers.map((tier) => (
|
|
173
|
+
<tr key={tier.tier} className="border-t border-border/30">
|
|
174
|
+
<td className="py-0.5">{tier.tier}</td>
|
|
175
|
+
<td className="py-0.5 text-right font-mono">{tier.from}–{tier.to}</td>
|
|
176
|
+
<td className="py-0.5 text-right font-mono">{formatMoney(tier.pricePerUnit, currency)}</td>
|
|
177
|
+
<td className="py-0.5 text-right font-mono">{tier.quantity}</td>
|
|
178
|
+
<td className="py-0.5 text-right font-mono">{formatMoney(tier.subtotal, currency)}</td>
|
|
179
|
+
</tr>
|
|
180
|
+
))}
|
|
181
|
+
<tr className="border-t font-medium">
|
|
182
|
+
<td colSpan={4} className="py-0.5 text-right">{t('cpq.charges.tieredTotal', 'Tiered Total')}</td>
|
|
183
|
+
<td className="py-0.5 text-right font-mono">
|
|
184
|
+
{formatMoney(tiers.reduce((sum, tier) => sum + tier.subtotal, 0), currency)}
|
|
185
|
+
</td>
|
|
186
|
+
</tr>
|
|
187
|
+
</tbody>
|
|
188
|
+
</table>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
<div>
|
|
193
|
+
<div className="font-medium text-muted-foreground mb-1">{t('cpq.priceAdjustments.waterfall.title', 'Price waterfall')}</div>
|
|
194
|
+
<PriceWaterfall charge={c} currency={currency} reasonLabels={reasonLabels} renderManualActions={renderManualActions} />
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="flex items-center justify-between pt-1 border-t text-sm font-semibold">
|
|
198
|
+
<span>{t('cpq.charges.finalTotal', 'Final Total')}</span>
|
|
199
|
+
<span className="font-mono">{c.totalPrice != null ? formatMoney(c.totalPrice, currency) : '—'}</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
6
|
+
import {
|
|
7
|
+
discountDepth,
|
|
8
|
+
formatDepth,
|
|
9
|
+
waterfallRows,
|
|
10
|
+
type WaterfallChargeInput,
|
|
11
|
+
} from '../lib/priceAdjustmentView'
|
|
12
|
+
|
|
13
|
+
function money(amount: number, currency: string): string {
|
|
14
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TYPE_FALLBACKS: Record<string, string> = {
|
|
18
|
+
discount_percent: '% discount',
|
|
19
|
+
discount_absolute: 'amount off',
|
|
20
|
+
surcharge_percent: '% surcharge',
|
|
21
|
+
surcharge_absolute: 'fixed surcharge',
|
|
22
|
+
price_override: 'price override',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Signed depth badge: "−20%" for a discount, "+5%" for a surcharge; text, never colour only. */
|
|
26
|
+
export function DepthBadge({ before, after, title }: { before: number | null | undefined; after: number | null | undefined; title?: string }) {
|
|
27
|
+
const label = formatDepth(discountDepth(before, after))
|
|
28
|
+
if (!label) return null
|
|
29
|
+
return <Tag variant="neutral" className="px-1.5 text-xs font-mono" title={title}>{label}</Tag>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type PriceWaterfallProps = {
|
|
33
|
+
charge: WaterfallChargeInput & { chargeName?: string }
|
|
34
|
+
currency: string
|
|
35
|
+
/** Reason code → label; manual rows fall back to the code. */
|
|
36
|
+
reasonLabels?: Record<string, string>
|
|
37
|
+
/** Optional per-adjustment actions (edit / delete) rendered on manual rows. */
|
|
38
|
+
renderManualActions?: (adjustmentId: string) => React.ReactNode
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read-only list → rule → standard → manual → offer breakdown for one charge (unit prices). */
|
|
42
|
+
export function PriceWaterfall({ charge, currency, reasonLabels, renderManualActions }: PriceWaterfallProps) {
|
|
43
|
+
const t = useT()
|
|
44
|
+
const rows = waterfallRows(charge)
|
|
45
|
+
const fmtValue = (ruleType: string, value: number) =>
|
|
46
|
+
ruleType.endsWith('_percent') ? `${value}%` : money(value, currency)
|
|
47
|
+
return (
|
|
48
|
+
<table className="w-full text-xs" data-testid="cpq-price-waterfall">
|
|
49
|
+
<thead>
|
|
50
|
+
<tr className="text-muted-foreground">
|
|
51
|
+
<th className="text-left py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.step', 'Step')}</th>
|
|
52
|
+
<th className="text-left py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.source', 'Source')}</th>
|
|
53
|
+
<th className="text-right py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.value', 'Value')}</th>
|
|
54
|
+
<th className="text-right py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.delta', 'Delta')}</th>
|
|
55
|
+
<th className="text-right py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.unitPrice', 'Unit price')}</th>
|
|
56
|
+
{renderManualActions && <th className="w-16" />}
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
{rows.map((row, index) => {
|
|
61
|
+
if (row.kind === 'list' || row.kind === 'standard' || row.kind === 'offer') {
|
|
62
|
+
const label = row.kind === 'list'
|
|
63
|
+
? t('cpq.priceAdjustments.waterfall.list', 'List price')
|
|
64
|
+
: row.kind === 'standard'
|
|
65
|
+
? t('cpq.priceAdjustments.waterfall.standard', 'Standard price')
|
|
66
|
+
: t('cpq.priceAdjustments.waterfall.offer', 'Offer price')
|
|
67
|
+
return (
|
|
68
|
+
<tr key={`${row.kind}-${index}`} className={`border-t border-border/30 ${row.kind === 'offer' ? 'font-semibold' : 'font-medium'}`}>
|
|
69
|
+
<td className="py-0.5" colSpan={4}>{label}</td>
|
|
70
|
+
<td className="py-0.5 text-right font-mono">{money(row.unitPrice, currency)}</td>
|
|
71
|
+
{renderManualActions && <td />}
|
|
72
|
+
</tr>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
const source = row.kind === 'rule'
|
|
76
|
+
? t('cpq.priceAdjustments.waterfall.ruleSource', 'Price rule: {name}', { name: row.label })
|
|
77
|
+
: t('cpq.priceAdjustments.waterfall.manualSource', 'Negotiated: {reason}', {
|
|
78
|
+
reason: reasonLabels?.[row.reasonCode ?? ''] ?? row.reasonCode ?? row.label,
|
|
79
|
+
})
|
|
80
|
+
return (
|
|
81
|
+
<tr key={`${row.kind}-${index}`} className="border-t border-border/30">
|
|
82
|
+
<td className="py-0.5 pl-3 text-muted-foreground">{t(`cpq.priceAdjustments.types.${row.ruleType}`, TYPE_FALLBACKS[row.ruleType] ?? row.ruleType)}</td>
|
|
83
|
+
<td className="py-0.5">{source}</td>
|
|
84
|
+
<td className="py-0.5 text-right font-mono">{fmtValue(row.ruleType, row.value)}</td>
|
|
85
|
+
<td className="py-0.5 text-right font-mono">{row.delta > 0 ? '+' : ''}{money(row.delta, currency)}</td>
|
|
86
|
+
<td className="py-0.5 text-right font-mono">{money(row.unitPriceAfter, currency)}</td>
|
|
87
|
+
{renderManualActions && (
|
|
88
|
+
<td className="py-0.5 text-right">{row.kind === 'manual' && row.adjustmentId ? renderManualActions(row.adjustmentId) : null}</td>
|
|
89
|
+
)}
|
|
90
|
+
</tr>
|
|
91
|
+
)
|
|
92
|
+
})}
|
|
93
|
+
</tbody>
|
|
94
|
+
</table>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { formatDepth } from '../lib/priceAdjustmentView'
|
|
6
|
+
import { chargeTypeDepths, formatChangePct, type ArcRecurringDelta } from '../lib/quoteSummaryView'
|
|
7
|
+
import { formatMoney } from '../lib/formatMoney'
|
|
8
|
+
|
|
9
|
+
/** Pricing summary fields shared by quote and order results. Waterfall fields are absent on legacy snapshots. */
|
|
10
|
+
export type PricingSummaryView = {
|
|
11
|
+
nrcTotal: number
|
|
12
|
+
mrcTotal: number
|
|
13
|
+
listNrcTotal?: number
|
|
14
|
+
listMrcTotal?: number
|
|
15
|
+
dealValue?: { list: number; standard: number; offer: number }
|
|
16
|
+
discountDepth?: { total: number | null } | null
|
|
17
|
+
usageTotalEstimated?: number | null
|
|
18
|
+
usageCharges?: Array<{ chargeName: string; unitPrice: number }>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function SummaryFigure({
|
|
22
|
+
label,
|
|
23
|
+
value,
|
|
24
|
+
note,
|
|
25
|
+
title,
|
|
26
|
+
size = 'lg',
|
|
27
|
+
valueClassName,
|
|
28
|
+
testId,
|
|
29
|
+
}: {
|
|
30
|
+
label: string
|
|
31
|
+
value: string
|
|
32
|
+
note?: string | null
|
|
33
|
+
title?: string
|
|
34
|
+
size?: 'lg' | 'sm'
|
|
35
|
+
valueClassName?: string
|
|
36
|
+
testId?: string
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<div data-testid={testId}>
|
|
40
|
+
<p className="text-xs text-muted-foreground" title={title}>{label}</p>
|
|
41
|
+
<p className={`${size === 'lg' ? 'text-lg' : 'text-sm'} font-semibold font-mono ${valueClassName ?? ''}`}>
|
|
42
|
+
{value}
|
|
43
|
+
{note && <span className="ml-1.5 text-xs font-normal text-muted-foreground">({note})</span>}
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* NRC / MRC / deal value with discount depth in brackets, plus the ARC
|
|
51
|
+
* current → new MRC row on Amend / Renew documents. Shared by the quote and
|
|
52
|
+
* order detail pages so both read the same.
|
|
53
|
+
*/
|
|
54
|
+
export function PricingSummaryCard({ summary, currency, arcDelta }: {
|
|
55
|
+
summary: PricingSummaryView
|
|
56
|
+
currency: string
|
|
57
|
+
arcDelta?: ArcRecurringDelta | null
|
|
58
|
+
}) {
|
|
59
|
+
const t = useT()
|
|
60
|
+
const typeDepths = chargeTypeDepths(summary)
|
|
61
|
+
const usageCharges = summary.usageCharges ?? []
|
|
62
|
+
return (
|
|
63
|
+
<div className="rounded-lg border bg-card p-4" data-testid="cpq-pricing-summary">
|
|
64
|
+
<h3 className="text-sm font-medium mb-3">{t('cpq.quotes.summary.title', 'Pricing Summary')}</h3>
|
|
65
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
66
|
+
<SummaryFigure
|
|
67
|
+
label={t('cpq.quotes.summary.nrc', 'Non-Recurring (NRC)')}
|
|
68
|
+
value={formatMoney(summary.nrcTotal, currency)}
|
|
69
|
+
note={formatDepth(typeDepths.nrc)}
|
|
70
|
+
testId="cpq-summary-nrc"
|
|
71
|
+
/>
|
|
72
|
+
<SummaryFigure
|
|
73
|
+
label={t('cpq.quotes.summary.mrc', 'Monthly Recurring (MRC)')}
|
|
74
|
+
value={formatMoney(summary.mrcTotal, currency)}
|
|
75
|
+
note={formatDepth(typeDepths.mrc)}
|
|
76
|
+
testId="cpq-summary-mrc"
|
|
77
|
+
/>
|
|
78
|
+
{summary.dealValue && (
|
|
79
|
+
<SummaryFigure
|
|
80
|
+
label={t('cpq.priceAdjustments.summary.dealValue', 'Deal value')}
|
|
81
|
+
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.')}
|
|
82
|
+
value={formatMoney(summary.dealValue.offer, currency)}
|
|
83
|
+
note={formatDepth(summary.discountDepth?.total ?? null)}
|
|
84
|
+
testId="cpq-deal-value"
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
{summary.usageTotalEstimated != null && (
|
|
88
|
+
<SummaryFigure
|
|
89
|
+
label={t('cpq.quotes.summary.usageEstimated', 'Usage (est.)')}
|
|
90
|
+
value={formatMoney(summary.usageTotalEstimated, currency)}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
{usageCharges.length > 0 && (
|
|
94
|
+
<div>
|
|
95
|
+
<p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.usageCharges', 'Usage Charges')}</p>
|
|
96
|
+
{usageCharges.map((uc, i) => (
|
|
97
|
+
<p key={i} className="text-xs mt-0.5">{t('cpq.quotes.summary.usageChargeLine', '{name}: {price}/unit', { name: uc.chargeName, price: formatMoney(uc.unitPrice, currency) })}</p>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{arcDelta && (
|
|
103
|
+
<div className="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-4 border-t pt-3" data-testid="cpq-arc-delta">
|
|
104
|
+
<SummaryFigure
|
|
105
|
+
size="sm"
|
|
106
|
+
testId="cpq-arc-current-mrc"
|
|
107
|
+
label={t('cpq.quotes.summary.arc.currentMrc', 'Current MRC')}
|
|
108
|
+
value={formatMoney(arcDelta.currentMrc, currency)}
|
|
109
|
+
/>
|
|
110
|
+
<SummaryFigure
|
|
111
|
+
size="sm"
|
|
112
|
+
testId="cpq-arc-new-mrc"
|
|
113
|
+
label={t('cpq.quotes.summary.arc.newMrc', 'New MRC')}
|
|
114
|
+
value={formatMoney(arcDelta.newMrc, currency)}
|
|
115
|
+
/>
|
|
116
|
+
<SummaryFigure
|
|
117
|
+
size="sm"
|
|
118
|
+
testId="cpq-arc-mrc-change"
|
|
119
|
+
label={t('cpq.quotes.summary.arc.mrcChange', 'MRC change')}
|
|
120
|
+
value={`${arcDelta.delta > 0 ? '+' : ''}${formatMoney(arcDelta.delta, currency)}`}
|
|
121
|
+
valueClassName={arcDelta.delta > 0 ? 'text-status-success-text' : arcDelta.delta < 0 ? 'text-destructive' : undefined}
|
|
122
|
+
note={formatChangePct(arcDelta.deltaPct)}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { ChevronRight } from 'lucide-react'
|
|
4
4
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
6
|
import type { TagMap } from '@open-mercato/ui/primitives/tag'
|
|
6
7
|
import { formatStatusLabel } from './statusMaps'
|
|
7
8
|
|
|
@@ -30,7 +31,7 @@ export function StatusPath({
|
|
|
30
31
|
onTransition,
|
|
31
32
|
allowedTransitions,
|
|
32
33
|
disabled,
|
|
33
|
-
ariaLabel
|
|
34
|
+
ariaLabel,
|
|
34
35
|
}: {
|
|
35
36
|
current: string
|
|
36
37
|
/** Happy-path statuses in order. */
|
|
@@ -39,15 +40,17 @@ export function StatusPath({
|
|
|
39
40
|
terminals?: string[]
|
|
40
41
|
/** Status → Tag variant. Missing entries fall back to "neutral". */
|
|
41
42
|
statusMap: Record<string, TagVariant>
|
|
42
|
-
/** Optional human labels (overrides `formatStatusLabel`). */
|
|
43
|
+
/** Optional human labels, already translated by the caller (overrides `formatStatusLabel`). */
|
|
43
44
|
labels?: Record<string, string>
|
|
44
45
|
/** Click handler. Omit for read-only path. */
|
|
45
46
|
onTransition?: (target: string) => void
|
|
46
47
|
/** If set, only these targets are clickable. Else any non-current. */
|
|
47
48
|
allowedTransitions?: string[]
|
|
48
49
|
disabled?: boolean
|
|
50
|
+
/** Translated accessible name; defaults to "Status path". */
|
|
49
51
|
ariaLabel?: string
|
|
50
52
|
}) {
|
|
53
|
+
const t = useT()
|
|
51
54
|
// Build the displayed sequence. Aliases can swap into the happy
|
|
52
55
|
// path slot (e.g. quote's `pre_approved` shown as `approved`). We
|
|
53
56
|
// don't handle aliasing here — consumers can swap entries in
|
|
@@ -61,7 +64,7 @@ export function StatusPath({
|
|
|
61
64
|
const interactiveMode = typeof onTransition === 'function'
|
|
62
65
|
|
|
63
66
|
return (
|
|
64
|
-
<nav aria-label={ariaLabel} className="flex items-center gap-1.5 flex-wrap text-xs">
|
|
67
|
+
<nav aria-label={ariaLabel ?? t('cpq.statusPath.ariaLabel', 'Status path')} className="flex items-center gap-1.5 flex-wrap text-xs">
|
|
65
68
|
{sequence.map((status, idx) => {
|
|
66
69
|
const reached = currentIdx >= 0 && idx <= currentIdx
|
|
67
70
|
const isCurrent = status === current
|
|
@@ -91,7 +94,7 @@ export function StatusPath({
|
|
|
91
94
|
type="button"
|
|
92
95
|
onClick={() => onTransition!(status)}
|
|
93
96
|
aria-current={isCurrent ? 'step' : undefined}
|
|
94
|
-
title={
|
|
97
|
+
title={t('cpq.statusPath.transitionTo', 'Transition to {status}', { status: label })}
|
|
95
98
|
className={wrapperClass}
|
|
96
99
|
>
|
|
97
100
|
<Tag variant={variant} dot={isCurrent} className={tagClass}>
|