feat: implement ClientsOverview dashboard with audit summary visualization and data filtering
This commit is contained in:
246
src/pages/clients/ClientsOverview.tsx
Normal file
246
src/pages/clients/ClientsOverview.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { Box, Typography, Paper, TextField, Button, CircularProgress, FormControl, Select, MenuItem } from '@mui/material';
|
||||
import {
|
||||
ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
|
||||
} from 'recharts';
|
||||
import { accountService } from '../../api/accountService';
|
||||
import { activityFeedsService } from '../../api/activityFeedsService';
|
||||
import { auditSummaryService } from '../../api/auditSummaryService';
|
||||
|
||||
interface ClientBarData {
|
||||
client: string;
|
||||
pending: number;
|
||||
deleted: number;
|
||||
audited: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const todayStr = () => new Date().toISOString().split('T')[0];
|
||||
|
||||
// Mirrors the site-parsing logic in MainLayout.tsx — /Master/site's response shape
|
||||
// varies (responseData[0].records, nested sub_sites, or a plain array).
|
||||
const parseSiteIds = (sitesRes: any): string => {
|
||||
let parsedSites: any[] = [];
|
||||
if (sitesRes?.responseData?.[0]?.records) {
|
||||
parsedSites = sitesRes.responseData[0].records;
|
||||
if (parsedSites.length === 1 && Array.isArray(parsedSites[0])) {
|
||||
parsedSites = parsedSites[0];
|
||||
} else if (parsedSites.length > 0 && Array.isArray(parsedSites[0])) {
|
||||
parsedSites = parsedSites.flat();
|
||||
}
|
||||
} else if (Array.isArray(sitesRes)) {
|
||||
parsedSites = sitesRes;
|
||||
} else if (sitesRes?.data && Array.isArray(sitesRes.data)) {
|
||||
parsedSites = sitesRes.data;
|
||||
}
|
||||
|
||||
return parsedSites
|
||||
.map((site) => site.site_id || (site.sub_sites || []).map((s: any) => s.site_id).filter(Boolean).join(','))
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
};
|
||||
|
||||
export const ClientsOverview: React.FC = () => {
|
||||
const [fromDate, setFromDate] = useState(todayStr());
|
||||
const [toDate, setToDate] = useState(todayStr());
|
||||
const [isRectificationEnabled, setIsRectificationEnabled] = useState(false);
|
||||
const [chartData, setChartData] = useState<ClientBarData[]>([]);
|
||||
const [selectedClient, setSelectedClient] = useState('All Clients');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const displayedData = useMemo(
|
||||
() => (selectedClient === 'All Clients' ? chartData : chartData.filter((c) => c.client === selectedClient)),
|
||||
[chartData, selectedClient]
|
||||
);
|
||||
|
||||
// Totals reflect the dropdown: sum of every client when "All Clients" is selected,
|
||||
// or just the one selected client's counts otherwise.
|
||||
const totals = useMemo(
|
||||
() =>
|
||||
displayedData.reduce(
|
||||
(acc, c) => ({
|
||||
pending: acc.pending + c.pending,
|
||||
deleted: acc.deleted + c.deleted,
|
||||
audited: acc.audited + c.audited,
|
||||
total: acc.total + c.total,
|
||||
}),
|
||||
{ pending: 0, deleted: 0, audited: 0, total: 0 }
|
||||
),
|
||||
[displayedData]
|
||||
);
|
||||
|
||||
const fetchAllClients = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const orgs: any[] = (await accountService.getOrganizations()) as any;
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
(orgs || []).map(async (org) => {
|
||||
const sitesRes = await activityFeedsService.getOrganizationSites(org.org_id);
|
||||
const siteIds = parseSiteIds(sitesRes);
|
||||
if (!siteIds) {
|
||||
return { client: org.org_name, pending: 0, deleted: 0, audited: 0, total: 0 };
|
||||
}
|
||||
const summary = await auditSummaryService.getSummary({
|
||||
dbName: org.db_name,
|
||||
site_id: siteIds,
|
||||
isRectificationEnabled,
|
||||
fromDate,
|
||||
toDate,
|
||||
});
|
||||
return {
|
||||
client: org.org_name,
|
||||
pending: summary.counts.pending,
|
||||
deleted: summary.counts.deleted,
|
||||
audited: summary.counts.audited,
|
||||
total: summary.counts.total,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const data: ClientBarData[] = results.map((r, i) =>
|
||||
r.status === 'fulfilled'
|
||||
? r.value
|
||||
: { client: orgs[i]?.org_name || `Client ${i}`, pending: 0, deleted: 0, audited: 0, total: 0 }
|
||||
);
|
||||
setChartData(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load client summary', err);
|
||||
setError('Failed to load client data. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isRectificationEnabled, fromDate, toDate]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllClients();
|
||||
}, [fetchAllClients]);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', maxWidth: '100vw', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, px: 2, py: 1.5, bgcolor: '#0b1121' }}>
|
||||
<TextField
|
||||
type="date"
|
||||
size="small"
|
||||
label="From"
|
||||
value={fromDate}
|
||||
onChange={(e) => setFromDate(e.target.value)}
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': { color: '#f8fafc', bgcolor: 'rgba(255,255,255,0.1)' },
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="date"
|
||||
size="small"
|
||||
label="To"
|
||||
value={toDate}
|
||||
onChange={(e) => setToDate(e.target.value)}
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': { color: '#f8fafc', bgcolor: 'rgba(255,255,255,0.1)' },
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setIsRectificationEnabled(!isRectificationEnabled)}
|
||||
sx={{
|
||||
bgcolor: isRectificationEnabled ? '#ff4d4f' : 'rgba(255,255,255,0.1)',
|
||||
color: '#fff',
|
||||
'&:hover': { bgcolor: isRectificationEnabled ? '#ff7875' : 'rgba(255,255,255,0.2)' },
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 'bold',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
{isRectificationEnabled ? 'RECTIFICATION' : 'AUDITS'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={fetchAllClients}
|
||||
sx={{ borderColor: '#334155', color: '#94a3b8', textTransform: 'none', fontWeight: 'bold' }}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
|
||||
<FormControl size="small" sx={{ bgcolor: 'rgba(255,255,255,0.1)', borderRadius: 2, minWidth: 180 }}>
|
||||
<Select
|
||||
value={selectedClient}
|
||||
onChange={(e) => setSelectedClient(e.target.value as string)}
|
||||
sx={{ '& .MuiSelect-select': { py: 1, px: 2 }, border: 'none', '& fieldset': { border: 'none' }, fontWeight: 'bold', color: '#fff' }}
|
||||
>
|
||||
<MenuItem value="All Clients">All Clients</MenuItem>
|
||||
{chartData.map((c) => (
|
||||
<MenuItem key={c.client} value={c.client}>{c.client}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2, pt: 2, pb: 2, width: '100%' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
<Paper sx={{ flex: 1, p: 3, bgcolor: '#422006', border: '1px solid #713f12', borderRadius: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Pending ({selectedClient})</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#eab308', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : totals.pending.toLocaleString()}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Paper sx={{ flex: 1, p: 3, bgcolor: '#450a0a', border: '1px solid #7f1d1d', borderRadius: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Deleted ({selectedClient})</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#ef4444', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : totals.deleted.toLocaleString()}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Paper sx={{ flex: 1, p: 3, bgcolor: '#052e16', border: '1px solid #14532d', borderRadius: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Audited ({selectedClient})</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#22c55e', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : totals.audited.toLocaleString()}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Paper sx={{ flex: 1, p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Grand Total ({selectedClient})</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#3b82f6', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : totals.total.toLocaleString()}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2, mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ color: '#f8fafc', mb: 2, fontWeight: 'bold' }}>
|
||||
{isRectificationEnabled ? 'Rectification' : 'Audit'} Counts by Client
|
||||
{selectedClient !== 'All Clients' ? ` — ${selectedClient}` : ''}
|
||||
</Typography>
|
||||
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Typography sx={{ color: '#ef4444', textAlign: 'center', py: 8 }}>{error}</Typography>
|
||||
) : displayedData.length === 0 ? (
|
||||
<Typography sx={{ color: '#64748b', textAlign: 'center', py: 8 }}>No clients found</Typography>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={420}>
|
||||
<BarChart data={displayedData} margin={{ top: 10, right: 20, left: 0, bottom: 20 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
||||
<XAxis dataKey="client" tick={{ fill: '#94a3b8' }} />
|
||||
<YAxis tick={{ fill: '#94a3b8' }} allowDecimals={false} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: 8 }}
|
||||
labelStyle={{ color: '#f8fafc' }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ color: '#94a3b8' }} />
|
||||
<Bar dataKey="pending" name="Pending" fill="#eab308" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="deleted" name="Deleted" fill="#ef4444" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="audited" name="Audited" fill="#22c55e" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user