@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
|
@@ -300,7 +300,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
300
300
|
// Load spec detail with nested data
|
|
301
301
|
const res = await fetch(`/api/cpq/product-specifications?id=${encodeURIComponent(specId)}`)
|
|
302
302
|
if (!res.ok) {
|
|
303
|
-
if (!cancelled) setError('Failed to load specification')
|
|
303
|
+
if (!cancelled) setError(t('cpq.specifications.detail.error.load', 'Failed to load specification'))
|
|
304
304
|
return
|
|
305
305
|
}
|
|
306
306
|
|
|
@@ -325,7 +325,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
325
325
|
}
|
|
326
326
|
} catch (err) {
|
|
327
327
|
console.error('Failed to load', err)
|
|
328
|
-
if (!cancelled) setError('Failed to load')
|
|
328
|
+
if (!cancelled) setError(t('cpq.specifications.detail.error.loadGeneric', 'Failed to load'))
|
|
329
329
|
} finally {
|
|
330
330
|
if (!cancelled) setLoading(false)
|
|
331
331
|
}
|
|
@@ -354,7 +354,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
354
354
|
|
|
355
355
|
if (!res.ok) {
|
|
356
356
|
const d = await res.json()
|
|
357
|
-
setError(d.error ?? 'Failed to save')
|
|
357
|
+
setError(d.error ?? t('cpq.specifications.detail.error.save', 'Failed to save'))
|
|
358
358
|
return
|
|
359
359
|
}
|
|
360
360
|
|
|
@@ -379,7 +379,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
379
379
|
setGeneralMode('view')
|
|
380
380
|
}
|
|
381
381
|
} catch {
|
|
382
|
-
setError('Failed to save')
|
|
382
|
+
setError(t('cpq.specifications.detail.error.save', 'Failed to save'))
|
|
383
383
|
} finally {
|
|
384
384
|
setSaving(false)
|
|
385
385
|
}
|
|
@@ -397,10 +397,10 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
397
397
|
router.push('/backend/cpq/specifications')
|
|
398
398
|
} else {
|
|
399
399
|
const d = await res.json()
|
|
400
|
-
setError(d.error ?? 'Failed to delete')
|
|
400
|
+
setError(d.error ?? t('cpq.specifications.detail.error.delete', 'Failed to delete'))
|
|
401
401
|
}
|
|
402
402
|
} catch {
|
|
403
|
-
setError('Failed to delete')
|
|
403
|
+
setError(t('cpq.specifications.detail.error.delete', 'Failed to delete'))
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
|
|
@@ -438,7 +438,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
438
438
|
})
|
|
439
439
|
if (!res.ok) {
|
|
440
440
|
const d = await res.json()
|
|
441
|
-
setError(d.error ?? 'Failed to save attribute')
|
|
441
|
+
setError(d.error ?? t('cpq.specifications.attributes.error.save', 'Failed to save attribute'))
|
|
442
442
|
return
|
|
443
443
|
}
|
|
444
444
|
const saved = await res.json()
|
|
@@ -449,14 +449,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
449
449
|
}
|
|
450
450
|
setEditingAttr(null)
|
|
451
451
|
} catch {
|
|
452
|
-
setError('Failed to save attribute')
|
|
452
|
+
setError(t('cpq.specifications.attributes.error.save', 'Failed to save attribute'))
|
|
453
453
|
} finally {
|
|
454
454
|
setSaving(false)
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
const deleteAttribute = async (attrId: string) => {
|
|
459
|
-
if (!confirm('Delete this attribute?')) return
|
|
459
|
+
if (!confirm(t('cpq.specifications.attributes.confirmDelete', 'Delete this attribute?'))) return
|
|
460
460
|
try {
|
|
461
461
|
const res = await fetch('/api/cpq/product-attributes', {
|
|
462
462
|
method: 'DELETE',
|
|
@@ -465,7 +465,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
465
465
|
})
|
|
466
466
|
if (res.ok) setAttributes((prev) => prev.filter((a) => a.id !== attrId))
|
|
467
467
|
} catch {
|
|
468
|
-
setError('Failed to delete attribute')
|
|
468
|
+
setError(t('cpq.specifications.attributes.error.delete', 'Failed to delete attribute'))
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
471
|
|
|
@@ -502,7 +502,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
502
502
|
})
|
|
503
503
|
if (!res.ok) {
|
|
504
504
|
const d = await res.json()
|
|
505
|
-
setError(d.error ?? 'Failed to save offering')
|
|
505
|
+
setError(d.error ?? t('cpq.specifications.offerings.error.save', 'Failed to save offering'))
|
|
506
506
|
return
|
|
507
507
|
}
|
|
508
508
|
const saved = await res.json()
|
|
@@ -513,14 +513,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
513
513
|
}
|
|
514
514
|
setEditingOffering(null)
|
|
515
515
|
} catch {
|
|
516
|
-
setError('Failed to save offering')
|
|
516
|
+
setError(t('cpq.specifications.offerings.error.save', 'Failed to save offering'))
|
|
517
517
|
} finally {
|
|
518
518
|
setSaving(false)
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
521
|
|
|
522
522
|
const deleteOffering = async (offeringId: string) => {
|
|
523
|
-
if (!confirm('Delete this offering?')) return
|
|
523
|
+
if (!confirm(t('cpq.specifications.offerings.confirmDelete', 'Delete this offering?'))) return
|
|
524
524
|
try {
|
|
525
525
|
const res = await fetch('/api/cpq/product-offerings', {
|
|
526
526
|
method: 'DELETE',
|
|
@@ -529,7 +529,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
529
529
|
})
|
|
530
530
|
if (res.ok) setOfferings((prev) => prev.filter((o) => o.id !== offeringId))
|
|
531
531
|
} catch {
|
|
532
|
-
setError('Failed to delete offering')
|
|
532
|
+
setError(t('cpq.specifications.offerings.error.delete', 'Failed to delete offering'))
|
|
533
533
|
}
|
|
534
534
|
}
|
|
535
535
|
|
|
@@ -623,21 +623,21 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
623
623
|
})
|
|
624
624
|
if (!res.ok) {
|
|
625
625
|
const d = await res.json()
|
|
626
|
-
setError(d.error ??
|
|
626
|
+
setError(d.error ?? t('cpq.specifications.charges.error.saveFor', 'Failed to save charge for {name}', { name: offering.name }))
|
|
627
627
|
break
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
630
|
setChargeFormOpen(false)
|
|
631
631
|
await loadAllOfferingCharges()
|
|
632
632
|
} catch {
|
|
633
|
-
setError('Failed to save charges')
|
|
633
|
+
setError(t('cpq.specifications.charges.error.saveAll', 'Failed to save charges'))
|
|
634
634
|
} finally {
|
|
635
635
|
setSaving(false)
|
|
636
636
|
}
|
|
637
637
|
}
|
|
638
638
|
|
|
639
639
|
const deleteCharge = async (offeringId: string, chargeId: string) => {
|
|
640
|
-
if (!confirm('Delete this charge?')) return
|
|
640
|
+
if (!confirm(t('cpq.specifications.charges.confirmDelete', 'Delete this charge?'))) return
|
|
641
641
|
try {
|
|
642
642
|
const res = await fetch('/api/cpq/product-charges', {
|
|
643
643
|
method: 'DELETE',
|
|
@@ -651,7 +651,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
651
651
|
}))
|
|
652
652
|
}
|
|
653
653
|
} catch {
|
|
654
|
-
setError('Failed to delete charge')
|
|
654
|
+
setError(t('cpq.specifications.charges.error.delete', 'Failed to delete charge'))
|
|
655
655
|
}
|
|
656
656
|
}
|
|
657
657
|
|
|
@@ -682,7 +682,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
682
682
|
})
|
|
683
683
|
if (!res.ok) {
|
|
684
684
|
const d = await res.json()
|
|
685
|
-
setError(d.error ?? 'Failed to save relationship')
|
|
685
|
+
setError(d.error ?? t('cpq.specifications.relationships.error.save', 'Failed to save relationship'))
|
|
686
686
|
return
|
|
687
687
|
}
|
|
688
688
|
const saved = await res.json()
|
|
@@ -693,14 +693,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
693
693
|
}
|
|
694
694
|
setEditingRel(null)
|
|
695
695
|
} catch {
|
|
696
|
-
setError('Failed to save relationship')
|
|
696
|
+
setError(t('cpq.specifications.relationships.error.save', 'Failed to save relationship'))
|
|
697
697
|
} finally {
|
|
698
698
|
setSaving(false)
|
|
699
699
|
}
|
|
700
700
|
}
|
|
701
701
|
|
|
702
702
|
const deleteRelationship = async (relId: string) => {
|
|
703
|
-
if (!confirm('Delete this relationship?')) return
|
|
703
|
+
if (!confirm(t('cpq.specifications.relationships.confirmDelete', 'Delete this relationship?'))) return
|
|
704
704
|
try {
|
|
705
705
|
const res = await fetch('/api/cpq/product-relationships', {
|
|
706
706
|
method: 'DELETE',
|
|
@@ -709,7 +709,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
709
709
|
})
|
|
710
710
|
if (res.ok) setRelationships((prev) => prev.filter((r) => r.id !== relId))
|
|
711
711
|
} catch {
|
|
712
|
-
setError('Failed to delete relationship')
|
|
712
|
+
setError(t('cpq.specifications.relationships.error.delete', 'Failed to delete relationship'))
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
715
|
|
|
@@ -726,7 +726,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
726
726
|
// ─── Render ─────────────────────────────────────────────────
|
|
727
727
|
|
|
728
728
|
if (loading) {
|
|
729
|
-
return <div className="text-sm text-muted-foreground">{t('common.loading', 'Loading...')}</div>
|
|
729
|
+
return <div className="text-sm text-muted-foreground">{t('cpq.common.loading', 'Loading...')}</div>
|
|
730
730
|
}
|
|
731
731
|
|
|
732
732
|
const designTimeAttrs = attributes.filter((a) => a.resolutionTime === 'design_time')
|
|
@@ -740,7 +740,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
740
740
|
onClick={() => router.push('/backend/cpq/specifications')}
|
|
741
741
|
className="text-sm text-muted-foreground hover:text-foreground"
|
|
742
742
|
>
|
|
743
|
-
← {t('common.back', 'Back')}
|
|
743
|
+
← {t('cpq.common.back', 'Back')}
|
|
744
744
|
</button>
|
|
745
745
|
<h1 className="text-2xl font-bold">
|
|
746
746
|
{isNew ? t('cpq.specifications.new', 'New Specification') : form.name}
|
|
@@ -764,11 +764,11 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
764
764
|
setGeneralMode('edit')
|
|
765
765
|
}}
|
|
766
766
|
>
|
|
767
|
-
{t('common.edit', 'Edit')}
|
|
767
|
+
{t('cpq.common.edit', 'Edit')}
|
|
768
768
|
</Button>
|
|
769
769
|
)}
|
|
770
770
|
<Button type="button" variant="destructive" onClick={deleteSpec}>
|
|
771
|
-
{t('common.delete', 'Delete')}
|
|
771
|
+
{t('cpq.common.delete', 'Delete')}
|
|
772
772
|
</Button>
|
|
773
773
|
</div>
|
|
774
774
|
)}
|
|
@@ -795,11 +795,11 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
795
795
|
tab === t_ ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground'
|
|
796
796
|
}`}
|
|
797
797
|
>
|
|
798
|
-
{t_ === 'general' && 'General'}
|
|
799
|
-
{t_ === 'attributes' &&
|
|
800
|
-
{t_ === 'offerings' &&
|
|
801
|
-
{t_ === 'relationships' &&
|
|
802
|
-
{t_ === 'slots' &&
|
|
798
|
+
{t_ === 'general' && t('cpq.specifications.detail.tabs.general', 'General')}
|
|
799
|
+
{t_ === 'attributes' && t('cpq.specifications.detail.tabs.attributes', 'Attributes ({count})', { count: attributes.length })}
|
|
800
|
+
{t_ === 'offerings' && t('cpq.specifications.detail.tabs.offerings', 'Offerings ({count})', { count: offerings.length })}
|
|
801
|
+
{t_ === 'relationships' && t('cpq.specifications.detail.tabs.relationships', 'Relationships ({count})', { count: relationships.length })}
|
|
802
|
+
{t_ === 'slots' && t('cpq.specifications.detail.tabs.slots', 'Bundle Slots ({count})', { count: bundleSlots.length })}
|
|
803
803
|
</button>
|
|
804
804
|
))}
|
|
805
805
|
</div>
|
|
@@ -824,7 +824,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
824
824
|
type="text"
|
|
825
825
|
value={form.code}
|
|
826
826
|
onChange={(e) => setForm({ ...form, code: e.target.value })}
|
|
827
|
-
placeholder=
|
|
827
|
+
placeholder={t('cpq.specifications.detail.codePlaceholder', 'e.g. gix-access-port')}
|
|
828
828
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
829
829
|
/>
|
|
830
830
|
)}
|
|
@@ -856,9 +856,9 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
856
856
|
onChange={(e) => setForm({ ...form, lifecycleStatus: e.target.value })}
|
|
857
857
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
858
858
|
>
|
|
859
|
-
<option value="draft">Draft</option>
|
|
860
|
-
<option value="active">Active</option>
|
|
861
|
-
<option value="deprecated">Deprecated</option>
|
|
859
|
+
<option value="draft">{t('cpq.specifications.lifecycle.draft', 'Draft')}</option>
|
|
860
|
+
<option value="active">{t('cpq.specifications.lifecycle.active', 'Active')}</option>
|
|
861
|
+
<option value="deprecated">{t('cpq.specifications.lifecycle.deprecated', 'Deprecated')}</option>
|
|
862
862
|
</select>
|
|
863
863
|
)}
|
|
864
864
|
</div>
|
|
@@ -867,7 +867,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
867
867
|
<div className="flex items-center h-[38px]">
|
|
868
868
|
{isViewing ? (
|
|
869
869
|
<Tag variant={form.isActive ? 'success' : 'neutral'} dot>
|
|
870
|
-
{form.isActive ? t('common.yes', 'Yes') : t('common.no', 'No')}
|
|
870
|
+
{form.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
|
|
871
871
|
</Tag>
|
|
872
872
|
) : (
|
|
873
873
|
<input
|
|
@@ -880,9 +880,9 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
880
880
|
</div>
|
|
881
881
|
</div>
|
|
882
882
|
<div>
|
|
883
|
-
<label className="block text-sm font-medium mb-1">Spec Type</label>
|
|
883
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.detail.specType', 'Spec Type')}</label>
|
|
884
884
|
{isViewing ? (
|
|
885
|
-
<p className="text-sm py-2">{form.specType === 'bundle' ? 'Bundle' : 'Simple'}</p>
|
|
885
|
+
<p className="text-sm py-2">{form.specType === 'bundle' ? t('cpq.specifications.type.bundle', 'Bundle') : t('cpq.specifications.type.simple', 'Simple')}</p>
|
|
886
886
|
) : (
|
|
887
887
|
<>
|
|
888
888
|
<select
|
|
@@ -891,21 +891,21 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
891
891
|
disabled={specTypeLocked}
|
|
892
892
|
className="w-full rounded-md border px-3 py-2 text-sm disabled:opacity-50"
|
|
893
893
|
>
|
|
894
|
-
<option value="simple">Simple</option>
|
|
895
|
-
<option value="bundle">Bundle</option>
|
|
894
|
+
<option value="simple">{t('cpq.specifications.type.simple', 'Simple')}</option>
|
|
895
|
+
<option value="bundle">{t('cpq.specifications.type.bundle', 'Bundle')}</option>
|
|
896
896
|
</select>
|
|
897
897
|
{specTypeLocked && (
|
|
898
|
-
<p className="text-xs text-muted-foreground mt-1">Cannot change after offerings or slots exist</p>
|
|
898
|
+
<p className="text-xs text-muted-foreground mt-1">{t('cpq.specifications.detail.specTypeLocked', 'Cannot change after offerings or slots exist')}</p>
|
|
899
899
|
)}
|
|
900
900
|
</>
|
|
901
901
|
)}
|
|
902
902
|
</div>
|
|
903
903
|
<div>
|
|
904
|
-
<label className="block text-sm font-medium mb-1">Is Assetizable
|
|
904
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.detail.isAssetizable', 'Is Assetizable?')}</label>
|
|
905
905
|
<div className="flex items-center h-[38px]">
|
|
906
906
|
{isViewing ? (
|
|
907
907
|
<Tag variant={form.isAssetizable ? 'warning' : 'neutral'} dot>
|
|
908
|
-
{form.isAssetizable ? t('common.yes', 'Yes') : t('common.no', 'No')}
|
|
908
|
+
{form.isAssetizable ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
|
|
909
909
|
</Tag>
|
|
910
910
|
) : (
|
|
911
911
|
<>
|
|
@@ -915,7 +915,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
915
915
|
onChange={(e) => setForm({ ...form, isAssetizable: e.target.checked })}
|
|
916
916
|
className="rounded border"
|
|
917
917
|
/>
|
|
918
|
-
<span className="ml-2 text-xs text-muted-foreground">Creates asset records during fulfilment</span>
|
|
918
|
+
<span className="ml-2 text-xs text-muted-foreground">{t('cpq.specifications.detail.isAssetizableHelp', 'Creates asset records during fulfilment')}</span>
|
|
919
919
|
</>
|
|
920
920
|
)}
|
|
921
921
|
</div>
|
|
@@ -935,7 +935,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
935
935
|
)}
|
|
936
936
|
</div>
|
|
937
937
|
{!isNew && (
|
|
938
|
-
<span className="text-xs text-muted-foreground">Version {form.version}</span>
|
|
938
|
+
<span className="text-xs text-muted-foreground">{t('cpq.specifications.detail.versionLabel', 'Version {version}', { version: form.version })}</span>
|
|
939
939
|
)}
|
|
940
940
|
{/* In view mode the Edit button lives in the page header next to
|
|
941
941
|
Delete (OM standard). Here we only render the Save/Cancel
|
|
@@ -947,7 +947,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
947
947
|
onClick={saveSpec}
|
|
948
948
|
disabled={saving || !form.code || !form.name}
|
|
949
949
|
>
|
|
950
|
-
{saving ? t('common.saving', 'Saving...') : t('common.save', 'Save')}
|
|
950
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
|
|
951
951
|
</Button>
|
|
952
952
|
<Button
|
|
953
953
|
type="button"
|
|
@@ -963,7 +963,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
963
963
|
setGeneralMode('view')
|
|
964
964
|
}}
|
|
965
965
|
>
|
|
966
|
-
{t('common.cancel', 'Cancel')}
|
|
966
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
967
967
|
</Button>
|
|
968
968
|
</div>
|
|
969
969
|
)}
|
|
@@ -986,40 +986,40 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
986
986
|
{editingAttr && (
|
|
987
987
|
<div className="rounded-lg border bg-card p-4 space-y-3">
|
|
988
988
|
<h3 className="font-medium text-sm">
|
|
989
|
-
{editingAttr.id ? 'Edit Attribute' : 'New Attribute'}
|
|
989
|
+
{editingAttr.id ? t('cpq.specifications.attributes.form.editTitle', 'Edit Attribute') : t('cpq.specifications.attributes.form.newTitle', 'New Attribute')}
|
|
990
990
|
</h3>
|
|
991
991
|
<div className="grid grid-cols-3 gap-3">
|
|
992
992
|
<div>
|
|
993
|
-
<label className="block text-xs font-medium mb-1">Code</label>
|
|
993
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.code', 'Code')}</label>
|
|
994
994
|
<input type="text" value={editingAttr.code} onChange={(e) => setEditingAttr({ ...editingAttr, code: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
995
995
|
</div>
|
|
996
996
|
<div>
|
|
997
|
-
<label className="block text-xs font-medium mb-1">Name</label>
|
|
997
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.name', 'Name')}</label>
|
|
998
998
|
<input type="text" value={editingAttr.name} onChange={(e) => setEditingAttr({ ...editingAttr, name: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
999
999
|
</div>
|
|
1000
1000
|
<div>
|
|
1001
|
-
<label className="block text-xs font-medium mb-1">Type</label>
|
|
1001
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.type', 'Type')}</label>
|
|
1002
1002
|
{/* Reset the default when the type changes — a default kept from a
|
|
1003
1003
|
different type (e.g. a numeric default on a now-boolean attr)
|
|
1004
1004
|
would be invalid and confusing. */}
|
|
1005
1005
|
<select value={editingAttr.attributeType} onChange={(e) => setEditingAttr({ ...editingAttr, attributeType: e.target.value, defaultValue: null })} className="w-full rounded-md border px-2 py-1.5 text-sm">
|
|
1006
|
-
<option value="text">Text</option>
|
|
1007
|
-
<option value="number">Number</option>
|
|
1008
|
-
<option value="boolean">Boolean</option>
|
|
1009
|
-
<option value="select">Select</option>
|
|
1010
|
-
<option value="multi-select">Multi-Select</option>
|
|
1011
|
-
<option value="reference">Reference</option>
|
|
1006
|
+
<option value="text">{t('cpq.specifications.attributeType.text', 'Text')}</option>
|
|
1007
|
+
<option value="number">{t('cpq.specifications.attributeType.number', 'Number')}</option>
|
|
1008
|
+
<option value="boolean">{t('cpq.specifications.attributeType.boolean', 'Boolean')}</option>
|
|
1009
|
+
<option value="select">{t('cpq.specifications.attributeType.select', 'Select')}</option>
|
|
1010
|
+
<option value="multi-select">{t('cpq.specifications.attributeType.multiSelect', 'Multi-Select')}</option>
|
|
1011
|
+
<option value="reference">{t('cpq.specifications.attributeType.reference', 'Reference')}</option>
|
|
1012
1012
|
</select>
|
|
1013
1013
|
</div>
|
|
1014
1014
|
<div>
|
|
1015
|
-
<label className="block text-xs font-medium mb-1">Resolution Time</label>
|
|
1015
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.resolutionTime', 'Resolution Time')}</label>
|
|
1016
1016
|
<select value={editingAttr.resolutionTime} onChange={(e) => setEditingAttr({ ...editingAttr, resolutionTime: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
|
|
1017
|
-
<option value="run_time">Run-time (quoting)</option>
|
|
1018
|
-
<option value="design_time">Design-time (offering)</option>
|
|
1017
|
+
<option value="run_time">{t('cpq.specifications.resolutionTime.runTime', 'Run-time (quoting)')}</option>
|
|
1018
|
+
<option value="design_time">{t('cpq.specifications.resolutionTime.designTime', 'Design-time (offering)')}</option>
|
|
1019
1019
|
</select>
|
|
1020
1020
|
</div>
|
|
1021
1021
|
<div>
|
|
1022
|
-
<label className="block text-xs font-medium mb-1">Sort Order</label>
|
|
1022
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.sortOrder', 'Sort Order')}</label>
|
|
1023
1023
|
<NumberInput
|
|
1024
1024
|
integer
|
|
1025
1025
|
value={editingAttr.sortOrder}
|
|
@@ -1027,7 +1027,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1027
1027
|
/>
|
|
1028
1028
|
</div>
|
|
1029
1029
|
<div>
|
|
1030
|
-
<label className="block text-xs font-medium mb-1">Help Text</label>
|
|
1030
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.helpText', 'Help Text')}</label>
|
|
1031
1031
|
<input type="text" value={editingAttr.helpText ?? ''} onChange={(e) => setEditingAttr({ ...editingAttr, helpText: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
1032
1032
|
</div>
|
|
1033
1033
|
{/* Reference attributes resolve to an FK UUID at run-time, so a
|
|
@@ -1102,25 +1102,25 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1102
1102
|
{editingAttr.attributeType === 'reference' && (
|
|
1103
1103
|
<>
|
|
1104
1104
|
<div>
|
|
1105
|
-
<label className="block text-xs font-medium mb-1">Reference Entity</label>
|
|
1106
|
-
<input type="text" value={editingAttr.referenceEntity ?? ''} onChange={(e) => setEditingAttr({ ...editingAttr, referenceEntity: e.target.value || null })} placeholder=
|
|
1105
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.fields.referenceEntity', 'Reference Entity')}</label>
|
|
1106
|
+
<input type="text" value={editingAttr.referenceEntity ?? ''} onChange={(e) => setEditingAttr({ ...editingAttr, referenceEntity: e.target.value || null })} placeholder={t('cpq.specifications.attributes.fields.referenceEntityPlaceholder', 'e.g. CatalogProduct')} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
1107
1107
|
</div>
|
|
1108
1108
|
</>
|
|
1109
1109
|
)}
|
|
1110
1110
|
{(editingAttr.attributeType === 'select' || editingAttr.attributeType === 'multi-select') && (
|
|
1111
1111
|
<div className="col-span-3 space-y-2">
|
|
1112
1112
|
<div className="flex items-center justify-between">
|
|
1113
|
-
<label className="block text-xs font-medium">Options</label>
|
|
1113
|
+
<label className="block text-xs font-medium">{t('cpq.specifications.attributes.fields.options', 'Options')}</label>
|
|
1114
1114
|
<button
|
|
1115
1115
|
type="button"
|
|
1116
1116
|
onClick={() => setEditingOptions([...editingOptions, { value: '', label: '' }])}
|
|
1117
1117
|
className="text-xs text-primary hover:underline"
|
|
1118
1118
|
>
|
|
1119
|
-
+ Add option
|
|
1119
|
+
{t('cpq.specifications.attributes.addOption', '+ Add option')}
|
|
1120
1120
|
</button>
|
|
1121
1121
|
</div>
|
|
1122
1122
|
{editingOptions.length === 0 ? (
|
|
1123
|
-
<div className="text-xs text-muted-foreground py-2">No options defined. Click
|
|
1123
|
+
<div className="text-xs text-muted-foreground py-2">{t('cpq.specifications.attributes.noOptions', 'No options defined. Click "Add option" to start.')}</div>
|
|
1124
1124
|
) : (
|
|
1125
1125
|
<div className="space-y-1.5">
|
|
1126
1126
|
{editingOptions.map((opt, idx) => (
|
|
@@ -1133,7 +1133,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1133
1133
|
next[idx] = { ...next[idx], value: e.target.value }
|
|
1134
1134
|
setEditingOptions(next)
|
|
1135
1135
|
}}
|
|
1136
|
-
placeholder=
|
|
1136
|
+
placeholder={t('cpq.specifications.attributes.optionValuePlaceholder', 'Value (e.g. aws)')}
|
|
1137
1137
|
className="flex-1 rounded-md border px-2 py-1.5 text-sm"
|
|
1138
1138
|
/>
|
|
1139
1139
|
<input
|
|
@@ -1144,14 +1144,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1144
1144
|
next[idx] = { ...next[idx], label: e.target.value }
|
|
1145
1145
|
setEditingOptions(next)
|
|
1146
1146
|
}}
|
|
1147
|
-
placeholder=
|
|
1147
|
+
placeholder={t('cpq.specifications.attributes.optionLabelPlaceholder', 'Label (e.g. Amazon Web Services)')}
|
|
1148
1148
|
className="flex-1 rounded-md border px-2 py-1.5 text-sm"
|
|
1149
1149
|
/>
|
|
1150
1150
|
<button
|
|
1151
1151
|
type="button"
|
|
1152
1152
|
onClick={() => setEditingOptions(editingOptions.filter((_, i) => i !== idx))}
|
|
1153
1153
|
className="text-destructive hover:text-destructive/80 text-sm px-1"
|
|
1154
|
-
title=
|
|
1154
|
+
title={t('cpq.specifications.attributes.removeOption', 'Remove')}
|
|
1155
1155
|
>
|
|
1156
1156
|
×
|
|
1157
1157
|
</button>
|
|
@@ -1169,7 +1169,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1169
1169
|
onChange={(e) => setDependsOnEnabled(e.target.checked)}
|
|
1170
1170
|
className="rounded border"
|
|
1171
1171
|
/>
|
|
1172
|
-
Depends on another attribute
|
|
1172
|
+
{t('cpq.specifications.attributes.dependsOn.enable', 'Depends on another attribute')}
|
|
1173
1173
|
</label>
|
|
1174
1174
|
{dependsOnEnabled && (() => {
|
|
1175
1175
|
const parentAttr = attributes.find((a) => a.code === dependsOnAttrCode)
|
|
@@ -1177,7 +1177,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1177
1177
|
<div className="space-y-3">
|
|
1178
1178
|
<div className="grid grid-cols-2 gap-3">
|
|
1179
1179
|
<div>
|
|
1180
|
-
<label className="block text-xs font-medium mb-1">Parent Attribute Code</label>
|
|
1180
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.dependsOn.parentAttribute', 'Parent Attribute Code')}</label>
|
|
1181
1181
|
<select
|
|
1182
1182
|
value={dependsOnAttrCode}
|
|
1183
1183
|
onChange={(e) => {
|
|
@@ -1190,14 +1190,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1190
1190
|
}}
|
|
1191
1191
|
className="w-full rounded-md border px-2 py-1.5 text-sm"
|
|
1192
1192
|
>
|
|
1193
|
-
<option value="">Select attribute
|
|
1193
|
+
<option value="">{t('cpq.specifications.attributes.dependsOn.selectAttribute', 'Select attribute...')}</option>
|
|
1194
1194
|
{attributes.filter((a) => editingAttr && a.code !== editingAttr.code).map((a) => (
|
|
1195
1195
|
<option key={a.code} value={a.code}>{a.name} ({a.code})</option>
|
|
1196
1196
|
))}
|
|
1197
1197
|
</select>
|
|
1198
1198
|
</div>
|
|
1199
1199
|
<div>
|
|
1200
|
-
<label className="block text-xs font-medium mb-1">Effect</label>
|
|
1200
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.attributes.dependsOn.effect', 'Effect')}</label>
|
|
1201
1201
|
<select
|
|
1202
1202
|
value={dependsOnEffect}
|
|
1203
1203
|
onChange={(e) => {
|
|
@@ -1209,14 +1209,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1209
1209
|
}}
|
|
1210
1210
|
className="w-full rounded-md border px-2 py-1.5 text-sm"
|
|
1211
1211
|
>
|
|
1212
|
-
<option value="filter_options">Filter options</option>
|
|
1213
|
-
<option value="set_value">Set value</option>
|
|
1214
|
-
<option value="toggle_required">Toggle required</option>
|
|
1212
|
+
<option value="filter_options">{t('cpq.specifications.attributes.dependsOn.effects.filterOptions', 'Filter options')}</option>
|
|
1213
|
+
<option value="set_value">{t('cpq.specifications.attributes.dependsOn.effects.setValue', 'Set value')}</option>
|
|
1214
|
+
<option value="toggle_required">{t('cpq.specifications.attributes.dependsOn.effects.toggleRequired', 'Toggle required')}</option>
|
|
1215
1215
|
</select>
|
|
1216
1216
|
</div>
|
|
1217
1217
|
</div>
|
|
1218
1218
|
<div>
|
|
1219
|
-
<label className="block text-xs font-medium mb-2">Rules</label>
|
|
1219
|
+
<label className="block text-xs font-medium mb-2">{t('cpq.specifications.attributes.dependsOn.rules', 'Rules')}</label>
|
|
1220
1220
|
<DependsOnRulesEditor
|
|
1221
1221
|
effect={dependsOnEffect}
|
|
1222
1222
|
rules={dependsOnRules}
|
|
@@ -1232,39 +1232,39 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1232
1232
|
<div className="flex items-end gap-4">
|
|
1233
1233
|
<label className="flex items-center gap-1.5 text-sm">
|
|
1234
1234
|
<input type="checkbox" checked={editingAttr.isRequired} onChange={(e) => setEditingAttr({ ...editingAttr, isRequired: e.target.checked })} className="rounded border" />
|
|
1235
|
-
Required
|
|
1235
|
+
{t('cpq.specifications.attributes.fields.isRequired', 'Required')}
|
|
1236
1236
|
</label>
|
|
1237
1237
|
<label className="flex items-center gap-1.5 text-sm">
|
|
1238
1238
|
<input type="checkbox" checked={editingAttr.isActive} onChange={(e) => setEditingAttr({ ...editingAttr, isActive: e.target.checked })} className="rounded border" />
|
|
1239
|
-
Active
|
|
1239
|
+
{t('cpq.common.active', 'Active')}
|
|
1240
1240
|
</label>
|
|
1241
1241
|
</div>
|
|
1242
1242
|
</div>
|
|
1243
1243
|
<div className="flex gap-2">
|
|
1244
1244
|
<button onClick={saveAttribute} disabled={saving} 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">
|
|
1245
|
-
{saving ? 'Saving...' : 'Save'}
|
|
1245
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
|
|
1246
1246
|
</button>
|
|
1247
|
-
<button onClick={() => setEditingAttr(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
|
|
1247
|
+
<button onClick={() => setEditingAttr(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>
|
|
1248
1248
|
</div>
|
|
1249
1249
|
</div>
|
|
1250
1250
|
)}
|
|
1251
1251
|
|
|
1252
1252
|
{attributes.length === 0 ? (
|
|
1253
1253
|
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
1254
|
-
No attributes defined. Add design-time and run-time attributes for this specification.
|
|
1254
|
+
{t('cpq.specifications.attributes.empty', 'No attributes defined. Add design-time and run-time attributes for this specification.')}
|
|
1255
1255
|
</div>
|
|
1256
1256
|
) : (
|
|
1257
1257
|
<div className="rounded-lg border">
|
|
1258
1258
|
<table className="w-full text-sm">
|
|
1259
1259
|
<thead>
|
|
1260
1260
|
<tr className="border-b bg-muted/50">
|
|
1261
|
-
<th className="px-4 py-3 text-left font-medium">Code</th>
|
|
1262
|
-
<th className="px-4 py-3 text-left font-medium">Name</th>
|
|
1263
|
-
<th className="px-4 py-3 text-left font-medium">Type</th>
|
|
1264
|
-
<th className="px-4 py-3 text-left font-medium">Resolution</th>
|
|
1265
|
-
<th className="px-4 py-3 text-left font-medium">Options</th>
|
|
1266
|
-
<th className="px-4 py-3 text-left font-medium">Required</th>
|
|
1267
|
-
<th className="px-4 py-3 text-right font-medium">Actions</th>
|
|
1261
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.code', 'Code')}</th>
|
|
1262
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.name', 'Name')}</th>
|
|
1263
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.type', 'Type')}</th>
|
|
1264
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.resolution', 'Resolution')}</th>
|
|
1265
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.options', 'Options')}</th>
|
|
1266
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.attributes.columns.required', 'Required')}</th>
|
|
1267
|
+
<th className="px-4 py-3 text-right font-medium">{t('cpq.specifications.attributes.columns.actions', 'Actions')}</th>
|
|
1268
1268
|
</tr>
|
|
1269
1269
|
</thead>
|
|
1270
1270
|
<tbody>
|
|
@@ -1275,14 +1275,14 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1275
1275
|
<td className="px-4 py-3">{attr.attributeType}</td>
|
|
1276
1276
|
<td className="px-4 py-3">
|
|
1277
1277
|
<Tag variant={attr.resolutionTime === 'design_time' ? 'info' : 'neutral'} className="px-2 text-xs">
|
|
1278
|
-
{attr.resolutionTime === 'design_time' ? 'design' : 'run'}
|
|
1278
|
+
{attr.resolutionTime === 'design_time' ? t('cpq.specifications.attributes.resolutionShort.design', 'design') : t('cpq.specifications.attributes.resolutionShort.run', 'run')}
|
|
1279
1279
|
</Tag>
|
|
1280
1280
|
</td>
|
|
1281
|
-
<td className="px-4 py-3 text-muted-foreground">{attr.options ?
|
|
1282
|
-
<td className="px-4 py-3">{attr.isRequired ? 'Yes' : 'No'}</td>
|
|
1281
|
+
<td className="px-4 py-3 text-muted-foreground">{attr.options ? t('cpq.specifications.attributes.optionsCount', '{count} options', { count: attr.options.length }) : '—'}</td>
|
|
1282
|
+
<td className="px-4 py-3">{attr.isRequired ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}</td>
|
|
1283
1283
|
<td className="px-4 py-3 text-right">
|
|
1284
|
-
<button onClick={() => startEditAttr(attr)} className="text-xs text-primary hover:underline mr-2">Edit</button>
|
|
1285
|
-
<button onClick={() => deleteAttribute(attr.id)} className="text-xs text-destructive hover:underline">Delete</button>
|
|
1284
|
+
<button onClick={() => startEditAttr(attr)} className="text-xs text-primary hover:underline mr-2">{t('cpq.common.edit', 'Edit')}</button>
|
|
1285
|
+
<button onClick={() => deleteAttribute(attr.id)} className="text-xs text-destructive hover:underline">{t('cpq.common.delete', 'Delete')}</button>
|
|
1286
1286
|
</td>
|
|
1287
1287
|
</tr>
|
|
1288
1288
|
))}
|
|
@@ -1308,39 +1308,39 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1308
1308
|
{editingOffering && (
|
|
1309
1309
|
<div className="rounded-lg border bg-card p-4 space-y-3">
|
|
1310
1310
|
<h3 className="font-medium text-sm">
|
|
1311
|
-
{editingOffering.id ? 'Edit Offering' : 'New Offering'}
|
|
1311
|
+
{editingOffering.id ? t('cpq.specifications.offerings.form.editTitle', 'Edit Offering') : t('cpq.specifications.offerings.form.newTitle', 'New Offering')}
|
|
1312
1312
|
</h3>
|
|
1313
1313
|
<div className="grid grid-cols-4 gap-3">
|
|
1314
1314
|
<div>
|
|
1315
|
-
<label className="block text-xs font-medium mb-1">Code</label>
|
|
1316
|
-
<input type="text" value={editingOffering.code} onChange={(e) => setEditingOffering({ ...editingOffering, code: e.target.value })} placeholder=
|
|
1315
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.code', 'Code')}</label>
|
|
1316
|
+
<input type="text" value={editingOffering.code} onChange={(e) => setEditingOffering({ ...editingOffering, code: e.target.value })} placeholder={t('cpq.specifications.offerings.fields.codePlaceholder', 'e.g. gix-cloud-connect-aws')} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
1317
1317
|
</div>
|
|
1318
1318
|
<div>
|
|
1319
|
-
<label className="block text-xs font-medium mb-1">Name</label>
|
|
1319
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.name', 'Name')}</label>
|
|
1320
1320
|
<input type="text" value={editingOffering.name} onChange={(e) => setEditingOffering({ ...editingOffering, name: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
1321
1321
|
</div>
|
|
1322
1322
|
<div>
|
|
1323
|
-
<label className="block text-xs font-medium mb-1">Lifecycle Status</label>
|
|
1323
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.lifecycleStatus', 'Lifecycle Status')}</label>
|
|
1324
1324
|
<select value={editingOffering.lifecycleStatus} onChange={(e) => setEditingOffering({ ...editingOffering, lifecycleStatus: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
|
|
1325
|
-
<option value="draft">Draft</option>
|
|
1326
|
-
<option value="active">Active</option>
|
|
1327
|
-
<option value="deprecated">Deprecated</option>
|
|
1325
|
+
<option value="draft">{t('cpq.offerings.lifecycle.draft', 'Draft')}</option>
|
|
1326
|
+
<option value="active">{t('cpq.offerings.lifecycle.active', 'Active')}</option>
|
|
1327
|
+
<option value="deprecated">{t('cpq.offerings.lifecycle.deprecated', 'Deprecated')}</option>
|
|
1328
1328
|
</select>
|
|
1329
1329
|
</div>
|
|
1330
1330
|
<div>
|
|
1331
|
-
<label className="block text-xs font-medium mb-1">Is Active
|
|
1331
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.isActive', 'Is Active?')}</label>
|
|
1332
1332
|
<div className="flex items-center h-[30px]">
|
|
1333
1333
|
<input type="checkbox" checked={editingOffering.isActive} onChange={(e) => setEditingOffering({ ...editingOffering, isActive: e.target.checked })} className="rounded border" />
|
|
1334
1334
|
</div>
|
|
1335
1335
|
</div>
|
|
1336
1336
|
</div>
|
|
1337
1337
|
<div>
|
|
1338
|
-
<label className="block text-xs font-medium mb-1">Description</label>
|
|
1338
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.offerings.fields.description', 'Description')}</label>
|
|
1339
1339
|
<input type="text" value={editingOffering.description ?? ''} onChange={(e) => setEditingOffering({ ...editingOffering, description: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
|
|
1340
1340
|
</div>
|
|
1341
1341
|
{designTimeAttrs.length > 0 ? (
|
|
1342
1342
|
<div className="space-y-3">
|
|
1343
|
-
<label className="block text-xs font-medium">Design-Time Values</label>
|
|
1343
|
+
<label className="block text-xs font-medium">{t('cpq.specifications.offerings.fields.designTimeValues', 'Design-Time Values')}</label>
|
|
1344
1344
|
<div className="grid grid-cols-2 gap-3">
|
|
1345
1345
|
{designTimeAttrs.map((attr) => (
|
|
1346
1346
|
<div key={attr.code}>
|
|
@@ -1354,7 +1354,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1354
1354
|
onChange={(e) => setEditingDtValues({ ...editingDtValues, [attr.code]: e.target.value || null })}
|
|
1355
1355
|
className="w-full rounded-md border px-2 py-1.5 text-sm"
|
|
1356
1356
|
>
|
|
1357
|
-
<option value="">Select {attr.name}
|
|
1357
|
+
<option value="">{t('cpq.specifications.offerings.fields.selectAttribute', 'Select {name}...', { name: attr.name })}</option>
|
|
1358
1358
|
{attr.options.map((opt) => (
|
|
1359
1359
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
1360
1360
|
))}
|
|
@@ -1392,32 +1392,32 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1392
1392
|
</div>
|
|
1393
1393
|
) : (
|
|
1394
1394
|
<div className="text-xs text-muted-foreground py-1">
|
|
1395
|
-
No design-time attributes defined on this specification. All attributes are run-time.
|
|
1395
|
+
{t('cpq.specifications.offerings.noDesignTimeAttributes', 'No design-time attributes defined on this specification. All attributes are run-time.')}
|
|
1396
1396
|
</div>
|
|
1397
1397
|
)}
|
|
1398
1398
|
<div className="flex gap-2">
|
|
1399
1399
|
<button onClick={saveOffering} disabled={saving} 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">
|
|
1400
|
-
{saving ? 'Saving...' : 'Save'}
|
|
1400
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
|
|
1401
1401
|
</button>
|
|
1402
|
-
<button onClick={() => setEditingOffering(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
|
|
1402
|
+
<button onClick={() => setEditingOffering(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>
|
|
1403
1403
|
</div>
|
|
1404
1404
|
</div>
|
|
1405
1405
|
)}
|
|
1406
1406
|
|
|
1407
1407
|
{offerings.length === 0 ? (
|
|
1408
1408
|
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
1409
|
-
No offerings created. An offering is a sellable instance of this specification with fixed design-time values and attached charges.
|
|
1409
|
+
{t('cpq.specifications.offerings.empty', 'No offerings created. An offering is a sellable instance of this specification with fixed design-time values and attached charges.')}
|
|
1410
1410
|
</div>
|
|
1411
1411
|
) : (
|
|
1412
1412
|
<div className="rounded-lg border">
|
|
1413
1413
|
<table className="w-full text-sm">
|
|
1414
1414
|
<thead>
|
|
1415
1415
|
<tr className="border-b bg-muted/50">
|
|
1416
|
-
<th className="px-3 py-3 text-left font-medium">Code</th>
|
|
1417
|
-
<th className="px-3 py-3 text-left font-medium">Name</th>
|
|
1418
|
-
<th className="px-3 py-3 text-left font-medium">Status</th>
|
|
1419
|
-
<th className="px-3 py-3 text-left font-medium">Design-Time Values</th>
|
|
1420
|
-
<th className="px-3 py-3 text-right font-medium">Actions</th>
|
|
1416
|
+
<th className="px-3 py-3 text-left font-medium">{t('cpq.specifications.offerings.columns.code', 'Code')}</th>
|
|
1417
|
+
<th className="px-3 py-3 text-left font-medium">{t('cpq.specifications.offerings.columns.name', 'Name')}</th>
|
|
1418
|
+
<th className="px-3 py-3 text-left font-medium">{t('cpq.specifications.offerings.columns.status', 'Status')}</th>
|
|
1419
|
+
<th className="px-3 py-3 text-left font-medium">{t('cpq.specifications.offerings.columns.designTimeValues', 'Design-Time Values')}</th>
|
|
1420
|
+
<th className="px-3 py-3 text-right font-medium">{t('cpq.specifications.offerings.columns.actions', 'Actions')}</th>
|
|
1421
1421
|
</tr>
|
|
1422
1422
|
</thead>
|
|
1423
1423
|
<tbody>
|
|
@@ -1440,8 +1440,8 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1440
1440
|
: '—'}
|
|
1441
1441
|
</td>
|
|
1442
1442
|
<td className="px-3 py-3 text-right">
|
|
1443
|
-
<button onClick={(e) => { e.stopPropagation(); startEditOffering(o) }} className="text-xs text-primary hover:underline mr-2">Edit</button>
|
|
1444
|
-
<button onClick={(e) => { e.stopPropagation(); deleteOffering(o.id) }} className="text-xs text-destructive hover:underline">Delete</button>
|
|
1443
|
+
<button onClick={(e) => { e.stopPropagation(); startEditOffering(o) }} className="text-xs text-primary hover:underline mr-2">{t('cpq.common.edit', 'Edit')}</button>
|
|
1444
|
+
<button onClick={(e) => { e.stopPropagation(); deleteOffering(o.id) }} className="text-xs text-destructive hover:underline">{t('cpq.common.delete', 'Delete')}</button>
|
|
1445
1445
|
</td>
|
|
1446
1446
|
</tr>
|
|
1447
1447
|
))}
|
|
@@ -1467,21 +1467,21 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1467
1467
|
{editingRel && (
|
|
1468
1468
|
<div className="rounded-lg border bg-card p-4 space-y-3">
|
|
1469
1469
|
<h3 className="font-medium text-sm">
|
|
1470
|
-
{editingRel.id ? 'Edit Relationship' : 'New Relationship'}
|
|
1470
|
+
{editingRel.id ? t('cpq.specifications.relationships.form.editTitle', 'Edit Relationship') : t('cpq.specifications.relationships.form.newTitle', 'New Relationship')}
|
|
1471
1471
|
</h3>
|
|
1472
1472
|
<div className="grid grid-cols-3 gap-3">
|
|
1473
1473
|
<div>
|
|
1474
|
-
<label className="block text-xs font-medium mb-1">Type</label>
|
|
1474
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.type', 'Type')}</label>
|
|
1475
1475
|
<select value={editingRel.relationshipType} onChange={(e) => setEditingRel({ ...editingRel, relationshipType: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
|
|
1476
|
-
<option value="parent_child">Parent / Child</option>
|
|
1477
|
-
<option value="requires">Requires</option>
|
|
1478
|
-
<option value="excludes">Excludes</option>
|
|
1476
|
+
<option value="parent_child">{t('cpq.specifications.relationships.type.parentChild', 'Parent / Child')}</option>
|
|
1477
|
+
<option value="requires">{t('cpq.specifications.relationships.type.requires', 'Requires')}</option>
|
|
1478
|
+
<option value="excludes">{t('cpq.specifications.relationships.type.excludes', 'Excludes')}</option>
|
|
1479
1479
|
</select>
|
|
1480
1480
|
</div>
|
|
1481
1481
|
<div>
|
|
1482
|
-
<label className="block text-xs font-medium mb-1">Target Specification</label>
|
|
1482
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.targetSpecification', 'Target Specification')}</label>
|
|
1483
1483
|
<select value={editingRel.targetSpecId} onChange={(e) => setEditingRel({ ...editingRel, targetSpecId: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
|
|
1484
|
-
<option value="">Select
|
|
1484
|
+
<option value="">{t('cpq.specifications.relationships.fields.selectTarget', 'Select...')}</option>
|
|
1485
1485
|
{allSpecs.filter((s) => s.id !== specId).map((s) => (
|
|
1486
1486
|
<option key={s.id} value={s.id}>{s.name} ({s.code})</option>
|
|
1487
1487
|
))}
|
|
@@ -1489,7 +1489,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1489
1489
|
</div>
|
|
1490
1490
|
<div className="grid grid-cols-2 gap-2">
|
|
1491
1491
|
<div>
|
|
1492
|
-
<label className="block text-xs font-medium mb-1">Min</label>
|
|
1492
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.min', 'Min')}</label>
|
|
1493
1493
|
<NumberInput
|
|
1494
1494
|
integer
|
|
1495
1495
|
min={0}
|
|
@@ -1498,7 +1498,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1498
1498
|
/>
|
|
1499
1499
|
</div>
|
|
1500
1500
|
<div>
|
|
1501
|
-
<label className="block text-xs font-medium mb-1">Max</label>
|
|
1501
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.max', 'Max')}</label>
|
|
1502
1502
|
<NumberInput
|
|
1503
1503
|
integer
|
|
1504
1504
|
min={0}
|
|
@@ -1510,33 +1510,33 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1510
1510
|
</div>
|
|
1511
1511
|
</div>
|
|
1512
1512
|
<div>
|
|
1513
|
-
<label className="block text-xs font-medium mb-1">Condition (JSON)</label>
|
|
1514
|
-
<textarea value={conditionJson} onChange={(e) => setConditionJson(e.target.value)} rows={3} className="w-full rounded-md border px-2 py-1.5 text-sm font-mono" placeholder='null or {"attribute":"direction","operator":"in","value":["inbound"]}' />
|
|
1513
|
+
<label className="block text-xs font-medium mb-1">{t('cpq.specifications.relationships.fields.condition', 'Condition (JSON)')}</label>
|
|
1514
|
+
<textarea value={conditionJson} onChange={(e) => setConditionJson(e.target.value)} rows={3} className="w-full rounded-md border px-2 py-1.5 text-sm font-mono" placeholder={t('cpq.specifications.relationships.fields.conditionPlaceholder', 'null or {"attribute":"direction","operator":"in","value":["inbound"]}')} />
|
|
1515
1515
|
</div>
|
|
1516
1516
|
<div className="flex gap-2">
|
|
1517
1517
|
<button onClick={saveRelationship} disabled={saving} 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">
|
|
1518
|
-
{saving ? 'Saving...' : 'Save'}
|
|
1518
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
|
|
1519
1519
|
</button>
|
|
1520
|
-
<button onClick={() => setEditingRel(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
|
|
1520
|
+
<button onClick={() => setEditingRel(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>
|
|
1521
1521
|
</div>
|
|
1522
1522
|
</div>
|
|
1523
1523
|
)}
|
|
1524
1524
|
|
|
1525
1525
|
{relationships.length === 0 ? (
|
|
1526
1526
|
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
1527
|
-
No relationships defined. Declare parent/child, requires, or excludes relationships with other specifications.
|
|
1527
|
+
{t('cpq.specifications.relationships.empty', 'No relationships defined. Declare parent/child, requires, or excludes relationships with other specifications.')}
|
|
1528
1528
|
</div>
|
|
1529
1529
|
) : (
|
|
1530
1530
|
<div className="rounded-lg border">
|
|
1531
1531
|
<table className="w-full text-sm">
|
|
1532
1532
|
<thead>
|
|
1533
1533
|
<tr className="border-b bg-muted/50">
|
|
1534
|
-
<th className="px-4 py-3 text-left font-medium">Type</th>
|
|
1535
|
-
<th className="px-4 py-3 text-left font-medium">Source</th>
|
|
1536
|
-
<th className="px-4 py-3 text-left font-medium">Target</th>
|
|
1537
|
-
<th className="px-4 py-3 text-left font-medium">Cardinality</th>
|
|
1538
|
-
<th className="px-4 py-3 text-left font-medium">Condition</th>
|
|
1539
|
-
<th className="px-4 py-3 text-right font-medium">Actions</th>
|
|
1534
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.type', 'Type')}</th>
|
|
1535
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.source', 'Source')}</th>
|
|
1536
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.target', 'Target')}</th>
|
|
1537
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.cardinality', 'Cardinality')}</th>
|
|
1538
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.relationships.columns.condition', 'Condition')}</th>
|
|
1539
|
+
<th className="px-4 py-3 text-right font-medium">{t('cpq.specifications.relationships.columns.actions', 'Actions')}</th>
|
|
1540
1540
|
</tr>
|
|
1541
1541
|
</thead>
|
|
1542
1542
|
<tbody>
|
|
@@ -1558,8 +1558,8 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1558
1558
|
<td className="px-4 py-3 text-muted-foreground">{rel.cardinalityMin}..{rel.cardinalityMax ?? '∞'}</td>
|
|
1559
1559
|
<td className="px-4 py-3 text-muted-foreground text-xs">{rel.condition ? JSON.stringify(rel.condition) : '—'}</td>
|
|
1560
1560
|
<td className="px-4 py-3 text-right">
|
|
1561
|
-
<button onClick={() => startEditRel(rel)} className="text-xs text-primary hover:underline mr-2">Edit</button>
|
|
1562
|
-
<button onClick={() => deleteRelationship(rel.id)} className="text-xs text-destructive hover:underline">Delete</button>
|
|
1561
|
+
<button onClick={() => startEditRel(rel)} className="text-xs text-primary hover:underline mr-2">{t('cpq.common.edit', 'Edit')}</button>
|
|
1562
|
+
<button onClick={() => deleteRelationship(rel.id)} className="text-xs text-destructive hover:underline">{t('cpq.common.delete', 'Delete')}</button>
|
|
1563
1563
|
</td>
|
|
1564
1564
|
</tr>
|
|
1565
1565
|
))}
|
|
@@ -1578,49 +1578,49 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1578
1578
|
onClick={() => setEditingSlot({ ...EMPTY_SLOT })}
|
|
1579
1579
|
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
1580
1580
|
>
|
|
1581
|
-
+ Add Slot
|
|
1581
|
+
{t('cpq.specifications.slots.add', '+ Add Slot')}
|
|
1582
1582
|
</button>
|
|
1583
1583
|
</div>
|
|
1584
1584
|
|
|
1585
1585
|
{editingSlot && (
|
|
1586
1586
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
1587
|
-
<h3 className="text-sm font-semibold">{editingSlot.id ? 'Edit Bundle Slot' : 'New Bundle Slot'}</h3>
|
|
1587
|
+
<h3 className="text-sm font-semibold">{editingSlot.id ? t('cpq.specifications.slots.form.editTitle', 'Edit Bundle Slot') : t('cpq.specifications.slots.form.newTitle', 'New Bundle Slot')}</h3>
|
|
1588
1588
|
<div className="grid grid-cols-2 gap-4">
|
|
1589
1589
|
<div>
|
|
1590
|
-
<label className="block text-sm font-medium mb-1">Name</label>
|
|
1590
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.name', 'Name')}</label>
|
|
1591
1591
|
<input
|
|
1592
1592
|
type="text"
|
|
1593
1593
|
value={editingSlot.name}
|
|
1594
1594
|
onChange={(e) => setEditingSlot({ ...editingSlot, name: e.target.value })}
|
|
1595
|
-
placeholder=
|
|
1595
|
+
placeholder={t('cpq.specifications.slots.fields.namePlaceholder', 'e.g. Internet Component')}
|
|
1596
1596
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
1597
1597
|
/>
|
|
1598
1598
|
</div>
|
|
1599
1599
|
<div>
|
|
1600
|
-
<label className="block text-sm font-medium mb-1">Component Group</label>
|
|
1600
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.componentGroup', 'Component Group')}</label>
|
|
1601
1601
|
<input
|
|
1602
1602
|
type="text"
|
|
1603
1603
|
value={editingSlot.componentGroup}
|
|
1604
1604
|
onChange={(e) => setEditingSlot({ ...editingSlot, componentGroup: e.target.value })}
|
|
1605
|
-
placeholder=
|
|
1605
|
+
placeholder={t('cpq.specifications.slots.fields.componentGroupPlaceholder', 'e.g. internet, tv, phone')}
|
|
1606
1606
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
1607
1607
|
/>
|
|
1608
1608
|
</div>
|
|
1609
1609
|
<div>
|
|
1610
|
-
<label className="block text-sm font-medium mb-1">Target Specification</label>
|
|
1610
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.targetSpecification', 'Target Specification')}</label>
|
|
1611
1611
|
<select
|
|
1612
1612
|
value={editingSlot.targetSpecId}
|
|
1613
1613
|
onChange={(e) => setEditingSlot({ ...editingSlot, targetSpecId: e.target.value })}
|
|
1614
1614
|
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
1615
1615
|
>
|
|
1616
|
-
<option value=""
|
|
1616
|
+
<option value="">{t('cpq.specifications.slots.fields.selectSpecification', '— Select specification —')}</option>
|
|
1617
1617
|
{allSpecs.filter((s) => s.id !== specId).map((s) => (
|
|
1618
1618
|
<option key={s.id} value={s.id}>{s.name} ({s.code})</option>
|
|
1619
1619
|
))}
|
|
1620
1620
|
</select>
|
|
1621
1621
|
</div>
|
|
1622
1622
|
<div>
|
|
1623
|
-
<label className="block text-sm font-medium mb-1">Sort Order</label>
|
|
1623
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.sortOrder', 'Sort Order')}</label>
|
|
1624
1624
|
<NumberInput
|
|
1625
1625
|
integer
|
|
1626
1626
|
value={editingSlot.sortOrder}
|
|
@@ -1628,7 +1628,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1628
1628
|
/>
|
|
1629
1629
|
</div>
|
|
1630
1630
|
<div>
|
|
1631
|
-
<label className="block text-sm font-medium mb-1">Cardinality Min</label>
|
|
1631
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.cardinalityMin', 'Cardinality Min')}</label>
|
|
1632
1632
|
<NumberInput
|
|
1633
1633
|
integer
|
|
1634
1634
|
min={0}
|
|
@@ -1637,18 +1637,18 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1637
1637
|
/>
|
|
1638
1638
|
</div>
|
|
1639
1639
|
<div>
|
|
1640
|
-
<label className="block text-sm font-medium mb-1">Cardinality Max</label>
|
|
1640
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.cardinalityMax', 'Cardinality Max')}</label>
|
|
1641
1641
|
<NumberInput
|
|
1642
1642
|
integer
|
|
1643
1643
|
min={0}
|
|
1644
|
-
placeholder=
|
|
1644
|
+
placeholder={t('cpq.specifications.slots.fields.cardinalityMaxPlaceholder', '∞ (unlimited)')}
|
|
1645
1645
|
value={editingSlot.cardinalityMax}
|
|
1646
1646
|
onChange={(n) => setEditingSlot({ ...editingSlot, cardinalityMax: n })}
|
|
1647
1647
|
/>
|
|
1648
1648
|
</div>
|
|
1649
1649
|
</div>
|
|
1650
1650
|
<div>
|
|
1651
|
-
<label className="block text-sm font-medium mb-1">Description</label>
|
|
1651
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.specifications.slots.fields.description', 'Description')}</label>
|
|
1652
1652
|
<textarea
|
|
1653
1653
|
value={editingSlot.description ?? ''}
|
|
1654
1654
|
onChange={(e) => setEditingSlot({ ...editingSlot, description: e.target.value })}
|
|
@@ -1673,7 +1673,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1673
1673
|
})
|
|
1674
1674
|
if (!res.ok) {
|
|
1675
1675
|
const d = await res.json()
|
|
1676
|
-
setError(d.error ?? 'Failed to save slot')
|
|
1676
|
+
setError(d.error ?? t('cpq.specifications.slots.error.save', 'Failed to save slot'))
|
|
1677
1677
|
return
|
|
1678
1678
|
}
|
|
1679
1679
|
setEditingSlot(null)
|
|
@@ -1683,7 +1683,7 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1683
1683
|
setBundleSlots(d.items ?? [])
|
|
1684
1684
|
}
|
|
1685
1685
|
} catch {
|
|
1686
|
-
setError('Failed to save slot')
|
|
1686
|
+
setError(t('cpq.specifications.slots.error.save', 'Failed to save slot'))
|
|
1687
1687
|
} finally {
|
|
1688
1688
|
setSaving(false)
|
|
1689
1689
|
}
|
|
@@ -1691,13 +1691,13 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1691
1691
|
disabled={saving || !editingSlot.name || !editingSlot.componentGroup || !editingSlot.targetSpecId}
|
|
1692
1692
|
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
1693
1693
|
>
|
|
1694
|
-
{saving ? 'Saving...' : (editingSlot.id ? 'Update' : 'Create')}
|
|
1694
|
+
{saving ? t('cpq.common.saving', 'Saving...') : (editingSlot.id ? t('cpq.specifications.slots.form.update', 'Update') : t('cpq.specifications.slots.form.create', 'Create'))}
|
|
1695
1695
|
</button>
|
|
1696
1696
|
<button
|
|
1697
1697
|
onClick={() => setEditingSlot(null)}
|
|
1698
1698
|
className="inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted"
|
|
1699
1699
|
>
|
|
1700
|
-
Cancel
|
|
1700
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
1701
1701
|
</button>
|
|
1702
1702
|
</div>
|
|
1703
1703
|
</div>
|
|
@@ -1705,19 +1705,19 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1705
1705
|
|
|
1706
1706
|
{bundleSlots.length === 0 && !editingSlot ? (
|
|
1707
1707
|
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
1708
|
-
No bundle slots defined yet. Add slots to define what component specifications can fill this bundle.
|
|
1708
|
+
{t('cpq.specifications.slots.empty', 'No bundle slots defined yet. Add slots to define what component specifications can fill this bundle.')}
|
|
1709
1709
|
</div>
|
|
1710
1710
|
) : bundleSlots.length > 0 && (
|
|
1711
1711
|
<div className="rounded-lg border overflow-hidden">
|
|
1712
1712
|
<table className="w-full text-sm">
|
|
1713
1713
|
<thead>
|
|
1714
1714
|
<tr className="border-b bg-muted/50">
|
|
1715
|
-
<th className="px-4 py-3 text-left font-medium">Name</th>
|
|
1716
|
-
<th className="px-4 py-3 text-left font-medium">Group</th>
|
|
1717
|
-
<th className="px-4 py-3 text-left font-medium">Target Spec</th>
|
|
1718
|
-
<th className="px-4 py-3 text-left font-medium">Cardinality</th>
|
|
1719
|
-
<th className="px-4 py-3 text-left font-medium">Order</th>
|
|
1720
|
-
<th className="px-4 py-3 text-right font-medium">Actions</th>
|
|
1715
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.name', 'Name')}</th>
|
|
1716
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.group', 'Group')}</th>
|
|
1717
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.targetSpec', 'Target Spec')}</th>
|
|
1718
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.cardinality', 'Cardinality')}</th>
|
|
1719
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.specifications.slots.columns.order', 'Order')}</th>
|
|
1720
|
+
<th className="px-4 py-3 text-right font-medium">{t('cpq.specifications.slots.columns.actions', 'Actions')}</th>
|
|
1721
1721
|
</tr>
|
|
1722
1722
|
</thead>
|
|
1723
1723
|
<tbody>
|
|
@@ -1732,11 +1732,11 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1732
1732
|
<span>
|
|
1733
1733
|
{slot.targetSpec.name}
|
|
1734
1734
|
{slot.targetSpec.specType === 'bundle' && (
|
|
1735
|
-
<Tag variant="brand" className="ml-1 px-1.5 text-[10px]">bundle</Tag>
|
|
1735
|
+
<Tag variant="brand" className="ml-1 px-1.5 text-[10px]">{t('cpq.specifications.slots.tag.bundle', 'bundle')}</Tag>
|
|
1736
1736
|
)}
|
|
1737
1737
|
</span>
|
|
1738
1738
|
) : (
|
|
1739
|
-
<span className="text-muted-foreground">Unknown</span>
|
|
1739
|
+
<span className="text-muted-foreground">{t('cpq.specifications.slots.unknown', 'Unknown')}</span>
|
|
1740
1740
|
)}
|
|
1741
1741
|
</td>
|
|
1742
1742
|
<td className="px-4 py-3 text-muted-foreground">{slot.cardinalityMin}..{slot.cardinalityMax ?? '∞'}</td>
|
|
@@ -1755,11 +1755,11 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1755
1755
|
})}
|
|
1756
1756
|
className="text-xs text-primary hover:underline mr-2"
|
|
1757
1757
|
>
|
|
1758
|
-
Edit
|
|
1758
|
+
{t('cpq.common.edit', 'Edit')}
|
|
1759
1759
|
</button>
|
|
1760
1760
|
<button
|
|
1761
1761
|
onClick={async () => {
|
|
1762
|
-
if (!confirm('Delete this bundle slot?')) return
|
|
1762
|
+
if (!confirm(t('cpq.specifications.slots.confirmDelete', 'Delete this bundle slot?'))) return
|
|
1763
1763
|
try {
|
|
1764
1764
|
const res = await fetch('/api/cpq/bundle-slots', {
|
|
1765
1765
|
method: 'DELETE',
|
|
@@ -1770,15 +1770,15 @@ export default function SpecificationDetailPage(props: { params?: { id?: string
|
|
|
1770
1770
|
setBundleSlots((prev) => prev.filter((s) => s.id !== slot.id))
|
|
1771
1771
|
} else {
|
|
1772
1772
|
const d = await res.json()
|
|
1773
|
-
setError(d.error ?? 'Failed to delete slot')
|
|
1773
|
+
setError(d.error ?? t('cpq.specifications.slots.error.delete', 'Failed to delete slot'))
|
|
1774
1774
|
}
|
|
1775
1775
|
} catch {
|
|
1776
|
-
setError('Failed to delete slot')
|
|
1776
|
+
setError(t('cpq.specifications.slots.error.delete', 'Failed to delete slot'))
|
|
1777
1777
|
}
|
|
1778
1778
|
}}
|
|
1779
1779
|
className="text-xs text-destructive hover:underline"
|
|
1780
1780
|
>
|
|
1781
|
-
Delete
|
|
1781
|
+
{t('cpq.common.delete', 'Delete')}
|
|
1782
1782
|
</button>
|
|
1783
1783
|
</td>
|
|
1784
1784
|
</tr>
|