updates on the zone selector for the specific login

This commit is contained in:
2026-08-27 12:03:12 +05:30
parent 4be2e73712
commit 656e180675
7 changed files with 425 additions and 61 deletions

View File

@@ -3,7 +3,7 @@ import { Ban, Bell, Bike, FileSpreadsheet, Package, PackageSearch, Truck } from
import {
Alert, Button, DataTable, Drawer, EmptyState, Field, IconButton, Input,
PageHeader, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Modal,
Stack, StatusBadge, Surface, Tabs, Textarea,
Stack, StatusBadge, Surface, Tabs, Textarea, ZoneSelector,
} from '@/components/ds';
import { ListToolbar } from '@/components/doormile/ListToolbar';
import {
@@ -11,6 +11,7 @@ import {
useDeliveryCounts, useNotifyMiler, useRiderOptions, useTenants,
useUpdateDeliveryStatus,
} from '@/lib/doormileHooks';
import { useZone } from '@/lib/ZoneContext';
import { BATCHES, batchRangeLabel, getRowBatchId } from '@/lib/batchBucket';
import { formatDoormileTimestamp, parseDoormileTimestamp } from '@/lib/doormileTimestamp';
import { currency, exportRows, km as formatKm, matchesQuery, orDash, useDebouncedValue } from '@/lib/doormileFormat';
@@ -93,6 +94,7 @@ export default function Deliveries() {
const updateStatus = useUpdateDeliveryStatus();
const cancelDelivery = useCancelDelivery();
const notifyMiler = useNotifyMiler();
const { selectedZone, isAllZones, matchesZone } = useZone();
const allRows = useMemo(() => {
return [...(data?.rows || [])].sort((a, b) => {
@@ -102,20 +104,17 @@ export default function Deliveries() {
});
}, [data?.rows]);
/* Batch, client and rider first, then status, then search — the counts under
/* Zone, Batch, client and rider first, then status, then search — the counts under
the tabs have to describe the slice the operator has chosen, not the whole
day, or a tab reads 6 and shows 2.
There is deliberately no site filter: a delivery row carries its client but
no tenant-location id, so the only honest filter at that level would be one
that silently matched nothing. */
day, or a tab reads 6 and shows 2. */
const batchRows = useMemo(
() =>
allRows
.filter(matchesZone)
.filter((row) => batch === 'all' || getRowBatchId(row) === batch)
.filter((row) => tenantId === ALL || String(row.tenantid) === tenantId)
.filter((row) => riderId === ALL || String(row.userid) === riderId),
[allRows, batch, tenantId, riderId]
[allRows, matchesZone, batch, tenantId, riderId]
);
const unmapped = useMemo(
@@ -399,6 +398,7 @@ export default function Deliveries() {
</Button>
}
>
<ZoneSelector className="w-full sm:w-auto" />
<Select value={batch} onValueChange={setBatch}>
<SelectTrigger className="w-full sm:w-52">
<SelectValue />

View File

@@ -2,9 +2,10 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { motion } from 'framer-motion';
import dayjs from 'dayjs';
import { Ban, FileSpreadsheet, Package, Plus, Truck, Upload } from 'lucide-react';
import { Button, ConfirmModal, DataTable, EmptyState, PageHeader, SearchInput, Stack, StatusBadge, Tabs } from '@/components/ds';
import { Ban, FileSpreadsheet, MapPin, Package, Plus, Truck, Upload } from 'lucide-react';
import { Button, ConfirmModal, DataTable, EmptyState, PageHeader, SearchInput, Stack, StatusBadge, Tabs, ZoneSelector } from '@/components/ds';
import { useBulkCancelBookings, useCustomers } from '@/lib/doormileHooks';
import { useZone } from '@/lib/ZoneContext';
import { createAutomationDeliveries } from '@/api/doormile';
import { drainBookings } from '@/lib/assistant/scan';
import { OpenToast, messageOf } from '@/api/doormile/notify';
@@ -68,20 +69,13 @@ export default function Orders() {
const [selectedIds, setSelectedIds] = useState([]);
const [cancelOpen, setCancelOpen] = useState(false);
const { selectedZone, selectedZoneId, isAllZones, matchesZone } = useZone();
const { data: customers = [] } = useCustomers();
const bulkCancel = useBulkCancelBookings();
/* Polled: an operator watching the pending queue needs new bookings to
appear without reaching for refresh. */
/* `getBookings(1, 1000)` used to back this screen. That is page ONE at the
API's hard cap, and it discards the envelope's `total`, so past 1000
lifetime bookings every tab here was computed from a partial list with no
way to know it. `/admin/bookings` documents no row order either (scan.js
detects it per call rather than assuming), so a freshly created order is
not guaranteed to be on page 1 — it could simply be absent from every tab.
Drain the pages instead, and surface `truncated` below rather than quietly
showing a short list. `cached: false` because TanStack owns freshness for
this screen; scan.js's own 20s page cache would cap the refetch interval. */
const {
data: scan = EMPTY_SCAN,
isLoading,
@@ -104,6 +98,10 @@ export default function Orders() {
});
}, [bookings]);
const zoneBookings = useMemo(() => {
return sortedBookings.filter(matchesZone);
}, [sortedBookings, matchesZone]);
const customerMap = useMemo(() => {
const map = new Map();
(customers || []).forEach((customer) => {
@@ -115,7 +113,7 @@ export default function Orders() {
const counts = useMemo(() => {
const byStatus = {};
(sortedBookings || []).forEach((booking) => {
(zoneBookings || []).forEach((booking) => {
const status = String(booking.status || '').toLowerCase();
byStatus[status] = (byStatus[status] || 0) + 1;
});
@@ -125,25 +123,25 @@ export default function Orders() {
statusesInGroup(group).reduce((sum, status) => sum + (byStatus[status] || 0), 0),
])
);
}, [sortedBookings]);
}, [zoneBookings]);
const tabStatuses = useMemo(() => statusesInGroup(tab), [tab]);
const rows = useMemo(
() =>
(sortedBookings || [])
(zoneBookings || [])
.filter((booking) => tabStatuses.includes(String(booking.status || '').toLowerCase()))
.filter((booking) =>
matchesQuery(booking, ['bookingno', 'pickupaddress', 'deliveryaddress', 'notes'], debouncedSearch)
),
[sortedBookings, tabStatuses, debouncedSearch]
[zoneBookings, tabStatuses, debouncedSearch]
);
/* A row that scrolled out of the current tab must not stay silently selected
— the assign buttons act on the selection, not on what is on screen. */
useEffect(() => {
setSelectedIds([]);
}, [tab]);
}, [tab, selectedZoneId]);
const selectedOrders = useMemo(
() => rows.filter((row) => selectedIds.includes(row.bookingid)),
@@ -168,6 +166,8 @@ export default function Orders() {
selectedMode: { value: 1 },
deliveryData: variables?.deliveries || [],
startdate: dayjs().format('YYYY-MM-DD'),
hubid: selectedZone.hubid !== 'all' ? selectedZone.hubid : undefined,
zoneName: selectedZone.hubname,
},
});
},
@@ -176,6 +176,11 @@ export default function Orders() {
const assignOrders = () => {
if (!selectedOrders.length) return;
if (isAllZones) {
OpenToast('Please select a specific Zone / Hub before assigning orders.', 'warning', 4000);
return;
}
const deliveries = selectedOrders.map((booking) => {
const charge = chargeOf(booking);
return {
@@ -195,6 +200,7 @@ export default function Orders() {
deliverycharges: charge,
pickuplocation: booking.pickupaddress,
deliverylocation: booking.deliveryaddress,
hubid: selectedZone.hubid !== 'all' ? selectedZone.hubid : booking.hubid,
};
});
@@ -203,6 +209,7 @@ export default function Orders() {
selectedMode: { value: 1 },
hypertuning_params: 'balanced',
absent_riders: [],
hubid: selectedZone.hubid !== 'all' ? selectedZone.hubid : undefined,
});
};
@@ -338,11 +345,12 @@ export default function Orders() {
</div>
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-end gap-2 sm:flex-none">
<ZoneSelector />
<SearchInput
value={search}
onChange={setSearch}
placeholder="Search by order number, address or note"
className="w-full sm:w-72"
className="w-full sm:w-64"
/>
{selectedIds.length > 0 && tab === 'pending' && (
<Button variant="outline" onClick={() => setCancelOpen(true)}>
@@ -359,21 +367,43 @@ export default function Orders() {
</div>
</div>
<p className="text-caption text-ink-3">
{rows.length === (counts[tab] ?? 0)
? `${counts[tab] ?? 0} orders`
: `${rows.length} of ${counts[tab] ?? 0} orders`}
{/* A capped scan means an order really can be missing from this tab.
Say so — a silently short list is what sent operators hunting for
an order they had just created. */}
{scan.truncated && (
<span className="text-warning">
{` — showing the first ${scan.scanned.toLocaleString('en-IN')} of ${scan.total.toLocaleString(
'en-IN'
)} bookings, so this count is a floor. Search by order number to find a specific order.`}
</span>
)}
</p>
{tab === 'pending' && isAllZones && (
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50/80 p-3 text-body-sm text-amber-900 shadow-2xs">
<div className="flex items-center gap-2.5 min-w-0">
<div className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-amber-100 text-amber-800">
<MapPin className="h-4 w-4" />
</div>
<div className="min-w-0">
<p className="text-xs font-bold text-amber-950">Select an Operational Zone for Dispatch Assignment</p>
<p className="text-[11px] text-amber-800 truncate sm:whitespace-normal">
To select and assign pending orders to riders, please choose a specific Hub/Zone (e.g. Coimbatore Neptune Hub) first.
</p>
</div>
</div>
<ZoneSelector width="w-64" className="border-amber-300 bg-white" />
</div>
)}
<div className="flex items-center justify-between gap-2">
<p className="text-caption text-ink-3">
{rows.length === (counts[tab] ?? 0)
? `${counts[tab] ?? 0} orders`
: `${rows.length} of ${counts[tab] ?? 0} orders`}
{!isAllZones && (
<span className="font-medium text-ink-2"> · Zone: {selectedZone.hubname}</span>
)}
{/* A capped scan means an order really can be missing from this tab.
Say so — a silently short list is what sent operators hunting for
an order they had just created. */}
{scan.truncated && (
<span className="text-warning">
{` — showing the first ${scan.scanned.toLocaleString('en-IN')} of ${scan.total.toLocaleString(
'en-IN'
)} bookings, so this count is a floor. Search by order number to find a specific order.`}
</span>
)}
</p>
</div>
<DataTable
columns={columns}
@@ -381,8 +411,8 @@ export default function Orders() {
getRowId={(row) => row.bookingid}
loading={isLoading}
refreshing={isFetching && !isLoading}
selectedIds={tab === 'pending' ? selectedIds : undefined}
onSelectionChange={tab === 'pending' ? setSelectedIds : undefined}
selectedIds={tab === 'pending' && !isAllZones ? selectedIds : undefined}
onSelectionChange={tab === 'pending' && !isAllZones ? setSelectedIds : undefined}
pageSize={25}
/* No defaultSort: `sortedBookings` above already orders these newest
first by `bookingid`, and DataTable leaves rows untouched when it

View File

@@ -7,12 +7,13 @@ import {
import {
Avatar, Button, ConfirmModal, DataTable, Drawer, EmptyState, Field, Grid, IconButton,
Input, KpiCard, Modal, PageHeader, SearchInput, Select, SelectContent, SelectItem, SelectTrigger,
SelectValue, Stack, StatusBadge, Surface, Tabs, Textarea,
SelectValue, Stack, StatusBadge, Surface, Tabs, Textarea, ZoneSelector,
} from '@/components/ds';
import {
useAssignMilerVehicle, useBlockMiler, useMilerLogs, useMilers, useNotifyMiler,
useRiderSummaryCounts, useVehicles,
} from '@/lib/doormileHooks';
import { useZone } from '@/lib/ZoneContext';
import { formatDoormileTimestamp } from '@/lib/doormileTimestamp';
import { currency, matchesQuery, orDash, useDebouncedValue } from '@/lib/doormileFormat';
@@ -64,17 +65,23 @@ export default function Riders() {
const assignVehicle = useAssignMilerVehicle();
const notifyMiler = useNotifyMiler();
const { selectedZone, isAllZones, matchesZone } = useZone();
const zoneRiders = useMemo(() => {
return (riders || []).filter(matchesZone);
}, [riders, matchesZone]);
const counts = useMemo(() => {
const list = riders || [];
const list = zoneRiders;
return {
all: list.length,
active: list.filter(isActive).length,
blocked: list.filter((rider) => String(rider.availabilitystatus || '').toLowerCase() === 'blocked').length,
};
}, [riders]);
}, [zoneRiders]);
const rows = useMemo(() => {
let list = riders || [];
let list = zoneRiders;
if (tab === 'active') list = list.filter(isActive);
else if (tab === 'blocked') {
list = list.filter((rider) => String(rider.availabilitystatus || '').toLowerCase() === 'blocked');
@@ -82,7 +89,7 @@ export default function Riders() {
return list.filter((rider) =>
matchesQuery(rider, [riderName, 'phone', 'userid', 'city', 'suburb', 'defaultvehicletype'], debouncedSearch)
);
}, [riders, tab, debouncedSearch]);
}, [zoneRiders, tab, debouncedSearch]);
const columns = useMemo(
() => [
@@ -218,7 +225,7 @@ export default function Riders() {
/>
<KpiCard
icon={CheckCircle2}
value={summary?.available ?? 0}
value={summary?.available ?? counts.active}
label="Available now"
tone="success"
index={1}
@@ -226,7 +233,14 @@ export default function Riders() {
/>
<KpiCard
icon={Bike}
value={summary?.onDelivery ?? 0}
value={
summary?.onDelivery ??
zoneRiders.filter(
(r) =>
String(r.availabilitystatus || '').toLowerCase() === 'on_delivery' ||
String(r.availabilitystatus || '').toLowerCase() === 'ondelivery'
).length
}
label="On delivery"
tone="accent"
index={2}
@@ -234,7 +248,7 @@ export default function Riders() {
/>
<KpiCard
icon={CircleSlash}
value={summary?.inactive ?? 0}
value={summary?.inactive ?? counts.blocked}
label="Offline or blocked"
tone="neutral"
index={3}
@@ -256,12 +270,13 @@ export default function Riders() {
/>
</div>
<div className="flex min-w-0 flex-1 items-center justify-end gap-2 sm:flex-none">
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-end gap-2 sm:flex-none">
<ZoneSelector />
<SearchInput
value={search}
onChange={setSearch}
placeholder="Search by name, phone, id or city"
className="w-full sm:w-72"
className="w-full sm:w-64"
/>
</div>
</div>