updates on the api integration and the design changes on the whole website
This commit is contained in:
@@ -24,8 +24,20 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { MdPeopleAlt, MdOutlinePeopleAlt, MdEdit, MdPhone, MdMail, MdOutlineMailOutline, MdOutlinePhoneAndroid } from 'react-icons/md';
|
||||
import {
|
||||
MdPeopleAlt,
|
||||
MdOutlinePeopleAlt,
|
||||
MdEdit,
|
||||
MdPhone,
|
||||
MdMail,
|
||||
MdOutlineMailOutline,
|
||||
MdOutlinePhoneAndroid,
|
||||
MdPersonAddAlt1,
|
||||
MdOutlineReceiptLong
|
||||
} from 'react-icons/md';
|
||||
import { FaRegEdit } from 'react-icons/fa';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import Loader from 'components/Loader';
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
@@ -63,6 +75,8 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
const tint = (c) => a(c, '08');
|
||||
|
||||
const AccentAvatar = ({ color, size = 24, children }) => (
|
||||
<Avatar sx={{ width: size, height: size, bgcolor: soft(color), color }}>{children}</Avatar>
|
||||
@@ -77,6 +91,7 @@ const custBookings = (row) => row.totalbookings ?? 0;
|
||||
const Customers = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
@@ -96,7 +111,8 @@ const Customers = () => {
|
||||
const stats = useMemo(() => {
|
||||
const total = customers.length;
|
||||
const withEmail = customers.filter((c) => c.email).length;
|
||||
return { total, withEmail };
|
||||
const totalBookings = customers.reduce((sum, c) => sum + (Number(c.totalbookings) || 0), 0);
|
||||
return { total, withEmail, totalBookings };
|
||||
}, [customers]);
|
||||
|
||||
const opentoast = (message, variant = 'error') =>
|
||||
@@ -129,7 +145,23 @@ const Customers = () => {
|
||||
<>
|
||||
{isLoading && <Loader />}
|
||||
|
||||
<PageHeader title="Customers" subtitle="Live · B2C Directory" live />
|
||||
<PageHeader
|
||||
title="Customers"
|
||||
subtitle="Live · B2C Directory"
|
||||
live
|
||||
action={
|
||||
<Tooltip title="Adds a contact to your tenant's own customer book (used when creating orders) — separate from the order-derived B2C directory below">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<MdPersonAddAlt1 size={16} />}
|
||||
onClick={() => navigate('/doormile/customer/create')}
|
||||
sx={{ bgcolor: BRAND, '&:hover': { bgcolor: BRAND } }}
|
||||
>
|
||||
Add Customer
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||
<Grid item xs={6} md={3}>
|
||||
@@ -138,6 +170,9 @@ const Customers = () => {
|
||||
<Grid item xs={6} md={3}>
|
||||
<StatCard title="With Email" value={stats.withEmail} icon={<MdOutlineMailOutline size={20} />} color="#0ea5e9" />
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
<StatCard title="Total Bookings" value={stats.totalBookings} icon={<MdOutlineReceiptLong size={20} />} color="#10b981" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Paper
|
||||
@@ -154,9 +189,19 @@ const Customers = () => {
|
||||
}}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} alignItems={{ xs: 'stretch', sm: 'center' }} justifyContent="space-between" spacing={1.25}>
|
||||
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
|
||||
{customers.length} total · {rows.length} shown
|
||||
</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdPeopleAlt size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}>
|
||||
Directory
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
|
||||
{customers.length} total · {rows.length} shown
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box sx={{ width: { xs: '100%', sm: 280 } }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
@@ -166,11 +211,11 @@ const Customers = () => {
|
||||
sx={{
|
||||
m: 0,
|
||||
width: '100%',
|
||||
borderRadius: DT.radiusField + 'px',
|
||||
bgcolor: DT.surface,
|
||||
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: DT.borderHover },
|
||||
'&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 1.5 },
|
||||
borderRadius: 999,
|
||||
bgcolor: tint(BRAND),
|
||||
'& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 },
|
||||
'&:hover fieldset': { borderColor: BRAND },
|
||||
'&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||
}}
|
||||
/>
|
||||
@@ -207,15 +252,20 @@ const Customers = () => {
|
||||
accent={BRAND}
|
||||
header={
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
|
||||
<AccentAvatar color={BRAND} size={36}>
|
||||
<MdPeopleAlt size={18} />
|
||||
</AccentAvatar>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }} noWrap>
|
||||
{custName(row)}
|
||||
</Typography>
|
||||
<Stack sx={{ minWidth: 0 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }} noWrap>
|
||||
{custName(row)}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{custId(row) ?? '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<IconButton size="small" onClick={() => openEdit(row)} sx={{ color: BRAND }}>
|
||||
<IconButton size="small" onClick={() => openEdit(row)} sx={{ color: BRAND, flexShrink: 0 }}>
|
||||
<MdEdit size={14} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
@@ -256,9 +306,8 @@ const Customers = () => {
|
||||
}}
|
||||
>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Phone</TableCell>
|
||||
<TableCell>Email</TableCell>
|
||||
<TableCell>Customer</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell align="center">Bookings</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
@@ -267,7 +316,7 @@ const Customers = () => {
|
||||
{isLoading && <OrdersTableSkeleton col={4} />}
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ py: 6 }}>
|
||||
<TableCell colSpan={5} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdPeopleAlt size={28} />
|
||||
@@ -284,25 +333,42 @@ const Customers = () => {
|
||||
key={custId(row) || index}
|
||||
sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` }, '&:hover': { backgroundColor: DT.surfaceAlt } }}
|
||||
>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdPeopleAlt size={16} />
|
||||
</AccentAvatar>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{custName(row)}
|
||||
</Typography>
|
||||
<Stack>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{custName(row)}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{custId(row) ?? '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdPhone size={13} color={DT.textMuted} /> <Typography variant="body2">{custPhone(row)}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdMail size={13} color={DT.textMuted} /> <Typography variant="body2">{custEmail(row)}</Typography>
|
||||
<Stack>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdPhone size={12} color={DT.textMuted} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{custPhone(row)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{row.email && (
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdMail size={12} color={DT.textMuted} />
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.email}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="center">{custBookings(row)}</TableCell>
|
||||
@@ -322,31 +388,69 @@ const Customers = () => {
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Dialog open={!!editRow} onClose={() => setEditRow(null)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 700 }}>Edit Customer</DialogTitle>
|
||||
<Dialog open={!!editRow} onClose={() => setEditRow(null)} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
|
||||
color: '#fff',
|
||||
py: 2
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 36, height: 36, bgcolor: 'rgba(255,255,255,0.2)', color: '#fff' }}>
|
||||
<FaRegEdit style={{ fontSize: 18 }} />
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography sx={{ fontSize: 11, fontWeight: 700, opacity: 0.85, letterSpacing: 0.6, textTransform: 'uppercase', lineHeight: 1 }}>
|
||||
Customer
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 800, fontSize: '1.1rem', lineHeight: 1.2, mt: 0.25 }}>
|
||||
Edit {editRow ? custName(editRow) : 'Customer'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
<Stack spacing={2} sx={{ mt: 2 }}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Name</InputLabel>
|
||||
<TextField value={form.name || ''} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} />
|
||||
<TextField fullWidth value={form.name || ''} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} />
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Phone</InputLabel>
|
||||
<TextField
|
||||
value={form.phone || ''}
|
||||
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
||||
InputProps={{ startAdornment: <MdOutlinePhoneAndroid size={14} style={{ marginRight: 6, color: DT.textMuted }} /> }}
|
||||
/>
|
||||
<InputLabel>Phone Number</InputLabel>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<TextField value="+91" disabled sx={{ width: 64, cursor: 'not-allowed' }} />
|
||||
<TextField
|
||||
fullWidth
|
||||
value={form.phone || ''}
|
||||
inputProps={{ maxLength: 10, inputMode: 'numeric', pattern: '[0-9]*' }}
|
||||
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value.replace(/\D/g, '') }))}
|
||||
InputProps={{ startAdornment: <MdOutlinePhoneAndroid size={14} style={{ marginRight: 6, color: DT.textMuted }} /> }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Email</InputLabel>
|
||||
<TextField value={form.email || ''} onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))} />
|
||||
<TextField
|
||||
fullWidth
|
||||
type="email"
|
||||
value={form.email || ''}
|
||||
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
||||
InputProps={{ startAdornment: <MdMail size={14} style={{ marginRight: 6, color: DT.textMuted }} /> }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setEditRow(null)}>Cancel</Button>
|
||||
<Button variant="contained" onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending} sx={{ bgcolor: BRAND }}>
|
||||
<Button onClick={() => setEditRow(null)} sx={{ color: DT.textSecondary }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
sx={{ bgcolor: BRAND, '&:hover': { bgcolor: BRAND } }}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
Reference in New Issue
Block a user