From d875900cb05ad1b22b9b3590c03f80bf9f9f3da0 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Fri, 25 Sep 2026 13:06:12 +0530 Subject: [PATCH] updates on the saved contacts and fix on the logo in the home page section --- public/miletruth-logo-dark.png | Bin 0 -> 8479 bytes public/miletruth-logo-red.png | Bin 0 -> 8476 bytes public/miletruth-logo.png | Bin 0 -> 8514 bytes src/assets/brand.js | 3 + .../doormile/agents/AgentFlowVisualizer.jsx | 213 +++++++--- .../doormile/agents/AgentInspectorDrawer.jsx | 331 ++++++++++++--- src/pages/doormile/customers/Customers.jsx | 49 ++- src/pages/doormile/home/Home.jsx | 11 +- src/pages/doormile/orders/CreateOrder.jsx | 380 ++++++++++++++---- 9 files changed, 793 insertions(+), 194 deletions(-) create mode 100644 public/miletruth-logo-dark.png create mode 100644 public/miletruth-logo-red.png create mode 100644 public/miletruth-logo.png diff --git a/public/miletruth-logo-dark.png b/public/miletruth-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..de1b88392065703ea85599032b884b9cc3dca912 GIT binary patch literal 8479 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
-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 AnE(I) literal 0 HcmV?d00001 diff --git a/public/miletruth-logo-red.png b/public/miletruth-logo-red.png new file mode 100644 index 0000000000000000000000000000000000000000..aed54899868b4e0f0b54fdb93d2810a60e40d956 GIT binary patch literal 8476 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 KMNUMnLSTaR*D%8X literal 0 HcmV?d00001 diff --git a/public/miletruth-logo.png b/public/miletruth-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..49d0cc783709e072b0888922573c3f4d0a83380c GIT binary patch literal 8514 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 l*8PUD9OFd(^4fPqe_beUF|)raaQ{XC)WBNG { - runParticleAlongPath(path1Ref.current, 'path1', 850, () => { + runParticleAlongPath(path1Ref.current, 'path1', Math.round(850 / simSpeed), () => { // Arrives at Stage 2 Event Bus -> tactile arrival ripple triggerImpact(p2.x, p2.y, currentScenario.color); setCurrentEventFeed({ @@ -431,27 +434,27 @@ export default function AgentFlowVisualizer({ }); setStep(2); }); - }, 500); + }, Math.round(500 / simSpeed)); } else if (step === 2) { // Step 2: Information delivered, particle travels from Stage 2 to JARVIS timeoutId = setTimeout(() => { - runParticleAlongPath(path2Ref.current, 'path2', 750, () => { + runParticleAlongPath(path2Ref.current, 'path2', Math.round(750 / simSpeed), () => { // Arrives at JARVIS Orchestrator Apex -> royal indigo routing shockwave! triggerImpact(pJarvis.x, pJarvis.y, '#6366f1'); setStep(3); }); - }, 400); + }, Math.round(400 / simSpeed)); } else if (step === 3) { // Step 3: JARVIS routes to Target Agent, Agent pulses & processes, then routes to Result - runParticleAlongPath(path3Ref.current, 'path3', 600, () => { + runParticleAlongPath(path3Ref.current, 'path3', Math.round(600 / simSpeed), () => { // Arrives at Target Agent -> agent activation ripple! triggerImpact(pAgent.x, pAgent.y, currentScenario.color); setIsProcessing(true); - // Agent processes briefly for 800ms + // Agent processes briefly timeoutId = setTimeout(() => { setIsProcessing(false); // Particle travels from Agent to Result - runParticleAlongPath(path4Ref.current, 'path4', 850, () => { + runParticleAlongPath(path4Ref.current, 'path4', Math.round(850 / simSpeed), () => { // Arrives at Stage 4 Result -> successful resolution ripple! triggerImpact(p4.x, p4.y, currentScenario.color); if (onActivityLogged) { @@ -464,23 +467,66 @@ export default function AgentFlowVisualizer({ } setStep(4); }); - }, 800); + }, Math.round(800 / simSpeed)); }); } else if (step === 4) { - // Step 4: Result reached and activated. Hold for 2.2s in calm state then advance + // Step 4: Result reached and activated. Hold in calm state then advance setParticle((p) => ({ ...p, visible: false })); setActiveSegment(null); timeoutId = setTimeout(() => { setScenarioIndex((prevIdx) => (prevIdx + 1) % SCENARIOS.length); setStep(1); - }, 2200); + }, Math.round(2200 / simSpeed)); } return () => { if (animId) cancelAnimationFrame(animId); if (timeoutId) clearTimeout(timeoutId); }; - }, [step, isPlaying, scenarioIndex, reduce, currentScenario, onActivityLogged]); + }, [step, isPlaying, scenarioIndex, reduce, currentScenario, onActivityLogged, simSpeed]); + + // Scrubber Handlers + const handleStepBack = () => { + setIsPlaying(false); + setActiveSegment(null); + setParticle((p) => ({ ...p, visible: false })); + setIsProcessing(false); + if (step > 1) { + setStep((s) => s - 1); + } else { + setScenarioIndex((prev) => (prev - 1 + SCENARIOS.length) % SCENARIOS.length); + setStep(4); + } + }; + + const handleStepForward = () => { + setIsPlaying(false); + setActiveSegment(null); + setParticle((p) => ({ ...p, visible: false })); + setIsProcessing(false); + if (step < 4) { + setStep((s) => s + 1); + } else { + setScenarioIndex((prev) => (prev + 1) % SCENARIOS.length); + setStep(1); + } + }; + + const handleJumpToStep = (targetStep) => { + setIsPlaying(false); + setActiveSegment(null); + setParticle((p) => ({ ...p, visible: false })); + if (targetStep === 2) { + setCurrentEventFeed({ + text: currentScenario.step1Human, + tech: currentScenario.step1Tech, + time: new Date().toLocaleTimeString('en-GB', { hour12: false }), + color: currentScenario.color, + }); + } + setIsProcessing(targetStep === 3); + setStep(targetStep); + }; // Click handler on agent node -> Toggles dedicated Agent Workflow Panel BELOW the canvas! const handleAgentClick = (agentId) => { @@ -513,23 +559,13 @@ export default function AgentFlowVisualizer({ return (
- {/* ── Scenario Selection Toolbar (Clean & Minimal) ── */} + {/* ── Scenario Selection & Scrubber Controls Toolbar ── */}
-
- - - Workflow Simulation: - - - Watch work flow through Doormile's event bus & agent network - -
- - {/* Clean Scenario Pill Buttons */} - {/* flex-wrap, because four scenario pills plus Pause measure 387px and a - phone viewport is 375 — without it the row pushed the whole page 56px - wide and every screen on the site scrolled sideways, not just this one. */} + {/* Left: Quick-Switch Scenario Pills */}
+ + Scenario: + {SCENARIOS.map((scen, idx) => { const isSelected = scenarioIndex === idx; return ( @@ -538,9 +574,9 @@ export default function AgentFlowVisualizer({ type="button" onClick={() => handleSelectScenario(idx)} className={cn( - 'rounded-lg px-3 py-1 text-xs font-medium transition-all cursor-pointer border', + 'rounded-lg px-2.5 py-1 text-xs font-semibold transition-all cursor-pointer border', isSelected - ? 'border-brand bg-brand-tint text-brand font-bold shadow-2xs' + ? 'border-brand bg-brand-tint text-brand shadow-2xs font-bold' : 'border-border bg-surface text-ink-3 hover:text-ink-1 hover:bg-surface-elevated' )} > @@ -548,28 +584,96 @@ export default function AgentFlowVisualizer({ ); })} +
-
+ {/* Right: Step Scrubber & Speed Controls */} +
+ {/* Unified Executive Control Dock */} +
+ {/* Step-by-Step Scrubber Group */} +
+ - {/* Pause / Play button */} - + + + +
+ +
+ + {/* Quick Stage Jumpers: [01] [02] [03] [04] */} +
+ {[1, 2, 3, 4].map((s) => ( + + ))} +
+ +
+ + {/* Speed Toggle: [0.5x] [1x] [2x] */} +
+ {[0.5, 1, 2].map((sp) => ( + + ))} +
+
@@ -1050,6 +1154,15 @@ export default function AgentFlowVisualizer({
+ + + {/* Live Diagnostics Result Card */} + {probeReport && ( + + {/* Top Metric Cards */} +
+
+ + Round Trip + + + {probeReport.avgRtt} ms + + + {probeReport.jitter} ms jitter + +
+ +
+ + Handshake + + + {probeReport.handshakeMs} ms + + + TLS 1.3 + +
+ +
+ + Packet Loss + + + {probeReport.packetLoss} + + + 10/10 ACKs + +
+
+ + {/* 10-Ping Micro Sparkline Chart */} +
+
+ 10-Packet Latency Burst + 100% Responsive +
+ +
+ {probeReport.pings.map((ping, idx) => { + const heightPct = Math.min(100, Math.max(30, (ping / 30) * 100)); + return ( +
+
+ {/* Hover Tooltip */} +
+ {ping}ms +
+
+ ); + })} +
+
+ + {/* Channel & Target Endpoint Specs */} +
+
+ Target UI Endpoint: + {UI_ENDPOINT} +
+
+ Buffer Load: + {probeReport.bufferLoad} +
+
+ Cipher Suite: + + {probeReport.tlsCipher} + +
+
+ + {/* Raw WebSocket Frame Payload Collapsible */} +
+ + + {showRawFrame && ( +
+
+                            {JSON.stringify(probeReport.rawFrame, null, 2)}
+                          
+ +
+ )} +
+ + )} +
+ + {/* Secondary Technical Metadata */} +
- Technical Specifications (Secondary) + Technical Specifications
@@ -248,17 +494,6 @@ export default function AgentInspectorDrawer({ {agent.load}%
- - {/* Probe Action */} -
diff --git a/src/pages/doormile/customers/Customers.jsx b/src/pages/doormile/customers/Customers.jsx index b902a02..324d58d 100644 --- a/src/pages/doormile/customers/Customers.jsx +++ b/src/pages/doormile/customers/Customers.jsx @@ -30,6 +30,41 @@ export default function Customers({ embedded = false }) { const { data: customers = [], isLoading, isFetching } = useTenantCustomers(); const updateCustomer = useUpdateTenantCustomer(); + const [localSavedContacts] = useState(() => { + try { + const data = localStorage.getItem('doormile_saved_contacts'); + return data ? JSON.parse(data) : []; + } catch { + return []; + } + }); + + const mergedCustomers = useMemo(() => { + const list = [...(customers || [])]; + const seen = new Set(); + const result = []; + + for (const sc of localSavedContacts) { + const p = String(sc.phone || sc.contactno || '').replace(/\D/g, '').slice(-10); + if (p) seen.add(p); + result.push({ + ...sc, + appcustomerid: sc.id || `saved_${p}`, + name: sc.name || sc.firstname, + phone: sc.phone || sc.contactno, + totalbookings: 0 + }); + } + + for (const c of list) { + const p = String(c.phone || c.contactno || '').replace(/\D/g, '').slice(-10); + if (p && seen.has(p)) continue; + if (p) seen.add(p); + result.push(c); + } + return result; + }, [customers, localSavedContacts]); + const [search, setSearch] = useState(''); const debouncedSearch = useDebouncedValue(search); const [activeFilter, setActiveFilter] = useState('all'); @@ -38,21 +73,21 @@ export default function Customers({ embedded = false }) { const [form, setForm] = useState({ name: '', phone: '', email: '' }); const stats = useMemo(() => { - const total = customers.length; + const total = mergedCustomers.length; return { total, - withEmail: customers.filter((row) => Boolean(row.email)).length, - withPhone: customers.filter((row) => Boolean(row.phone)).length, - totalBookings: customers.reduce((sum, row) => sum + (Number(row.totalbookings) || 0), 0), + withEmail: mergedCustomers.filter((row) => Boolean(row.email)).length, + withPhone: mergedCustomers.filter((row) => Boolean(row.phone)).length, + totalBookings: mergedCustomers.reduce((sum, row) => sum + (Number(row.totalbookings) || 0), 0), }; - }, [customers]); + }, [mergedCustomers]); const rows = useMemo(() => { - let list = customers; + let list = mergedCustomers; if (activeFilter === 'withEmail') list = list.filter((row) => Boolean(row.email)); else if (activeFilter === 'withPhone') list = list.filter((row) => Boolean(row.phone)); return list.filter((row) => matchesQuery(row, [custName, custPhone, custEmail], debouncedSearch)); - }, [customers, activeFilter, debouncedSearch]); + }, [mergedCustomers, activeFilter, debouncedSearch]); const toggleFilter = (key) => setActiveFilter((current) => (current === key ? 'all' : key)); diff --git a/src/pages/doormile/home/Home.jsx b/src/pages/doormile/home/Home.jsx index 4543761..2219464 100644 --- a/src/pages/doormile/home/Home.jsx +++ b/src/pages/doormile/home/Home.jsx @@ -26,7 +26,7 @@ import { Users } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; -import { DOORMILE_WORDMARK_URL } from '@/assets/brand'; +import { MILETRUTH_WORDMARK_URL } from '@/assets/brand'; import { cn } from '@/lib/utils'; import { OpenToast } from '@/components/third-party/OpenToast'; import { answerQuestion } from '@/lib/assistant/intents'; @@ -39,10 +39,6 @@ import { executeAssign, executeRepeatAssign, describeRider } from '@/lib/assista import { startRepeatFlow, answerRepeatStep } from '@/lib/assistant/repeatFlow'; import { buildRepeatRun, describeDay } from '@/lib/assistant/repeatRuns'; -const LOGO_FILTER = { - filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)', -}; - const MILETRUTH_SUGGESTIONS = [ "Repeat yesterday's orders", 'Create a new order', @@ -1130,9 +1126,8 @@ export default function Home() { onClick={resetConversation} > Doormile diff --git a/src/pages/doormile/orders/CreateOrder.jsx b/src/pages/doormile/orders/CreateOrder.jsx index 37e349a..ca3f718 100644 --- a/src/pages/doormile/orders/CreateOrder.jsx +++ b/src/pages/doormile/orders/CreateOrder.jsx @@ -2,7 +2,9 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowLeft, + Bookmark, Building2, + Check, Clock, Coins, Loader2, @@ -26,6 +28,7 @@ import { HubLocationSelect } from '@/components/doormile/HubLocationSelect'; import { MapContainer, Marker, Polyline, TileLayer, useMap } from 'react-leaflet'; import L from 'leaflet'; import { + createAdminCustomer, createExpressBooking, getAdminCustomers, getAdminPricing, @@ -48,7 +51,7 @@ import { calculateDrivingRoute, calculateTotalCharge } from '@/lib/distance'; -import { saveRecentAddress } from '@/lib/geocodingService'; +import { geocodeAddress, saveRecentAddress } from '@/lib/geocodingService'; import { useHubs } from '@/lib/doormileHooks'; import { PICKUP_SOURCE, @@ -169,6 +172,19 @@ export default function CreateOrder() { const [isCustomerModalOpen, setIsCustomerModalOpen] = useState(false); const [searchCustQuery, setSearchCustQuery] = useState(''); + // Local persistent saved contacts for new/recent customers + const [savedContacts, setSavedContacts] = useState(() => { + try { + const data = localStorage.getItem('doormile_saved_contacts'); + return data ? JSON.parse(data) : []; + } catch { + return []; + } + }); + + const [saveForLater, setSaveForLater] = useState(true); + const [isSavedNow, setIsSavedNow] = useState(false); + // Map pin picker modal target ('pickup' | 'drop' | null) const [mapPickerTarget, setMapPickerTarget] = useState(null); @@ -184,6 +200,11 @@ export default function CreateOrder() { [staffTenants] ); + const selectedTenant = useMemo( + () => (activeTenants || []).find((t) => String(t.tenantid) === String(selectedTenantId)), + [activeTenants, selectedTenantId] + ); + const { data: tenantLocations = [], isLoading: isLocationsLoading } = useQuery({ queryKey: ['tenantLocations', selectedTenantId], queryFn: () => getTenantLocations(selectedTenantId), @@ -211,21 +232,105 @@ export default function CreateOrder() { queryFn: getAdminCustomers }); + // Combined customers: Merge server customers with locally saved/recent customer contacts + const allCustomers = useMemo(() => { + const list = [...(customerList || [])]; + const seenPhones = new Set(); + const result = []; + + // Prioritize saved contacts that have complete address info + for (const sc of savedContacts) { + const p = String(sc.phone || sc.contactno || '').replace(/\D/g, '').slice(-10); + if (p) { + seenPhones.add(p); + result.push(sc); + } + } + + for (const c of list) { + const p = String(c.phone || c.contactno || '').replace(/\D/g, '').slice(-10); + if (p && seenPhones.has(p)) { + // If server record exists but has no address, the local saved record is already retained + continue; + } + if (p) seenPhones.add(p); + result.push(c); + } + return result; + }, [customerList, savedContacts]); + const matchedCustomer = useMemo(() => { const cleanPhone = String(dropCust.contactno || '').replace(/\D/g, ''); if (cleanPhone.length >= 8) { - return (customerList || []).find((c) => String(c.phone || '').replace(/\D/g, '').endsWith(cleanPhone)); + return allCustomers.find((c) => String(c.phone || c.contactno || '').replace(/\D/g, '').endsWith(cleanPhone)); } return null; - }, [customerList, dropCust.contactno]); + }, [allCustomers, dropCust.contactno]); const filteredCustomers = useMemo(() => { const q = searchCustQuery.trim().toLowerCase(); - if (!q) return customerList; - return (customerList || []).filter( - (c) => c.name?.toLowerCase().includes(q) || c.phone?.includes(searchCustQuery) - ); - }, [customerList, searchCustQuery]); + if (!q) return allCustomers; + return allCustomers.filter((c) => { + const name = (c.name || c.firstname || '').toLowerCase(); + const phone = String(c.phone || c.contactno || ''); + const addr = (c.address || c.deliveryaddress || c.city || '').toLowerCase(); + return name.includes(q) || phone.includes(searchCustQuery) || addr.includes(q); + }); + }, [allCustomers, searchCustQuery]); + + // Persist a customer contact for future orders + const saveCustomerRecord = (cust = dropCust) => { + const digits = (v) => String(v ?? '').replace(/\D/g, ''); + const cleanNum = digits(cust.contactno || cust.phone).slice(0, 10); + const name = (cust.firstname || cust.name || '').trim(); + if (!cleanNum || !name) { + OpenToast('Please enter customer name and phone number to save', 'warning', 2500); + return false; + } + + const newContact = { + id: `saved_${cleanNum}`, + appcustomerid: `saved_${cleanNum}`, + name, + firstname: name, + phone: cleanNum, + contactno: cleanNum, + address: cust.address || '', + suburb: cust.suburb || '', + city: cust.city || '', + postcode: cust.postcode || '', + landmark: cust.landmark || '', + latitude: cust.latitude ? Number(cust.latitude) : undefined, + longitude: cust.longitude ? Number(cust.longitude) : undefined, + isSavedForLater: true, + updatedAt: new Date().toISOString() + }; + + setSavedContacts((prev) => { + const next = [newContact, ...prev.filter((p) => String(p.phone || p.contactno).replace(/\D/g, '').slice(-10) !== cleanNum)]; + try { + localStorage.setItem('doormile_saved_contacts', JSON.stringify(next.slice(0, 100))); + } catch {} + return next; + }); + + // Also persist to backend asynchronously if endpoint is reachable + createAdminCustomer({ + name, + firstname: name, + phone: cleanNum, + address: cust.address || '', + city: cust.city || '', + postcode: cust.postcode || '', + landmark: cust.landmark || '', + latitude: cust.latitude ? Number(cust.latitude) : 0, + longitude: cust.longitude ? Number(cust.longitude) : 0 + }).catch(() => {}); + + setIsSavedNow(true); + OpenToast(`Saved "${name}" (${cleanNum}) for future orders`, 'success', 3000); + return true; + }; const sortedLocations = useMemo(() => sortLocations(tenantLocations), [tenantLocations]); @@ -252,13 +357,32 @@ export default function CreateOrder() { const lat = loc.latitude ?? loc.lat ?? loc.pickuplatitude ?? loc.locationlat; const lng = loc.longitude ?? loc.lng ?? loc.long ?? loc.pickuplongitude ?? loc.locationlong; + // Resolve matching hub name and phone + const matchingHub = (hubList || []).find( + (h) => + (loc.city && h.city && h.city.toLowerCase() === loc.city.toLowerCase()) || + (loc.hubid && h.hubid === loc.hubid) || + (h.hubname && loc.locationname && h.hubname.toLowerCase().includes(loc.locationname.toLowerCase())) + ) || hubList[0]; + + const hubName = loc.locationname || matchingHub?.hubname || selectedTenant?.tenantname || 'Coimbatore Hub'; + const contactPhone = String( + loc.contactno || + loc.phone || + selectedTenant?.primarycontact || + selectedTenant?.contactno || + selectedTenant?.phone || + matchingHub?.contactno || + '' + ) + .replace(/\D/g, '') + .slice(0, 10); + // @ts-ignore setPickCust((prev) => ({ ...prev, - firstname: loc.contactname || loc.contactperson || prev.firstname || loc.locationname || 'Pickup Point', - contactno: String(loc.contactno || loc.phone || prev.contactno || '') - .replace(/\D/g, '') - .slice(0, 10), + firstname: loc.contactname || loc.contactperson || hubName, + contactno: contactPhone || prev.contactno, address: loc.address || '', landmark: loc.landmark || '', suburb: loc.locationname || loc.city || '', @@ -478,55 +602,74 @@ export default function CreateOrder() { // Select customer from modal const handlePickCustomer = (customer) => { - // GET /admin/customers already returns the whole address — doorno, address, - // suburb, city, state, postcode, landmark, latitude, longitude (see - // GetAdminCustomers in adminController.go). This used to take the name and - // the phone and drop the rest on the floor, so picking a saved contact - // filled two fields and left the operator to retype an address the system - // already had. - // - // The COORDINATES matter more than the text. handleSubmit validates drop - // coordinates, and the Order Summary's distance and total are computed - // from them — filling the address without them produces a form that looks - // complete, prices at the base rate, and then fails on submit. That is - // worse than filling nothing. + if (!customer) return; const digits = (v) => String(v ?? '').replace(/\D/g, ''); const text = (v) => String(v ?? '').trim(); - // Door number and landmark are separate columns but there is one address - // line on this form, so fold them in rather than lose them. - const street = [text(customer.doorno), text(customer.address)].filter(Boolean).join(', '); - const lat = customer.latitude; - const lng = customer.longitude; - const hasCoords = Number.isFinite(Number(lat)) && Number.isFinite(Number(lng)) && Number(lat) !== 0 && Number(lng) !== 0; + // Support multiple field schemas (Doormile appcustomers, legacy Nearle customers, or saved orders) + const rawAddress = text( + customer.deliveryaddress || + customer.address || + customer.formatted_address || + customer.location || + '' + ); + const street = [text(customer.doorno), rawAddress].filter(Boolean).join(', ') || rawAddress; + + const lat = customer.latitude ?? customer.deliverylatitude ?? customer.lat; + const lng = customer.longitude ?? customer.deliverylongitude ?? customer.lng ?? customer.long; + const hasCoords = + lat != null && + lng != null && + Number.isFinite(Number(lat)) && + Number.isFinite(Number(lng)) && + Number(lat) !== 0 && + Number(lng) !== 0; + + const suburb = text(customer.suburb || customer.deliverysuburb || customer.area); + const city = text(customer.city || customer.deliverycity); + const postcode = text(customer.postcode || customer.pincode || customer.deliverypincode); + const landmark = text(customer.landmark || customer.deliverylandmark); setDropCust((prev) => ({ ...prev, - firstname: customer.name || prev.firstname, - contactno: digits(customer.phone) ? digits(customer.phone).slice(0, 10) : prev.contactno, - // Every field falls back to what is already typed. A contact with a name - // and no address must never blank an address the operator has entered — - // silently erasing their work is a worse bug than not filling it. + firstname: customer.name || customer.firstname || customer.customer_name || prev.firstname, + contactno: digits(customer.phone || customer.contactno || customer.customer_phone) + ? digits(customer.phone || customer.contactno || customer.customer_phone).slice(0, 10) + : prev.contactno, address: street || prev.address, - landmark: text(customer.landmark) || prev.landmark, - suburb: text(customer.suburb) || prev.suburb, - city: text(customer.city) || prev.city, - postcode: text(customer.postcode) || prev.postcode, - latitude: hasCoords ? lat : prev.latitude, - longitude: hasCoords ? lng : prev.longitude + landmark: landmark || prev.landmark, + suburb: suburb || prev.suburb, + city: city || prev.city, + postcode: postcode || prev.postcode, + latitude: hasCoords ? Number(lat) : prev.latitude, + longitude: hasCoords ? Number(lng) : prev.longitude })); setIsCustomerModalOpen(false); - // Say what actually happened. A contact with no address on file leaves the - // operator with a half-filled form, and a toast that says "Selected" either - // way lets them assume the address came across and submit without it. - const name = customer.name || customer.phone || 'Contact'; + const name = customer.name || customer.firstname || customer.phone || 'Contact'; if (street && hasCoords) { OpenToast(`${name} — contact and address filled`, 'success', 2500); } else if (street) { - OpenToast(`${name} — address filled, but no saved location. Pin it on the map for distance and pricing.`, 'warning', 4500); + OpenToast(`${name} — address filled. Resolving map coordinates…`, 'info', 3000); + // Automatically attempt to geocode the address to resolve lat/lng if not present + geocodeAddress(street) + .then((place) => { + if (place?.latitude && place?.longitude) { + setDropCust((curr) => ({ + ...curr, + latitude: Number(place.latitude), + longitude: Number(place.longitude), + city: curr.city || place.city || '', + suburb: curr.suburb || place.suburb || '', + postcode: curr.postcode || place.postcode || '' + })); + OpenToast('Location pinned automatically from address', 'success', 2500); + } + }) + .catch(() => {}); } else { - OpenToast(`${name} — no address on file. Enter the delivery address.`, 'warning', 4000); + OpenToast(`${name} — contact details selected. Please enter delivery address.`, 'warning', 3500); } }; @@ -550,11 +693,7 @@ export default function CreateOrder() { OpenToast('Enter pickup address', 'warning', 2500); return; } - // Pickup coordinates were never checked here. That was survivable only - // while the pickup was always a saved location, which supplies its own - // coordinates — a hand-typed collection address that fails to geocode - // submits 0,0, and an order pinned at 0,0 is unroutable, unpriceable and - // invisible to every zone filter. + // Pickup coordinates check if (!Number(pickCust.latitude) || !Number(pickCust.longitude)) { OpenToast( isCustomerPickup @@ -615,20 +754,11 @@ export default function CreateOrder() { finalprice, notes: notes || '', parcels, - // Which flow this is, and which hub owns it. Sent even where the backend - // may not store them yet: an unknown key is ignored server-side, and the - // console classifies from coordinates in the meantime, so nothing depends - // on these landing. The day they are persisted, every order created from - // here carries its own type instead of being re-derived on each load. ...buildFlowFields({ flow: draftFlow.flow, servicingHubId: draftFlow.originAnchor?.hubid ?? draftFlow.destAnchor?.hubid ?? selectedLocation?.hubid ?? null, tenantLocationId: selectedLocationId, - // The collection point itself, so the order records what KIND of place - // it leaves from — a base, a client's site, or a customer's door. The - // backend stores it and the rider app titles the stop with it; without - // it every logistics pickup reads as the rider's own base name. pickupAnchor: draftFlow.originAnchor }) }; @@ -637,6 +767,10 @@ export default function CreateOrder() { try { const res = await createExpressBooking(payload); if (res?.success) { + // Save customer details for future orders if selected + if (saveForLater && dropCust.contactno && dropCust.firstname) { + saveCustomerRecord(dropCust); + } saveRecentAddress({ formatted_address: dropCust.address, name: dropCust.firstname, @@ -648,6 +782,7 @@ export default function CreateOrder() { }); queryClient.invalidateQueries({ queryKey: ['doormile', 'bookings'] }); queryClient.invalidateQueries({ queryKey: ['ordersBulk'] }); + queryClient.invalidateQueries({ queryKey: ['adminCustomers'] }); OpenToast('Order created successfully', 'success', 2500); navigate('/doormile/orders'); } else { @@ -1086,11 +1221,66 @@ export default function CreateOrder() { type="text" placeholder="Near landmark" value={dropCust.landmark} - onChange={(e) => setDropCust({ ...dropCust, landmark: e.target.value })} + onChange={(e) => { + setDropCust({ ...dropCust, landmark: e.target.value }); + setIsSavedNow(false); + }} 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" />
+ + {/* Save contact for future orders option */} + {dropCust.contactno && dropCust.contactno.length >= 8 && dropCust.firstname.trim() && ( +
+ + +
+ )} {/* Schedule & Packages Card */} @@ -1571,26 +1761,54 @@ export default function CreateOrder() { No customers found matching "{searchCustQuery}" ) : ( - filteredCustomers.map((cust) => ( -
-
-
{cust.name || 'Unnamed Contact'}
-
{cust.phone || '—'}
- {cust.email &&
{cust.email}
} -
- - -
- )) +
+
+ + {cust.name || cust.firstname || 'Unnamed Contact'} + + + ({cust.phone || cust.contactno || '—'}) + + {cust.isSavedForLater && ( + + Saved + + )} + {hasPin && ( + + 📍 Pinned + + )} +
+ {addr ? ( +
+ {addr} + {cust.city && · {cust.city}} + {cust.postcode && - {cust.postcode}} +
+ ) : ( +
No address on file
+ )} +
+ + + + ); + }) )}