feat: implement ClientsOverview dashboard with audit summary visualization and data filtering

This commit is contained in:
2026-07-06 18:30:46 +05:30
parent 76888915e7
commit 89daec453b
7 changed files with 674 additions and 1 deletions

View 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>
);
};