@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
|
@@ -3,15 +3,9 @@ import * as React from 'react'
|
|
|
3
3
|
import { useRouter, useParams } from 'next/navigation'
|
|
4
4
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
5
5
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
7
|
import { NumberInput } from '../../../../components/NumberInput'
|
|
7
|
-
|
|
8
|
-
const RULE_TYPE_LABELS: Record<string, string> = {
|
|
9
|
-
discount_percent: 'Discount %',
|
|
10
|
-
discount_absolute: 'Discount $',
|
|
11
|
-
surcharge_percent: 'Surcharge %',
|
|
12
|
-
surcharge_absolute: 'Surcharge $',
|
|
13
|
-
price_override: 'Override',
|
|
14
|
-
}
|
|
8
|
+
import { buildRuleTypeLabels } from '../PriceRuleForm'
|
|
15
9
|
|
|
16
10
|
type PriceRule = {
|
|
17
11
|
id: string
|
|
@@ -66,6 +60,8 @@ function ruleToForm(rule: PriceRule): FormData {
|
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
export default function PriceRuleDetailPage(props: { params?: { id?: string } }) {
|
|
63
|
+
const t = useT()
|
|
64
|
+
const ruleTypeLabels = React.useMemo(() => buildRuleTypeLabels(t), [t])
|
|
69
65
|
const router = useRouter()
|
|
70
66
|
const urlParams = useParams()
|
|
71
67
|
const ruleId = (props.params?.id ?? urlParams?.id) as string
|
|
@@ -102,7 +98,7 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
102
98
|
setError(null)
|
|
103
99
|
fetch(`/api/cpq/price-rules?id=${ruleId}`)
|
|
104
100
|
.then(async (r) => {
|
|
105
|
-
if (!r.ok) throw new Error('Not found')
|
|
101
|
+
if (!r.ok) throw new Error(t('cpq.priceRules.detail.notFound', 'Not found'))
|
|
106
102
|
return r.json()
|
|
107
103
|
})
|
|
108
104
|
.then((data: PriceRule) => {
|
|
@@ -110,7 +106,7 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
110
106
|
})
|
|
111
107
|
.catch((err) => setError(err.message))
|
|
112
108
|
.finally(() => setLoading(false))
|
|
113
|
-
}, [ruleId])
|
|
109
|
+
}, [ruleId, t])
|
|
114
110
|
|
|
115
111
|
React.useEffect(() => { loadRule() }, [loadRule])
|
|
116
112
|
|
|
@@ -164,14 +160,14 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
164
160
|
})
|
|
165
161
|
if (!res.ok) {
|
|
166
162
|
const err = await res.json()
|
|
167
|
-
throw new Error(err.error ?? 'Failed to update')
|
|
163
|
+
throw new Error(err.error ?? t('cpq.priceRules.errors.updateFailed', 'Failed to update'))
|
|
168
164
|
}
|
|
169
165
|
const updated = await res.json()
|
|
170
166
|
setRule(updated)
|
|
171
167
|
setEditing(false)
|
|
172
168
|
setForm(null)
|
|
173
169
|
} catch (err: unknown) {
|
|
174
|
-
setSaveError(err instanceof Error ? err.message : 'Unknown error')
|
|
170
|
+
setSaveError(err instanceof Error ? err.message : t('cpq.priceRules.errors.unknown', 'Unknown error'))
|
|
175
171
|
} finally {
|
|
176
172
|
setSaving(false)
|
|
177
173
|
}
|
|
@@ -205,18 +201,18 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
if (loading) {
|
|
208
|
-
return <div className="text-sm text-muted-foreground py-12 text-center">Loading
|
|
204
|
+
return <div className="text-sm text-muted-foreground py-12 text-center">{t('cpq.common.loading', 'Loading...')}</div>
|
|
209
205
|
}
|
|
210
206
|
|
|
211
207
|
if (error || !rule) {
|
|
212
208
|
return (
|
|
213
209
|
<div className="space-y-4 py-12 text-center">
|
|
214
|
-
<div className="text-sm text-destructive">{error ?? 'Rule not found'}</div>
|
|
210
|
+
<div className="text-sm text-destructive">{error ?? t('cpq.priceRules.detail.ruleNotFound', 'Rule not found')}</div>
|
|
215
211
|
<button
|
|
216
212
|
onClick={() => router.push('/backend/cpq/price-rules')}
|
|
217
213
|
className="text-sm text-primary hover:underline"
|
|
218
214
|
>
|
|
219
|
-
|
|
215
|
+
← {t('cpq.priceRules.detail.backToList', 'Back to Price Rules')}
|
|
220
216
|
</button>
|
|
221
217
|
</div>
|
|
222
218
|
)
|
|
@@ -224,6 +220,8 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
224
220
|
|
|
225
221
|
const cond = rule.applicabilityCondition as { attribute?: string; operator?: string; value?: string } | null
|
|
226
222
|
const offering = rule.productOfferingId ? offeringLookup.get(rule.productOfferingId) : null
|
|
223
|
+
// `{name}` is left unresolved so the rule name can be rendered in <strong>.
|
|
224
|
+
const deleteConfirmParts = t('cpq.priceRules.detail.deleteConfirm', 'Are you sure you want to delete {name}?').split('{name}')
|
|
227
225
|
|
|
228
226
|
return (
|
|
229
227
|
<div className="space-y-6">
|
|
@@ -234,30 +232,32 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
234
232
|
onClick={() => router.push('/backend/cpq/price-rules')}
|
|
235
233
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
236
234
|
>
|
|
237
|
-
|
|
235
|
+
← {t('cpq.common.back', 'Back')}
|
|
238
236
|
</button>
|
|
239
237
|
<h1 className="text-2xl font-bold">{rule.name}</h1>
|
|
240
238
|
<Tag variant={rule.isActive ? 'success' : 'neutral'} dot>
|
|
241
|
-
{rule.isActive ? 'Active' : 'Inactive'}
|
|
239
|
+
{rule.isActive ? t('cpq.common.active', 'Active') : t('cpq.common.inactive', 'Inactive')}
|
|
242
240
|
</Tag>
|
|
243
241
|
</div>
|
|
244
242
|
<div className="flex items-center gap-2">
|
|
245
243
|
<Button type="button" variant="outline" onClick={handleToggleActive}>
|
|
246
|
-
{rule.isActive
|
|
244
|
+
{rule.isActive
|
|
245
|
+
? t('cpq.priceRules.detail.deactivate', 'Deactivate')
|
|
246
|
+
: t('cpq.priceRules.detail.activate', 'Activate')}
|
|
247
247
|
</Button>
|
|
248
248
|
{!editing && (
|
|
249
249
|
<button
|
|
250
250
|
onClick={startEdit}
|
|
251
251
|
className="rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
252
252
|
>
|
|
253
|
-
Edit
|
|
253
|
+
{t('cpq.common.edit', 'Edit')}
|
|
254
254
|
</button>
|
|
255
255
|
)}
|
|
256
256
|
<button
|
|
257
257
|
onClick={() => setShowDeleteConfirm(true)}
|
|
258
258
|
className="rounded-md border border-destructive/30 px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10 transition-colors"
|
|
259
259
|
>
|
|
260
|
-
Delete
|
|
260
|
+
{t('cpq.common.delete', 'Delete')}
|
|
261
261
|
</button>
|
|
262
262
|
</div>
|
|
263
263
|
</div>
|
|
@@ -265,20 +265,26 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
265
265
|
{/* Delete confirmation */}
|
|
266
266
|
{showDeleteConfirm && (
|
|
267
267
|
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 flex items-center justify-between">
|
|
268
|
-
<span className="text-sm">
|
|
268
|
+
<span className="text-sm">
|
|
269
|
+
{deleteConfirmParts[0]}
|
|
270
|
+
<strong>{rule.name}</strong>
|
|
271
|
+
{deleteConfirmParts[1]}
|
|
272
|
+
</span>
|
|
269
273
|
<div className="flex gap-2">
|
|
270
274
|
<button
|
|
271
275
|
onClick={handleDelete}
|
|
272
276
|
disabled={deleting}
|
|
273
277
|
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50"
|
|
274
278
|
>
|
|
275
|
-
{deleting
|
|
279
|
+
{deleting
|
|
280
|
+
? t('cpq.priceRules.detail.deleting', 'Deleting...')
|
|
281
|
+
: t('cpq.priceRules.detail.confirmDelete', 'Yes, Delete')}
|
|
276
282
|
</button>
|
|
277
283
|
<button
|
|
278
284
|
onClick={() => setShowDeleteConfirm(false)}
|
|
279
285
|
className="rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted/50"
|
|
280
286
|
>
|
|
281
|
-
Cancel
|
|
287
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
282
288
|
</button>
|
|
283
289
|
</div>
|
|
284
290
|
</div>
|
|
@@ -287,32 +293,32 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
287
293
|
{/* Edit mode */}
|
|
288
294
|
{editing && form ? (
|
|
289
295
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
290
|
-
<h2 className="text-lg font-semibold">Edit Rule</h2>
|
|
296
|
+
<h2 className="text-lg font-semibold">{t('cpq.priceRules.form.editTitle', 'Edit Rule')}</h2>
|
|
291
297
|
{saveError && <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{saveError}</div>}
|
|
292
298
|
<div className="grid grid-cols-2 gap-4">
|
|
293
299
|
<label className="space-y-1">
|
|
294
|
-
<span className="text-sm font-medium">Code</span>
|
|
300
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.code', 'Code')}</span>
|
|
295
301
|
<input className="w-full rounded-md border bg-muted/30 px-3 py-2 text-sm" value={rule.code} disabled />
|
|
296
302
|
</label>
|
|
297
303
|
<label className="space-y-1">
|
|
298
|
-
<span className="text-sm font-medium">Name</span>
|
|
304
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.name', 'Name')}</span>
|
|
299
305
|
<input className="w-full rounded-md border px-3 py-2 text-sm" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
|
300
306
|
</label>
|
|
301
307
|
<label className="space-y-1">
|
|
302
|
-
<span className="text-sm font-medium">Rule Type</span>
|
|
308
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.ruleType', 'Rule Type')}</span>
|
|
303
309
|
<select className="w-full rounded-md border px-3 py-2 text-sm" value={form.ruleType} onChange={(e) => setForm({ ...form, ruleType: e.target.value })}>
|
|
304
|
-
{Object.entries(
|
|
310
|
+
{Object.entries(ruleTypeLabels).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
|
305
311
|
</select>
|
|
306
312
|
</label>
|
|
307
313
|
<label className="space-y-1">
|
|
308
|
-
<span className="text-sm font-medium">Value</span>
|
|
314
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.value', 'Value')}</span>
|
|
309
315
|
<NumberInput
|
|
310
316
|
value={form.value === '' ? null : Number(form.value)}
|
|
311
317
|
onChange={(n) => setForm({ ...form, value: n == null ? '' : String(n) })}
|
|
312
318
|
/>
|
|
313
319
|
</label>
|
|
314
320
|
<label className="space-y-1">
|
|
315
|
-
<span className="text-sm font-medium">Sort Order</span>
|
|
321
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.sortOrder', 'Sort Order')}</span>
|
|
316
322
|
<NumberInput
|
|
317
323
|
integer
|
|
318
324
|
value={form.sortOrder === '' ? null : Number(form.sortOrder)}
|
|
@@ -320,52 +326,52 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
320
326
|
/>
|
|
321
327
|
</label>
|
|
322
328
|
<label className="space-y-1">
|
|
323
|
-
<span className="text-sm font-medium">Description</span>
|
|
324
|
-
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder=
|
|
329
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.description', 'Description')}</span>
|
|
330
|
+
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.descriptionPlaceholder', 'Optional')} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
|
325
331
|
</label>
|
|
326
332
|
<label className="space-y-1">
|
|
327
|
-
<span className="text-sm font-medium">Charge Type Filter</span>
|
|
333
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.chargeTypeFilter', 'Charge Type Filter')}</span>
|
|
328
334
|
<select className="w-full rounded-md border px-3 py-2 text-sm" value={form.chargeTypeFilter} onChange={(e) => setForm({ ...form, chargeTypeFilter: e.target.value })}>
|
|
329
|
-
<option value="">All charge types</option>
|
|
330
|
-
<option value="nrc">NRC only</option>
|
|
331
|
-
<option value="mrc">MRC only</option>
|
|
332
|
-
<option value="usage">Usage only</option>
|
|
335
|
+
<option value="">{t('cpq.priceRules.form.allChargeTypes', 'All charge types')}</option>
|
|
336
|
+
<option value="nrc">{t('cpq.priceRules.form.nrcOnly', 'NRC only')}</option>
|
|
337
|
+
<option value="mrc">{t('cpq.priceRules.form.mrcOnly', 'MRC only')}</option>
|
|
338
|
+
<option value="usage">{t('cpq.priceRules.form.usageOnly', 'Usage only')}</option>
|
|
333
339
|
</select>
|
|
334
340
|
</label>
|
|
335
341
|
<label className="space-y-1">
|
|
336
|
-
<span className="text-sm font-medium">Charge Code Filter</span>
|
|
337
|
-
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder=
|
|
342
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.chargeCodeFilter', 'Charge Code Filter')}</span>
|
|
343
|
+
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.chargeCodeFilterPlaceholder', 'e.g. setup_fee (leave empty for all)')} value={form.chargeCodeFilter} onChange={(e) => setForm({ ...form, chargeCodeFilter: e.target.value })} />
|
|
338
344
|
</label>
|
|
339
345
|
<label className="space-y-1 col-span-2">
|
|
340
|
-
<span className="text-sm font-medium">Product Offering</span>
|
|
346
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.productOffering', 'Product Offering')}</span>
|
|
341
347
|
<select className="w-full rounded-md border px-3 py-2 text-sm" value={form.productOfferingId} onChange={(e) => setForm({ ...form, productOfferingId: e.target.value })}>
|
|
342
|
-
<option value="">Global (all products)</option>
|
|
348
|
+
<option value="">{t('cpq.priceRules.form.globalAllProducts', 'Global (all products)')}</option>
|
|
343
349
|
{offerings.map((o) => <option key={o.id} value={o.id}>{o.name} ({o.code})</option>)}
|
|
344
350
|
</select>
|
|
345
351
|
</label>
|
|
346
352
|
<div className="col-span-2 space-y-1">
|
|
347
|
-
<span className="text-sm font-medium">Applicability Condition</span>
|
|
348
|
-
<span className="ml-2 text-xs text-muted-foreground">(optional)</span>
|
|
353
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.applicabilityCondition', 'Applicability Condition')}</span>
|
|
354
|
+
<span className="ml-2 text-xs text-muted-foreground">{t('cpq.priceRules.form.optionalHint', '(optional)')}</span>
|
|
349
355
|
<div className="grid grid-cols-3 gap-2">
|
|
350
|
-
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder=
|
|
356
|
+
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.conditionAttributePlaceholder', 'Attribute (e.g. port_size)')} value={form.conditionAttribute} onChange={(e) => setForm({ ...form, conditionAttribute: e.target.value })} />
|
|
351
357
|
<select className="w-full rounded-md border px-3 py-2 text-sm" value={form.conditionOperator} onChange={(e) => setForm({ ...form, conditionOperator: e.target.value })}>
|
|
352
|
-
<option value="eq">equals (=)</option>
|
|
353
|
-
<option value="neq">not equals (
|
|
358
|
+
<option value="eq">{t('cpq.priceRules.form.operatorEq', 'equals (=)')}</option>
|
|
359
|
+
<option value="neq">{t('cpq.priceRules.form.operatorNeq', 'not equals (≠)')}</option>
|
|
354
360
|
</select>
|
|
355
|
-
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder=
|
|
361
|
+
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.conditionValuePlaceholderShort', 'Value')} value={form.conditionValue} onChange={(e) => setForm({ ...form, conditionValue: e.target.value })} />
|
|
356
362
|
</div>
|
|
357
363
|
</div>
|
|
358
364
|
</div>
|
|
359
365
|
<label className="flex items-center gap-2 text-sm">
|
|
360
366
|
<input type="checkbox" checked={form.isActive} onChange={(e) => setForm({ ...form, isActive: e.target.checked })} />
|
|
361
|
-
Active
|
|
367
|
+
{t('cpq.common.active', 'Active')}
|
|
362
368
|
</label>
|
|
363
369
|
<div className="flex gap-2">
|
|
364
370
|
<button onClick={handleSave} disabled={saving} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">
|
|
365
|
-
{saving ? 'Saving...' : 'Save Changes'}
|
|
371
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.priceRules.detail.saveChanges', 'Save Changes')}
|
|
366
372
|
</button>
|
|
367
373
|
<button onClick={cancelEdit} className="rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted/50">
|
|
368
|
-
Cancel
|
|
374
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
369
375
|
</button>
|
|
370
376
|
</div>
|
|
371
377
|
</div>
|
|
@@ -374,29 +380,29 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
374
380
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
375
381
|
{/* Main info */}
|
|
376
382
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
377
|
-
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Rule Configuration</h2>
|
|
383
|
+
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.ruleConfiguration', 'Rule Configuration')}</h2>
|
|
378
384
|
<dl className="space-y-3">
|
|
379
385
|
<div className="flex justify-between">
|
|
380
|
-
<dt className="text-sm text-muted-foreground">Code</dt>
|
|
386
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.code', 'Code')}</dt>
|
|
381
387
|
<dd className="text-sm font-mono">{rule.code}</dd>
|
|
382
388
|
</div>
|
|
383
389
|
<div className="flex justify-between border-t pt-3">
|
|
384
|
-
<dt className="text-sm text-muted-foreground">Rule Type</dt>
|
|
385
|
-
<dd className="text-sm font-medium">{
|
|
390
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.ruleType', 'Rule Type')}</dt>
|
|
391
|
+
<dd className="text-sm font-medium">{ruleTypeLabels[rule.ruleType] ?? rule.ruleType}</dd>
|
|
386
392
|
</div>
|
|
387
393
|
<div className="flex justify-between border-t pt-3">
|
|
388
|
-
<dt className="text-sm text-muted-foreground">Value</dt>
|
|
394
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.value', 'Value')}</dt>
|
|
389
395
|
<dd className="text-sm font-mono font-medium">
|
|
390
396
|
{rule.ruleType.includes('percent') ? `${rule.value}%` : rule.ruleType === 'price_override' ? `= $${rule.value}` : `$${rule.value}`}
|
|
391
397
|
</dd>
|
|
392
398
|
</div>
|
|
393
399
|
<div className="flex justify-between border-t pt-3">
|
|
394
|
-
<dt className="text-sm text-muted-foreground">Sort Order</dt>
|
|
400
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.sortOrder', 'Sort Order')}</dt>
|
|
395
401
|
<dd className="text-sm font-mono">{rule.sortOrder}</dd>
|
|
396
402
|
</div>
|
|
397
403
|
{rule.description && (
|
|
398
404
|
<div className="border-t pt-3">
|
|
399
|
-
<dt className="text-sm text-muted-foreground mb-1">Description</dt>
|
|
405
|
+
<dt className="text-sm text-muted-foreground mb-1">{t('cpq.priceRules.detail.description', 'Description')}</dt>
|
|
400
406
|
<dd className="text-sm">{rule.description}</dd>
|
|
401
407
|
</div>
|
|
402
408
|
)}
|
|
@@ -405,10 +411,10 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
405
411
|
|
|
406
412
|
{/* Scope & Filters */}
|
|
407
413
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
408
|
-
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Scope &
|
|
414
|
+
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.scopeAndFilters', 'Scope & Filters')}</h2>
|
|
409
415
|
<dl className="space-y-3">
|
|
410
416
|
<div className="flex justify-between">
|
|
411
|
-
<dt className="text-sm text-muted-foreground">Scope</dt>
|
|
417
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.scope', 'Scope')}</dt>
|
|
412
418
|
<dd className="text-sm">
|
|
413
419
|
{offering ? (
|
|
414
420
|
<span className="inline-flex items-center gap-1.5">
|
|
@@ -419,28 +425,28 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
419
425
|
) : (
|
|
420
426
|
<span className="inline-flex items-center gap-1.5">
|
|
421
427
|
<span className="inline-block w-2 h-2 rounded-full bg-muted-foreground" />
|
|
422
|
-
Global (all offerings)
|
|
428
|
+
{t('cpq.priceRules.detail.globalAllOfferings', 'Global (all offerings)')}
|
|
423
429
|
</span>
|
|
424
430
|
)}
|
|
425
431
|
</dd>
|
|
426
432
|
</div>
|
|
427
433
|
<div className="flex justify-between border-t pt-3">
|
|
428
|
-
<dt className="text-sm text-muted-foreground">Charge Type</dt>
|
|
429
|
-
<dd className="text-sm">{rule.chargeTypeFilter ? rule.chargeTypeFilter.toUpperCase() : 'All types'}</dd>
|
|
434
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.chargeType', 'Charge Type')}</dt>
|
|
435
|
+
<dd className="text-sm">{rule.chargeTypeFilter ? rule.chargeTypeFilter.toUpperCase() : t('cpq.priceRules.detail.allTypes', 'All types')}</dd>
|
|
430
436
|
</div>
|
|
431
437
|
<div className="flex justify-between border-t pt-3">
|
|
432
|
-
<dt className="text-sm text-muted-foreground">Charge Code</dt>
|
|
433
|
-
<dd className="text-sm font-mono">{rule.chargeCodeFilter ?? 'All charges'}</dd>
|
|
438
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.chargeCode', 'Charge Code')}</dt>
|
|
439
|
+
<dd className="text-sm font-mono">{rule.chargeCodeFilter ?? t('cpq.priceRules.detail.allCharges', 'All charges')}</dd>
|
|
434
440
|
</div>
|
|
435
441
|
<div className="border-t pt-3">
|
|
436
|
-
<dt className="text-sm text-muted-foreground mb-1">Applicability Condition</dt>
|
|
442
|
+
<dt className="text-sm text-muted-foreground mb-1">{t('cpq.priceRules.detail.applicabilityCondition', 'Applicability Condition')}</dt>
|
|
437
443
|
<dd className="text-sm">
|
|
438
444
|
{cond?.attribute ? (
|
|
439
445
|
<code className="rounded bg-muted px-2 py-0.5 text-xs">
|
|
440
446
|
{cond.attribute} {cond.operator === 'neq' ? '≠' : '='} {cond.value ?? '""'}
|
|
441
447
|
</code>
|
|
442
448
|
) : (
|
|
443
|
-
<span className="text-muted-foreground">No condition (always applies)</span>
|
|
449
|
+
<span className="text-muted-foreground">{t('cpq.priceRules.detail.noCondition', 'No condition (always applies)')}</span>
|
|
444
450
|
)}
|
|
445
451
|
</dd>
|
|
446
452
|
</div>
|
|
@@ -449,18 +455,18 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
449
455
|
|
|
450
456
|
{/* Metadata */}
|
|
451
457
|
<div className="rounded-lg border bg-card p-6 space-y-4 lg:col-span-2">
|
|
452
|
-
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Metadata</h2>
|
|
458
|
+
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.metadata', 'Metadata')}</h2>
|
|
453
459
|
<dl className="flex gap-8 text-sm">
|
|
454
460
|
<div>
|
|
455
|
-
<dt className="text-muted-foreground">Created</dt>
|
|
461
|
+
<dt className="text-muted-foreground">{t('cpq.priceRules.detail.created', 'Created')}</dt>
|
|
456
462
|
<dd>{new Date(rule.createdAt).toLocaleString()}</dd>
|
|
457
463
|
</div>
|
|
458
464
|
<div>
|
|
459
|
-
<dt className="text-muted-foreground">Updated</dt>
|
|
465
|
+
<dt className="text-muted-foreground">{t('cpq.priceRules.detail.updated', 'Updated')}</dt>
|
|
460
466
|
<dd>{new Date(rule.updatedAt).toLocaleString()}</dd>
|
|
461
467
|
</div>
|
|
462
468
|
<div>
|
|
463
|
-
<dt className="text-muted-foreground">ID</dt>
|
|
469
|
+
<dt className="text-muted-foreground">{t('cpq.priceRules.detail.id', 'ID')}</dt>
|
|
464
470
|
<dd className="font-mono text-xs">{rule.id}</dd>
|
|
465
471
|
</div>
|
|
466
472
|
</dl>
|
|
@@ -9,7 +9,7 @@ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar
|
|
|
9
9
|
import { CpqListView, useCpqListData } from '../../../components/CpqListView'
|
|
10
10
|
import {
|
|
11
11
|
PriceRuleForm,
|
|
12
|
-
|
|
12
|
+
buildRuleTypeLabels,
|
|
13
13
|
emptyPriceRuleForm,
|
|
14
14
|
priceRuleFormToPayload,
|
|
15
15
|
type PriceRule,
|
|
@@ -62,18 +62,20 @@ export default function PriceRulesPage() {
|
|
|
62
62
|
.catch(() => {})
|
|
63
63
|
}, [])
|
|
64
64
|
|
|
65
|
+
const ruleTypeLabels = React.useMemo(() => buildRuleTypeLabels(t), [t])
|
|
66
|
+
|
|
65
67
|
const ruleTypeOptions = React.useMemo(
|
|
66
|
-
() => Object.entries(
|
|
67
|
-
[],
|
|
68
|
+
() => Object.entries(ruleTypeLabels).map(([value, label]) => ({ value, label })),
|
|
69
|
+
[ruleTypeLabels],
|
|
68
70
|
)
|
|
69
71
|
|
|
70
72
|
const chargeTypeOptions = React.useMemo(
|
|
71
73
|
() => [
|
|
72
|
-
{ value: 'nrc', label: 'NRC' },
|
|
73
|
-
{ value: 'mrc', label: 'MRC' },
|
|
74
|
-
{ value: 'usage', label: 'Usage' },
|
|
74
|
+
{ value: 'nrc', label: t('cpq.priceRules.chargeTypes.nrc', 'NRC') },
|
|
75
|
+
{ value: 'mrc', label: t('cpq.priceRules.chargeTypes.mrc', 'MRC') },
|
|
76
|
+
{ value: 'usage', label: t('cpq.priceRules.chargeTypes.usage', 'Usage') },
|
|
75
77
|
],
|
|
76
|
-
[],
|
|
78
|
+
[t],
|
|
77
79
|
)
|
|
78
80
|
|
|
79
81
|
const filters = React.useMemo<FilterDef[]>(
|
|
@@ -122,16 +124,20 @@ export default function PriceRulesPage() {
|
|
|
122
124
|
})
|
|
123
125
|
if (!res.ok) {
|
|
124
126
|
const err = await res.json()
|
|
125
|
-
throw new Error(
|
|
127
|
+
throw new Error(
|
|
128
|
+
err.details
|
|
129
|
+
? JSON.stringify(err.details)
|
|
130
|
+
: err.error ?? t('cpq.priceRules.errors.createFailed', 'Failed to create'),
|
|
131
|
+
)
|
|
126
132
|
}
|
|
127
133
|
setShowForm(false)
|
|
128
134
|
data.reload()
|
|
129
135
|
} catch (err: unknown) {
|
|
130
|
-
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
136
|
+
setError(err instanceof Error ? err.message : t('cpq.priceRules.errors.unknown', 'Unknown error'))
|
|
131
137
|
} finally {
|
|
132
138
|
setSaving(false)
|
|
133
139
|
}
|
|
134
|
-
}, [data, form])
|
|
140
|
+
}, [data, form, t])
|
|
135
141
|
|
|
136
142
|
const columns = React.useMemo<ColumnDef<PriceRule>[]>(
|
|
137
143
|
() => [
|
|
@@ -154,7 +160,7 @@ export default function PriceRulesPage() {
|
|
|
154
160
|
accessorKey: 'ruleType',
|
|
155
161
|
header: t('cpq.priceRules.table.type', 'Type'),
|
|
156
162
|
cell: ({ row }) => (
|
|
157
|
-
<span>{
|
|
163
|
+
<span>{ruleTypeLabels[row.original.ruleType] ?? row.original.ruleType}</span>
|
|
158
164
|
),
|
|
159
165
|
},
|
|
160
166
|
{
|
|
@@ -168,8 +174,9 @@ export default function PriceRulesPage() {
|
|
|
168
174
|
cell: ({ row }) => (
|
|
169
175
|
<span className="text-xs">
|
|
170
176
|
{row.original.productOfferingId
|
|
171
|
-
? row.original.productOfferingName ??
|
|
172
|
-
|
|
177
|
+
? row.original.productOfferingName ??
|
|
178
|
+
t('cpq.priceRules.scope.productScoped', 'Product-scoped')
|
|
179
|
+
: t('cpq.priceRules.scope.global', 'Global')}
|
|
173
180
|
</span>
|
|
174
181
|
),
|
|
175
182
|
},
|
|
@@ -201,12 +208,14 @@ export default function PriceRulesPage() {
|
|
|
201
208
|
header: t('cpq.priceRules.table.status', 'Status'),
|
|
202
209
|
cell: ({ row }) => (
|
|
203
210
|
<Tag variant={row.original.isActive ? 'success' : 'neutral'} dot>
|
|
204
|
-
{row.original.isActive
|
|
211
|
+
{row.original.isActive
|
|
212
|
+
? t('cpq.common.active', 'Active')
|
|
213
|
+
: t('cpq.common.inactive', 'Inactive')}
|
|
205
214
|
</Tag>
|
|
206
215
|
),
|
|
207
216
|
},
|
|
208
217
|
],
|
|
209
|
-
[t],
|
|
218
|
+
[ruleTypeLabels, t],
|
|
210
219
|
)
|
|
211
220
|
|
|
212
221
|
return (
|
|
@@ -245,7 +254,7 @@ export default function PriceRulesPage() {
|
|
|
245
254
|
}
|
|
246
255
|
emptyState={
|
|
247
256
|
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
248
|
-
No price rules found. Click
|
|
257
|
+
{t('cpq.priceRules.empty', 'No price rules found. Click "Create Rule" to add one.')}
|
|
249
258
|
</div>
|
|
250
259
|
}
|
|
251
260
|
/>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
SelectValue,
|
|
12
12
|
} from '@open-mercato/ui/primitives/select'
|
|
13
13
|
import { Trash2 } from 'lucide-react'
|
|
14
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
14
15
|
|
|
15
16
|
// ─── Types ──────────────────────────────────────────────────────
|
|
16
17
|
|
|
@@ -54,6 +55,7 @@ export function EntriesEditor({
|
|
|
54
55
|
initialEntries: Entry[]
|
|
55
56
|
onSaved: (entries: Entry[]) => void
|
|
56
57
|
}) {
|
|
58
|
+
const t = useT()
|
|
57
59
|
const [drafts, setDrafts] = React.useState<DraftEntry[]>(() =>
|
|
58
60
|
initialEntries.map((e) => ({ ...e, _state: null, _localId: e.id })),
|
|
59
61
|
)
|
|
@@ -142,7 +144,7 @@ export function EntriesEditor({
|
|
|
142
144
|
})
|
|
143
145
|
if (!res.ok) {
|
|
144
146
|
const body = await res.json().catch(() => ({}))
|
|
145
|
-
return { ok: false, error: body.error ?? 'Failed to create entries' }
|
|
147
|
+
return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.createFailed', 'Failed to create entries') }
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
@@ -164,7 +166,7 @@ export function EntriesEditor({
|
|
|
164
166
|
})
|
|
165
167
|
if (!res.ok) {
|
|
166
168
|
const body = await res.json().catch(() => ({}))
|
|
167
|
-
return { ok: false, error: body.error ??
|
|
169
|
+
return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.updateFailed', 'Failed to update entry {id}', { id: d.id }) }
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
|
|
@@ -178,13 +180,13 @@ export function EntriesEditor({
|
|
|
178
180
|
})
|
|
179
181
|
if (!res.ok) {
|
|
180
182
|
const body = await res.json().catch(() => ({}))
|
|
181
|
-
return { ok: false, error: body.error ??
|
|
183
|
+
return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.deleteFailed', 'Failed to delete entry {id}', { id: d.id }) }
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
return { ok: true }
|
|
186
188
|
} catch (err) {
|
|
187
|
-
return { ok: false, error: err instanceof Error ? err.message : 'Failed to save entries' }
|
|
189
|
+
return { ok: false, error: err instanceof Error ? err.message : t('cpq.pricing.entries.errors.saveFailed', 'Failed to save entries') }
|
|
188
190
|
} finally {
|
|
189
191
|
setSaving(false)
|
|
190
192
|
}
|
|
@@ -208,26 +210,26 @@ export function EntriesEditor({
|
|
|
208
210
|
<div className="space-y-3">
|
|
209
211
|
<div className="flex items-center justify-between">
|
|
210
212
|
<h2 className="text-lg font-semibold">
|
|
211
|
-
Entries ({visibleDrafts.length})
|
|
213
|
+
{t('cpq.pricing.entries.title', 'Entries ({count})', { count: visibleDrafts.length })}
|
|
212
214
|
</h2>
|
|
213
215
|
<div className="flex items-center gap-2">
|
|
214
216
|
{mode === 'view' ? (
|
|
215
217
|
<Button type="button" variant="outline" size="sm" onClick={() => setMode('edit')}>
|
|
216
|
-
Edit Entries
|
|
218
|
+
{t('cpq.pricing.entries.edit', 'Edit Entries')}
|
|
217
219
|
</Button>
|
|
218
220
|
) : (
|
|
219
221
|
<>
|
|
220
222
|
<Button type="button" variant="outline" size="sm" onClick={() => addRow(false)}>
|
|
221
|
-
+ Add Row
|
|
223
|
+
{t('cpq.pricing.entries.addRow', '+ Add Row')}
|
|
222
224
|
</Button>
|
|
223
225
|
<Button type="button" variant="outline" size="sm" onClick={() => addRow(true)}>
|
|
224
|
-
+ Add Tier
|
|
226
|
+
{t('cpq.pricing.entries.addTier', '+ Add Tier')}
|
|
225
227
|
</Button>
|
|
226
228
|
<Button type="button" size="sm" onClick={handleSave} disabled={saving || !dirty}>
|
|
227
|
-
{saving ? 'Saving...' : 'Save'}
|
|
229
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
|
|
228
230
|
</Button>
|
|
229
231
|
<Button type="button" variant="outline" size="sm" onClick={cancelEdit} disabled={saving}>
|
|
230
|
-
Cancel
|
|
232
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
231
233
|
</Button>
|
|
232
234
|
</>
|
|
233
235
|
)}
|
|
@@ -239,8 +241,11 @@ export function EntriesEditor({
|
|
|
239
241
|
{visibleDrafts.length === 0 ? (
|
|
240
242
|
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
241
243
|
{mode === 'edit'
|
|
242
|
-
?
|
|
243
|
-
|
|
244
|
+
? t(
|
|
245
|
+
'cpq.pricing.entries.emptyEditing',
|
|
246
|
+
'No entries yet — use "+ Add Row" (dimensions lookup) or "+ Add Tier" (range lookup).',
|
|
247
|
+
)
|
|
248
|
+
: t('cpq.pricing.entries.empty', 'No entries in this pricing table.')}
|
|
244
249
|
</div>
|
|
245
250
|
) : (
|
|
246
251
|
<div className="rounded-lg border overflow-x-auto">
|
|
@@ -256,9 +261,9 @@ export function EntriesEditor({
|
|
|
256
261
|
prices they qualify, so the row reads "what → how much". */}
|
|
257
262
|
{hasTieredEntries && (
|
|
258
263
|
<>
|
|
259
|
-
<th className="px-3 py-3 text-left font-medium whitespace-nowrap">Tier
|
|
260
|
-
<th className="px-3 py-3 text-left font-medium whitespace-nowrap">Range From</th>
|
|
261
|
-
<th className="px-3 py-3 text-left font-medium whitespace-nowrap">Range To</th>
|
|
264
|
+
<th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.tierNumber', 'Tier #')}</th>
|
|
265
|
+
<th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.rangeFrom', 'Range From')}</th>
|
|
266
|
+
<th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.rangeTo', 'Range To')}</th>
|
|
262
267
|
</>
|
|
263
268
|
)}
|
|
264
269
|
{table.priceColumns.map((col) => (
|
|
@@ -266,8 +271,8 @@ export function EntriesEditor({
|
|
|
266
271
|
{col.label}
|
|
267
272
|
</th>
|
|
268
273
|
))}
|
|
269
|
-
<th className="px-3 py-3 text-left font-medium">Currency</th>
|
|
270
|
-
{mode === 'edit' && <th className="px-3 py-3 w-8" aria-label=
|
|
274
|
+
<th className="px-3 py-3 text-left font-medium">{t('cpq.pricing.entries.currency', 'Currency')}</th>
|
|
275
|
+
{mode === 'edit' && <th className="px-3 py-3 w-8" aria-label={t('cpq.pricing.entries.actions', 'Actions')} />}
|
|
271
276
|
</tr>
|
|
272
277
|
</thead>
|
|
273
278
|
<tbody>
|
|
@@ -307,6 +312,7 @@ function EntryRow({
|
|
|
307
312
|
onChange: (patch: Partial<DraftEntry>) => void
|
|
308
313
|
onDelete: () => void
|
|
309
314
|
}) {
|
|
315
|
+
const t = useT()
|
|
310
316
|
const readOnly = mode === 'view'
|
|
311
317
|
const rowBg = entry._state === 'new'
|
|
312
318
|
? 'bg-success/5'
|
|
@@ -421,7 +427,7 @@ function EntryRow({
|
|
|
421
427
|
type="button"
|
|
422
428
|
onClick={onDelete}
|
|
423
429
|
className="text-destructive hover:bg-destructive/10 rounded p-1"
|
|
424
|
-
aria-label=
|
|
430
|
+
aria-label={t('cpq.pricing.entries.deleteRow', 'Delete row')}
|
|
425
431
|
>
|
|
426
432
|
<Trash2 className="h-4 w-4" />
|
|
427
433
|
</button>
|