Revert "updates on the ui changes"

This reverts commit 298fc603a0.
This commit is contained in:
2026-07-14 12:13:46 +05:30
parent 298fc603a0
commit d2f264460a
44 changed files with 4661 additions and 7053 deletions

View File

@@ -1,24 +1,18 @@
import { useState, useEffect, useMemo, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import { useState, useEffect, useMemo } from 'react';
import {
Building2,
UserPlus,
Package,
Handshake,
History,
PieChart,
Layers,
Users,
ArrowRight,
Calendar
} from 'lucide-react';
import { Button } from '@astryxdesign/core/Button';
import { Card } from '@astryxdesign/core/Card';
import { Table, proportional, pixel } from '@astryxdesign/core/Table';
import { Badge } from '@astryxdesign/core/Badge';
import { Text, Heading } from '@astryxdesign/core/Text';
import { Banner } from '@astryxdesign/core/Banner';
Grid, Card, Box, Stack, Typography, Button, Divider, LinearProgress, CircularProgress, Alert,
Table, TableBody, TableCell, TableHead, TableRow, TableContainer, useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import RefreshIcon from '@mui/icons-material/Refresh';
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 HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined';
import DonutLargeOutlinedIcon from '@mui/icons-material/DonutLargeOutlined';
import CategoryOutlinedIcon from '@mui/icons-material/CategoryOutlined';
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
@@ -36,86 +30,50 @@ const BAR_COLORS = ['#C01227', '#00A2AE', '#00A854', '#FFBF00', '#9E0E20', '#8C8
function Panel({ icon: Icon, title, action, color = 'primary', noPadding = false, children }) {
return (
<Card
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
borderRadius: '16px',
border: '1px solid rgba(5, 54, 89, 0.08)',
backgroundColor: '#ffffff',
overflow: 'hidden',
boxShadow: '0 10px 30px -5px rgba(0,0,0,0.02), 0 4px 12px -3px rgba(0,0,0,0.02)',
boxSizing: 'border-box'
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
padding: '10px 16px',
borderBottom: '1px solid rgba(5, 54, 89, 0.06)',
background: 'linear-gradient(90deg, rgba(192, 18, 39, 0.03) 0%, rgba(255, 255, 255, 0) 100%)',
gap: '10px',
flexWrap: 'wrap'
<Card sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
borderRadius: 4,
boxShadow: '0 10px 30px rgba(0,0,0,0.03)',
border: '1px solid rgba(0,0,0,0.04)'
}}>
<Stack
direction="row" spacing={1.5} alignItems="center"
sx={{
px: { xs: 2, sm: 3 }, py: 2, borderBottom: 1, borderColor: 'divider',
background: (theme) => `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, transparent 100%)`
}}
>
<div
style={{
width: '30px',
height: '30px',
borderRadius: '7px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(192, 18, 39, 0.08)',
color: '#C01227',
flexShrink: 0
}}
>
<Icon size={15} />
</div>
<div style={{ flexGrow: 1, minWidth: 0 }}>
<Heading level={5}>{title}</Heading>
</div>
<Box sx={{ width: 40, height: 40, borderRadius: 2, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: `${color}.lighter`, color: `${color}.main`, boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.05)' }}>
<Icon fontSize="small" />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: 'grey.800', flexGrow: 1, letterSpacing: '-0.3px' }}>{title}</Typography>
{action}
</div>
<div style={{ padding: noPadding ? '0' : '16px', flexGrow: 1, boxSizing: 'border-box' }}>
{children}
</div>
</Stack>
<Box sx={{ p: noPadding ? 0 : { xs: 2, sm: 3 }, flexGrow: 1 }}>{children}</Box>
</Card>
);
}
export default function Dashboard() {
const navigate = useNavigate();
const [selectedDate, setSelectedDate] = useState(() => dayjs().format('YYYY-MM-DD'));
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [clients, setClients] = useState([]);
const [team, setTeam] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Guards against out-of-order responses (e.g. React StrictMode's double-invoked
// effect) so a stale request can never clobber a newer one's result.
const loadRequestId = useRef(0);
const load = () => {
const requestId = ++loadRequestId.current;
setLoading(true);
setError(null);
Promise.all([fetchClients(), fetchUsers()])
.then(([cs, us]) => {
if (loadRequestId.current !== requestId) return;
setClients((cs || []).map(toClient));
setTeam((us || []).map(toUser));
})
.catch((e) => {
if (loadRequestId.current === requestId) setError(e.message || 'Failed to load dashboard data');
})
.finally(() => {
if (loadRequestId.current === requestId) setLoading(false);
});
.catch((e) => setError(e.message || 'Failed to load dashboard data'))
.finally(() => setLoading(false));
};
useEffect(() => { load(); }, []);
@@ -145,273 +103,148 @@ export default function Dashboard() {
[clients]
);
const recentColumns = useMemo(() => [
{
key: 'name',
header: <div style={{ paddingLeft: '24px' }}>Client</div>,
width: proportional(2),
renderCell: (c) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', paddingLeft: '24px' }}>
<UserAvatar name={c.name} size={32} />
<Text type="body" weight="semibold">{c.name}</Text>
</div>
)
},
{
key: 'businessType',
header: 'Type',
width: proportional(1),
renderCell: (c) => c.businessType
? <Badge variant="neutral" label={titleCase(c.businessType)} />
: <Text type="supporting" color="disabled"></Text>
},
{
key: 'location',
header: 'Location',
width: proportional(1),
renderCell: (c) => (
<Text type="body">{`${c.city || '—'}${c.businessState ? `, ${c.businessState}` : ''}`}</Text>
)
},
{
key: 'parcelVolume',
header: 'Parcels',
width: pixel(110),
align: 'end',
renderCell: (c) => <Text type="body" weight="semibold" hasTabularNumbers>{c.parcelVolume.toLocaleString('en-IN')}</Text>
},
{
key: 'status',
header: 'Status',
width: pixel(140),
renderCell: (c) => <StatusChip status={c.status} />
}
], []);
const datePickerAction = (
<input
type="date"
className="dashboard-date-picker"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
style={{
padding: '8px 12px',
borderRadius: '10px',
border: '1px solid rgba(5, 54, 89, 0.12)',
fontSize: '0.875rem',
fontWeight: 500,
fontFamily: 'inherit',
color: '#1e293b',
backgroundColor: '#ffffff',
outline: 'none',
cursor: 'pointer',
transition: 'all 0.2s ease',
boxShadow: '0 1px 2px rgba(15, 23, 42, 0.04)',
width: '150px'
}}
onFocus={(e) => {
e.target.style.borderColor = '#C01227';
e.target.style.boxShadow = '0 0 0 3px rgba(192, 18, 39, 0.12)';
}}
onBlur={(e) => {
e.target.style.borderColor = 'rgba(5, 54, 89, 0.12)';
e.target.style.boxShadow = '0 1px 2px rgba(15, 23, 42, 0.04)';
}}
/>
);
const today = new Date().toLocaleDateString('en-IN', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
if (loading) {
return (
<div style={{ fontFamily: 'system-ui, sans-serif' }}>
<PageHeader title="Dashboard" action={datePickerAction} />
<div style={{ display: 'flex', justifyContent: 'center', padding: '96px 0' }}>
<div className="spinner" style={{ width: '40px', height: '40px', border: '4px solid #f1f5f9', borderTop: '4px solid #C01227', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
<style>{`@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`}</style>
</div>
</div>
<>
<PageHeader title="Dashboard" breadcrumbs={[{ label: 'Dashboard' }]} />
<Box sx={{ display: 'flex', justifyContent: 'center', py: 12 }}><CircularProgress /></Box>
</>
);
}
return (
<div style={{ fontFamily: 'system-ui, sans-serif' }}>
<PageHeader title="Dashboard" action={datePickerAction} />
<>
<PageHeader
title="Dashboard"
breadcrumbs={[{ label: 'Dashboard' }]}
action={<Button variant="outlined" startIcon={<RefreshIcon />} onClick={load}>Refresh</Button>}
/>
{error && (
<div style={{ marginBottom: '24px' }}>
<Banner
status="error"
title={error}
endContent={<Button label="Retry" variant="ghost" size="sm" onClick={load} />}
/>
</div>
)}
{error && <Alert severity="error" sx={{ mb: 2.5 }} action={<Button color="inherit" size="small" onClick={load}>Retry</Button>}>{error}</Alert>}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '12px', marginBottom: '16px' }}>
<StatCard size="sm" title="Total Clients" value={stats.total} icon={Building2} color="primary" />
<StatCard size="sm" title="New Clients" value={stats.newCount} icon={UserPlus} color="info" />
<StatCard size="sm" title="Total Parcel Volume" value={stats.parcels.toLocaleString('en-IN')} icon={Package} color="success" />
<StatCard size="sm" title="Active Contracts" value={stats.contracts} icon={Handshake} color="warning" />
</div>
{/* Main dashboard panels */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '16px', marginBottom: '16px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', lg: 'repeat(12, 1fr)', gap: '16px' }} className="responsive-row">
{/* Recent Clients */}
<div style={{ gridColumn: 'span 8' }} className="col-span-8">
<Panel
icon={History}
title="Recent Clients"
color="primary"
noPadding
action={recent.length > 0 && (
<Button
label="View all"
variant="ghost"
size="sm"
icon={<ArrowRight size={14} />}
onClick={() => navigate('/clients')}
/>
)}
>
{recent.length === 0 ? (
<EmptyState icon={Building2} title="No clients yet" caption="Add a client under the Clients menu to see it here." />
) : (
<div style={{ paddingTop: '8px', paddingBottom: '8px' }}>
<Table
data={recent}
columns={recentColumns}
idKey="id"
density="compact"
dividers="rows"
hasHover
isStriped
textOverflow="truncate"
/>
</div>
)}
</Panel>
</div>
{/* Clients by Status */}
<div style={{ gridColumn: 'span 4' }} className="col-span-4">
<Panel icon={PieChart} title="Clients by Status" color="primary">
<div style={{ display: 'flex', justifyContent: 'center', padding: '4px 0' }}>
{statusData.length === 0 ? (
<EmptyState icon={PieChart} title="No status breakdown" caption="No client status distribution available." />
) : (
<DonutChart data={statusData} size={140} thickness={20} centerValue={stats.total} centerLabel="Clients" />
)}
</div>
</Panel>
</div>
</div>
<Grid container spacing={3}>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Total Clients" value={stats.total} icon={ApartmentOutlinedIcon} color="primary" caption="All registered" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="New Clients" value={stats.newCount} icon={FiberNewOutlinedIcon} color="primary" caption="Awaiting onboarding" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Total Parcel Volume" value={stats.parcels.toLocaleString('en-IN')} icon={Inventory2OutlinedIcon} color="primary" caption="Across all clients" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Active Contracts" value={stats.contracts} icon={HandshakeOutlinedIcon} color="primary" caption="Currently running" /></Grid>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', lg: 'repeat(12, 1fr)', gap: '16px' }} className="responsive-row">
{/* Business Type bar chart */}
<div style={{ gridColumn: 'span 8' }} className="col-span-8">
<Panel icon={Layers} title="Clients by Business Type" color="primary">
{byType.length === 0 ? (
<EmptyState icon={Layers} title="No business types" caption="No business type distribution available." />
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{byType.map(([type, count], i) => (
<div key={type}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '10px', height: '10px', borderRadius: '3px', backgroundColor: BAR_COLORS[i % BAR_COLORS.length] }} />
<Text type="body" weight="semibold">{titleCase(type)}</Text>
</div>
<Text type="body" color="secondary" hasTabularNumbers>
{count} · {Math.round((count / clients.length) * 100)}%
</Text>
</div>
<div style={{ height: '6px', borderRadius: '3px', backgroundColor: '#f1f5f9', width: '100%', overflow: 'hidden' }}>
<div style={{ height: '100%', borderRadius: '3px', backgroundColor: BAR_COLORS[i % BAR_COLORS.length], width: `${(count / maxType) * 100}%` }} />
</div>
</div>
<Grid item xs={12} lg={8}>
<Panel icon={HistoryOutlinedIcon} title="Recent Clients" color="primary" noPadding>
{recent.length === 0 ? (
<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>
<Table sx={{ minWidth: 600 }}>
<TableHead>
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', textTransform: 'uppercase', fontSize: '0.72rem', letterSpacing: 0.4 } }}>
<TableCell>Client</TableCell>
<TableCell>Type</TableCell>
<TableCell>Location</TableCell>
<TableCell align="right">Parcels</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{recent.map((c) => (
<TableRow key={c.id} hover>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={c.name} size={32} />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{c.name}</Typography>
</Stack>
</TableCell>
<TableCell><Typography variant="body2">{titleCase(c.businessType) || '—'}</Typography></TableCell>
<TableCell><Typography variant="body2">{c.city || '—'}{c.businessState ? `, ${c.businessState}` : ''}</Typography></TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{c.parcelVolume.toLocaleString('en-IN')}</TableCell>
<TableCell><StatusChip status={c.status} /></TableCell>
</TableRow>
))}
</div>
)}
</Panel>
</div>
</TableBody>
</Table>
</TableContainer>
)}
</Panel>
</Grid>
{/* App Users */}
<div style={{ gridColumn: 'span 4' }} className="col-span-4">
<Panel
icon={Users}
title="App Users"
color="primary"
>
{team.length === 0 ? (
<EmptyState title="No team users" />
) : (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{team.slice(0, 6).map((u) => (
<div
key={u.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '8px 12px',
margin: '1px -12px',
borderRadius: '10px',
transition: 'background-color 0.2s ease',
cursor: 'pointer'
}}
className="team-row-hover"
>
<UserAvatar name={u.name} size={30} />
<div style={{ flexGrow: 1, minWidth: 0 }}>
<Text type="body" weight="semibold" maxLines={1}>{u.name}</Text>
<Text type="supporting" color="secondary" maxLines={1}>{u.email}</Text>
</div>
<StatusChip status={u.role} />
</div>
))}
</div>
)}
</Panel>
</div>
</div>
</div>
<Grid item xs={12} lg={4}>
<Panel icon={DonutLargeOutlinedIcon} title="Clients by Status" color="primary">
<Box sx={{ py: 1.5 }}>
{statusData.length === 0
? <EmptyState title="No data" />
: <DonutChart data={statusData} centerValue={stats.total} centerLabel="Clients" />}
</Box>
</Panel>
</Grid>
<style>{`
@media (min-width: 1024px) {
.responsive-row {
grid-template-columns: repeat(12, 1fr) !important;
}
.col-span-8 {
grid-column: span 8 !important;
}
.col-span-4 {
grid-column: span 4 !important;
}
}
@media (max-width: 1023px) {
.responsive-row {
grid-template-columns: 1fr !important;
}
.col-span-8, .col-span-4 {
grid-column: span 1 !important;
}
}
/* Stat card hover transition effects */
.stat-card {
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08) !important;
border-color: rgba(192, 18, 39, 0.2) !important;
}
<Grid item xs={12} lg={8}>
<Panel icon={CategoryOutlinedIcon} title="Clients by Business Type" color="primary">
{byType.length === 0 ? (
<EmptyState title="No data" />
) : (
<Stack spacing={2.25}>
{byType.map(([type, count], i) => (
<Box key={type}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.75 }}>
<Stack direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: BAR_COLORS[i % BAR_COLORS.length] }} />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{titleCase(type)}</Typography>
</Stack>
<Typography variant="body2" color="text.secondary">
{count} · {Math.round((count / clients.length) * 100)}%
</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={(count / maxType) * 100}
sx={{ height: 8, borderRadius: 4, bgcolor: 'grey.100', '& .MuiLinearProgress-bar': { borderRadius: 4, backgroundColor: BAR_COLORS[i % BAR_COLORS.length] } }}
/>
</Box>
))}
</Stack>
)}
</Panel>
</Grid>
/* Team member row hover polish */
.team-row-hover:hover {
background-color: rgba(192, 18, 39, 0.04) !important;
}
`}</style>
</div>
<Grid item xs={12} lg={4}>
<Panel icon={GroupsOutlinedIcon} title={`App Users · ${team.length}`} color="primary">
{team.length === 0 ? (
<EmptyState title="No team users" />
) : (
<Stack divider={<Divider />} spacing={0}>
{team.slice(0, 6).map((u) => (
<Stack key={u.id} direction="row" spacing={1.5} alignItems="center" sx={{ py: 1.25 }}>
<UserAvatar name={u.name} size={36} />
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{u.name}</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{u.email}</Typography>
</Box>
<StatusChip status={u.role} />
</Stack>
))}
</Stack>
)}
</Panel>
</Grid>
</Grid>
</>
);
}