@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
|
@@ -10,16 +10,24 @@ import {
|
|
|
10
10
|
SelectTrigger,
|
|
11
11
|
SelectValue,
|
|
12
12
|
} from '@open-mercato/ui/primitives/select'
|
|
13
|
+
import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
13
14
|
import { NumberInput } from '../../../components/NumberInput'
|
|
14
15
|
|
|
15
16
|
// ─── Shared types ────────────────────────────────────────────────
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Display labels for price-rule types, keyed by the persisted `ruleType`
|
|
20
|
+
* value. Single translatable source shared by the list, the create form and
|
|
21
|
+
* the detail page — call inside render with the component's `t`.
|
|
22
|
+
*/
|
|
23
|
+
export function buildRuleTypeLabels(t: TranslateFn): Record<string, string> {
|
|
24
|
+
return {
|
|
25
|
+
discount_percent: t('cpq.priceRules.ruleTypes.discountPercent', 'Discount %'),
|
|
26
|
+
discount_absolute: t('cpq.priceRules.ruleTypes.discountAbsolute', 'Discount $'),
|
|
27
|
+
surcharge_percent: t('cpq.priceRules.ruleTypes.surchargePercent', 'Surcharge %'),
|
|
28
|
+
surcharge_absolute: t('cpq.priceRules.ruleTypes.surchargeAbsolute', 'Surcharge $'),
|
|
29
|
+
price_override: t('cpq.priceRules.ruleTypes.priceOverride', 'Override'),
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export type PriceRuleApplicabilityCondition = {
|
|
@@ -145,15 +153,17 @@ export function PriceRuleForm({
|
|
|
145
153
|
onSave,
|
|
146
154
|
onCancel,
|
|
147
155
|
}: PriceRuleFormProps) {
|
|
156
|
+
const t = useT()
|
|
157
|
+
const ruleTypeLabels = buildRuleTypeLabels(t)
|
|
148
158
|
return (
|
|
149
159
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
150
|
-
<h2 className="text-lg font-semibold">{editingId ? 'Edit Rule' : 'New Rule'}</h2>
|
|
160
|
+
<h2 className="text-lg font-semibold">{editingId ? t('cpq.priceRules.form.editTitle', 'Edit Rule') : t('cpq.priceRules.form.newTitle', 'New Rule')}</h2>
|
|
151
161
|
{error && (
|
|
152
162
|
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{error}</div>
|
|
153
163
|
)}
|
|
154
164
|
<div className="grid grid-cols-2 gap-4">
|
|
155
165
|
<label className="space-y-1">
|
|
156
|
-
<span className="text-sm font-medium">Code</span>
|
|
166
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.code', 'Code')}</span>
|
|
157
167
|
<Input
|
|
158
168
|
value={form.code}
|
|
159
169
|
onChange={(e) => onFormChange({ ...form, code: e.target.value })}
|
|
@@ -161,14 +171,14 @@ export function PriceRuleForm({
|
|
|
161
171
|
/>
|
|
162
172
|
</label>
|
|
163
173
|
<label className="space-y-1">
|
|
164
|
-
<span className="text-sm font-medium">Name</span>
|
|
174
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.name', 'Name')}</span>
|
|
165
175
|
<Input
|
|
166
176
|
value={form.name}
|
|
167
177
|
onChange={(e) => onFormChange({ ...form, name: e.target.value })}
|
|
168
178
|
/>
|
|
169
179
|
</label>
|
|
170
180
|
<label className="space-y-1">
|
|
171
|
-
<span className="text-sm font-medium">Rule Type</span>
|
|
181
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.ruleType', 'Rule Type')}</span>
|
|
172
182
|
<Select
|
|
173
183
|
value={form.ruleType}
|
|
174
184
|
onValueChange={(value) => onFormChange({ ...form, ruleType: value })}
|
|
@@ -177,7 +187,7 @@ export function PriceRuleForm({
|
|
|
177
187
|
<SelectValue />
|
|
178
188
|
</SelectTrigger>
|
|
179
189
|
<SelectContent>
|
|
180
|
-
{Object.entries(
|
|
190
|
+
{Object.entries(ruleTypeLabels).map(([k, v]) => (
|
|
181
191
|
<SelectItem key={k} value={k}>
|
|
182
192
|
{v}
|
|
183
193
|
</SelectItem>
|
|
@@ -186,14 +196,14 @@ export function PriceRuleForm({
|
|
|
186
196
|
</Select>
|
|
187
197
|
</label>
|
|
188
198
|
<label className="space-y-1">
|
|
189
|
-
<span className="text-sm font-medium">Value</span>
|
|
199
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.value', 'Value')}</span>
|
|
190
200
|
<NumberInput
|
|
191
201
|
value={form.value === '' ? null : Number(form.value)}
|
|
192
202
|
onChange={(n) => onFormChange({ ...form, value: n == null ? '' : String(n) })}
|
|
193
203
|
/>
|
|
194
204
|
</label>
|
|
195
205
|
<label className="space-y-1">
|
|
196
|
-
<span className="text-sm font-medium">Charge Type Filter</span>
|
|
206
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.chargeTypeFilter', 'Charge Type Filter')}</span>
|
|
197
207
|
<Select
|
|
198
208
|
value={form.chargeTypeFilter || '__all__'}
|
|
199
209
|
onValueChange={(value) =>
|
|
@@ -204,23 +214,23 @@ export function PriceRuleForm({
|
|
|
204
214
|
<SelectValue />
|
|
205
215
|
</SelectTrigger>
|
|
206
216
|
<SelectContent>
|
|
207
|
-
<SelectItem value="__all__">All charge types</SelectItem>
|
|
208
|
-
<SelectItem value="nrc">NRC only</SelectItem>
|
|
209
|
-
<SelectItem value="mrc">MRC only</SelectItem>
|
|
210
|
-
<SelectItem value="usage">Usage only</SelectItem>
|
|
217
|
+
<SelectItem value="__all__">{t('cpq.priceRules.form.allChargeTypes', 'All charge types')}</SelectItem>
|
|
218
|
+
<SelectItem value="nrc">{t('cpq.priceRules.form.nrcOnly', 'NRC only')}</SelectItem>
|
|
219
|
+
<SelectItem value="mrc">{t('cpq.priceRules.form.mrcOnly', 'MRC only')}</SelectItem>
|
|
220
|
+
<SelectItem value="usage">{t('cpq.priceRules.form.usageOnly', 'Usage only')}</SelectItem>
|
|
211
221
|
</SelectContent>
|
|
212
222
|
</Select>
|
|
213
223
|
</label>
|
|
214
224
|
<label className="space-y-1">
|
|
215
|
-
<span className="text-sm font-medium">Charge Code Filter</span>
|
|
225
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.chargeCodeFilter', 'Charge Code Filter')}</span>
|
|
216
226
|
<Input
|
|
217
|
-
placeholder=
|
|
227
|
+
placeholder={t('cpq.priceRules.form.chargeCodeFilterPlaceholder', 'e.g. setup_fee (leave empty for all)')}
|
|
218
228
|
value={form.chargeCodeFilter}
|
|
219
229
|
onChange={(e) => onFormChange({ ...form, chargeCodeFilter: e.target.value })}
|
|
220
230
|
/>
|
|
221
231
|
</label>
|
|
222
232
|
<label className="space-y-1">
|
|
223
|
-
<span className="text-sm font-medium">Sort Order</span>
|
|
233
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.sortOrder', 'Sort Order')}</span>
|
|
224
234
|
<NumberInput
|
|
225
235
|
integer
|
|
226
236
|
value={form.sortOrder === '' ? null : Number(form.sortOrder)}
|
|
@@ -228,15 +238,15 @@ export function PriceRuleForm({
|
|
|
228
238
|
/>
|
|
229
239
|
</label>
|
|
230
240
|
<label className="space-y-1">
|
|
231
|
-
<span className="text-sm font-medium">Description</span>
|
|
241
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.description', 'Description')}</span>
|
|
232
242
|
<Input
|
|
233
|
-
placeholder=
|
|
243
|
+
placeholder={t('cpq.priceRules.form.descriptionPlaceholder', 'Optional')}
|
|
234
244
|
value={form.description}
|
|
235
245
|
onChange={(e) => onFormChange({ ...form, description: e.target.value })}
|
|
236
246
|
/>
|
|
237
247
|
</label>
|
|
238
248
|
<label className="space-y-1 col-span-2">
|
|
239
|
-
<span className="text-sm font-medium">Product Offering</span>
|
|
249
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.productOffering', 'Product Offering')}</span>
|
|
240
250
|
<Select
|
|
241
251
|
value={form.productOfferingId || '__global__'}
|
|
242
252
|
onValueChange={(value) =>
|
|
@@ -247,7 +257,7 @@ export function PriceRuleForm({
|
|
|
247
257
|
<SelectValue />
|
|
248
258
|
</SelectTrigger>
|
|
249
259
|
<SelectContent>
|
|
250
|
-
<SelectItem value="__global__">Global (all products)</SelectItem>
|
|
260
|
+
<SelectItem value="__global__">{t('cpq.priceRules.form.globalAllProducts', 'Global (all products)')}</SelectItem>
|
|
251
261
|
{offerings.map((o) => (
|
|
252
262
|
<SelectItem key={o.id} value={o.id}>
|
|
253
263
|
{o.name} ({o.code})
|
|
@@ -256,17 +266,17 @@ export function PriceRuleForm({
|
|
|
256
266
|
</SelectContent>
|
|
257
267
|
</Select>
|
|
258
268
|
<span className="text-xs text-muted-foreground">
|
|
259
|
-
Leave as
|
|
269
|
+
{t('cpq.priceRules.form.productOfferingHint', 'Leave as "Global" to apply to all products, or select a specific offering')}
|
|
260
270
|
</span>
|
|
261
271
|
</label>
|
|
262
272
|
<div className="col-span-2 space-y-1">
|
|
263
|
-
<span className="text-sm font-medium">Applicability Condition</span>
|
|
273
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.applicabilityCondition', 'Applicability Condition')}</span>
|
|
264
274
|
<span className="ml-2 text-xs text-muted-foreground">
|
|
265
|
-
(optional — only apply when a product attribute matches)
|
|
275
|
+
{t('cpq.priceRules.form.applicabilityConditionHint', '(optional — only apply when a product attribute matches)')}
|
|
266
276
|
</span>
|
|
267
277
|
<div className="grid grid-cols-3 gap-2">
|
|
268
278
|
<Input
|
|
269
|
-
placeholder=
|
|
279
|
+
placeholder={t('cpq.priceRules.form.conditionAttributePlaceholder', 'Attribute (e.g. port_size)')}
|
|
270
280
|
value={form.conditionAttribute}
|
|
271
281
|
onChange={(e) => onFormChange({ ...form, conditionAttribute: e.target.value })}
|
|
272
282
|
/>
|
|
@@ -278,12 +288,12 @@ export function PriceRuleForm({
|
|
|
278
288
|
<SelectValue />
|
|
279
289
|
</SelectTrigger>
|
|
280
290
|
<SelectContent>
|
|
281
|
-
<SelectItem value="eq">equals (=)</SelectItem>
|
|
282
|
-
<SelectItem value="neq">not equals (≠)</SelectItem>
|
|
291
|
+
<SelectItem value="eq">{t('cpq.priceRules.form.operatorEq', 'equals (=)')}</SelectItem>
|
|
292
|
+
<SelectItem value="neq">{t('cpq.priceRules.form.operatorNeq', 'not equals (≠)')}</SelectItem>
|
|
283
293
|
</SelectContent>
|
|
284
294
|
</Select>
|
|
285
295
|
<Input
|
|
286
|
-
placeholder=
|
|
296
|
+
placeholder={t('cpq.priceRules.form.conditionValuePlaceholder', 'Value (e.g. 100G)')}
|
|
287
297
|
value={form.conditionValue}
|
|
288
298
|
onChange={(e) => onFormChange({ ...form, conditionValue: e.target.value })}
|
|
289
299
|
/>
|
|
@@ -295,14 +305,18 @@ export function PriceRuleForm({
|
|
|
295
305
|
checked={form.isActive}
|
|
296
306
|
onCheckedChange={(checked) => onFormChange({ ...form, isActive: checked === true })}
|
|
297
307
|
/>
|
|
298
|
-
Active
|
|
308
|
+
{t('cpq.common.active', 'Active')}
|
|
299
309
|
</label>
|
|
300
310
|
<div className="flex gap-2">
|
|
301
311
|
<Button type="button" onClick={onSave} disabled={saving}>
|
|
302
|
-
{saving
|
|
312
|
+
{saving
|
|
313
|
+
? t('cpq.common.saving', 'Saving...')
|
|
314
|
+
: editingId
|
|
315
|
+
? t('cpq.priceRules.form.update', 'Update')
|
|
316
|
+
: t('cpq.priceRules.form.create', 'Create')}
|
|
303
317
|
</Button>
|
|
304
318
|
<Button type="button" variant="outline" onClick={onCancel}>
|
|
305
|
-
Cancel
|
|
319
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
306
320
|
</Button>
|
|
307
321
|
</div>
|
|
308
322
|
</div>
|
|
@@ -4,6 +4,6 @@ export const metadata = {
|
|
|
4
4
|
navHidden: true,
|
|
5
5
|
breadcrumb: [
|
|
6
6
|
{ label: 'Price Rules', labelKey: 'cpq.priceRules.list.title', href: '/backend/cpq/price-rules' },
|
|
7
|
-
{ label: 'Rule Detail' },
|
|
7
|
+
{ label: 'Rule Detail', labelKey: 'cpq.priceRules.detail.breadcrumb' },
|
|
8
8
|
],
|
|
9
9
|
} as const
|
|
@@ -3,15 +3,9 @@ import * as React from 'react'
|
|
|
3
3
|
import { useRouter, useParams } from 'next/navigation'
|
|
4
4
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
5
5
|
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
7
|
import { NumberInput } from '../../../../components/NumberInput'
|
|
7
|
-
|
|
8
|
-
const RULE_TYPE_LABELS: Record<string, string> = {
|
|
9
|
-
discount_percent: 'Discount %',
|
|
10
|
-
discount_absolute: 'Discount $',
|
|
11
|
-
surcharge_percent: 'Surcharge %',
|
|
12
|
-
surcharge_absolute: 'Surcharge $',
|
|
13
|
-
price_override: 'Override',
|
|
14
|
-
}
|
|
8
|
+
import { buildRuleTypeLabels } from '../PriceRuleForm'
|
|
15
9
|
|
|
16
10
|
type PriceRule = {
|
|
17
11
|
id: string
|
|
@@ -66,6 +60,8 @@ function ruleToForm(rule: PriceRule): FormData {
|
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
export default function PriceRuleDetailPage(props: { params?: { id?: string } }) {
|
|
63
|
+
const t = useT()
|
|
64
|
+
const ruleTypeLabels = React.useMemo(() => buildRuleTypeLabels(t), [t])
|
|
69
65
|
const router = useRouter()
|
|
70
66
|
const urlParams = useParams()
|
|
71
67
|
const ruleId = (props.params?.id ?? urlParams?.id) as string
|
|
@@ -102,7 +98,7 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
102
98
|
setError(null)
|
|
103
99
|
fetch(`/api/cpq/price-rules?id=${ruleId}`)
|
|
104
100
|
.then(async (r) => {
|
|
105
|
-
if (!r.ok) throw new Error('Not found')
|
|
101
|
+
if (!r.ok) throw new Error(t('cpq.priceRules.detail.notFound', 'Not found'))
|
|
106
102
|
return r.json()
|
|
107
103
|
})
|
|
108
104
|
.then((data: PriceRule) => {
|
|
@@ -110,7 +106,7 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
110
106
|
})
|
|
111
107
|
.catch((err) => setError(err.message))
|
|
112
108
|
.finally(() => setLoading(false))
|
|
113
|
-
}, [ruleId])
|
|
109
|
+
}, [ruleId, t])
|
|
114
110
|
|
|
115
111
|
React.useEffect(() => { loadRule() }, [loadRule])
|
|
116
112
|
|
|
@@ -164,14 +160,14 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
164
160
|
})
|
|
165
161
|
if (!res.ok) {
|
|
166
162
|
const err = await res.json()
|
|
167
|
-
throw new Error(err.error ?? 'Failed to update')
|
|
163
|
+
throw new Error(err.error ?? t('cpq.priceRules.errors.updateFailed', 'Failed to update'))
|
|
168
164
|
}
|
|
169
165
|
const updated = await res.json()
|
|
170
166
|
setRule(updated)
|
|
171
167
|
setEditing(false)
|
|
172
168
|
setForm(null)
|
|
173
169
|
} catch (err: unknown) {
|
|
174
|
-
setSaveError(err instanceof Error ? err.message : 'Unknown error')
|
|
170
|
+
setSaveError(err instanceof Error ? err.message : t('cpq.priceRules.errors.unknown', 'Unknown error'))
|
|
175
171
|
} finally {
|
|
176
172
|
setSaving(false)
|
|
177
173
|
}
|
|
@@ -205,18 +201,18 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
if (loading) {
|
|
208
|
-
return <div className="text-sm text-muted-foreground py-12 text-center">Loading
|
|
204
|
+
return <div className="text-sm text-muted-foreground py-12 text-center">{t('cpq.common.loading', 'Loading...')}</div>
|
|
209
205
|
}
|
|
210
206
|
|
|
211
207
|
if (error || !rule) {
|
|
212
208
|
return (
|
|
213
209
|
<div className="space-y-4 py-12 text-center">
|
|
214
|
-
<div className="text-sm text-destructive">{error ?? 'Rule not found'}</div>
|
|
210
|
+
<div className="text-sm text-destructive">{error ?? t('cpq.priceRules.detail.ruleNotFound', 'Rule not found')}</div>
|
|
215
211
|
<button
|
|
216
212
|
onClick={() => router.push('/backend/cpq/price-rules')}
|
|
217
213
|
className="text-sm text-primary hover:underline"
|
|
218
214
|
>
|
|
219
|
-
|
|
215
|
+
← {t('cpq.priceRules.detail.backToList', 'Back to Price Rules')}
|
|
220
216
|
</button>
|
|
221
217
|
</div>
|
|
222
218
|
)
|
|
@@ -224,6 +220,8 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
224
220
|
|
|
225
221
|
const cond = rule.applicabilityCondition as { attribute?: string; operator?: string; value?: string } | null
|
|
226
222
|
const offering = rule.productOfferingId ? offeringLookup.get(rule.productOfferingId) : null
|
|
223
|
+
// `{name}` is left unresolved so the rule name can be rendered in <strong>.
|
|
224
|
+
const deleteConfirmParts = t('cpq.priceRules.detail.deleteConfirm', 'Are you sure you want to delete {name}?').split('{name}')
|
|
227
225
|
|
|
228
226
|
return (
|
|
229
227
|
<div className="space-y-6">
|
|
@@ -234,30 +232,32 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
234
232
|
onClick={() => router.push('/backend/cpq/price-rules')}
|
|
235
233
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
236
234
|
>
|
|
237
|
-
|
|
235
|
+
← {t('cpq.common.back', 'Back')}
|
|
238
236
|
</button>
|
|
239
237
|
<h1 className="text-2xl font-bold">{rule.name}</h1>
|
|
240
238
|
<Tag variant={rule.isActive ? 'success' : 'neutral'} dot>
|
|
241
|
-
{rule.isActive ? 'Active' : 'Inactive'}
|
|
239
|
+
{rule.isActive ? t('cpq.common.active', 'Active') : t('cpq.common.inactive', 'Inactive')}
|
|
242
240
|
</Tag>
|
|
243
241
|
</div>
|
|
244
242
|
<div className="flex items-center gap-2">
|
|
245
243
|
<Button type="button" variant="outline" onClick={handleToggleActive}>
|
|
246
|
-
{rule.isActive
|
|
244
|
+
{rule.isActive
|
|
245
|
+
? t('cpq.priceRules.detail.deactivate', 'Deactivate')
|
|
246
|
+
: t('cpq.priceRules.detail.activate', 'Activate')}
|
|
247
247
|
</Button>
|
|
248
248
|
{!editing && (
|
|
249
249
|
<button
|
|
250
250
|
onClick={startEdit}
|
|
251
251
|
className="rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
252
252
|
>
|
|
253
|
-
Edit
|
|
253
|
+
{t('cpq.common.edit', 'Edit')}
|
|
254
254
|
</button>
|
|
255
255
|
)}
|
|
256
256
|
<button
|
|
257
257
|
onClick={() => setShowDeleteConfirm(true)}
|
|
258
258
|
className="rounded-md border border-destructive/30 px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10 transition-colors"
|
|
259
259
|
>
|
|
260
|
-
Delete
|
|
260
|
+
{t('cpq.common.delete', 'Delete')}
|
|
261
261
|
</button>
|
|
262
262
|
</div>
|
|
263
263
|
</div>
|
|
@@ -265,20 +265,26 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
265
265
|
{/* Delete confirmation */}
|
|
266
266
|
{showDeleteConfirm && (
|
|
267
267
|
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 flex items-center justify-between">
|
|
268
|
-
<span className="text-sm">
|
|
268
|
+
<span className="text-sm">
|
|
269
|
+
{deleteConfirmParts[0]}
|
|
270
|
+
<strong>{rule.name}</strong>
|
|
271
|
+
{deleteConfirmParts[1]}
|
|
272
|
+
</span>
|
|
269
273
|
<div className="flex gap-2">
|
|
270
274
|
<button
|
|
271
275
|
onClick={handleDelete}
|
|
272
276
|
disabled={deleting}
|
|
273
277
|
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50"
|
|
274
278
|
>
|
|
275
|
-
{deleting
|
|
279
|
+
{deleting
|
|
280
|
+
? t('cpq.priceRules.detail.deleting', 'Deleting...')
|
|
281
|
+
: t('cpq.priceRules.detail.confirmDelete', 'Yes, Delete')}
|
|
276
282
|
</button>
|
|
277
283
|
<button
|
|
278
284
|
onClick={() => setShowDeleteConfirm(false)}
|
|
279
285
|
className="rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted/50"
|
|
280
286
|
>
|
|
281
|
-
Cancel
|
|
287
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
282
288
|
</button>
|
|
283
289
|
</div>
|
|
284
290
|
</div>
|
|
@@ -287,32 +293,32 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
287
293
|
{/* Edit mode */}
|
|
288
294
|
{editing && form ? (
|
|
289
295
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
290
|
-
<h2 className="text-lg font-semibold">Edit Rule</h2>
|
|
296
|
+
<h2 className="text-lg font-semibold">{t('cpq.priceRules.form.editTitle', 'Edit Rule')}</h2>
|
|
291
297
|
{saveError && <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{saveError}</div>}
|
|
292
298
|
<div className="grid grid-cols-2 gap-4">
|
|
293
299
|
<label className="space-y-1">
|
|
294
|
-
<span className="text-sm font-medium">Code</span>
|
|
300
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.code', 'Code')}</span>
|
|
295
301
|
<input className="w-full rounded-md border bg-muted/30 px-3 py-2 text-sm" value={rule.code} disabled />
|
|
296
302
|
</label>
|
|
297
303
|
<label className="space-y-1">
|
|
298
|
-
<span className="text-sm font-medium">Name</span>
|
|
304
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.name', 'Name')}</span>
|
|
299
305
|
<input className="w-full rounded-md border px-3 py-2 text-sm" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
|
300
306
|
</label>
|
|
301
307
|
<label className="space-y-1">
|
|
302
|
-
<span className="text-sm font-medium">Rule Type</span>
|
|
308
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.ruleType', 'Rule Type')}</span>
|
|
303
309
|
<select className="w-full rounded-md border px-3 py-2 text-sm" value={form.ruleType} onChange={(e) => setForm({ ...form, ruleType: e.target.value })}>
|
|
304
|
-
{Object.entries(
|
|
310
|
+
{Object.entries(ruleTypeLabels).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
|
305
311
|
</select>
|
|
306
312
|
</label>
|
|
307
313
|
<label className="space-y-1">
|
|
308
|
-
<span className="text-sm font-medium">Value</span>
|
|
314
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.value', 'Value')}</span>
|
|
309
315
|
<NumberInput
|
|
310
316
|
value={form.value === '' ? null : Number(form.value)}
|
|
311
317
|
onChange={(n) => setForm({ ...form, value: n == null ? '' : String(n) })}
|
|
312
318
|
/>
|
|
313
319
|
</label>
|
|
314
320
|
<label className="space-y-1">
|
|
315
|
-
<span className="text-sm font-medium">Sort Order</span>
|
|
321
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.sortOrder', 'Sort Order')}</span>
|
|
316
322
|
<NumberInput
|
|
317
323
|
integer
|
|
318
324
|
value={form.sortOrder === '' ? null : Number(form.sortOrder)}
|
|
@@ -320,52 +326,52 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
320
326
|
/>
|
|
321
327
|
</label>
|
|
322
328
|
<label className="space-y-1">
|
|
323
|
-
<span className="text-sm font-medium">Description</span>
|
|
324
|
-
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder=
|
|
329
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.description', 'Description')}</span>
|
|
330
|
+
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.descriptionPlaceholder', 'Optional')} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
|
325
331
|
</label>
|
|
326
332
|
<label className="space-y-1">
|
|
327
|
-
<span className="text-sm font-medium">Charge Type Filter</span>
|
|
333
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.chargeTypeFilter', 'Charge Type Filter')}</span>
|
|
328
334
|
<select className="w-full rounded-md border px-3 py-2 text-sm" value={form.chargeTypeFilter} onChange={(e) => setForm({ ...form, chargeTypeFilter: e.target.value })}>
|
|
329
|
-
<option value="">All charge types</option>
|
|
330
|
-
<option value="nrc">NRC only</option>
|
|
331
|
-
<option value="mrc">MRC only</option>
|
|
332
|
-
<option value="usage">Usage only</option>
|
|
335
|
+
<option value="">{t('cpq.priceRules.form.allChargeTypes', 'All charge types')}</option>
|
|
336
|
+
<option value="nrc">{t('cpq.priceRules.form.nrcOnly', 'NRC only')}</option>
|
|
337
|
+
<option value="mrc">{t('cpq.priceRules.form.mrcOnly', 'MRC only')}</option>
|
|
338
|
+
<option value="usage">{t('cpq.priceRules.form.usageOnly', 'Usage only')}</option>
|
|
333
339
|
</select>
|
|
334
340
|
</label>
|
|
335
341
|
<label className="space-y-1">
|
|
336
|
-
<span className="text-sm font-medium">Charge Code Filter</span>
|
|
337
|
-
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder=
|
|
342
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.chargeCodeFilter', 'Charge Code Filter')}</span>
|
|
343
|
+
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.chargeCodeFilterPlaceholder', 'e.g. setup_fee (leave empty for all)')} value={form.chargeCodeFilter} onChange={(e) => setForm({ ...form, chargeCodeFilter: e.target.value })} />
|
|
338
344
|
</label>
|
|
339
345
|
<label className="space-y-1 col-span-2">
|
|
340
|
-
<span className="text-sm font-medium">Product Offering</span>
|
|
346
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.productOffering', 'Product Offering')}</span>
|
|
341
347
|
<select className="w-full rounded-md border px-3 py-2 text-sm" value={form.productOfferingId} onChange={(e) => setForm({ ...form, productOfferingId: e.target.value })}>
|
|
342
|
-
<option value="">Global (all products)</option>
|
|
348
|
+
<option value="">{t('cpq.priceRules.form.globalAllProducts', 'Global (all products)')}</option>
|
|
343
349
|
{offerings.map((o) => <option key={o.id} value={o.id}>{o.name} ({o.code})</option>)}
|
|
344
350
|
</select>
|
|
345
351
|
</label>
|
|
346
352
|
<div className="col-span-2 space-y-1">
|
|
347
|
-
<span className="text-sm font-medium">Applicability Condition</span>
|
|
348
|
-
<span className="ml-2 text-xs text-muted-foreground">(optional)</span>
|
|
353
|
+
<span className="text-sm font-medium">{t('cpq.priceRules.form.applicabilityCondition', 'Applicability Condition')}</span>
|
|
354
|
+
<span className="ml-2 text-xs text-muted-foreground">{t('cpq.priceRules.form.optionalHint', '(optional)')}</span>
|
|
349
355
|
<div className="grid grid-cols-3 gap-2">
|
|
350
|
-
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder=
|
|
356
|
+
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.conditionAttributePlaceholder', 'Attribute (e.g. port_size)')} value={form.conditionAttribute} onChange={(e) => setForm({ ...form, conditionAttribute: e.target.value })} />
|
|
351
357
|
<select className="w-full rounded-md border px-3 py-2 text-sm" value={form.conditionOperator} onChange={(e) => setForm({ ...form, conditionOperator: e.target.value })}>
|
|
352
|
-
<option value="eq">equals (=)</option>
|
|
353
|
-
<option value="neq">not equals (
|
|
358
|
+
<option value="eq">{t('cpq.priceRules.form.operatorEq', 'equals (=)')}</option>
|
|
359
|
+
<option value="neq">{t('cpq.priceRules.form.operatorNeq', 'not equals (≠)')}</option>
|
|
354
360
|
</select>
|
|
355
|
-
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder=
|
|
361
|
+
<input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.conditionValuePlaceholderShort', 'Value')} value={form.conditionValue} onChange={(e) => setForm({ ...form, conditionValue: e.target.value })} />
|
|
356
362
|
</div>
|
|
357
363
|
</div>
|
|
358
364
|
</div>
|
|
359
365
|
<label className="flex items-center gap-2 text-sm">
|
|
360
366
|
<input type="checkbox" checked={form.isActive} onChange={(e) => setForm({ ...form, isActive: e.target.checked })} />
|
|
361
|
-
Active
|
|
367
|
+
{t('cpq.common.active', 'Active')}
|
|
362
368
|
</label>
|
|
363
369
|
<div className="flex gap-2">
|
|
364
370
|
<button onClick={handleSave} disabled={saving} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">
|
|
365
|
-
{saving ? 'Saving...' : 'Save Changes'}
|
|
371
|
+
{saving ? t('cpq.common.saving', 'Saving...') : t('cpq.priceRules.detail.saveChanges', 'Save Changes')}
|
|
366
372
|
</button>
|
|
367
373
|
<button onClick={cancelEdit} className="rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted/50">
|
|
368
|
-
Cancel
|
|
374
|
+
{t('cpq.common.cancel', 'Cancel')}
|
|
369
375
|
</button>
|
|
370
376
|
</div>
|
|
371
377
|
</div>
|
|
@@ -374,29 +380,29 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
374
380
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
375
381
|
{/* Main info */}
|
|
376
382
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
377
|
-
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Rule Configuration</h2>
|
|
383
|
+
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.ruleConfiguration', 'Rule Configuration')}</h2>
|
|
378
384
|
<dl className="space-y-3">
|
|
379
385
|
<div className="flex justify-between">
|
|
380
|
-
<dt className="text-sm text-muted-foreground">Code</dt>
|
|
386
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.code', 'Code')}</dt>
|
|
381
387
|
<dd className="text-sm font-mono">{rule.code}</dd>
|
|
382
388
|
</div>
|
|
383
389
|
<div className="flex justify-between border-t pt-3">
|
|
384
|
-
<dt className="text-sm text-muted-foreground">Rule Type</dt>
|
|
385
|
-
<dd className="text-sm font-medium">{
|
|
390
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.ruleType', 'Rule Type')}</dt>
|
|
391
|
+
<dd className="text-sm font-medium">{ruleTypeLabels[rule.ruleType] ?? rule.ruleType}</dd>
|
|
386
392
|
</div>
|
|
387
393
|
<div className="flex justify-between border-t pt-3">
|
|
388
|
-
<dt className="text-sm text-muted-foreground">Value</dt>
|
|
394
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.value', 'Value')}</dt>
|
|
389
395
|
<dd className="text-sm font-mono font-medium">
|
|
390
396
|
{rule.ruleType.includes('percent') ? `${rule.value}%` : rule.ruleType === 'price_override' ? `= $${rule.value}` : `$${rule.value}`}
|
|
391
397
|
</dd>
|
|
392
398
|
</div>
|
|
393
399
|
<div className="flex justify-between border-t pt-3">
|
|
394
|
-
<dt className="text-sm text-muted-foreground">Sort Order</dt>
|
|
400
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.sortOrder', 'Sort Order')}</dt>
|
|
395
401
|
<dd className="text-sm font-mono">{rule.sortOrder}</dd>
|
|
396
402
|
</div>
|
|
397
403
|
{rule.description && (
|
|
398
404
|
<div className="border-t pt-3">
|
|
399
|
-
<dt className="text-sm text-muted-foreground mb-1">Description</dt>
|
|
405
|
+
<dt className="text-sm text-muted-foreground mb-1">{t('cpq.priceRules.detail.description', 'Description')}</dt>
|
|
400
406
|
<dd className="text-sm">{rule.description}</dd>
|
|
401
407
|
</div>
|
|
402
408
|
)}
|
|
@@ -405,10 +411,10 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
405
411
|
|
|
406
412
|
{/* Scope & Filters */}
|
|
407
413
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
408
|
-
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Scope &
|
|
414
|
+
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.scopeAndFilters', 'Scope & Filters')}</h2>
|
|
409
415
|
<dl className="space-y-3">
|
|
410
416
|
<div className="flex justify-between">
|
|
411
|
-
<dt className="text-sm text-muted-foreground">Scope</dt>
|
|
417
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.scope', 'Scope')}</dt>
|
|
412
418
|
<dd className="text-sm">
|
|
413
419
|
{offering ? (
|
|
414
420
|
<span className="inline-flex items-center gap-1.5">
|
|
@@ -419,28 +425,28 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
419
425
|
) : (
|
|
420
426
|
<span className="inline-flex items-center gap-1.5">
|
|
421
427
|
<span className="inline-block w-2 h-2 rounded-full bg-muted-foreground" />
|
|
422
|
-
Global (all offerings)
|
|
428
|
+
{t('cpq.priceRules.detail.globalAllOfferings', 'Global (all offerings)')}
|
|
423
429
|
</span>
|
|
424
430
|
)}
|
|
425
431
|
</dd>
|
|
426
432
|
</div>
|
|
427
433
|
<div className="flex justify-between border-t pt-3">
|
|
428
|
-
<dt className="text-sm text-muted-foreground">Charge Type</dt>
|
|
429
|
-
<dd className="text-sm">{rule.chargeTypeFilter ? rule.chargeTypeFilter.toUpperCase() : 'All types'}</dd>
|
|
434
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.chargeType', 'Charge Type')}</dt>
|
|
435
|
+
<dd className="text-sm">{rule.chargeTypeFilter ? rule.chargeTypeFilter.toUpperCase() : t('cpq.priceRules.detail.allTypes', 'All types')}</dd>
|
|
430
436
|
</div>
|
|
431
437
|
<div className="flex justify-between border-t pt-3">
|
|
432
|
-
<dt className="text-sm text-muted-foreground">Charge Code</dt>
|
|
433
|
-
<dd className="text-sm font-mono">{rule.chargeCodeFilter ?? 'All charges'}</dd>
|
|
438
|
+
<dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.chargeCode', 'Charge Code')}</dt>
|
|
439
|
+
<dd className="text-sm font-mono">{rule.chargeCodeFilter ?? t('cpq.priceRules.detail.allCharges', 'All charges')}</dd>
|
|
434
440
|
</div>
|
|
435
441
|
<div className="border-t pt-3">
|
|
436
|
-
<dt className="text-sm text-muted-foreground mb-1">Applicability Condition</dt>
|
|
442
|
+
<dt className="text-sm text-muted-foreground mb-1">{t('cpq.priceRules.detail.applicabilityCondition', 'Applicability Condition')}</dt>
|
|
437
443
|
<dd className="text-sm">
|
|
438
444
|
{cond?.attribute ? (
|
|
439
445
|
<code className="rounded bg-muted px-2 py-0.5 text-xs">
|
|
440
446
|
{cond.attribute} {cond.operator === 'neq' ? '≠' : '='} {cond.value ?? '""'}
|
|
441
447
|
</code>
|
|
442
448
|
) : (
|
|
443
|
-
<span className="text-muted-foreground">No condition (always applies)</span>
|
|
449
|
+
<span className="text-muted-foreground">{t('cpq.priceRules.detail.noCondition', 'No condition (always applies)')}</span>
|
|
444
450
|
)}
|
|
445
451
|
</dd>
|
|
446
452
|
</div>
|
|
@@ -449,18 +455,18 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
|
|
|
449
455
|
|
|
450
456
|
{/* Metadata */}
|
|
451
457
|
<div className="rounded-lg border bg-card p-6 space-y-4 lg:col-span-2">
|
|
452
|
-
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Metadata</h2>
|
|
458
|
+
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.metadata', 'Metadata')}</h2>
|
|
453
459
|
<dl className="flex gap-8 text-sm">
|
|
454
460
|
<div>
|
|
455
|
-
<dt className="text-muted-foreground">Created</dt>
|
|
461
|
+
<dt className="text-muted-foreground">{t('cpq.priceRules.detail.created', 'Created')}</dt>
|
|
456
462
|
<dd>{new Date(rule.createdAt).toLocaleString()}</dd>
|
|
457
463
|
</div>
|
|
458
464
|
<div>
|
|
459
|
-
<dt className="text-muted-foreground">Updated</dt>
|
|
465
|
+
<dt className="text-muted-foreground">{t('cpq.priceRules.detail.updated', 'Updated')}</dt>
|
|
460
466
|
<dd>{new Date(rule.updatedAt).toLocaleString()}</dd>
|
|
461
467
|
</div>
|
|
462
468
|
<div>
|
|
463
|
-
<dt className="text-muted-foreground">ID</dt>
|
|
469
|
+
<dt className="text-muted-foreground">{t('cpq.priceRules.detail.id', 'ID')}</dt>
|
|
464
470
|
<dd className="font-mono text-xs">{rule.id}</dd>
|
|
465
471
|
</div>
|
|
466
472
|
</dl>
|