@dainamite/cpq 0.5.0 → 0.6.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 +48 -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/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +22 -8
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +89 -232
- package/dist/modules/cpq/backend/cpq/orders/[id]/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/quotes/[id]/_components/PriceAdjustmentDialog.js +205 -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 +144 -221
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
- 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/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 +154 -0
- package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
- package/dist/modules/cpq/components/PriceWaterfall.js +66 -0
- package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
- package/dist/modules/cpq/components/PricingSummaryCard.js +117 -0
- package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
- 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/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/package.json +1 -1
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -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/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +32 -10
- package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +60 -269
- package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
- 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 +174 -263
- package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +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 +201 -0
- package/src/modules/cpq/components/PriceWaterfall.tsx +94 -0
- package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
- package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
- 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/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
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
3
4
|
import { useRouter, useParams, useSearchParams } from 'next/navigation'
|
|
4
5
|
import ArcQuoteConfigurator, {
|
|
5
6
|
type AttachedTarget,
|
|
6
7
|
} from './_components/ArcQuoteConfigurator'
|
|
7
8
|
import { StatusPath } from '../../../../components/StatusPath'
|
|
8
|
-
import { ArrowRight, ExternalLink, Plus } from 'lucide-react'
|
|
9
|
+
import { ArrowRight, ExternalLink, Plus, RotateCcw } from 'lucide-react'
|
|
9
10
|
import { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
10
11
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
11
12
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
12
13
|
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
13
14
|
import type { ActionItem } from '@open-mercato/ui/backend/forms/ActionsDropdown'
|
|
14
15
|
import { NumberInput } from '../../../../components/NumberInput'
|
|
16
|
+
import { ChargeBreakdownTable } from '../../../../components/ChargeBreakdownTable'
|
|
17
|
+
import { PricingSummaryCard } from '../../../../components/PricingSummaryCard'
|
|
18
|
+
import { ArcLineDiff, LineActionBadge } from '../../../../components/ArcLineDiff'
|
|
19
|
+
import { ADJUST_PRICE_FEATURE } from '../../../../lib/priceAdjustmentView'
|
|
20
|
+
import { arcRecurringDelta } from '../../../../lib/quoteSummaryView'
|
|
21
|
+
import { usePriceAdjustments } from './_components/usePriceAdjustments'
|
|
22
|
+
import { PriceAdjustmentsPanel } from './_components/PriceAdjustmentsPanel'
|
|
23
|
+
import { ArcLineRemovalProvider, useArcLineRemoval, useArcLineRemovalUi } from './_components/arcLineRemoval'
|
|
24
|
+
import { AdjustChargeAction, DiscountLineAction, ManualAdjustmentActions, PriceAdjustmentProvider, usePriceAdjustmentUi } from './_components/priceAdjustmentContext'
|
|
25
|
+
import { useGrantedFeatures } from '../../../../components/useGrantedFeatures'
|
|
26
|
+
import { canRecalculateQuote, QUOTE_MANAGE_FEATURE } from '../../../../lib/quotePermissions'
|
|
15
27
|
import {
|
|
16
28
|
formatStatusLabel,
|
|
17
29
|
quoteCpqStatusMap,
|
|
@@ -32,7 +44,12 @@ type ConstrainedAttribute = {
|
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
type TierBreakdownEntry = { tier: number; from: number; to: number; pricePerUnit: number; quantity: number; subtotal: number }
|
|
35
|
-
type RuleAdjustment = {
|
|
47
|
+
type RuleAdjustment = {
|
|
48
|
+
ruleCode: string; ruleName: string; ruleType: string; value: number; unitPriceBefore: number; unitPriceAfter: number; delta: number
|
|
49
|
+
/** Negotiated-pricing fields; absent on legacy snapshots (treated as rules). */
|
|
50
|
+
origin?: 'rule' | 'manual'; adjustmentId?: string; reasonCode?: string; createdByUserId?: string
|
|
51
|
+
}
|
|
52
|
+
type AdjustmentErrorEntry = { adjustmentId: string; quoteLineId: string; chargeCode?: string; code: string }
|
|
36
53
|
|
|
37
54
|
type ResolvedCharge = {
|
|
38
55
|
chargeCode: string
|
|
@@ -46,6 +63,11 @@ type ResolvedCharge = {
|
|
|
46
63
|
breakdown?: { tiers: TierBreakdownEntry[] } | null
|
|
47
64
|
note?: string | null
|
|
48
65
|
adjustments?: RuleAdjustment[] | null
|
|
66
|
+
listUnitPrice?: number
|
|
67
|
+
listTotalPrice?: number | null
|
|
68
|
+
standardUnitPrice?: number
|
|
69
|
+
standardTotalPrice?: number | null
|
|
70
|
+
adjustmentErrors?: AdjustmentErrorEntry[]
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
type QuoteLineAction = 'add' | 'modify' | 'cancel'
|
|
@@ -61,6 +83,7 @@ type ArcLineSource = {
|
|
|
61
83
|
type QuoteLine = {
|
|
62
84
|
lineId: string
|
|
63
85
|
offeringId: string | null
|
|
86
|
+
productId?: string | null
|
|
64
87
|
offeringName: string
|
|
65
88
|
offeringType: string | null
|
|
66
89
|
parentLineId: string | null
|
|
@@ -84,15 +107,23 @@ type QuoteResult = {
|
|
|
84
107
|
customerName: string | null
|
|
85
108
|
cpqStatus: string
|
|
86
109
|
version: number
|
|
110
|
+
pricingRevision?: number
|
|
87
111
|
currencyCode: string
|
|
88
112
|
convertedOrderId: string | null
|
|
89
113
|
validationResult: { valid: boolean; errors: Array<{ message: string }> }
|
|
114
|
+
adjustmentErrors?: AdjustmentErrorEntry[]
|
|
90
115
|
pricingSummary: {
|
|
91
116
|
nrcTotal: number
|
|
92
117
|
mrcTotal: number
|
|
93
118
|
usageCharges: Array<{ chargeCode: string; chargeName: string; unitPrice: number; note: string | null }>
|
|
94
119
|
usageTotalEstimated: number | null
|
|
95
120
|
currencyCode: string
|
|
121
|
+
listNrcTotal?: number
|
|
122
|
+
listMrcTotal?: number
|
|
123
|
+
dealValue?: { list: number; standard: number; offer: number }
|
|
124
|
+
discountDepth?: { rule: number | null; manual: number | null; total: number | null; maxLineManual: number | null }
|
|
125
|
+
ruleDiscountTotal?: number
|
|
126
|
+
manualDiscountTotal?: number
|
|
96
127
|
}
|
|
97
128
|
lines: QuoteLine[]
|
|
98
129
|
}
|
|
@@ -138,20 +169,6 @@ function fmt(amount: number, currency: string): string {
|
|
|
138
169
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
|
|
139
170
|
}
|
|
140
171
|
|
|
141
|
-
const RULE_TYPE_LABELS: Record<string, string> = {
|
|
142
|
-
discount_percent: '% discount',
|
|
143
|
-
discount_absolute: 'fixed discount',
|
|
144
|
-
surcharge_percent: '% surcharge',
|
|
145
|
-
surcharge_absolute: 'fixed surcharge',
|
|
146
|
-
price_override: 'override',
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function fmtRuleValue(adj: RuleAdjustment, currency?: string): string {
|
|
150
|
-
if (adj.ruleType === 'discount_percent' || adj.ruleType === 'surcharge_percent') return `${adj.value}%`
|
|
151
|
-
if (currency) return fmt(adj.value, currency)
|
|
152
|
-
return String(adj.value)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
172
|
async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
156
173
|
const res = await fetch(url, init)
|
|
157
174
|
const json = await res.json()
|
|
@@ -172,6 +189,8 @@ function MetaCard({ label, children }: { label: string; children: React.ReactNod
|
|
|
172
189
|
)
|
|
173
190
|
}
|
|
174
191
|
|
|
192
|
+
// Pricing-summary figure: label, monospace amount, and an optional smaller
|
|
193
|
+
// bracketed note (discount depth / change %) inline after the amount.
|
|
175
194
|
function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
|
|
176
195
|
// Local dropdown — `ActionsDropdown` inside FormHeader.actionsContent
|
|
177
196
|
// would conflict with the inline button placement. This is a thin
|
|
@@ -231,26 +250,31 @@ function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
|
|
|
231
250
|
|
|
232
251
|
function buildMoreActions({
|
|
233
252
|
recalculate,
|
|
253
|
+
canRecalculate,
|
|
234
254
|
submitting,
|
|
235
255
|
deletable,
|
|
236
256
|
onDelete,
|
|
237
257
|
deleting,
|
|
238
258
|
}: {
|
|
239
259
|
recalculate: () => void
|
|
260
|
+
canRecalculate: boolean
|
|
240
261
|
submitting: boolean
|
|
241
262
|
deletable: boolean
|
|
242
263
|
onDelete: () => void
|
|
243
264
|
deleting: boolean
|
|
244
265
|
}): ActionItem[] {
|
|
245
|
-
const items: ActionItem[] = [
|
|
246
|
-
|
|
266
|
+
const items: ActionItem[] = []
|
|
267
|
+
// Recalculate persists prices: offered only to users with cpq.quotes.manage
|
|
268
|
+
// and only on non-terminal quotes (the API refuses both otherwise).
|
|
269
|
+
if (canRecalculate) {
|
|
270
|
+
items.push({
|
|
247
271
|
id: 'recalculate',
|
|
248
272
|
label: 'Recalculate',
|
|
249
273
|
onSelect: recalculate,
|
|
250
274
|
loading: submitting,
|
|
251
275
|
disabled: submitting,
|
|
252
|
-
}
|
|
253
|
-
|
|
276
|
+
})
|
|
277
|
+
}
|
|
254
278
|
if (deletable) {
|
|
255
279
|
items.push({
|
|
256
280
|
id: 'delete',
|
|
@@ -272,7 +296,9 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
272
296
|
const cpqConfigId = (props.params?.id ?? urlParams?.id) as string
|
|
273
297
|
const isNew = cpqConfigId === 'new'
|
|
274
298
|
|
|
299
|
+
const t = useT()
|
|
275
300
|
const [cpqQuote, setCpqQuote] = React.useState<QuoteResult | null>(null)
|
|
301
|
+
const grantedFeatures = useGrantedFeatures([QUOTE_MANAGE_FEATURE, ADJUST_PRICE_FEATURE])
|
|
276
302
|
const [loading, setLoading] = React.useState(true)
|
|
277
303
|
const [error, setError] = React.useState<string | null>(null)
|
|
278
304
|
const [submitting, setSubmitting] = React.useState(false)
|
|
@@ -327,6 +353,22 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
327
353
|
void loadArcTargets()
|
|
328
354
|
}, [cpqConfigId, isNew, loadArcTargets])
|
|
329
355
|
|
|
356
|
+
// ─── Negotiated prices ─────────────────────────────────────────
|
|
357
|
+
const reloadQuote = React.useCallback(async () => {
|
|
358
|
+
try {
|
|
359
|
+
setCpqQuote(await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqConfigId}`))
|
|
360
|
+
} catch (err) {
|
|
361
|
+
setError((err as Error).message)
|
|
362
|
+
}
|
|
363
|
+
}, [cpqConfigId])
|
|
364
|
+
const priceAdjustments = usePriceAdjustments<QuoteResult>({
|
|
365
|
+
quote: isNew ? null : cpqQuote,
|
|
366
|
+
quoteType: arcQuoteType,
|
|
367
|
+
canAdjust: grantedFeatures.includes(QUOTE_MANAGE_FEATURE) && grantedFeatures.includes(ADJUST_PRICE_FEATURE),
|
|
368
|
+
onQuote: setCpqQuote,
|
|
369
|
+
reloadQuote,
|
|
370
|
+
})
|
|
371
|
+
|
|
330
372
|
// ─── Load or create ──────────────────────────────────────────
|
|
331
373
|
|
|
332
374
|
React.useEffect(() => {
|
|
@@ -436,14 +478,15 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
436
478
|
finally { setSubmitting(false) }
|
|
437
479
|
}
|
|
438
480
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
481
|
+
// Amend / Renew: removing an existing subscription item confirms first and
|
|
482
|
+
// marks it for removal (a `cancel` line) instead of deleting the line.
|
|
483
|
+
const lineRemoval = useArcLineRemoval<QuoteResult>({
|
|
484
|
+
quote: isNew ? null : cpqQuote,
|
|
485
|
+
quoteType: arcQuoteType,
|
|
486
|
+
onQuote: setCpqQuote,
|
|
487
|
+
onError: setError,
|
|
488
|
+
})
|
|
489
|
+
const removeItem = lineRemoval.remove
|
|
447
490
|
|
|
448
491
|
const recalculate = async () => {
|
|
449
492
|
if (!cpqQuote) return
|
|
@@ -582,6 +625,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
582
625
|
// ─── Render: detail ──────────────────────────────────────────
|
|
583
626
|
|
|
584
627
|
const { pricingSummary: summary, currencyCode: currency } = cpqQuote
|
|
628
|
+
const arcDelta = arcQuoteType === 'amend' || arcQuoteType === 'renew' ? arcRecurringDelta(cpqQuote.lines) : null
|
|
585
629
|
|
|
586
630
|
const arcEditable = ['new', 'incomplete', 'ready'].includes(cpqQuote.cpqStatus)
|
|
587
631
|
const isMergeRenew =
|
|
@@ -601,7 +645,11 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
601
645
|
}
|
|
602
646
|
|
|
603
647
|
return (
|
|
648
|
+
<PriceAdjustmentProvider value={priceAdjustments.ui}>
|
|
649
|
+
<ArcLineRemovalProvider value={lineRemoval.ui}>
|
|
604
650
|
<div className="space-y-6">
|
|
651
|
+
{priceAdjustments.elements}
|
|
652
|
+
{lineRemoval.ConfirmDialogElement}
|
|
605
653
|
{arcDrawerOpen && (
|
|
606
654
|
<ArcQuoteConfigurator
|
|
607
655
|
quoteId={cpqQuote.id}
|
|
@@ -693,6 +741,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
693
741
|
<MoreActionsDropdown
|
|
694
742
|
items={buildMoreActions({
|
|
695
743
|
recalculate,
|
|
744
|
+
canRecalculate: canRecalculateQuote(grantedFeatures, cpqQuote.cpqStatus),
|
|
696
745
|
submitting,
|
|
697
746
|
deletable: !['accepted', 'rejected', 'cancelled'].includes(cpqQuote.cpqStatus),
|
|
698
747
|
onDelete: () => setShowDeleteConfirm(true),
|
|
@@ -816,34 +865,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
816
865
|
{/* Summary view */}
|
|
817
866
|
{view === 'summary' && (
|
|
818
867
|
<>
|
|
819
|
-
{
|
|
820
|
-
<div className="rounded-lg border bg-card p-4">
|
|
821
|
-
<h3 className="text-sm font-medium mb-3">Pricing Summary</h3>
|
|
822
|
-
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
823
|
-
<div>
|
|
824
|
-
<p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
|
|
825
|
-
<p className="text-lg font-semibold font-mono">{fmt(summary.nrcTotal, currency)}</p>
|
|
826
|
-
</div>
|
|
827
|
-
<div>
|
|
828
|
-
<p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
|
|
829
|
-
<p className="text-lg font-semibold font-mono">{fmt(summary.mrcTotal, currency)}</p>
|
|
830
|
-
</div>
|
|
831
|
-
{summary.usageTotalEstimated != null && (
|
|
832
|
-
<div>
|
|
833
|
-
<p className="text-xs text-muted-foreground">Usage (est.)</p>
|
|
834
|
-
<p className="text-lg font-semibold font-mono">{fmt(summary.usageTotalEstimated, currency)}</p>
|
|
835
|
-
</div>
|
|
836
|
-
)}
|
|
837
|
-
{summary.usageCharges.length > 0 && (
|
|
838
|
-
<div>
|
|
839
|
-
<p className="text-xs text-muted-foreground">Usage Charges</p>
|
|
840
|
-
{summary.usageCharges.map((uc, i) => (
|
|
841
|
-
<p key={i} className="text-xs mt-0.5">{uc.chargeName}: {fmt(uc.unitPrice, currency)}/unit</p>
|
|
842
|
-
))}
|
|
843
|
-
</div>
|
|
844
|
-
)}
|
|
845
|
-
</div>
|
|
846
|
-
</div>
|
|
868
|
+
<PricingSummaryCard summary={summary} currency={currency} arcDelta={arcDelta} />
|
|
847
869
|
|
|
848
870
|
{/* Validation errors */}
|
|
849
871
|
{!cpqQuote.validationResult.valid && (
|
|
@@ -863,6 +885,23 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
863
885
|
</Alert>
|
|
864
886
|
)}
|
|
865
887
|
|
|
888
|
+
{/* Amend lines the rep reconfigured are re-priced at today's catalog
|
|
889
|
+
price (server marks them `_arcRepriced`); negotiated prices are
|
|
890
|
+
not carried over yet (later release). */}
|
|
891
|
+
{arcQuoteType === 'amend' && cpqQuote.lines.some((l) => l.action !== 'cancel' && l.configuration?._arcRepriced === true) && (
|
|
892
|
+
<Alert status="information" style="lighter">
|
|
893
|
+
<AlertTitle>
|
|
894
|
+
{t('cpq.quotes.arc.repricedTitle', 'Some existing items use the current catalog price')}
|
|
895
|
+
</AlertTitle>
|
|
896
|
+
<AlertDescription>
|
|
897
|
+
{t(
|
|
898
|
+
'cpq.quotes.arc.repricedDescription',
|
|
899
|
+
"Reconfigured lines were re-priced at today's catalog price. Negotiated prices from the subscription are not carried over to reconfigured lines yet.",
|
|
900
|
+
)}
|
|
901
|
+
</AlertDescription>
|
|
902
|
+
</Alert>
|
|
903
|
+
)}
|
|
904
|
+
|
|
866
905
|
{/* Quote Lines */}
|
|
867
906
|
<QuoteLineTree
|
|
868
907
|
lines={cpqQuote.lines}
|
|
@@ -882,6 +921,15 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
882
921
|
)
|
|
883
922
|
}
|
|
884
923
|
/>
|
|
924
|
+
|
|
925
|
+
<PriceAdjustmentsPanel
|
|
926
|
+
quoteId={cpqQuote.id}
|
|
927
|
+
items={priceAdjustments.items}
|
|
928
|
+
total={priceAdjustments.total}
|
|
929
|
+
lines={cpqQuote.lines}
|
|
930
|
+
currency={currency}
|
|
931
|
+
errorsById={priceAdjustments.errorsById}
|
|
932
|
+
/>
|
|
885
933
|
</>
|
|
886
934
|
)}
|
|
887
935
|
|
|
@@ -960,6 +1008,8 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
960
1008
|
/>
|
|
961
1009
|
)}
|
|
962
1010
|
</div>
|
|
1011
|
+
</ArcLineRemovalProvider>
|
|
1012
|
+
</PriceAdjustmentProvider>
|
|
963
1013
|
)
|
|
964
1014
|
}
|
|
965
1015
|
|
|
@@ -1215,11 +1265,20 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1215
1265
|
onAddComponent?: () => void
|
|
1216
1266
|
onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
|
|
1217
1267
|
}) {
|
|
1268
|
+
const t = useT()
|
|
1269
|
+
const adjustmentUi = usePriceAdjustmentUi()
|
|
1270
|
+
const removalUi = useArcLineRemovalUi()
|
|
1271
|
+
const isRemoved = removalUi?.isRemoved(line) ?? false
|
|
1218
1272
|
const paddingLeft = indent > 0 ? `${indent * 2 + 1}rem` : undefined
|
|
1273
|
+
const chargeActions = {
|
|
1274
|
+
reasonLabels: adjustmentUi?.reasonLabels,
|
|
1275
|
+
renderChargeActions: (charge: ResolvedCharge) => <AdjustChargeAction line={line} charge={charge} />,
|
|
1276
|
+
renderManualActions: (adjustmentId: string) => <ManualAdjustmentActions adjustmentId={adjustmentId} />,
|
|
1277
|
+
}
|
|
1219
1278
|
|
|
1220
1279
|
return (
|
|
1221
1280
|
<div>
|
|
1222
|
-
<div className=
|
|
1281
|
+
<div className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors ${isRemoved ? 'bg-muted/20' : ''}`} style={paddingLeft ? { paddingLeft } : undefined} data-testid="cpq-quote-line" data-removed={isRemoved ? 'true' : undefined}>
|
|
1223
1282
|
{/* Expand toggle */}
|
|
1224
1283
|
<button onClick={onToggle} className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground">
|
|
1225
1284
|
<svg className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
@@ -1236,9 +1295,25 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1236
1295
|
{isBundle && (
|
|
1237
1296
|
<Tag variant="brand" className="px-1.5 text-xs">bundle</Tag>
|
|
1238
1297
|
)}
|
|
1239
|
-
<span className=
|
|
1298
|
+
<span className={`font-medium text-sm ${isRemoved ? 'text-muted-foreground line-through' : ''}`}>{line.offeringName}</span>
|
|
1240
1299
|
{line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
|
|
1241
|
-
<
|
|
1300
|
+
<LineActionBadge
|
|
1301
|
+
action={line.action}
|
|
1302
|
+
label={isRemoved ? t('cpq.quotes.arc.lineRemoved', 'remove') : undefined}
|
|
1303
|
+
title={isRemoved ? t('cpq.quotes.arc.lineRemovedHint', 'This item is cancelled on the subscription when the order is activated') : undefined}
|
|
1304
|
+
/>
|
|
1305
|
+
{!isRemoved && line.configuration?._arcPriceFrozen === true && (
|
|
1306
|
+
<Tag
|
|
1307
|
+
variant="neutral"
|
|
1308
|
+
className="px-1.5 text-xs"
|
|
1309
|
+
title={t(
|
|
1310
|
+
'cpq.quotes.arc.priceKeptHint',
|
|
1311
|
+
'Price kept from the subscription — it changes only if you reconfigure this line',
|
|
1312
|
+
)}
|
|
1313
|
+
>
|
|
1314
|
+
{t('cpq.quotes.arc.priceKept', 'price kept')}
|
|
1315
|
+
</Tag>
|
|
1316
|
+
)}
|
|
1242
1317
|
{targetCodes.size >= 2 && line.targetSubscriptionId && targetCodes.get(line.targetSubscriptionId) && (
|
|
1243
1318
|
<Tag
|
|
1244
1319
|
variant="brand"
|
|
@@ -1248,7 +1323,7 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1248
1323
|
→ {targetCodes.get(line.targetSubscriptionId)}
|
|
1249
1324
|
</Tag>
|
|
1250
1325
|
)}
|
|
1251
|
-
{line.isConfigured ? (
|
|
1326
|
+
{isRemoved ? null : line.isConfigured ? (
|
|
1252
1327
|
<Tag variant="success" className="px-1.5 text-xs">configured</Tag>
|
|
1253
1328
|
) : (
|
|
1254
1329
|
<Tag variant="warning" className="px-1.5 text-xs">incomplete</Tag>
|
|
@@ -1264,12 +1339,32 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1264
1339
|
|
|
1265
1340
|
{/* Pricing */}
|
|
1266
1341
|
<div className="shrink-0 text-right text-xs space-y-0.5">
|
|
1342
|
+
{isRemoved && line.arcSource && line.arcSource.mrcAmount > 0 && (
|
|
1343
|
+
<div>
|
|
1344
|
+
<span className="text-muted-foreground">MRC </span>
|
|
1345
|
+
<span className="font-mono text-muted-foreground line-through">{fmt(line.arcSource.mrcAmount, currency)}</span>
|
|
1346
|
+
</div>
|
|
1347
|
+
)}
|
|
1267
1348
|
{line.nrcTotal > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
|
|
1268
1349
|
{line.mrcTotal > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
|
|
1269
1350
|
</div>
|
|
1270
1351
|
|
|
1271
1352
|
{/* Actions */}
|
|
1353
|
+
{isRemoved ? (
|
|
1354
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
1355
|
+
<Button
|
|
1356
|
+
type="button" variant="ghost" size="sm"
|
|
1357
|
+
onClick={() => removalUi?.restore(line.lineId)}
|
|
1358
|
+
title={t('cpq.quotes.arc.restoreHint', 'Keep this item on the subscription')}
|
|
1359
|
+
data-testid="cpq-line-restore"
|
|
1360
|
+
>
|
|
1361
|
+
<RotateCcw className="h-4 w-4" />
|
|
1362
|
+
{t('cpq.quotes.arc.restore', 'Restore')}
|
|
1363
|
+
</Button>
|
|
1364
|
+
</div>
|
|
1365
|
+
) : (
|
|
1272
1366
|
<div className="flex items-center gap-1 shrink-0">
|
|
1367
|
+
<DiscountLineAction line={line} />
|
|
1273
1368
|
{onAddComponent && (
|
|
1274
1369
|
<button onClick={onAddComponent} className="rounded p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/5 transition-colors" title="Add Component">
|
|
1275
1370
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
|
@@ -1288,19 +1383,22 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1288
1383
|
</svg>
|
|
1289
1384
|
</button>
|
|
1290
1385
|
</div>
|
|
1386
|
+
)}
|
|
1291
1387
|
</div>
|
|
1292
1388
|
|
|
1293
|
-
{/* XD-250 ARC: before/after preview when line mirrors a sub item */}
|
|
1294
|
-
{isExpanded && line.arcSource && (
|
|
1295
|
-
<ArcLineDiff line={line} currency={currency} />
|
|
1296
|
-
)}
|
|
1297
1389
|
{/* Charge breakdown (non-bundle or any expanded line) */}
|
|
1298
1390
|
{isExpanded && !isBundle && line.charges.length > 0 && (
|
|
1299
|
-
<ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} />
|
|
1391
|
+
<ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} {...chargeActions} />
|
|
1300
1392
|
)}
|
|
1301
1393
|
{/* For bundles: show own charges first, then children are rendered by parent */}
|
|
1302
1394
|
{isExpanded && isBundle && line.charges.length > 0 && (
|
|
1303
|
-
<ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} />
|
|
1395
|
+
<ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} {...chargeActions} />
|
|
1396
|
+
)}
|
|
1397
|
+
{/* XD-250 ARC: before/after preview, below the charges, when line mirrors a sub item */}
|
|
1398
|
+
{isExpanded && line.arcSource && (
|
|
1399
|
+
<div className="border-t bg-muted/10 px-12 py-2">
|
|
1400
|
+
<ArcLineDiff line={line} currency={currency} />
|
|
1401
|
+
</div>
|
|
1304
1402
|
)}
|
|
1305
1403
|
{/* Fallback: line has no per-charge breakdown and no ARC diff (truly empty) */}
|
|
1306
1404
|
{isExpanded && line.charges.length === 0 && !line.arcSource && (
|
|
@@ -1310,77 +1408,6 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1310
1408
|
)
|
|
1311
1409
|
}
|
|
1312
1410
|
|
|
1313
|
-
function ArcLineDiff({ line, currency }: { line: QuoteLine; currency: string }) {
|
|
1314
|
-
const src = line.arcSource
|
|
1315
|
-
if (!src) return null
|
|
1316
|
-
const isCancel = line.action === 'cancel'
|
|
1317
|
-
const beforeMrc = src.mrcAmount
|
|
1318
|
-
const beforeNrc = src.nrcAmount
|
|
1319
|
-
const beforeQty = src.quantity
|
|
1320
|
-
const afterMrc = isCancel ? 0 : line.mrcTotal
|
|
1321
|
-
const afterNrc = isCancel ? 0 : line.nrcTotal
|
|
1322
|
-
const afterQty = isCancel ? 0 : line.quantity
|
|
1323
|
-
const fmtVal = (v: number, asCurrency: boolean) =>
|
|
1324
|
-
asCurrency ? fmt(v, currency) : String(v)
|
|
1325
|
-
const rows: Array<{ label: string; before: number; after: number; asCurrency: boolean }> = [
|
|
1326
|
-
{ label: 'Quantity', before: beforeQty, after: afterQty, asCurrency: false },
|
|
1327
|
-
{ label: 'MRC', before: beforeMrc, after: afterMrc, asCurrency: true },
|
|
1328
|
-
{ label: 'NRC', before: beforeNrc, after: afterNrc, asCurrency: true },
|
|
1329
|
-
]
|
|
1330
|
-
|
|
1331
|
-
return (
|
|
1332
|
-
<div className="border-t bg-muted/10 px-12 py-2 space-y-1.5">
|
|
1333
|
-
<p className="text-xs font-medium text-muted-foreground">Change Preview</p>
|
|
1334
|
-
<div className="rounded border bg-background overflow-hidden">
|
|
1335
|
-
<table className="w-full text-xs">
|
|
1336
|
-
<thead className="bg-muted/40">
|
|
1337
|
-
<tr className="text-muted-foreground">
|
|
1338
|
-
<th className="text-left px-3 py-1.5 font-medium" />
|
|
1339
|
-
<th className="text-right px-3 py-1.5 font-medium">Before</th>
|
|
1340
|
-
<th className="text-right px-3 py-1.5 font-medium">After</th>
|
|
1341
|
-
<th className="text-right px-3 py-1.5 font-medium">Δ</th>
|
|
1342
|
-
</tr>
|
|
1343
|
-
</thead>
|
|
1344
|
-
<tbody>
|
|
1345
|
-
{rows.map((r) => {
|
|
1346
|
-
const delta = r.after - r.before
|
|
1347
|
-
const showDelta = delta !== 0
|
|
1348
|
-
return (
|
|
1349
|
-
<tr key={r.label} className="border-t border-dashed border-border/40">
|
|
1350
|
-
<td className="px-3 py-1 text-muted-foreground">{r.label}</td>
|
|
1351
|
-
<td className="px-3 py-1 text-right font-mono">{fmtVal(r.before, r.asCurrency)}</td>
|
|
1352
|
-
<td className={`px-3 py-1 text-right font-mono ${isCancel ? 'text-muted-foreground line-through' : ''}`}>
|
|
1353
|
-
{fmtVal(r.after, r.asCurrency)}
|
|
1354
|
-
</td>
|
|
1355
|
-
<td className={`px-3 py-1 text-right font-mono font-medium ${showDelta ? (delta > 0 ? 'text-status-success-text' : 'text-destructive') : 'text-muted-foreground'}`}>
|
|
1356
|
-
{showDelta ? `${delta > 0 ? '+' : ''}${fmtVal(delta, r.asCurrency)}` : '—'}
|
|
1357
|
-
</td>
|
|
1358
|
-
</tr>
|
|
1359
|
-
)
|
|
1360
|
-
})}
|
|
1361
|
-
</tbody>
|
|
1362
|
-
</table>
|
|
1363
|
-
</div>
|
|
1364
|
-
{isCancel && (
|
|
1365
|
-
<p className="text-xs text-destructive italic">Cancellation — item will be terminated on activation.</p>
|
|
1366
|
-
)}
|
|
1367
|
-
</div>
|
|
1368
|
-
)
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
function ActionBadge({ action }: { action: QuoteLineAction }) {
|
|
1372
|
-
const variantByAction: Record<QuoteLineAction, 'info' | 'warning' | 'error'> = {
|
|
1373
|
-
add: 'info',
|
|
1374
|
-
modify: 'warning',
|
|
1375
|
-
cancel: 'error',
|
|
1376
|
-
}
|
|
1377
|
-
return (
|
|
1378
|
-
<Tag variant={variantByAction[action]} className="px-1.5 text-xs">
|
|
1379
|
-
{action}
|
|
1380
|
-
</Tag>
|
|
1381
|
-
)
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
1411
|
function LineSummaryFallback({ line, currency }: { line: QuoteLine; currency: string }) {
|
|
1385
1412
|
const hasTotals = line.nrcTotal > 0 || line.mrcTotal > 0
|
|
1386
1413
|
const mirroredFromItemId = (line.configuration?._arcMirroredFromItemId as string | undefined) ?? null
|
|
@@ -1404,138 +1431,22 @@ function LineSummaryFallback({ line, currency }: { line: QuoteLine; currency: st
|
|
|
1404
1431
|
)
|
|
1405
1432
|
}
|
|
1406
1433
|
|
|
1407
|
-
function ChargeBreakdown({ line, lineIdx,
|
|
1434
|
+
function ChargeBreakdown({ line, lineIdx, pricingDetail, onTogglePricingDetail, ...tableProps }: {
|
|
1408
1435
|
line: QuoteLine; lineIdx: number; currency: string
|
|
1409
1436
|
pricingDetail: { lineIdx: number; chargeIdx: number } | null
|
|
1410
1437
|
onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
|
|
1438
|
+
reasonLabels?: Record<string, string>
|
|
1439
|
+
renderChargeActions?: (charge: ResolvedCharge) => React.ReactNode
|
|
1440
|
+
renderManualActions?: (adjustmentId: string) => React.ReactNode
|
|
1411
1441
|
}) {
|
|
1412
1442
|
return (
|
|
1413
1443
|
<div className="border-t bg-muted/10 px-12 py-2">
|
|
1414
|
-
<
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
<th className="text-right py-1 font-medium">Qty</th>
|
|
1421
|
-
<th className="text-right py-1 font-medium">Total</th>
|
|
1422
|
-
<th className="w-8" />
|
|
1423
|
-
</tr>
|
|
1424
|
-
</thead>
|
|
1425
|
-
<tbody>
|
|
1426
|
-
{line.charges.map((c, i) => {
|
|
1427
|
-
const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length)
|
|
1428
|
-
const isDetailOpen = pricingDetail?.lineIdx === lineIdx && pricingDetail?.chargeIdx === i
|
|
1429
|
-
return (
|
|
1430
|
-
<React.Fragment key={i}>
|
|
1431
|
-
<tr className="border-t border-dashed border-border/50">
|
|
1432
|
-
<td className="py-1">{c.chargeName}</td>
|
|
1433
|
-
<td className="py-1 uppercase">{c.chargeType}</td>
|
|
1434
|
-
<td className="py-1 text-right font-mono">{fmt(c.unitPrice, currency)}</td>
|
|
1435
|
-
<td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
|
|
1436
|
-
<td className="py-1 text-right font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : c.note ?? '—'}</td>
|
|
1437
|
-
<td className="py-1 text-center">
|
|
1438
|
-
{hasDetail && (
|
|
1439
|
-
<button
|
|
1440
|
-
onClick={() => onTogglePricingDetail(lineIdx, i)}
|
|
1441
|
-
className="inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
|
1442
|
-
title="Show pricing breakdown"
|
|
1443
|
-
>
|
|
1444
|
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-3.5 h-3.5">
|
|
1445
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
|
1446
|
-
</svg>
|
|
1447
|
-
</button>
|
|
1448
|
-
)}
|
|
1449
|
-
</td>
|
|
1450
|
-
</tr>
|
|
1451
|
-
{isDetailOpen && (
|
|
1452
|
-
<tr>
|
|
1453
|
-
<td colSpan={6} className="pb-3 pt-1">
|
|
1454
|
-
<div className="rounded border bg-background p-3 space-y-3 text-xs">
|
|
1455
|
-
<div className="flex items-center justify-between">
|
|
1456
|
-
<span className="font-semibold text-sm">Pricing Details — {c.chargeName}</span>
|
|
1457
|
-
<span className="text-muted-foreground">{c.pricingMethod ?? 'flat'}</span>
|
|
1458
|
-
</div>
|
|
1459
|
-
|
|
1460
|
-
{c.breakdown?.tiers && c.breakdown.tiers.length > 0 && (
|
|
1461
|
-
<div>
|
|
1462
|
-
<div className="font-medium text-muted-foreground mb-1">Tier Breakdown</div>
|
|
1463
|
-
<table className="w-full">
|
|
1464
|
-
<thead>
|
|
1465
|
-
<tr className="text-muted-foreground">
|
|
1466
|
-
<th className="text-left py-0.5 font-medium">Tier</th>
|
|
1467
|
-
<th className="text-right py-0.5 font-medium">Range</th>
|
|
1468
|
-
<th className="text-right py-0.5 font-medium">Price/Unit</th>
|
|
1469
|
-
<th className="text-right py-0.5 font-medium">Qty</th>
|
|
1470
|
-
<th className="text-right py-0.5 font-medium">Subtotal</th>
|
|
1471
|
-
</tr>
|
|
1472
|
-
</thead>
|
|
1473
|
-
<tbody>
|
|
1474
|
-
{c.breakdown.tiers.map((t) => (
|
|
1475
|
-
<tr key={t.tier} className="border-t border-border/30">
|
|
1476
|
-
<td className="py-0.5">{t.tier}</td>
|
|
1477
|
-
<td className="py-0.5 text-right font-mono">{t.from}–{t.to}</td>
|
|
1478
|
-
<td className="py-0.5 text-right font-mono">{fmt(t.pricePerUnit, currency)}</td>
|
|
1479
|
-
<td className="py-0.5 text-right font-mono">{t.quantity}</td>
|
|
1480
|
-
<td className="py-0.5 text-right font-mono">{fmt(t.subtotal, currency)}</td>
|
|
1481
|
-
</tr>
|
|
1482
|
-
))}
|
|
1483
|
-
<tr className="border-t font-medium">
|
|
1484
|
-
<td colSpan={4} className="py-0.5 text-right">Tiered Total</td>
|
|
1485
|
-
<td className="py-0.5 text-right font-mono">
|
|
1486
|
-
{fmt(c.breakdown.tiers.reduce((s, t) => s + t.subtotal, 0), currency)}
|
|
1487
|
-
</td>
|
|
1488
|
-
</tr>
|
|
1489
|
-
</tbody>
|
|
1490
|
-
</table>
|
|
1491
|
-
</div>
|
|
1492
|
-
)}
|
|
1493
|
-
|
|
1494
|
-
{c.adjustments && c.adjustments.length > 0 && (
|
|
1495
|
-
<div>
|
|
1496
|
-
<div className="font-medium text-muted-foreground mb-1">Applied Price Rules</div>
|
|
1497
|
-
<table className="w-full">
|
|
1498
|
-
<thead>
|
|
1499
|
-
<tr className="text-muted-foreground">
|
|
1500
|
-
<th className="text-left py-0.5 font-medium">Rule</th>
|
|
1501
|
-
<th className="text-left py-0.5 font-medium">Type</th>
|
|
1502
|
-
<th className="text-right py-0.5 font-medium">Value</th>
|
|
1503
|
-
<th className="text-right py-0.5 font-medium">Before</th>
|
|
1504
|
-
<th className="text-right py-0.5 font-medium">After</th>
|
|
1505
|
-
<th className="text-right py-0.5 font-medium">Delta</th>
|
|
1506
|
-
</tr>
|
|
1507
|
-
</thead>
|
|
1508
|
-
<tbody>
|
|
1509
|
-
{c.adjustments.map((adj, ai) => (
|
|
1510
|
-
<tr key={ai} className="border-t border-border/30">
|
|
1511
|
-
<td className="py-0.5">{adj.ruleName}</td>
|
|
1512
|
-
<td className="py-0.5">{RULE_TYPE_LABELS[adj.ruleType] ?? adj.ruleType}</td>
|
|
1513
|
-
<td className="py-0.5 text-right font-mono">{fmtRuleValue(adj, currency)}</td>
|
|
1514
|
-
<td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceBefore, currency)}</td>
|
|
1515
|
-
<td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceAfter, currency)}</td>
|
|
1516
|
-
<td className={`py-0.5 text-right font-mono ${adj.delta < 0 ? 'text-destructive' : adj.delta > 0 ? 'text-status-success-text' : ''}`}>
|
|
1517
|
-
{adj.delta > 0 ? '+' : ''}{fmt(adj.delta, currency)}
|
|
1518
|
-
</td>
|
|
1519
|
-
</tr>
|
|
1520
|
-
))}
|
|
1521
|
-
</tbody>
|
|
1522
|
-
</table>
|
|
1523
|
-
</div>
|
|
1524
|
-
)}
|
|
1525
|
-
|
|
1526
|
-
<div className="flex items-center justify-between pt-1 border-t text-sm font-semibold">
|
|
1527
|
-
<span>Final Total</span>
|
|
1528
|
-
<span className="font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : '—'}</span>
|
|
1529
|
-
</div>
|
|
1530
|
-
</div>
|
|
1531
|
-
</td>
|
|
1532
|
-
</tr>
|
|
1533
|
-
)}
|
|
1534
|
-
</React.Fragment>
|
|
1535
|
-
)
|
|
1536
|
-
})}
|
|
1537
|
-
</tbody>
|
|
1538
|
-
</table>
|
|
1444
|
+
<ChargeBreakdownTable
|
|
1445
|
+
charges={line.charges}
|
|
1446
|
+
openDetailIndex={pricingDetail?.lineIdx === lineIdx ? pricingDetail.chargeIdx : null}
|
|
1447
|
+
onToggleDetail={(chargeIdx) => onTogglePricingDetail(lineIdx, chargeIdx)}
|
|
1448
|
+
{...tableProps}
|
|
1449
|
+
/>
|
|
1539
1450
|
</div>
|
|
1540
1451
|
)
|
|
1541
1452
|
}
|