diff --git a/src/api/doormile/queries.js b/src/api/doormile/queries.js
index 8c12922..7d26ad6 100644
--- a/src/api/doormile/queries.js
+++ b/src/api/doormile/queries.js
@@ -353,15 +353,18 @@ export const fetchRidersList = async () => {
try {
const milers = await getMilers();
return (milers || []).map((val) => {
- const name = val.displayname || val.authname || '';
+ const name =
+ val.displayname ||
+ [val.firstname, val.lastname].filter(Boolean).join(' ').trim() ||
+ val.authname ||
+ val.name ||
+ val.username ||
+ (val.userid ? `Rider #${val.userid}` : '');
return {
...val,
- // Only append " | phone" when a phone actually exists — an
- // unconditional template literal left a dangling " | " on every
- // rider missing contactno, rendering literally in every dropdown
- // that falls back to this default label (e.g. OrdersPreview.js,
- // Preview.js's Change Rider dialog).
- label: val.contactno ? `${name} | ${val.contactno}` : name
+ name,
+ displayname: name,
+ label: val.contactno || val.phone ? `${name} | ${val.contactno || val.phone}` : name
};
});
} catch (err) {
@@ -685,11 +688,55 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
const consignmentMap = new Map();
(consignments || []).forEach((c) => {
const id = c?.consignmentid ?? c?.id;
- if (id != null) consignmentMap.set(String(id), c);
+ if (id != null) {
+ consignmentMap.set(String(id), c);
+ consignmentMap.set(Number(id), c);
+ }
+ });
+ const customerMap = new Map();
+ (customers || []).forEach((c) => {
+ const id = c.appcustomerid ?? c.customerid ?? c.id;
+ if (id != null) {
+ customerMap.set(String(id), c);
+ customerMap.set(Number(id), c);
+ }
+ });
+
+ const milerMap = new Map();
+ (milers || []).forEach((m) => {
+ const riderName =
+ m.displayname ||
+ [m.firstname, m.lastname].filter(Boolean).join(' ').trim() ||
+ m.authname ||
+ m.name ||
+ m.username ||
+ '';
+ const riderObj = { ...m, resolvedName: riderName };
+ if (m.userid != null) {
+ milerMap.set(String(m.userid), riderObj);
+ milerMap.set(Number(m.userid), riderObj);
+ }
+ if (m.milerid != null) {
+ milerMap.set(String(m.milerid), riderObj);
+ milerMap.set(Number(m.milerid), riderObj);
+ }
+ if (m.milerprofileid != null) {
+ milerMap.set(String(m.milerprofileid), riderObj);
+ milerMap.set(Number(m.milerprofileid), riderObj);
+ }
+ if (m.id != null) {
+ milerMap.set(String(m.id), riderObj);
+ milerMap.set(Number(m.id), riderObj);
+ }
+ });
+
+ const tenantMap = new Map();
+ (tenants || []).forEach((t) => {
+ if (t.tenantid != null) {
+ tenantMap.set(String(t.tenantid), t);
+ tenantMap.set(Number(t.tenantid), t);
+ }
});
- const customerMap = new Map((customers || []).map((c) => [c.appcustomerid ?? c.customerid ?? c.id, c]));
- const milerMap = new Map((milers || []).map((m) => [m.userid ?? m.milerid, m]));
- const tenantMap = new Map((tenants || []).map((t) => [t.tenantid, t]));
const isDispatched = (b) => {
if (!b) return false;
@@ -706,17 +753,42 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
const dispatched = (bookings || []).filter(isDispatched);
const rows = dispatched.map((b) => {
- const riderId = b.assignedmileruserid ?? b.assigned_miler_user_id ?? b.mileruserid ?? b.milerid ?? b.assignedmilerid;
const consignmentId = b.consignmentid ?? b.consignment_id;
+ const consignment = consignmentId != null ? consignmentMap.get(String(consignmentId)) : null;
+
+ const riderId =
+ b.assignedmileruserid ??
+ b.assigned_miler_user_id ??
+ b.mileruserid ??
+ b.milerid ??
+ b.assignedmilerid ??
+ consignment?.assignedmileruserid ??
+ consignment?.mileruserid ??
+ consignment?.milerid;
+
const customerId = b.appcustomerid ?? b.customerid ?? b.id;
- const customer = customerMap.get(customerId);
- const miler = milerMap.get(riderId) || (riderId != null ? milerMap.get(Number(riderId)) : null);
- const tenant = tenantMap.get(b.tenantid);
+ const customer = customerId != null ? customerMap.get(String(customerId)) : null;
+ const miler = riderId != null ? (milerMap.get(String(riderId)) || milerMap.get(Number(riderId))) : null;
+ const tenant = b.tenantid != null ? (tenantMap.get(String(b.tenantid)) || tenantMap.get(Number(b.tenantid))) : null;
const charge = b.serviceoptions?.[0]?.estimatedprice;
const cStatus = consignmentStatusFor(b, consignmentMap);
const reached = b.reachedat ?? b.reached_at ?? b.reachedAt ?? b.reachedtime ?? b.reached_time;
const effectiveDeliveryStatus = deriveDeliveryStatus(b, consignmentMap);
+ const actualRiderName =
+ miler?.resolvedName ||
+ miler?.displayname ||
+ [miler?.firstname, miler?.lastname].filter(Boolean).join(' ').trim() ||
+ miler?.authname ||
+ miler?.name ||
+ b.milername ||
+ b.ridername ||
+ b.assignedmilername ||
+ consignment?.milername ||
+ consignment?.ridername ||
+ consignment?.assignedmilername ||
+ (riderId ? `Rider #${riderId}` : '');
+
return {
orderheaderid: b.bookingid ?? b.id,
deliveryid: b.bookingid ?? b.id,
@@ -731,13 +803,6 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
tenantadress: tenant?.primaryemail || '',
locationname: tenant?.tenantname || '',
locationsuburb: '',
- // Was hardcoded '' — Dispatch.js's kitchen markers read this as the
- // pickup business name (`o.pickupcustomer || o.kitchen_key || 'Unknown'`,
- // Dispatch.js:1660), and no booking on this API carries a `kitchen_key`
- // field at all, so every kitchen pin fell through to the literal string
- // 'Unknown' — rendered as a "U" marker whose hover/popup then showed
- // "Unknown". The tenant IS the kitchen for a B2B booking (same value
- // already used for tenantname/locationname above), so reuse it here.
pickupcustomer: tenant?.tenantname || '',
pickupcontactno: '',
Pickupaddress: b.pickupaddress || '',
@@ -749,15 +814,10 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
deliveryaddress: b.deliveryaddress || '',
deliverylocation: b.deliveryaddress || '',
deliverysuburb: '',
- ridername: miler?.displayname || miler?.name || (riderId ? `Rider #${riderId}` : ''),
+ ridername: actualRiderName,
userid: riderId,
- // GET /admin/milers/:id/notify (and block/assign-vehicle) key off
- // milerprofileid, not the userid stored on the booking — confirmed
- // live (a booking's assignedmileruserid matches a miler's `userid`
- // field, which 404s against /admin/milers/:id; milerprofileid is the
- // real primary key of that resource).
milerprofileid: miler?.milerprofileid ?? miler?.id,
- ridercontact: miler?.phone || miler?.contactno || '',
+ ridercontact: miler?.phone || miler?.contactno || b.riderphone || consignment?.riderphone || '',
expecteddeliverytime: b.serviceoptions?.[0]?.estimateddeliveryat,
// No route-plan data source (step order/transit time/cumulative km were
// computed by the old jupiter backend from the dispatch optimiser's
diff --git a/src/components/doormile/HubLocationSelect.jsx b/src/components/doormile/HubLocationSelect.jsx
new file mode 100644
index 0000000..aae71d5
--- /dev/null
+++ b/src/components/doormile/HubLocationSelect.jsx
@@ -0,0 +1,234 @@
+import React, { useMemo, useState } from 'react';
+import { Building2, Check, ChevronDown, MapPin, Search, Sparkles } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+
+const locationDisplayName = (loc) =>
+ (loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location';
+
+const isValidLocation = (loc) => {
+ const name = String(loc?.locationname || '').toLowerCase();
+ return !name.includes('retired') && !name.includes('do not use') && !name.includes('test row');
+};
+
+export function HubLocationSelect({
+ locations = [],
+ selectedLocationId = '',
+ onSelectLocation,
+ disabled = false,
+ placeholder = 'Select Hub Location',
+ variant = 'header', // 'header' | 'field'
+ className,
+}) {
+ const [search, setSearch] = useState('');
+
+ const validLocations = useMemo(() => {
+ return (locations || [])
+ .filter(isValidLocation)
+ .sort((a, b) => {
+ if (Boolean(a.isprimary) !== Boolean(b.isprimary)) return a.isprimary ? -1 : 1;
+ return locationDisplayName(a).localeCompare(locationDisplayName(b));
+ });
+ }, [locations]);
+
+ const filtered = useMemo(() => {
+ if (!search.trim()) return validLocations;
+ const q = search.toLowerCase();
+ return validLocations.filter(
+ (loc) =>
+ locationDisplayName(loc).toLowerCase().includes(q) ||
+ loc.address?.toLowerCase().includes(q) ||
+ loc.city?.toLowerCase().includes(q) ||
+ loc.pincode?.includes(q)
+ );
+ }, [validLocations, search]);
+
+ const selectedLoc = useMemo(() => {
+ return validLocations.find(
+ (l) => String(l.tenantlocationid || l.locationid) === String(selectedLocationId)
+ );
+ }, [validLocations, selectedLocationId]);
+
+ const displayLabel = selectedLoc ? locationDisplayName(selectedLoc) : placeholder;
+
+ if (variant === 'field') {
+ return (
+ !open && setSearch('')}>
+
+
+
+
+
+
+ Select Pickup Hub / Kitchen Location
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search location or address…"
+ onClick={(e) => e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ className="h-8 w-full rounded-lg border border-slate-200 bg-slate-50 pl-8 pr-2 text-xs text-slate-900 placeholder:text-slate-400 outline-none focus:border-slate-400 focus:bg-white"
+ />
+
+
+
+ {filtered.map((loc) => {
+ const id = String(loc.tenantlocationid || loc.locationid);
+ const isSelected = String(selectedLocationId) === id;
+
+ return (
+
onSelectLocation(id)}
+ className={cn(
+ 'flex items-start justify-between gap-2 px-2.5 py-2 text-xs cursor-pointer rounded-lg mb-0.5',
+ isSelected ? 'bg-blue-50 text-blue-900 font-semibold' : 'hover:bg-slate-50'
+ )}
+ >
+
+
+
+ {locationDisplayName(loc)}
+
+ {loc.isprimary && (
+
+ PRIMARY
+
+ )}
+
+ {(loc.address || loc.city) && (
+
+ {[loc.address, loc.city, loc.pincode].filter(Boolean).join(' · ')}
+
+ )}
+
+
+ {isSelected && }
+
+ );
+ })}
+
+ {filtered.length === 0 && (
+
No matching locations found
+ )}
+
+
+
+ );
+ }
+
+ // Header quick picker variant
+ return (
+ !open && setSearch('')}>
+
+
+
+
+
+
+ Switch Pickup Hub / Kitchen
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search kitchen or hub…"
+ onClick={(e) => e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ className="h-8 w-full rounded-lg border border-slate-200 bg-slate-50 pl-8 pr-2 text-xs text-slate-900 placeholder:text-slate-400 outline-none focus:border-slate-400 focus:bg-white"
+ />
+
+
+
+ {filtered.map((loc) => {
+ const id = String(loc.tenantlocationid || loc.locationid);
+ const isSelected = String(selectedLocationId) === id;
+
+ return (
+
onSelectLocation(id)}
+ className={cn(
+ 'flex items-start justify-between gap-2 px-2.5 py-2 text-xs cursor-pointer rounded-lg mb-0.5',
+ isSelected ? 'bg-blue-50 text-blue-900 font-semibold' : 'hover:bg-slate-50'
+ )}
+ >
+
+
+
+ {locationDisplayName(loc)}
+
+ {loc.isprimary && (
+
+ PRIMARY
+
+ )}
+
+ {(loc.address || loc.city) && (
+
+ {[loc.address, loc.city].filter(Boolean).join(' · ')}
+
+ )}
+
+
+ {isSelected && }
+
+ );
+ })}
+
+ {filtered.length === 0 && (
+
No matching locations found
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/doormile/deliveries/Deliveries.jsx b/src/pages/doormile/deliveries/Deliveries.jsx
index ed7ac77..8804aed 100644
--- a/src/pages/doormile/deliveries/Deliveries.jsx
+++ b/src/pages/doormile/deliveries/Deliveries.jsx
@@ -96,6 +96,25 @@ export default function Deliveries() {
const notifyMiler = useNotifyMiler();
const { selectedZone, isAllZones, matchesZone } = useZone();
+ const riderLookup = useMemo(() => {
+ const map = new Map();
+ (riders || []).forEach((r) => {
+ const name =
+ r.displayname ||
+ [r.firstname, r.lastname].filter(Boolean).join(' ').trim() ||
+ r.name ||
+ r.authname ||
+ (r.label ? r.label.split('|')[0].trim() : '');
+ const phone = r.contactno || r.phone || '';
+ if (r.userid != null) map.set(String(r.userid), { name, phone });
+ if (r.milerid != null) map.set(String(r.milerid), { name, phone });
+ if (r.milerprofileid != null) map.set(String(r.milerprofileid), { name, phone });
+ if (r.id != null) map.set(String(r.id), { name, phone });
+ if (r.value != null) map.set(String(r.value), { name, phone });
+ });
+ return map;
+ }, [riders]);
+
const allRows = useMemo(() => {
return [...(data?.rows || [])].sort((a, b) => {
const ta = parseDoormileTimestamp(a.orderdate || a.assigntime).valueOf() || 0;
@@ -138,15 +157,14 @@ export default function Deliveries() {
}, [batchRows, unmapped]);
const rows = useMemo(() => {
- const scoped =
- tab === 'all'
- ? batchRows
- : tab === 'other'
- ? unmapped
- : batchRows.filter((row) => {
- const status = String(row.orderstatus || '').toLowerCase();
- return tab === 'cancelled' ? status === 'cancelled' || status === 'canceled' : status === tab;
- });
+ let scoped = batchRows;
+ if (tab === 'other') scoped = unmapped;
+ else if (tab !== 'all') {
+ scoped = batchRows.filter((row) => {
+ const status = String(row.orderstatus || '').toLowerCase();
+ return tab === 'cancelled' ? status === 'cancelled' || status === 'canceled' : status === tab;
+ });
+ }
return scoped.filter((row) =>
matchesQuery(
@@ -213,12 +231,24 @@ export default function Deliveries() {
key: 'ridername',
header: 'Rider',
sortable: true,
- cell: (row) => (
-
-
{orDash(row.ridername)}
-
{orDash(row.ridercontact)}
-
- ),
+ cell: (row) => {
+ const riderKey = String(row.userid || row.milerprofileid || '');
+ const info = riderLookup.get(riderKey);
+ const name =
+ (info?.name && !info.name.startsWith('Rider #'))
+ ? info.name
+ : (row.ridername && !row.ridername.startsWith('Rider #'))
+ ? row.ridername
+ : (info?.name || row.ridername || (row.userid ? `Rider #${row.userid}` : '—'));
+ const contact = info?.phone || row.ridercontact;
+
+ return (
+
+
{name}
+
{orDash(contact)}
+
+ );
+ },
hideBelow: 'md',
},
{
diff --git a/src/pages/doormile/orders/CreateOrder.jsx b/src/pages/doormile/orders/CreateOrder.jsx
index dae439e..6b10d52 100644
--- a/src/pages/doormile/orders/CreateOrder.jsx
+++ b/src/pages/doormile/orders/CreateOrder.jsx
@@ -21,6 +21,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import dayjs from 'dayjs';
import AddressAutocomplete, { geocodeAddress } from '@/components/doormile/AddressAutocomplete';
+import { HubLocationSelect } from '@/components/doormile/HubLocationSelect';
import {
createExpressBooking,
getAdminCustomers,
@@ -31,9 +32,20 @@ import {
import { calculateDrivingDistance, calculateTotalCharge, getLastRouteDurationMin } from '@/lib/distance';
import { OpenToast } from '@/components/third-party/OpenToast';
-const SERVICE_OPTIONS = ['Normal', 'Express', 'Scheduled'];
+const SERVICE_OPTIONS = ['Normal', 'Fast', 'Superfast'];
const ITEM_CATEGORIES = ['General', 'Food & Beverage', 'Grocery', 'Pharmacy', 'Documents', 'Electronics'];
+const WEIGHT_OPTIONS = [
+ { value: '1 - 10 kg', label: '1 to 10 kg' },
+ { value: '10 - 20 kg', label: '10 to 20 kg' },
+ { value: '20 - 30 kg', label: '20 to 30 kg' },
+ { value: '30 - 50 kg', label: '30 to 50 kg' },
+ { value: '50+ kg', label: '50+ kg' },
+ { value: 'custom', label: 'Custom weight…' }
+];
+
+const COUNT_OPTIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+
const EMPTY_PARTY = {
firstname: '',
contactno: '',
@@ -46,7 +58,7 @@ const EMPTY_PARTY = {
longitude: ''
};
-const EMPTY_PACKAGE = { category: 'General', weight: '', description: '', count: 1 };
+const EMPTY_PACKAGE = { category: 'General', weight: '1 - 10 kg', count: 1, isCustomWeight: false, isCustomCount: false };
const buildDefaultPickupSlots = () => {
const arr = [];
@@ -331,21 +343,16 @@ export default function CreateOrder() {
const deliverytime = dayjs(`${pickupDate} ${selectedSlotTime}`, 'YYYY-MM-DD HH:mm').format('YYYY-MM-DD HH:mm:ss');
const finalprice = Number(totalCharge.toFixed(2)) || 0;
- const parcels = packages.some((pkg) => pkg.category || pkg.description)
- ? packages.flatMap((pkg) =>
- Array.from({ length: Math.max(1, Number(pkg.count) || 1) }, () => ({
- itemcategory: pkg.category || 'General',
- itemdescription: [pkg.description, pkg.weight ? `${pkg.weight}kg` : ''].filter(Boolean).join(' · ') || 'Order',
- declaredvalue: finalprice
- }))
- )
- : [
- {
- itemcategory: 'General',
- itemdescription: 'Order',
- declaredvalue: finalprice
- }
- ];
+ const parcels = packages.flatMap((pkg) => {
+ const weightStr = pkg.weight ? (String(pkg.weight).toLowerCase().includes('kg') ? pkg.weight : `${pkg.weight} kg`) : '1 - 10 kg';
+ const countNum = Math.max(1, Number(pkg.count) || 1);
+ const desc = `${pkg.category || 'Package'} (${weightStr})`;
+ return Array.from({ length: countNum }, () => ({
+ itemcategory: pkg.category || 'General',
+ itemdescription: desc,
+ declaredvalue: finalprice
+ }));
+ });
const payload = {
tenantid: Number(selectedTenantId),
@@ -362,7 +369,7 @@ export default function CreateOrder() {
deliverylatitude: Number(dropCust.latitude) || 0,
deliverylongitude: Number(dropCust.longitude) || 0,
deliverytime,
- service_option: serviceOption,
+ service_option: SERVICE_OPTIONS.includes(serviceOption) ? serviceOption : 'Normal',
finalprice,
notes: notes || '',
parcels
@@ -380,7 +387,8 @@ export default function CreateOrder() {
OpenToast(res?.message || 'Failed to create order', 'error', 3500);
}
} catch (err) {
- OpenToast(err?.message || 'Cannot create order. Please check inputs.', 'error', 3500);
+ const serverMsg = err?.response?.data?.message || err?.message || 'Cannot create order. Please check inputs.';
+ OpenToast(serverMsg, 'error', 4000);
} finally {
setIsSubmitting(false);
}
@@ -416,27 +424,13 @@ export default function CreateOrder() {
whole page sideways. */}
{/* Header Hub Quick Picker */}
-
-
-
Hub:
-
-
+
@@ -756,7 +732,7 @@ export default function CreateOrder() {
{/* Schedule & Packages Card */}
-
+
@@ -775,7 +751,7 @@ export default function CreateOrder() {
min={dayjs().format('YYYY-MM-DD')}
value={pickupDate}
onChange={(e) => setPickupDate(e.target.value)}
- className="w-full h-9 px-3 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
+ className="w-full h-10 px-3 bg-white border border-slate-200 rounded-xl text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black transition-colors cursor-pointer"
/>
@@ -784,7 +760,7 @@ export default function CreateOrder() {