client-site based dropdown in the clients tab
This commit is contained in:
@@ -9,17 +9,29 @@ import { auditSummaryService } from '../../api/auditSummaryService';
|
||||
|
||||
interface ClientBarData {
|
||||
client: string;
|
||||
org_id: string | number;
|
||||
db_name: string;
|
||||
pending: number;
|
||||
deleted: number;
|
||||
audited: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface SiteOption {
|
||||
site_id: string;
|
||||
site_name: string;
|
||||
}
|
||||
|
||||
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
|
||||
// varies (responseData[0].records, nested sub_sites, or a plain array).
|
||||
const parseSiteIds = (sitesRes: any): string => {
|
||||
const parseSites = (sitesRes: any): SiteOption[] => {
|
||||
let parsedSites: any[] = [];
|
||||
if (sitesRes?.responseData?.[0]?.records) {
|
||||
parsedSites = sitesRes.responseData[0].records;
|
||||
@@ -35,41 +47,44 @@ const parseSiteIds = (sitesRes: any): string => {
|
||||
}
|
||||
|
||||
return parsedSites
|
||||
.map((site) => site.site_id || (site.sub_sites || []).map((s: any) => s.site_id).filter(Boolean).join(','))
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
.map((site) => {
|
||||
const site_id = site.site_id || (site.sub_sites || []).map((s: any) => s.site_id).filter(Boolean).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 = () => {
|
||||
const [fromDate, setFromDate] = useState(todayStr());
|
||||
const [fromDate, setFromDate] = useState(pastWeekStr());
|
||||
const [toDate, setToDate] = useState(todayStr());
|
||||
const [isRectificationEnabled, setIsRectificationEnabled] = useState(false);
|
||||
const [chartData, setChartData] = useState<ClientBarData[]>([]);
|
||||
const [sitesByOrg, setSitesByOrg] = useState<Record<string, SiteOption[]>>({});
|
||||
const [selectedClient, setSelectedClient] = useState('All Clients');
|
||||
const [selectedSite, setSelectedSite] = useState('All Sites');
|
||||
const [loading, setLoading] = useState(true);
|
||||
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(
|
||||
() => (selectedClient === 'All Clients' ? chartData : chartData.filter((c) => c.client === selectedClient)),
|
||||
const currentOrg = useMemo(
|
||||
() => chartData.find((c) => c.client === selectedClient) || null,
|
||||
[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 availableSites = useMemo(
|
||||
() => (currentOrg ? sitesByOrg[currentOrg.org_id] || [] : []),
|
||||
[currentOrg, sitesByOrg]
|
||||
);
|
||||
|
||||
// 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 () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -79,9 +94,12 @@ export const ClientsOverview: React.FC = () => {
|
||||
const results = await Promise.allSettled(
|
||||
(orgs || []).map(async (org) => {
|
||||
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) {
|
||||
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({
|
||||
dbName: org.db_name,
|
||||
@@ -92,6 +110,8 @@ export const ClientsOverview: React.FC = () => {
|
||||
});
|
||||
return {
|
||||
client: org.org_name,
|
||||
org_id: org.org_id,
|
||||
db_name: org.db_name,
|
||||
pending: summary.counts.pending,
|
||||
deleted: summary.counts.deleted,
|
||||
audited: summary.counts.audited,
|
||||
@@ -103,7 +123,7 @@ export const ClientsOverview: React.FC = () => {
|
||||
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 }
|
||||
: { 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);
|
||||
} catch (err: any) {
|
||||
@@ -118,9 +138,80 @@ export const ClientsOverview: React.FC = () => {
|
||||
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 (
|
||||
<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
|
||||
type="date"
|
||||
size="small"
|
||||
@@ -178,32 +269,47 @@ export const ClientsOverview: React.FC = () => {
|
||||
))}
|
||||
</Select>
|
||||
</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 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="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Pending ({totalsLabel})</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#eab308', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : totals.pending.toLocaleString()}
|
||||
{effectiveLoading ? '...' : 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="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Deleted ({totalsLabel})</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#ef4444', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : totals.deleted.toLocaleString()}
|
||||
{effectiveLoading ? '...' : 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="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Audited ({totalsLabel})</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#22c55e', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : totals.audited.toLocaleString()}
|
||||
{effectiveLoading ? '...' : 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="body2" sx={{ color: '#94a3b8', mb: 1 }}>Grand Total ({totalsLabel})</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#3b82f6', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : totals.total.toLocaleString()}
|
||||
{effectiveLoading ? '...' : totals.total.toLocaleString()}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
@@ -212,16 +318,17 @@ export const ClientsOverview: React.FC = () => {
|
||||
<Typography variant="h6" sx={{ color: '#f8fafc', mb: 2, fontWeight: 'bold' }}>
|
||||
{isRectificationEnabled ? 'Rectification' : 'Audit'} Counts by Client
|
||||
{selectedClient !== 'All Clients' ? ` — ${selectedClient}` : ''}
|
||||
{selectedSite !== 'All Sites' ? ` — ${totalsLabel}` : ''}
|
||||
</Typography>
|
||||
|
||||
{loading ? (
|
||||
{effectiveLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Typography sx={{ color: '#ef4444', textAlign: 'center', py: 8 }}>{error}</Typography>
|
||||
) : effectiveError ? (
|
||||
<Typography sx={{ color: '#ef4444', textAlign: 'center', py: 8 }}>{effectiveError}</Typography>
|
||||
) : 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}>
|
||||
<BarChart data={displayedData} margin={{ top: 10, right: 20, left: 0, bottom: 20 }}>
|
||||
|
||||
Reference in New Issue
Block a user