@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,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
3
|
+
import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
4
4
|
import { useRouter, useParams, useSearchParams } from 'next/navigation'
|
|
5
5
|
import ArcQuoteConfigurator, {
|
|
6
6
|
type AttachedTarget,
|
|
@@ -169,13 +169,26 @@ function fmt(amount: number, currency: string): string {
|
|
|
169
169
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
172
|
+
async function apiJson<T>(t: TranslateFn, url: string, init?: RequestInit): Promise<T> {
|
|
173
173
|
const res = await fetch(url, init)
|
|
174
174
|
const json = await res.json()
|
|
175
|
-
if (!res.ok) throw new Error(json?.error ??
|
|
175
|
+
if (!res.ok) throw new Error(json?.error ?? t('cpq.common.requestFailed', 'Request failed ({status})', { status: res.status }))
|
|
176
176
|
return json as T
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function quoteTypeLabel(t: TranslateFn, quoteType: string): string {
|
|
180
|
+
switch (quoteType) {
|
|
181
|
+
case 'amend':
|
|
182
|
+
return t('cpq.quotes.type.amend', 'amend')
|
|
183
|
+
case 'renew':
|
|
184
|
+
return t('cpq.quotes.type.renew', 'renew')
|
|
185
|
+
case 'cancel':
|
|
186
|
+
return t('cpq.quotes.type.cancel', 'cancel')
|
|
187
|
+
default:
|
|
188
|
+
return quoteType
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
179
192
|
// ─── Header sub-components ─────────────────────────────────────
|
|
180
193
|
|
|
181
194
|
function MetaCard({ label, children }: { label: string; children: React.ReactNode }) {
|
|
@@ -197,6 +210,7 @@ function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
|
|
|
197
210
|
// <details>-style trigger; menuActions on FormHeader would render it
|
|
198
211
|
// inside the header layout, but here we want it adjacent to other
|
|
199
212
|
// primary buttons.
|
|
213
|
+
const t = useT()
|
|
200
214
|
const [open, setOpen] = React.useState(false)
|
|
201
215
|
const ref = React.useRef<HTMLDivElement>(null)
|
|
202
216
|
React.useEffect(() => {
|
|
@@ -212,7 +226,7 @@ function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
|
|
|
212
226
|
return (
|
|
213
227
|
<div className="relative" ref={ref}>
|
|
214
228
|
<Button type="button" variant="outline" onClick={() => setOpen((v) => !v)}>
|
|
215
|
-
Actions
|
|
229
|
+
{t('cpq.quotes.detail.actions', 'Actions')}
|
|
216
230
|
<svg className="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
217
231
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
218
232
|
</svg>
|
|
@@ -249,6 +263,7 @@ function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
|
|
|
249
263
|
}
|
|
250
264
|
|
|
251
265
|
function buildMoreActions({
|
|
266
|
+
t,
|
|
252
267
|
recalculate,
|
|
253
268
|
canRecalculate,
|
|
254
269
|
submitting,
|
|
@@ -256,6 +271,7 @@ function buildMoreActions({
|
|
|
256
271
|
onDelete,
|
|
257
272
|
deleting,
|
|
258
273
|
}: {
|
|
274
|
+
t: TranslateFn
|
|
259
275
|
recalculate: () => void
|
|
260
276
|
canRecalculate: boolean
|
|
261
277
|
submitting: boolean
|
|
@@ -269,7 +285,7 @@ function buildMoreActions({
|
|
|
269
285
|
if (canRecalculate) {
|
|
270
286
|
items.push({
|
|
271
287
|
id: 'recalculate',
|
|
272
|
-
label: 'Recalculate',
|
|
288
|
+
label: t('cpq.quotes.detail.recalculate', 'Recalculate'),
|
|
273
289
|
onSelect: recalculate,
|
|
274
290
|
loading: submitting,
|
|
275
291
|
disabled: submitting,
|
|
@@ -278,7 +294,7 @@ function buildMoreActions({
|
|
|
278
294
|
if (deletable) {
|
|
279
295
|
items.push({
|
|
280
296
|
id: 'delete',
|
|
281
|
-
label: 'Delete',
|
|
297
|
+
label: t('cpq.common.delete', 'Delete'),
|
|
282
298
|
onSelect: onDelete,
|
|
283
299
|
disabled: deleting,
|
|
284
300
|
loading: deleting,
|
|
@@ -356,11 +372,11 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
356
372
|
// ─── Negotiated prices ─────────────────────────────────────────
|
|
357
373
|
const reloadQuote = React.useCallback(async () => {
|
|
358
374
|
try {
|
|
359
|
-
setCpqQuote(await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqConfigId}`))
|
|
375
|
+
setCpqQuote(await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqConfigId}`))
|
|
360
376
|
} catch (err) {
|
|
361
377
|
setError((err as Error).message)
|
|
362
378
|
}
|
|
363
|
-
}, [cpqConfigId])
|
|
379
|
+
}, [cpqConfigId, t])
|
|
364
380
|
const priceAdjustments = usePriceAdjustments<QuoteResult>({
|
|
365
381
|
quote: isNew ? null : cpqQuote,
|
|
366
382
|
quoteType: arcQuoteType,
|
|
@@ -377,7 +393,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
377
393
|
const quoteId = searchParams.get('quoteId')
|
|
378
394
|
const customerId = searchParams.get('customerId')
|
|
379
395
|
if (!customerId) {
|
|
380
|
-
setError('customerId is required to create a CPQ quote')
|
|
396
|
+
setError(t('cpq.quotes.detail.customerIdRequired', 'customerId is required to create a CPQ quote'))
|
|
381
397
|
setLoading(false)
|
|
382
398
|
return
|
|
383
399
|
}
|
|
@@ -385,7 +401,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
385
401
|
try {
|
|
386
402
|
const body: Record<string, string> = { customerId: customerId! }
|
|
387
403
|
if (quoteId) body.quoteId = quoteId
|
|
388
|
-
const result = await apiJson<QuoteResult>('/api/cpq/quotes', {
|
|
404
|
+
const result = await apiJson<QuoteResult>(t, '/api/cpq/quotes', {
|
|
389
405
|
method: 'POST',
|
|
390
406
|
headers: { 'Content-Type': 'application/json' },
|
|
391
407
|
body: JSON.stringify(body),
|
|
@@ -403,7 +419,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
403
419
|
let cancelled = false
|
|
404
420
|
async function load() {
|
|
405
421
|
try {
|
|
406
|
-
const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqConfigId}`)
|
|
422
|
+
const result = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqConfigId}`)
|
|
407
423
|
if (!cancelled) setCpqQuote(result)
|
|
408
424
|
} catch (err) {
|
|
409
425
|
if (!cancelled) setError((err as Error).message)
|
|
@@ -413,14 +429,14 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
413
429
|
}
|
|
414
430
|
load()
|
|
415
431
|
return () => { cancelled = true }
|
|
416
|
-
}, [cpqConfigId, isNew, searchParams, router])
|
|
432
|
+
}, [cpqConfigId, isNew, searchParams, router, t])
|
|
417
433
|
|
|
418
434
|
// ─── Actions ─────────────────────────────────────────────────
|
|
419
435
|
|
|
420
436
|
const loadOfferings = async () => {
|
|
421
437
|
setOfferingsLoading(true)
|
|
422
438
|
try {
|
|
423
|
-
const result = await apiJson<{ items: Offering[] }>('/api/cpq/product-offerings?pageSize=100&lifecycleStatus=active')
|
|
439
|
+
const result = await apiJson<{ items: Offering[] }>(t, '/api/cpq/product-offerings?pageSize=100&lifecycleStatus=active')
|
|
424
440
|
setOfferings(result.items ?? [])
|
|
425
441
|
} catch { setOfferings([]) }
|
|
426
442
|
finally { setOfferingsLoading(false) }
|
|
@@ -430,7 +446,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
430
446
|
try {
|
|
431
447
|
const params = new URLSearchParams({ offeringId })
|
|
432
448
|
if (Object.keys(currentConfig).length > 0) params.set('configuration', JSON.stringify(currentConfig))
|
|
433
|
-
const result = await apiJson<{ attributes: ConstrainedAttribute[] }>(`/api/cpq/quotes/attributes?${params}`)
|
|
449
|
+
const result = await apiJson<{ attributes: ConstrainedAttribute[] }>(t, `/api/cpq/quotes/attributes?${params}`)
|
|
434
450
|
setAttributes(result.attributes ?? [])
|
|
435
451
|
} catch { setAttributes([]) }
|
|
436
452
|
}
|
|
@@ -449,7 +465,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
449
465
|
const body: Record<string, unknown> = { offeringId: selectedOffering.id, configuration: config, quantity }
|
|
450
466
|
if (addingToParentLineId) body.parentLineId = addingToParentLineId
|
|
451
467
|
if (tid) body.targetSubscriptionId = tid
|
|
452
|
-
lastResult = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/items`, {
|
|
468
|
+
lastResult = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}/items`, {
|
|
453
469
|
method: 'POST',
|
|
454
470
|
headers: { 'Content-Type': 'application/json' },
|
|
455
471
|
body: JSON.stringify(body),
|
|
@@ -466,7 +482,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
466
482
|
if (!cpqQuote) return
|
|
467
483
|
try {
|
|
468
484
|
setSubmitting(true); setError(null)
|
|
469
|
-
const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/items/${lineId}`, {
|
|
485
|
+
const result = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}/items/${lineId}`, {
|
|
470
486
|
method: 'PUT',
|
|
471
487
|
headers: { 'Content-Type': 'application/json' },
|
|
472
488
|
body: JSON.stringify({ configuration: config, quantity }),
|
|
@@ -492,7 +508,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
492
508
|
if (!cpqQuote) return
|
|
493
509
|
try {
|
|
494
510
|
setSubmitting(true); setError(null)
|
|
495
|
-
const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/recalculate`, {
|
|
511
|
+
const result = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}/recalculate`, {
|
|
496
512
|
method: 'POST',
|
|
497
513
|
headers: { 'Content-Type': 'application/json' },
|
|
498
514
|
body: JSON.stringify({ save: true }),
|
|
@@ -506,7 +522,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
506
522
|
if (!cpqQuote) return
|
|
507
523
|
try {
|
|
508
524
|
setConverting(true); setError(null)
|
|
509
|
-
const result = await apiJson<{ id: string }>('/api/cpq/orders', {
|
|
525
|
+
const result = await apiJson<{ id: string }>(t, '/api/cpq/orders', {
|
|
510
526
|
method: 'POST',
|
|
511
527
|
headers: { 'Content-Type': 'application/json' },
|
|
512
528
|
body: JSON.stringify({ quoteId: cpqQuote.id }),
|
|
@@ -520,7 +536,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
520
536
|
if (!cpqQuote) return
|
|
521
537
|
try {
|
|
522
538
|
setTransitioning(true); setError(null)
|
|
523
|
-
const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/status`, {
|
|
539
|
+
const result = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}/status`, {
|
|
524
540
|
method: 'POST',
|
|
525
541
|
headers: { 'Content-Type': 'application/json' },
|
|
526
542
|
body: JSON.stringify({ targetStatus }),
|
|
@@ -534,7 +550,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
534
550
|
if (!cpqQuote) return
|
|
535
551
|
try {
|
|
536
552
|
setDeleting(true); setError(null); setShowDeleteConfirm(false)
|
|
537
|
-
await apiJson<{ ok: boolean }>(`/api/cpq/quotes/${cpqQuote.id}`, { method: 'DELETE' })
|
|
553
|
+
await apiJson<{ ok: boolean }>(t, `/api/cpq/quotes/${cpqQuote.id}`, { method: 'DELETE' })
|
|
538
554
|
router.push('/backend/cpq/quotes')
|
|
539
555
|
} catch (err) { setError((err as Error).message) }
|
|
540
556
|
finally { setDeleting(false) }
|
|
@@ -572,7 +588,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
572
588
|
setAddingTargetSubIds(inherited ? [inherited] : [])
|
|
573
589
|
if (offeringId) {
|
|
574
590
|
try {
|
|
575
|
-
const result = await apiJson<{ items: Offering[] }>('/api/cpq/product-offerings?pageSize=100&lifecycleStatus=active')
|
|
591
|
+
const result = await apiJson<{ items: Offering[] }>(t, '/api/cpq/product-offerings?pageSize=100&lifecycleStatus=active')
|
|
576
592
|
const match = (result.items ?? []).find((o) => o.id === offeringId)
|
|
577
593
|
if (match) {
|
|
578
594
|
selectOffering(match)
|
|
@@ -607,7 +623,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
607
623
|
if (loading) {
|
|
608
624
|
return (
|
|
609
625
|
<div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
|
|
610
|
-
<Spinner /> Loading…
|
|
626
|
+
<Spinner /> {t('cpq.quotes.detail.loading', 'Loading…')}
|
|
611
627
|
</div>
|
|
612
628
|
)
|
|
613
629
|
}
|
|
@@ -615,9 +631,9 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
615
631
|
if (!cpqQuote) {
|
|
616
632
|
return (
|
|
617
633
|
<div className="space-y-4">
|
|
618
|
-
<button onClick={() => router.push('/backend/cpq/quotes')} className="text-sm text-muted-foreground hover:text-foreground"
|
|
634
|
+
<button onClick={() => router.push('/backend/cpq/quotes')} className="text-sm text-muted-foreground hover:text-foreground">{t('cpq.quotes.detail.backToQuotes', '← Back to Quotes')}</button>
|
|
619
635
|
{error && <ErrorBanner message={error} />}
|
|
620
|
-
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Quote not found
|
|
636
|
+
<div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">{t('cpq.quotes.detail.notFound', 'Quote not found.')}</div>
|
|
621
637
|
</div>
|
|
622
638
|
)
|
|
623
639
|
}
|
|
@@ -666,7 +682,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
666
682
|
// items from new targets (XD-250 multi-target ARC), so refetch
|
|
667
683
|
// the full quote to pick up the new lines without a hard refresh.
|
|
668
684
|
try {
|
|
669
|
-
const fresh = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}`)
|
|
685
|
+
const fresh = await apiJson<QuoteResult>(t, `/api/cpq/quotes/${cpqQuote.id}`)
|
|
670
686
|
setCpqQuote(fresh)
|
|
671
687
|
} catch {
|
|
672
688
|
/* ignore — loadArcTargets already surfaced any error */
|
|
@@ -682,18 +698,18 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
682
698
|
<FormHeader
|
|
683
699
|
mode="detail"
|
|
684
700
|
backHref="/backend/cpq/quotes"
|
|
685
|
-
backLabel=
|
|
686
|
-
entityTypeLabel=
|
|
687
|
-
title={cpqQuote.quoteNumber || cpqQuote.customerName || 'Untitled'}
|
|
701
|
+
backLabel={t('cpq.quotes.detail.backLabel', 'Back to CPQ Quotes')}
|
|
702
|
+
entityTypeLabel={t('cpq.quotes.detail.entityType', 'CPQ Quote')}
|
|
703
|
+
title={cpqQuote.quoteNumber || cpqQuote.customerName || t('cpq.quotes.detail.untitled', 'Untitled')}
|
|
688
704
|
statusBadge={
|
|
689
705
|
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
690
706
|
<Tag variant={quoteCpqStatusMap[cpqQuote.cpqStatus as QuoteCpqStatus] ?? 'neutral'} dot>
|
|
691
707
|
{formatStatusLabel(cpqQuote.cpqStatus)}
|
|
692
708
|
</Tag>
|
|
693
709
|
{arcQuoteType !== 'new' && (
|
|
694
|
-
<Tag variant="brand" className="uppercase">{arcQuoteType}</Tag>
|
|
710
|
+
<Tag variant="brand" className="uppercase">{quoteTypeLabel(t, arcQuoteType)}</Tag>
|
|
695
711
|
)}
|
|
696
|
-
{cpqQuote.convertedOrderId && <Tag variant="success" dot>Converted</Tag>}
|
|
712
|
+
{cpqQuote.convertedOrderId && <Tag variant="success" dot>{t('cpq.quotes.detail.converted', 'Converted')}</Tag>}
|
|
697
713
|
</div>
|
|
698
714
|
}
|
|
699
715
|
// FormHeader.detail ignores `utilityActions` whenever `actionsContent`
|
|
@@ -707,7 +723,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
707
723
|
onClick={() => router.push(`/backend/sales/quotes/${cpqQuote.quoteId}`)}
|
|
708
724
|
>
|
|
709
725
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
710
|
-
View Sales Quote
|
|
726
|
+
{t('cpq.quotes.detail.viewSalesQuote', 'View Sales Quote')}
|
|
711
727
|
</Button>
|
|
712
728
|
{cpqQuote.convertedOrderId ? (
|
|
713
729
|
<Button
|
|
@@ -715,31 +731,32 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
715
731
|
onClick={() => router.push(`/backend/cpq/orders/${cpqQuote.convertedOrderId}`)}
|
|
716
732
|
>
|
|
717
733
|
<ArrowRight className="h-4 w-4" />
|
|
718
|
-
Go to order
|
|
734
|
+
{t('cpq.quotes.detail.goToOrder', 'Go to order')}
|
|
719
735
|
</Button>
|
|
720
736
|
) : view !== 'summary' ? (
|
|
721
737
|
<Button type="button" variant="outline" onClick={() => { resetConfigState(); setView('summary') }}>
|
|
722
|
-
Cancel
|
|
738
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
723
739
|
</Button>
|
|
724
740
|
) : (
|
|
725
741
|
<>
|
|
726
742
|
<Button type="button" onClick={startAddOffering}>
|
|
727
743
|
<Plus className="h-4 w-4" />
|
|
728
|
-
Add Offering
|
|
744
|
+
{t('cpq.quotes.detail.addOffering', 'Add Offering')}
|
|
729
745
|
</Button>
|
|
730
746
|
{arcEditable && (
|
|
731
747
|
<Button type="button" variant="outline" onClick={() => setArcDrawerOpen(true)}>
|
|
732
|
-
Modify Subscription
|
|
748
|
+
{t('cpq.quotes.detail.modifySubscription', 'Modify Subscription')}
|
|
733
749
|
</Button>
|
|
734
750
|
)}
|
|
735
751
|
{cpqQuote.cpqStatus === 'accepted' && (
|
|
736
752
|
<Button type="button" onClick={convertToOrder} disabled={converting}>
|
|
737
753
|
{converting ? <Spinner /> : <ArrowRight className="h-4 w-4" />}
|
|
738
|
-
Convert to Order
|
|
754
|
+
{t('cpq.quotes.detail.convertToOrder', 'Convert to Order')}
|
|
739
755
|
</Button>
|
|
740
756
|
)}
|
|
741
757
|
<MoreActionsDropdown
|
|
742
758
|
items={buildMoreActions({
|
|
759
|
+
t,
|
|
743
760
|
recalculate,
|
|
744
761
|
canRecalculate: canRecalculateQuote(grantedFeatures, cpqQuote.cpqStatus),
|
|
745
762
|
submitting,
|
|
@@ -782,7 +799,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
782
799
|
statusMap={quoteCpqStatusMap}
|
|
783
800
|
onTransition={transitionStatus}
|
|
784
801
|
disabled={transitioning}
|
|
785
|
-
ariaLabel=
|
|
802
|
+
ariaLabel={t('cpq.quotes.detail.statusPath', 'Quote status path')}
|
|
786
803
|
/>
|
|
787
804
|
{transitioning && <Spinner />}
|
|
788
805
|
</div>
|
|
@@ -793,9 +810,11 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
793
810
|
<div className="rounded-md border border-brand-violet/30 bg-brand-violet/10 px-4 py-3 text-sm text-brand-violet">
|
|
794
811
|
<div className="flex items-center justify-between">
|
|
795
812
|
<div>
|
|
796
|
-
<span className="font-medium uppercase">{arcQuoteType}</span>
|
|
813
|
+
<span className="font-medium uppercase">{quoteTypeLabel(t, arcQuoteType)}</span>
|
|
797
814
|
{' • '}
|
|
798
|
-
{arcTargets.length
|
|
815
|
+
{arcTargets.length === 1
|
|
816
|
+
? t('cpq.quotes.detail.arcTargets.one', '{count} target:', { count: arcTargets.length })
|
|
817
|
+
: t('cpq.quotes.detail.arcTargets.other', '{count} targets:', { count: arcTargets.length })}
|
|
799
818
|
{' '}
|
|
800
819
|
{arcTargets.map((t, i) => (
|
|
801
820
|
<span key={t.id}>
|
|
@@ -812,7 +831,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
812
831
|
))}
|
|
813
832
|
{isMergeRenew && (
|
|
814
833
|
<span className="ml-2 text-status-warning-text font-medium">
|
|
815
|
-
• Merging into a new contract
|
|
834
|
+
{t('cpq.quotes.detail.mergingRenew', '• Merging into a new contract')}
|
|
816
835
|
</span>
|
|
817
836
|
)}
|
|
818
837
|
</div>
|
|
@@ -824,7 +843,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
824
843
|
currency). Mirrors the sidebar tiles in the standard
|
|
825
844
|
sales-document detail layout. */}
|
|
826
845
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
827
|
-
<MetaCard label=
|
|
846
|
+
<MetaCard label={t('cpq.quotes.detail.meta.customer', 'Customer')}>
|
|
828
847
|
{cpqQuote.customerName ? (
|
|
829
848
|
<span className="font-medium">{cpqQuote.customerName}</span>
|
|
830
849
|
) : (
|
|
@@ -833,10 +852,10 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
833
852
|
</span>
|
|
834
853
|
)}
|
|
835
854
|
</MetaCard>
|
|
836
|
-
<MetaCard label=
|
|
855
|
+
<MetaCard label={t('cpq.quotes.detail.meta.version', 'Version')}>
|
|
837
856
|
<span className="font-medium">v{cpqQuote.version}</span>
|
|
838
857
|
</MetaCard>
|
|
839
|
-
<MetaCard label=
|
|
858
|
+
<MetaCard label={t('cpq.quotes.detail.meta.currency', 'Currency')}>
|
|
840
859
|
<span className="font-medium">{currency}</span>
|
|
841
860
|
</MetaCard>
|
|
842
861
|
</div>
|
|
@@ -845,17 +864,17 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
845
864
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setShowDeleteConfirm(false)}>
|
|
846
865
|
<div className="w-full max-w-sm rounded-lg border bg-card shadow-xl" onClick={(e) => e.stopPropagation()}>
|
|
847
866
|
<div className="px-4 py-3 border-b">
|
|
848
|
-
<h3 className="text-sm font-medium">Delete Quote</h3>
|
|
867
|
+
<h3 className="text-sm font-medium">{t('cpq.quotes.detail.deleteConfirm.title', 'Delete Quote')}</h3>
|
|
849
868
|
</div>
|
|
850
869
|
<div className="p-4">
|
|
851
|
-
<p className="text-sm text-muted-foreground">Are you sure you want to delete this quote? This will soft-delete the quote and all its line items
|
|
870
|
+
<p className="text-sm text-muted-foreground">{t('cpq.quotes.detail.deleteConfirm.text', 'Are you sure you want to delete this quote? This will soft-delete the quote and all its line items.')}</p>
|
|
852
871
|
</div>
|
|
853
872
|
<div className="flex justify-end gap-2 px-4 py-3 border-t">
|
|
854
873
|
<Button type="button" variant="outline" onClick={() => setShowDeleteConfirm(false)}>
|
|
855
|
-
Cancel
|
|
874
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
856
875
|
</Button>
|
|
857
876
|
<Button type="button" variant="destructive" onClick={deleteQuote} disabled={deleting}>
|
|
858
|
-
{deleting ? <Spinner /> : null} Delete
|
|
877
|
+
{deleting ? <Spinner /> : null} {t('cpq.common.delete', 'Delete')}
|
|
859
878
|
</Button>
|
|
860
879
|
</div>
|
|
861
880
|
</div>
|
|
@@ -870,7 +889,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
870
889
|
{/* Validation errors */}
|
|
871
890
|
{!cpqQuote.validationResult.valid && (
|
|
872
891
|
<Alert variant="warning">
|
|
873
|
-
<AlertTitle>Validation Issues</AlertTitle>
|
|
892
|
+
<AlertTitle>{t('cpq.quotes.detail.validationIssues', 'Validation Issues')}</AlertTitle>
|
|
874
893
|
<AlertDescription>
|
|
875
894
|
{/* Stacked <span>s rather than <ul>/<li>: before Open Mercato
|
|
876
895
|
0.8 AlertDescription rendered a <p>, where a list is invalid
|
|
@@ -938,7 +957,8 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
938
957
|
<>
|
|
939
958
|
{addingToParentLineId && (
|
|
940
959
|
<Alert variant="info">
|
|
941
|
-
Adding component to bundle:
|
|
960
|
+
{t('cpq.quotes.detail.addingComponentTo', 'Adding component to bundle:')}{' '}
|
|
961
|
+
<span className="font-medium">{cpqQuote.lines.find((l) => l.lineId === addingToParentLineId)?.offeringName ?? t('cpq.quotes.detail.bundleFallback', 'Bundle')}</span>
|
|
942
962
|
</Alert>
|
|
943
963
|
)}
|
|
944
964
|
<OfferingBrowser
|
|
@@ -954,7 +974,11 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
954
974
|
{/* Configure new offering */}
|
|
955
975
|
{view === 'configure' && selectedOffering && (
|
|
956
976
|
<ConfigurePanel
|
|
957
|
-
title={
|
|
977
|
+
title={
|
|
978
|
+
addingToParentLineId
|
|
979
|
+
? t('cpq.quotes.detail.configure.addComponentTitle', 'Add Component: {name}', { name: selectedOffering.name })
|
|
980
|
+
: t('cpq.quotes.detail.configure.configureTitle', 'Configure: {name}', { name: selectedOffering.name })
|
|
981
|
+
}
|
|
958
982
|
attributes={attributes}
|
|
959
983
|
config={config}
|
|
960
984
|
quantity={quantity}
|
|
@@ -980,7 +1004,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
980
1004
|
}}
|
|
981
1005
|
onQuantityChange={setQuantity}
|
|
982
1006
|
onSubmit={addItem}
|
|
983
|
-
submitLabel=
|
|
1007
|
+
submitLabel={t('cpq.quotes.detail.configure.addToQuote', 'Add to Quote')}
|
|
984
1008
|
submitting={submitting}
|
|
985
1009
|
/>
|
|
986
1010
|
)}
|
|
@@ -988,7 +1012,9 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
988
1012
|
{/* Edit existing line */}
|
|
989
1013
|
{view === 'edit' && editingLineId && (
|
|
990
1014
|
<ConfigurePanel
|
|
991
|
-
title={
|
|
1015
|
+
title={t('cpq.quotes.detail.configure.editTitle', 'Edit: {name}', {
|
|
1016
|
+
name: cpqQuote.lines.find((l) => l.lineId === editingLineId)?.offeringName ?? t('cpq.quotes.detail.configure.lineFallback', 'Line'),
|
|
1017
|
+
})}
|
|
992
1018
|
attributes={attributes}
|
|
993
1019
|
config={config}
|
|
994
1020
|
quantity={quantity}
|
|
@@ -1003,7 +1029,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
|
|
|
1003
1029
|
}}
|
|
1004
1030
|
onQuantityChange={setQuantity}
|
|
1005
1031
|
onSubmit={() => updateItem(editingLineId)}
|
|
1006
|
-
submitLabel=
|
|
1032
|
+
submitLabel={t('cpq.quotes.detail.configure.updateLine', 'Update Line')}
|
|
1007
1033
|
submitting={submitting}
|
|
1008
1034
|
/>
|
|
1009
1035
|
)}
|
|
@@ -1023,6 +1049,7 @@ function QuoteLineTree({ lines, currency, expandedLines, pricingDetail, targetCo
|
|
|
1023
1049
|
onRemoveLine: (lineId: string) => void; onAddComponent: (parentLineId: string, offeringId?: string) => void
|
|
1024
1050
|
onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
|
|
1025
1051
|
}) {
|
|
1052
|
+
const t = useT()
|
|
1026
1053
|
const [bundleTrees, setBundleTrees] = React.useState<Map<string, BundleTree>>(new Map())
|
|
1027
1054
|
const [loadingTrees, setLoadingTrees] = React.useState<Set<string>>(new Set())
|
|
1028
1055
|
|
|
@@ -1040,18 +1067,18 @@ function QuoteLineTree({ lines, currency, expandedLines, pricingDetail, targetCo
|
|
|
1040
1067
|
if (bundleTrees.has(offeringId) || loadingTrees.has(offeringId)) return
|
|
1041
1068
|
setLoadingTrees((prev) => new Set(prev).add(offeringId))
|
|
1042
1069
|
try {
|
|
1043
|
-
const result = await apiJson<{ tree: BundleTree }>(`/api/cpq/bundle-tree?offeringId=${offeringId}`)
|
|
1070
|
+
const result = await apiJson<{ tree: BundleTree }>(t, `/api/cpq/bundle-tree?offeringId=${offeringId}`)
|
|
1044
1071
|
setBundleTrees((prev) => new Map(prev).set(offeringId, result.tree))
|
|
1045
1072
|
} catch { /* ignore — will just not show structure */ }
|
|
1046
1073
|
finally { setLoadingTrees((prev) => { const n = new Set(prev); n.delete(offeringId); return n }) }
|
|
1047
|
-
}, [bundleTrees, loadingTrees])
|
|
1074
|
+
}, [bundleTrees, loadingTrees, t])
|
|
1048
1075
|
|
|
1049
1076
|
return (
|
|
1050
1077
|
<div>
|
|
1051
|
-
<h3 className="text-sm font-medium mb-2 px-1">Line Items</h3>
|
|
1078
|
+
<h3 className="text-sm font-medium mb-2 px-1">{t('cpq.quotes.detail.lineItems', 'Line Items')}</h3>
|
|
1052
1079
|
{lines.length === 0 ? (
|
|
1053
1080
|
<div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
|
|
1054
|
-
No items configured yet. Click
|
|
1081
|
+
{t('cpq.quotes.detail.noLines', 'No items configured yet. Click "Add Offering" to start.')}
|
|
1055
1082
|
</div>
|
|
1056
1083
|
) : (
|
|
1057
1084
|
<div className="rounded-lg border bg-card divide-y">
|
|
@@ -1119,10 +1146,11 @@ function BundleSlotPanel({ bundleTree, treeLoading, children, allLines, currency
|
|
|
1119
1146
|
onRemoveLine: (lineId: string) => void; onAddComponent: (parentLineId: string, offeringId?: string) => void
|
|
1120
1147
|
onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
|
|
1121
1148
|
}) {
|
|
1149
|
+
const t = useT()
|
|
1122
1150
|
if (treeLoading) {
|
|
1123
1151
|
return (
|
|
1124
1152
|
<div className="border-t bg-muted/5 px-8 py-3">
|
|
1125
|
-
<div className="flex items-center gap-2 text-xs text-muted-foreground"><Spinner /> Loading bundle structure
|
|
1153
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground"><Spinner /> {t('cpq.quotes.bundle.loadingStructure', 'Loading bundle structure…')}</div>
|
|
1126
1154
|
</div>
|
|
1127
1155
|
)
|
|
1128
1156
|
}
|
|
@@ -1171,13 +1199,14 @@ function BundleSlotPanel({ bundleTree, treeLoading, children, allLines, currency
|
|
|
1171
1199
|
<div className="flex items-center gap-2 px-6 py-2 bg-muted/20">
|
|
1172
1200
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{slot.name}</span>
|
|
1173
1201
|
{isRequired ? (
|
|
1174
|
-
<Tag variant="warning" className="text-[10px] px-1.5">required</Tag>
|
|
1202
|
+
<Tag variant="warning" className="text-[10px] px-1.5">{t('cpq.quotes.bundle.slotRequired', 'required')}</Tag>
|
|
1175
1203
|
) : (
|
|
1176
|
-
<Tag variant="neutral" className="text-[10px] px-1.5">optional</Tag>
|
|
1204
|
+
<Tag variant="neutral" className="text-[10px] px-1.5">{t('cpq.quotes.bundle.slotOptional', 'optional')}</Tag>
|
|
1177
1205
|
)}
|
|
1178
1206
|
<span className="text-[10px] text-muted-foreground">
|
|
1179
1207
|
{filledCount}/{slot.cardinalityMax ?? '∞'}
|
|
1180
|
-
{slot.cardinalityMin > 0 &&
|
|
1208
|
+
{slot.cardinalityMin > 0 && ' '}
|
|
1209
|
+
{slot.cardinalityMin > 0 && t('cpq.quotes.bundle.slotMin', '(min {min})', { min: slot.cardinalityMin })}
|
|
1181
1210
|
</span>
|
|
1182
1211
|
<div className="flex-1" />
|
|
1183
1212
|
{/* Available offerings in this slot */}
|
|
@@ -1196,7 +1225,13 @@ function BundleSlotPanel({ bundleTree, treeLoading, children, allLines, currency
|
|
|
1196
1225
|
? 'border-border hover:border-primary hover:bg-primary/5 hover:text-primary cursor-pointer'
|
|
1197
1226
|
: 'border-border text-muted-foreground opacity-50 cursor-not-allowed'
|
|
1198
1227
|
}`}
|
|
1199
|
-
title={
|
|
1228
|
+
title={
|
|
1229
|
+
isFilled
|
|
1230
|
+
? t('cpq.quotes.bundle.alreadyAdded', 'Already added')
|
|
1231
|
+
: canAdd
|
|
1232
|
+
? t('cpq.quotes.bundle.addComponent', 'Add {name}', { name: comp.childOffering.offeringName })
|
|
1233
|
+
: t('cpq.quotes.bundle.slotFull', 'Slot is full')
|
|
1234
|
+
}
|
|
1200
1235
|
>
|
|
1201
1236
|
{isFilled ? (
|
|
1202
1237
|
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
@@ -1293,7 +1328,7 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1293
1328
|
<span className="text-muted-foreground text-xs">└</span>
|
|
1294
1329
|
)}
|
|
1295
1330
|
{isBundle && (
|
|
1296
|
-
<Tag variant="brand" className="px-1.5 text-xs">bundle</Tag>
|
|
1331
|
+
<Tag variant="brand" className="px-1.5 text-xs">{t('cpq.lines.bundle', 'bundle')}</Tag>
|
|
1297
1332
|
)}
|
|
1298
1333
|
<span className={`font-medium text-sm ${isRemoved ? 'text-muted-foreground line-through' : ''}`}>{line.offeringName}</span>
|
|
1299
1334
|
{line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
|
|
@@ -1318,18 +1353,22 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1318
1353
|
<Tag
|
|
1319
1354
|
variant="brand"
|
|
1320
1355
|
className="px-1.5 text-xs"
|
|
1321
|
-
title={
|
|
1356
|
+
title={t('cpq.quotes.detail.actsOnSubscription', 'Acts on subscription {code}', { code: targetCodes.get(line.targetSubscriptionId) ?? '' })}
|
|
1322
1357
|
>
|
|
1323
1358
|
→ {targetCodes.get(line.targetSubscriptionId)}
|
|
1324
1359
|
</Tag>
|
|
1325
1360
|
)}
|
|
1326
1361
|
{isRemoved ? null : line.isConfigured ? (
|
|
1327
|
-
<Tag variant="success" className="px-1.5 text-xs">configured</Tag>
|
|
1362
|
+
<Tag variant="success" className="px-1.5 text-xs">{t('cpq.quotes.detail.lineConfigured', 'configured')}</Tag>
|
|
1328
1363
|
) : (
|
|
1329
|
-
<Tag variant="warning" className="px-1.5 text-xs">incomplete</Tag>
|
|
1364
|
+
<Tag variant="warning" className="px-1.5 text-xs">{t('cpq.quotes.detail.lineIncomplete', 'incomplete')}</Tag>
|
|
1330
1365
|
)}
|
|
1331
1366
|
{isBundle && (
|
|
1332
|
-
<span className="text-xs text-muted-foreground">
|
|
1367
|
+
<span className="text-xs text-muted-foreground">
|
|
1368
|
+
{childCount !== 1
|
|
1369
|
+
? t('cpq.lines.componentCount.other', '{count} components', { count: childCount })
|
|
1370
|
+
: t('cpq.lines.componentCount.one', '{count} component', { count: childCount })}
|
|
1371
|
+
</span>
|
|
1333
1372
|
)}
|
|
1334
1373
|
</div>
|
|
1335
1374
|
{line.validationErrors && line.validationErrors.length > 0 && (
|
|
@@ -1341,12 +1380,12 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1341
1380
|
<div className="shrink-0 text-right text-xs space-y-0.5">
|
|
1342
1381
|
{isRemoved && line.arcSource && line.arcSource.mrcAmount > 0 && (
|
|
1343
1382
|
<div>
|
|
1344
|
-
<span className="text-muted-foreground">MRC </span>
|
|
1383
|
+
<span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span>
|
|
1345
1384
|
<span className="font-mono text-muted-foreground line-through">{fmt(line.arcSource.mrcAmount, currency)}</span>
|
|
1346
1385
|
</div>
|
|
1347
1386
|
)}
|
|
1348
|
-
{line.nrcTotal > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
|
|
1349
|
-
{line.mrcTotal > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
|
|
1387
|
+
{line.nrcTotal > 0 && <div><span className="text-muted-foreground">{t('cpq.lines.nrc', 'NRC')} </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
|
|
1388
|
+
{line.mrcTotal > 0 && <div><span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
|
|
1350
1389
|
</div>
|
|
1351
1390
|
|
|
1352
1391
|
{/* Actions */}
|
|
@@ -1366,18 +1405,18 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1366
1405
|
<div className="flex items-center gap-1 shrink-0">
|
|
1367
1406
|
<DiscountLineAction line={line} />
|
|
1368
1407
|
{onAddComponent && (
|
|
1369
|
-
<button onClick={onAddComponent} className="rounded p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/5 transition-colors" title=
|
|
1408
|
+
<button onClick={onAddComponent} className="rounded p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/5 transition-colors" title={t('cpq.quotes.detail.addComponent', 'Add Component')}>
|
|
1370
1409
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
|
1371
1410
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
1372
1411
|
</svg>
|
|
1373
1412
|
</button>
|
|
1374
1413
|
)}
|
|
1375
|
-
<button onClick={onEdit} className="rounded p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title=
|
|
1414
|
+
<button onClick={onEdit} className="rounded p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t('cpq.common.edit', 'Edit')}>
|
|
1376
1415
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
|
1377
1416
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
|
1378
1417
|
</svg>
|
|
1379
1418
|
</button>
|
|
1380
|
-
<button onClick={onRemove} className="rounded p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors" title=
|
|
1419
|
+
<button onClick={onRemove} className="rounded p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors" title={t('cpq.quotes.detail.removeLine', 'Remove')}>
|
|
1381
1420
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
|
1382
1421
|
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
|
1383
1422
|
</svg>
|
|
@@ -1409,21 +1448,23 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
|
|
|
1409
1448
|
}
|
|
1410
1449
|
|
|
1411
1450
|
function LineSummaryFallback({ line, currency }: { line: QuoteLine; currency: string }) {
|
|
1451
|
+
const t = useT()
|
|
1412
1452
|
const hasTotals = line.nrcTotal > 0 || line.mrcTotal > 0
|
|
1413
1453
|
const mirroredFromItemId = (line.configuration?._arcMirroredFromItemId as string | undefined) ?? null
|
|
1414
1454
|
return (
|
|
1415
1455
|
<div className="border-t bg-muted/10 px-12 py-2 text-xs text-muted-foreground space-y-1">
|
|
1416
1456
|
<div>
|
|
1417
|
-
No detailed charge breakdown available for this line.
|
|
1418
|
-
{mirroredFromItemId && '
|
|
1457
|
+
{t('cpq.quotes.detail.noBreakdown', 'No detailed charge breakdown available for this line.')}
|
|
1458
|
+
{mirroredFromItemId && ' '}
|
|
1459
|
+
{mirroredFromItemId && t('cpq.orders.detail.mirroredItem', 'Mirrored from existing subscription item.')}
|
|
1419
1460
|
</div>
|
|
1420
1461
|
{hasTotals && (
|
|
1421
1462
|
<div className="flex gap-4">
|
|
1422
1463
|
{line.nrcTotal > 0 && (
|
|
1423
|
-
<span>NRC <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
|
|
1464
|
+
<span>{t('cpq.lines.nrc', 'NRC')} <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
|
|
1424
1465
|
)}
|
|
1425
1466
|
{line.mrcTotal > 0 && (
|
|
1426
|
-
<span>MRC <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
|
|
1467
|
+
<span>{t('cpq.lines.mrc', 'MRC')} <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
|
|
1427
1468
|
)}
|
|
1428
1469
|
</div>
|
|
1429
1470
|
)}
|
|
@@ -1461,6 +1502,7 @@ function Spinner() {
|
|
|
1461
1502
|
}
|
|
1462
1503
|
|
|
1463
1504
|
function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () => void }) {
|
|
1505
|
+
const t = useT()
|
|
1464
1506
|
return (
|
|
1465
1507
|
<Alert variant="destructive" className="flex items-start gap-2">
|
|
1466
1508
|
<span className="flex-1">{message}</span>
|
|
@@ -1468,7 +1510,7 @@ function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () =
|
|
|
1468
1510
|
<button
|
|
1469
1511
|
onClick={onDismiss}
|
|
1470
1512
|
className="text-destructive/70 hover:text-destructive text-sm font-bold"
|
|
1471
|
-
aria-label=
|
|
1513
|
+
aria-label={t('cpq.common.dismiss', 'Dismiss')}
|
|
1472
1514
|
>
|
|
1473
1515
|
×
|
|
1474
1516
|
</button>
|
|
@@ -1480,6 +1522,7 @@ function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () =
|
|
|
1480
1522
|
function OfferingBrowser({ offerings, loading, search, onSearchChange, onSelect }: {
|
|
1481
1523
|
offerings: Offering[]; loading: boolean; search: string; onSearchChange: (v: string) => void; onSelect: (o: Offering) => void
|
|
1482
1524
|
}) {
|
|
1525
|
+
const t = useT()
|
|
1483
1526
|
const filtered = offerings.filter((o) => {
|
|
1484
1527
|
if (!search.trim()) return true
|
|
1485
1528
|
const q = search.toLowerCase()
|
|
@@ -1488,18 +1531,18 @@ function OfferingBrowser({ offerings, loading, search, onSearchChange, onSelect
|
|
|
1488
1531
|
|
|
1489
1532
|
return (
|
|
1490
1533
|
<div className="space-y-3">
|
|
1491
|
-
<h3 className="text-sm font-medium px-1">Select Product Offering</h3>
|
|
1534
|
+
<h3 className="text-sm font-medium px-1">{t('cpq.quotes.offeringBrowser.title', 'Select Product Offering')}</h3>
|
|
1492
1535
|
<div className="relative">
|
|
1493
1536
|
<svg className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
|
|
1494
1537
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
1495
1538
|
</svg>
|
|
1496
|
-
<input type="text" placeholder=
|
|
1539
|
+
<input type="text" placeholder={t('cpq.quotes.offeringBrowser.searchPlaceholder', 'Search offerings…')} value={search} onChange={(e) => onSearchChange(e.target.value)}
|
|
1497
1540
|
className="w-full rounded-md border bg-background pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50" />
|
|
1498
1541
|
</div>
|
|
1499
1542
|
{loading ? (
|
|
1500
|
-
<div className="flex items-center gap-2 py-8 justify-center text-sm text-muted-foreground"><Spinner /> Loading offerings
|
|
1543
|
+
<div className="flex items-center gap-2 py-8 justify-center text-sm text-muted-foreground"><Spinner /> {t('cpq.quotes.offeringBrowser.loading', 'Loading offerings…')}</div>
|
|
1501
1544
|
) : filtered.length === 0 ? (
|
|
1502
|
-
<div className="py-8 text-center text-sm text-muted-foreground">{search ? 'No offerings match your search.' : 'No active offerings available.'}</div>
|
|
1545
|
+
<div className="py-8 text-center text-sm text-muted-foreground">{search ? t('cpq.quotes.offeringBrowser.noMatch', 'No offerings match your search.') : t('cpq.quotes.offeringBrowser.empty', 'No active offerings available.')}</div>
|
|
1503
1546
|
) : (
|
|
1504
1547
|
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
1505
1548
|
{filtered.map((offering) => (
|
|
@@ -1524,6 +1567,7 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1524
1567
|
onConfigChange: (key: string, value: unknown) => void; onQuantityChange: (q: number) => void
|
|
1525
1568
|
onSubmit: () => void; submitLabel: string; submitting: boolean
|
|
1526
1569
|
}) {
|
|
1570
|
+
const t = useT()
|
|
1527
1571
|
const noTargetsPicked =
|
|
1528
1572
|
arcTargetOptions !== null && arcTargetOptions.length > 0 && arcSelectedTargetIds.length === 0
|
|
1529
1573
|
return (
|
|
@@ -1533,7 +1577,7 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1533
1577
|
{arcTargetOptions && arcTargetOptions.length > 0 && (
|
|
1534
1578
|
<div>
|
|
1535
1579
|
<label className="block text-sm font-medium mb-1">
|
|
1536
|
-
Apply to subscription(s)
|
|
1580
|
+
{t('cpq.quotes.detail.configure.applyToSubscriptions', 'Apply to subscription(s)')}
|
|
1537
1581
|
<span className="text-destructive ml-1">*</span>
|
|
1538
1582
|
</label>
|
|
1539
1583
|
<div className="space-y-1.5 rounded-md border bg-background px-3 py-2">
|
|
@@ -1557,15 +1601,15 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1557
1601
|
})}
|
|
1558
1602
|
</div>
|
|
1559
1603
|
<p className="text-xs text-muted-foreground mt-1">
|
|
1560
|
-
Pick one or more — a separate line will be created on each selected subscription.
|
|
1604
|
+
{t('cpq.quotes.detail.configure.applyToSubscriptionsHint', 'Pick one or more — a separate line will be created on each selected subscription.')}
|
|
1561
1605
|
</p>
|
|
1562
1606
|
{noTargetsPicked && (
|
|
1563
|
-
<p className="text-xs text-destructive mt-1">Select at least one target
|
|
1607
|
+
<p className="text-xs text-destructive mt-1">{t('cpq.quotes.detail.configure.selectTarget', 'Select at least one target.')}</p>
|
|
1564
1608
|
)}
|
|
1565
1609
|
</div>
|
|
1566
1610
|
)}
|
|
1567
1611
|
<div>
|
|
1568
|
-
<label className="block text-sm font-medium mb-1">Quantity</label>
|
|
1612
|
+
<label className="block text-sm font-medium mb-1">{t('cpq.quotes.detail.configure.quantity', 'Quantity')}</label>
|
|
1569
1613
|
<NumberInput
|
|
1570
1614
|
integer
|
|
1571
1615
|
min={1}
|
|
@@ -1575,7 +1619,7 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1575
1619
|
/>
|
|
1576
1620
|
</div>
|
|
1577
1621
|
{attributes.length === 0 ? (
|
|
1578
|
-
<p className="text-sm text-muted-foreground">No configurable attributes for this offering
|
|
1622
|
+
<p className="text-sm text-muted-foreground">{t('cpq.quotes.detail.configure.noAttributes', 'No configurable attributes for this offering.')}</p>
|
|
1579
1623
|
) : (
|
|
1580
1624
|
attributes.map((attr) => <AttributeField key={attr.code} attribute={attr} value={config[attr.code]} onChange={(v) => onConfigChange(attr.code, v)} />)
|
|
1581
1625
|
)}
|
|
@@ -1591,6 +1635,7 @@ function ConfigurePanel({ title, attributes, config, quantity, arcTargetOptions,
|
|
|
1591
1635
|
}
|
|
1592
1636
|
|
|
1593
1637
|
function AttributeField({ attribute, value, onChange }: { attribute: ConstrainedAttribute; value: unknown; onChange: (v: unknown) => void }) {
|
|
1638
|
+
const t = useT()
|
|
1594
1639
|
const { code, name, attributeType, isRequired, options, constraints, helpText, defaultValue } = attribute
|
|
1595
1640
|
const currentValue = value ?? defaultValue ?? ''
|
|
1596
1641
|
|
|
@@ -1602,13 +1647,13 @@ function AttributeField({ attribute, value, onChange }: { attribute: Constrained
|
|
|
1602
1647
|
{(attributeType === 'select' || attributeType === 'enum') && options && options.length > 0 ? (
|
|
1603
1648
|
<select value={String(currentValue)} onChange={(e) => onChange(e.target.value)}
|
|
1604
1649
|
className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50">
|
|
1605
|
-
<option value=""
|
|
1650
|
+
<option value="">{t('cpq.quotes.detail.configure.selectPlaceholder', '— Select —')}</option>
|
|
1606
1651
|
{options.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
|
|
1607
1652
|
</select>
|
|
1608
1653
|
) : attributeType === 'boolean' ? (
|
|
1609
1654
|
<label className="flex items-center gap-2">
|
|
1610
1655
|
<input type="checkbox" checked={Boolean(currentValue)} onChange={(e) => onChange(e.target.checked)} className="rounded border-border" />
|
|
1611
|
-
<span className="text-sm text-muted-foreground">{helpText ?? 'Enable'}</span>
|
|
1656
|
+
<span className="text-sm text-muted-foreground">{helpText ?? t('cpq.quotes.detail.configure.enable', 'Enable')}</span>
|
|
1612
1657
|
</label>
|
|
1613
1658
|
) : attributeType === 'number' ? (
|
|
1614
1659
|
<NumberInput
|