@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,17 +1,29 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
+
import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
3
4
|
import { useRouter, useParams, useSearchParams } from 'next/navigation'
|
|
4
5
|
import ArcQuoteConfigurator, {
|
|
5
6
|
type AttachedTarget,
|
|
6
7
|
} from './_components/ArcQuoteConfigurator'
|
|
7
8
|
import { StatusPath } from '../../../../components/StatusPath'
|
|
8
|
-
import { ArrowRight, ExternalLink, Plus } from 'lucide-react'
|
|
9
|
+
import { ArrowRight, ExternalLink, Plus, RotateCcw } from 'lucide-react'
|
|
9
10
|
import { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
10
11
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
11
12
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
12
13
|
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
13
14
|
import type { ActionItem } from '@open-mercato/ui/backend/forms/ActionsDropdown'
|
|
14
15
|
import { NumberInput } from '../../../../components/NumberInput'
|
|
16
|
+
import { ChargeBreakdownTable } from '../../../../components/ChargeBreakdownTable'
|
|
17
|
+
import { PricingSummaryCard } from '../../../../components/PricingSummaryCard'
|
|
18
|
+
import { ArcLineDiff, LineActionBadge } from '../../../../components/ArcLineDiff'
|
|
19
|
+
import { ADJUST_PRICE_FEATURE } from '../../../../lib/priceAdjustmentView'
|
|
20
|
+
import { arcRecurringDelta } from '../../../../lib/quoteSummaryView'
|
|
21
|
+
import { usePriceAdjustments } from './_components/usePriceAdjustments'
|
|
22
|
+
import { PriceAdjustmentsPanel } from './_components/PriceAdjustmentsPanel'
|
|
23
|
+
import { ArcLineRemovalProvider, useArcLineRemoval, useArcLineRemovalUi } from './_components/arcLineRemoval'
|
|
24
|
+
import { AdjustChargeAction, DiscountLineAction, ManualAdjustmentActions, PriceAdjustmentProvider, usePriceAdjustmentUi } from './_components/priceAdjustmentContext'
|
|
25
|
+
import { useGrantedFeatures } from '../../../../components/useGrantedFeatures'
|
|
26
|
+
import { canRecalculateQuote, QUOTE_MANAGE_FEATURE } from '../../../../lib/quotePermissions'
|
|
15
27
|
import {
|
|
16
28
|
formatStatusLabel,
|
|
17
29
|
quoteCpqStatusMap,
|
|
@@ -32,7 +44,12 @@ type ConstrainedAttribute = {
|
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
type TierBreakdownEntry = { tier: number; from: number; to: number; pricePerUnit: number; quantity: number; subtotal: number }
|
|
35
|
-
type RuleAdjustment = {
|
|
47
|
+
type RuleAdjustment = {
|
|
48
|
+
ruleCode: string; ruleName: string; ruleType: string; value: number; unitPriceBefore: number; unitPriceAfter: number; delta: number
|
|
49
|
+
/** Negotiated-pricing fields; absent on legacy snapshots (treated as rules). */
|
|
50
|
+
origin?: 'rule' | 'manual'; adjustmentId?: string; reasonCode?: string; createdByUserId?: string
|
|
51
|
+
}
|
|
52
|
+
type AdjustmentErrorEntry = { adjustmentId: string; quoteLineId: string; chargeCode?: string; code: string }
|
|
36
53
|
|
|
37
54
|
type ResolvedCharge = {
|
|
38
55
|
chargeCode: string
|
|
@@ -46,6 +63,11 @@ type ResolvedCharge = {
|
|
|
46
63
|
breakdown?: { tiers: TierBreakdownEntry[] } | null
|
|
47
64
|
note?: string | null
|
|
48
65
|
adjustments?: RuleAdjustment[] | null
|
|
66
|
+
listUnitPrice?: number
|
|
67
|
+
listTotalPrice?: number | null
|
|
68
|
+
standardUnitPrice?: number
|
|
69
|
+
standardTotalPrice?: number | null
|
|
70
|
+
adjustmentErrors?: AdjustmentErrorEntry[]
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
type QuoteLineAction = 'add' | 'modify' | 'cancel'
|
|
@@ -61,6 +83,7 @@ type ArcLineSource = {
|
|
|
61
83
|
type QuoteLine = {
|
|
62
84
|
lineId: string
|
|
63
85
|
offeringId: string | null
|
|
86
|
+
productId?: string | null
|
|
64
87
|
offeringName: string
|
|
65
88
|
offeringType: string | null
|
|
66
89
|
parentLineId: string | null
|
|
@@ -84,15 +107,23 @@ type QuoteResult = {
|
|
|
84
107
|
customerName: string | null
|
|
85
108
|
cpqStatus: string
|
|
86
109
|
version: number
|
|
110
|
+
pricingRevision?: number
|
|
87
111
|
currencyCode: string
|
|
88
112
|
convertedOrderId: string | null
|
|
89
113
|
validationResult: { valid: boolean; errors: Array<{ message: string }> }
|
|
114
|
+
adjustmentErrors?: AdjustmentErrorEntry[]
|
|
90
115
|
pricingSummary: {
|
|
91
116
|
nrcTotal: number
|
|
92
117
|
mrcTotal: number
|
|
93
118
|
usageCharges: Array<{ chargeCode: string; chargeName: string; unitPrice: number; note: string | null }>
|
|
94
119
|
usageTotalEstimated: number | null
|
|
95
120
|
currencyCode: string
|
|
121
|
+
listNrcTotal?: number
|
|
122
|
+
listMrcTotal?: number
|
|
123
|
+
dealValue?: { list: number; standard: number; offer: number }
|
|
124
|
+
discountDepth?: { rule: number | null; manual: number | null; total: number | null; maxLineManual: number | null }
|
|
125
|
+
ruleDiscountTotal?: number
|
|
126
|
+
manualDiscountTotal?: number
|
|
96
127
|
}
|
|
97
128
|
lines: QuoteLine[]
|
|
98
129
|
}
|
|
@@ -138,27 +169,26 @@ function fmt(amount: number, currency: string): string {
|
|
|
138
169
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
|
|
139
170
|
}
|
|
140
171
|
|
|
141
|
-
|
|
142
|
-
discount_percent: '% discount',
|
|
143
|
-
discount_absolute: 'fixed discount',
|
|
144
|
-
surcharge_percent: '% surcharge',
|
|
145
|
-
surcharge_absolute: 'fixed surcharge',
|
|
146
|
-
price_override: 'override',
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function fmtRuleValue(adj: RuleAdjustment, currency?: string): string {
|
|
150
|
-
if (adj.ruleType === 'discount_percent' || adj.ruleType === 'surcharge_percent') return `${adj.value}%`
|
|
151
|
-
if (currency) return fmt(adj.value, currency)
|
|
152
|
-
return String(adj.value)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
172
|
+
async function apiJson<T>(t: TranslateFn, url: string, init?: RequestInit): Promise<T> {
|
|
156
173
|
const res = await fetch(url, init)
|
|
157
174
|
const json = await res.json()
|
|
158
|
-
if (!res.ok) throw new Error(json?.error ??
|
|
175
|
+
if (!res.ok) throw new Error(json?.error ?? t('cpq.common.requestFailed', 'Request failed ({status})', { status: res.status }))
|
|
159
176
|
return json as T
|
|
160
177
|
}
|
|
161
178
|
|
|
179
|
+
function quoteTypeLabel(t: TranslateFn, quoteType: string): string {
|
|
180
|
+
switch (quoteType) {
|
|
181
|
+
case 'amend':
|
|
182
|
+
return t('cpq.quotes.type.amend', 'amend')
|
|
183
|
+
case 'renew':
|
|
184
|
+
return t('cpq.quotes.type.renew', 'renew')
|
|
185
|
+
case 'cancel':
|
|
186
|
+
return t('cpq.quotes.type.cancel', 'cancel')
|
|
187
|
+
default:
|
|
188
|
+
return quoteType
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
162
192
|
// ─── Header sub-components ─────────────────────────────────────
|
|
163
193
|
|
|
164
194
|
function MetaCard({ label, children }: { label: string; children: React.ReactNode }) {
|
|
@@ -172,12 +202,15 @@ function MetaCard({ label, children }: { label: string; children: React.ReactNod
|
|
|
172
202
|
)
|
|
173
203
|
}
|
|
174
204
|
|
|
205
|
+
// Pricing-summary figure: label, monospace amount, and an optional smaller
|
|
206
|
+
// bracketed note (discount depth / change %) inline after the amount.
|
|
175
207
|
function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
|
|
176
208
|
// Local dropdown — `ActionsDropdown` inside FormHeader.actionsContent
|
|
177
209
|
// would conflict with the inline button placement. This is a thin
|
|
178
210
|
// <details>-style trigger; menuActions on FormHeader would render it
|
|
179
211
|
// inside the header layout, but here we want it adjacent to other
|
|
180
212
|
// primary buttons.
|
|
213
|
+
const t = useT()
|
|
181
214
|
const [open, setOpen] = React.useState(false)
|
|
182
215
|
const ref = React.useRef<HTMLDivElement>(null)
|
|
183
216
|
React.useEffect(() => {
|
|
@@ -193,7 +226,7 @@ function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
|
|
|
193
226
|
return (
|
|
194
227
|
<div className="relative" ref={ref}>
|
|
195
228
|
<Button type="button" variant="outline" onClick={() => setOpen((v) => !v)}>
|
|
196
|
-
Actions
|
|
229
|
+
{t('cpq.quotes.detail.actions', 'Actions')}
|
|
197
230
|
<svg className="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
198
231
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
199
232
|
</svg>
|
|
@@ -230,31 +263,38 @@ function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
|
|
|
230
263
|
}
|
|
231
264
|
|
|
232
265
|
function buildMoreActions({
|
|
266
|
+
t,
|
|
233
267
|
recalculate,
|
|
268
|
+
canRecalculate,
|
|
234
269
|
submitting,
|
|
235
270
|
deletable,
|
|
236
271
|
onDelete,
|
|
237
272
|
deleting,
|
|
238
273
|
}: {
|
|
274
|
+
t: TranslateFn
|
|
239
275
|
recalculate: () => void
|
|
276
|
+
canRecalculate: boolean
|
|
240
277
|
submitting: boolean
|
|
241
278
|
deletable: boolean
|
|
242
279
|
onDelete: () => void
|
|
243
280
|
deleting: boolean
|
|
244
281
|
}): ActionItem[] {
|
|
245
|
-
const items: ActionItem[] = [
|
|
246
|
-
|
|
282
|
+
const items: ActionItem[] = []
|
|
283
|
+
// Recalculate persists prices: offered only to users with cpq.quotes.manage
|
|
284
|
+
// and only on non-terminal quotes (the API refuses both otherwise).
|
|
285
|
+
if (canRecalculate) {
|
|
286
|
+
items.push({
|
|
247
287
|
id: 'recalculate',
|
|
248
|
-
label: 'Recalculate',
|
|
288
|
+
label: t('cpq.quotes.detail.recalculate', 'Recalculate'),
|
|
249
289
|
onSelect: recalculate,
|
|
250
290
|
loading: submitting,
|
|
251
291
|
disabled: submitting,
|
|
252
|
-
}
|
|
253
|
-
|
|
292
|
+
})
|
|
293
|
+
}
|
|
254
294
|
if (deletable) {
|
|
255
295
|
items.push({
|
|
256
296
|
id: 'delete',
|
|
257
|
-
label: 'Delete',
|
|
297
|
+
label: t('cpq.common.delete', 'Delete'),
|
|
258
298
|
onSelect: onDelete,
|
|
259
299
|
disabled: deleting,
|
|
260
300
|
loading: deleting,
|
|
@@ -272,7 +312,9 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
272
312
|
const cpqConfigId = (props.params?.id ?? urlParams?.id) as string
|
|
273
313
|
const isNew = cpqConfigId === 'new'
|
|
274
314
|
|
|
315
|
+
const t = useT()
|
|
275
316
|
const [cpqQuote, setCpqQuote] = React.useState<QuoteResult | null>(null)
|
|
317
|
+
const grantedFeatures = useGrantedFeatures([QUOTE_MANAGE_FEATURE, ADJUST_PRICE_FEATURE])
|
|
276
318
|
const [loading, setLoading] = React.useState(true)
|
|
277
319
|
const [error, setError] = React.useState<string | null>(null)
|
|
278
320
|
const [submitting, setSubmitting] = React.useState(false)
|
|
@@ -327,6 +369,22 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
327
369
|
void loadArcTargets()
|
|
328
370
|
}, [cpqConfigId, isNew, loadArcTargets])
|
|
329
371
|
|
|
372
|
+
// ─── Negotiated prices ─────────────────────────────────────────
|
|
373
|
+
const reloadQuote = React.useCallback(async () => {
|
|
374
|
+
try {
|
|
375
|
+
setCpqQuote(await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqConfigId}`))
|
|
376
|
+
} catch (err) {
|
|
377
|
+
setError((err as Error).message)
|
|
378
|
+
}
|
|
379
|
+
}, [cpqConfigId, t])
|
|
380
|
+
const priceAdjustments = usePriceAdjustments<QuoteResult>({
|
|
381
|
+
quote: isNew ? null : cpqQuote,
|
|
382
|
+
quoteType: arcQuoteType,
|
|
383
|
+
canAdjust: grantedFeatures.includes(QUOTE_MANAGE_FEATURE) && grantedFeatures.includes(ADJUST_PRICE_FEATURE),
|
|
384
|
+
onQuote: setCpqQuote,
|
|
385
|
+
reloadQuote,
|
|
386
|
+
})
|
|
387
|
+
|
|
330
388
|
// ─── Load or create ──────────────────────────────────────────
|
|
331
389
|
|
|
332
390
|
React.useEffect(() => {
|
|
@@ -335,7 +393,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
335
393
|
const quoteId = searchParams.get('quoteId')
|
|
336
394
|
const customerId = searchParams.get('customerId')
|
|
337
395
|
if (!customerId) {
|
|
338
|
-
setError('customerId is required to create a CPQ quote')
|
|
396
|
+
setError(t('cpq.quotes.detail.customerIdRequired', 'customerId is required to create a CPQ quote'))
|
|
339
397
|
setLoading(false)
|
|
340
398
|
return
|
|
341
399
|
}
|
|
@@ -343,7 +401,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
343
401
|
try {
|
|
344
402
|
const body: Record<string, string> = { customerId: customerId! }
|
|
345
403
|
if (quoteId) body.quoteId = quoteId
|
|
346
|
-
const result = await apiJson<QuoteResult>('/api/cpq/quotes', {
|
|
404
|
+
const result = await apiJson<QuoteResult>(t, '/api/cpq/quotes', {
|
|
347
405
|
method: 'POST',
|
|
348
406
|
headers: { 'Content-Type': 'application/json' },
|
|
349
407
|
body: JSON.stringify(body),
|
|
@@ -361,7 +419,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
361
419
|
let cancelled = false
|
|
362
420
|
async function load() {
|
|
363
421
|
try {
|
|
364
|
-
const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqConfigId}`)
|
|
422
|
+
const result = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqConfigId}`)
|
|
365
423
|
if (!cancelled) setCpqQuote(result)
|
|
366
424
|
} catch (err) {
|
|
367
425
|
if (!cancelled) setError((err as Error).message)
|
|
@@ -371,14 +429,14 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
371
429
|
}
|
|
372
430
|
load()
|
|
373
431
|
return () => { cancelled = true }
|
|
374
|
-
}, [cpqConfigId, isNew, searchParams, router])
|
|
432
|
+
}, [cpqConfigId, isNew, searchParams, router, t])
|
|
375
433
|
|
|
376
434
|
// ─── Actions ─────────────────────────────────────────────────
|
|
377
435
|
|
|
378
436
|
const loadOfferings = async () => {
|
|
379
437
|
setOfferingsLoading(true)
|
|
380
438
|
try {
|
|
381
|
-
const result = await apiJson<{ items: Offering[] }>('/api/cpq/product-offerings?pageSize=100&lifecycleStatus=active')
|
|
439
|
+
const result = await apiJson<{ items: Offering[] }>(t, '/api/cpq/product-offerings?pageSize=100&lifecycleStatus=active')
|
|
382
440
|
setOfferings(result.items ?? [])
|
|
383
441
|
} catch { setOfferings([]) }
|
|
384
442
|
finally { setOfferingsLoading(false) }
|
|
@@ -388,7 +446,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
388
446
|
try {
|
|
389
447
|
const params = new URLSearchParams({ offeringId })
|
|
390
448
|
if (Object.keys(currentConfig).length > 0) params.set('configuration', JSON.stringify(currentConfig))
|
|
391
|
-
const result = await apiJson<{ attributes: ConstrainedAttribute[] }>(`/api/cpq/quotes/attributes?${params}`)
|
|
449
|
+
const result = await apiJson<{ attributes: ConstrainedAttribute[] }>(t, `/api/cpq/quotes/attributes?${params}`)
|
|
392
450
|
setAttributes(result.attributes ?? [])
|
|
393
451
|
} catch { setAttributes([]) }
|
|
394
452
|
}
|
|
@@ -407,7 +465,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
407
465
|
const body: Record<string, unknown> = { offeringId: selectedOffering.id, configuration: config, quantity }
|
|
408
466
|
if (addingToParentLineId) body.parentLineId = addingToParentLineId
|
|
409
467
|
if (tid) body.targetSubscriptionId = tid
|
|
410
|
-
lastResult = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/items`, {
|
|
468
|
+
lastResult = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}/items`, {
|
|
411
469
|
method: 'POST',
|
|
412
470
|
headers: { 'Content-Type': 'application/json' },
|
|
413
471
|
body: JSON.stringify(body),
|
|
@@ -424,7 +482,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
424
482
|
if (!cpqQuote) return
|
|
425
483
|
try {
|
|
426
484
|
setSubmitting(true); setError(null)
|
|
427
|
-
const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/items/${lineId}`, {
|
|
485
|
+
const result = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}/items/${lineId}`, {
|
|
428
486
|
method: 'PUT',
|
|
429
487
|
headers: { 'Content-Type': 'application/json' },
|
|
430
488
|
body: JSON.stringify({ configuration: config, quantity }),
|
|
@@ -436,20 +494,21 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
436
494
|
finally { setSubmitting(false) }
|
|
437
495
|
}
|
|
438
496
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
497
|
+
// Amend / Renew: removing an existing subscription item confirms first and
|
|
498
|
+
// marks it for removal (a `cancel` line) instead of deleting the line.
|
|
499
|
+
const lineRemoval = useArcLineRemoval<QuoteResult>({
|
|
500
|
+
quote: isNew ? null : cpqQuote,
|
|
501
|
+
quoteType: arcQuoteType,
|
|
502
|
+
onQuote: setCpqQuote,
|
|
503
|
+
onError: setError,
|
|
504
|
+
})
|
|
505
|
+
const removeItem = lineRemoval.remove
|
|
447
506
|
|
|
448
507
|
const recalculate = async () => {
|
|
449
508
|
if (!cpqQuote) return
|
|
450
509
|
try {
|
|
451
510
|
setSubmitting(true); setError(null)
|
|
452
|
-
const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/recalculate`, {
|
|
511
|
+
const result = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}/recalculate`, {
|
|
453
512
|
method: 'POST',
|
|
454
513
|
headers: { 'Content-Type': 'application/json' },
|
|
455
514
|
body: JSON.stringify({ save: true }),
|
|
@@ -463,7 +522,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
463
522
|
if (!cpqQuote) return
|
|
464
523
|
try {
|
|
465
524
|
setConverting(true); setError(null)
|
|
466
|
-
const result = await apiJson<{ id: string }>('/api/cpq/orders', {
|
|
525
|
+
const result = await apiJson<{ id: string }>(t, '/api/cpq/orders', {
|
|
467
526
|
method: 'POST',
|
|
468
527
|
headers: { 'Content-Type': 'application/json' },
|
|
469
528
|
body: JSON.stringify({ quoteId: cpqQuote.id }),
|
|
@@ -477,7 +536,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
477
536
|
if (!cpqQuote) return
|
|
478
537
|
try {
|
|
479
538
|
setTransitioning(true); setError(null)
|
|
480
|
-
const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/status`, {
|
|
539
|
+
const result = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}/status`, {
|
|
481
540
|
method: 'POST',
|
|
482
541
|
headers: { 'Content-Type': 'application/json' },
|
|
483
542
|
body: JSON.stringify({ targetStatus }),
|
|
@@ -491,7 +550,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
491
550
|
if (!cpqQuote) return
|
|
492
551
|
try {
|
|
493
552
|
setDeleting(true); setError(null); setShowDeleteConfirm(false)
|
|
494
|
-
await apiJson<{ ok: boolean }>(`/api/cpq/quotes/${cpqQuote.id}`, { method: 'DELETE' })
|
|
553
|
+
await apiJson<{ ok: boolean }>(t, `/api/cpq/quotes/${cpqQuote.id}`, { method: 'DELETE' })
|
|
495
554
|
router.push('/backend/cpq/quotes')
|
|
496
555
|
} catch (err) { setError((err as Error).message) }
|
|
497
556
|
finally { setDeleting(false) }
|
|
@@ -529,7 +588,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
529
588
|
setAddingTargetSubIds(inherited ? [inherited] : [])
|
|
530
589
|
if (offeringId) {
|
|
531
590
|
try {
|
|
532
|
-
const result = await apiJson<{ items: Offering[] }>('/api/cpq/product-offerings?pageSize=100&lifecycleStatus=active')
|
|
591
|
+
const result = await apiJson<{ items: Offering[] }>(t, '/api/cpq/product-offerings?pageSize=100&lifecycleStatus=active')
|
|
533
592
|
const match = (result.items ?? []).find((o) => o.id === offeringId)
|
|
534
593
|
if (match) {
|
|
535
594
|
selectOffering(match)
|
|
@@ -564,7 +623,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
564
623
|
if (loading) {
|
|
565
624
|
return (
|
|
566
625
|
<div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
|
|
567
|
-
<Spinner /> Loading…
|
|
626
|
+
<Spinner /> {t('cpq.quotes.detail.loading', 'Loading…')}
|
|
568
627
|
</div>
|
|
569
628
|
)
|
|
570
629
|
}
|
|
@@ -572,9 +631,9 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
572
631
|
if (!cpqQuote) {
|
|
573
632
|
return (
|
|
574
633
|
<div className="space-y-4">
|
|
575
|
-
<button onClick={() => router.push('/backend/cpq/quotes')} className="text-sm text-muted-foreground hover:text-foreground"
|
|
634
|
+
<button onClick={() => router.push('/backend/cpq/quotes')} className="text-sm text-muted-foreground hover:text-foreground">{t('cpq.quotes.detail.backToQuotes', '← Back to Quotes')}</button>
|
|
576
635
|
{error && <ErrorBanner message={error} />}
|
|
577
|
-
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Quote not found
|
|
636
|
+
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">{t('cpq.quotes.detail.notFound', 'Quote not found.')}</div>
|
|
578
637
|
</div>
|
|
579
638
|
)
|
|
580
639
|
}
|
|
@@ -582,6 +641,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
582
641
|
// ─── Render: detail ──────────────────────────────────────────
|
|
583
642
|
|
|
584
643
|
const { pricingSummary: summary, currencyCode: currency } = cpqQuote
|
|
644
|
+
const arcDelta = arcQuoteType === 'amend' || arcQuoteType === 'renew' ? arcRecurringDelta(cpqQuote.lines) : null
|
|
585
645
|
|
|
586
646
|
const arcEditable = ['new', 'incomplete', 'ready'].includes(cpqQuote.cpqStatus)
|
|
587
647
|
const isMergeRenew =
|
|
@@ -601,7 +661,11 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
601
661
|
}
|
|
602
662
|
|
|
603
663
|
return (
|
|
664
|
+
<PriceAdjustmentProvider value={priceAdjustments.ui}>
|
|
665
|
+
<ArcLineRemovalProvider value={lineRemoval.ui}>
|
|
604
666
|
<div className="space-y-6">
|
|
667
|
+
{priceAdjustments.elements}
|
|
668
|
+
{lineRemoval.ConfirmDialogElement}
|
|
605
669
|
{arcDrawerOpen && (
|
|
606
670
|
<ArcQuoteConfigurator
|
|
607
671
|
quoteId={cpqQuote.id}
|
|
@@ -618,7 +682,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
618
682
|
// items from new targets (XD-250 multi-target ARC), so refetch
|
|
619
683
|
// the full quote to pick up the new lines without a hard refresh.
|
|
620
684
|
try {
|
|
621
|
-
const fresh = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}`)
|
|
685
|
+
const fresh = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}`)
|
|
622
686
|
setCpqQuote(fresh)
|
|
623
687
|
} catch {
|
|
624
688
|
/* ignore — loadArcTargets already surfaced any error */
|
|
@@ -634,18 +698,18 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
634
698
|
<FormHeader
|
|
635
699
|
mode="detail"
|
|
636
700
|
backHref="/backend/cpq/quotes"
|
|
637
|
-
backLabel=
|
|
638
|
-
entityTypeLabel=
|
|
639
|
-
title={cpqQuote.quoteNumber || cpqQuote.customerName || 'Untitled'}
|
|
701
|
+
backLabel={t('cpq.quotes.detail.backLabel', 'Back to CPQ Quotes')}
|
|
702
|
+
entityTypeLabel={t('cpq.quotes.detail.entityType', 'CPQ Quote')}
|
|
703
|
+
title={cpqQuote.quoteNumber || cpqQuote.customerName || t('cpq.quotes.detail.untitled', 'Untitled')}
|
|
640
704
|
statusBadge={
|
|
641
705
|
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
642
706
|
<Tag variant={quoteCpqStatusMap[cpqQuote.cpqStatus as QuoteCpqStatus] ?? 'neutral'} dot>
|
|
643
707
|
{formatStatusLabel(cpqQuote.cpqStatus)}
|
|
644
708
|
</Tag>
|
|
645
709
|
{arcQuoteType !== 'new' && (
|
|
646
|
-
<Tag variant="brand" className="uppercase">{arcQuoteType}</Tag>
|
|
710
|
+
<Tag variant="brand" className="uppercase">{quoteTypeLabel(t, arcQuoteType)}</Tag>
|
|
647
711
|
)}
|
|
648
|
-
{cpqQuote.convertedOrderId && <Tag variant="success" dot>Converted</Tag>}
|
|
712
|
+
{cpqQuote.convertedOrderId && <Tag variant="success" dot>{t('cpq.quotes.detail.converted', 'Converted')}</Tag>}
|
|
649
713
|
</div>
|
|
650
714
|
}
|
|
651
715
|
// FormHeader.detail ignores `utilityActions` whenever `actionsContent`
|
|
@@ -659,7 +723,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
659
723
|
onClick={() => router.push(`/backend/sales/quotes/${cpqQuote.quoteId}`)}
|
|
660
724
|
>
|
|
661
725
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
662
|
-
View Sales Quote
|
|
726
|
+
{t('cpq.quotes.detail.viewSalesQuote', 'View Sales Quote')}
|
|
663
727
|
</Button>
|
|
664
728
|
{cpqQuote.convertedOrderId ? (
|
|
665
729
|
<Button
|
|
@@ -667,32 +731,34 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
667
731
|
onClick={() => router.push(`/backend/cpq/orders/${cpqQuote.convertedOrderId}`)}
|
|
668
732
|
>
|
|
669
733
|
<ArrowRight className="h-4 w-4" />
|
|
670
|
-
Go to order
|
|
734
|
+
{t('cpq.quotes.detail.goToOrder', 'Go to order')}
|
|
671
735
|
</Button>
|
|
672
736
|
) : view !== 'summary' ? (
|
|
673
737
|
<Button type="button" variant="outline" onClick={() => { resetConfigState(); setView('summary') }}>
|
|
674
|
-
Cancel
|
|
738
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
675
739
|
</Button>
|
|
676
740
|
) : (
|
|
677
741
|
<>
|
|
678
742
|
<Button type="button" onClick={startAddOffering}>
|
|
679
743
|
<Plus className="h-4 w-4" />
|
|
680
|
-
Add Offering
|
|
744
|
+
{t('cpq.quotes.detail.addOffering', 'Add Offering')}
|
|
681
745
|
</Button>
|
|
682
746
|
{arcEditable && (
|
|
683
747
|
<Button type="button" variant="outline" onClick={() => setArcDrawerOpen(true)}>
|
|
684
|
-
Modify Subscription
|
|
748
|
+
{t('cpq.quotes.detail.modifySubscription', 'Modify Subscription')}
|
|
685
749
|
</Button>
|
|
686
750
|
)}
|
|
687
751
|
{cpqQuote.cpqStatus === 'accepted' && (
|
|
688
752
|
<Button type="button" onClick={convertToOrder} disabled={converting}>
|
|
689
753
|
{converting ? <Spinner /> : <ArrowRight className="h-4 w-4" />}
|
|
690
|
-
Convert to Order
|
|
754
|
+
{t('cpq.quotes.detail.convertToOrder', 'Convert to Order')}
|
|
691
755
|
</Button>
|
|
692
756
|
)}
|
|
693
757
|
<MoreActionsDropdown
|
|
694
758
|
items={buildMoreActions({
|
|
759
|
+
t,
|
|
695
760
|
recalculate,
|
|
761
|
+
canRecalculate: canRecalculateQuote(grantedFeatures, cpqQuote.cpqStatus),
|
|
696
762
|
submitting,
|
|
697
763
|
deletable: !['accepted', 'rejected', 'cancelled'].includes(cpqQuote.cpqStatus),
|
|
698
764
|
onDelete: () => setShowDeleteConfirm(true),
|
|
@@ -733,7 +799,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
733
799
|
statusMap={quoteCpqStatusMap}
|
|
734
800
|
onTransition={transitionStatus}
|
|
735
801
|
disabled={transitioning}
|
|
736
|
-
ariaLabel=
|
|
802
|
+
ariaLabel={t('cpq.quotes.detail.statusPath', 'Quote status path')}
|
|
737
803
|
/>
|
|
738
804
|
{transitioning && <Spinner />}
|
|
739
805
|
</div>
|
|
@@ -744,9 +810,11 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
744
810
|
<div className="rounded-md border border-brand-violet/30 bg-brand-violet/10 px-4 py-3 text-sm text-brand-violet">
|
|
745
811
|
<div className="flex items-center justify-between">
|
|
746
812
|
<div>
|
|
747
|
-
<span className="font-medium uppercase">{arcQuoteType}</span>
|
|
813
|
+
<span className="font-medium uppercase">{quoteTypeLabel(t, arcQuoteType)}</span>
|
|
748
814
|
{' • '}
|
|
749
|
-
{arcTargets.length
|
|
815
|
+
{arcTargets.length === 1
|
|
816
|
+
? t('cpq.quotes.detail.arcTargets.one', '{count} target:', { count: arcTargets.length })
|
|
817
|
+
: t('cpq.quotes.detail.arcTargets.other', '{count} targets:', { count: arcTargets.length })}
|
|
750
818
|
{' '}
|
|
751
819
|
{arcTargets.map((t, i) => (
|
|
752
820
|
<span key={t.id}>
|
|
@@ -763,7 +831,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
763
831
|
))}
|
|
764
832
|
{isMergeRenew && (
|
|
765
833
|
<span className="ml-2 text-status-warning-text font-medium">
|
|
766
|
-
• Merging into a new contract
|
|
834
|
+
{t('cpq.quotes.detail.mergingRenew', '• Merging into a new contract')}
|
|
767
835
|
</span>
|
|
768
836
|
)}
|
|
769
837
|
</div>
|
|
@@ -775,7 +843,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
775
843
|
currency). Mirrors the sidebar tiles in the standard
|
|
776
844
|
sales-document detail layout. */}
|
|
777
845
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
778
|
-
<MetaCard label=
|
|
846
|
+
<MetaCard label={t('cpq.quotes.detail.meta.customer', 'Customer')}>
|
|
779
847
|
{cpqQuote.customerName ? (
|
|
780
848
|
<span className="font-medium">{cpqQuote.customerName}</span>
|
|
781
849
|
) : (
|
|
@@ -784,10 +852,10 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
784
852
|
</span>
|
|
785
853
|
)}
|
|
786
854
|
</MetaCard>
|
|
787
|
-
<MetaCard label=
|
|
855
|
+
<MetaCard label={t('cpq.quotes.detail.meta.version', 'Version')}>
|
|
788
856
|
<span className="font-medium">v{cpqQuote.version}</span>
|
|
789
857
|
</MetaCard>
|
|
790
|
-
<MetaCard label=
|
|
858
|
+
<MetaCard label={t('cpq.quotes.detail.meta.currency', 'Currency')}>
|
|
791
859
|
<span className="font-medium">{currency}</span>
|
|
792
860
|
</MetaCard>
|
|
793
861
|
</div>
|
|
@@ -796,17 +864,17 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
796
864
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setShowDeleteConfirm(false)}>
|
|
797
865
|
<div className="w-full max-w-sm rounded-lg border bg-card shadow-xl" onClick={(e) => e.stopPropagation()}>
|
|
798
866
|
<div className="px-4 py-3 border-b">
|
|
799
|
-
<h3 className="text-sm font-medium">Delete Quote</h3>
|
|
867
|
+
<h3 className="text-sm font-medium">{t('cpq.quotes.detail.deleteConfirm.title', 'Delete Quote')}</h3>
|
|
800
868
|
</div>
|
|
801
869
|
<div className="p-4">
|
|
802
|
-
<p className="text-sm text-muted-foreground">Are you sure you want to delete this quote? This will soft-delete the quote and all its line items
|
|
870
|
+
<p className="text-sm text-muted-foreground">{t('cpq.quotes.detail.deleteConfirm.text', 'Are you sure you want to delete this quote? This will soft-delete the quote and all its line items.')}</p>
|
|
803
871
|
</div>
|
|
804
872
|
<div className="flex justify-end gap-2 px-4 py-3 border-t">
|
|
805
873
|
<Button type="button" variant="outline" onClick={() => setShowDeleteConfirm(false)}>
|
|
806
|
-
Cancel
|
|
874
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
807
875
|
</Button>
|
|
808
876
|
<Button type="button" variant="destructive" onClick={deleteQuote} disabled={deleting}>
|
|
809
|
-
{deleting ? <Spinner /> : null} Delete
|
|
877
|
+
{deleting ? <Spinner /> : null} {t('cpq.common.delete', 'Delete')}
|
|
810
878
|
</Button>
|
|
811
879
|
</div>
|
|
812
880
|
</div>
|
|
@@ -816,39 +884,12 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
816
884
|
{/* Summary view */}
|
|
817
885
|
{view === 'summary' && (
|
|
818
886
|
<>
|
|
819
|
-
{
|
|
820
|
-
<div className="rounded-lg border bg-card p-4">
|
|
821
|
-
<h3 className="text-sm font-medium mb-3">Pricing Summary</h3>
|
|
822
|
-
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
823
|
-
<div>
|
|
824
|
-
<p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
|
|
825
|
-
<p className="text-lg font-semibold font-mono">{fmt(summary.nrcTotal, currency)}</p>
|
|
826
|
-
</div>
|
|
827
|
-
<div>
|
|
828
|
-
<p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
|
|
829
|
-
<p className="text-lg font-semibold font-mono">{fmt(summary.mrcTotal, currency)}</p>
|
|
830
|
-
</div>
|
|
831
|
-
{summary.usageTotalEstimated != null && (
|
|
832
|
-
<div>
|
|
833
|
-
<p className="text-xs text-muted-foreground">Usage (est.)</p>
|
|
834
|
-
<p className="text-lg font-semibold font-mono">{fmt(summary.usageTotalEstimated, currency)}</p>
|
|
835
|
-
</div>
|
|
836
|
-
)}
|
|
837
|
-
{summary.usageCharges.length > 0 && (
|
|
838
|
-
<div>
|
|
839
|
-
<p className="text-xs text-muted-foreground">Usage Charges</p>
|
|
840
|
-
{summary.usageCharges.map((uc, i) => (
|
|
841
|
-
<p key={i} className="text-xs mt-0.5">{uc.chargeName}: {fmt(uc.unitPrice, currency)}/unit</p>
|
|
842
|
-
))}
|
|
843
|
-
</div>
|
|
844
|
-
)}
|
|
845
|
-
</div>
|
|
846
|
-
</div>
|
|
887
|
+
<PricingSummaryCard summary={summary} currency={currency} arcDelta={arcDelta} />
|
|
847
888
|
|
|
848
889
|
{/* Validation errors */}
|
|
849
890
|
{!cpqQuote.validationResult.valid && (
|
|
850
891
|
<Alert variant="warning">
|
|
851
|
-
<AlertTitle>Validation Issues</AlertTitle>
|
|
892
|
+
<AlertTitle>{t('cpq.quotes.detail.validationIssues', 'Validation Issues')}</AlertTitle>
|
|
852
893
|
<AlertDescription>
|
|
853
894
|
{/* Stacked <span>s rather than <ul>/<li>: before Open Mercato
|
|
854
895
|
0.8 AlertDescription rendered a <p>, where a list is invalid
|
|
@@ -863,6 +904,23 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
863
904
|
</Alert>
|
|
864
905
|
)}
|
|
865
906
|
|
|
907
|
+
{/* Amend lines the rep reconfigured are re-priced at today's catalog
|
|
908
|
+
price (server marks them `_arcRepriced`); negotiated prices are
|
|
909
|
+
not carried over yet (later release). */}
|
|
910
|
+
{arcQuoteType === 'amend' && cpqQuote.lines.some((l) => l.action !== 'cancel' && l.configuration?._arcRepriced === true) && (
|
|
911
|
+
<Alert status="information" style="lighter">
|
|
912
|
+
<AlertTitle>
|
|
913
|
+
{t('cpq.quotes.arc.repricedTitle', 'Some existing items use the current catalog price')}
|
|
914
|
+
</AlertTitle>
|
|
915
|
+
<AlertDescription>
|
|
916
|
+
{t(
|
|
917
|
+
'cpq.quotes.arc.repricedDescription',
|
|
918
|
+
"Reconfigured lines were re-priced at today's catalog price. Negotiated prices from the subscription are not carried over to reconfigured lines yet.",
|
|
919
|
+
)}
|
|
920
|
+
</AlertDescription>
|
|
921
|
+
</Alert>
|
|
922
|
+
)}
|
|
923
|
+
|
|
866
924
|
{/* Quote Lines */}
|
|
867
925
|
<QuoteLineTree
|
|
868
926
|
lines={cpqQuote.lines}
|
|
@@ -882,6 +940,15 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
882
940
|
)
|
|
883
941
|
}
|
|
884
942
|
/>
|
|
943
|
+
|
|
944
|
+
<PriceAdjustmentsPanel
|
|
945
|
+
quoteId={cpqQuote.id}
|
|
946
|
+
items={priceAdjustments.items}
|
|
947
|
+
total={priceAdjustments.total}
|
|
948
|
+
lines={cpqQuote.lines}
|
|
949
|
+
currency={currency}
|
|
950
|
+
errorsById={priceAdjustments.errorsById}
|
|
951
|
+
/>
|
|
885
952
|
</>
|
|
886
953
|
)}
|
|
887
954
|
|
|
@@ -890,7 +957,8 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
890
957
|
<>
|
|
891
958
|
{addingToParentLineId && (
|
|
892
959
|
<Alert variant="info">
|
|
893
|
-
Adding component to bundle:
|
|
960
|
+
{t('cpq.quotes.detail.addingComponentTo', 'Adding component to bundle:')}{' '}
|
|
961
|
+
<span className="font-medium">{cpqQuote.lines.find((l) => l.lineId === addingToParentLineId)?.offeringName ?? t('cpq.quotes.detail.bundleFallback', 'Bundle')}</span>
|
|
894
962
|
</Alert>
|
|
895
963
|
)}
|
|
896
964
|
<OfferingBrowser
|
|
@@ -906,7 +974,11 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
906
974
|
{/* Configure new offering */}
|
|
907
975
|
{view === 'configure' && selectedOffering && (
|
|
908
976
|
<ConfigurePanel
|
|
909
|
-
title={
|
|
977
|
+
title={
|
|
978
|
+
addingToParentLineId
|
|
979
|
+
? t('cpq.quotes.detail.configure.addComponentTitle', 'Add Component: {name}', { name: selectedOffering.name })
|
|
980
|
+
: t('cpq.quotes.detail.configure.configureTitle', 'Configure: {name}', { name: selectedOffering.name })
|
|
981
|
+
}
|
|
910
982
|
attributes={attributes}
|
|
911
983
|
config={config}
|
|
912
984
|
quantity={quantity}
|
|
@@ -932,7 +1004,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
932
1004
|
}}
|
|
933
1005
|
onQuantityChange={setQuantity}
|
|
934
1006
|
onSubmit={addItem}
|
|
935
|
-
submitLabel=
|
|
1007
|
+
submitLabel={t('cpq.quotes.detail.configure.addToQuote', 'Add to Quote')}
|
|
936
1008
|
submitting={submitting}
|
|
937
1009
|
/>
|
|
938
1010
|
)}
|
|
@@ -940,7 +1012,9 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
940
1012
|
{/* Edit existing line */}
|
|
941
1013
|
{view === 'edit' && editingLineId && (
|
|
942
1014
|
<ConfigurePanel
|
|
943
|
-
title={
|
|
1015
|
+
title={t('cpq.quotes.detail.configure.editTitle', 'Edit: {name}', {
|
|
1016
|
+
name: cpqQuote.lines.find((l) => l.lineId === editingLineId)?.offeringName ?? t('cpq.quotes.detail.configure.lineFallback', 'Line'),
|
|
1017
|
+
})}
|
|
944
1018
|
attributes={attributes}
|
|
945
1019
|
config={config}
|
|
946
1020
|
quantity={quantity}
|
|
@@ -955,11 +1029,13 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
955
1029
|
}}
|
|
956
1030
|
onQuantityChange={setQuantity}
|
|
957
1031
|
onSubmit={() => updateItem(editingLineId)}
|
|
958
|
-
submitLabel=
|
|
1032
|
+
submitLabel={t('cpq.quotes.detail.configure.updateLine', 'Update Line')}
|
|
959
1033
|
submitting={submitting}
|
|
960
1034
|
/>
|
|
961
1035
|
)}
|
|
962
1036
|
</div>
|
|
1037
|
+
</ArcLineRemovalProvider>
|
|
1038
|
+
</PriceAdjustmentProvider>
|
|
963
1039
|
)
|
|
964
1040
|
}
|
|
965
1041
|
|
|
@@ -973,6 +1049,7 @@ function QuoteLineTree({ lines, currency, expandedLines, pricingDetail, targetCo
|
|
|
973
1049
|
onRemoveLine: (lineId: string) => void; onAddComponent: (parentLineId: string, offeringId?: string) => void
|
|
974
1050
|
onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
|
|
975
1051
|
}) {
|
|
1052
|
+
const t = useT()
|
|
976
1053
|
const [bundleTrees, setBundleTrees] = React.useState<Map<string, BundleTree>>(new Map())
|
|
977
1054
|
const [loadingTrees, setLoadingTrees] = React.useState<Set<string>>(new Set())
|
|
978
1055
|
|
|
@@ -990,18 +1067,18 @@ function QuoteLineTree({ lines, currency, expandedLines, pricingDetail, targetCo
|
|
|
990
1067
|
if (bundleTrees.has(offeringId) || loadingTrees.has(offeringId)) return
|
|
991
1068
|
setLoadingTrees((prev) => new Set(prev).add(offeringId))
|
|
992
1069
|
try {
|
|
993
|
-
const result = await apiJson<{ tree: BundleTree }>(`/api/cpq/bundle-tree?offeringId=${offeringId}`)
|
|
1070
|
+
const result = await apiJson<{ tree: BundleTree }>(t, `/api/cpq/bundle-tree?offeringId=${offeringId}`)
|
|
994
1071
|
setBundleTrees((prev) => new Map(prev).set(offeringId, result.tree))
|
|
995
1072
|
} catch { /* ignore — will just not show structure */ }
|
|
996
1073
|
finally { setLoadingTrees((prev) => { const n = new Set(prev); n.delete(offeringId); return n }) }
|
|
997
|
-
}, [bundleTrees, loadingTrees])
|
|
1074
|
+
}, [bundleTrees, loadingTrees, t])
|
|
998
1075
|
|
|
999
1076
|
return (
|
|
1000
1077
|
<div>
|
|
1001
|
-
<h3 className="text-sm font-medium mb-2 px-1">Line Items</h3>
|
|
1078
|
+
<h3 className="text-sm font-medium mb-2 px-1">{t('cpq.quotes.detail.lineItems', 'Line Items')}</h3>
|
|
1002
1079
|
{lines.length === 0 ? (
|
|
1003
1080
|
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
|
|
1004
|
-
No items configured yet. Click
|
|
1081
|
+
{t('cpq.quotes.detail.noLines', 'No items configured yet. Click "Add Offering" to start.')}
|
|
1005
1082
|
</div>
|
|
1006
1083
|
) : (
|
|
1007
1084
|
<div className="rounded-lg border bg-card divide-y">
|
|
@@ -1069,10 +1146,11 @@ function BundleSlotPanel({ bundleTree, treeLoading, children, allLines, currency
|
|
|
1069
1146
|
onRemoveLine: (lineId: string) => void; onAddComponent: (parentLineId: string, offeringId?: string) => void
|
|
1070
1147
|
onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
|
|
1071
1148
|
}) {
|
|
1149
|
+
const t = useT()
|
|
1072
1150
|
if (treeLoading) {
|
|
1073
1151
|
return (
|
|
1074
1152
|
<div className="border-t bg-muted/5 px-8 py-3">
|
|
1075
|
-
<div className="flex items-center gap-2 text-xs text-muted-foreground"><Spinner /> Loading bundle structure
|
|
1153
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground"><Spinner /> {t('cpq.quotes.bundle.loadingStructure', 'Loading bundle structure…')}</div>
|
|
1076
1154
|
</div>
|
|
1077
1155
|
)
|
|
1078
1156
|
}
|
|
@@ -1121,13 +1199,14 @@ function BundleSlotPanel({ bundleTree, treeLoading, children, allLines, currency
|
|
|
1121
1199
|
<div className="flex items-center gap-2 px-6 py-2 bg-muted/20">
|
|
1122
1200
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{slot.name}</span>
|
|
1123
1201
|
{isRequired ? (
|
|
1124
|
-
<Tag variant="warning" className="text-[10px] px-1.5">required</Tag>
|
|
1202
|
+
<Tag variant="warning" className="text-[10px] px-1.5">{t('cpq.quotes.bundle.slotRequired', 'required')}</Tag>
|
|
1125
1203
|
) : (
|
|
1126
|
-
<Tag variant="neutral" className="text-[10px] px-1.5">optional</Tag>
|
|
1204
|
+
<Tag variant="neutral" className="text-[10px] px-1.5">{t('cpq.quotes.bundle.slotOptional', 'optional')}</Tag>
|
|
1127
1205
|
)}
|
|
1128
1206
|
<span className="text-[10px] text-muted-foreground">
|
|
1129
1207
|
{filledCount}/{slot.cardinalityMax ?? '∞'}
|
|
1130
|
-
{slot.cardinalityMin > 0 &&
|
|
1208
|
+
{slot.cardinalityMin > 0 && ' '}
|
|
1209
|
+
{slot.cardinalityMin > 0 && t('cpq.quotes.bundle.slotMin', '(min {min})', { min: slot.cardinalityMin })}
|
|
1131
1210
|
</span>
|
|
1132
1211
|
<div className="flex-1" />
|
|
1133
1212
|
{/* Available offerings in this slot */}
|
|
@@ -1146,7 +1225,13 @@ function BundleSlotPanel({ bundleTree, treeLoading, children, allLines, currency
|
|
|
1146
1225
|
? 'border-border hover:border-primary hover:bg-primary/5 hover:text-primary cursor-pointer'
|
|
1147
1226
|
: 'border-border text-muted-foreground opacity-50 cursor-not-allowed'
|
|
1148
1227
|
}`}
|
|
1149
|
-
title={
|
|
1228
|
+
title={
|
|
1229
|
+
isFilled
|
|
1230
|
+
? t('cpq.quotes.bundle.alreadyAdded', 'Already added')
|
|
1231
|
+
: canAdd
|
|
1232
|
+
? t('cpq.quotes.bundle.addComponent', 'Add {name}', { name: comp.childOffering.offeringName })
|
|
1233
|
+
: t('cpq.quotes.bundle.slotFull', 'Slot is full')
|
|
1234
|
+
}
|
|
1150
1235
|
>
|
|
1151
1236
|
{isFilled ? (
|
|
1152
1237
|
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
@@ -1215,11 +1300,20 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1215
1300
|
onAddComponent?: () => void
|
|
1216
1301
|
onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
|
|
1217
1302
|
}) {
|
|
1303
|
+
const t = useT()
|
|
1304
|
+
const adjustmentUi = usePriceAdjustmentUi()
|
|
1305
|
+
const removalUi = useArcLineRemovalUi()
|
|
1306
|
+
const isRemoved = removalUi?.isRemoved(line) ?? false
|
|
1218
1307
|
const paddingLeft = indent > 0 ? `${indent * 2 + 1}rem` : undefined
|
|
1308
|
+
const chargeActions = {
|
|
1309
|
+
reasonLabels: adjustmentUi?.reasonLabels,
|
|
1310
|
+
renderChargeActions: (charge: ResolvedCharge) => <AdjustChargeAction line={line} charge={charge} />,
|
|
1311
|
+
renderManualActions: (adjustmentId: string) => <ManualAdjustmentActions adjustmentId={adjustmentId} />,
|
|
1312
|
+
}
|
|
1219
1313
|
|
|
1220
1314
|
return (
|
|
1221
1315
|
<div>
|
|
1222
|
-
<div className=
|
|
1316
|
+
<div className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors ${isRemoved ? 'bg-muted/20' : ''}`} style={paddingLeft ? { paddingLeft } : undefined} data-testid="cpq-quote-line" data-removed={isRemoved ? 'true' : undefined}>
|
|
1223
1317
|
{/* Expand toggle */}
|
|
1224
1318
|
<button onClick={onToggle} className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground">
|
|
1225
1319
|
<svg className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
@@ -1234,27 +1328,47 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1234
1328
|
<span className="text-muted-foreground text-xs">└</span>
|
|
1235
1329
|
)}
|
|
1236
1330
|
{isBundle && (
|
|
1237
|
-
<Tag variant="brand" className="px-1.5 text-xs">bundle</Tag>
|
|
1331
|
+
<Tag variant="brand" className="px-1.5 text-xs">{t('cpq.lines.bundle', 'bundle')}</Tag>
|
|
1238
1332
|
)}
|
|
1239
|
-
<span className=
|
|
1333
|
+
<span className={`font-medium text-sm ${isRemoved ? 'text-muted-foreground line-through' : ''}`}>{line.offeringName}</span>
|
|
1240
1334
|
{line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
|
|
1241
|
-
<
|
|
1335
|
+
<LineActionBadge
|
|
1336
|
+
action={line.action}
|
|
1337
|
+
label={isRemoved ? t('cpq.quotes.arc.lineRemoved', 'remove') : undefined}
|
|
1338
|
+
title={isRemoved ? t('cpq.quotes.arc.lineRemovedHint', 'This item is cancelled on the subscription when the order is activated') : undefined}
|
|
1339
|
+
/>
|
|
1340
|
+
{!isRemoved && line.configuration?._arcPriceFrozen === true && (
|
|
1341
|
+
<Tag
|
|
1342
|
+
variant="neutral"
|
|
1343
|
+
className="px-1.5 text-xs"
|
|
1344
|
+
title={t(
|
|
1345
|
+
'cpq.quotes.arc.priceKeptHint',
|
|
1346
|
+
'Price kept from the subscription — it changes only if you reconfigure this line',
|
|
1347
|
+
)}
|
|
1348
|
+
>
|
|
1349
|
+
{t('cpq.quotes.arc.priceKept', 'price kept')}
|
|
1350
|
+
</Tag>
|
|
1351
|
+
)}
|
|
1242
1352
|
{targetCodes.size >= 2 && line.targetSubscriptionId && targetCodes.get(line.targetSubscriptionId) && (
|
|
1243
1353
|
<Tag
|
|
1244
1354
|
variant="brand"
|
|
1245
1355
|
className="px-1.5 text-xs"
|
|
1246
|
-
title={
|
|
1356
|
+
title={t('cpq.quotes.detail.actsOnSubscription', 'Acts on subscription {code}', { code: targetCodes.get(line.targetSubscriptionId) ?? '' })}
|
|
1247
1357
|
>
|
|
1248
1358
|
→ {targetCodes.get(line.targetSubscriptionId)}
|
|
1249
1359
|
</Tag>
|
|
1250
1360
|
)}
|
|
1251
|
-
{line.isConfigured ? (
|
|
1252
|
-
<Tag variant="success" className="px-1.5 text-xs">configured</Tag>
|
|
1361
|
+
{isRemoved ? null : line.isConfigured ? (
|
|
1362
|
+
<Tag variant="success" className="px-1.5 text-xs">{t('cpq.quotes.detail.lineConfigured', 'configured')}</Tag>
|
|
1253
1363
|
) : (
|
|
1254
|
-
<Tag variant="warning" className="px-1.5 text-xs">incomplete</Tag>
|
|
1364
|
+
<Tag variant="warning" className="px-1.5 text-xs">{t('cpq.quotes.detail.lineIncomplete', 'incomplete')}</Tag>
|
|
1255
1365
|
)}
|
|
1256
1366
|
{isBundle && (
|
|
1257
|
-
<span className="text-xs text-muted-foreground">
|
|
1367
|
+
<span className="text-xs text-muted-foreground">
|
|
1368
|
+
{childCount !== 1
|
|
1369
|
+
? t('cpq.lines.componentCount.other', '{count} components', { count: childCount })
|
|
1370
|
+
: t('cpq.lines.componentCount.one', '{count} component', { count: childCount })}
|
|
1371
|
+
</span>
|
|
1258
1372
|
)}
|
|
1259
1373
|
</div>
|
|
1260
1374
|
{line.validationErrors && line.validationErrors.length > 0 && (
|
|
@@ -1264,43 +1378,66 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1264
1378
|
|
|
1265
1379
|
{/* Pricing */}
|
|
1266
1380
|
<div className="shrink-0 text-right text-xs space-y-0.5">
|
|
1267
|
-
{line.
|
|
1268
|
-
|
|
1381
|
+
{isRemoved && line.arcSource && line.arcSource.mrcAmount > 0 && (
|
|
1382
|
+
<div>
|
|
1383
|
+
<span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span>
|
|
1384
|
+
<span className="font-mono text-muted-foreground line-through">{fmt(line.arcSource.mrcAmount, currency)}</span>
|
|
1385
|
+
</div>
|
|
1386
|
+
)}
|
|
1387
|
+
{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>}
|
|
1388
|
+
{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>}
|
|
1269
1389
|
</div>
|
|
1270
1390
|
|
|
1271
1391
|
{/* Actions */}
|
|
1392
|
+
{isRemoved ? (
|
|
1393
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
1394
|
+
<Button
|
|
1395
|
+
type="button" variant="ghost" size="sm"
|
|
1396
|
+
onClick={() => removalUi?.restore(line.lineId)}
|
|
1397
|
+
title={t('cpq.quotes.arc.restoreHint', 'Keep this item on the subscription')}
|
|
1398
|
+
data-testid="cpq-line-restore"
|
|
1399
|
+
>
|
|
1400
|
+
<RotateCcw className="h-4 w-4" />
|
|
1401
|
+
{t('cpq.quotes.arc.restore', 'Restore')}
|
|
1402
|
+
</Button>
|
|
1403
|
+
</div>
|
|
1404
|
+
) : (
|
|
1272
1405
|
<div className="flex items-center gap-1 shrink-0">
|
|
1406
|
+
<DiscountLineAction line={line} />
|
|
1273
1407
|
{onAddComponent && (
|
|
1274
|
-
<button onClick={onAddComponent} className="rounded p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/5 transition-colors" title=
|
|
1408
|
+
<button onClick={onAddComponent} className="rounded p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/5 transition-colors" title={t('cpq.quotes.detail.addComponent', 'Add Component')}>
|
|
1275
1409
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
|
1276
1410
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
1277
1411
|
</svg>
|
|
1278
1412
|
</button>
|
|
1279
1413
|
)}
|
|
1280
|
-
<button onClick={onEdit} className="rounded p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title=
|
|
1414
|
+
<button onClick={onEdit} className="rounded p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t('cpq.common.edit', 'Edit')}>
|
|
1281
1415
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
|
1282
1416
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
|
1283
1417
|
</svg>
|
|
1284
1418
|
</button>
|
|
1285
|
-
<button onClick={onRemove} className="rounded p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors" title=
|
|
1419
|
+
<button onClick={onRemove} className="rounded p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors" title={t('cpq.quotes.detail.removeLine', 'Remove')}>
|
|
1286
1420
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
|
1287
1421
|
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
|
1288
1422
|
</svg>
|
|
1289
1423
|
</button>
|
|
1290
1424
|
</div>
|
|
1425
|
+
)}
|
|
1291
1426
|
</div>
|
|
1292
1427
|
|
|
1293
|
-
{/* XD-250 ARC: before/after preview when line mirrors a sub item */}
|
|
1294
|
-
{isExpanded && line.arcSource && (
|
|
1295
|
-
<ArcLineDiff line={line} currency={currency} />
|
|
1296
|
-
)}
|
|
1297
1428
|
{/* Charge breakdown (non-bundle or any expanded line) */}
|
|
1298
1429
|
{isExpanded && !isBundle && line.charges.length > 0 && (
|
|
1299
|
-
<ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} />
|
|
1430
|
+
<ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} {...chargeActions} />
|
|
1300
1431
|
)}
|
|
1301
1432
|
{/* For bundles: show own charges first, then children are rendered by parent */}
|
|
1302
1433
|
{isExpanded && isBundle && line.charges.length > 0 && (
|
|
1303
|
-
<ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} />
|
|
1434
|
+
<ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} {...chargeActions} />
|
|
1435
|
+
)}
|
|
1436
|
+
{/* XD-250 ARC: before/after preview, below the charges, when line mirrors a sub item */}
|
|
1437
|
+
{isExpanded && line.arcSource && (
|
|
1438
|
+
<div className="border-t bg-muted/10 px-12 py-2">
|
|
1439
|
+
<ArcLineDiff line={line} currency={currency} />
|
|
1440
|
+
</div>
|
|
1304
1441
|
)}
|
|
1305
1442
|
{/* Fallback: line has no per-charge breakdown and no ARC diff (truly empty) */}
|
|
1306
1443
|
{isExpanded && line.charges.length === 0 && !line.arcSource && (
|
|
@@ -1310,93 +1447,24 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1310
1447
|
)
|
|
1311
1448
|
}
|
|
1312
1449
|
|
|
1313
|
-
function ArcLineDiff({ line, currency }: { line: QuoteLine; currency: string }) {
|
|
1314
|
-
const src = line.arcSource
|
|
1315
|
-
if (!src) return null
|
|
1316
|
-
const isCancel = line.action === 'cancel'
|
|
1317
|
-
const beforeMrc = src.mrcAmount
|
|
1318
|
-
const beforeNrc = src.nrcAmount
|
|
1319
|
-
const beforeQty = src.quantity
|
|
1320
|
-
const afterMrc = isCancel ? 0 : line.mrcTotal
|
|
1321
|
-
const afterNrc = isCancel ? 0 : line.nrcTotal
|
|
1322
|
-
const afterQty = isCancel ? 0 : line.quantity
|
|
1323
|
-
const fmtVal = (v: number, asCurrency: boolean) =>
|
|
1324
|
-
asCurrency ? fmt(v, currency) : String(v)
|
|
1325
|
-
const rows: Array<{ label: string; before: number; after: number; asCurrency: boolean }> = [
|
|
1326
|
-
{ label: 'Quantity', before: beforeQty, after: afterQty, asCurrency: false },
|
|
1327
|
-
{ label: 'MRC', before: beforeMrc, after: afterMrc, asCurrency: true },
|
|
1328
|
-
{ label: 'NRC', before: beforeNrc, after: afterNrc, asCurrency: true },
|
|
1329
|
-
]
|
|
1330
|
-
|
|
1331
|
-
return (
|
|
1332
|
-
<div className="border-t bg-muted/10 px-12 py-2 space-y-1.5">
|
|
1333
|
-
<p className="text-xs font-medium text-muted-foreground">Change Preview</p>
|
|
1334
|
-
<div className="rounded border bg-background overflow-hidden">
|
|
1335
|
-
<table className="w-full text-xs">
|
|
1336
|
-
<thead className="bg-muted/40">
|
|
1337
|
-
<tr className="text-muted-foreground">
|
|
1338
|
-
<th className="text-left px-3 py-1.5 font-medium" />
|
|
1339
|
-
<th className="text-right px-3 py-1.5 font-medium">Before</th>
|
|
1340
|
-
<th className="text-right px-3 py-1.5 font-medium">After</th>
|
|
1341
|
-
<th className="text-right px-3 py-1.5 font-medium">Δ</th>
|
|
1342
|
-
</tr>
|
|
1343
|
-
</thead>
|
|
1344
|
-
<tbody>
|
|
1345
|
-
{rows.map((r) => {
|
|
1346
|
-
const delta = r.after - r.before
|
|
1347
|
-
const showDelta = delta !== 0
|
|
1348
|
-
return (
|
|
1349
|
-
<tr key={r.label} className="border-t border-dashed border-border/40">
|
|
1350
|
-
<td className="px-3 py-1 text-muted-foreground">{r.label}</td>
|
|
1351
|
-
<td className="px-3 py-1 text-right font-mono">{fmtVal(r.before, r.asCurrency)}</td>
|
|
1352
|
-
<td className={`px-3 py-1 text-right font-mono ${isCancel ? 'text-muted-foreground line-through' : ''}`}>
|
|
1353
|
-
{fmtVal(r.after, r.asCurrency)}
|
|
1354
|
-
</td>
|
|
1355
|
-
<td className={`px-3 py-1 text-right font-mono font-medium ${showDelta ? (delta > 0 ? 'text-status-success-text' : 'text-destructive') : 'text-muted-foreground'}`}>
|
|
1356
|
-
{showDelta ? `${delta > 0 ? '+' : ''}${fmtVal(delta, r.asCurrency)}` : '—'}
|
|
1357
|
-
</td>
|
|
1358
|
-
</tr>
|
|
1359
|
-
)
|
|
1360
|
-
})}
|
|
1361
|
-
</tbody>
|
|
1362
|
-
</table>
|
|
1363
|
-
</div>
|
|
1364
|
-
{isCancel && (
|
|
1365
|
-
<p className="text-xs text-destructive italic">Cancellation — item will be terminated on activation.</p>
|
|
1366
|
-
)}
|
|
1367
|
-
</div>
|
|
1368
|
-
)
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
function ActionBadge({ action }: { action: QuoteLineAction }) {
|
|
1372
|
-
const variantByAction: Record<QuoteLineAction, 'info' | 'warning' | 'error'> = {
|
|
1373
|
-
add: 'info',
|
|
1374
|
-
modify: 'warning',
|
|
1375
|
-
cancel: 'error',
|
|
1376
|
-
}
|
|
1377
|
-
return (
|
|
1378
|
-
<Tag variant={variantByAction[action]} className="px-1.5 text-xs">
|
|
1379
|
-
{action}
|
|
1380
|
-
</Tag>
|
|
1381
|
-
)
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
1450
|
function LineSummaryFallback({ line, currency }: { line: QuoteLine; currency: string }) {
|
|
1451
|
+
const t = useT()
|
|
1385
1452
|
const hasTotals = line.nrcTotal > 0 || line.mrcTotal > 0
|
|
1386
1453
|
const mirroredFromItemId = (line.configuration?._arcMirroredFromItemId as string | undefined) ?? null
|
|
1387
1454
|
return (
|
|
1388
1455
|
<div className="border-t bg-muted/10 px-12 py-2 text-xs text-muted-foreground space-y-1">
|
|
1389
1456
|
<div>
|
|
1390
|
-
No detailed charge breakdown available for this line.
|
|
1391
|
-
{mirroredFromItemId && '
|
|
1457
|
+
{t('cpq.quotes.detail.noBreakdown', 'No detailed charge breakdown available for this line.')}
|
|
1458
|
+
{mirroredFromItemId && ' '}
|
|
1459
|
+
{mirroredFromItemId && t('cpq.orders.detail.mirroredItem', 'Mirrored from existing subscription item.')}
|
|
1392
1460
|
</div>
|
|
1393
1461
|
{hasTotals && (
|
|
1394
1462
|
<div className="flex gap-4">
|
|
1395
1463
|
{line.nrcTotal > 0 && (
|
|
1396
|
-
<span>NRC <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
|
|
1464
|
+
<span>{t('cpq.lines.nrc', 'NRC')} <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
|
|
1397
1465
|
)}
|
|
1398
1466
|
{line.mrcTotal > 0 && (
|
|
1399
|
-
<span>MRC <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
|
|
1467
|
+
<span>{t('cpq.lines.mrc', 'MRC')} <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
|
|
1400
1468
|
)}
|
|
1401
1469
|
</div>
|
|
1402
1470
|
)}
|
|
@@ -1404,138 +1472,22 @@ function LineSummaryFallback({ line, currency }: { line: QuoteLine; currency: st
|
|
|
1404
1472
|
)
|
|
1405
1473
|
}
|
|
1406
1474
|
|
|
1407
|
-
function ChargeBreakdown({ line, lineIdx,
|
|
1475
|
+
function ChargeBreakdown({ line, lineIdx, pricingDetail, onTogglePricingDetail, ...tableProps }: {
|
|
1408
1476
|
line: QuoteLine; lineIdx: number; currency: string
|
|
1409
1477
|
pricingDetail: { lineIdx: number; chargeIdx: number } | null
|
|
1410
1478
|
onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
|
|
1479
|
+
reasonLabels?: Record<string, string>
|
|
1480
|
+
renderChargeActions?: (charge: ResolvedCharge) => React.ReactNode
|
|
1481
|
+
renderManualActions?: (adjustmentId: string) => React.ReactNode
|
|
1411
1482
|
}) {
|
|
1412
1483
|
return (
|
|
1413
1484
|
<div className="border-t bg-muted/10 px-12 py-2">
|
|
1414
|
-
<
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
<th className="text-right py-1 font-medium">Qty</th>
|
|
1421
|
-
<th className="text-right py-1 font-medium">Total</th>
|
|
1422
|
-
<th className="w-8" />
|
|
1423
|
-
</tr>
|
|
1424
|
-
</thead>
|
|
1425
|
-
<tbody>
|
|
1426
|
-
{line.charges.map((c, i) => {
|
|
1427
|
-
const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length)
|
|
1428
|
-
const isDetailOpen = pricingDetail?.lineIdx === lineIdx && pricingDetail?.chargeIdx === i
|
|
1429
|
-
return (
|
|
1430
|
-
<React.Fragment key={i}>
|
|
1431
|
-
<tr className="border-t border-dashed border-border/50">
|
|
1432
|
-
<td className="py-1">{c.chargeName}</td>
|
|
1433
|
-
<td className="py-1 uppercase">{c.chargeType}</td>
|
|
1434
|
-
<td className="py-1 text-right font-mono">{fmt(c.unitPrice, currency)}</td>
|
|
1435
|
-
<td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
|
|
1436
|
-
<td className="py-1 text-right font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : c.note ?? '—'}</td>
|
|
1437
|
-
<td className="py-1 text-center">
|
|
1438
|
-
{hasDetail && (
|
|
1439
|
-
<button
|
|
1440
|
-
onClick={() => onTogglePricingDetail(lineIdx, i)}
|
|
1441
|
-
className="inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
|
1442
|
-
title="Show pricing breakdown"
|
|
1443
|
-
>
|
|
1444
|
-
<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">
|
|
1445
|
-
<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" />
|
|
1446
|
-
</svg>
|
|
1447
|
-
</button>
|
|
1448
|
-
)}
|
|
1449
|
-
</td>
|
|
1450
|
-
</tr>
|
|
1451
|
-
{isDetailOpen && (
|
|
1452
|
-
<tr>
|
|
1453
|
-
<td colSpan={6} className="pb-3 pt-1">
|
|
1454
|
-
<div className="rounded border bg-background p-3 space-y-3 text-xs">
|
|
1455
|
-
<div className="flex items-center justify-between">
|
|
1456
|
-
<span className="font-semibold text-sm">Pricing Details — {c.chargeName}</span>
|
|
1457
|
-
<span className="text-muted-foreground">{c.pricingMethod ?? 'flat'}</span>
|
|
1458
|
-
</div>
|
|
1459
|
-
|
|
1460
|
-
{c.breakdown?.tiers && c.breakdown.tiers.length > 0 && (
|
|
1461
|
-
<div>
|
|
1462
|
-
<div className="font-medium text-muted-foreground mb-1">Tier Breakdown</div>
|
|
1463
|
-
<table className="w-full">
|
|
1464
|
-
<thead>
|
|
1465
|
-
<tr className="text-muted-foreground">
|
|
1466
|
-
<th className="text-left py-0.5 font-medium">Tier</th>
|
|
1467
|
-
<th className="text-right py-0.5 font-medium">Range</th>
|
|
1468
|
-
<th className="text-right py-0.5 font-medium">Price/Unit</th>
|
|
1469
|
-
<th className="text-right py-0.5 font-medium">Qty</th>
|
|
1470
|
-
<th className="text-right py-0.5 font-medium">Subtotal</th>
|
|
1471
|
-
</tr>
|
|
1472
|
-
</thead>
|
|
1473
|
-
<tbody>
|
|
1474
|
-
{c.breakdown.tiers.map((t) => (
|
|
1475
|
-
<tr key={t.tier} className="border-t border-border/30">
|
|
1476
|
-
<td className="py-0.5">{t.tier}</td>
|
|
1477
|
-
<td className="py-0.5 text-right font-mono">{t.from}–{t.to}</td>
|
|
1478
|
-
<td className="py-0.5 text-right font-mono">{fmt(t.pricePerUnit, currency)}</td>
|
|
1479
|
-
<td className="py-0.5 text-right font-mono">{t.quantity}</td>
|
|
1480
|
-
<td className="py-0.5 text-right font-mono">{fmt(t.subtotal, currency)}</td>
|
|
1481
|
-
</tr>
|
|
1482
|
-
))}
|
|
1483
|
-
<tr className="border-t font-medium">
|
|
1484
|
-
<td colSpan={4} className="py-0.5 text-right">Tiered Total</td>
|
|
1485
|
-
<td className="py-0.5 text-right font-mono">
|
|
1486
|
-
{fmt(c.breakdown.tiers.reduce((s, t) => s + t.subtotal, 0), currency)}
|
|
1487
|
-
</td>
|
|
1488
|
-
</tr>
|
|
1489
|
-
</tbody>
|
|
1490
|
-
</table>
|
|
1491
|
-
</div>
|
|
1492
|
-
)}
|
|
1493
|
-
|
|
1494
|
-
{c.adjustments && c.adjustments.length > 0 && (
|
|
1495
|
-
<div>
|
|
1496
|
-
<div className="font-medium text-muted-foreground mb-1">Applied Price Rules</div>
|
|
1497
|
-
<table className="w-full">
|
|
1498
|
-
<thead>
|
|
1499
|
-
<tr className="text-muted-foreground">
|
|
1500
|
-
<th className="text-left py-0.5 font-medium">Rule</th>
|
|
1501
|
-
<th className="text-left py-0.5 font-medium">Type</th>
|
|
1502
|
-
<th className="text-right py-0.5 font-medium">Value</th>
|
|
1503
|
-
<th className="text-right py-0.5 font-medium">Before</th>
|
|
1504
|
-
<th className="text-right py-0.5 font-medium">After</th>
|
|
1505
|
-
<th className="text-right py-0.5 font-medium">Delta</th>
|
|
1506
|
-
</tr>
|
|
1507
|
-
</thead>
|
|
1508
|
-
<tbody>
|
|
1509
|
-
{c.adjustments.map((adj, ai) => (
|
|
1510
|
-
<tr key={ai} className="border-t border-border/30">
|
|
1511
|
-
<td className="py-0.5">{adj.ruleName}</td>
|
|
1512
|
-
<td className="py-0.5">{RULE_TYPE_LABELS[adj.ruleType] ?? adj.ruleType}</td>
|
|
1513
|
-
<td className="py-0.5 text-right font-mono">{fmtRuleValue(adj, currency)}</td>
|
|
1514
|
-
<td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceBefore, currency)}</td>
|
|
1515
|
-
<td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceAfter, currency)}</td>
|
|
1516
|
-
<td className={`py-0.5 text-right font-mono ${adj.delta < 0 ? 'text-destructive' : adj.delta > 0 ? 'text-status-success-text' : ''}`}>
|
|
1517
|
-
{adj.delta > 0 ? '+' : ''}{fmt(adj.delta, currency)}
|
|
1518
|
-
</td>
|
|
1519
|
-
</tr>
|
|
1520
|
-
))}
|
|
1521
|
-
</tbody>
|
|
1522
|
-
</table>
|
|
1523
|
-
</div>
|
|
1524
|
-
)}
|
|
1525
|
-
|
|
1526
|
-
<div className="flex items-center justify-between pt-1 border-t text-sm font-semibold">
|
|
1527
|
-
<span>Final Total</span>
|
|
1528
|
-
<span className="font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : '—'}</span>
|
|
1529
|
-
</div>
|
|
1530
|
-
</div>
|
|
1531
|
-
</td>
|
|
1532
|
-
</tr>
|
|
1533
|
-
)}
|
|
1534
|
-
</React.Fragment>
|
|
1535
|
-
)
|
|
1536
|
-
})}
|
|
1537
|
-
</tbody>
|
|
1538
|
-
</table>
|
|
1485
|
+
<ChargeBreakdownTable
|
|
1486
|
+
charges={line.charges}
|
|
1487
|
+
openDetailIndex={pricingDetail?.lineIdx === lineIdx ? pricingDetail.chargeIdx : null}
|
|
1488
|
+
onToggleDetail={(chargeIdx) => onTogglePricingDetail(lineIdx, chargeIdx)}
|
|
1489
|
+
{...tableProps}
|
|
1490
|
+
/>
|
|
1539
1491
|
</div>
|
|
1540
1492
|
)
|
|
1541
1493
|
}
|
|
@@ -1550,6 +1502,7 @@ function Spinner() {
|
|
|
1550
1502
|
}
|
|
1551
1503
|
|
|
1552
1504
|
function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () => void }) {
|
|
1505
|
+
const t = useT()
|
|
1553
1506
|
return (
|
|
1554
1507
|
<Alert variant="destructive" className="flex items-start gap-2">
|
|
1555
1508
|
<span className="flex-1">{message}</span>
|
|
@@ -1557,7 +1510,7 @@ function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () =
|
|
|
1557
1510
|
<button
|
|
1558
1511
|
onClick={onDismiss}
|
|
1559
1512
|
className="text-destructive/70 hover:text-destructive text-sm font-bold"
|
|
1560
|
-
aria-label=
|
|
1513
|
+
aria-label={t('cpq.common.dismiss', 'Dismiss')}
|
|
1561
1514
|
>
|
|
1562
1515
|
×
|
|
1563
1516
|
</button>
|
|
@@ -1569,6 +1522,7 @@ function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () =
|
|
|
1569
1522
|
function OfferingBrowser({ offerings, loading, search, onSearchChange, onSelect }: {
|
|
1570
1523
|
offerings: Offering[]; loading: boolean; search: string; onSearchChange: (v: string) => void; onSelect: (o: Offering) => void
|
|
1571
1524
|
}) {
|
|
1525
|
+
const t = useT()
|
|
1572
1526
|
const filtered = offerings.filter((o) => {
|
|
1573
1527
|
if (!search.trim()) return true
|
|
1574
1528
|
const q = search.toLowerCase()
|
|
@@ -1577,18 +1531,18 @@ function OfferingBrowser({ offerings, loading, search, onSearchChange, onSelect
|
|
|
1577
1531
|
|
|
1578
1532
|
return (
|
|
1579
1533
|
<div className="space-y-3">
|
|
1580
|
-
<h3 className="text-sm font-medium px-1">Select Product Offering</h3>
|
|
1534
|
+
<h3 className="text-sm font-medium px-1">{t('cpq.quotes.offeringBrowser.title', 'Select Product Offering')}</h3>
|
|
1581
1535
|
<div className="relative">
|
|
1582
1536
|
<svg className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
1583
1537
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
1584
1538
|
</svg>
|
|
1585
|
-
<input type="text" placeholder=
|
|
1539
|
+
<input type="text" placeholder={t('cpq.quotes.offeringBrowser.searchPlaceholder', 'Search offerings…')} value={search} onChange={(e) => onSearchChange(e.target.value)}
|
|
1586
1540
|
className="w-full rounded-md border bg-background pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50" />
|
|
1587
1541
|
</div>
|
|
1588
1542
|
{loading ? (
|
|
1589
|
-
<div className="flex items-center gap-2 py-8 justify-center text-sm text-muted-foreground"><Spinner /> Loading offerings
|
|
1543
|
+
<div className="flex items-center gap-2 py-8 justify-center text-sm text-muted-foreground"><Spinner /> {t('cpq.quotes.offeringBrowser.loading', 'Loading offerings…')}</div>
|
|
1590
1544
|
) : filtered.length === 0 ? (
|
|
1591
|
-
<div className="py-8 text-center text-sm text-muted-foreground">{search ? 'No offerings match your search.' : 'No active offerings available.'}</div>
|
|
1545
|
+
<div className="py-8 text-center text-sm text-muted-foreground">{search ? t('cpq.quotes.offeringBrowser.noMatch', 'No offerings match your search.') : t('cpq.quotes.offeringBrowser.empty', 'No active offerings available.')}</div>
|
|
1592
1546
|
) : (
|
|
1593
1547
|
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
1594
1548
|
{filtered.map((offering) => (
|
|
@@ -1613,6 +1567,7 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1613
1567
|
onConfigChange: (key: string, value: unknown) => void; onQuantityChange: (q: number) => void
|
|
1614
1568
|
onSubmit: () => void; submitLabel: string; submitting: boolean
|
|
1615
1569
|
}) {
|
|
1570
|
+
const t = useT()
|
|
1616
1571
|
const noTargetsPicked =
|
|
1617
1572
|
arcTargetOptions !== null && arcTargetOptions.length > 0 && arcSelectedTargetIds.length === 0
|
|
1618
1573
|
return (
|
|
@@ -1622,7 +1577,7 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1622
1577
|
{arcTargetOptions && arcTargetOptions.length > 0 && (
|
|
1623
1578
|
<div>
|
|
1624
1579
|
<label className="block text-sm font-medium mb-1">
|
|
1625
|
-
Apply to subscription(s)
|
|
1580
|
+
{t('cpq.quotes.detail.configure.applyToSubscriptions', 'Apply to subscription(s)')}
|
|
1626
1581
|
<span className="text-destructive ml-1">*</span>
|
|
1627
1582
|
</label>
|
|
1628
1583
|
<div className="space-y-1.5 rounded-md border bg-background px-3 py-2">
|
|
@@ -1646,15 +1601,15 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1646
1601
|
})}
|
|
1647
1602
|
</div>
|
|
1648
1603
|
<p className="text-xs text-muted-foreground mt-1">
|
|
1649
|
-
Pick one or more — a separate line will be created on each selected subscription.
|
|
1604
|
+
{t('cpq.quotes.detail.configure.applyToSubscriptionsHint', 'Pick one or more — a separate line will be created on each selected subscription.')}
|
|
1650
1605
|
</p>
|
|
1651
1606
|
{noTargetsPicked && (
|
|
1652
|
-
<p className="text-xs text-destructive mt-1">Select at least one target
|
|
1607
|
+
<p className="text-xs text-destructive mt-1">{t('cpq.quotes.detail.configure.selectTarget', 'Select at least one target.')}</p>
|
|
1653
1608
|
)}
|
|
1654
1609
|
</div>
|
|
1655
1610
|
)}
|
|
1656
1611
|
<div>
|
|
1657
|
-
<label className="block text-sm font-medium mb-1">Quantity</label>
|
|
1612
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.quotes.detail.configure.quantity', 'Quantity')}</label>
|
|
1658
1613
|
<NumberInput
|
|
1659
1614
|
integer
|
|
1660
1615
|
min={1}
|
|
@@ -1664,7 +1619,7 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1664
1619
|
/>
|
|
1665
1620
|
</div>
|
|
1666
1621
|
{attributes.length === 0 ? (
|
|
1667
|
-
<p className="text-sm text-muted-foreground">No configurable attributes for this offering
|
|
1622
|
+
<p className="text-sm text-muted-foreground">{t('cpq.quotes.detail.configure.noAttributes', 'No configurable attributes for this offering.')}</p>
|
|
1668
1623
|
) : (
|
|
1669
1624
|
attributes.map((attr) => <AttributeField key={attr.code} attribute={attr} value={config[attr.code]} onChange={(v) => onConfigChange(attr.code, v)} />)
|
|
1670
1625
|
)}
|
|
@@ -1680,6 +1635,7 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1680
1635
|
}
|
|
1681
1636
|
|
|
1682
1637
|
function AttributeField({ attribute, value, onChange }: { attribute: ConstrainedAttribute; value: unknown; onChange: (v: unknown) => void }) {
|
|
1638
|
+
const t = useT()
|
|
1683
1639
|
const { code, name, attributeType, isRequired, options, constraints, helpText, defaultValue } = attribute
|
|
1684
1640
|
const currentValue = value ?? defaultValue ?? ''
|
|
1685
1641
|
|
|
@@ -1691,13 +1647,13 @@ function AttributeField({ attribute, value, onChange }: { attribute: Constrained
|
|
|
1691
1647
|
{(attributeType === 'select' || attributeType === 'enum') && options && options.length > 0 ? (
|
|
1692
1648
|
<select value={String(currentValue)} onChange={(e) => onChange(e.target.value)}
|
|
1693
1649
|
className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50">
|
|
1694
|
-
<option value=""
|
|
1650
|
+
<option value="">{t('cpq.quotes.detail.configure.selectPlaceholder', '— Select —')}</option>
|
|
1695
1651
|
{options.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
|
|
1696
1652
|
</select>
|
|
1697
1653
|
) : attributeType === 'boolean' ? (
|
|
1698
1654
|
<label className="flex items-center gap-2">
|
|
1699
1655
|
<input type="checkbox" checked={Boolean(currentValue)} onChange={(e) => onChange(e.target.checked)} className="rounded border-border" />
|
|
1700
|
-
<span className="text-sm text-muted-foreground">{helpText ?? 'Enable'}</span>
|
|
1656
|
+
<span className="text-sm text-muted-foreground">{helpText ?? t('cpq.quotes.detail.configure.enable', 'Enable')}</span>
|
|
1701
1657
|
</label>
|
|
1702
1658
|
) : attributeType === 'number' ? (
|
|
1703
1659
|
<NumberInput
|