updates on the ui changes and table
This commit is contained in:
@@ -40,6 +40,7 @@ import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import { PageShell, KpiGrid, FilterToolbar, DataCard } from 'components/nearle_components/PageLayout';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
import { getallpricing, getTenants, fetchAppLocations } from 'pages/api/api';
|
||||
import { createAdminPricing, updateAdminPricing, deleteAdminPricing, simulatePricing } from 'pages/api/doormileApi';
|
||||
@@ -115,7 +116,10 @@ const ClientsPricing = () => {
|
||||
[locations]
|
||||
);
|
||||
const locationOptions = useMemo(() => (locations || []).filter((l) => l.applocationid), [locations]);
|
||||
const tenantSelectOptions = useMemo(() => (tenants || []).map((t) => ({ value: String(t.tenantid), label: t.tenantname || `Tenant #${t.tenantid}` })), [tenants]);
|
||||
const tenantSelectOptions = useMemo(
|
||||
() => (tenants || []).map((t) => ({ value: String(t.tenantid), label: t.tenantname || `Tenant #${t.tenantid}` })),
|
||||
[tenants]
|
||||
);
|
||||
const locationSelectOptions = useMemo(
|
||||
() => locationOptions.map((l) => ({ value: String(l.applocationid), label: l.locationname || `Zone #${l.applocationid}` })),
|
||||
[locationOptions]
|
||||
@@ -123,9 +127,7 @@ const ClientsPricing = () => {
|
||||
|
||||
const tenantName = (row) => row.tenantname || tenantMap.get(row.tenantid)?.tenantname || (row.tenantid ? `Tenant #${row.tenantid}` : '—');
|
||||
const zoneName = (row) =>
|
||||
row.applocation ||
|
||||
locationMap.get(String(row.applocationid))?.locationname ||
|
||||
(row.applocationid ? `Zone #${row.applocationid}` : '—');
|
||||
row.applocation || locationMap.get(String(row.applocationid))?.locationname || (row.applocationid ? `Zone #${row.applocationid}` : '—');
|
||||
|
||||
// getallpricing() takes no zone arg (see api.js) — the LocationAutocomplete
|
||||
// only ever changed the header subtitle text, not which rows were shown.
|
||||
@@ -136,10 +138,7 @@ const ClientsPricing = () => {
|
||||
// from) are two independently-serialized resources with no guarantee they
|
||||
// agree on number-vs-string; a raw === silently zeroed out every row
|
||||
// whenever they didn't match, making the table look empty for any zone.
|
||||
const zonePricing = useMemo(
|
||||
() => (appId ? pricing.filter((r) => String(r.applocationid) === String(appId)) : pricing),
|
||||
[pricing, appId]
|
||||
);
|
||||
const zonePricing = useMemo(() => (appId ? pricing.filter((r) => String(r.applocationid) === String(appId)) : pricing), [pricing, appId]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
if (!debouncedSearch) return zonePricing;
|
||||
@@ -286,54 +285,56 @@ const ClientsPricing = () => {
|
||||
<>
|
||||
{(isLoading || savePricingMutation.isPending || deletePricingMutation.isPending) && <Loader />}
|
||||
|
||||
<PageHeader
|
||||
title="Pricing"
|
||||
subtitle={`Live · ${locaName || 'All Zones'}`}
|
||||
live
|
||||
action={
|
||||
<HStack gap={1.5} vAlign="center" wrap="wrap">
|
||||
<LocationAutocomplete locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} placeholder="Select Zone" />
|
||||
<Button label="Quote Price" variant="secondary" icon={<MdCalculate size={16} />} onClick={() => setQuoteOpen(true)} />
|
||||
<Button label="New Pricing Rule" variant="primary" icon={<MdAdd size={16} />} onClick={openCreateDialog} />
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid columns={{ minWidth: 180 }} gap={3}>
|
||||
{KPI_META.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return <StatCard key={item.key} title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} />;
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
<VStack gap={0} padding={0} style={{ marginTop: 16 }}>
|
||||
<Card padding={3} elevation="none" style={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0, borderBottom: 'none' }}>
|
||||
<HStack justify="between" vAlign="center" gap={3} wrap="wrap">
|
||||
<HStack gap={2} vAlign="center">
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdLocalOffer size={18} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text type="label" weight="bold" color="secondary" style={{ letterSpacing: 0.6, textTransform: 'uppercase' }}>
|
||||
Pricing Catalog
|
||||
</Text>
|
||||
<Text weight="bold">
|
||||
{pricing.length} total · {rows.length} shown
|
||||
</Text>
|
||||
</VStack>
|
||||
<PageShell>
|
||||
<PageHeader
|
||||
title="Pricing"
|
||||
subtitle={`Live · ${locaName || 'All Zones'}`}
|
||||
live
|
||||
action={
|
||||
<HStack gap={1.5} vAlign="center" wrap="wrap">
|
||||
<LocationAutocomplete locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} placeholder="Select Zone" />
|
||||
<Button label="Quote Price" variant="secondary" icon={<MdCalculate size={16} />} onClick={() => setQuoteOpen(true)} />
|
||||
<Button label="New Pricing Rule" variant="primary" icon={<MdAdd size={16} />} onClick={openCreateDialog} />
|
||||
</HStack>
|
||||
<div style={{ width: 300, maxWidth: '100%' }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search pricing (ctrl+k)"
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</Card>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card padding={0} elevation="none" style={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, overflow: 'hidden' }}>
|
||||
<KpiGrid>
|
||||
{KPI_META.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return <StatCard key={item.key} title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} />;
|
||||
})}
|
||||
</KpiGrid>
|
||||
|
||||
<DataCard
|
||||
toolbar={
|
||||
<FilterToolbar max={2}>
|
||||
<HStack gap={2} vAlign="center">
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdLocalOffer size={18} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text type="label" weight="bold" color="secondary" style={{ letterSpacing: 0.6, textTransform: 'uppercase' }}>
|
||||
Pricing Catalog
|
||||
</Text>
|
||||
<Text weight="bold">
|
||||
{pricing.length} total · {rows.length} shown
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<HStack justify="end">
|
||||
<VStack width={260} padding={0} gap={0}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search pricing (ctrl+k)"
|
||||
/>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</FilterToolbar>
|
||||
}
|
||||
>
|
||||
{isMobile ? (
|
||||
rows.length === 0 && !isLoading ? (
|
||||
<EmptyState
|
||||
@@ -363,7 +364,14 @@ const ClientsPricing = () => {
|
||||
</VStack>
|
||||
</HStack>
|
||||
<HStack gap={0.5}>
|
||||
<IconButton label="Edit rule" tooltip="Edit rule" size="sm" variant="ghost" icon={<MdEdit size={14} />} onClick={() => openEditDialog(row)} />
|
||||
<IconButton
|
||||
label="Edit rule"
|
||||
tooltip="Edit rule"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MdEdit size={14} />}
|
||||
onClick={() => openEditDialog(row)}
|
||||
/>
|
||||
<IconButton
|
||||
label="Delete rule"
|
||||
tooltip="Delete rule"
|
||||
@@ -491,7 +499,13 @@ const ClientsPricing = () => {
|
||||
<TableCell>
|
||||
<HStack gap={1} justify="end">
|
||||
<Tooltip content="Edit rule">
|
||||
<IconButton label="Edit rule" size="sm" variant="ghost" icon={<MdEdit size={16} />} onClick={() => openEditDialog(row)} />
|
||||
<IconButton
|
||||
label="Edit rule"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MdEdit size={16} />}
|
||||
onClick={() => openEditDialog(row)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="Delete rule">
|
||||
<IconButton
|
||||
@@ -511,8 +525,8 @@ const ClientsPricing = () => {
|
||||
</Table>
|
||||
</TableScroll>
|
||||
)}
|
||||
</Card>
|
||||
</VStack>
|
||||
</DataCard>
|
||||
</PageShell>
|
||||
|
||||
<Dialog isOpen={dialogOpen} onOpenChange={(next) => !next && setDialogOpen(false)} width={720} purpose="form">
|
||||
<Layout
|
||||
@@ -529,30 +543,81 @@ const ClientsPricing = () => {
|
||||
}
|
||||
content={
|
||||
<LayoutContent padding={4}>
|
||||
<Grid columns={{ minWidth: 220 }} gap={3}>
|
||||
<Selector label="Tenant" options={tenantSelectOptions} value={form.tenant} onChange={set('tenant')} placeholder="Choose tenant" hasSearch isRequired />
|
||||
<Selector label="Zone" options={locationSelectOptions} value={form.location} onChange={set('location')} placeholder="Choose zone" hasSearch isOptional />
|
||||
<Grid columns={{ minWidth: 220, repeat: 'fit' }} gap={3}>
|
||||
<Selector
|
||||
label="Tenant"
|
||||
options={tenantSelectOptions}
|
||||
value={form.tenant}
|
||||
onChange={set('tenant')}
|
||||
placeholder="Choose tenant"
|
||||
hasSearch
|
||||
isRequired
|
||||
/>
|
||||
<Selector
|
||||
label="Zone"
|
||||
options={locationSelectOptions}
|
||||
value={form.location}
|
||||
onChange={set('location')}
|
||||
placeholder="Choose zone"
|
||||
hasSearch
|
||||
isOptional
|
||||
/>
|
||||
<Selector label="Vehicle Type" options={VEHICLE_TYPE_OPTIONS} value={form.vehicletype} onChange={set('vehicletype')} />
|
||||
<Selector label="Status" options={STATUS_OPTIONS} value={form.status} onChange={set('status')} />
|
||||
<NumberInput label="Base Price" value={form.baseprice === '' ? undefined : Number(form.baseprice)} onChange={(v) => set('baseprice')(v ?? '')} isRequired />
|
||||
<NumberInput label="Base Weight (kg)" value={form.baseweight === '' ? undefined : Number(form.baseweight)} onChange={(v) => set('baseweight')(v ?? '')} isOptional />
|
||||
<NumberInput label="Price / KG" value={form.priceperkg === '' ? undefined : Number(form.priceperkg)} onChange={(v) => set('priceperkg')(v ?? '')} isOptional />
|
||||
<NumberInput label="Base Distance (km)" value={form.basedistance === '' ? undefined : Number(form.basedistance)} onChange={(v) => set('basedistance')(v ?? '')} isOptional />
|
||||
<NumberInput label="Price / KM" value={form.priceperkm === '' ? undefined : Number(form.priceperkm)} onChange={(v) => set('priceperkm')(v ?? '')} isRequired />
|
||||
<NumberInput
|
||||
label="Base Price"
|
||||
value={form.baseprice === '' ? undefined : Number(form.baseprice)}
|
||||
onChange={(v) => set('baseprice')(v ?? '')}
|
||||
isRequired
|
||||
/>
|
||||
<NumberInput
|
||||
label="Base Weight (kg)"
|
||||
value={form.baseweight === '' ? undefined : Number(form.baseweight)}
|
||||
onChange={(v) => set('baseweight')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
<NumberInput
|
||||
label="Price / KG"
|
||||
value={form.priceperkg === '' ? undefined : Number(form.priceperkg)}
|
||||
onChange={(v) => set('priceperkg')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
<NumberInput
|
||||
label="Base Distance (km)"
|
||||
value={form.basedistance === '' ? undefined : Number(form.basedistance)}
|
||||
onChange={(v) => set('basedistance')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
<NumberInput
|
||||
label="Price / KM"
|
||||
value={form.priceperkm === '' ? undefined : Number(form.priceperkm)}
|
||||
onChange={(v) => set('priceperkm')(v ?? '')}
|
||||
isRequired
|
||||
/>
|
||||
<NumberInput
|
||||
label="Handling Charges"
|
||||
value={form.handlingcharges === '' ? undefined : Number(form.handlingcharges)}
|
||||
onChange={(v) => set('handlingcharges')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
<NumberInput label="Priority" value={form.priority === '' ? undefined : Number(form.priority)} onChange={(v) => set('priority')(v ?? '')} isOptional />
|
||||
<NumberInput
|
||||
label="Priority"
|
||||
value={form.priority === '' ? undefined : Number(form.priority)}
|
||||
onChange={(v) => set('priority')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
</Grid>
|
||||
</LayoutContent>
|
||||
}
|
||||
footer={
|
||||
<LayoutFooter hasDivider>
|
||||
<HStack gap={2} justify="end">
|
||||
<Button label="Cancel" variant="secondary" onClick={() => setDialogOpen(false)} isDisabled={savePricingMutation.isPending} />
|
||||
<Button
|
||||
label="Cancel"
|
||||
variant="secondary"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
isDisabled={savePricingMutation.isPending}
|
||||
/>
|
||||
<Button
|
||||
label={form.pricingid ? 'Save Changes' : 'Create'}
|
||||
variant="primary"
|
||||
@@ -598,17 +663,46 @@ const ClientsPricing = () => {
|
||||
content={
|
||||
<LayoutContent padding={4}>
|
||||
<VStack gap={3} padding={0}>
|
||||
<Selector label="Tenant" options={tenantSelectOptions} value={quoteForm.tenant} onChange={setQuote('tenant')} placeholder="Choose tenant" hasSearch isRequired />
|
||||
<Selector label="Zone" options={locationSelectOptions} value={quoteForm.location} onChange={setQuote('location')} placeholder="Choose zone" hasSearch isOptional />
|
||||
<Grid columns={{ minWidth: 160 }} gap={3}>
|
||||
<Selector label="Vehicle Type" options={VEHICLE_TYPE_OPTIONS} value={quoteForm.vehicletype} onChange={setQuote('vehicletype')} />
|
||||
<NumberInput label="Weight (kg)" value={quoteForm.weight === '' ? undefined : Number(quoteForm.weight)} onChange={(v) => setQuote('weight')(v ?? '')} isOptional />
|
||||
<NumberInput label="Distance (km)" value={quoteForm.distance === '' ? undefined : Number(quoteForm.distance)} onChange={(v) => setQuote('distance')(v ?? '')} isRequired />
|
||||
<Selector
|
||||
label="Tenant"
|
||||
options={tenantSelectOptions}
|
||||
value={quoteForm.tenant}
|
||||
onChange={setQuote('tenant')}
|
||||
placeholder="Choose tenant"
|
||||
hasSearch
|
||||
isRequired
|
||||
/>
|
||||
<Selector
|
||||
label="Zone"
|
||||
options={locationSelectOptions}
|
||||
value={quoteForm.location}
|
||||
onChange={setQuote('location')}
|
||||
placeholder="Choose zone"
|
||||
hasSearch
|
||||
isOptional
|
||||
/>
|
||||
<Grid columns={{ minWidth: 160, repeat: 'fit' }} gap={3}>
|
||||
<Selector
|
||||
label="Vehicle Type"
|
||||
options={VEHICLE_TYPE_OPTIONS}
|
||||
value={quoteForm.vehicletype}
|
||||
onChange={setQuote('vehicletype')}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Weight (kg)"
|
||||
value={quoteForm.weight === '' ? undefined : Number(quoteForm.weight)}
|
||||
onChange={(v) => setQuote('weight')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
<NumberInput
|
||||
label="Distance (km)"
|
||||
value={quoteForm.distance === '' ? undefined : Number(quoteForm.distance)}
|
||||
onChange={(v) => setQuote('distance')(v ?? '')}
|
||||
isRequired
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{quoteMutation.isPending && (
|
||||
<Text color="secondary">Calculating…</Text>
|
||||
)}
|
||||
{quoteMutation.isPending && <Text color="secondary">Calculating…</Text>}
|
||||
|
||||
{quoteMutation.data && (
|
||||
<Card padding={3} elevation="none" style={{ backgroundColor: DT.surfaceAlt }}>
|
||||
|
||||
Reference in New Issue
Block a user