updates on the ui changes and changed into doormile
This commit is contained in:
583
src/pages/doormile/pricing/Pricing.jsx
Normal file
583
src/pages/doormile/pricing/Pricing.jsx
Normal file
@@ -0,0 +1,583 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Building2, Calculator, IndianRupee, MapPin, Pencil, Plus, Tag, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
Button, ConfirmModal, DataTable, EmptyState, Field, Grid, IconButton, Input, KpiCard,
|
||||
Modal, PageHeader, Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
Stack, StatusBadge, Surface,
|
||||
} from '@/components/ds';
|
||||
import { ListToolbar } from '@/components/doormile/ListToolbar';
|
||||
import {
|
||||
useAppLocations, useCreatePricing, useDeletePricing, usePricing, useSimulatePricing,
|
||||
useTenants, useUpdatePricing,
|
||||
} from '@/lib/doormileHooks';
|
||||
import { currency, matchesQuery, orDash, useDebouncedValue } from '@/lib/doormileFormat';
|
||||
|
||||
/**
|
||||
* Client pricing — the per-tenant rate cards quotes are calculated from.
|
||||
*
|
||||
* This is `/admin/pricing`, the tenant-scoped rules. Doormile's own internal
|
||||
* banding lives on a separate resource with no tenant concept at all and is not
|
||||
* edited here.
|
||||
*/
|
||||
|
||||
const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'];
|
||||
|
||||
/** Confirmed live: every real pricing row uses this exact casing. */
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'Active', label: 'Active' },
|
||||
{ value: 'Inactive', label: 'Inactive' },
|
||||
];
|
||||
|
||||
const EMPTY_FORM = {
|
||||
pricingid: null,
|
||||
tenantid: '',
|
||||
applocationid: '',
|
||||
vehicletype: 'Bike',
|
||||
baseprice: '',
|
||||
baseweight: '',
|
||||
priceperkg: '',
|
||||
basedistance: '',
|
||||
priceperkm: '',
|
||||
handlingcharges: '',
|
||||
priority: 1,
|
||||
status: 'Active',
|
||||
};
|
||||
|
||||
const EMPTY_QUOTE = { tenantid: '', applocationid: '', vehicletype: 'Bike', weight: '', distance: '' };
|
||||
|
||||
const ALL_ZONES = 'all';
|
||||
|
||||
export default function Pricing() {
|
||||
const { data: pricing = [], isLoading, isFetching } = usePricing();
|
||||
const { data: tenants = [] } = useTenants();
|
||||
const { data: locations = [] } = useAppLocations();
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const debouncedSearch = useDebouncedValue(search);
|
||||
const [zone, setZone] = useState(ALL_ZONES);
|
||||
|
||||
const [form, setForm] = useState(EMPTY_FORM);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [pendingDelete, setPendingDelete] = useState(null);
|
||||
|
||||
const [quoteOpen, setQuoteOpen] = useState(false);
|
||||
const [quoteForm, setQuoteForm] = useState(EMPTY_QUOTE);
|
||||
|
||||
const createPricing = useCreatePricing();
|
||||
const updatePricing = useUpdatePricing();
|
||||
const deletePricing = useDeletePricing();
|
||||
const simulate = useSimulatePricing();
|
||||
|
||||
const tenantMap = useMemo(
|
||||
() => new Map((tenants || []).map((tenant) => [tenant.tenantid, tenant])),
|
||||
[tenants]
|
||||
);
|
||||
const zoneMap = useMemo(
|
||||
() => new Map((locations || []).map((location) => [location.applocationid, location.locationname])),
|
||||
[locations]
|
||||
);
|
||||
|
||||
const tenantName = (id) => tenantMap.get(id)?.tenantname || (id ? `Tenant #${id}` : '—');
|
||||
const zoneName = (id) => zoneMap.get(id) || (id ? `Zone #${id}` : 'All zones');
|
||||
|
||||
/* Zone is applied before the stats are taken, so the KPI row describes what
|
||||
the table is actually showing rather than the whole account. */
|
||||
const zonePricing = useMemo(() => {
|
||||
if (zone === ALL_ZONES) return pricing || [];
|
||||
return (pricing || []).filter((row) => String(row.applocationid) === String(zone));
|
||||
}, [pricing, zone]);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const total = zonePricing.length;
|
||||
const tenantsPriced = new Set(zonePricing.map((row) => row.tenantid).filter(Boolean)).size;
|
||||
const avgBase = total
|
||||
? zonePricing.reduce((sum, row) => sum + (Number(row.baseprice) || 0), 0) / total
|
||||
: 0;
|
||||
const active = zonePricing.filter((row) => String(row.status || '').toLowerCase() === 'active').length;
|
||||
return { total, tenantsPriced, avgBase, active };
|
||||
}, [zonePricing]);
|
||||
|
||||
const rows = useMemo(
|
||||
() =>
|
||||
zonePricing.filter((row) =>
|
||||
matchesQuery(row, ['vehicletype', 'status', (r) => tenantName(r.tenantid), (r) => zoneName(r.applocationid)], debouncedSearch)
|
||||
),
|
||||
|
||||
[zonePricing, debouncedSearch, tenantMap, zoneMap]
|
||||
);
|
||||
|
||||
const set = (key) => (value) => setForm((current) => ({ ...current, [key]: value }));
|
||||
const setQuote = (key) => (value) => setQuoteForm((current) => ({ ...current, [key]: value }));
|
||||
|
||||
const openCreate = () => {
|
||||
setForm(EMPTY_FORM);
|
||||
setErrors({});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (row) => {
|
||||
setForm({
|
||||
pricingid: row.pricingid,
|
||||
tenantid: row.tenantid ?? '',
|
||||
applocationid: row.applocationid ?? '',
|
||||
vehicletype: row.vehicletype || 'Bike',
|
||||
baseprice: row.baseprice ?? '',
|
||||
baseweight: row.baseweight ?? '',
|
||||
priceperkg: row.priceperkg ?? '',
|
||||
basedistance: row.basedistance ?? '',
|
||||
priceperkm: row.priceperkm ?? '',
|
||||
handlingcharges: row.handlingcharges ?? '',
|
||||
priority: row.priority ?? 1,
|
||||
status: row.status || 'Active',
|
||||
});
|
||||
setErrors({});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
const nextErrors = {};
|
||||
if (!form.tenantid) nextErrors.tenantid = 'Choose the client this rate applies to';
|
||||
if (form.baseprice === '') nextErrors.baseprice = 'Enter a base price';
|
||||
if (form.priceperkm === '') nextErrors.priceperkm = 'Enter a per-kilometre rate';
|
||||
setErrors(nextErrors);
|
||||
if (Object.keys(nextErrors).length) return;
|
||||
|
||||
const data = {
|
||||
tenantid: Number(form.tenantid),
|
||||
applocationid: form.applocationid ? Number(form.applocationid) : undefined,
|
||||
vehicletype: form.vehicletype,
|
||||
baseprice: Number(form.baseprice),
|
||||
baseweight: Number(form.baseweight) || 0,
|
||||
priceperkg: Number(form.priceperkg) || 0,
|
||||
basedistance: Number(form.basedistance) || 0,
|
||||
priceperkm: Number(form.priceperkm),
|
||||
handlingcharges: Number(form.handlingcharges) || 0,
|
||||
/* `effectivefrom` is required and confirmed live to reject a bare
|
||||
YYYY-MM-DD — it needs a full ISO datetime. */
|
||||
effectivefrom: new Date().toISOString(),
|
||||
currency: 'INR',
|
||||
priority: Number(form.priority) || 1,
|
||||
status: form.status,
|
||||
};
|
||||
|
||||
const mutation = form.pricingid ? updatePricing : createPricing;
|
||||
const variables = form.pricingid ? { id: form.pricingid, data } : data;
|
||||
mutation.mutate(variables, { onSuccess: (res) => res?.success !== false && setDialogOpen(false) });
|
||||
};
|
||||
|
||||
const runQuote = () => {
|
||||
if (!quoteForm.tenantid || quoteForm.distance === '') return;
|
||||
simulate.mutate({
|
||||
tenantid: Number(quoteForm.tenantid),
|
||||
applocationid: quoteForm.applocationid ? Number(quoteForm.applocationid) : undefined,
|
||||
vehicletype: quoteForm.vehicletype,
|
||||
weight: Number(quoteForm.weight) || 0,
|
||||
distance: Number(quoteForm.distance) || 0,
|
||||
});
|
||||
};
|
||||
|
||||
const closeQuote = () => {
|
||||
setQuoteOpen(false);
|
||||
simulate.reset();
|
||||
setQuoteForm(EMPTY_QUOTE);
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'tenant',
|
||||
header: 'Client',
|
||||
sortable: true,
|
||||
accessor: (row) => tenantName(row.tenantid),
|
||||
cell: (row) => (
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-body-sm font-semibold text-ink-1">{tenantName(row.tenantid)}</p>
|
||||
<p className="text-caption text-ink-3">{zoneName(row.applocationid)}</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'vehicletype', header: 'Vehicle', sortable: true, accessor: (row) => orDash(row.vehicletype) },
|
||||
{
|
||||
key: 'baseprice',
|
||||
header: 'Base',
|
||||
sortValue: (row) => Number(row.baseprice) || 0,
|
||||
accessor: (row) => currency(row.baseprice),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'priceperkm',
|
||||
header: 'Per km',
|
||||
sortValue: (row) => Number(row.priceperkm) || 0,
|
||||
accessor: (row) => currency(row.priceperkm),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'priceperkg',
|
||||
header: 'Per kg',
|
||||
sortValue: (row) => Number(row.priceperkg) || 0,
|
||||
accessor: (row) => currency(row.priceperkg),
|
||||
align: 'right',
|
||||
hideBelow: 'lg',
|
||||
},
|
||||
{
|
||||
key: 'handlingcharges',
|
||||
header: 'Handling',
|
||||
sortValue: (row) => Number(row.handlingcharges) || 0,
|
||||
accessor: (row) => currency(row.handlingcharges),
|
||||
align: 'right',
|
||||
hideBelow: 'lg',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
sortable: true,
|
||||
cell: (row) => <StatusBadge status={row.status} dot size="sm" />,
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'Actions',
|
||||
align: 'right',
|
||||
cell: (row) => (
|
||||
<div className="flex justify-end gap-1">
|
||||
<IconButton label="Edit rule" icon={Pencil} variant="ghost" size="sm" onClick={() => openEdit(row)} />
|
||||
<IconButton label="Delete rule" icon={Trash2} variant="ghost" size="sm" onClick={() => setPendingDelete(row)} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
[tenantMap, zoneMap]
|
||||
);
|
||||
|
||||
const quoteResult = simulate.data?.data || simulate.data;
|
||||
|
||||
return (
|
||||
<Stack space="lg">
|
||||
<PageHeader
|
||||
icon={Tag}
|
||||
title="Pricing"
|
||||
subtitle="Per-client rate cards — what a delivery is quoted at"
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setQuoteOpen(true)}>
|
||||
<Calculator className="mr-1.5 h-4 w-4" /> Get a quote
|
||||
</Button>
|
||||
<Button onClick={openCreate}>
|
||||
<Plus className="mr-1.5 h-4 w-4" /> New rule
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid cols={4}>
|
||||
<KpiCard icon={Tag} value={stats.total} label="Pricing rules" tone="brand" index={0} loading={isLoading} />
|
||||
<KpiCard
|
||||
icon={Building2}
|
||||
value={stats.tenantsPriced}
|
||||
label="Clients priced"
|
||||
tone="navy"
|
||||
index={1}
|
||||
loading={isLoading}
|
||||
/>
|
||||
<KpiCard
|
||||
icon={IndianRupee}
|
||||
value={currency(stats.avgBase, '₹0')}
|
||||
label="Average base price"
|
||||
tone="accent"
|
||||
index={2}
|
||||
loading={isLoading}
|
||||
/>
|
||||
<KpiCard icon={MapPin} value={stats.active} label="Active rules" tone="success" index={3} loading={isLoading} />
|
||||
</Grid>
|
||||
|
||||
<Stack space="sm">
|
||||
<ListToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
placeholder="Search by client, zone or vehicle"
|
||||
count={{ shown: rows.length, total: zonePricing.length, noun: 'rules' }}
|
||||
>
|
||||
<Select value={String(zone)} onValueChange={setZone}>
|
||||
<SelectTrigger className="w-full sm:w-48">
|
||||
<SelectValue placeholder="All zones" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_ZONES}>All zones</SelectItem>
|
||||
{(locations || [])
|
||||
.filter((location) => location.applocationid)
|
||||
.map((location) => (
|
||||
<SelectItem key={location.applocationid} value={String(location.applocationid)}>
|
||||
{location.locationname}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ListToolbar>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
getRowId={(row, index) => row.pricingid ?? index}
|
||||
loading={isLoading}
|
||||
refreshing={isFetching && !isLoading}
|
||||
pageSize={10}
|
||||
defaultSort={{ key: 'tenant', direction: 'asc' }}
|
||||
isFiltered={Boolean(debouncedSearch) || zone !== ALL_ZONES}
|
||||
onClearFilters={() => {
|
||||
setSearch('');
|
||||
setZone(ALL_ZONES);
|
||||
}}
|
||||
emptyState={
|
||||
debouncedSearch || zone !== ALL_ZONES ? undefined : (
|
||||
<EmptyState
|
||||
icon={Tag}
|
||||
title="No pricing rules yet"
|
||||
description="Add a rate card so this client's deliveries can be quoted."
|
||||
action={{ label: 'New rule', icon: Plus, onClick: openCreate }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Modal
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
title={form.pricingid ? 'Edit pricing rule' : 'New pricing rule'}
|
||||
description="Leave the zone empty to price a client the same way everywhere."
|
||||
icon={Tag}
|
||||
size="lg"
|
||||
busy={createPricing.isPending || updatePricing.isPending}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={save} disabled={createPricing.isPending || updatePricing.isPending}>
|
||||
{form.pricingid ? 'Save changes' : 'Create rule'}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<Field label="Client" required error={errors.tenantid}>
|
||||
<Select value={String(form.tenantid)} onValueChange={set('tenantid')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a client" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tenants || []).map((tenant) => (
|
||||
<SelectItem key={tenant.tenantid} value={String(tenant.tenantid)}>
|
||||
{tenant.tenantname || `Tenant #${tenant.tenantid}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label="Zone" hint="Optional — applies everywhere when left empty">
|
||||
<Select
|
||||
value={form.applocationid === '' ? ALL_ZONES : String(form.applocationid)}
|
||||
onValueChange={(value) => set('applocationid')(value === ALL_ZONES ? '' : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All zones" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_ZONES}>All zones</SelectItem>
|
||||
{(locations || [])
|
||||
.filter((location) => location.applocationid)
|
||||
.map((location) => (
|
||||
<SelectItem key={location.applocationid} value={String(location.applocationid)}>
|
||||
{location.locationname}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label="Vehicle type">
|
||||
<Select value={form.vehicletype} onValueChange={set('vehicletype')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{VEHICLE_TYPES.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label="Status">
|
||||
<Select value={form.status} onValueChange={set('status')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label="Base price" required error={errors.baseprice}>
|
||||
<Input type="number" value={form.baseprice} onChange={(e) => set('baseprice')(e.target.value)} placeholder="40" />
|
||||
</Field>
|
||||
|
||||
<Field label="Price per km" required error={errors.priceperkm}>
|
||||
<Input type="number" value={form.priceperkm} onChange={(e) => set('priceperkm')(e.target.value)} placeholder="8" />
|
||||
</Field>
|
||||
|
||||
<Field label="Base distance (km)" hint="Distance included in the base price">
|
||||
<Input type="number" value={form.basedistance} onChange={(e) => set('basedistance')(e.target.value)} placeholder="3" />
|
||||
</Field>
|
||||
|
||||
<Field label="Base weight (kg)" hint="Weight included in the base price">
|
||||
<Input type="number" value={form.baseweight} onChange={(e) => set('baseweight')(e.target.value)} placeholder="5" />
|
||||
</Field>
|
||||
|
||||
<Field label="Price per kg">
|
||||
<Input type="number" value={form.priceperkg} onChange={(e) => set('priceperkg')(e.target.value)} placeholder="4" />
|
||||
</Field>
|
||||
|
||||
<Field label="Handling charges">
|
||||
<Input
|
||||
type="number"
|
||||
value={form.handlingcharges}
|
||||
onChange={(e) => set('handlingcharges')(e.target.value)}
|
||||
placeholder="10"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Priority" hint="Lower wins when two rules could both apply">
|
||||
<Input type="number" value={form.priority} onChange={(e) => set('priority')(e.target.value)} placeholder="1" />
|
||||
</Field>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={quoteOpen}
|
||||
onOpenChange={(open) => !open && closeQuote()}
|
||||
title="Get a quote"
|
||||
description="Runs the same calculation the booking flow uses, without creating anything."
|
||||
icon={Calculator}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={closeQuote}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={runQuote} disabled={simulate.isPending || !quoteForm.tenantid || quoteForm.distance === ''}>
|
||||
Get quote
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Stack space="default">
|
||||
<Field label="Client" required>
|
||||
<Select value={String(quoteForm.tenantid)} onValueChange={setQuote('tenantid')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a client" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tenants || []).map((tenant) => (
|
||||
<SelectItem key={tenant.tenantid} value={String(tenant.tenantid)}>
|
||||
{tenant.tenantname || `Tenant #${tenant.tenantid}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label="Zone">
|
||||
<Select
|
||||
value={quoteForm.applocationid === '' ? ALL_ZONES : String(quoteForm.applocationid)}
|
||||
onValueChange={(value) => setQuote('applocationid')(value === ALL_ZONES ? '' : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All zones" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_ZONES}>All zones</SelectItem>
|
||||
{(locations || [])
|
||||
.filter((location) => location.applocationid)
|
||||
.map((location) => (
|
||||
<SelectItem key={location.applocationid} value={String(location.applocationid)}>
|
||||
{location.locationname}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Field label="Vehicle">
|
||||
<Select value={quoteForm.vehicletype} onValueChange={setQuote('vehicletype')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{VEHICLE_TYPES.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label="Weight (kg)">
|
||||
<Input type="number" value={quoteForm.weight} onChange={(e) => setQuote('weight')(e.target.value)} />
|
||||
</Field>
|
||||
|
||||
<Field label="Distance (km)" required>
|
||||
<Input type="number" value={quoteForm.distance} onChange={(e) => setQuote('distance')(e.target.value)} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{simulate.isPending && <p className="text-body-sm text-ink-3">Calculating…</p>}
|
||||
|
||||
{quoteResult ? (
|
||||
<Surface variant="sunken" padding="sm" radius="lg">
|
||||
{simulate.data?.success === false ? (
|
||||
<p className="text-body-sm text-destructive">{simulate.data.message || 'No quote available'}</p>
|
||||
) : (
|
||||
/* The quote response has no documented schema, so every field
|
||||
it returns is shown rather than a hand-picked subset that
|
||||
might omit the one that matters. */
|
||||
<dl className="space-y-1.5">
|
||||
{Object.entries(quoteResult).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center justify-between gap-4">
|
||||
<dt className="text-caption uppercase tracking-wide text-ink-3">{key}</dt>
|
||||
<dd className="text-body-sm font-semibold text-ink-1">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
</Surface>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<ConfirmModal
|
||||
open={Boolean(pendingDelete)}
|
||||
onOpenChange={(open) => !open && setPendingDelete(null)}
|
||||
title="Delete this pricing rule?"
|
||||
description={`${tenantName(pendingDelete?.tenantid)} · ${pendingDelete?.vehicletype || ''} will no longer be priced by this rule.`}
|
||||
confirmLabel="Delete rule"
|
||||
busy={deletePricing.isPending}
|
||||
onConfirm={() => {
|
||||
deletePricing.mutate(pendingDelete.pricingid, { onSettled: () => setPendingDelete(null) });
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user