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