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}
} onClick={() => onEdit(row)} sx={{ width: { xs: '100%', sm: 'auto' } }}>Edit Client
{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'}
}
sx={{
py: 0.25, px: 1.25, minWidth: 0, fontSize: '0.75rem', fontWeight: 600, borderRadius: 5,
color: 'primary.main', borderColor: 'primary.100', bgcolor: 'primary.lighter',
'&:hover': { borderColor: 'primary.main', bgcolor: 'primary.lighter' }
}}
>
View on map
) : —}
{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 }}>
} onClick={() => onEdit(row)}>Edit
} onClick={() => onDelete(row)}>Delete
);
}
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 (
<>
} onClick={load} disabled={loading} sx={{ flex: { xs: 1, sm: 'none' } }}>Refresh
} onClick={() => setDialog({ open: true, mode: 'add', initial: null })} sx={{ flex: { xs: 1, sm: 'none' } }}>Add Client
}
/>
`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}
/>
>
);
}