From 3e7aaf1e251f00f7e93a5bb6c2e44703690b1a7b Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Fri, 25 Sep 2026 15:10:17 +0530 Subject: [PATCH] updates on the customer app and map stuck issue --- src/App.jsx | 2 + src/api/doormile/client.js | 35 ++++++++ src/pages/doormile/customers/Customers.jsx | 95 +++++++++++++++++----- src/pages/doormile/orders/CreateOrder.jsx | 86 ++++++++++++++------ tests/api/client.test.js | 43 +++++++++- 5 files changed, 215 insertions(+), 46 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index dedfbd3..adf35c4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -83,6 +83,8 @@ const AuthenticatedApp = () => { now" without a filter being set first. */} } /> } /> + } /> + } /> }> } /> diff --git a/src/api/doormile/client.js b/src/api/doormile/client.js index 87e04a0..ca9ab6e 100644 --- a/src/api/doormile/client.js +++ b/src/api/doormile/client.js @@ -120,4 +120,39 @@ export const readStoredToken = () => localStorage.getItem(DOORMILE_TOKEN_KEY); export const errorMessage = (err, fallback = 'Something went wrong') => err?.message || err?.response?.data?.message || fallback; +/** + * A failed call explained for the person at the screen, not the developer. + * + * `action` finishes the sentence "We couldn't …", e.g. 'create this order'. + * The server's own message is kept where it describes the user's input + * (400/409/422 — e.g. a CityGate pincode rejection), because that is the + * part they can fix. Everything else — no connection, a timeout, a 5xx, raw + * axios text like "Network Error" — is replaced with what happened and what + * to do next. + */ +export const friendlyErrorMessage = (err, action = 'complete this request') => { + const status = err?.httpStatus ?? err?.response?.status; + const serverMsg = String(err?.response?.data?.message || (status ? err?.message : '') || '').trim(); + + if (!status) { + if (typeof navigator !== 'undefined' && navigator.onLine === false) { + return `We couldn't ${action} because you're offline. Check your internet connection and try again.`; + } + if (err?.code === 'ECONNABORTED' || /timeout/i.test(err?.message || '')) { + return `We couldn't ${action} — the server took too long to respond. Please try again in a moment.`; + } + return `We couldn't ${action} because the Doormile server couldn't be reached. Check your internet connection and try again.`; + } + if (status === 401) return 'Your session has expired. Please sign in again.'; + if (status === 403) return `You don't have permission to ${action}. Ask your admin for access.`; + if (status === 404) return `We couldn't ${action} — the record it refers to no longer exists. Refresh the page and try again.`; + if (status === 429) return `Too many requests at once. Wait a few seconds, then try to ${action} again.`; + if (status >= 500) { + return `We couldn't ${action} because of a problem on our server. Your details are still here — please try again in a minute. If it keeps happening, contact Doormile support.`; + } + return serverMsg + ? `We couldn't ${action}: ${serverMsg}` + : `We couldn't ${action}. Please check the details you entered and try again.`; +}; + export default doormileAxios; diff --git a/src/pages/doormile/customers/Customers.jsx b/src/pages/doormile/customers/Customers.jsx index 324d58d..5fbfa2d 100644 --- a/src/pages/doormile/customers/Customers.jsx +++ b/src/pages/doormile/customers/Customers.jsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Mail, Package, Pencil, Phone, Plus, Users } from 'lucide-react'; import { @@ -22,22 +22,38 @@ const custId = (row) => row.appcustomerid ?? row.tenantcustomerid ?? row.custome const custName = (row) => row.name || [row.firstname, row.lastname].filter(Boolean).join(' ') || '—'; const custPhone = (row) => row.phone || row.contactno || ''; const custEmail = (row) => row.email || ''; +/** Address accessor for search and preview */ +const custAddress = (row) => [row.address, row.deliveryaddress, row.suburb, row.city, row.postcode].filter(Boolean).join(', '); /** Tenant customers carry no booking count; 0 is honest rather than blank. */ const custBookings = (row) => row.totalbookings ?? 0; +const readSavedContacts = () => { + try { + const data = localStorage.getItem('doormile_saved_contacts'); + return data ? JSON.parse(data) : []; + } catch { + return []; + } +}; + export default function Customers({ embedded = false }) { const navigate = useNavigate(); 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 [localSavedContacts, setLocalSavedContacts] = useState(readSavedContacts); + + useEffect(() => { + const handleUpdate = () => setLocalSavedContacts(readSavedContacts()); + window.addEventListener('doormile_saved_contacts_updated', handleUpdate); + window.addEventListener('storage', handleUpdate); + window.addEventListener('focus', handleUpdate); + return () => { + window.removeEventListener('doormile_saved_contacts_updated', handleUpdate); + window.removeEventListener('storage', handleUpdate); + window.removeEventListener('focus', handleUpdate); + }; + }, []); const mergedCustomers = useMemo(() => { const list = [...(customers || [])]; @@ -86,7 +102,7 @@ export default function Customers({ embedded = false }) { 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)); + return list.filter((row) => matchesQuery(row, [custName, custPhone, custEmail, custAddress], debouncedSearch)); }, [mergedCustomers, activeFilter, debouncedSearch]); const toggleFilter = (key) => setActiveFilter((current) => (current === key ? 'all' : key)); @@ -97,13 +113,39 @@ export default function Customers({ embedded = false }) { }; const save = () => { + const id = custId(editRow); + // If it's a locally saved contact, update localStorage directly + if (String(id).startsWith('saved_')) { + const cleanPhone = String(form.phone || '').replace(/\D/g, '').slice(-10); + try { + const stored = JSON.parse(localStorage.getItem('doormile_saved_contacts') || '[]'); + const updated = stored.map((item) => { + if (item.id === id || String(item.phone || item.contactno).replace(/\D/g, '').slice(-10) === cleanPhone) { + return { + ...item, + name: form.name, + firstname: form.name, + phone: form.phone, + contactno: form.phone, + email: form.email + }; + } + return item; + }); + localStorage.setItem('doormile_saved_contacts', JSON.stringify(updated)); + window.dispatchEvent(new Event('doormile_saved_contacts_updated')); + } catch {} + setEditRow(null); + return; + } + /* The record stores firstname/lastname but the dialog edits one name field. Splitting on the first space keeps the surname through a round trip instead of blanking it. */ const [firstname, ...rest] = String(form.name || '').trim().split(/\s+/); updateCustomer.mutate( { - id: custId(editRow), + id, data: { firstname: firstname || '', lastname: rest.join(' '), @@ -122,15 +164,28 @@ export default function Customers({ embedded = false }) { header: 'Customer', sortable: true, accessor: custName, - cell: (row) => ( -
- -
-

{custName(row)}

-

ID #{custId(row) ?? '—'}

+ cell: (row) => { + const addr = custAddress(row); + const isSaved = row.isSavedForLater || String(custId(row)).startsWith('saved_'); + return ( +
+ +
+
+

{custName(row)}

+ {isSaved && ( + + Saved + + )} +
+

+ {addr ? `📍 ${addr}` : `ID #${custId(row) ?? '—'}`} +

+
-
- ), + ); + }, }, { key: 'contact', @@ -219,7 +274,7 @@ export default function Customers({ embedded = false }) { search={search} onSearchChange={setSearch} placeholder="Search by name, phone or email" - count={{ shown: rows.length, total: customers.length, noun: 'customers' }} + count={{ shown: rows.length, total: mergedCustomers.length, noun: 'customers' }} actions={