client-site based dropdown in the clients tab

This commit is contained in:
2026-07-07 14:29:23 +05:30
parent 89daec453b
commit c1e3212e07

View File

@@ -9,17 +9,29 @@ import { auditSummaryService } from '../../api/auditSummaryService';
interface ClientBarData { interface ClientBarData {
client: string; client: string;
org_id: string | number;
db_name: string;
pending: number; pending: number;
deleted: number; deleted: number;
audited: number; audited: number;
total: number; total: number;
} }
interface SiteOption {
site_id: string;
site_name: string;
}
const todayStr = () => new Date().toISOString().split('T')[0]; const todayStr = () => new Date().toISOString().split('T')[0];
const pastWeekStr = () => {
const d = new Date();
d.setDate(d.getDate() - 7);
return d.toISOString().split('T')[0];
};
// Mirrors the site-parsing logic in MainLayout.tsx — /Master/site's response shape // Mirrors the site-parsing logic in MainLayout.tsx — /Master/site's response shape
// varies (responseData[0].records, nested sub_sites, or a plain array). // varies (responseData[0].records, nested sub_sites, or a plain array).
const parseSiteIds = (sitesRes: any): string => { const parseSites = (sitesRes: any): SiteOption[] => {
let parsedSites: any[] = []; let parsedSites: any[] = [];
if (sitesRes?.responseData?.[0]?.records) { if (sitesRes?.responseData?.[0]?.records) {
parsedSites = sitesRes.responseData[0].records; parsedSites = sitesRes.responseData[0].records;
@@ -35,41 +47,44 @@ const parseSiteIds = (sitesRes: any): string => {
} }
return parsedSites return parsedSites
.map((site) => site.site_id || (site.sub_sites || []).map((s: any) => s.site_id).filter(Boolean).join(',')) .map((site) => {
.filter(Boolean) const site_id = site.site_id || (site.sub_sites || []).map((s: any) => s.site_id).filter(Boolean).join(',');
.join(','); const site_name = site.site_name || site.name || site.siteName || site_id;
return { site_id, site_name };
})
.filter((s) => s.site_id);
}; };
export const ClientsOverview: React.FC = () => { export const ClientsOverview: React.FC = () => {
const [fromDate, setFromDate] = useState(todayStr()); const [fromDate, setFromDate] = useState(pastWeekStr());
const [toDate, setToDate] = useState(todayStr()); const [toDate, setToDate] = useState(todayStr());
const [isRectificationEnabled, setIsRectificationEnabled] = useState(false); const [isRectificationEnabled, setIsRectificationEnabled] = useState(false);
const [chartData, setChartData] = useState<ClientBarData[]>([]); const [chartData, setChartData] = useState<ClientBarData[]>([]);
const [sitesByOrg, setSitesByOrg] = useState<Record<string, SiteOption[]>>({});
const [selectedClient, setSelectedClient] = useState('All Clients'); const [selectedClient, setSelectedClient] = useState('All Clients');
const [selectedSite, setSelectedSite] = useState('All Sites');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [siteSummary, setSiteSummary] = useState<ClientBarData | null>(null);
const [siteLoading, setSiteLoading] = useState(false);
const [siteError, setSiteError] = useState<string | null>(null);
const displayedData = useMemo( const currentOrg = useMemo(
() => (selectedClient === 'All Clients' ? chartData : chartData.filter((c) => c.client === selectedClient)), () => chartData.find((c) => c.client === selectedClient) || null,
[chartData, selectedClient] [chartData, selectedClient]
); );
// Totals reflect the dropdown: sum of every client when "All Clients" is selected, const availableSites = useMemo(
// or just the one selected client's counts otherwise. () => (currentOrg ? sitesByOrg[currentOrg.org_id] || [] : []),
const totals = useMemo( [currentOrg, sitesByOrg]
() =>
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]
); );
// Reset the site filter whenever the client changes — a site from the
// previous org shouldn't stay selected against a different org's list.
useEffect(() => {
setSelectedSite('All Sites');
}, [selectedClient]);
const fetchAllClients = useCallback(async () => { const fetchAllClients = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -79,9 +94,12 @@ export const ClientsOverview: React.FC = () => {
const results = await Promise.allSettled( const results = await Promise.allSettled(
(orgs || []).map(async (org) => { (orgs || []).map(async (org) => {
const sitesRes = await activityFeedsService.getOrganizationSites(org.org_id); const sitesRes = await activityFeedsService.getOrganizationSites(org.org_id);
const siteIds = parseSiteIds(sitesRes); const sites = parseSites(sitesRes);
setSitesByOrg((prev) => ({ ...prev, [org.org_id]: sites }));
const siteIds = sites.map((s) => s.site_id).join(',');
if (!siteIds) { if (!siteIds) {
return { client: org.org_name, pending: 0, deleted: 0, audited: 0, total: 0 }; return { client: org.org_name, org_id: org.org_id, db_name: org.db_name, pending: 0, deleted: 0, audited: 0, total: 0 };
} }
const summary = await auditSummaryService.getSummary({ const summary = await auditSummaryService.getSummary({
dbName: org.db_name, dbName: org.db_name,
@@ -92,6 +110,8 @@ export const ClientsOverview: React.FC = () => {
}); });
return { return {
client: org.org_name, client: org.org_name,
org_id: org.org_id,
db_name: org.db_name,
pending: summary.counts.pending, pending: summary.counts.pending,
deleted: summary.counts.deleted, deleted: summary.counts.deleted,
audited: summary.counts.audited, audited: summary.counts.audited,
@@ -103,7 +123,7 @@ export const ClientsOverview: React.FC = () => {
const data: ClientBarData[] = results.map((r, i) => const data: ClientBarData[] = results.map((r, i) =>
r.status === 'fulfilled' r.status === 'fulfilled'
? r.value ? r.value
: { client: orgs[i]?.org_name || `Client ${i}`, pending: 0, deleted: 0, audited: 0, total: 0 } : { client: orgs[i]?.org_name || `Client ${i}`, org_id: orgs[i]?.org_id, db_name: orgs[i]?.db_name, pending: 0, deleted: 0, audited: 0, total: 0 }
); );
setChartData(data); setChartData(data);
} catch (err: any) { } catch (err: any) {
@@ -118,9 +138,80 @@ export const ClientsOverview: React.FC = () => {
fetchAllClients(); fetchAllClients();
}, [fetchAllClients]); }, [fetchAllClients]);
// Drill down to a single site within the selected client — a fresh,
// site-scoped call, since the org-level fetch above only has aggregates.
useEffect(() => {
if (selectedClient === 'All Clients' || selectedSite === 'All Sites' || !currentOrg) {
setSiteSummary(null);
return;
}
let cancelled = false;
const fetchSite = async () => {
setSiteLoading(true);
setSiteError(null);
try {
const summary = await auditSummaryService.getSummary({
dbName: currentOrg.db_name,
site_id: selectedSite,
isRectificationEnabled,
fromDate,
toDate,
});
if (cancelled) return;
const siteName = availableSites.find((s) => s.site_id === selectedSite)?.site_name || selectedSite;
setSiteSummary({
client: siteName,
org_id: currentOrg.org_id,
db_name: currentOrg.db_name,
pending: summary.counts.pending,
deleted: summary.counts.deleted,
audited: summary.counts.audited,
total: summary.counts.total,
});
} catch (err) {
if (cancelled) return;
console.error('Failed to load site summary', err);
setSiteError('Failed to load site data. Please try again.');
} finally {
if (!cancelled) setSiteLoading(false);
}
};
fetchSite();
return () => { cancelled = true; };
}, [selectedClient, selectedSite, currentOrg, availableSites, isRectificationEnabled, fromDate, toDate]);
const displayedData = useMemo(() => {
if (selectedClient === 'All Clients') return chartData;
if (selectedSite === 'All Sites') return chartData.filter((c) => c.client === selectedClient);
return siteSummary ? [siteSummary] : [];
}, [chartData, selectedClient, selectedSite, siteSummary]);
const effectiveLoading = loading || (selectedSite !== 'All Sites' && siteLoading);
const effectiveError = error || (selectedSite !== 'All Sites' ? siteError : null);
// Totals reflect the dropdowns: sum of every client, just the selected
// client, or just the selected site's counts.
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 totalsLabel = selectedSite !== 'All Sites'
? (availableSites.find((s) => s.site_id === selectedSite)?.site_name || selectedSite)
: selectedClient;
return ( return (
<Box sx={{ width: '100%', maxWidth: '100vw', display: 'flex', flexDirection: 'column' }}> <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' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, px: 2, py: 1.5, bgcolor: '#0b1121', flexWrap: 'wrap' }}>
<TextField <TextField
type="date" type="date"
size="small" size="small"
@@ -178,32 +269,47 @@ export const ClientsOverview: React.FC = () => {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
{selectedClient !== 'All Clients' && (
<FormControl size="small" sx={{ bgcolor: 'rgba(255,255,255,0.1)', borderRadius: 2, minWidth: 180 }}>
<Select
value={selectedSite}
onChange={(e) => setSelectedSite(e.target.value as string)}
sx={{ '& .MuiSelect-select': { py: 1, px: 2 }, border: 'none', '& fieldset': { border: 'none' }, fontWeight: 'bold', color: '#fff' }}
>
<MenuItem value="All Sites">All Sites</MenuItem>
{availableSites.map((s) => (
<MenuItem key={s.site_id} value={s.site_id}>{s.site_name}</MenuItem>
))}
</Select>
</FormControl>
)}
</Box> </Box>
<Box sx={{ px: 2, pt: 2, pb: 2, width: '100%' }}> <Box sx={{ px: 2, pt: 2, pb: 2, width: '100%' }}>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}> <Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<Paper sx={{ flex: 1, p: 3, bgcolor: '#422006', border: '1px solid #713f12', borderRadius: 2 }}> <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="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Pending ({totalsLabel})</Typography>
<Typography variant="h4" sx={{ color: '#eab308', fontWeight: 'bold' }}> <Typography variant="h4" sx={{ color: '#eab308', fontWeight: 'bold' }}>
{loading ? '...' : totals.pending.toLocaleString()} {effectiveLoading ? '...' : totals.pending.toLocaleString()}
</Typography> </Typography>
</Paper> </Paper>
<Paper sx={{ flex: 1, p: 3, bgcolor: '#450a0a', border: '1px solid #7f1d1d', borderRadius: 2 }}> <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="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Deleted ({totalsLabel})</Typography>
<Typography variant="h4" sx={{ color: '#ef4444', fontWeight: 'bold' }}> <Typography variant="h4" sx={{ color: '#ef4444', fontWeight: 'bold' }}>
{loading ? '...' : totals.deleted.toLocaleString()} {effectiveLoading ? '...' : totals.deleted.toLocaleString()}
</Typography> </Typography>
</Paper> </Paper>
<Paper sx={{ flex: 1, p: 3, bgcolor: '#052e16', border: '1px solid #14532d', borderRadius: 2 }}> <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="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Audited ({totalsLabel})</Typography>
<Typography variant="h4" sx={{ color: '#22c55e', fontWeight: 'bold' }}> <Typography variant="h4" sx={{ color: '#22c55e', fontWeight: 'bold' }}>
{loading ? '...' : totals.audited.toLocaleString()} {effectiveLoading ? '...' : totals.audited.toLocaleString()}
</Typography> </Typography>
</Paper> </Paper>
<Paper sx={{ flex: 1, p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2 }}> <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="body2" sx={{ color: '#94a3b8', mb: 1 }}>Grand Total ({totalsLabel})</Typography>
<Typography variant="h4" sx={{ color: '#3b82f6', fontWeight: 'bold' }}> <Typography variant="h4" sx={{ color: '#3b82f6', fontWeight: 'bold' }}>
{loading ? '...' : totals.total.toLocaleString()} {effectiveLoading ? '...' : totals.total.toLocaleString()}
</Typography> </Typography>
</Paper> </Paper>
</Box> </Box>
@@ -212,16 +318,17 @@ export const ClientsOverview: React.FC = () => {
<Typography variant="h6" sx={{ color: '#f8fafc', mb: 2, fontWeight: 'bold' }}> <Typography variant="h6" sx={{ color: '#f8fafc', mb: 2, fontWeight: 'bold' }}>
{isRectificationEnabled ? 'Rectification' : 'Audit'} Counts by Client {isRectificationEnabled ? 'Rectification' : 'Audit'} Counts by Client
{selectedClient !== 'All Clients' ? `${selectedClient}` : ''} {selectedClient !== 'All Clients' ? `${selectedClient}` : ''}
{selectedSite !== 'All Sites' ? `${totalsLabel}` : ''}
</Typography> </Typography>
{loading ? ( {effectiveLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}> <Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : error ? ( ) : effectiveError ? (
<Typography sx={{ color: '#ef4444', textAlign: 'center', py: 8 }}>{error}</Typography> <Typography sx={{ color: '#ef4444', textAlign: 'center', py: 8 }}>{effectiveError}</Typography>
) : displayedData.length === 0 ? ( ) : displayedData.length === 0 ? (
<Typography sx={{ color: '#64748b', textAlign: 'center', py: 8 }}>No clients found</Typography> <Typography sx={{ color: '#64748b', textAlign: 'center', py: 8 }}>No data found</Typography>
) : ( ) : (
<ResponsiveContainer width="100%" height={420}> <ResponsiveContainer width="100%" height={420}>
<BarChart data={displayedData} margin={{ top: 10, right: 20, left: 0, bottom: 20 }}> <BarChart data={displayedData} margin={{ top: 10, right: 20, left: 0, bottom: 20 }}>