client-site based dropdown in the clients tab
This commit is contained in:
@@ -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 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user