@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
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { useRouter, useParams } from 'next/navigation'
|
|
4
|
+
import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
4
5
|
import { Alert } from '@open-mercato/ui/primitives/alert'
|
|
5
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
7
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
7
8
|
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
8
9
|
import {
|
|
9
|
-
formatStatusLabel,
|
|
10
10
|
subscriptionStatusMap,
|
|
11
11
|
type SubscriptionStatus,
|
|
12
12
|
} from '../../../../../components/statusMaps'
|
|
13
13
|
import { StatusPath } from '../../../../../components/StatusPath'
|
|
14
|
+
import { DepthBadge, PriceWaterfall } from '../../../../../components/PriceWaterfall'
|
|
15
|
+
import { parseStoredCharge } from '../../../../../lib/priceAdjustmentView'
|
|
16
|
+
import { buildInventoryStatusLabels, inventoryStatusLabel } from '../../shared'
|
|
14
17
|
|
|
15
18
|
// XD-250 — ARC integration on subscription detail.
|
|
16
19
|
// • header buttons (Amend / Renew / Cancel) call /from-subscription
|
|
@@ -62,13 +65,16 @@ type ChangeLogRow = {
|
|
|
62
65
|
mergedFromSubscriptionIds: string[] | null
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
function changeTypeLabel(t: TranslateFn, changeType: string): string {
|
|
69
|
+
switch (changeType) {
|
|
70
|
+
case 'created': return t('cpq.inventory.subscriptions.detail.changeType.created', 'Created')
|
|
71
|
+
case 'amend': return t('cpq.inventory.subscriptions.detail.changeType.amend', 'Amended')
|
|
72
|
+
case 'renew': return t('cpq.inventory.subscriptions.detail.changeType.renew', 'Renewed')
|
|
73
|
+
case 'cancel': return t('cpq.inventory.subscriptions.detail.changeType.cancel', 'Cancelled')
|
|
74
|
+
case 'merge-result': return t('cpq.inventory.subscriptions.detail.changeType.mergeResult', 'Created from merging contracts')
|
|
75
|
+
case 'merge-source': return t('cpq.inventory.subscriptions.detail.changeType.mergeSource', 'Merged into a new contract')
|
|
76
|
+
default: return changeType
|
|
77
|
+
}
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
const CHANGE_TYPE_VARIANTS: Record<string, 'info' | 'success' | 'error' | 'brand' | 'warning' | 'neutral'> = {
|
|
@@ -154,27 +160,23 @@ const ALLOWED_TRANSITIONS: Record<string, string[]> = {
|
|
|
154
160
|
expired: [],
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
const
|
|
158
|
-
pending: 'Pending',
|
|
159
|
-
active: 'Active',
|
|
160
|
-
suspended: 'Suspended',
|
|
161
|
-
terminated: 'Terminated',
|
|
162
|
-
expired: 'Expired',
|
|
163
|
-
}
|
|
163
|
+
const SUBSCRIPTION_STATUSES = ['pending', 'active', 'suspended', 'terminated', 'expired'] as const
|
|
164
164
|
|
|
165
165
|
function fmt(amount: number | string, currency: string): string {
|
|
166
166
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(Number(amount))
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
function StatusBadge({ status }: { status: string }) {
|
|
170
|
+
const t = useT()
|
|
170
171
|
return (
|
|
171
172
|
<Tag variant={subscriptionStatusMap[status as SubscriptionStatus] ?? 'neutral'} dot>
|
|
172
|
-
{
|
|
173
|
+
{inventoryStatusLabel(t, status)}
|
|
173
174
|
</Tag>
|
|
174
175
|
)
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
export default function SubscriptionDetailPage(props: { params?: { id?: string } }) {
|
|
179
|
+
const t = useT()
|
|
178
180
|
const router = useRouter()
|
|
179
181
|
const urlParams = useParams()
|
|
180
182
|
const subId = (props.params?.id ?? urlParams?.id) as string
|
|
@@ -218,15 +220,16 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
218
220
|
// Offer to navigate to that quote instead of showing a dead alert.
|
|
219
221
|
if (body.existingQuoteId) {
|
|
220
222
|
const ok = window.confirm(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
t('cpq.inventory.subscriptions.detail.arc.existingQuoteConfirm', '{error}\n\nOpen the existing quote now?', {
|
|
224
|
+
error: body.error ?? t('cpq.inventory.subscriptions.detail.arc.existingQuoteInProgress', 'Subscription has another ARC quote in progress.'),
|
|
225
|
+
}),
|
|
223
226
|
)
|
|
224
227
|
if (ok) {
|
|
225
228
|
router.push(`/backend/cpq/quotes/${body.existingQuoteId}`)
|
|
226
229
|
}
|
|
227
230
|
return
|
|
228
231
|
}
|
|
229
|
-
alert(
|
|
232
|
+
alert(t('cpq.inventory.subscriptions.detail.arc.startFailed', 'Failed to start {type} quote: {error}', { type, error: body.error ?? res.statusText }))
|
|
230
233
|
return
|
|
231
234
|
}
|
|
232
235
|
const data = await res.json()
|
|
@@ -235,7 +238,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
235
238
|
setArcInFlight(null)
|
|
236
239
|
}
|
|
237
240
|
},
|
|
238
|
-
[router, sub],
|
|
241
|
+
[router, sub, t],
|
|
239
242
|
)
|
|
240
243
|
|
|
241
244
|
const transitionStatus = async (targetStatus: string) => {
|
|
@@ -249,7 +252,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
249
252
|
body: JSON.stringify({ id: sub.id, targetStatus }),
|
|
250
253
|
})
|
|
251
254
|
const data = await res.json()
|
|
252
|
-
if (!res.ok) throw new Error(data?.error ?? 'Status transition failed')
|
|
255
|
+
if (!res.ok) throw new Error(data?.error ?? t('cpq.inventory.subscriptions.detail.error.transitionFailed', 'Status transition failed'))
|
|
253
256
|
// The status endpoint cascades to items and linked assets server-side,
|
|
254
257
|
// but only returns the subscription scalar fields. Re-fetch the full
|
|
255
258
|
// detail (which includes the item tree) and the asset list so the UI
|
|
@@ -322,7 +325,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
322
325
|
async function load() {
|
|
323
326
|
try {
|
|
324
327
|
const res = await fetch(`/api/cpq/inventory/subscriptions?id=${subId}`)
|
|
325
|
-
if (!res.ok) throw new Error('Subscription not found')
|
|
328
|
+
if (!res.ok) throw new Error(t('cpq.inventory.subscriptions.detail.error.notFound', 'Subscription not found'))
|
|
326
329
|
const data: SubscriptionDetail = await res.json()
|
|
327
330
|
if (!cancelled) {
|
|
328
331
|
setSub(data)
|
|
@@ -409,7 +412,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
409
412
|
}
|
|
410
413
|
load()
|
|
411
414
|
return () => { cancelled = true }
|
|
412
|
-
}, [subId])
|
|
415
|
+
}, [subId, t])
|
|
413
416
|
|
|
414
417
|
const toggleItem = (id: string) => {
|
|
415
418
|
setExpandedItems((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n })
|
|
@@ -418,7 +421,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
418
421
|
if (loading) {
|
|
419
422
|
return (
|
|
420
423
|
<div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
|
|
421
|
-
<Spinner /> Loading…
|
|
424
|
+
<Spinner /> {t('cpq.inventory.subscriptions.detail.loading', 'Loading…')}
|
|
422
425
|
</div>
|
|
423
426
|
)
|
|
424
427
|
}
|
|
@@ -426,9 +429,9 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
426
429
|
if (!sub) {
|
|
427
430
|
return (
|
|
428
431
|
<div className="space-y-4">
|
|
429
|
-
<button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← Back</button>
|
|
432
|
+
<button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← {t('cpq.common.back', 'Back')}</button>
|
|
430
433
|
{error && <ErrorBanner message={error} />}
|
|
431
|
-
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Subscription not found
|
|
434
|
+
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.notFound', 'Subscription not found.')}</div>
|
|
432
435
|
</div>
|
|
433
436
|
)
|
|
434
437
|
}
|
|
@@ -438,6 +441,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
438
441
|
|
|
439
442
|
const canArc = sub.status === 'active' || sub.status === 'suspended'
|
|
440
443
|
const statusTransitions = ALLOWED_TRANSITIONS[sub.status] ?? []
|
|
444
|
+
const statusLabels = buildInventoryStatusLabels(t, SUBSCRIPTION_STATUSES)
|
|
441
445
|
|
|
442
446
|
return (
|
|
443
447
|
<div className="space-y-6">
|
|
@@ -450,13 +454,13 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
450
454
|
<FormHeader
|
|
451
455
|
mode="detail"
|
|
452
456
|
backHref="/backend/cpq/inventory"
|
|
453
|
-
backLabel=
|
|
454
|
-
entityTypeLabel=
|
|
457
|
+
backLabel={t('cpq.inventory.detail.backToInventory', 'Back to Customer Inventory')}
|
|
458
|
+
entityTypeLabel={t('cpq.inventory.subscriptions.detail.entityType', 'Subscription')}
|
|
455
459
|
title={sub.name}
|
|
456
460
|
statusBadge={
|
|
457
461
|
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
458
462
|
<Tag variant={subscriptionStatusMap[sub.status as SubscriptionStatus] ?? 'neutral'} dot>
|
|
459
|
-
{
|
|
463
|
+
{inventoryStatusLabel(t, sub.status)}
|
|
460
464
|
</Tag>
|
|
461
465
|
<Tag variant="neutral" className="capitalize">{sub.billingCycle}</Tag>
|
|
462
466
|
{transitioning && <Spinner />}
|
|
@@ -471,7 +475,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
471
475
|
disabled={arcInFlight !== null}
|
|
472
476
|
onClick={() => startArcQuote('amend')}
|
|
473
477
|
>
|
|
474
|
-
Amend
|
|
478
|
+
{t('cpq.inventory.subscriptions.detail.arc.amend', 'Amend')}
|
|
475
479
|
</Button>
|
|
476
480
|
<Button
|
|
477
481
|
type="button"
|
|
@@ -479,7 +483,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
479
483
|
disabled={arcInFlight !== null}
|
|
480
484
|
onClick={() => startArcQuote('renew')}
|
|
481
485
|
>
|
|
482
|
-
Renew
|
|
486
|
+
{t('cpq.inventory.subscriptions.detail.arc.renew', 'Renew')}
|
|
483
487
|
</Button>
|
|
484
488
|
<Button
|
|
485
489
|
type="button"
|
|
@@ -487,7 +491,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
487
491
|
disabled={arcInFlight !== null}
|
|
488
492
|
onClick={() => startArcQuote('cancel')}
|
|
489
493
|
>
|
|
490
|
-
Cancel
|
|
494
|
+
{t('cpq.inventory.subscriptions.detail.arc.cancel', 'Cancel')}
|
|
491
495
|
</Button>
|
|
492
496
|
</>
|
|
493
497
|
) : null
|
|
@@ -502,11 +506,11 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
502
506
|
path={['pending', 'active']}
|
|
503
507
|
terminals={['suspended', 'terminated', 'expired']}
|
|
504
508
|
statusMap={subscriptionStatusMap}
|
|
505
|
-
labels={
|
|
509
|
+
labels={statusLabels}
|
|
506
510
|
onTransition={transitionStatus}
|
|
507
511
|
allowedTransitions={statusTransitions}
|
|
508
512
|
disabled={transitioning}
|
|
509
|
-
ariaLabel=
|
|
513
|
+
ariaLabel={t('cpq.inventory.subscriptions.detail.statusPath', 'Subscription status path')}
|
|
510
514
|
/>
|
|
511
515
|
</div>
|
|
512
516
|
|
|
@@ -515,46 +519,46 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
515
519
|
{/* Detail Cards */}
|
|
516
520
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
517
521
|
<div className="rounded-lg border bg-card p-4 space-y-3">
|
|
518
|
-
<h3 className="text-sm font-medium">Subscription Details</h3>
|
|
522
|
+
<h3 className="text-sm font-medium">{t('cpq.inventory.subscriptions.detail.sections.details', 'Subscription Details')}</h3>
|
|
519
523
|
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
|
520
|
-
<Field label=
|
|
521
|
-
<Field label=
|
|
522
|
-
<Field label=
|
|
523
|
-
<Field label=
|
|
524
|
-
<Field label=
|
|
525
|
-
{sub.termMonths != null && <Field label=
|
|
526
|
-
{sub.startDate && <Field label=
|
|
527
|
-
{sub.currentTermEnd && <Field label=
|
|
528
|
-
{sub.activatedAt && <Field label=
|
|
529
|
-
{sub.suspendedAt && <Field label=
|
|
530
|
-
{sub.terminatedAt && <Field label=
|
|
524
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.code', 'Code')} value={sub.code} mono />
|
|
525
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.status', 'Status')} value={sub.status} />
|
|
526
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.billingCycle', 'Billing Cycle')} value={sub.billingCycle} />
|
|
527
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.currency', 'Currency')} value={currency} />
|
|
528
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.autoRenew', 'Auto-Renew')} value={sub.autoRenew ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')} />
|
|
529
|
+
{sub.termMonths != null && <Field label={t('cpq.inventory.subscriptions.detail.fields.term', 'Term')} value={t('cpq.inventory.subscriptions.detail.fields.termMonths', '{count} months', { count: sub.termMonths })} />}
|
|
530
|
+
{sub.startDate && <Field label={t('cpq.inventory.subscriptions.detail.fields.startDate', 'Start Date')} value={new Date(sub.startDate).toLocaleDateString()} />}
|
|
531
|
+
{sub.currentTermEnd && <Field label={t('cpq.inventory.subscriptions.detail.fields.termEnd', 'Term End')} value={new Date(sub.currentTermEnd).toLocaleDateString()} />}
|
|
532
|
+
{sub.activatedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.activated', 'Activated')} value={new Date(sub.activatedAt).toLocaleString()} />}
|
|
533
|
+
{sub.suspendedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.suspended', 'Suspended')} value={new Date(sub.suspendedAt).toLocaleString()} />}
|
|
534
|
+
{sub.terminatedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.terminated', 'Terminated')} value={new Date(sub.terminatedAt).toLocaleString()} />}
|
|
531
535
|
</div>
|
|
532
536
|
{sub.description && (
|
|
533
537
|
<div className="pt-1">
|
|
534
|
-
<p className="text-xs text-muted-foreground">Description</p>
|
|
538
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.description', 'Description')}</p>
|
|
535
539
|
<p className="text-sm">{sub.description}</p>
|
|
536
540
|
</div>
|
|
537
541
|
)}
|
|
538
542
|
</div>
|
|
539
543
|
|
|
540
544
|
<div className="rounded-lg border bg-card p-4 space-y-3">
|
|
541
|
-
<h3 className="text-sm font-medium">Pricing & References</h3>
|
|
545
|
+
<h3 className="text-sm font-medium">{t('cpq.inventory.subscriptions.detail.sections.pricing', 'Pricing & References')}</h3>
|
|
542
546
|
<div className="grid grid-cols-2 gap-4">
|
|
543
547
|
<div>
|
|
544
|
-
<p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
|
|
548
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.mrc', 'Monthly Recurring (MRC)')}</p>
|
|
545
549
|
<p className="text-lg font-semibold font-mono">{fmt(sub.mrcAmount, currency)}</p>
|
|
546
550
|
</div>
|
|
547
551
|
<div>
|
|
548
|
-
<p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
|
|
552
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.nrc', 'Non-Recurring (NRC)')}</p>
|
|
549
553
|
<p className="text-lg font-semibold font-mono">{fmt(sub.nrcAmount, currency)}</p>
|
|
550
554
|
</div>
|
|
551
555
|
</div>
|
|
552
556
|
<div className="grid grid-cols-2 gap-x-4 gap-y-2 pt-1">
|
|
553
|
-
<Field label=
|
|
554
|
-
<Field label=
|
|
557
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.customer', 'Customer')} value={customerName ?? sub.customerId.slice(0, 12) + '…'} />
|
|
558
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.created', 'Created')} value={new Date(sub.createdAt).toLocaleString()} />
|
|
555
559
|
{sub.sourceQuoteId && (
|
|
556
560
|
<div>
|
|
557
|
-
<p className="text-xs text-muted-foreground">Source Quote</p>
|
|
561
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.sourceQuote', 'Source Quote')}</p>
|
|
558
562
|
<button
|
|
559
563
|
onClick={() => router.push(`/backend/cpq/quotes/${sub.sourceQuoteId}`)}
|
|
560
564
|
className="text-sm text-primary hover:underline"
|
|
@@ -565,7 +569,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
565
569
|
)}
|
|
566
570
|
{sub.sourceOrderId && (
|
|
567
571
|
<div>
|
|
568
|
-
<p className="text-xs text-muted-foreground">Source Order</p>
|
|
572
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.sourceOrder', 'Source Order')}</p>
|
|
569
573
|
<button
|
|
570
574
|
onClick={() => router.push(`/backend/cpq/orders/${sub.sourceOrderId}`)}
|
|
571
575
|
className="text-sm text-primary hover:underline"
|
|
@@ -581,7 +585,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
581
585
|
{/* Metadata */}
|
|
582
586
|
{sub.metadata && Object.keys(sub.metadata).length > 0 && (
|
|
583
587
|
<div className="rounded-lg border bg-card p-4">
|
|
584
|
-
<h3 className="text-sm font-medium mb-2">Metadata</h3>
|
|
588
|
+
<h3 className="text-sm font-medium mb-2">{t('cpq.inventory.subscriptions.detail.sections.metadata', 'Metadata')}</h3>
|
|
585
589
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
|
|
586
590
|
{Object.entries(sub.metadata).map(([key, value]) => (
|
|
587
591
|
<div key={key} className="text-xs">
|
|
@@ -595,9 +599,9 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
595
599
|
|
|
596
600
|
{/* Subscription Items */}
|
|
597
601
|
<div>
|
|
598
|
-
<h3 className="text-sm font-medium mb-2 px-1">Subscription Items ({flatItems.length})</h3>
|
|
602
|
+
<h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.items.title', 'Subscription Items ({count})', { count: flatItems.length })}</h3>
|
|
599
603
|
{flatItems.length === 0 ? (
|
|
600
|
-
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No items
|
|
604
|
+
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.empty', 'No items.')}</div>
|
|
601
605
|
) : (
|
|
602
606
|
<div className="rounded-lg border bg-card divide-y">
|
|
603
607
|
{flatItems.map((item) => {
|
|
@@ -623,8 +627,8 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
623
627
|
</div>
|
|
624
628
|
</div>
|
|
625
629
|
<div className="shrink-0 text-right text-xs space-y-0.5">
|
|
626
|
-
{Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
|
|
627
|
-
{Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
|
|
630
|
+
{Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.mrc', 'MRC')}{' '}</span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
|
|
631
|
+
{Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.nrc', 'NRC')}{' '}</span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
|
|
628
632
|
</div>
|
|
629
633
|
</div>
|
|
630
634
|
|
|
@@ -633,7 +637,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
633
637
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2">
|
|
634
638
|
{item.offeringId && (
|
|
635
639
|
<RefField
|
|
636
|
-
label=
|
|
640
|
+
label={t('cpq.inventory.subscriptions.detail.fields.offering', 'Offering')}
|
|
637
641
|
name={offeringNames[item.offeringId]}
|
|
638
642
|
fallback={item.offeringId}
|
|
639
643
|
onClick={() => router.push(`/backend/cpq/offerings/${item.offeringId}`)}
|
|
@@ -641,7 +645,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
641
645
|
)}
|
|
642
646
|
{item.specId && (
|
|
643
647
|
<RefField
|
|
644
|
-
label=
|
|
648
|
+
label={t('cpq.inventory.subscriptions.detail.fields.specification', 'Specification')}
|
|
645
649
|
name={specNames[item.specId]}
|
|
646
650
|
fallback={item.specId}
|
|
647
651
|
onClick={() => router.push(`/backend/cpq/specifications/${item.specId}`)}
|
|
@@ -649,20 +653,20 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
649
653
|
)}
|
|
650
654
|
{item.productId && (
|
|
651
655
|
<RefField
|
|
652
|
-
label=
|
|
656
|
+
label={t('cpq.inventory.subscriptions.detail.fields.product', 'Product')}
|
|
653
657
|
name={productNames[item.productId]}
|
|
654
658
|
fallback={item.productId}
|
|
655
659
|
onClick={() => router.push(`/backend/catalog/products/${item.productId}`)}
|
|
656
660
|
/>
|
|
657
661
|
)}
|
|
658
|
-
<Field label=
|
|
659
|
-
<Field label=
|
|
660
|
-
{item.capacityTotal && <Field label=
|
|
662
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.currency', 'Currency')} value={item.currencyCode || currency} />
|
|
663
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.quantity', 'Quantity')} value={String(item.quantity)} />
|
|
664
|
+
{item.capacityTotal && <Field label={t('cpq.inventory.subscriptions.detail.fields.capacity', 'Capacity')} value={`${item.capacityUsed ?? '0'} / ${item.capacityTotal} ${item.capacityUnit ?? ''}`} />}
|
|
661
665
|
</div>
|
|
662
666
|
|
|
663
667
|
{item.configuration && Object.keys(item.configuration).length > 0 && (
|
|
664
668
|
<div>
|
|
665
|
-
<p className="text-xs font-medium text-muted-foreground mb-1">Configuration</p>
|
|
669
|
+
<p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.configuration', 'Configuration')}</p>
|
|
666
670
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
|
|
667
671
|
{Object.entries(item.configuration).map(([key, value]) => (
|
|
668
672
|
<div key={key} className="text-xs">
|
|
@@ -676,27 +680,47 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
676
680
|
|
|
677
681
|
{item.charges && item.charges.length > 0 && (
|
|
678
682
|
<div>
|
|
679
|
-
<p className="text-xs font-medium text-muted-foreground mb-1">Charges</p>
|
|
683
|
+
<p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.charges', 'Charges')}</p>
|
|
680
684
|
<table className="w-full text-xs">
|
|
681
685
|
<thead>
|
|
682
686
|
<tr className="text-muted-foreground">
|
|
683
|
-
<th className="text-left py-1 font-medium">Charge</th>
|
|
684
|
-
<th className="text-left py-1 font-medium">Type</th>
|
|
685
|
-
<th className="text-right py-1 font-medium">
|
|
686
|
-
<th className="text-right py-1 font-medium">
|
|
687
|
-
<th className="text-right py-1 font-medium">
|
|
687
|
+
<th className="text-left py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.charge', 'Charge')}</th>
|
|
688
|
+
<th className="text-left py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.type', 'Type')}</th>
|
|
689
|
+
<th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.list', 'List')}</th>
|
|
690
|
+
<th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.offer', 'Offer')}</th>
|
|
691
|
+
<th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.qty', 'Qty')}</th>
|
|
692
|
+
<th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.total', 'Total')}</th>
|
|
688
693
|
</tr>
|
|
689
694
|
</thead>
|
|
690
695
|
<tbody>
|
|
691
|
-
{item.charges.map((
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
<
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
696
|
+
{item.charges.map((raw, i) => {
|
|
697
|
+
const c = parseStoredCharge(raw)
|
|
698
|
+
if (!c) return null
|
|
699
|
+
return (
|
|
700
|
+
<React.Fragment key={i}>
|
|
701
|
+
<tr className="border-t border-dashed border-border/50">
|
|
702
|
+
<td className="py-1">{c.chargeName}</td>
|
|
703
|
+
<td className="py-1 uppercase">{c.chargeType}</td>
|
|
704
|
+
<td className="py-1 text-right font-mono text-muted-foreground">{fmt(c.listUnitPrice ?? c.unitPrice, currency)}</td>
|
|
705
|
+
<td className="py-1 text-right font-mono">
|
|
706
|
+
<span className="inline-flex items-center gap-1">
|
|
707
|
+
<DepthBadge before={c.listUnitPrice ?? c.unitPrice} after={c.unitPrice} />
|
|
708
|
+
{fmt(c.unitPrice, currency)}
|
|
709
|
+
</span>
|
|
710
|
+
</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) : '—'}</td>
|
|
713
|
+
</tr>
|
|
714
|
+
{(c.adjustments?.length ?? 0) > 0 && (
|
|
715
|
+
<tr>
|
|
716
|
+
<td colSpan={6} className="pb-2 pl-4">
|
|
717
|
+
<PriceWaterfall charge={c} currency={currency} />
|
|
718
|
+
</td>
|
|
719
|
+
</tr>
|
|
720
|
+
)}
|
|
721
|
+
</React.Fragment>
|
|
722
|
+
)
|
|
723
|
+
})}
|
|
700
724
|
</tbody>
|
|
701
725
|
</table>
|
|
702
726
|
</div>
|
|
@@ -704,7 +728,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
704
728
|
|
|
705
729
|
{item.metadata && Object.keys(item.metadata).length > 0 && (
|
|
706
730
|
<div>
|
|
707
|
-
<p className="text-xs font-medium text-muted-foreground mb-1">Metadata</p>
|
|
731
|
+
<p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.metadata', 'Metadata')}</p>
|
|
708
732
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
|
|
709
733
|
{Object.entries(item.metadata).map(([key, value]) => (
|
|
710
734
|
<div key={key} className="text-xs">
|
|
@@ -717,8 +741,8 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
717
741
|
)}
|
|
718
742
|
|
|
719
743
|
<div className="flex gap-4 text-xs text-muted-foreground">
|
|
720
|
-
<span>Created: {new Date(item.createdAt).toLocaleString()}</span>
|
|
721
|
-
<span>Updated: {new Date(item.updatedAt).toLocaleString()}</span>
|
|
744
|
+
<span>{t('cpq.inventory.subscriptions.detail.items.createdAt', 'Created: {date}', { date: new Date(item.createdAt).toLocaleString() })}</span>
|
|
745
|
+
<span>{t('cpq.inventory.subscriptions.detail.items.updatedAt', 'Updated: {date}', { date: new Date(item.updatedAt).toLocaleString() })}</span>
|
|
722
746
|
</div>
|
|
723
747
|
</div>
|
|
724
748
|
)}
|
|
@@ -731,18 +755,18 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
731
755
|
|
|
732
756
|
{/* XD-250 Change History */}
|
|
733
757
|
<div>
|
|
734
|
-
<h3 className="text-sm font-medium mb-2 px-1">Change History ({changeLog.length})</h3>
|
|
758
|
+
<h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.changeLog.title', 'Change History ({count})', { count: changeLog.length })}</h3>
|
|
735
759
|
{changeLogLoading ? (
|
|
736
|
-
<div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">Loading
|
|
760
|
+
<div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.loading', 'Loading…')}</div>
|
|
737
761
|
) : changeLog.length === 0 ? (
|
|
738
|
-
<div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">No ARC changes yet
|
|
762
|
+
<div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.changeLog.empty', 'No ARC changes yet.')}</div>
|
|
739
763
|
) : (
|
|
740
764
|
<div className="rounded-lg border bg-card divide-y">
|
|
741
765
|
{changeLog.map((row) => (
|
|
742
766
|
<div key={row.id} className="px-4 py-3 space-y-1">
|
|
743
767
|
<div className="flex items-center gap-2">
|
|
744
768
|
<Tag variant={CHANGE_TYPE_VARIANTS[row.changeType] ?? 'neutral'}>
|
|
745
|
-
{
|
|
769
|
+
{changeTypeLabel(t, row.changeType)}
|
|
746
770
|
</Tag>
|
|
747
771
|
<span className="text-xs text-muted-foreground">
|
|
748
772
|
{new Date(row.effectiveAt).toLocaleString()}
|
|
@@ -752,7 +776,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
752
776
|
onClick={() => router.push(`/backend/cpq/quotes/${row.sourceQuoteId}`)}
|
|
753
777
|
className="text-xs text-primary hover:underline"
|
|
754
778
|
>
|
|
755
|
-
Quote {logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…`}
|
|
779
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.quote', 'Quote {number}', { number: logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…` })}
|
|
756
780
|
</button>
|
|
757
781
|
)}
|
|
758
782
|
{row.sourceOrderId && (
|
|
@@ -760,42 +784,47 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
760
784
|
onClick={() => router.push(`/backend/cpq/orders/${row.sourceOrderId}`)}
|
|
761
785
|
className="text-xs text-primary hover:underline"
|
|
762
786
|
>
|
|
763
|
-
Order {logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…`}
|
|
787
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.order', 'Order {number}', { number: logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…` })}
|
|
764
788
|
</button>
|
|
765
789
|
)}
|
|
766
790
|
</div>
|
|
767
791
|
<ChangeLogLineDetails row={row} currency={currency} />
|
|
768
792
|
{row.termChange && (
|
|
769
793
|
<div className="text-xs text-muted-foreground">
|
|
770
|
-
|
|
794
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.term', 'Term: {from} → {to}', {
|
|
795
|
+
from: String(row.termChange.oldTermEnd ?? '—'),
|
|
796
|
+
to: String(row.termChange.newTermEnd ?? '—'),
|
|
797
|
+
})}
|
|
771
798
|
</div>
|
|
772
799
|
)}
|
|
773
800
|
{row.reasonCode && (
|
|
774
801
|
<div className="text-xs text-muted-foreground">
|
|
775
|
-
|
|
802
|
+
{row.reasonText
|
|
803
|
+
? t('cpq.inventory.subscriptions.detail.changeLog.reasonWithText', 'Reason: {code} — {text}', { code: row.reasonCode, text: row.reasonText })
|
|
804
|
+
: t('cpq.inventory.subscriptions.detail.changeLog.reason', 'Reason: {code}', { code: row.reasonCode })}
|
|
776
805
|
</div>
|
|
777
806
|
)}
|
|
778
807
|
{row.etfAmount && (
|
|
779
808
|
<div className="text-xs text-muted-foreground">
|
|
780
|
-
ETF: {row.etfAmount
|
|
809
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.etf', 'ETF: {amount} {currency}', { amount: row.etfAmount, currency: row.etfCurrency ?? '' })}
|
|
781
810
|
</div>
|
|
782
811
|
)}
|
|
783
812
|
{row.mergedIntoSubscriptionId && (
|
|
784
813
|
<div className="text-xs text-muted-foreground">
|
|
785
|
-
Merged into{' '}
|
|
814
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.mergedInto', 'Merged into')}{' '}
|
|
786
815
|
<button
|
|
787
816
|
onClick={() =>
|
|
788
817
|
router.push(`/backend/cpq/inventory/subscriptions/${row.mergedIntoSubscriptionId}`)
|
|
789
818
|
}
|
|
790
819
|
className="text-primary hover:underline"
|
|
791
820
|
>
|
|
792
|
-
subscription {row.mergedIntoSubscriptionId.slice(0, 8)}
|
|
821
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.mergedIntoSubscription', 'subscription {id}…', { id: row.mergedIntoSubscriptionId.slice(0, 8) })}
|
|
793
822
|
</button>
|
|
794
823
|
</div>
|
|
795
824
|
)}
|
|
796
825
|
{row.mergedFromSubscriptionIds && row.mergedFromSubscriptionIds.length > 0 && (
|
|
797
826
|
<div className="text-xs text-muted-foreground">
|
|
798
|
-
Merged from {row.mergedFromSubscriptionIds.length}
|
|
827
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.mergedFrom', 'Merged from {count} subscriptions', { count: row.mergedFromSubscriptionIds.length })}
|
|
799
828
|
</div>
|
|
800
829
|
)}
|
|
801
830
|
</div>
|
|
@@ -806,20 +835,20 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
806
835
|
|
|
807
836
|
{/* Linked Assets */}
|
|
808
837
|
<div>
|
|
809
|
-
<h3 className="text-sm font-medium mb-2 px-1">Linked Assets ({assets.length})</h3>
|
|
838
|
+
<h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.assets.title', 'Linked Assets ({count})', { count: assets.length })}</h3>
|
|
810
839
|
{assets.length === 0 ? (
|
|
811
|
-
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No assets linked to this subscription
|
|
840
|
+
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.assets.empty', 'No assets linked to this subscription.')}</div>
|
|
812
841
|
) : (
|
|
813
842
|
<div className="rounded-lg border">
|
|
814
843
|
<table className="w-full text-sm">
|
|
815
844
|
<thead>
|
|
816
845
|
<tr className="border-b bg-muted/50">
|
|
817
|
-
<th className="px-4 py-3 text-left font-medium">Code</th>
|
|
818
|
-
<th className="px-4 py-3 text-left font-medium">Name</th>
|
|
819
|
-
<th className="px-4 py-3 text-left font-medium">Type</th>
|
|
820
|
-
<th className="px-4 py-3 text-left font-medium">Status</th>
|
|
821
|
-
<th className="px-4 py-3 text-right font-medium">Price</th>
|
|
822
|
-
<th className="px-4 py-3 text-right font-medium">Qty</th>
|
|
846
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.code', 'Code')}</th>
|
|
847
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.name', 'Name')}</th>
|
|
848
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.type', 'Type')}</th>
|
|
849
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.status', 'Status')}</th>
|
|
850
|
+
<th className="px-4 py-3 text-right font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.price', 'Price')}</th>
|
|
851
|
+
<th className="px-4 py-3 text-right font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.qty', 'Qty')}</th>
|
|
823
852
|
</tr>
|
|
824
853
|
</thead>
|
|
825
854
|
<tbody>
|
|
@@ -910,6 +939,8 @@ function ErrorBanner({ message }: { message: string }) {
|
|
|
910
939
|
// names and before/after MRC values. Falls back gracefully when snapshots are
|
|
911
940
|
// missing (older log rows) or when lineChanges is null (cancel / merge).
|
|
912
941
|
function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency: string }) {
|
|
942
|
+
const t = useT()
|
|
943
|
+
const itemFallback = t('cpq.inventory.subscriptions.detail.changeLog.item', 'Item')
|
|
913
944
|
const beforeItems = new Map<string, SnapshotItem>(
|
|
914
945
|
(row.beforeSnapshot?.items ?? []).map((it) => [it.id, it]),
|
|
915
946
|
)
|
|
@@ -943,7 +974,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
943
974
|
derived.push({
|
|
944
975
|
key: `${itemId}-${i}`,
|
|
945
976
|
action: lc.action,
|
|
946
|
-
name: item?.name ?? (lc.action === 'add' ? 'New item' :
|
|
977
|
+
name: item?.name ?? (lc.action === 'add' ? t('cpq.inventory.subscriptions.detail.changeLog.newItem', 'New item') : itemFallback),
|
|
947
978
|
beforeMrc,
|
|
948
979
|
afterMrc:
|
|
949
980
|
lc.action === 'add' && afterMrc == null && lc.mrcAmount != null
|
|
@@ -959,7 +990,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
959
990
|
derived.push({
|
|
960
991
|
key: it.id,
|
|
961
992
|
action: 'cancel',
|
|
962
|
-
name: it.name ??
|
|
993
|
+
name: it.name ?? itemFallback,
|
|
963
994
|
beforeMrc: mrc,
|
|
964
995
|
afterMrc: 0,
|
|
965
996
|
delta: -mrc,
|
|
@@ -971,7 +1002,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
971
1002
|
derived.push({
|
|
972
1003
|
key: it.id,
|
|
973
1004
|
action: 'add',
|
|
974
|
-
name: it.name ??
|
|
1005
|
+
name: it.name ?? itemFallback,
|
|
975
1006
|
beforeMrc: null,
|
|
976
1007
|
afterMrc: mrc,
|
|
977
1008
|
delta: null,
|
|
@@ -983,7 +1014,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
983
1014
|
derived.push({
|
|
984
1015
|
key: it.id,
|
|
985
1016
|
action: 'cancel',
|
|
986
|
-
name: it.name ??
|
|
1017
|
+
name: it.name ?? itemFallback,
|
|
987
1018
|
beforeMrc: mrc,
|
|
988
1019
|
afterMrc: 0,
|
|
989
1020
|
delta: -mrc,
|
|
@@ -999,7 +1030,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
999
1030
|
if (row.changeType === 'amend' || row.changeType === 'renew') {
|
|
1000
1031
|
return (
|
|
1001
1032
|
<div className="mt-1.5 text-xs text-muted-foreground italic">
|
|
1002
|
-
No item-level changes recorded.
|
|
1033
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.noItemChanges', 'No item-level changes recorded.')}
|
|
1003
1034
|
</div>
|
|
1004
1035
|
)
|
|
1005
1036
|
}
|
|
@@ -1015,10 +1046,18 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
1015
1046
|
)
|
|
1016
1047
|
}
|
|
1017
1048
|
|
|
1018
|
-
const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string
|
|
1019
|
-
add: { sign: '+', color: 'text-status-success-text'
|
|
1020
|
-
modify: { sign: '~', color: 'text-status-info-text'
|
|
1021
|
-
cancel: { sign: '−', color: 'text-destructive'
|
|
1049
|
+
const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string }> = {
|
|
1050
|
+
add: { sign: '+', color: 'text-status-success-text' },
|
|
1051
|
+
modify: { sign: '~', color: 'text-status-info-text' },
|
|
1052
|
+
cancel: { sign: '−', color: 'text-destructive' },
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function actionGlyphLabel(t: TranslateFn, action: 'add' | 'modify' | 'cancel'): string {
|
|
1056
|
+
switch (action) {
|
|
1057
|
+
case 'add': return t('cpq.inventory.subscriptions.detail.changeLog.action.add', 'added')
|
|
1058
|
+
case 'modify': return t('cpq.inventory.subscriptions.detail.changeLog.action.modify', 'modified')
|
|
1059
|
+
case 'cancel': return t('cpq.inventory.subscriptions.detail.changeLog.action.cancel', 'removed')
|
|
1060
|
+
}
|
|
1022
1061
|
}
|
|
1023
1062
|
|
|
1024
1063
|
function ChangeLogLineRow({
|
|
@@ -1034,6 +1073,7 @@ function ChangeLogLineRow({
|
|
|
1034
1073
|
}
|
|
1035
1074
|
currency: string
|
|
1036
1075
|
}) {
|
|
1076
|
+
const t = useT()
|
|
1037
1077
|
const glyph = ACTION_GLYPH[change.action]
|
|
1038
1078
|
const showPriceChange =
|
|
1039
1079
|
change.action === 'modify' &&
|
|
@@ -1050,35 +1090,39 @@ function ChangeLogLineRow({
|
|
|
1050
1090
|
|
|
1051
1091
|
return (
|
|
1052
1092
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
|
|
1053
|
-
<span className={`font-bold w-3 text-center ${glyph.color}`} title={
|
|
1093
|
+
<span className={`font-bold w-3 text-center ${glyph.color}`} title={actionGlyphLabel(t, change.action)}>
|
|
1054
1094
|
{glyph.sign}
|
|
1055
1095
|
</span>
|
|
1056
1096
|
<span className="font-medium text-foreground">{change.name}</span>
|
|
1057
1097
|
{showPriceChange && (
|
|
1058
1098
|
<span className="font-mono text-muted-foreground">
|
|
1059
|
-
{
|
|
1099
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.priceChangePerMonth', '{before} → {after}/mo', {
|
|
1100
|
+
before: fmt(change.beforeMrc!, currency),
|
|
1101
|
+
after: fmt(change.afterMrc!, currency),
|
|
1102
|
+
})}
|
|
1060
1103
|
</span>
|
|
1061
1104
|
)}
|
|
1062
1105
|
{showSinglePrice && change.action === 'add' && (
|
|
1063
1106
|
<span className="font-mono text-muted-foreground">
|
|
1064
|
-
{fmt(change.afterMrc!, currency)}
|
|
1107
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.pricePerMonth', '{price}/mo', { price: fmt(change.afterMrc!, currency) })}
|
|
1065
1108
|
</span>
|
|
1066
1109
|
)}
|
|
1067
1110
|
{showSinglePrice && change.action === 'cancel' && (
|
|
1068
1111
|
<span className="font-mono text-muted-foreground">
|
|
1069
|
-
was {fmt(change.beforeMrc!, currency)}
|
|
1112
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.wasPricePerMonth', 'was {price}/mo', { price: fmt(change.beforeMrc!, currency) })}
|
|
1070
1113
|
</span>
|
|
1071
1114
|
)}
|
|
1072
1115
|
{showDelta && (
|
|
1073
1116
|
<span
|
|
1074
1117
|
className={`font-mono font-medium ${change.delta! > 0 ? 'text-status-success-text' : 'text-destructive'}`}
|
|
1075
1118
|
>
|
|
1076
|
-
(
|
|
1077
|
-
|
|
1119
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.deltaPerMonth', '({delta}/mo)', {
|
|
1120
|
+
delta: `${change.delta! > 0 ? '+' : ''}${fmt(change.delta!, currency)}`,
|
|
1121
|
+
})}
|
|
1078
1122
|
</span>
|
|
1079
1123
|
)}
|
|
1080
1124
|
{isNoopModify && (
|
|
1081
|
-
<span className="text-muted-foreground italic">no price change</span>
|
|
1125
|
+
<span className="text-muted-foreground italic">{t('cpq.inventory.subscriptions.detail.changeLog.noPriceChange', 'no price change')}</span>
|
|
1082
1126
|
)}
|
|
1083
1127
|
</div>
|
|
1084
1128
|
)
|