@dainamite/cpq 0.6.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 +16 -0
- 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 +2 -2
- 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 +154 -178
- 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 +69 -66
- 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/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 +6 -3
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +129 -94
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +2 -2
- 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/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/components/ArcLineDiff.js +2 -2
- package/dist/modules/cpq/components/ArcLineDiff.js.map +2 -2
- package/dist/modules/cpq/components/ChargeBreakdownTable.js +4 -10
- package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +2 -2
- package/dist/modules/cpq/components/PriceWaterfall.js +3 -1
- package/dist/modules/cpq/components/PriceWaterfall.js.map +2 -2
- package/dist/modules/cpq/components/PricingSummaryCard.js +1 -6
- package/dist/modules/cpq/components/PricingSummaryCard.js.map +2 -2
- 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/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/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/__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/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 +2 -2
- 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 +134 -112
- 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 +66 -43
- 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 +3 -3
- package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +134 -89
- 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/components/ArcLineDiff.tsx +2 -2
- package/src/modules/cpq/components/ChargeBreakdownTable.tsx +4 -2
- package/src/modules/cpq/components/PriceWaterfall.tsx +4 -2
- package/src/modules/cpq/components/PricingSummaryCard.tsx +1 -1
- package/src/modules/cpq/components/StatusPath.tsx +7 -4
- package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
- 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/widgets/injection/customer-inventory/widget.client.tsx +38 -31
- package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
- package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
- package/src/modules/cpq/widgets/injection-table.ts +8 -2
- package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
- package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
- package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
- package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
- package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
- package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
- package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
- package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
- package/src/modules/cpq/workflows/steps/index.ts +12 -0
- package/src/modules/cpq/workflows/types.ts +4 -0
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { useRouter, useParams } from 'next/navigation'
|
|
4
|
+
import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
4
5
|
import { Alert } from '@open-mercato/ui/primitives/alert'
|
|
5
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
7
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
7
8
|
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
8
9
|
import {
|
|
9
|
-
formatStatusLabel,
|
|
10
10
|
subscriptionStatusMap,
|
|
11
11
|
type SubscriptionStatus,
|
|
12
12
|
} from '../../../../../components/statusMaps'
|
|
13
13
|
import { StatusPath } from '../../../../../components/StatusPath'
|
|
14
14
|
import { DepthBadge, PriceWaterfall } from '../../../../../components/PriceWaterfall'
|
|
15
15
|
import { parseStoredCharge } from '../../../../../lib/priceAdjustmentView'
|
|
16
|
+
import { buildInventoryStatusLabels, inventoryStatusLabel } from '../../shared'
|
|
16
17
|
|
|
17
18
|
// XD-250 — ARC integration on subscription detail.
|
|
18
19
|
// • header buttons (Amend / Renew / Cancel) call /from-subscription
|
|
@@ -64,13 +65,16 @@ type ChangeLogRow = {
|
|
|
64
65
|
mergedFromSubscriptionIds: string[] | null
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
function changeTypeLabel(t: TranslateFn, changeType: string): string {
|
|
69
|
+
switch (changeType) {
|
|
70
|
+
case 'created': return t('cpq.inventory.subscriptions.detail.changeType.created', 'Created')
|
|
71
|
+
case 'amend': return t('cpq.inventory.subscriptions.detail.changeType.amend', 'Amended')
|
|
72
|
+
case 'renew': return t('cpq.inventory.subscriptions.detail.changeType.renew', 'Renewed')
|
|
73
|
+
case 'cancel': return t('cpq.inventory.subscriptions.detail.changeType.cancel', 'Cancelled')
|
|
74
|
+
case 'merge-result': return t('cpq.inventory.subscriptions.detail.changeType.mergeResult', 'Created from merging contracts')
|
|
75
|
+
case 'merge-source': return t('cpq.inventory.subscriptions.detail.changeType.mergeSource', 'Merged into a new contract')
|
|
76
|
+
default: return changeType
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
const CHANGE_TYPE_VARIANTS: Record<string, 'info' | 'success' | 'error' | 'brand' | 'warning' | 'neutral'> = {
|
|
@@ -156,27 +160,23 @@ const ALLOWED_TRANSITIONS: Record<string, string[]> = {
|
|
|
156
160
|
expired: [],
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
const
|
|
160
|
-
pending: 'Pending',
|
|
161
|
-
active: 'Active',
|
|
162
|
-
suspended: 'Suspended',
|
|
163
|
-
terminated: 'Terminated',
|
|
164
|
-
expired: 'Expired',
|
|
165
|
-
}
|
|
163
|
+
const SUBSCRIPTION_STATUSES = ['pending', 'active', 'suspended', 'terminated', 'expired'] as const
|
|
166
164
|
|
|
167
165
|
function fmt(amount: number | string, currency: string): string {
|
|
168
166
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(Number(amount))
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
function StatusBadge({ status }: { status: string }) {
|
|
170
|
+
const t = useT()
|
|
172
171
|
return (
|
|
173
172
|
<Tag variant={subscriptionStatusMap[status as SubscriptionStatus] ?? 'neutral'} dot>
|
|
174
|
-
{
|
|
173
|
+
{inventoryStatusLabel(t, status)}
|
|
175
174
|
</Tag>
|
|
176
175
|
)
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
export default function SubscriptionDetailPage(props: { params?: { id?: string } }) {
|
|
179
|
+
const t = useT()
|
|
180
180
|
const router = useRouter()
|
|
181
181
|
const urlParams = useParams()
|
|
182
182
|
const subId = (props.params?.id ?? urlParams?.id) as string
|
|
@@ -220,15 +220,16 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
220
220
|
// Offer to navigate to that quote instead of showing a dead alert.
|
|
221
221
|
if (body.existingQuoteId) {
|
|
222
222
|
const ok = window.confirm(
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
t('cpq.inventory.subscriptions.detail.arc.existingQuoteConfirm', '{error}\n\nOpen the existing quote now?', {
|
|
224
|
+
error: body.error ?? t('cpq.inventory.subscriptions.detail.arc.existingQuoteInProgress', 'Subscription has another ARC quote in progress.'),
|
|
225
|
+
}),
|
|
225
226
|
)
|
|
226
227
|
if (ok) {
|
|
227
228
|
router.push(`/backend/cpq/quotes/${body.existingQuoteId}`)
|
|
228
229
|
}
|
|
229
230
|
return
|
|
230
231
|
}
|
|
231
|
-
alert(
|
|
232
|
+
alert(t('cpq.inventory.subscriptions.detail.arc.startFailed', 'Failed to start {type} quote: {error}', { type, error: body.error ?? res.statusText }))
|
|
232
233
|
return
|
|
233
234
|
}
|
|
234
235
|
const data = await res.json()
|
|
@@ -237,7 +238,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
237
238
|
setArcInFlight(null)
|
|
238
239
|
}
|
|
239
240
|
},
|
|
240
|
-
[router, sub],
|
|
241
|
+
[router, sub, t],
|
|
241
242
|
)
|
|
242
243
|
|
|
243
244
|
const transitionStatus = async (targetStatus: string) => {
|
|
@@ -251,7 +252,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
251
252
|
body: JSON.stringify({ id: sub.id, targetStatus }),
|
|
252
253
|
})
|
|
253
254
|
const data = await res.json()
|
|
254
|
-
if (!res.ok) throw new Error(data?.error ?? 'Status transition failed')
|
|
255
|
+
if (!res.ok) throw new Error(data?.error ?? t('cpq.inventory.subscriptions.detail.error.transitionFailed', 'Status transition failed'))
|
|
255
256
|
// The status endpoint cascades to items and linked assets server-side,
|
|
256
257
|
// but only returns the subscription scalar fields. Re-fetch the full
|
|
257
258
|
// detail (which includes the item tree) and the asset list so the UI
|
|
@@ -324,7 +325,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
324
325
|
async function load() {
|
|
325
326
|
try {
|
|
326
327
|
const res = await fetch(`/api/cpq/inventory/subscriptions?id=${subId}`)
|
|
327
|
-
if (!res.ok) throw new Error('Subscription not found')
|
|
328
|
+
if (!res.ok) throw new Error(t('cpq.inventory.subscriptions.detail.error.notFound', 'Subscription not found'))
|
|
328
329
|
const data: SubscriptionDetail = await res.json()
|
|
329
330
|
if (!cancelled) {
|
|
330
331
|
setSub(data)
|
|
@@ -411,7 +412,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
411
412
|
}
|
|
412
413
|
load()
|
|
413
414
|
return () => { cancelled = true }
|
|
414
|
-
}, [subId])
|
|
415
|
+
}, [subId, t])
|
|
415
416
|
|
|
416
417
|
const toggleItem = (id: string) => {
|
|
417
418
|
setExpandedItems((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n })
|
|
@@ -420,7 +421,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
420
421
|
if (loading) {
|
|
421
422
|
return (
|
|
422
423
|
<div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
|
|
423
|
-
<Spinner /> Loading…
|
|
424
|
+
<Spinner /> {t('cpq.inventory.subscriptions.detail.loading', 'Loading…')}
|
|
424
425
|
</div>
|
|
425
426
|
)
|
|
426
427
|
}
|
|
@@ -428,9 +429,9 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
428
429
|
if (!sub) {
|
|
429
430
|
return (
|
|
430
431
|
<div className="space-y-4">
|
|
431
|
-
<button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← Back</button>
|
|
432
|
+
<button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← {t('cpq.common.back', 'Back')}</button>
|
|
432
433
|
{error && <ErrorBanner message={error} />}
|
|
433
|
-
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Subscription not found
|
|
434
|
+
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.notFound', 'Subscription not found.')}</div>
|
|
434
435
|
</div>
|
|
435
436
|
)
|
|
436
437
|
}
|
|
@@ -440,6 +441,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
440
441
|
|
|
441
442
|
const canArc = sub.status === 'active' || sub.status === 'suspended'
|
|
442
443
|
const statusTransitions = ALLOWED_TRANSITIONS[sub.status] ?? []
|
|
444
|
+
const statusLabels = buildInventoryStatusLabels(t, SUBSCRIPTION_STATUSES)
|
|
443
445
|
|
|
444
446
|
return (
|
|
445
447
|
<div className="space-y-6">
|
|
@@ -452,13 +454,13 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
452
454
|
<FormHeader
|
|
453
455
|
mode="detail"
|
|
454
456
|
backHref="/backend/cpq/inventory"
|
|
455
|
-
backLabel=
|
|
456
|
-
entityTypeLabel=
|
|
457
|
+
backLabel={t('cpq.inventory.detail.backToInventory', 'Back to Customer Inventory')}
|
|
458
|
+
entityTypeLabel={t('cpq.inventory.subscriptions.detail.entityType', 'Subscription')}
|
|
457
459
|
title={sub.name}
|
|
458
460
|
statusBadge={
|
|
459
461
|
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
460
462
|
<Tag variant={subscriptionStatusMap[sub.status as SubscriptionStatus] ?? 'neutral'} dot>
|
|
461
|
-
{
|
|
463
|
+
{inventoryStatusLabel(t, sub.status)}
|
|
462
464
|
</Tag>
|
|
463
465
|
<Tag variant="neutral" className="capitalize">{sub.billingCycle}</Tag>
|
|
464
466
|
{transitioning && <Spinner />}
|
|
@@ -473,7 +475,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
473
475
|
disabled={arcInFlight !== null}
|
|
474
476
|
onClick={() => startArcQuote('amend')}
|
|
475
477
|
>
|
|
476
|
-
Amend
|
|
478
|
+
{t('cpq.inventory.subscriptions.detail.arc.amend', 'Amend')}
|
|
477
479
|
</Button>
|
|
478
480
|
<Button
|
|
479
481
|
type="button"
|
|
@@ -481,7 +483,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
481
483
|
disabled={arcInFlight !== null}
|
|
482
484
|
onClick={() => startArcQuote('renew')}
|
|
483
485
|
>
|
|
484
|
-
Renew
|
|
486
|
+
{t('cpq.inventory.subscriptions.detail.arc.renew', 'Renew')}
|
|
485
487
|
</Button>
|
|
486
488
|
<Button
|
|
487
489
|
type="button"
|
|
@@ -489,7 +491,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
489
491
|
disabled={arcInFlight !== null}
|
|
490
492
|
onClick={() => startArcQuote('cancel')}
|
|
491
493
|
>
|
|
492
|
-
Cancel
|
|
494
|
+
{t('cpq.inventory.subscriptions.detail.arc.cancel', 'Cancel')}
|
|
493
495
|
</Button>
|
|
494
496
|
</>
|
|
495
497
|
) : null
|
|
@@ -504,11 +506,11 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
504
506
|
path={['pending', 'active']}
|
|
505
507
|
terminals={['suspended', 'terminated', 'expired']}
|
|
506
508
|
statusMap={subscriptionStatusMap}
|
|
507
|
-
labels={
|
|
509
|
+
labels={statusLabels}
|
|
508
510
|
onTransition={transitionStatus}
|
|
509
511
|
allowedTransitions={statusTransitions}
|
|
510
512
|
disabled={transitioning}
|
|
511
|
-
ariaLabel=
|
|
513
|
+
ariaLabel={t('cpq.inventory.subscriptions.detail.statusPath', 'Subscription status path')}
|
|
512
514
|
/>
|
|
513
515
|
</div>
|
|
514
516
|
|
|
@@ -517,46 +519,46 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
517
519
|
{/* Detail Cards */}
|
|
518
520
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
519
521
|
<div className="rounded-lg border bg-card p-4 space-y-3">
|
|
520
|
-
<h3 className="text-sm font-medium">Subscription Details</h3>
|
|
522
|
+
<h3 className="text-sm font-medium">{t('cpq.inventory.subscriptions.detail.sections.details', 'Subscription Details')}</h3>
|
|
521
523
|
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
|
522
|
-
<Field label=
|
|
523
|
-
<Field label=
|
|
524
|
-
<Field label=
|
|
525
|
-
<Field label=
|
|
526
|
-
<Field label=
|
|
527
|
-
{sub.termMonths != null && <Field label=
|
|
528
|
-
{sub.startDate && <Field label=
|
|
529
|
-
{sub.currentTermEnd && <Field label=
|
|
530
|
-
{sub.activatedAt && <Field label=
|
|
531
|
-
{sub.suspendedAt && <Field label=
|
|
532
|
-
{sub.terminatedAt && <Field label=
|
|
524
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.code', 'Code')} value={sub.code} mono />
|
|
525
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.status', 'Status')} value={sub.status} />
|
|
526
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.billingCycle', 'Billing Cycle')} value={sub.billingCycle} />
|
|
527
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.currency', 'Currency')} value={currency} />
|
|
528
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.autoRenew', 'Auto-Renew')} value={sub.autoRenew ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')} />
|
|
529
|
+
{sub.termMonths != null && <Field label={t('cpq.inventory.subscriptions.detail.fields.term', 'Term')} value={t('cpq.inventory.subscriptions.detail.fields.termMonths', '{count} months', { count: sub.termMonths })} />}
|
|
530
|
+
{sub.startDate && <Field label={t('cpq.inventory.subscriptions.detail.fields.startDate', 'Start Date')} value={new Date(sub.startDate).toLocaleDateString()} />}
|
|
531
|
+
{sub.currentTermEnd && <Field label={t('cpq.inventory.subscriptions.detail.fields.termEnd', 'Term End')} value={new Date(sub.currentTermEnd).toLocaleDateString()} />}
|
|
532
|
+
{sub.activatedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.activated', 'Activated')} value={new Date(sub.activatedAt).toLocaleString()} />}
|
|
533
|
+
{sub.suspendedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.suspended', 'Suspended')} value={new Date(sub.suspendedAt).toLocaleString()} />}
|
|
534
|
+
{sub.terminatedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.terminated', 'Terminated')} value={new Date(sub.terminatedAt).toLocaleString()} />}
|
|
533
535
|
</div>
|
|
534
536
|
{sub.description && (
|
|
535
537
|
<div className="pt-1">
|
|
536
|
-
<p className="text-xs text-muted-foreground">Description</p>
|
|
538
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.description', 'Description')}</p>
|
|
537
539
|
<p className="text-sm">{sub.description}</p>
|
|
538
540
|
</div>
|
|
539
541
|
)}
|
|
540
542
|
</div>
|
|
541
543
|
|
|
542
544
|
<div className="rounded-lg border bg-card p-4 space-y-3">
|
|
543
|
-
<h3 className="text-sm font-medium">Pricing & References</h3>
|
|
545
|
+
<h3 className="text-sm font-medium">{t('cpq.inventory.subscriptions.detail.sections.pricing', 'Pricing & References')}</h3>
|
|
544
546
|
<div className="grid grid-cols-2 gap-4">
|
|
545
547
|
<div>
|
|
546
|
-
<p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
|
|
548
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.mrc', 'Monthly Recurring (MRC)')}</p>
|
|
547
549
|
<p className="text-lg font-semibold font-mono">{fmt(sub.mrcAmount, currency)}</p>
|
|
548
550
|
</div>
|
|
549
551
|
<div>
|
|
550
|
-
<p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
|
|
552
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.nrc', 'Non-Recurring (NRC)')}</p>
|
|
551
553
|
<p className="text-lg font-semibold font-mono">{fmt(sub.nrcAmount, currency)}</p>
|
|
552
554
|
</div>
|
|
553
555
|
</div>
|
|
554
556
|
<div className="grid grid-cols-2 gap-x-4 gap-y-2 pt-1">
|
|
555
|
-
<Field label=
|
|
556
|
-
<Field label=
|
|
557
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.customer', 'Customer')} value={customerName ?? sub.customerId.slice(0, 12) + '…'} />
|
|
558
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.created', 'Created')} value={new Date(sub.createdAt).toLocaleString()} />
|
|
557
559
|
{sub.sourceQuoteId && (
|
|
558
560
|
<div>
|
|
559
|
-
<p className="text-xs text-muted-foreground">Source Quote</p>
|
|
561
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.sourceQuote', 'Source Quote')}</p>
|
|
560
562
|
<button
|
|
561
563
|
onClick={() => router.push(`/backend/cpq/quotes/${sub.sourceQuoteId}`)}
|
|
562
564
|
className="text-sm text-primary hover:underline"
|
|
@@ -567,7 +569,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
567
569
|
)}
|
|
568
570
|
{sub.sourceOrderId && (
|
|
569
571
|
<div>
|
|
570
|
-
<p className="text-xs text-muted-foreground">Source Order</p>
|
|
572
|
+
<p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.sourceOrder', 'Source Order')}</p>
|
|
571
573
|
<button
|
|
572
574
|
onClick={() => router.push(`/backend/cpq/orders/${sub.sourceOrderId}`)}
|
|
573
575
|
className="text-sm text-primary hover:underline"
|
|
@@ -583,7 +585,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
583
585
|
{/* Metadata */}
|
|
584
586
|
{sub.metadata && Object.keys(sub.metadata).length > 0 && (
|
|
585
587
|
<div className="rounded-lg border bg-card p-4">
|
|
586
|
-
<h3 className="text-sm font-medium mb-2">Metadata</h3>
|
|
588
|
+
<h3 className="text-sm font-medium mb-2">{t('cpq.inventory.subscriptions.detail.sections.metadata', 'Metadata')}</h3>
|
|
587
589
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
|
|
588
590
|
{Object.entries(sub.metadata).map(([key, value]) => (
|
|
589
591
|
<div key={key} className="text-xs">
|
|
@@ -597,9 +599,9 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
597
599
|
|
|
598
600
|
{/* Subscription Items */}
|
|
599
601
|
<div>
|
|
600
|
-
<h3 className="text-sm font-medium mb-2 px-1">Subscription Items ({flatItems.length})</h3>
|
|
602
|
+
<h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.items.title', 'Subscription Items ({count})', { count: flatItems.length })}</h3>
|
|
601
603
|
{flatItems.length === 0 ? (
|
|
602
|
-
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No items
|
|
604
|
+
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.empty', 'No items.')}</div>
|
|
603
605
|
) : (
|
|
604
606
|
<div className="rounded-lg border bg-card divide-y">
|
|
605
607
|
{flatItems.map((item) => {
|
|
@@ -625,8 +627,8 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
625
627
|
</div>
|
|
626
628
|
</div>
|
|
627
629
|
<div className="shrink-0 text-right text-xs space-y-0.5">
|
|
628
|
-
{Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
|
|
629
|
-
{Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
|
|
630
|
+
{Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.mrc', 'MRC')}{' '}</span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
|
|
631
|
+
{Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.nrc', 'NRC')}{' '}</span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
|
|
630
632
|
</div>
|
|
631
633
|
</div>
|
|
632
634
|
|
|
@@ -635,7 +637,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
635
637
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2">
|
|
636
638
|
{item.offeringId && (
|
|
637
639
|
<RefField
|
|
638
|
-
label=
|
|
640
|
+
label={t('cpq.inventory.subscriptions.detail.fields.offering', 'Offering')}
|
|
639
641
|
name={offeringNames[item.offeringId]}
|
|
640
642
|
fallback={item.offeringId}
|
|
641
643
|
onClick={() => router.push(`/backend/cpq/offerings/${item.offeringId}`)}
|
|
@@ -643,7 +645,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
643
645
|
)}
|
|
644
646
|
{item.specId && (
|
|
645
647
|
<RefField
|
|
646
|
-
label=
|
|
648
|
+
label={t('cpq.inventory.subscriptions.detail.fields.specification', 'Specification')}
|
|
647
649
|
name={specNames[item.specId]}
|
|
648
650
|
fallback={item.specId}
|
|
649
651
|
onClick={() => router.push(`/backend/cpq/specifications/${item.specId}`)}
|
|
@@ -651,20 +653,20 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
651
653
|
)}
|
|
652
654
|
{item.productId && (
|
|
653
655
|
<RefField
|
|
654
|
-
label=
|
|
656
|
+
label={t('cpq.inventory.subscriptions.detail.fields.product', 'Product')}
|
|
655
657
|
name={productNames[item.productId]}
|
|
656
658
|
fallback={item.productId}
|
|
657
659
|
onClick={() => router.push(`/backend/catalog/products/${item.productId}`)}
|
|
658
660
|
/>
|
|
659
661
|
)}
|
|
660
|
-
<Field label=
|
|
661
|
-
<Field label=
|
|
662
|
-
{item.capacityTotal && <Field label=
|
|
662
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.currency', 'Currency')} value={item.currencyCode || currency} />
|
|
663
|
+
<Field label={t('cpq.inventory.subscriptions.detail.fields.quantity', 'Quantity')} value={String(item.quantity)} />
|
|
664
|
+
{item.capacityTotal && <Field label={t('cpq.inventory.subscriptions.detail.fields.capacity', 'Capacity')} value={`${item.capacityUsed ?? '0'} / ${item.capacityTotal} ${item.capacityUnit ?? ''}`} />}
|
|
663
665
|
</div>
|
|
664
666
|
|
|
665
667
|
{item.configuration && Object.keys(item.configuration).length > 0 && (
|
|
666
668
|
<div>
|
|
667
|
-
<p className="text-xs font-medium text-muted-foreground mb-1">Configuration</p>
|
|
669
|
+
<p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.configuration', 'Configuration')}</p>
|
|
668
670
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
|
|
669
671
|
{Object.entries(item.configuration).map(([key, value]) => (
|
|
670
672
|
<div key={key} className="text-xs">
|
|
@@ -678,16 +680,16 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
678
680
|
|
|
679
681
|
{item.charges && item.charges.length > 0 && (
|
|
680
682
|
<div>
|
|
681
|
-
<p className="text-xs font-medium text-muted-foreground mb-1">Charges</p>
|
|
683
|
+
<p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.charges', 'Charges')}</p>
|
|
682
684
|
<table className="w-full text-xs">
|
|
683
685
|
<thead>
|
|
684
686
|
<tr className="text-muted-foreground">
|
|
685
|
-
<th className="text-left py-1 font-medium">Charge</th>
|
|
686
|
-
<th className="text-left py-1 font-medium">Type</th>
|
|
687
|
-
<th className="text-right py-1 font-medium">List</th>
|
|
688
|
-
<th className="text-right py-1 font-medium">Offer</th>
|
|
689
|
-
<th className="text-right py-1 font-medium">Qty</th>
|
|
690
|
-
<th className="text-right py-1 font-medium">Total</th>
|
|
687
|
+
<th className="text-left py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.charge', 'Charge')}</th>
|
|
688
|
+
<th className="text-left py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.type', 'Type')}</th>
|
|
689
|
+
<th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.list', 'List')}</th>
|
|
690
|
+
<th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.offer', 'Offer')}</th>
|
|
691
|
+
<th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.qty', 'Qty')}</th>
|
|
692
|
+
<th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.total', 'Total')}</th>
|
|
691
693
|
</tr>
|
|
692
694
|
</thead>
|
|
693
695
|
<tbody>
|
|
@@ -726,7 +728,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
726
728
|
|
|
727
729
|
{item.metadata && Object.keys(item.metadata).length > 0 && (
|
|
728
730
|
<div>
|
|
729
|
-
<p className="text-xs font-medium text-muted-foreground mb-1">Metadata</p>
|
|
731
|
+
<p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.metadata', 'Metadata')}</p>
|
|
730
732
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
|
|
731
733
|
{Object.entries(item.metadata).map(([key, value]) => (
|
|
732
734
|
<div key={key} className="text-xs">
|
|
@@ -739,8 +741,8 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
739
741
|
)}
|
|
740
742
|
|
|
741
743
|
<div className="flex gap-4 text-xs text-muted-foreground">
|
|
742
|
-
<span>Created: {new Date(item.createdAt).toLocaleString()}</span>
|
|
743
|
-
<span>Updated: {new Date(item.updatedAt).toLocaleString()}</span>
|
|
744
|
+
<span>{t('cpq.inventory.subscriptions.detail.items.createdAt', 'Created: {date}', { date: new Date(item.createdAt).toLocaleString() })}</span>
|
|
745
|
+
<span>{t('cpq.inventory.subscriptions.detail.items.updatedAt', 'Updated: {date}', { date: new Date(item.updatedAt).toLocaleString() })}</span>
|
|
744
746
|
</div>
|
|
745
747
|
</div>
|
|
746
748
|
)}
|
|
@@ -753,18 +755,18 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
753
755
|
|
|
754
756
|
{/* XD-250 Change History */}
|
|
755
757
|
<div>
|
|
756
|
-
<h3 className="text-sm font-medium mb-2 px-1">Change History ({changeLog.length})</h3>
|
|
758
|
+
<h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.changeLog.title', 'Change History ({count})', { count: changeLog.length })}</h3>
|
|
757
759
|
{changeLogLoading ? (
|
|
758
|
-
<div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">Loading
|
|
760
|
+
<div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.loading', 'Loading…')}</div>
|
|
759
761
|
) : changeLog.length === 0 ? (
|
|
760
|
-
<div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">No ARC changes yet
|
|
762
|
+
<div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.changeLog.empty', 'No ARC changes yet.')}</div>
|
|
761
763
|
) : (
|
|
762
764
|
<div className="rounded-lg border bg-card divide-y">
|
|
763
765
|
{changeLog.map((row) => (
|
|
764
766
|
<div key={row.id} className="px-4 py-3 space-y-1">
|
|
765
767
|
<div className="flex items-center gap-2">
|
|
766
768
|
<Tag variant={CHANGE_TYPE_VARIANTS[row.changeType] ?? 'neutral'}>
|
|
767
|
-
{
|
|
769
|
+
{changeTypeLabel(t, row.changeType)}
|
|
768
770
|
</Tag>
|
|
769
771
|
<span className="text-xs text-muted-foreground">
|
|
770
772
|
{new Date(row.effectiveAt).toLocaleString()}
|
|
@@ -774,7 +776,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
774
776
|
onClick={() => router.push(`/backend/cpq/quotes/${row.sourceQuoteId}`)}
|
|
775
777
|
className="text-xs text-primary hover:underline"
|
|
776
778
|
>
|
|
777
|
-
Quote {logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…`}
|
|
779
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.quote', 'Quote {number}', { number: logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…` })}
|
|
778
780
|
</button>
|
|
779
781
|
)}
|
|
780
782
|
{row.sourceOrderId && (
|
|
@@ -782,42 +784,47 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
782
784
|
onClick={() => router.push(`/backend/cpq/orders/${row.sourceOrderId}`)}
|
|
783
785
|
className="text-xs text-primary hover:underline"
|
|
784
786
|
>
|
|
785
|
-
Order {logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…`}
|
|
787
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.order', 'Order {number}', { number: logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…` })}
|
|
786
788
|
</button>
|
|
787
789
|
)}
|
|
788
790
|
</div>
|
|
789
791
|
<ChangeLogLineDetails row={row} currency={currency} />
|
|
790
792
|
{row.termChange && (
|
|
791
793
|
<div className="text-xs text-muted-foreground">
|
|
792
|
-
|
|
794
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.term', 'Term: {from} → {to}', {
|
|
795
|
+
from: String(row.termChange.oldTermEnd ?? '—'),
|
|
796
|
+
to: String(row.termChange.newTermEnd ?? '—'),
|
|
797
|
+
})}
|
|
793
798
|
</div>
|
|
794
799
|
)}
|
|
795
800
|
{row.reasonCode && (
|
|
796
801
|
<div className="text-xs text-muted-foreground">
|
|
797
|
-
|
|
802
|
+
{row.reasonText
|
|
803
|
+
? t('cpq.inventory.subscriptions.detail.changeLog.reasonWithText', 'Reason: {code} — {text}', { code: row.reasonCode, text: row.reasonText })
|
|
804
|
+
: t('cpq.inventory.subscriptions.detail.changeLog.reason', 'Reason: {code}', { code: row.reasonCode })}
|
|
798
805
|
</div>
|
|
799
806
|
)}
|
|
800
807
|
{row.etfAmount && (
|
|
801
808
|
<div className="text-xs text-muted-foreground">
|
|
802
|
-
ETF: {row.etfAmount
|
|
809
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.etf', 'ETF: {amount} {currency}', { amount: row.etfAmount, currency: row.etfCurrency ?? '' })}
|
|
803
810
|
</div>
|
|
804
811
|
)}
|
|
805
812
|
{row.mergedIntoSubscriptionId && (
|
|
806
813
|
<div className="text-xs text-muted-foreground">
|
|
807
|
-
Merged into{' '}
|
|
814
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.mergedInto', 'Merged into')}{' '}
|
|
808
815
|
<button
|
|
809
816
|
onClick={() =>
|
|
810
817
|
router.push(`/backend/cpq/inventory/subscriptions/${row.mergedIntoSubscriptionId}`)
|
|
811
818
|
}
|
|
812
819
|
className="text-primary hover:underline"
|
|
813
820
|
>
|
|
814
|
-
subscription {row.mergedIntoSubscriptionId.slice(0, 8)}
|
|
821
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.mergedIntoSubscription', 'subscription {id}…', { id: row.mergedIntoSubscriptionId.slice(0, 8) })}
|
|
815
822
|
</button>
|
|
816
823
|
</div>
|
|
817
824
|
)}
|
|
818
825
|
{row.mergedFromSubscriptionIds && row.mergedFromSubscriptionIds.length > 0 && (
|
|
819
826
|
<div className="text-xs text-muted-foreground">
|
|
820
|
-
Merged from {row.mergedFromSubscriptionIds.length}
|
|
827
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.mergedFrom', 'Merged from {count} subscriptions', { count: row.mergedFromSubscriptionIds.length })}
|
|
821
828
|
</div>
|
|
822
829
|
)}
|
|
823
830
|
</div>
|
|
@@ -828,20 +835,20 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
|
|
|
828
835
|
|
|
829
836
|
{/* Linked Assets */}
|
|
830
837
|
<div>
|
|
831
|
-
<h3 className="text-sm font-medium mb-2 px-1">Linked Assets ({assets.length})</h3>
|
|
838
|
+
<h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.assets.title', 'Linked Assets ({count})', { count: assets.length })}</h3>
|
|
832
839
|
{assets.length === 0 ? (
|
|
833
|
-
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No assets linked to this subscription
|
|
840
|
+
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.assets.empty', 'No assets linked to this subscription.')}</div>
|
|
834
841
|
) : (
|
|
835
842
|
<div className="rounded-lg border">
|
|
836
843
|
<table className="w-full text-sm">
|
|
837
844
|
<thead>
|
|
838
845
|
<tr className="border-b bg-muted/50">
|
|
839
|
-
<th className="px-4 py-3 text-left font-medium">Code</th>
|
|
840
|
-
<th className="px-4 py-3 text-left font-medium">Name</th>
|
|
841
|
-
<th className="px-4 py-3 text-left font-medium">Type</th>
|
|
842
|
-
<th className="px-4 py-3 text-left font-medium">Status</th>
|
|
843
|
-
<th className="px-4 py-3 text-right font-medium">Price</th>
|
|
844
|
-
<th className="px-4 py-3 text-right font-medium">Qty</th>
|
|
846
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.code', 'Code')}</th>
|
|
847
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.name', 'Name')}</th>
|
|
848
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.type', 'Type')}</th>
|
|
849
|
+
<th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.status', 'Status')}</th>
|
|
850
|
+
<th className="px-4 py-3 text-right font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.price', 'Price')}</th>
|
|
851
|
+
<th className="px-4 py-3 text-right font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.qty', 'Qty')}</th>
|
|
845
852
|
</tr>
|
|
846
853
|
</thead>
|
|
847
854
|
<tbody>
|
|
@@ -932,6 +939,8 @@ function ErrorBanner({ message }: { message: string }) {
|
|
|
932
939
|
// names and before/after MRC values. Falls back gracefully when snapshots are
|
|
933
940
|
// missing (older log rows) or when lineChanges is null (cancel / merge).
|
|
934
941
|
function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency: string }) {
|
|
942
|
+
const t = useT()
|
|
943
|
+
const itemFallback = t('cpq.inventory.subscriptions.detail.changeLog.item', 'Item')
|
|
935
944
|
const beforeItems = new Map<string, SnapshotItem>(
|
|
936
945
|
(row.beforeSnapshot?.items ?? []).map((it) => [it.id, it]),
|
|
937
946
|
)
|
|
@@ -965,7 +974,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
965
974
|
derived.push({
|
|
966
975
|
key: `${itemId}-${i}`,
|
|
967
976
|
action: lc.action,
|
|
968
|
-
name: item?.name ?? (lc.action === 'add' ? 'New item' :
|
|
977
|
+
name: item?.name ?? (lc.action === 'add' ? t('cpq.inventory.subscriptions.detail.changeLog.newItem', 'New item') : itemFallback),
|
|
969
978
|
beforeMrc,
|
|
970
979
|
afterMrc:
|
|
971
980
|
lc.action === 'add' && afterMrc == null && lc.mrcAmount != null
|
|
@@ -981,7 +990,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
981
990
|
derived.push({
|
|
982
991
|
key: it.id,
|
|
983
992
|
action: 'cancel',
|
|
984
|
-
name: it.name ??
|
|
993
|
+
name: it.name ?? itemFallback,
|
|
985
994
|
beforeMrc: mrc,
|
|
986
995
|
afterMrc: 0,
|
|
987
996
|
delta: -mrc,
|
|
@@ -993,7 +1002,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
993
1002
|
derived.push({
|
|
994
1003
|
key: it.id,
|
|
995
1004
|
action: 'add',
|
|
996
|
-
name: it.name ??
|
|
1005
|
+
name: it.name ?? itemFallback,
|
|
997
1006
|
beforeMrc: null,
|
|
998
1007
|
afterMrc: mrc,
|
|
999
1008
|
delta: null,
|
|
@@ -1005,7 +1014,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
1005
1014
|
derived.push({
|
|
1006
1015
|
key: it.id,
|
|
1007
1016
|
action: 'cancel',
|
|
1008
|
-
name: it.name ??
|
|
1017
|
+
name: it.name ?? itemFallback,
|
|
1009
1018
|
beforeMrc: mrc,
|
|
1010
1019
|
afterMrc: 0,
|
|
1011
1020
|
delta: -mrc,
|
|
@@ -1021,7 +1030,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
1021
1030
|
if (row.changeType === 'amend' || row.changeType === 'renew') {
|
|
1022
1031
|
return (
|
|
1023
1032
|
<div className="mt-1.5 text-xs text-muted-foreground italic">
|
|
1024
|
-
No item-level changes recorded.
|
|
1033
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.noItemChanges', 'No item-level changes recorded.')}
|
|
1025
1034
|
</div>
|
|
1026
1035
|
)
|
|
1027
1036
|
}
|
|
@@ -1037,10 +1046,18 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
|
|
|
1037
1046
|
)
|
|
1038
1047
|
}
|
|
1039
1048
|
|
|
1040
|
-
const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string
|
|
1041
|
-
add: { sign: '+', color: 'text-status-success-text'
|
|
1042
|
-
modify: { sign: '~', color: 'text-status-info-text'
|
|
1043
|
-
cancel: { sign: '−', color: 'text-destructive'
|
|
1049
|
+
const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string }> = {
|
|
1050
|
+
add: { sign: '+', color: 'text-status-success-text' },
|
|
1051
|
+
modify: { sign: '~', color: 'text-status-info-text' },
|
|
1052
|
+
cancel: { sign: '−', color: 'text-destructive' },
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function actionGlyphLabel(t: TranslateFn, action: 'add' | 'modify' | 'cancel'): string {
|
|
1056
|
+
switch (action) {
|
|
1057
|
+
case 'add': return t('cpq.inventory.subscriptions.detail.changeLog.action.add', 'added')
|
|
1058
|
+
case 'modify': return t('cpq.inventory.subscriptions.detail.changeLog.action.modify', 'modified')
|
|
1059
|
+
case 'cancel': return t('cpq.inventory.subscriptions.detail.changeLog.action.cancel', 'removed')
|
|
1060
|
+
}
|
|
1044
1061
|
}
|
|
1045
1062
|
|
|
1046
1063
|
function ChangeLogLineRow({
|
|
@@ -1056,6 +1073,7 @@ function ChangeLogLineRow({
|
|
|
1056
1073
|
}
|
|
1057
1074
|
currency: string
|
|
1058
1075
|
}) {
|
|
1076
|
+
const t = useT()
|
|
1059
1077
|
const glyph = ACTION_GLYPH[change.action]
|
|
1060
1078
|
const showPriceChange =
|
|
1061
1079
|
change.action === 'modify' &&
|
|
@@ -1072,35 +1090,39 @@ function ChangeLogLineRow({
|
|
|
1072
1090
|
|
|
1073
1091
|
return (
|
|
1074
1092
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
|
|
1075
|
-
<span className={`font-bold w-3 text-center ${glyph.color}`} title={
|
|
1093
|
+
<span className={`font-bold w-3 text-center ${glyph.color}`} title={actionGlyphLabel(t, change.action)}>
|
|
1076
1094
|
{glyph.sign}
|
|
1077
1095
|
</span>
|
|
1078
1096
|
<span className="font-medium text-foreground">{change.name}</span>
|
|
1079
1097
|
{showPriceChange && (
|
|
1080
1098
|
<span className="font-mono text-muted-foreground">
|
|
1081
|
-
{
|
|
1099
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.priceChangePerMonth', '{before} → {after}/mo', {
|
|
1100
|
+
before: fmt(change.beforeMrc!, currency),
|
|
1101
|
+
after: fmt(change.afterMrc!, currency),
|
|
1102
|
+
})}
|
|
1082
1103
|
</span>
|
|
1083
1104
|
)}
|
|
1084
1105
|
{showSinglePrice && change.action === 'add' && (
|
|
1085
1106
|
<span className="font-mono text-muted-foreground">
|
|
1086
|
-
{fmt(change.afterMrc!, currency)}
|
|
1107
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.pricePerMonth', '{price}/mo', { price: fmt(change.afterMrc!, currency) })}
|
|
1087
1108
|
</span>
|
|
1088
1109
|
)}
|
|
1089
1110
|
{showSinglePrice && change.action === 'cancel' && (
|
|
1090
1111
|
<span className="font-mono text-muted-foreground">
|
|
1091
|
-
was {fmt(change.beforeMrc!, currency)}
|
|
1112
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.wasPricePerMonth', 'was {price}/mo', { price: fmt(change.beforeMrc!, currency) })}
|
|
1092
1113
|
</span>
|
|
1093
1114
|
)}
|
|
1094
1115
|
{showDelta && (
|
|
1095
1116
|
<span
|
|
1096
1117
|
className={`font-mono font-medium ${change.delta! > 0 ? 'text-status-success-text' : 'text-destructive'}`}
|
|
1097
1118
|
>
|
|
1098
|
-
(
|
|
1099
|
-
|
|
1119
|
+
{t('cpq.inventory.subscriptions.detail.changeLog.deltaPerMonth', '({delta}/mo)', {
|
|
1120
|
+
delta: `${change.delta! > 0 ? '+' : ''}${fmt(change.delta!, currency)}`,
|
|
1121
|
+
})}
|
|
1100
1122
|
</span>
|
|
1101
1123
|
)}
|
|
1102
1124
|
{isNoopModify && (
|
|
1103
|
-
<span className="text-muted-foreground italic">no price change</span>
|
|
1125
|
+
<span className="text-muted-foreground italic">{t('cpq.inventory.subscriptions.detail.changeLog.noPriceChange', 'no price change')}</span>
|
|
1104
1126
|
)}
|
|
1105
1127
|
</div>
|
|
1106
1128
|
)
|