updated for the mobile view
This commit is contained in:
@@ -36,7 +36,7 @@ export default function PageHeader({ title, breadcrumbs = [], action }) {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{action && <Box>{action}</Box>}
|
{action && <Box sx={{ width: { xs: '100%', sm: 'auto' }, flexShrink: 0 }}>{action}</Box>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Grid, Card, Box, Stack, Typography, Button, Divider, LinearProgress, CircularProgress, Alert,
|
Grid, Card, Box, Stack, Typography, Button, Divider, LinearProgress, CircularProgress, Alert,
|
||||||
Table, TableBody, TableCell, TableHead, TableRow, TableContainer
|
Table, TableBody, TableCell, TableHead, TableRow, TableContainer, useMediaQuery
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined';
|
import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined';
|
||||||
import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined';
|
import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined';
|
||||||
@@ -68,7 +69,7 @@ function Panel({ icon: Icon, title, action, color = 'primary', noPadding = false
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row" spacing={1.5} alignItems="center"
|
direction="row" spacing={1.5} alignItems="center"
|
||||||
sx={{
|
sx={{
|
||||||
px: 3, py: 2, borderBottom: 1, borderColor: 'divider',
|
px: { xs: 2, sm: 3 }, py: 2, borderBottom: 1, borderColor: 'divider',
|
||||||
background: (theme) => `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, transparent 100%)`
|
background: (theme) => `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, transparent 100%)`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -78,12 +79,14 @@ function Panel({ icon: Icon, title, action, color = 'primary', noPadding = false
|
|||||||
<Typography variant="h6" sx={{ fontWeight: 700, color: 'grey.800', flexGrow: 1, letterSpacing: '-0.3px' }}>{title}</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 700, color: 'grey.800', flexGrow: 1, letterSpacing: '-0.3px' }}>{title}</Typography>
|
||||||
{action}
|
{action}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box sx={{ p: noPadding ? 0 : 3, flexGrow: 1 }}>{children}</Box>
|
<Box sx={{ p: noPadding ? 0 : { xs: 2, sm: 3 }, flexGrow: 1 }}>{children}</Box>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const [clients, setClients] = useState([]);
|
const [clients, setClients] = useState([]);
|
||||||
const [team, setTeam] = useState([]);
|
const [team, setTeam] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -161,6 +164,21 @@ export default function Dashboard() {
|
|||||||
<Panel icon={HistoryOutlinedIcon} title="Recent Clients" color="primary" noPadding>
|
<Panel icon={HistoryOutlinedIcon} title="Recent Clients" color="primary" noPadding>
|
||||||
{recent.length === 0 ? (
|
{recent.length === 0 ? (
|
||||||
<EmptyState title="No clients yet" caption="Add a client to see it here." />
|
<EmptyState title="No clients yet" caption="Add a client to see it here." />
|
||||||
|
) : isMobile ? (
|
||||||
|
<Stack divider={<Divider />}>
|
||||||
|
{recent.map((c) => (
|
||||||
|
<Stack key={c.id} direction="row" spacing={1.25} alignItems="center" sx={{ px: 2, py: 1.5 }}>
|
||||||
|
<UserAvatar name={c.name} size={36} />
|
||||||
|
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }} noWrap>{c.name}</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>
|
||||||
|
{[titleCase(c.businessType), c.city].filter(Boolean).join(' · ') || '—'} · {c.parcelVolume.toLocaleString('en-IN')} parcels
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<StatusChip status={c.status} />
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table sx={{ minWidth: 600 }}>
|
<Table sx={{ minWidth: 600 }}>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function Section({ icon: Icon, title, subtitle, color = 'primary', danger = fals
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row" spacing={1.75} alignItems="center"
|
direction="row" spacing={1.75} alignItems="center"
|
||||||
sx={{
|
sx={{
|
||||||
px: 3, py: 2.25, borderBottom: 1, borderColor: 'divider',
|
px: { xs: 2, sm: 3 }, py: 2.25, borderBottom: 1, borderColor: 'divider',
|
||||||
background: (theme) => `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, ${theme.palette.background.paper} 72%)`
|
background: (theme) => `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, ${theme.palette.background.paper} 72%)`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -83,7 +83,7 @@ function Section({ icon: Icon, title, subtitle, color = 'primary', danger = fals
|
|||||||
{subtitle && <Typography variant="caption" color="text.secondary">{subtitle}</Typography>}
|
{subtitle && <Typography variant="caption" color="text.secondary">{subtitle}</Typography>}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box sx={{ px: 3 }}>{children}</Box>
|
<Box sx={{ px: { xs: 2, sm: 3 } }}>{children}</Box>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -155,10 +155,13 @@ export default function Settings() {
|
|||||||
title="Settings"
|
title="Settings"
|
||||||
breadcrumbs={[{ label: 'Settings' }]}
|
breadcrumbs={[{ label: 'Settings' }]}
|
||||||
action={
|
action={
|
||||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
<Stack
|
||||||
|
direction="row" spacing={1.5} alignItems="center" useFlexGap flexWrap="wrap"
|
||||||
|
sx={{ width: { xs: '100%', sm: 'auto' }, justifyContent: { xs: 'flex-start', sm: 'flex-end' } }}
|
||||||
|
>
|
||||||
{dirty && <Chip size="small" label="Unsaved changes" sx={{ bgcolor: 'warning.lighter', color: 'warning.dark', fontWeight: 600 }} />}
|
{dirty && <Chip size="small" label="Unsaved changes" sx={{ bgcolor: 'warning.lighter', color: 'warning.dark', fontWeight: 600 }} />}
|
||||||
<Button variant="outlined" onClick={discard}>Discard</Button>
|
<Button variant="outlined" onClick={discard} sx={{ flex: { xs: 1, sm: 'none' } }}>Discard</Button>
|
||||||
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}>Save Changes</Button>
|
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save} sx={{ flex: { xs: 1, sm: 'none' } }}>Save Changes</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useState, useMemo, useEffect } from 'react';
|
|||||||
import {
|
import {
|
||||||
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Chip, Link,
|
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Chip, Link,
|
||||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
||||||
TablePagination, Typography, CircularProgress, Alert, Tooltip,
|
TablePagination, Typography, CircularProgress, Alert, Tooltip, Divider, useMediaQuery,
|
||||||
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions
|
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
@@ -94,7 +95,51 @@ function RoleCell({ role }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mobile presentation of a user row — a self-contained card instead of a wide table row.
|
||||||
|
function UserCard({ row, index, onEdit, onDelete }) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 1.75, borderRadius: 3, border: 1, borderColor: 'divider', bgcolor: 'background.paper' }}>
|
||||||
|
<Stack direction="row" spacing={1.25} alignItems="flex-start">
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
|
<UserIdentity name={row.name} email={row.email} role={row.role} />
|
||||||
|
</Box>
|
||||||
|
<RoleCell role={row.role} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ mt: 1.75 }}>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center" sx={{ minWidth: 0 }}>
|
||||||
|
<MailOutlineIcon sx={{ fontSize: 16, color: 'grey.400', flexShrink: 0 }} />
|
||||||
|
{row.email ? (
|
||||||
|
<Link href={`mailto:${row.email}`} underline="hover" color="text.primary" noWrap sx={{ fontSize: '0.8125rem' }}>
|
||||||
|
{row.email}
|
||||||
|
</Link>
|
||||||
|
) : <Typography variant="body2" color="text.disabled">No email</Typography>}
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<PhoneOutlinedIcon sx={{ fontSize: 16, color: 'grey.400', flexShrink: 0 }} />
|
||||||
|
{row.phone ? (
|
||||||
|
<Link href={`tel:${row.phone}`} underline="hover" color="text.primary" sx={{ fontSize: '0.8125rem' }}>
|
||||||
|
{row.phone}
|
||||||
|
</Link>
|
||||||
|
) : <Typography variant="body2" color="text.disabled">No phone</Typography>}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 1.5 }} />
|
||||||
|
<Stack direction="row" spacing={1} justifyContent="space-between" alignItems="center">
|
||||||
|
<Typography variant="caption" color="text.secondary">#{index}</Typography>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Button size="small" variant="outlined" startIcon={<EditOutlinedIcon fontSize="small" />} onClick={() => onEdit(row)}>Edit</Button>
|
||||||
|
<Button size="small" variant="outlined" color="error" startIcon={<DeleteOutlineIcon fontSize="small" />} onClick={() => onDelete(row)}>Delete</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function TeamUsers() {
|
export default function TeamUsers() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -167,9 +212,9 @@ export default function TeamUsers() {
|
|||||||
title="App Users"
|
title="App Users"
|
||||||
breadcrumbs={[{ label: 'App Users' }]}
|
breadcrumbs={[{ label: 'App Users' }]}
|
||||||
action={
|
action={
|
||||||
<Stack direction="row" spacing={1.5}>
|
<Stack direction="row" spacing={1.5} sx={{ width: { xs: '100%', sm: 'auto' } }}>
|
||||||
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={load} disabled={loading}>Refresh</Button>
|
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={load} disabled={loading} sx={{ flex: { xs: 1, sm: 'none' } }}>Refresh</Button>
|
||||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setDialog({ open: true, mode: 'add', initial: null })}>Add User</Button>
|
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setDialog({ open: true, mode: 'add', initial: null })} sx={{ flex: { xs: 1, sm: 'none' } }}>Add User</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -177,7 +222,7 @@ export default function TeamUsers() {
|
|||||||
<Card sx={{ overflow: 'hidden' }}>
|
<Card sx={{ overflow: 'hidden' }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
px: 2.5, py: 2, borderBottom: 1, borderColor: 'divider',
|
px: { xs: 1.75, sm: 2.5 }, py: 2, borderBottom: 1, borderColor: 'divider',
|
||||||
display: 'flex', alignItems: 'center', gap: 1.5,
|
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||||
background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)`
|
background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)`
|
||||||
}}
|
}}
|
||||||
@@ -191,10 +236,10 @@ export default function TeamUsers() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
|
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: { xs: 1.5, sm: 2 } }} alignItems={{ md: 'center' }}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small" placeholder="Search by name, email, phone…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
size="small" placeholder="Search by name, email, phone…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
||||||
sx={{ minWidth: 300 }}
|
sx={{ width: { xs: '100%', md: 300 } }}
|
||||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
@@ -218,6 +263,23 @@ export default function TeamUsers() {
|
|||||||
|
|
||||||
{error && <Alert severity="error" sx={{ m: 2 }} action={<Button color="inherit" size="small" onClick={load}>Retry</Button>}>{error}</Alert>}
|
{error && <Alert severity="error" sx={{ m: 2 }} action={<Button color="inherit" size="small" onClick={load}>Retry</Button>}>{error}</Alert>}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}><CircularProgress /></Box>
|
||||||
|
) : paged.length === 0 ? (
|
||||||
|
<EmptyState title="No team users found" caption="Try a different role or search term, or add a user." />
|
||||||
|
) : isMobile ? (
|
||||||
|
<Stack spacing={1.25} sx={{ p: { xs: 1.5, sm: 2 } }}>
|
||||||
|
{paged.map((row, i) => (
|
||||||
|
<UserCard
|
||||||
|
key={row.id}
|
||||||
|
row={row}
|
||||||
|
index={page * rpp + i + 1}
|
||||||
|
onEdit={(r) => setDialog({ open: true, mode: 'edit', initial: r })}
|
||||||
|
onDelete={(r) => setToDelete(r)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table sx={{ minWidth: 800 }}>
|
<Table sx={{ minWidth: 800 }}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
@@ -231,20 +293,7 @@ export default function TeamUsers() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{paged.map((row, i) => (
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={6} sx={{ border: 'none' }}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}><CircularProgress /></Box>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : paged.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={6} sx={{ border: 'none' }}>
|
|
||||||
<EmptyState title="No team users found" caption="Try a different role or search term, or add a user." />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
paged.map((row, i) => (
|
|
||||||
<TableRow key={row.id} hover>
|
<TableRow key={row.id} hover>
|
||||||
<TableCell><Typography variant="body2" color="text.secondary">{page * rpp + i + 1}</Typography></TableCell>
|
<TableCell><Typography variant="body2" color="text.secondary">{page * rpp + i + 1}</Typography></TableCell>
|
||||||
<TableCell><UserIdentity name={row.name} email={row.email} role={row.role} /></TableCell>
|
<TableCell><UserIdentity name={row.name} email={row.email} role={row.role} /></TableCell>
|
||||||
@@ -268,11 +317,11 @@ export default function TeamUsers() {
|
|||||||
<Tooltip title="Delete"><IconButton size="small" color="error" onClick={() => setToDelete(row)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
|
<Tooltip title="Delete"><IconButton size="small" color="error" onClick={() => setToDelete(row)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
)}
|
||||||
<TablePagination
|
<TablePagination
|
||||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
|
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, TextField,
|
Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, TextField,
|
||||||
MenuItem, Alert, CircularProgress, IconButton
|
MenuItem, Alert, CircularProgress, IconButton, useMediaQuery
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import { createPoint, setPayload, COLLECTIONS } from '@/utils/qdrant';
|
import { createPoint, setPayload, COLLECTIONS } from '@/utils/qdrant';
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ const withValue = (opts, v) => (v && !opts.includes(v) ? [v, ...opts] : opts);
|
|||||||
|
|
||||||
export default function UserFormDialog({ open, mode, initial, onClose, onSaved }) {
|
export default function UserFormDialog({ open, mode, initial, onClose, onSaved }) {
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
|
const theme = useTheme();
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const [form, setForm] = useState(EMPTY);
|
const [form, setForm] = useState(EMPTY);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -56,7 +59,7 @@ export default function UserFormDialog({ open, mode, initial, onClose, onSaved }
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={saving ? undefined : onClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={saving ? undefined : onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
{isEdit ? 'Edit Team User' : 'Add Team User'}
|
{isEdit ? 'Edit Team User' : 'Add Team User'}
|
||||||
<IconButton onClick={onClose} size="small" disabled={saving}><CloseIcon /></IconButton>
|
<IconButton onClick={onClose} size="small" disabled={saving}><CloseIcon /></IconButton>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, TextField,
|
Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, TextField,
|
||||||
MenuItem, Box, Typography, Divider, Alert, CircularProgress, IconButton
|
MenuItem, Box, Typography, Divider, Alert, CircularProgress, IconButton, useMediaQuery
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import { createPoint, setPayload, COLLECTIONS } from '@/utils/qdrant';
|
import { createPoint, setPayload, COLLECTIONS } from '@/utils/qdrant';
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ const withValue = (opts, v) => (v && !opts.includes(v) ? [v, ...opts] : opts);
|
|||||||
|
|
||||||
export default function ClientFormDialog({ open, mode, initial, onClose, onSaved }) {
|
export default function ClientFormDialog({ open, mode, initial, onClose, onSaved }) {
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
|
const theme = useTheme();
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const [form, setForm] = useState(EMPTY);
|
const [form, setForm] = useState(EMPTY);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -89,7 +92,7 @@ export default function ClientFormDialog({ open, mode, initial, onClose, onSaved
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={saving ? undefined : onClose} maxWidth="md" fullWidth>
|
<Dialog open={open} onClose={saving ? undefined : onClose} maxWidth="md" fullWidth fullScreen={fullScreen}>
|
||||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
{isEdit ? 'Edit Client' : 'Add Client'}
|
{isEdit ? 'Edit Client' : 'Add Client'}
|
||||||
<IconButton onClick={onClose} size="small" disabled={saving}><CloseIcon /></IconButton>
|
<IconButton onClick={onClose} size="small" disabled={saving}><CloseIcon /></IconButton>
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import {
|
|||||||
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Grid,
|
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Grid,
|
||||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
||||||
TablePagination, Typography, Collapse, CircularProgress, Alert, Tooltip, Chip, Divider, Link,
|
TablePagination, Typography, Collapse, CircularProgress, Alert, Tooltip, Chip, Divider, Link,
|
||||||
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions
|
useMediaQuery, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
@@ -180,80 +181,9 @@ function Metric({ label, value, color = 'grey.800' }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClientRow({ row, index, onEdit, onDelete }) {
|
// The expandable detail panel — shared by the desktop table row and the mobile card.
|
||||||
const [open, setOpen] = useState(false);
|
function ClientDetail({ row, onEdit }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<TableRow
|
|
||||||
hover
|
|
||||||
sx={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
'& > *': { borderBottom: open ? 'unset' : undefined },
|
|
||||||
...(open && { bgcolor: 'primary.lighter', '&:hover': { bgcolor: 'primary.lighter' } })
|
|
||||||
}}
|
|
||||||
onClick={() => setOpen((o) => !o)}
|
|
||||||
>
|
|
||||||
<TableCell padding="checkbox">
|
|
||||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); setOpen((o) => !o); }}>
|
|
||||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ whiteSpace: 'nowrap' }}>
|
|
||||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 700, color: 'primary.main', bgcolor: 'primary.lighter', px: 1, py: 0.5, borderRadius: 1, border: '1px solid', borderColor: 'primary.light', whiteSpace: 'nowrap' }}>
|
|
||||||
{row.logicalId}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
|
||||||
<UserAvatar name={row.name} size={38} />
|
|
||||||
<Box sx={{ minWidth: 0 }}>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{row.name}</Typography>
|
|
||||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ mt: 0.4 }}>
|
|
||||||
{row.businessType && (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
label={titleCase(row.businessType)}
|
|
||||||
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600, bgcolor: 'grey.100', color: 'grey.700' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Stack direction="row" spacing={0.75} alignItems="center">
|
|
||||||
<PhoneOutlinedIcon sx={{ fontSize: 15, color: 'grey.400' }} />
|
|
||||||
<Box>
|
|
||||||
<Typography variant="body2">{row.phone || '—'}</Typography>
|
|
||||||
{row.frequency && <Typography variant="caption" color="text.secondary">{row.frequency}</Typography>}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Stack direction="row" spacing={0.75} alignItems="center">
|
|
||||||
<PlaceOutlinedIcon sx={{ fontSize: 15, color: 'grey.400' }} />
|
|
||||||
<Box>
|
|
||||||
<Typography variant="body2">{row.city || '—'}{row.businessState ? `, ${row.businessState}` : ''}</Typography>
|
|
||||||
{row.neighbourhood && <Typography variant="caption" color="text.secondary">{row.neighbourhood}</Typography>}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Stack direction="row" spacing={1.5} alignItems="center" justifyContent="center" divider={<Divider orientation="vertical" flexItem />}>
|
|
||||||
<Metric label="Parcels" value={Number(row.parcelVolume).toLocaleString()} />
|
|
||||||
<Metric label="Contracts" value={row.activeContracts} color="primary.main" />
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell><StatusChip status={row.status} /></TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<Tooltip title="Edit"><IconButton size="small" onClick={(e) => { e.stopPropagation(); onEdit(row); }}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
|
||||||
<Tooltip title="Delete"><IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(row); }}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={8} sx={{ py: 0, borderBottom: open ? undefined : 'none' }}>
|
|
||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
|
||||||
<Box sx={{ m: 2, borderRadius: 2.5, border: 1, borderColor: 'divider', overflow: 'hidden', boxShadow: '0 4px 16px rgba(0,0,0,0.06)' }}>
|
<Box sx={{ m: 2, borderRadius: 2.5, border: 1, borderColor: 'divider', overflow: 'hidden', boxShadow: '0 4px 16px rgba(0,0,0,0.06)' }}>
|
||||||
<Stack
|
<Stack
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
@@ -276,10 +206,10 @@ function ClientRow({ row, index, onEdit, onDelete }) {
|
|||||||
</Box>
|
</Box>
|
||||||
<StatusChip status={row.status} sx={{ ml: 0.5 }} />
|
<StatusChip status={row.status} sx={{ ml: 0.5 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Button size="small" variant="contained" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(row)}>Edit Client</Button>
|
<Button size="small" variant="contained" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(row)} sx={{ width: { xs: '100%', sm: 'auto' } }}>Edit Client</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Box sx={{ p: 2.5, bgcolor: 'grey.50' }}>
|
<Box sx={{ p: { xs: 1.5, sm: 2.5 }, bgcolor: 'grey.50' }}>
|
||||||
<Grid container spacing={2.5} alignItems="stretch">
|
<Grid container spacing={2.5} alignItems="stretch">
|
||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<SectionCard icon={StorefrontOutlinedIcon} title="Business" accent="primary">
|
<SectionCard icon={StorefrontOutlinedIcon} title="Business" accent="primary">
|
||||||
@@ -393,6 +323,135 @@ function ClientRow({ row, index, onEdit, onDelete }) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile presentation of a client — summary card with an expandable detail panel.
|
||||||
|
function ClientCard({ row, onEdit, onDelete }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Box sx={{ borderRadius: 3, border: 1, borderColor: open ? 'primary.light' : 'divider', bgcolor: 'background.paper', overflow: 'hidden' }}>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||||
|
<UserAvatar name={row.name} size={42} />
|
||||||
|
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 700, color: 'grey.800' }} noWrap>{row.name}</Typography>
|
||||||
|
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 700, color: 'primary.main' }}>{row.logicalId}</Typography>
|
||||||
|
</Box>
|
||||||
|
<StatusChip status={row.status} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ mt: 1.75 }}>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<PhoneOutlinedIcon sx={{ fontSize: 16, color: 'grey.400', flexShrink: 0 }} />
|
||||||
|
<Typography variant="body2">{row.phone || '—'}</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<PlaceOutlinedIcon sx={{ fontSize: 16, color: 'grey.400', flexShrink: 0 }} />
|
||||||
|
<Typography variant="body2" noWrap>{row.city || '—'}{row.businessState ? `, ${row.businessState}` : ''}</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1.5} alignItems="center" divider={<Divider orientation="vertical" flexItem />} sx={{ mt: 1.75 }}>
|
||||||
|
<Metric label="Parcels" value={Number(row.parcelVolume).toLocaleString()} />
|
||||||
|
<Metric label="Contracts" value={row.activeContracts} color="primary.main" />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 1.5 }} />
|
||||||
|
<Stack direction="row" spacing={1} justifyContent="space-between" alignItems="center">
|
||||||
|
<Button size="small" onClick={() => setOpen((o) => !o)} endIcon={open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}>
|
||||||
|
{open ? 'Hide' : 'Details'}
|
||||||
|
</Button>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Button size="small" variant="outlined" startIcon={<EditOutlinedIcon fontSize="small" />} onClick={() => onEdit(row)}>Edit</Button>
|
||||||
|
<Button size="small" variant="outlined" color="error" startIcon={<DeleteOutlineIcon fontSize="small" />} onClick={() => onDelete(row)}>Delete</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
<Box sx={{ borderTop: 1, borderColor: 'divider' }}>
|
||||||
|
<ClientDetail row={row} onEdit={onEdit} />
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClientRow({ row, index, onEdit, onDelete }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TableRow
|
||||||
|
hover
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
'& > *': { borderBottom: open ? 'unset' : undefined },
|
||||||
|
...(open && { bgcolor: 'primary.lighter', '&:hover': { bgcolor: 'primary.lighter' } })
|
||||||
|
}}
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
>
|
||||||
|
<TableCell padding="checkbox">
|
||||||
|
<IconButton size="small" onClick={(e) => { e.stopPropagation(); setOpen((o) => !o); }}>
|
||||||
|
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ whiteSpace: 'nowrap' }}>
|
||||||
|
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 700, color: 'primary.main', bgcolor: 'primary.lighter', px: 1, py: 0.5, borderRadius: 1, border: '1px solid', borderColor: 'primary.light', whiteSpace: 'nowrap' }}>
|
||||||
|
{row.logicalId}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||||
|
<UserAvatar name={row.name} size={38} />
|
||||||
|
<Box sx={{ minWidth: 0 }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{row.name}</Typography>
|
||||||
|
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ mt: 0.4 }}>
|
||||||
|
{row.businessType && (
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={titleCase(row.businessType)}
|
||||||
|
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600, bgcolor: 'grey.100', color: 'grey.700' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" spacing={0.75} alignItems="center">
|
||||||
|
<PhoneOutlinedIcon sx={{ fontSize: 15, color: 'grey.400' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2">{row.phone || '—'}</Typography>
|
||||||
|
{row.frequency && <Typography variant="caption" color="text.secondary">{row.frequency}</Typography>}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" spacing={0.75} alignItems="center">
|
||||||
|
<PlaceOutlinedIcon sx={{ fontSize: 15, color: 'grey.400' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2">{row.city || '—'}{row.businessState ? `, ${row.businessState}` : ''}</Typography>
|
||||||
|
{row.neighbourhood && <Typography variant="caption" color="text.secondary">{row.neighbourhood}</Typography>}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" spacing={1.5} alignItems="center" justifyContent="center" divider={<Divider orientation="vertical" flexItem />}>
|
||||||
|
<Metric label="Parcels" value={Number(row.parcelVolume).toLocaleString()} />
|
||||||
|
<Metric label="Contracts" value={row.activeContracts} color="primary.main" />
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell><StatusChip status={row.status} /></TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Tooltip title="Edit"><IconButton size="small" onClick={(e) => { e.stopPropagation(); onEdit(row); }}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||||
|
<Tooltip title="Delete"><IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(row); }}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} sx={{ py: 0, borderBottom: open ? undefined : 'none' }}>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
<ClientDetail row={row} onEdit={onEdit} />
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -401,6 +460,8 @@ function ClientRow({ row, index, onEdit, onDelete }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Tenants() {
|
export default function Tenants() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [clients, setClients] = useState([]);
|
const [clients, setClients] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -489,9 +550,9 @@ export default function Tenants() {
|
|||||||
title="Clients"
|
title="Clients"
|
||||||
breadcrumbs={[{ label: 'Clients' }]}
|
breadcrumbs={[{ label: 'Clients' }]}
|
||||||
action={
|
action={
|
||||||
<Stack direction="row" spacing={1.5}>
|
<Stack direction="row" spacing={1.5} sx={{ width: { xs: '100%', sm: 'auto' } }}>
|
||||||
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={load} disabled={loading}>Refresh</Button>
|
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={load} disabled={loading} sx={{ flex: { xs: 1, sm: 'none' } }}>Refresh</Button>
|
||||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setDialog({ open: true, mode: 'add', initial: null })}>Add Client</Button>
|
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setDialog({ open: true, mode: 'add', initial: null })} sx={{ flex: { xs: 1, sm: 'none' } }}>Add Client</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -523,7 +584,7 @@ export default function Tenants() {
|
|||||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
|
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small" placeholder="Search by name, phone, city, ID…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
size="small" placeholder="Search by name, phone, city, ID…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
||||||
sx={{ minWidth: 300 }}
|
sx={{ width: { xs: '100%', md: 300 } }}
|
||||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
@@ -547,6 +608,22 @@ export default function Tenants() {
|
|||||||
|
|
||||||
{error && <Alert severity="error" sx={{ m: 2 }} action={<Button color="inherit" size="small" onClick={load}>Retry</Button>}>{error}</Alert>}
|
{error && <Alert severity="error" sx={{ m: 2 }} action={<Button color="inherit" size="small" onClick={load}>Retry</Button>}>{error}</Alert>}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}><CircularProgress /></Box>
|
||||||
|
) : paged.length === 0 ? (
|
||||||
|
<EmptyState title="No clients found" caption="Try a different tab or search term, or add a client." />
|
||||||
|
) : isMobile ? (
|
||||||
|
<Stack spacing={1.5} sx={{ p: 2 }}>
|
||||||
|
{paged.map((row) => (
|
||||||
|
<ClientCard
|
||||||
|
key={row.id}
|
||||||
|
row={row}
|
||||||
|
onEdit={(r) => setDialog({ open: true, mode: 'edit', initial: r })}
|
||||||
|
onDelete={(r) => setToDelete(r)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table sx={{ minWidth: 900 }}>
|
<Table sx={{ minWidth: 900 }}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
@@ -562,20 +639,7 @@ export default function Tenants() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{paged.map((row, i) => (
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={8} sx={{ border: 'none' }}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}><CircularProgress /></Box>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : paged.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={8} sx={{ border: 'none' }}>
|
|
||||||
<EmptyState title="No clients found" caption="Try a different tab or search term, or add a client." />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
paged.map((row, i) => (
|
|
||||||
<ClientRow
|
<ClientRow
|
||||||
key={row.id}
|
key={row.id}
|
||||||
row={row}
|
row={row}
|
||||||
@@ -583,11 +647,11 @@ export default function Tenants() {
|
|||||||
onEdit={(r) => setDialog({ open: true, mode: 'edit', initial: r })}
|
onEdit={(r) => setDialog({ open: true, mode: 'edit', initial: r })}
|
||||||
onDelete={(r) => setToDelete(r)}
|
onDelete={(r) => setToDelete(r)}
|
||||||
/>
|
/>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
)}
|
||||||
<TablePagination
|
<TablePagination
|
||||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
|
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
|
||||||
|
|||||||
Reference in New Issue
Block a user