import { useState, useMemo, useEffect, Fragment } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Grid, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, TablePagination, Typography, Collapse, CircularProgress, Alert, Tooltip, Chip, Divider, Link, useMediaQuery, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import SearchIcon from '@mui/icons-material/Search'; import AddIcon from '@mui/icons-material/Add'; import RefreshIcon from '@mui/icons-material/Refresh'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined'; import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import HandshakeOutlinedIcon from '@mui/icons-material/HandshakeOutlined'; import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined'; import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import NotesOutlinedIcon from '@mui/icons-material/NotesOutlined'; import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt'; import MapOutlinedIcon from '@mui/icons-material/MapOutlined'; import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined'; import PageHeader from '@/components/PageHeader'; import StatCard from '@/components/StatCard'; import StatusChip from '@/components/StatusChip'; import EmptyState from '@/components/EmptyState'; import UserAvatar from '@/components/UserAvatar'; import TabLabelCount from '@/components/TabLabelCount'; import { fetchClients, deleteClient } from '@/utils/apiClient'; import { toClient } from '@/utils/mappers'; import { titleCase } from '@/utils/format'; import ClientFormDialog from './ClientFormDialog'; // Map categorical enum values to a semantic palette color. const consentTone = (v) => ({ full: 'success', basiconly: 'info', none: 'default' }[String(v || '').toLowerCase()] || 'default'); const efficiencyTone = (v) => { const k = String(v || '').toLowerCase(); if (/high|good|excellent/.test(k)) return 'success'; if (/med|average|moderate/.test(k)) return 'warning'; if (/low|poor|bad/.test(k)) return 'error'; return 'info'; }; const FIELD_LABEL_SX = { textTransform: 'uppercase', letterSpacing: 0.4, fontSize: '0.68rem', fontWeight: 700, color: 'grey.700' }; // A soft, theme-tinted pill for categorical values. Falls back to a dash placeholder. function Pill({ label, color = 'default' }) { if (label === undefined || label === null || label === '') { return ; } return ( ); } // Label + arbitrary node value (text, pill, or grouped chips). function Field({ label, children }) { return ( {label} {children} ); } function ReadField({ label, value }) { return ( {value || '—'} ); } // A bordered sub-card with a tinted header strip — frames each group of fields. function SectionCard({ icon: Icon, title, accent = 'primary', children }) { return ( {title} {children} ); } // Mini KPI tile used inside the Business card for the headline numbers. function StatTile({ label, value, icon: Icon, color = 'primary' }) { return ( {value} {label} ); } // Compact metric used inline in the table so key numbers show without expanding. function Metric({ label, value, color = 'grey.800' }) { return ( {value} {label} ); } // The expandable detail panel — shared by the desktop table row and the mobile card. function ClientDetail({ row, onEdit }) { return ( `linear-gradient(90deg, ${theme.palette.primary.lighter}88 0%, ${theme.palette.background.paper} 75%)` }} > {row.name} {row.clientId} {row.clientId} {row.logisticsSegment ? ( {String(row.logisticsSegment).split(/[,/]/).map((seg) => seg.trim()).filter(Boolean).map((seg) => ( ))} ) : } {(() => { const stops = [row.transitFrom, ...String(row.transitTo || '').split(',')] .map((s) => s.trim()).filter(Boolean); if (!stops.length) return ; return ( {stops.map((stop, idx) => ( {idx > 0 && } ))} ); })()} {[row.city, row.businessState].filter(Boolean).join(', ') || '—'} {row.surveyAddress || '—'} {row.surveyLat && row.surveyLng ? ( {[row.neighbourhood, row.city].filter(Boolean).join(', ') || 'Pinned location'} ) : } {row.notes && ( Notes {row.notes} )} ); } // Mobile presentation of a client — summary card with an expandable detail panel. function ClientCard({ row, onEdit, onDelete }) { const [open, setOpen] = useState(false); return ( {row.name} {row.logicalId} {row.phone || '—'} {row.city || '—'}{row.businessState ? `, ${row.businessState}` : ''} } sx={{ mt: 1.75 }}> ); } function ClientRow({ row, index, onEdit, onDelete }) { const [open, setOpen] = useState(false); return ( *': { borderBottom: open ? 'unset' : undefined }, ...(open && { bgcolor: 'primary.lighter', '&:hover': { bgcolor: 'primary.lighter' } }) }} onClick={() => setOpen((o) => !o)} > { e.stopPropagation(); setOpen((o) => !o); }}> {open ? : } {row.logicalId} {row.name} {row.businessType && ( )} {row.phone || '—'} {row.frequency && {row.frequency}} {row.city || '—'}{row.businessState ? `, ${row.businessState}` : ''} {row.neighbourhood && {row.neighbourhood}} }> { e.stopPropagation(); onEdit(row); }}> { e.stopPropagation(); onDelete(row); }}> ); } export default function Tenants() { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const [clients, setClients] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchParams] = useSearchParams(); const [tab, setTab] = useState(0); const [search, setSearch] = useState(searchParams.get('q') || ''); const [page, setPage] = useState(0); const [rpp, setRpp] = useState(10); // Keep the search box in sync when navigated here with a ?q= query (e.g. from the top search bar). useEffect(() => { const q = searchParams.get('q'); if (q != null) { setSearch(q); setPage(0); } }, [searchParams]); const [dialog, setDialog] = useState({ open: false, mode: 'add', initial: null }); const [toDelete, setToDelete] = useState(null); const [deleting, setDeleting] = useState(false); const load = (silent = false) => { if (!silent) { setLoading(true); setError(null); } fetchClients() .then((points) => setClients(points.map(toClient))) .catch((e) => { if (!silent) setError(e.message || 'Failed to load clients'); }) .finally(() => { if (!silent) setLoading(false); }); }; useEffect(() => { load(); const intervalId = setInterval(() => { load(true); // Silent poll every 10 seconds }, 10000); return () => clearInterval(intervalId); }, []); const stats = useMemo(() => ({ total: clients.length, newCount: clients.filter((c) => c.status === 'newClient').length, parcels: clients.reduce((s, c) => s + (Number(c.parcelVolume) || 0), 0), contracts: clients.reduce((s, c) => s + (Number(c.activeContracts) || 0), 0) }), [clients]); const tabs = useMemo(() => { const seen = []; clients.forEach((c) => { if (!seen.includes(c.status)) seen.push(c.status); }); return [{ key: 'all', label: 'All' }, ...seen.map((s) => ({ key: s, label: titleCase(s) }))]; }, [clients]); const tabKey = tabs[Math.min(tab, tabs.length - 1)]?.key || 'all'; const counts = useMemo(() => { const c = { all: clients.length }; clients.forEach((cl) => { c[cl.status] = (c[cl.status] || 0) + 1; }); return c; }, [clients]); const filtered = useMemo( () => clients.filter((t) => { const matchTab = tabKey === 'all' || t.status === tabKey; const matchSearch = !search || [t.name, t.phone, t.city, t.businessType, t.clientId, t.neighbourhood] .join(' ').toLowerCase().includes(search.toLowerCase()); return matchTab && matchSearch; }), [clients, tabKey, search] ); const paged = filtered.slice(page * rpp, page * rpp + rpp); const handleSaved = () => { setDialog({ open: false, mode: 'add', initial: null }); load(); }; const confirmDelete = async () => { setDeleting(true); try { await deleteClient(toDelete.id); setToDelete(null); load(); } catch (e) { setError(e.message || 'Failed to delete client'); } finally { setDeleting(false); } }; return ( <> } /> `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)` }} > Client Directory Browse, search and manage every client account { setSearch(e.target.value); setPage(0); }} sx={{ width: { xs: '100%', md: 300 } }} InputProps={{ startAdornment: }} /> {!loading && ( {filtered.length} {filtered.length === 1 ? 'client' : 'clients'} )} { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto"> {tabs.map((t, i) => ( } /> ))} {error && Retry}>{error}} {loading ? ( ) : paged.length === 0 ? ( ) : isMobile ? ( {paged.map((row) => ( setDialog({ open: true, mode: 'edit', initial: r })} onDelete={(r) => setToDelete(r)} /> ))} ) : ( ID Client Contact Location Volume Status Actions {paged.map((row, i) => ( setDialog({ open: true, mode: 'edit', initial: r })} onDelete={(r) => setToDelete(r)} /> ))}
)} setPage(p)} rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]} />
setDialog({ open: false, mode: 'add', initial: null })} onSaved={handleSaved} /> setToDelete(null)}> Delete client? This will permanently remove {toDelete?.name} from the doormile_clients collection. This cannot be undone. ); }