diff --git a/src/pages/doormile/orders/Orders.jsx b/src/pages/doormile/orders/Orders.jsx
index dcb975d..bcbff65 100644
--- a/src/pages/doormile/orders/Orders.jsx
+++ b/src/pages/doormile/orders/Orders.jsx
@@ -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() {
+
{selectedIds.length > 0 && tab === 'pending' && (
-
- {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 && (
-
- {` — 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.`}
-
- )}
-
+ {tab === 'pending' && isAllZones && (
+
+
+
+
+
+
+
Select an Operational Zone for Dispatch Assignment
+
+ To select and assign pending orders to riders, please choose a specific Hub/Zone (e.g. Coimbatore Neptune Hub) first.
+
+
+
+
+
+ )}
+
+
+
+ {rows.length === (counts[tab] ?? 0)
+ ? `${counts[tab] ?? 0} orders`
+ : `${rows.length} of ${counts[tab] ?? 0} orders`}
+ {!isAllZones && (
+ · Zone: {selectedZone.hubname}
+ )}
+ {/* 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 && (
+
+ {` — 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.`}
+
+ )}
+
+
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
diff --git a/src/pages/doormile/riders/Riders.jsx b/src/pages/doormile/riders/Riders.jsx
index 4982627..c2ed1bc 100644
--- a/src/pages/doormile/riders/Riders.jsx
+++ b/src/pages/doormile/riders/Riders.jsx
@@ -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() {
/>
+ 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() {
/>
-