214 lines
8.7 KiB
TypeScript
214 lines
8.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Box, Typography, Button, Paper } from '@mui/material';
|
|
import { activityFeedsService } from '../../api/activityFeedsService';
|
|
import { TopFilterBar } from '../../components/common/TopFilterBar';
|
|
import { useFeedStore } from '../../store/feedStore';
|
|
|
|
export const Dashboard: React.FC = () => {
|
|
const {
|
|
selectedSite, auditStatus, filterFromDate, filterTillDate,
|
|
filterMinChainage, filterMaxChainage,
|
|
selectedAssetIds, dashboardCache, setDashboardCache,
|
|
searchAnomalyId, filterPlaza, isRectificationEnabled,
|
|
tabs, selectedTabIndex
|
|
} = useFeedStore();
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const selectedSiteId = selectedSite?.site_id ?? '';
|
|
const assetIdsKey = selectedAssetIds.join(',');
|
|
|
|
const activeTab = tabs[selectedTabIndex] || { date: '', isRectificationTab: false };
|
|
const activeDate = activeTab.date || '';
|
|
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
|
|
|
|
// Build a key that uniquely identifies the current filter state
|
|
const currentFilterKey = `${selectedSiteId}|${auditStatus}|${filterFromDate}|${filterTillDate}|${assetIdsKey}|${filterMinChainage}|${filterMaxChainage}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeDate}`;
|
|
|
|
// Initialise stats from cache immediately (no flicker on tab switch)
|
|
const [stats, setStats] = useState(() =>
|
|
dashboardCache.stats ?? {
|
|
globalTotal: 0,
|
|
batchTotal: 0,
|
|
batchAudited: 0,
|
|
batchPending: 0,
|
|
batchAnomaliesFound: 0,
|
|
firstPendingIndex: -1,
|
|
}
|
|
);
|
|
// Only show loading spinner if there's no cached result for these filters
|
|
const [loading, setLoading] = useState(dashboardCache.stats === null);
|
|
|
|
useEffect(() => {
|
|
// CACHE HIT: filters unchanged since last fetch — skip API call entirely
|
|
if (dashboardCache.stats !== null && currentFilterKey === `${dashboardCache.siteId}|${dashboardCache.auditStatus}|${dashboardCache.fromDate}|${dashboardCache.tillDate}|${dashboardCache.assetIds}|${dashboardCache.minChainage}|${dashboardCache.maxChainage}|${dashboardCache.searchAnomalyId}|${dashboardCache.filterPlaza}|${dashboardCache.isRectificationEnabled ?? false}|${dashboardCache.date ?? ''}`) {
|
|
setStats(dashboardCache.stats);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const fetchStats = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const res: any = await activityFeedsService.getHistory({
|
|
date: activeDate,
|
|
pageNo: 0,
|
|
pageSize: 200,
|
|
sortByDateAsc: false,
|
|
isRectificationEnabled: isRectActive,
|
|
auditStatus: auditStatus,
|
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
|
siteIds: (!selectedSiteId || selectedSiteId === 'All Sites') ? '' : selectedSiteId,
|
|
assetsIds: assetIdsKey,
|
|
fromDate: filterFromDate,
|
|
tillDate: filterTillDate,
|
|
fromChainage: filterMinChainage,
|
|
toChainage: filterMaxChainage,
|
|
anomalyIds: searchAnomalyId,
|
|
plaza: filterPlaza,
|
|
});
|
|
|
|
if (res) {
|
|
const anomalies = res.anomalies || [];
|
|
const newStats = {
|
|
globalTotal: res.count || anomalies.length,
|
|
batchTotal: anomalies.length,
|
|
batchAudited: anomalies.filter((a: any) => a.IsAudited === 1).length,
|
|
batchPending: anomalies.filter((a: any) => a.IsAudited === 0).length,
|
|
batchAnomaliesFound: anomalies.filter((a: any) => a.Audit_status === 1).length,
|
|
firstPendingIndex: anomalies.findIndex((a: any) => a.IsAudited === 0)
|
|
};
|
|
setStats(newStats);
|
|
// Persist to store cache so re-mounting the Dashboard skips this fetch
|
|
setDashboardCache({
|
|
siteId: selectedSiteId,
|
|
auditStatus,
|
|
fromDate: filterFromDate,
|
|
tillDate: filterTillDate,
|
|
assetIds: assetIdsKey,
|
|
minChainage: filterMinChainage,
|
|
maxChainage: filterMaxChainage,
|
|
searchAnomalyId,
|
|
filterPlaza,
|
|
isRectificationEnabled: isRectActive,
|
|
date: activeDate,
|
|
stats: newStats,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load dashboard stats', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchStats();
|
|
}, [currentFilterKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
return (
|
|
<Box sx={{ width: '100%', maxWidth: '100vw', display: 'flex', flexDirection: 'column' }}>
|
|
<TopFilterBar />
|
|
<Box sx={{ px: 2, pt: 2, pb: 4, width: '100%' }}>
|
|
<Box sx={{ width: '100%' }}>
|
|
|
|
{/* Stats Row */}
|
|
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
|
{/* Active Batch */}
|
|
<Paper sx={{
|
|
flex: 1, p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2
|
|
}}>
|
|
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Active Batch</Typography>
|
|
<Typography variant="h4" sx={{ color: '#3b82f6', fontWeight: 'bold' }}>
|
|
{loading ? '...' : stats.batchTotal.toLocaleString()}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: '#64748b', mt: 0.5, display: 'block' }}>
|
|
of {loading ? '...' : stats.globalTotal.toLocaleString()} total items on site
|
|
</Typography>
|
|
</Paper>
|
|
|
|
{/* Audited */}
|
|
<Paper sx={{
|
|
flex: 1, p: 3, bgcolor: '#052e16', border: '1px solid #14532d', borderRadius: 2
|
|
}}>
|
|
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Audited</Typography>
|
|
<Typography variant="h4" sx={{ color: '#22c55e', fontWeight: 'bold' }}>
|
|
{loading ? '...' : stats.batchAudited.toLocaleString()}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: '#475569', mt: 0.5, display: 'block' }}>
|
|
in current batch
|
|
</Typography>
|
|
</Paper>
|
|
|
|
{/* Pending */}
|
|
<Paper sx={{
|
|
flex: 1, p: 3, bgcolor: '#422006', border: '1px solid #713f12', borderRadius: 2
|
|
}}>
|
|
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Pending</Typography>
|
|
<Typography variant="h4" sx={{ color: '#eab308', fontWeight: 'bold' }}>
|
|
{loading ? '...' : stats.batchPending.toLocaleString()}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: '#713f12', mt: 0.5, display: 'block' }}>
|
|
in current batch
|
|
</Typography>
|
|
</Paper>
|
|
|
|
{/* Anomalies Found */}
|
|
<Paper sx={{
|
|
flex: 1, p: 3, bgcolor: '#450a0a', border: '1px solid #7f1d1d', borderRadius: 2
|
|
}}>
|
|
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Anomalies Found</Typography>
|
|
<Typography variant="h4" sx={{ color: '#ef4444', fontWeight: 'bold' }}>
|
|
{loading ? '...' : stats.batchAnomaliesFound.toLocaleString()}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: '#7f1d1d', mt: 0.5, display: 'block' }}>
|
|
in current batch
|
|
</Typography>
|
|
</Paper>
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{/* Start Button */}
|
|
<Button
|
|
variant="contained"
|
|
fullWidth
|
|
onClick={() => navigate('/audit-session', {
|
|
state: {
|
|
isReviewMode: stats.batchPending === 0,
|
|
startPage: stats.firstPendingIndex !== undefined && stats.firstPendingIndex !== -1 ? Math.floor(stats.firstPendingIndex / 50) : 0
|
|
}
|
|
})}
|
|
disabled={stats.batchTotal === 0}
|
|
sx={{
|
|
py: 2,
|
|
bgcolor: '#3b82f6',
|
|
'&:hover': { bgcolor: '#2563eb' },
|
|
'&.Mui-disabled': { bgcolor: 'rgba(59, 130, 246, 0.3)', color: 'rgba(255,255,255,0.3)' },
|
|
textTransform: 'none',
|
|
fontSize: '1.1rem',
|
|
fontWeight: 'bold',
|
|
borderRadius: 2
|
|
}}
|
|
>
|
|
{stats.batchTotal === 0
|
|
? 'No Items to Audit'
|
|
: stats.batchPending === 0
|
|
? `Start Session — Review ${Math.min(stats.batchTotal, 50)} Audited Items`
|
|
: `Start Session — Audit Next ${Math.min(stats.batchPending, 50)} Items`}
|
|
</Button>
|
|
|
|
{/* Footer */}
|
|
<Box sx={{ mt: 2 }}>
|
|
<Typography variant="caption" sx={{ color: '#475569' }}>Keyboard shortcuts available inside the audit view</Typography>
|
|
</Box>
|
|
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|