@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,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
3
|
+
import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
4
4
|
import { useParams, useRouter } from 'next/navigation'
|
|
5
5
|
import { Alert } from '@open-mercato/ui/primitives/alert'
|
|
6
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
@@ -153,16 +153,47 @@ const EMPTY_CHARGE: EditingCharge = {
|
|
|
153
153
|
|
|
154
154
|
// XD-297: the charge "shape" is two axes — Charge Model (how quantity drives
|
|
155
155
|
// the charge) and Pricing Method (where the unit price comes from).
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
function chargeModelLabel(t: TranslateFn, model: string): string {
|
|
157
|
+
switch (model) {
|
|
158
|
+
case 'flat':
|
|
159
|
+
return t('cpq.offerings.chargeModel.flat', 'Flat Fee')
|
|
160
|
+
case 'per_unit':
|
|
161
|
+
return t('cpq.offerings.chargeModel.perUnit', 'Per Unit')
|
|
162
|
+
case 'volume':
|
|
163
|
+
return t('cpq.offerings.chargeModel.volume', 'Volume-based')
|
|
164
|
+
case 'tiered':
|
|
165
|
+
return t('cpq.offerings.chargeModel.tiered', 'Tiered')
|
|
166
|
+
default:
|
|
167
|
+
return model
|
|
168
|
+
}
|
|
161
169
|
}
|
|
162
170
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
function pricingSourceLabel(t: TranslateFn, source: string): string {
|
|
172
|
+
switch (source) {
|
|
173
|
+
case 'fixed':
|
|
174
|
+
return t('cpq.offerings.pricingMethod.fixed', 'Fixed Price')
|
|
175
|
+
case 'table':
|
|
176
|
+
return t('cpq.offerings.pricingMethod.table', 'Table Lookup')
|
|
177
|
+
default:
|
|
178
|
+
return source
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type ChargeMissingField = 'fixedPrice' | 'currency' | 'quantityAttribute' | 'pricingTable' | 'priceColumn'
|
|
183
|
+
|
|
184
|
+
function chargeMissingFieldLabel(t: TranslateFn, field: ChargeMissingField): string {
|
|
185
|
+
switch (field) {
|
|
186
|
+
case 'fixedPrice':
|
|
187
|
+
return t('cpq.offerings.charge.fields.fixedPrice', 'Fixed Price')
|
|
188
|
+
case 'currency':
|
|
189
|
+
return t('cpq.offerings.charge.fields.currency', 'Currency')
|
|
190
|
+
case 'quantityAttribute':
|
|
191
|
+
return t('cpq.offerings.charge.fields.quantityAttribute', 'Quantity Attribute')
|
|
192
|
+
case 'pricingTable':
|
|
193
|
+
return t('cpq.offerings.charge.fields.pricingTable', 'Pricing Table')
|
|
194
|
+
case 'priceColumn':
|
|
195
|
+
return t('cpq.offerings.charge.fields.priceColumn', 'Price Column')
|
|
196
|
+
}
|
|
166
197
|
}
|
|
167
198
|
|
|
168
199
|
// V-CHG-1 in `validators.ts`: mirror the (chargeModel × pricingMethod) rules in
|
|
@@ -181,17 +212,17 @@ function isChargeComplete(charge: ChargeShape): boolean {
|
|
|
181
212
|
return chargeMissingFields(charge).length === 0
|
|
182
213
|
}
|
|
183
214
|
|
|
184
|
-
function chargeMissingFields(charge: ChargeShape):
|
|
215
|
+
function chargeMissingFields(charge: ChargeShape): ChargeMissingField[] {
|
|
185
216
|
const { model, source } = normalizeChargePricing(charge)
|
|
186
|
-
const missing:
|
|
217
|
+
const missing: ChargeMissingField[] = []
|
|
187
218
|
if (source === 'fixed') {
|
|
188
|
-
if (!charge.fixedPrice) missing.push('
|
|
189
|
-
if (!charge.currencyCode) missing.push('
|
|
190
|
-
if (model === 'per_unit' && !charge.quantityAttributeCode) missing.push('
|
|
219
|
+
if (!charge.fixedPrice) missing.push('fixedPrice')
|
|
220
|
+
if (!charge.currencyCode) missing.push('currency')
|
|
221
|
+
if (model === 'per_unit' && !charge.quantityAttributeCode) missing.push('quantityAttribute')
|
|
191
222
|
} else {
|
|
192
|
-
if (!charge.pricingTableId) missing.push('
|
|
193
|
-
if (!charge.priceColumnKey) missing.push('
|
|
194
|
-
if (model !== 'flat' && !charge.quantityAttributeCode) missing.push('
|
|
223
|
+
if (!charge.pricingTableId) missing.push('pricingTable')
|
|
224
|
+
if (!charge.priceColumnKey) missing.push('priceColumn')
|
|
225
|
+
if (model !== 'flat' && !charge.quantityAttributeCode) missing.push('quantityAttribute')
|
|
195
226
|
}
|
|
196
227
|
return missing
|
|
197
228
|
}
|
|
@@ -251,7 +282,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
251
282
|
// Load offering detail
|
|
252
283
|
const res = await fetch(`/api/cpq/product-offerings?id=${encodeURIComponent(offeringId)}`)
|
|
253
284
|
if (!res.ok) {
|
|
254
|
-
if (!cancelled) setError('Failed to load offering')
|
|
285
|
+
if (!cancelled) setError(t('cpq.offerings.detail.error.load', 'Failed to load offering'))
|
|
255
286
|
return
|
|
256
287
|
}
|
|
257
288
|
|
|
@@ -301,7 +332,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
301
332
|
}
|
|
302
333
|
} catch (err) {
|
|
303
334
|
console.error('Failed to load', err)
|
|
304
|
-
if (!cancelled) setError('Failed to load')
|
|
335
|
+
if (!cancelled) setError(t('cpq.offerings.detail.error.loadGeneric', 'Failed to load'))
|
|
305
336
|
} finally {
|
|
306
337
|
if (!cancelled) setLoading(false)
|
|
307
338
|
}
|
|
@@ -343,7 +374,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
343
374
|
|
|
344
375
|
if (!res.ok) {
|
|
345
376
|
const d = await res.json()
|
|
346
|
-
setError(d.error ?? 'Failed to save')
|
|
377
|
+
setError(d.error ?? t('cpq.offerings.detail.error.save', 'Failed to save'))
|
|
347
378
|
return
|
|
348
379
|
}
|
|
349
380
|
|
|
@@ -369,7 +400,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
369
400
|
setGeneralMode('view')
|
|
370
401
|
}
|
|
371
402
|
} catch {
|
|
372
|
-
setError('Failed to save')
|
|
403
|
+
setError(t('cpq.offerings.detail.error.save', 'Failed to save'))
|
|
373
404
|
} finally {
|
|
374
405
|
setSaving(false)
|
|
375
406
|
}
|
|
@@ -387,10 +418,10 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
387
418
|
router.push('/backend/cpq/offerings')
|
|
388
419
|
} else {
|
|
389
420
|
const d = await res.json()
|
|
390
|
-
setError(d.error ?? 'Failed to delete')
|
|
421
|
+
setError(d.error ?? t('cpq.offerings.detail.error.delete', 'Failed to delete'))
|
|
391
422
|
}
|
|
392
423
|
} catch {
|
|
393
|
-
setError('Failed to delete')
|
|
424
|
+
setError(t('cpq.offerings.detail.error.delete', 'Failed to delete'))
|
|
394
425
|
}
|
|
395
426
|
}
|
|
396
427
|
|
|
@@ -430,7 +461,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
430
461
|
})
|
|
431
462
|
if (!res.ok) {
|
|
432
463
|
const d = await res.json()
|
|
433
|
-
setError(d.error ?? 'Failed to save charge')
|
|
464
|
+
setError(d.error ?? t('cpq.offerings.detail.error.saveCharge', 'Failed to save charge'))
|
|
434
465
|
return
|
|
435
466
|
}
|
|
436
467
|
// Reload offering to get updated charges
|
|
@@ -441,14 +472,14 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
441
472
|
}
|
|
442
473
|
setEditingCharge(null)
|
|
443
474
|
} catch {
|
|
444
|
-
setError('Failed to save charge')
|
|
475
|
+
setError(t('cpq.offerings.detail.error.saveCharge', 'Failed to save charge'))
|
|
445
476
|
} finally {
|
|
446
477
|
setSaving(false)
|
|
447
478
|
}
|
|
448
479
|
}
|
|
449
480
|
|
|
450
481
|
const deleteCharge = async (chargeId: string) => {
|
|
451
|
-
if (!confirm('Delete this charge?')) return
|
|
482
|
+
if (!confirm(t('cpq.offerings.detail.confirmDeleteCharge', 'Delete this charge?'))) return
|
|
452
483
|
try {
|
|
453
484
|
const res = await fetch('/api/cpq/product-charges', {
|
|
454
485
|
method: 'DELETE',
|
|
@@ -457,7 +488,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
457
488
|
})
|
|
458
489
|
if (res.ok) setCharges((prev) => prev.filter((c) => c.id !== chargeId))
|
|
459
490
|
} catch {
|
|
460
|
-
setError('Failed to delete charge')
|
|
491
|
+
setError(t('cpq.offerings.detail.error.deleteCharge', 'Failed to delete charge'))
|
|
461
492
|
}
|
|
462
493
|
}
|
|
463
494
|
|
|
@@ -472,7 +503,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
472
503
|
// ─── Render ─────────────────────────────────────────────────
|
|
473
504
|
|
|
474
505
|
if (loading) {
|
|
475
|
-
return <div className="text-sm text-muted-foreground">{t('common.loading', 'Loading...')}</div>
|
|
506
|
+
return <div className="text-sm text-muted-foreground">{t('cpq.common.loading', 'Loading...')}</div>
|
|
476
507
|
}
|
|
477
508
|
|
|
478
509
|
return (
|
|
@@ -484,7 +515,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
484
515
|
onClick={() => router.push('/backend/cpq/offerings')}
|
|
485
516
|
className="text-sm text-muted-foreground hover:text-foreground"
|
|
486
517
|
>
|
|
487
|
-
← {t('common.back', 'Back')}
|
|
518
|
+
← {t('cpq.common.back', 'Back')}
|
|
488
519
|
</button>
|
|
489
520
|
<h1 className="text-2xl font-bold">
|
|
490
521
|
{isNew ? t('cpq.offerings.new', 'New Offering') : form.name}
|
|
@@ -496,7 +527,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
496
527
|
)}
|
|
497
528
|
{specDetail && (
|
|
498
529
|
<span className="text-sm text-muted-foreground">
|
|
499
|
-
Spec: <button onClick={() => router.push(`/backend/cpq/specifications/${specDetail.id}`)} className="text-primary hover:underline">{specDetail.name}</button>
|
|
530
|
+
{t('cpq.offerings.detail.specLabel', 'Spec:')} <button onClick={() => router.push(`/backend/cpq/specifications/${specDetail.id}`)} className="text-primary hover:underline">{specDetail.name}</button>
|
|
500
531
|
</span>
|
|
501
532
|
)}
|
|
502
533
|
</div>
|
|
@@ -511,11 +542,11 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
511
542
|
setGeneralMode('edit')
|
|
512
543
|
}}
|
|
513
544
|
>
|
|
514
|
-
{t('common.edit', 'Edit')}
|
|
545
|
+
{t('cpq.common.edit', 'Edit')}
|
|
515
546
|
</Button>
|
|
516
547
|
)}
|
|
517
548
|
<Button type="button" variant="destructive" onClick={deleteOffering}>
|
|
518
|
-
{t('common.delete', 'Delete')}
|
|
549
|
+
{t('cpq.common.delete', 'Delete')}
|
|
519
550
|
</Button>
|
|
520
551
|
</div>
|
|
521
552
|
)}
|
|
@@ -540,9 +571,9 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
540
571
|
tab === t_ ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground'
|
|
541
572
|
}`}
|
|
542
573
|
>
|
|
543
|
-
{t_ === 'general' && 'General'}
|
|
544
|
-
{t_ === 'charges' &&
|
|
545
|
-
{t_ === 'components' &&
|
|
574
|
+
{t_ === 'general' && t('cpq.offerings.detail.tabs.general', 'General')}
|
|
575
|
+
{t_ === 'charges' && t('cpq.offerings.detail.tabs.charges', 'Charges ({count})', { count: charges.length })}
|
|
576
|
+
{t_ === 'components' && t('cpq.offerings.detail.tabs.components', 'Components ({count})', { count: components.length })}
|
|
546
577
|
</button>
|
|
547
578
|
))}
|
|
548
579
|
</div>
|
|
@@ -553,7 +584,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
553
584
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
554
585
|
{isNew && (
|
|
555
586
|
<div>
|
|
556
|
-
<label className="block text-sm font-medium mb-1">Specification</label>
|
|
587
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.offerings.detail.specification', 'Specification')}</label>
|
|
557
588
|
<select
|
|
558
589
|
value={form.specId}
|
|
559
590
|
onChange={(e) => {
|
|
@@ -563,7 +594,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
563
594
|
}}
|
|
564
595
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
565
596
|
>
|
|
566
|
-
<option value="">Select a specification
|
|
597
|
+
<option value="">{t('cpq.offerings.detail.selectSpecification', 'Select a specification...')}</option>
|
|
567
598
|
{specs.map((s) => (
|
|
568
599
|
<option key={s.id} value={s.id}>{s.name} ({s.code})</option>
|
|
569
600
|
))}
|
|
@@ -581,7 +612,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
581
612
|
type="text"
|
|
582
613
|
value={form.code}
|
|
583
614
|
onChange={(e) => setForm({ ...form, code: e.target.value })}
|
|
584
|
-
placeholder=
|
|
615
|
+
placeholder={t('cpq.offerings.detail.codePlaceholder', 'e.g. gix-cloud-connect-aws')}
|
|
585
616
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
586
617
|
/>
|
|
587
618
|
)}
|
|
@@ -613,9 +644,9 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
613
644
|
onChange={(e) => setForm({ ...form, lifecycleStatus: e.target.value })}
|
|
614
645
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
615
646
|
>
|
|
616
|
-
<option value="draft">Draft</option>
|
|
617
|
-
<option value="active">Active</option>
|
|
618
|
-
<option value="deprecated">Deprecated</option>
|
|
647
|
+
<option value="draft">{t('cpq.offerings.lifecycle.draft', 'Draft')}</option>
|
|
648
|
+
<option value="active">{t('cpq.offerings.lifecycle.active', 'Active')}</option>
|
|
649
|
+
<option value="deprecated">{t('cpq.offerings.lifecycle.deprecated', 'Deprecated')}</option>
|
|
619
650
|
</select>
|
|
620
651
|
)}
|
|
621
652
|
</div>
|
|
@@ -624,7 +655,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
624
655
|
<div className="flex items-center h-[38px]">
|
|
625
656
|
{generalMode === 'view' && !isNew ? (
|
|
626
657
|
<Tag variant={form.isActive ? 'success' : 'neutral'} dot>
|
|
627
|
-
{form.isActive ? t('common.yes', 'Yes') : t('common.no', 'No')}
|
|
658
|
+
{form.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
|
|
628
659
|
</Tag>
|
|
629
660
|
) : (
|
|
630
661
|
<input
|
|
@@ -654,7 +685,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
654
685
|
{/* Design-time values */}
|
|
655
686
|
{designTimeAttrs.length > 0 && (
|
|
656
687
|
<div className="space-y-3">
|
|
657
|
-
<label className="block text-sm font-medium">Design-Time Values</label>
|
|
688
|
+
<label className="block text-sm font-medium">{t('cpq.offerings.detail.designTimeValues', 'Design-Time Values')}</label>
|
|
658
689
|
<div className="grid grid-cols-2 gap-3">
|
|
659
690
|
{designTimeAttrs.map((attr) => {
|
|
660
691
|
const value = designTimeValues[attr.code]
|
|
@@ -677,7 +708,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
677
708
|
<span className="text-muted-foreground">—</span>
|
|
678
709
|
) : attr.attributeType === 'boolean' ? (
|
|
679
710
|
<Tag variant={value ? 'success' : 'neutral'} dot>
|
|
680
|
-
{value ? t('common.yes', 'Yes') : t('common.no', 'No')}
|
|
711
|
+
{value ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
|
|
681
712
|
</Tag>
|
|
682
713
|
) : (
|
|
683
714
|
optionLabel ?? String(value)
|
|
@@ -689,7 +720,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
689
720
|
onChange={(e) => setDesignTimeValues({ ...designTimeValues, [attr.code]: e.target.value || null })}
|
|
690
721
|
className="w-full rounded-md border px-2 py-1.5 text-sm"
|
|
691
722
|
>
|
|
692
|
-
<option value="">Select {attr.name}
|
|
723
|
+
<option value="">{t('cpq.offerings.detail.selectAttribute', 'Select {name}...', { name: attr.name })}</option>
|
|
693
724
|
{attr.options.map((opt) => (
|
|
694
725
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
695
726
|
))}
|
|
@@ -738,7 +769,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
738
769
|
onClick={saveOffering}
|
|
739
770
|
disabled={saving || !form.code || !form.name || (isNew && !form.specId)}
|
|
740
771
|
>
|
|
741
|
-
{saving ? t('common.saving', 'Saving...') : t('common.save', 'Save')}
|
|
772
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
|
|
742
773
|
</Button>
|
|
743
774
|
<Button
|
|
744
775
|
type="button"
|
|
@@ -755,7 +786,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
755
786
|
setGeneralMode('view')
|
|
756
787
|
}}
|
|
757
788
|
>
|
|
758
|
-
{t('common.cancel', 'Cancel')}
|
|
789
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
759
790
|
</Button>
|
|
760
791
|
</div>
|
|
761
792
|
)}
|
|
@@ -778,27 +809,27 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
778
809
|
{editingCharge && (
|
|
779
810
|
<div className="rounded-lg border bg-card p-4 space-y-3">
|
|
780
811
|
<h3 className="font-medium text-sm">
|
|
781
|
-
{editingCharge.id ? 'Edit Charge' : 'New Charge'}
|
|
812
|
+
{editingCharge.id ? t('cpq.offerings.charge.form.editTitle', 'Edit Charge') : t('cpq.offerings.charge.form.newTitle', 'New Charge')}
|
|
782
813
|
</h3>
|
|
783
814
|
<div className="grid grid-cols-3 gap-3">
|
|
784
815
|
<div>
|
|
785
|
-
<label className="block text-xs font-medium mb-1">Code</label>
|
|
786
|
-
<input type="text" value={editingCharge.code} onChange={(e) => setEditingCharge({ ...editingCharge, code: e.target.value })} placeholder=
|
|
816
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.code', 'Code')}</label>
|
|
817
|
+
<input type="text" value={editingCharge.code} onChange={(e) => setEditingCharge({ ...editingCharge, code: e.target.value })} placeholder={t('cpq.offerings.charge.fields.codePlaceholder', 'e.g. mrc-monthly')} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
787
818
|
</div>
|
|
788
819
|
<div>
|
|
789
|
-
<label className="block text-xs font-medium mb-1">Name</label>
|
|
820
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.name', 'Name')}</label>
|
|
790
821
|
<input type="text" value={editingCharge.name} onChange={(e) => setEditingCharge({ ...editingCharge, name: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
791
822
|
</div>
|
|
792
823
|
<div>
|
|
793
|
-
<label className="block text-xs font-medium mb-1">Charge Type</label>
|
|
824
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.chargeType', 'Charge Type')}</label>
|
|
794
825
|
<select value={editingCharge.chargeType} onChange={(e) => setEditingCharge({ ...editingCharge, chargeType: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
|
|
795
|
-
<option value="mrc">MRC (Monthly Recurring)</option>
|
|
796
|
-
<option value="nrc">NRC (Non-Recurring)</option>
|
|
797
|
-
<option value="usage">Usage</option>
|
|
826
|
+
<option value="mrc">{t('cpq.offerings.chargeType.mrc', 'MRC (Monthly Recurring)')}</option>
|
|
827
|
+
<option value="nrc">{t('cpq.offerings.chargeType.nrc', 'NRC (Non-Recurring)')}</option>
|
|
828
|
+
<option value="usage">{t('cpq.offerings.chargeType.usage', 'Usage')}</option>
|
|
798
829
|
</select>
|
|
799
830
|
</div>
|
|
800
831
|
<div>
|
|
801
|
-
<label className="block text-xs font-medium mb-1">Charge Model</label>
|
|
832
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.chargeModel', 'Charge Model')}</label>
|
|
802
833
|
<select
|
|
803
834
|
value={editingCharge.chargeModel ?? 'flat'}
|
|
804
835
|
onChange={(e) => {
|
|
@@ -815,14 +846,14 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
815
846
|
}}
|
|
816
847
|
className="w-full rounded-md border px-2 py-1.5 text-sm"
|
|
817
848
|
>
|
|
818
|
-
<option value="flat">
|
|
819
|
-
<option value="per_unit">
|
|
820
|
-
<option value="volume">
|
|
821
|
-
<option value="tiered">
|
|
849
|
+
<option value="flat">{chargeModelLabel(t, 'flat')}</option>
|
|
850
|
+
<option value="per_unit">{chargeModelLabel(t, 'per_unit')}</option>
|
|
851
|
+
<option value="volume">{chargeModelLabel(t, 'volume')}</option>
|
|
852
|
+
<option value="tiered">{chargeModelLabel(t, 'tiered')}</option>
|
|
822
853
|
</select>
|
|
823
854
|
</div>
|
|
824
855
|
<div>
|
|
825
|
-
<label className="block text-xs font-medium mb-1">Pricing Method</label>
|
|
856
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.pricingMethod', 'Pricing Method')}</label>
|
|
826
857
|
<select
|
|
827
858
|
value={editingCharge.pricingMethod}
|
|
828
859
|
onChange={(e) => {
|
|
@@ -840,44 +871,44 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
840
871
|
>
|
|
841
872
|
{/* Volume / tiered are graduated over table ranges — a fixed price isn't applicable. */}
|
|
842
873
|
{editingCharge.chargeModel !== 'volume' && editingCharge.chargeModel !== 'tiered' && (
|
|
843
|
-
<option value="fixed">
|
|
874
|
+
<option value="fixed">{pricingSourceLabel(t, 'fixed')}</option>
|
|
844
875
|
)}
|
|
845
|
-
<option value="table">
|
|
876
|
+
<option value="table">{pricingSourceLabel(t, 'table')}</option>
|
|
846
877
|
</select>
|
|
847
878
|
</div>
|
|
848
879
|
{editingCharge.pricingMethod === 'fixed' ? (
|
|
849
880
|
<>
|
|
850
881
|
<div>
|
|
851
|
-
<label className="block text-xs font-medium mb-1">
|
|
882
|
+
<label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'fixedPrice')} <span className="text-destructive">*</span></label>
|
|
852
883
|
<input type="text" value={editingCharge.fixedPrice ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, fixedPrice: e.target.value || null })} placeholder="0.00" className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
853
884
|
</div>
|
|
854
885
|
<div>
|
|
855
|
-
<label className="block text-xs font-medium mb-1">
|
|
886
|
+
<label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'currency')} <span className="text-destructive">*</span></label>
|
|
856
887
|
<input type="text" value={editingCharge.currencyCode ?? 'USD'} onChange={(e) => setEditingCharge({ ...editingCharge, currencyCode: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
857
888
|
</div>
|
|
858
889
|
</>
|
|
859
890
|
) : (
|
|
860
891
|
<>
|
|
861
892
|
<div>
|
|
862
|
-
<label className="block text-xs font-medium mb-1">
|
|
893
|
+
<label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'pricingTable')} <span className="text-destructive">*</span></label>
|
|
863
894
|
<select value={editingCharge.pricingTableId ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, pricingTableId: e.target.value || null, priceColumnKey: null })} className="w-full rounded-md border px-2 py-1.5 text-sm">
|
|
864
|
-
<option value="">Select table
|
|
895
|
+
<option value="">{t('cpq.offerings.charge.fields.selectTable', 'Select table...')}</option>
|
|
865
896
|
{pricingTables.map((pt) => (
|
|
866
897
|
<option key={pt.id} value={pt.id}>{pt.name} ({pt.code})</option>
|
|
867
898
|
))}
|
|
868
899
|
</select>
|
|
869
900
|
{pricingTables.length === 0 && (
|
|
870
901
|
<p className="text-xs text-warning mt-0.5">
|
|
871
|
-
No pricing tables yet —{' '}
|
|
872
|
-
<a href="/backend/cpq/pricing-tables" className="text-primary hover:underline">create one</a>{' '}
|
|
873
|
-
before using Table Lookup pricing.
|
|
902
|
+
{t('cpq.offerings.charge.noPricingTables.prefix', 'No pricing tables yet —')}{' '}
|
|
903
|
+
<a href="/backend/cpq/pricing-tables" className="text-primary hover:underline">{t('cpq.offerings.charge.noPricingTables.link', 'create one')}</a>{' '}
|
|
904
|
+
{t('cpq.offerings.charge.noPricingTables.suffix', 'before using Table Lookup pricing.')}
|
|
874
905
|
</p>
|
|
875
906
|
)}
|
|
876
907
|
</div>
|
|
877
908
|
<div>
|
|
878
|
-
<label className="block text-xs font-medium mb-1">
|
|
909
|
+
<label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'priceColumn')} <span className="text-destructive">*</span></label>
|
|
879
910
|
<select value={editingCharge.priceColumnKey ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, priceColumnKey: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" disabled={!selectedPricingTable}>
|
|
880
|
-
<option value="">Select column
|
|
911
|
+
<option value="">{t('cpq.offerings.charge.fields.selectColumn', 'Select column...')}</option>
|
|
881
912
|
{selectedPricingTable?.priceColumns.map((col) => (
|
|
882
913
|
<option key={col.key} value={col.key}>{col.label}</option>
|
|
883
914
|
))}
|
|
@@ -890,27 +921,27 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
890
921
|
{((editingCharge.pricingMethod === 'table' && editingCharge.chargeModel !== 'flat') ||
|
|
891
922
|
(editingCharge.pricingMethod === 'fixed' && editingCharge.chargeModel === 'per_unit')) && (
|
|
892
923
|
<div>
|
|
893
|
-
<label className="block text-xs font-medium mb-1">
|
|
924
|
+
<label className="block text-xs font-medium mb-1">{chargeMissingFieldLabel(t, 'quantityAttribute')} <span className="text-destructive">*</span></label>
|
|
894
925
|
<select value={editingCharge.quantityAttributeCode ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, quantityAttributeCode: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm">
|
|
895
|
-
<option value="">Select attribute
|
|
926
|
+
<option value="">{t('cpq.offerings.charge.fields.selectAttribute', 'Select attribute...')}</option>
|
|
896
927
|
{attributes.filter((a) => a.attributeType === 'number').map((a) => (
|
|
897
928
|
<option key={a.code} value={a.code}>{a.name} ({a.code})</option>
|
|
898
929
|
))}
|
|
899
930
|
</select>
|
|
900
|
-
<p className="text-xs text-muted-foreground mt-0.5">Attribute whose value drives the quantity for pricing</p>
|
|
931
|
+
<p className="text-xs text-muted-foreground mt-0.5">{t('cpq.offerings.charge.fields.quantityAttributeHelp', 'Attribute whose value drives the quantity for pricing')}</p>
|
|
901
932
|
{attributes.filter((a) => a.attributeType === 'number').length === 0 && form.specId && (
|
|
902
933
|
<p className="text-xs text-warning mt-0.5">
|
|
903
|
-
Spec has no number attributes —{' '}
|
|
934
|
+
{t('cpq.offerings.charge.noNumberAttributes.prefix', 'Spec has no number attributes —')}{' '}
|
|
904
935
|
<a href={`/backend/cpq/specifications/${form.specId}`} className="text-primary hover:underline">
|
|
905
|
-
add one to the spec
|
|
936
|
+
{t('cpq.offerings.charge.noNumberAttributes.link', 'add one to the spec')}
|
|
906
937
|
</a>{' '}
|
|
907
|
-
first.
|
|
938
|
+
{t('cpq.offerings.charge.noNumberAttributes.suffix', 'first.')}
|
|
908
939
|
</p>
|
|
909
940
|
)}
|
|
910
941
|
</div>
|
|
911
942
|
)}
|
|
912
943
|
<div>
|
|
913
|
-
<label className="block text-xs font-medium mb-1">Sort Order</label>
|
|
944
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.sortOrder', 'Sort Order')}</label>
|
|
914
945
|
<NumberInput
|
|
915
946
|
integer
|
|
916
947
|
value={editingCharge.sortOrder}
|
|
@@ -919,13 +950,13 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
919
950
|
</div>
|
|
920
951
|
</div>
|
|
921
952
|
<div>
|
|
922
|
-
<label className="block text-xs font-medium mb-1">Description</label>
|
|
953
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.charge.fields.description', 'Description')}</label>
|
|
923
954
|
<input type="text" value={editingCharge.description ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, description: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
924
955
|
</div>
|
|
925
956
|
<div className="flex items-end gap-4">
|
|
926
957
|
<label className="flex items-center gap-1.5 text-sm">
|
|
927
958
|
<input type="checkbox" checked={editingCharge.isActive} onChange={(e) => setEditingCharge({ ...editingCharge, isActive: e.target.checked })} className="rounded border" />
|
|
928
|
-
Active
|
|
959
|
+
{t('cpq.common.active', 'Active')}
|
|
929
960
|
</label>
|
|
930
961
|
</div>
|
|
931
962
|
{(() => {
|
|
@@ -935,14 +966,22 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
935
966
|
<>
|
|
936
967
|
{missing.length > 0 && (
|
|
937
968
|
<Alert variant="destructive">
|
|
938
|
-
|
|
969
|
+
{missing.length > 1
|
|
970
|
+
? t('cpq.offerings.charge.missingFieldsPlural', 'Missing required fields for {method} pricing: {fields}', {
|
|
971
|
+
method: editingCharge.pricingMethod,
|
|
972
|
+
fields: missing.map((field) => chargeMissingFieldLabel(t, field)).join(', '),
|
|
973
|
+
})
|
|
974
|
+
: t('cpq.offerings.charge.missingFieldsSingular', 'Missing required field for {method} pricing: {fields}', {
|
|
975
|
+
method: editingCharge.pricingMethod,
|
|
976
|
+
fields: missing.map((field) => chargeMissingFieldLabel(t, field)).join(', '),
|
|
977
|
+
})}
|
|
939
978
|
</Alert>
|
|
940
979
|
)}
|
|
941
980
|
<div className="flex gap-2">
|
|
942
981
|
<button onClick={saveCharge} disabled={blocked} className="inline-flex items-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
943
|
-
{saving ? 'Saving...' : 'Save'}
|
|
982
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
|
|
944
983
|
</button>
|
|
945
|
-
<button onClick={() => setEditingCharge(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
|
|
984
|
+
<button onClick={() => setEditingCharge(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">{t('cpq.common.cancel', 'Cancel')}</button>
|
|
946
985
|
</div>
|
|
947
986
|
</>
|
|
948
987
|
)
|
|
@@ -953,20 +992,20 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
953
992
|
{/* Charges table */}
|
|
954
993
|
{charges.length === 0 ? (
|
|
955
994
|
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
956
|
-
No charges defined. Add recurring, non-recurring, or usage charges to this offering.
|
|
995
|
+
{t('cpq.offerings.charges.empty', 'No charges defined. Add recurring, non-recurring, or usage charges to this offering.')}
|
|
957
996
|
</div>
|
|
958
997
|
) : (
|
|
959
998
|
<div className="rounded-lg border">
|
|
960
999
|
<table className="w-full text-sm">
|
|
961
1000
|
<thead>
|
|
962
1001
|
<tr className="border-b bg-muted/50">
|
|
963
|
-
<th className="px-4 py-3 text-left font-medium">Code</th>
|
|
964
|
-
<th className="px-4 py-3 text-left font-medium">Name</th>
|
|
965
|
-
<th className="px-4 py-3 text-left font-medium">Type</th>
|
|
966
|
-
<th className="px-4 py-3 text-left font-medium">Pricing</th>
|
|
967
|
-
<th className="px-4 py-3 text-left font-medium">Price</th>
|
|
968
|
-
<th className="px-4 py-3 text-left font-medium">Active</th>
|
|
969
|
-
<th className="px-4 py-3 text-right font-medium">Actions</th>
|
|
1002
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.code', 'Code')}</th>
|
|
1003
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.name', 'Name')}</th>
|
|
1004
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.type', 'Type')}</th>
|
|
1005
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.pricing', 'Pricing')}</th>
|
|
1006
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.price', 'Price')}</th>
|
|
1007
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.offerings.charges.columns.active', 'Active')}</th>
|
|
1008
|
+
<th className="px-4 py-3 text-right font-medium">{t('cpq.offerings.charges.columns.actions', 'Actions')}</th>
|
|
970
1009
|
</tr>
|
|
971
1010
|
</thead>
|
|
972
1011
|
<tbody>
|
|
@@ -985,7 +1024,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
985
1024
|
<td className="px-4 py-3">
|
|
986
1025
|
{charge.name}
|
|
987
1026
|
{incomplete && (
|
|
988
|
-
<Tag variant="error" className="ml-2 px-2 text-[10px]" dot>Incomplete</Tag>
|
|
1027
|
+
<Tag variant="error" className="ml-2 px-2 text-[10px]" dot>{t('cpq.offerings.charges.incomplete', 'Incomplete')}</Tag>
|
|
989
1028
|
)}
|
|
990
1029
|
</td>
|
|
991
1030
|
<td className="px-4 py-3">
|
|
@@ -994,8 +1033,8 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
994
1033
|
</Tag>
|
|
995
1034
|
</td>
|
|
996
1035
|
<td className="px-4 py-3 text-xs">
|
|
997
|
-
{
|
|
998
|
-
<span className="text-muted-foreground"> · {
|
|
1036
|
+
{chargeModelLabel(t, model)}
|
|
1037
|
+
<span className="text-muted-foreground"> · {pricingSourceLabel(t, source)}</span>
|
|
999
1038
|
</td>
|
|
1000
1039
|
<td className="px-4 py-3 text-xs">
|
|
1001
1040
|
{source === 'fixed' ? (
|
|
@@ -1005,30 +1044,30 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1005
1044
|
: <span className="text-muted-foreground">—</span>}
|
|
1006
1045
|
{model === 'per_unit' && (
|
|
1007
1046
|
<span>
|
|
1008
|
-
<span className="text-muted-foreground"
|
|
1009
|
-
{qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">missing</span>}
|
|
1047
|
+
<span className="text-muted-foreground">{t('cpq.offerings.charges.qtyLabel', '× Qty:')}</span>{' '}
|
|
1048
|
+
{qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">{t('cpq.offerings.charges.missing', 'missing')}</span>}
|
|
1010
1049
|
</span>
|
|
1011
1050
|
)}
|
|
1012
1051
|
</div>
|
|
1013
1052
|
) : (
|
|
1014
1053
|
<div className="flex flex-col gap-0.5">
|
|
1015
1054
|
<span>
|
|
1016
|
-
<span className="text-muted-foreground">Table
|
|
1017
|
-
{tableRef ? `${tableRef.name} → ${charge.priceColumnKey ?? '?'}` : <span className="text-destructive">missing</span>}
|
|
1055
|
+
<span className="text-muted-foreground">{t('cpq.offerings.charges.tableLabel', 'Table:')}</span>{' '}
|
|
1056
|
+
{tableRef ? `${tableRef.name} → ${charge.priceColumnKey ?? '?'}` : <span className="text-destructive">{t('cpq.offerings.charges.missing', 'missing')}</span>}
|
|
1018
1057
|
</span>
|
|
1019
1058
|
{model !== 'flat' && (
|
|
1020
1059
|
<span>
|
|
1021
|
-
<span className="text-muted-foreground"
|
|
1022
|
-
{qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">missing</span>}
|
|
1060
|
+
<span className="text-muted-foreground">{t('cpq.offerings.charges.qtyLabel', '× Qty:')}</span>{' '}
|
|
1061
|
+
{qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">{t('cpq.offerings.charges.missing', 'missing')}</span>}
|
|
1023
1062
|
</span>
|
|
1024
1063
|
)}
|
|
1025
1064
|
</div>
|
|
1026
1065
|
)}
|
|
1027
1066
|
</td>
|
|
1028
|
-
<td className="px-4 py-3">{charge.isActive ? 'Yes' : 'No'}</td>
|
|
1067
|
+
<td className="px-4 py-3">{charge.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}</td>
|
|
1029
1068
|
<td className="px-4 py-3 text-right">
|
|
1030
|
-
<button onClick={() => startEditCharge(charge)} className="text-xs text-primary hover:underline mr-2">Edit</button>
|
|
1031
|
-
<button onClick={() => deleteCharge(charge.id)} className="text-xs text-destructive hover:underline">Delete</button>
|
|
1069
|
+
<button onClick={() => startEditCharge(charge)} className="text-xs text-primary hover:underline mr-2">{t('cpq.common.edit', 'Edit')}</button>
|
|
1070
|
+
<button onClick={() => deleteCharge(charge.id)} className="text-xs text-destructive hover:underline">{t('cpq.common.delete', 'Delete')}</button>
|
|
1032
1071
|
</td>
|
|
1033
1072
|
</tr>
|
|
1034
1073
|
)
|
|
@@ -1045,7 +1084,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1045
1084
|
<div className="space-y-4">
|
|
1046
1085
|
{bundleSlots.length === 0 ? (
|
|
1047
1086
|
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
1048
|
-
No bundle slots defined on this specification. Add slots on the specification first.
|
|
1087
|
+
{t('cpq.offerings.components.noSlots', 'No bundle slots defined on this specification. Add slots on the specification first.')}
|
|
1049
1088
|
</div>
|
|
1050
1089
|
) : (
|
|
1051
1090
|
<>
|
|
@@ -1080,7 +1119,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1080
1119
|
}}
|
|
1081
1120
|
className="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
|
1082
1121
|
>
|
|
1083
|
-
+ Add Component
|
|
1122
|
+
{t('cpq.offerings.components.add', '+ Add Component')}
|
|
1084
1123
|
</button>
|
|
1085
1124
|
</div>
|
|
1086
1125
|
|
|
@@ -1088,20 +1127,20 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1088
1127
|
<div className="p-4 border-b bg-muted/20 space-y-3">
|
|
1089
1128
|
<div className="grid grid-cols-3 gap-3">
|
|
1090
1129
|
<div className="col-span-2">
|
|
1091
|
-
<label className="block text-xs font-medium mb-1">Component Offering</label>
|
|
1130
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.components.fields.componentOffering', 'Component Offering')}</label>
|
|
1092
1131
|
<select
|
|
1093
1132
|
value={addingComponent.childOfferingId}
|
|
1094
1133
|
onChange={(e) => setAddingComponent({ ...addingComponent, childOfferingId: e.target.value })}
|
|
1095
1134
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
1096
1135
|
>
|
|
1097
|
-
<option value=""
|
|
1136
|
+
<option value="">{t('cpq.offerings.components.fields.selectOffering', '— Select offering —')}</option>
|
|
1098
1137
|
{(slotOfferings[slot.targetSpecId] ?? []).map((o) => (
|
|
1099
|
-
<option key={o.id} value={o.id}>{o.name} ({o.code}){o.offeringType === 'bundle' ? ' [bundle]' : ''}</option>
|
|
1138
|
+
<option key={o.id} value={o.id}>{o.name} ({o.code}){o.offeringType === 'bundle' ? t('cpq.offerings.components.bundleSuffix', ' [bundle]') : ''}</option>
|
|
1100
1139
|
))}
|
|
1101
1140
|
</select>
|
|
1102
1141
|
</div>
|
|
1103
1142
|
<div>
|
|
1104
|
-
<label className="block text-xs font-medium mb-1">Default
|
|
1143
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.offerings.components.fields.isDefault', 'Default?')}</label>
|
|
1105
1144
|
<div className="flex items-center h-[38px]">
|
|
1106
1145
|
<input
|
|
1107
1146
|
type="checkbox"
|
|
@@ -1109,7 +1148,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1109
1148
|
onChange={(e) => setAddingComponent({ ...addingComponent, isDefault: e.target.checked })}
|
|
1110
1149
|
className="rounded border"
|
|
1111
1150
|
/>
|
|
1112
|
-
<span className="ml-2 text-xs text-muted-foreground">Pre-selected in quote</span>
|
|
1151
|
+
<span className="ml-2 text-xs text-muted-foreground">{t('cpq.offerings.components.fields.isDefaultHelp', 'Pre-selected in quote')}</span>
|
|
1113
1152
|
</div>
|
|
1114
1153
|
</div>
|
|
1115
1154
|
</div>
|
|
@@ -1132,7 +1171,7 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1132
1171
|
})
|
|
1133
1172
|
if (!res.ok) {
|
|
1134
1173
|
const d = await res.json()
|
|
1135
|
-
setError(d.error ?? 'Failed to add component')
|
|
1174
|
+
setError(d.error ?? t('cpq.offerings.components.error.add', 'Failed to add component'))
|
|
1136
1175
|
return
|
|
1137
1176
|
}
|
|
1138
1177
|
setAddingComponent(null)
|
|
@@ -1142,20 +1181,20 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1142
1181
|
setComponents(d.items ?? [])
|
|
1143
1182
|
}
|
|
1144
1183
|
} catch {
|
|
1145
|
-
setError('Failed to add component')
|
|
1184
|
+
setError(t('cpq.offerings.components.error.add', 'Failed to add component'))
|
|
1146
1185
|
} finally {
|
|
1147
1186
|
setSaving(false)
|
|
1148
1187
|
}
|
|
1149
1188
|
}}
|
|
1150
1189
|
className="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
1151
1190
|
>
|
|
1152
|
-
{saving ? 'Adding...' : 'Add'}
|
|
1191
|
+
{saving ? t('cpq.offerings.components.adding', 'Adding...') : t('cpq.offerings.components.addSubmit', 'Add')}
|
|
1153
1192
|
</button>
|
|
1154
1193
|
<button
|
|
1155
1194
|
onClick={() => setAddingComponent(null)}
|
|
1156
1195
|
className="inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-xs font-medium hover:bg-muted"
|
|
1157
1196
|
>
|
|
1158
|
-
Cancel
|
|
1197
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
1159
1198
|
</button>
|
|
1160
1199
|
</div>
|
|
1161
1200
|
</div>
|
|
@@ -1163,32 +1202,32 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1163
1202
|
|
|
1164
1203
|
{slotComponents.length === 0 ? (
|
|
1165
1204
|
<div className="p-4 text-center text-xs text-muted-foreground">
|
|
1166
|
-
No components assigned to this slot
|
|
1205
|
+
{t('cpq.offerings.components.emptySlot', 'No components assigned to this slot')}
|
|
1167
1206
|
{slot.cardinalityMin > 0 && (
|
|
1168
|
-
<span className="ml-1 text-status-warning-text font-medium">(required: min {slot.cardinalityMin})</span>
|
|
1207
|
+
<span className="ml-1 text-status-warning-text font-medium">{t('cpq.offerings.components.requiredMin', '(required: min {min})', { min: slot.cardinalityMin })}</span>
|
|
1169
1208
|
)}
|
|
1170
1209
|
</div>
|
|
1171
1210
|
) : (
|
|
1172
1211
|
<table className="w-full text-sm">
|
|
1173
1212
|
<thead>
|
|
1174
1213
|
<tr className="border-b bg-muted/30">
|
|
1175
|
-
<th className="px-4 py-2 text-left text-xs font-medium">Offering</th>
|
|
1176
|
-
<th className="px-4 py-2 text-left text-xs font-medium">Code</th>
|
|
1177
|
-
<th className="px-4 py-2 text-left text-xs font-medium">Type</th>
|
|
1178
|
-
<th className="px-4 py-2 text-left text-xs font-medium">Default</th>
|
|
1179
|
-
<th className="px-4 py-2 text-right text-xs font-medium">Actions</th>
|
|
1214
|
+
<th className="px-4 py-2 text-left text-xs font-medium">{t('cpq.offerings.components.columns.offering', 'Offering')}</th>
|
|
1215
|
+
<th className="px-4 py-2 text-left text-xs font-medium">{t('cpq.offerings.components.columns.code', 'Code')}</th>
|
|
1216
|
+
<th className="px-4 py-2 text-left text-xs font-medium">{t('cpq.offerings.components.columns.type', 'Type')}</th>
|
|
1217
|
+
<th className="px-4 py-2 text-left text-xs font-medium">{t('cpq.offerings.components.columns.default', 'Default')}</th>
|
|
1218
|
+
<th className="px-4 py-2 text-right text-xs font-medium">{t('cpq.offerings.components.columns.actions', 'Actions')}</th>
|
|
1180
1219
|
</tr>
|
|
1181
1220
|
</thead>
|
|
1182
1221
|
<tbody>
|
|
1183
1222
|
{slotComponents.map((comp) => (
|
|
1184
1223
|
<tr key={comp.id} className="border-b last:border-0">
|
|
1185
|
-
<td className="px-4 py-2.5 font-medium">{comp.childOffering?.name ?? 'Unknown'}</td>
|
|
1224
|
+
<td className="px-4 py-2.5 font-medium">{comp.childOffering?.name ?? t('cpq.offerings.components.unknown', 'Unknown')}</td>
|
|
1186
1225
|
<td className="px-4 py-2.5 text-muted-foreground text-xs">{comp.childOffering?.code ?? '—'}</td>
|
|
1187
1226
|
<td className="px-4 py-2.5">
|
|
1188
1227
|
{comp.childOffering?.offeringType === 'bundle' ? (
|
|
1189
|
-
<Tag variant="brand" className="px-2 text-[10px]">bundle</Tag>
|
|
1228
|
+
<Tag variant="brand" className="px-2 text-[10px]">{t('cpq.offerings.components.tag.bundle', 'bundle')}</Tag>
|
|
1190
1229
|
) : (
|
|
1191
|
-
<Tag variant="neutral" className="px-2 text-[10px]">simple</Tag>
|
|
1230
|
+
<Tag variant="neutral" className="px-2 text-[10px]">{t('cpq.offerings.components.tag.simple', 'simple')}</Tag>
|
|
1192
1231
|
)}
|
|
1193
1232
|
</td>
|
|
1194
1233
|
<td className="px-4 py-2.5">
|
|
@@ -1208,14 +1247,14 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1208
1247
|
className="cursor-pointer"
|
|
1209
1248
|
>
|
|
1210
1249
|
<Tag variant={comp.isDefault ? 'success' : 'neutral'} className="px-2 text-[10px]">
|
|
1211
|
-
{comp.isDefault ? 'Yes' : 'No'}
|
|
1250
|
+
{comp.isDefault ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
|
|
1212
1251
|
</Tag>
|
|
1213
1252
|
</button>
|
|
1214
1253
|
</td>
|
|
1215
1254
|
<td className="px-4 py-2.5 text-right">
|
|
1216
1255
|
<button
|
|
1217
1256
|
onClick={async () => {
|
|
1218
|
-
if (!confirm('Remove this component from the bundle?')) return
|
|
1257
|
+
if (!confirm(t('cpq.offerings.components.confirmRemove', 'Remove this component from the bundle?'))) return
|
|
1219
1258
|
try {
|
|
1220
1259
|
const res = await fetch('/api/cpq/offering-components', {
|
|
1221
1260
|
method: 'DELETE',
|
|
@@ -1226,15 +1265,15 @@ export default function OfferingDetailPage(props: { params?: { id?: string } })
|
|
|
1226
1265
|
setComponents((prev) => prev.filter((c) => c.id !== comp.id))
|
|
1227
1266
|
} else {
|
|
1228
1267
|
const d = await res.json()
|
|
1229
|
-
setError(d.error ?? 'Failed to remove')
|
|
1268
|
+
setError(d.error ?? t('cpq.offerings.components.error.remove', 'Failed to remove'))
|
|
1230
1269
|
}
|
|
1231
1270
|
} catch {
|
|
1232
|
-
setError('Failed to remove component')
|
|
1271
|
+
setError(t('cpq.offerings.components.error.removeComponent', 'Failed to remove component'))
|
|
1233
1272
|
}
|
|
1234
1273
|
}}
|
|
1235
1274
|
className="text-xs text-destructive hover:underline"
|
|
1236
1275
|
>
|
|
1237
|
-
Remove
|
|
1276
|
+
{t('cpq.offerings.components.remove', 'Remove')}
|
|
1238
1277
|
</button>
|
|
1239
1278
|
</td>
|
|
1240
1279
|
</tr>
|