From c6607ae75d3b42aff8313ed63f13c4673e3e3822 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Thu, 27 Aug 2026 12:15:31 +0530 Subject: [PATCH] updates on the create order design and select location updates --- src/api/doormile/queries.js | 118 ++++++-- src/components/doormile/HubLocationSelect.jsx | 234 +++++++++++++++ src/pages/doormile/deliveries/Deliveries.jsx | 60 +++- src/pages/doormile/orders/CreateOrder.jsx | 273 +++++++++++------- src/pages/doormile/orders/MultipleOrders.jsx | 33 +-- 5 files changed, 543 insertions(+), 175 deletions(-) create mode 100644 src/components/doormile/HubLocationSelect.jsx 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() { setServiceOption(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" > {SERVICE_OPTIONS.map((opt) => (