diff --git a/src/components/AdminCustomersView.tsx b/src/components/AdminCustomersView.tsx index d2f04a4..6f76abf 100644 --- a/src/components/AdminCustomersView.tsx +++ b/src/components/AdminCustomersView.tsx @@ -6,7 +6,7 @@ import React, { useState } from 'react'; import { Users, Search, Phone, Mail, ChevronRight, Award, MapPin } from 'lucide-react'; import { useFiestaTenantCustomers, useFiestaTenantLocations } from '../services/fiestaQueries'; -import { num as fnum, str as fstr } from '../services/fiestaApi'; +import { num as fnum, str as fstr, customerName, customerStoreId } from '../services/fiestaApi'; interface AdminCustomersViewProps { tenantId: number; @@ -36,16 +36,19 @@ export default function AdminCustomersView({ tenantId }: AdminCustomersViewProps null; const spent = fnum(c.totalspent); - const locId = fnum(c.locationid) || fnum(c.applocationid); - const loc = locations.find((l: any) => fnum(l.locationid) === locId || fnum(l.applocationid) === locId); - const storeName = loc ? fstr(loc.locationname) : (fstr(c.locationname) || 'Central'); + // `locationid` is not a column on this response — the store link is + // aliased `tenantlocationid`. Matching on the wrong name meant every + // customer fell through to the invented store name 'Central'. + const locId = customerStoreId(c); + const loc = locations.find((l: any) => fnum(l.locationid) === locId); + const storeName = (loc ? fstr(loc.locationname) : fstr(c.locationname)).trim() || '—'; return { id, - name: fstr(c.fullname) || `${fstr(c.firstname)} ${fstr(c.lastname)}`.trim() || 'Customer', + name: customerName(c) || 'Customer', phone: fstr(c.contactno) || '—', email: fstr(c.email), - address: fstr(c.address) || 'Coimbatore', + address: fstr(c.address) || '—', ordersCount: Number(c.orderscount) || 0, totalSpent: spent > 0 ? `₹${spent.toLocaleString('en-IN')}` : '—', storeName, diff --git a/src/components/CustomerDetailPanel.tsx b/src/components/CustomerDetailPanel.tsx index 27e666c..cb176a8 100644 --- a/src/components/CustomerDetailPanel.tsx +++ b/src/components/CustomerDetailPanel.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from 'react'; import { useFiestaCustomerOrders } from '../services/fiestaQueries'; -import { num as fnum, str as fstr, type Row } from '../services/fiestaApi'; +import { num as fnum, str as fstr, customerName, customerStoreId, type Row } from '../services/fiestaApi'; import { Phone, MapPin, Mail, Receipt, X, Calendar, ShoppingBag, Wallet, TrendingUp, IndianRupee, Store } from 'lucide-react'; import OrderDetailsModal from './OrderDetailsModal'; import './CustomerDetailPanel.css'; @@ -70,8 +70,12 @@ export default function CustomerDetailPanel({ customer, onClose }: CustomerDetai }; }, [orders]); - const name = fstr(customer.customername) || fstr(customer.name) || 'Unknown Customer'; + // firstname/lastname are what the endpoint actually returns; `customername` + // and `name` are not columns on this response, so the old read always produced + // "Unknown Customer" (and "UC" initials) for every customer. + const name = customerName(customer) || 'Unknown Customer'; const phone = fstr(customer.contactno) || fstr(customer.phone) || ''; + const storeId = customerStoreId(customer); const email = fstr(customer.email) || ''; const address = fstr(customer.address) || fstr(customer.deliveryaddress) || ''; @@ -113,10 +117,10 @@ export default function CustomerDetailPanel({ customer, onClose }: CustomerDetai {fstr(customer.locationname) || fstr(customer.storename)} - ) : fnum(customer.locationid) ? ( + ) : storeId ? (
- Store {fnum(customer.locationid)} + Store {storeId}
) : null} {phone && ( diff --git a/src/components/DeliveriesView.tsx b/src/components/DeliveriesView.tsx index d93e335..19255a6 100644 --- a/src/components/DeliveriesView.tsx +++ b/src/components/DeliveriesView.tsx @@ -406,9 +406,9 @@ function DeliveryDetailModal({ row, riders, onClose }: { row: Row; riders: Row[] return createPortal(
{ if (e.target === e.currentTarget) onClose(); }}>
-
+
-

{fstr(row.orderid) || `Delivery ${fstr(row.deliveryid)}`}

+

{fstr(row.orderid) || `Delivery ${fstr(row.deliveryid)}`}

diff --git a/src/components/DispatchHubView.tsx b/src/components/DispatchHubView.tsx index b7f55c8..13f3141 100644 --- a/src/components/DispatchHubView.tsx +++ b/src/components/DispatchHubView.tsx @@ -8,8 +8,8 @@ import { Route, ShoppingBag, Truck, MapPin, Calendar, ChevronLeft, ChevronRight, import DispatchView from './DispatchView'; import OrdersView from './OrdersView'; import DeliveriesView from './DeliveriesView'; -import { useFiestaDeliveries } from '../services/fiestaQueries'; -import { FIESTA_TENANT_ID, ymd, num as fnum } from '../services/fiestaApi'; +import { useFiestaDeliveries, useFiestaTenantLocations } from '../services/fiestaQueries'; +import { FIESTA_TENANT_ID, ymd, num as fnum, str as fstr } from '../services/fiestaApi'; import type { Row } from '../services/fiestaApi'; import './DispatchView.css'; // For #hdr and date-chip styles @@ -34,6 +34,26 @@ export default function DispatchHubView({ locationid, tenantId = FIESTA_TENANT_I const rows = useMemo(() => allRows.filter(inScope), [allRows, locationid]); const totalOrders = rows.length; + // Operating area for the header pill. This was the hardcoded string + // "Coimbatore", shown to every tenant on the platform regardless of where they + // actually trade. The locations query is already cached by DispatchView (same + // key), so reading it here costs no extra request. + const locationsQ = useFiestaTenantLocations(tenantId); + const scopeLabel = useMemo(() => { + const locs = locationsQ.data ?? []; + if (locs.length === 0) return ''; + if (locationid) { + // Store user — name the outlet's own city. + const mine = locs.find((l) => fnum(l.locationid) === locationid); + return fstr(mine?.city).trim() || fstr(mine?.state).trim(); + } + // Admin — one city if the whole tenant sits in one, otherwise a count. + const cities = [...new Set(locs.map((l) => fstr(l.city).trim()).filter(Boolean))]; + if (cities.length === 1) return cities[0]; + if (cities.length > 1) return `${cities.length} cities`; + return ''; + }, [locationsQ.data, locationid]); + const isToday = date === ymd(today); const dateObj = new Date(`${date}T00:00:00`); const prettyDate = `${WEEKDAYS[dateObj.getDay()]}, ${dateObj.getDate()} ${MONTHS[dateObj.getMonth()]}`; @@ -53,12 +73,14 @@ export default function DispatchHubView({ locationid, tenantId = FIESTA_TENANT_I
C
Console
-
- - - Coimbatore - -
+ {scopeLabel && ( +
+ + + {scopeLabel} + +
+ )}
@@ -77,7 +99,7 @@ export default function DispatchHubView({ locationid, tenantId = FIESTA_TENANT_I ) : ( - Live · {totalOrders} orders + Live · {totalOrders} {totalOrders === 1 ? 'delivery' : 'deliveries'} )} @@ -140,7 +162,9 @@ export default function DispatchHubView({ locationid, tenantId = FIESTA_TENANT_I
- {/* Sub-tabs for Map View */} + {/* Sub-tabs for Map View — only meaningful on the Map tab; they used to + stay visible (and inert) on Orders and Deliveries. */} + {activeTab === 'map' && (
+ )}
diff --git a/src/components/DispatchView.css b/src/components/DispatchView.css index e2a72da..a47667e 100644 --- a/src/components/DispatchView.css +++ b/src/components/DispatchView.css @@ -2555,6 +2555,32 @@ transform: translateX(4px); } +/* Idle outlet — listed so the admin can see every store under the tenant, but + visually recessed so the stores actually dispatching today read first. Still + clickable: the focused view explains that nothing has gone out. */ +.dispatch-container .rcard.zone-card.is-idle { + background: #fcfcfd; + box-shadow: none; +} + +.dispatch-container .rcard.zone-card.is-idle::before { + background: linear-gradient(180deg, #cbd5e1, #94a3b8); + opacity: 0.4; +} + +.dispatch-container .rcard.zone-card.is-idle .zone-card-name { + color: var(--text-muted); +} + +.dispatch-container .rcard.zone-card.is-idle .zone-card-emoji { + background: rgba(148, 163, 184, 0.12); + border-color: rgba(148, 163, 184, 0.25); +} + +.dispatch-container .rcard.zone-card.is-idle .zone-card-header { + margin-bottom: 0; +} + /* Progress row: status bar + delivered/total counter */ .dispatch-container .zone-progress-row { display: flex; diff --git a/src/components/DispatchView.tsx b/src/components/DispatchView.tsx index 84b7f33..8cbd72f 100644 --- a/src/components/DispatchView.tsx +++ b/src/components/DispatchView.tsx @@ -22,10 +22,9 @@ import { Map as MapIcon, MapPin, Bike, - Store, Users, Phone, - ShoppingBag, + Store, Truck, Package, Ruler, @@ -36,7 +35,6 @@ import { Mailbox, StickyNote, ArrowLeftRight, - Calendar, ChevronLeft, ChevronRight, List, @@ -44,20 +42,19 @@ import { } from 'lucide-react'; import { useFiestaDeliveries, - useFiestaRiders, - useFiestaRiderPeriodicLogs, useFiestaTenantLocations, useFiestaTenantCustomers, } from '../services/fiestaQueries'; -import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi'; import { - colorFor, - getStatusStyle, - STATUS_STYLES, - extractTimeOnly, -} from '../services/dispatchShared'; + FIESTA_TENANT_ID, + num as fnum, + str as fstr, + customerName, + customerStoreId, + type Row, +} from '../services/fiestaApi'; +import { colorFor } from '../services/dispatchShared'; import DispatchMap, { type MapPoint } from './DispatchMap'; -import RiderTelemetryPanel from './RiderTelemetryPanel'; import CustomerDetailPanel from './CustomerDetailPanel'; import './DispatchView.css'; @@ -78,6 +75,25 @@ function statusStyle(s: string): React.CSSProperties { return { background: `${hex}1f`, color: hex }; } +/** + * Best available area label for a delivery row. `getdeliveries` has NO + * `deliverysuburb` column — reading it left every "areas" count at 0 and hid the + * suburb strip entirely. The feed does carry `locationsuburb` and a full + * `deliveryaddress`, so fall back to the address's locality segment + * ("12 Main St, Peelamedu, Coimbatore, 641004" → "Coimbatore"). + */ +function areaOf(r: Row): string { + const direct = fstr(r.deliverysuburb).trim() || fstr(r.locationsuburb).trim(); + if (direct) return direct; + const parts = fstr(r.deliveryaddress) + .split(',') + .map((p) => p.trim()) + .filter(Boolean) + // Trailing postcode / country segments aren't areas. + .filter((p) => !/^\d{4,6}$/.test(p) && p.toLowerCase() !== 'india'); + return parts.length > 1 ? parts[parts.length - 1] : ''; +} + /** Drop coordinates from a delivery row (several field spellings), or null. */ function dropLatLon(r: Row): [number, number] | null { const lat = fnum(r.droplat) || fnum(r.deliverylat) || fnum(r.deliverylatitude); @@ -92,12 +108,9 @@ function pickupLatLon(r: Row): [number, number] | null { return lat && lon ? [lat, lon] : null; } -// ── View modes (match #strat-row tabs) ─────────────────────────────────────────── -type ViewMode = 'stores' | 'zones' | 'customers' | 'riders' | 'kitchens'; -const VIEW_TABS: Array<{ id: ViewMode; label: string; icon: typeof MapIcon }> = [ - { id: 'stores', label: 'By Store', icon: Store }, - { id: 'customers', label: 'By Customer', icon: Users }, -]; +// ── View modes ────────────────────────────────────────────────────────────────── +// The tab row lives in DispatchHubView; only these two modes are reachable. +type ViewMode = 'stores' | 'customers'; interface Group { id: string; @@ -111,21 +124,19 @@ interface Group { suburbs: Map; statusCounts: Record; raw?: any; + /** Customer view: the outlet this customer is registered against, for the card badge. */ + storeName?: string; } interface DispatchViewProps { locationid?: number; tenantId?: number; date: string; - viewMode: 'stores' | 'zones' | 'customers' | 'riders' | 'kitchens'; + viewMode: ViewMode; } -const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; -const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, date, viewMode }: DispatchViewProps) { const [focusedId, setFocusedId] = useState(null); - const [focusedRiderId, setFocusedRiderId] = useState(null); useEffect(() => { setFocusedId(null); @@ -140,16 +151,10 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, // Core dispatch data const deliveriesQ = useFiestaDeliveries({ tenantid: tenantId, fromdate: date, todate: date, locationid }); const locationsQ = useFiestaTenantLocations(tenantId); + // Customers. Admin (no locationid) gets every customer under the tenant and + // filters client-side; a store user sends its locationid so the backend scopes + // to that outlet server-side via `tenantcustomers.locationid`. const customersQ = useFiestaTenantCustomers({ tenantid: tenantId, locationid: locationid || 0 }); - const ridersQ = useFiestaRiders({ tenantid: tenantId }); - - // Rider periodic logs (GPS snapshots) for the focused rider - const riderLogsQ = useFiestaRiderPeriodicLogs({ - userid: focusedRiderId ?? undefined, - fromdate: date, - todate: date, - tenantid: tenantId, - }); // Live deliveries only — no sample/demo fallback. When the feed is empty the // cockpit shows a genuine empty state rather than fabricated riders/stops. @@ -162,55 +167,94 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, [allRows, locationid], ); + /** locationid → outlet name, for stamping a store badge on customer cards. */ + const storeNames = useMemo(() => { + const m = new Map(); + for (const loc of locationsQ.data ?? []) { + const id = fnum(loc.locationid); + if (id) m.set(id, fstr(loc.locationname).trim() || `Store ${id}`); + } + return m; + }, [locationsQ.data]); + // ── Grouping ──────────────────────────────────────────────────────────────── + // Both modes seed from the roster first (every outlet / every customer under + // the scope) and then fold the day's deliveries into those seeds. That's what + // makes an outlet with no deliveries today, or a customer who hasn't ordered + // today, still appear in the list rather than vanishing. const groups = useMemo(() => { const map = new Map(); - + const blank = (id: string, name: string): Group => ({ + id, + name, + color: colorFor(id), + orders: [], + delivered: 0, + totalKm: 0, + profit: 0, + riders: new Set(), + suburbs: new Map(), + statusCounts: {}, + }); + if (viewMode === 'stores' && locationsQ.data) { + // Admin: every outlet under the tenant. Store user: only its own, because + // `locationid` is set. for (const loc of locationsQ.data) { if (locationid && fnum(loc.locationid) !== locationid) continue; const id = String(fnum(loc.locationid)).toLowerCase(); - const name = fstr(loc.locationname) || `Store ${id}`; - map.set(id, { id, name, color: colorFor(id), orders: [], delivered: 0, totalKm: 0, profit: 0, riders: new Set(), suburbs: new Map(), statusCounts: {} }); + map.set(id, blank(id, fstr(loc.locationname) || `Store ${id}`)); } } if (viewMode === 'customers' && customersQ.data) { for (const cust of customersQ.data) { - if (customerStoreFilter !== 'all') { - const locId = String(fnum(cust.locationid)); - if (locId !== customerStoreFilter) continue; - } + const storeId = customerStoreId(cust); + // Admin's store dropdown. A store user never sees it (the backend has + // already scoped the list), so this only ever narrows the admin view. + if (customerStoreFilter !== 'all' && String(storeId) !== customerStoreFilter) continue; const id = String(fnum(cust.customerid) || fstr(cust.contactno)).toLowerCase(); - const name = fstr(cust.customername) || fstr(cust.name) || `Customer ${id}`; - map.set(id, { id, name, color: colorFor(id), orders: [], delivered: 0, totalKm: 0, profit: 0, riders: new Set(), suburbs: new Map(), statusCounts: {}, raw: cust }); + map.set(id, { + ...blank(id, customerName(cust) || `Customer ${id}`), + raw: cust, + storeName: storeNames.get(storeId), + }); } } - const titleCase = (s: string) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s); const keyOf = (r: Row): { id: string; name: string } => { - if (viewMode === 'riders') { - const id = fstr(r.userid) || fstr(r.ridername) || 'unassigned'; - return { id, name: fstr(r.ridername) || fstr(r.username) || (id === 'unassigned' ? 'Unassigned' : `Rider ${id}`) }; - } if (viewMode === 'stores') { const locId = fstr(r.locationid) || fstr(r.pickuplocationid) || 'unknown'; const name = fstr(r.pickupcustomer) || fstr(r.pickuplocation) || `Store ${locId}`; return { id: locId.toLowerCase(), name }; } - if (viewMode === 'customers') { - const custId = fstr(r.customerid) || fstr(r.contactno) || fstr(r.deliverycustomerphone) || 'unknown'; - const name = fstr(r.deliverycustomer) || fstr(r.customername) || `Customer ${custId}`; - return { id: custId.toLowerCase(), name }; - } - const name = fstr(r.deliverysuburb) || fstr(r.zone_name) || 'Unzoned'; - return { id: name.toLowerCase(), name }; + const custId = fstr(r.customerid) || fstr(r.deliverycontactno) || 'unknown'; + return { id: custId.toLowerCase(), name: customerName(r) || `Customer ${custId}` }; }; + for (const r of rows) { const { id, name } = keyOf(r); let g = map.get(id); if (!g) { - g = { id, name, color: colorFor(id), orders: [], delivered: 0, totalKm: 0, profit: 0, riders: new Set(), suburbs: new Map(), statusCounts: {} }; + // A delivery whose customer isn't in the roster (not linked to this + // tenant, or beyond the page we fetched). Keep it — dropping it would + // hide real work from the board — but honour an active store filter. + if (viewMode === 'customers' && customerStoreFilter !== 'all') continue; + g = blank(id, name); + if (viewMode === 'customers') { + // Stand in for the missing customer record so the card still opens. + // Without a `raw` the detail panel stayed on "Select a customer" no + // matter how many times the card was clicked. + g.raw = { + customerid: fnum(r.customerid), + firstname: fstr(r.deliverycustomer), + contactno: fstr(r.deliverycontactno), + address: fstr(r.deliveryaddress), + tenantlocationid: fnum(r.locationid), + locationname: fstr(r.locationname), + }; + g.storeName = storeNames.get(fnum(r.locationid)) || fstr(r.locationname) || undefined; + } map.set(id, g); } g.orders.push(r); @@ -221,11 +265,11 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, g.profit += fnum(r.profit); const rid = fstr(r.userid) || fstr(r.ridername); if (rid) g.riders.add(rid); - const sub = fstr(r.deliverysuburb); + const sub = areaOf(r); if (sub) g.suburbs.set(sub, (g.suburbs.get(sub) ?? 0) + 1); } return Array.from(map.values()).sort((a, b) => b.orders.length - a.orders.length || a.name.localeCompare(b.name)); - }, [rows, viewMode, locationsQ.data, customersQ.data]); + }, [rows, viewMode, locationid, locationsQ.data, customersQ.data, customerStoreFilter, storeNames]); useEffect(() => { if (viewMode === 'stores' && locationid && groups.length === 1 && !focusedId) { @@ -234,28 +278,16 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, }, [viewMode, locationid, groups, focusedId]); const focused = groups.find((g) => g.id === focusedId) ?? null; - const groupedByRider = viewMode !== 'riders'; - // Trip blocks for the focused group: by trip# (rider view) or by rider (zone/all view). + // Trip blocks for the focused group, one per rider. const tripBlocks = useMemo(() => { if (!focused) return []; const map = new Map(); for (const r of focused.orders) { - let key: string; - let label: string; - let color: string; - if (groupedByRider) { - const rid = fstr(r.userid) || fstr(r.ridername) || 'unassigned'; - key = rid; - label = fstr(r.ridername) || fstr(r.username) || (rid === 'unassigned' ? 'Unassigned' : `Rider ${rid}`); - color = colorFor(rid); - } else { - key = fstr(r.trip_number) || '1'; - label = `Trip ${key}`; - color = focused.color; - } - let blk = map.get(key); - if (!blk) { blk = { label, color, orders: [] }; map.set(key, blk); } + const rid = fstr(r.userid) || fstr(r.ridername) || 'unassigned'; + const label = fstr(r.ridername) || (rid === 'unassigned' ? 'Unassigned' : `Rider ${rid}`); + let blk = map.get(rid); + if (!blk) { blk = { label, color: colorFor(rid), orders: [] }; map.set(rid, blk); } blk.orders.push(r); } const blocks = Array.from(map.values()); @@ -266,6 +298,8 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, const tb = fstr(b.deliverytime) || fstr(b.expecteddeliverytime); return ta.localeCompare(tb); } + // `step` isn't in the getdeliveries response, so planned order falls back + // to assignment time. Kept in case the backend starts sending it. const sa = fnum(a.step); const sb = fnum(b.step); if (sa && sb && sa !== sb) return sa - sb; @@ -273,7 +307,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, }); } return blocks; - }, [focused, groupedByRider, tripSort]); + }, [focused, tripSort]); // Map points: the focused group's ordered stops (with a route), else every stop // for the day (coloured per rider). Rows without coordinates are skipped. @@ -289,8 +323,8 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, lon: ll[1], step: fnum(r.step) || i + 1, color: focused ? focused.color : colorFor(fstr(r.userid) || fstr(r.ridername) || 'x'), - title: fstr(r.deliverycustomer) || `Order ${fstr(r.orderid)}`, - subtitle: fstr(r.deliverysuburb) || fstr(r.deliveryaddress), + title: customerName(r) || `Order ${fstr(r.orderid)}`, + subtitle: areaOf(r) || fstr(r.deliveryaddress), status: fstr(r.orderstatus), raw: r, }); @@ -302,11 +336,14 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, const firstOrder = tripBlocks[0]?.orders[0] ?? focused?.orders[0]; const routeStart = focused && firstOrder ? pickupLatLon(firstOrder) : null; + // The roster query matters as much as the delivery feed — gating on deliveries + // alone flashed "No customers found" while the customer list was still loading. + const isLoading = + deliveriesQ.isLoading || (viewMode === 'customers' ? customersQ.isLoading : locationsQ.isLoading); + // KPI scope. const totalOrders = rows.length; const totalDelivered = rows.filter((r) => fstr(r.orderstatus).toLowerCase() === 'delivered').length; - const activeRiders = new Set(rows.map((r) => fstr(r.userid) || fstr(r.ridername)).filter(Boolean)).size; - const fleetSize = (ridersQ.data ?? []).length; const fmtTime = (raw: unknown): string => { const m = fstr(raw).match(/(\d{1,2}):(\d{2})/); @@ -364,26 +401,27 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
- {deliveriesQ.isLoading ? ( -
Loading dispatch feed…
+ {isLoading ? ( +
{viewMode === 'customers' ? 'Loading customers…' : 'Loading dispatch feed…'}
) : focused && viewMode !== 'customers' ? ( setFocusedId(null)} fmtTime={fmtTime} - riderLogs={riderLogsQ.data} - riderLogsLoading={riderLogsQ.isLoading} /> ) : groups.length === 0 ? ( -
{viewMode === 'customers' ? 'No customers found' : 'No deliveries for this day'}
+
+ {viewMode === 'customers' + ? (customerStoreFilter !== 'all' ? 'No customers for this store' : 'No customers found') + : 'No stores found'} +
) : ( <>
- {viewMode === 'riders' ? 'Riders' : viewMode === 'customers' ? 'Customers' : viewMode === 'stores' ? 'Stores' : 'Zones'} ({groups.length}) + {viewMode === 'customers' ? 'Customers' : 'Stores'} ({groups.length}) {viewMode === 'customers' && !locationid && locationsQ.data && (