feat: initialize dashboard structure with auth, feed management, and activity feed filtering components
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Typography, Button, Paper, LinearProgress, Container } from '@mui/material';
|
||||
import { Box, Typography, Button, Paper, LinearProgress } from '@mui/material';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import { activityFeedsService } from '../../api/activityFeedsService';
|
||||
import { TopFilterBar } from '../../components/common/TopFilterBar';
|
||||
@@ -10,7 +10,9 @@ export const Dashboard: React.FC = () => {
|
||||
const {
|
||||
selectedSite, auditStatus, filterFromDate, filterTillDate,
|
||||
filterMinChainage, filterMaxChainage,
|
||||
selectedAssetIds, dashboardCache, setDashboardCache
|
||||
selectedAssetIds, dashboardCache, setDashboardCache,
|
||||
searchAnomalyId, filterPlaza, isRectificationEnabled,
|
||||
tabs, selectedTabIndex
|
||||
} = useFeedStore();
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -18,19 +20,29 @@ export const Dashboard: React.FC = () => {
|
||||
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}`;
|
||||
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 ?? { total: 0, audited: 0, pending: 0, anomaliesFound: 0 }
|
||||
dashboardCache.stats ?? {
|
||||
globalTotal: 0,
|
||||
batchTotal: 0,
|
||||
batchAudited: 0,
|
||||
batchPending: 0,
|
||||
batchAnomaliesFound: 0,
|
||||
}
|
||||
);
|
||||
// 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}`) {
|
||||
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;
|
||||
@@ -40,28 +52,32 @@ export const Dashboard: React.FC = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res: any = await activityFeedsService.getHistory({
|
||||
date: '',
|
||||
date: activeDate,
|
||||
pageNo: 0,
|
||||
pageSize: 200,
|
||||
sortByDateAsc: false,
|
||||
isRectificationEnabled: false,
|
||||
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
|
||||
auto_audit: 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 = {
|
||||
total: res.count || anomalies.length,
|
||||
audited: anomalies.filter((a: any) => a.IsAudited === 1).length,
|
||||
pending: anomalies.filter((a: any) => a.IsAudited === 0).length,
|
||||
anomaliesFound: anomalies.filter((a: any) => a.Audit_status === 1).length,
|
||||
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,
|
||||
};
|
||||
setStats(newStats);
|
||||
// Persist to store cache so re-mounting the Dashboard skips this fetch
|
||||
@@ -73,6 +89,10 @@ export const Dashboard: React.FC = () => {
|
||||
assetIds: assetIdsKey,
|
||||
minChainage: filterMinChainage,
|
||||
maxChainage: filterMaxChainage,
|
||||
searchAnomalyId,
|
||||
filterPlaza,
|
||||
isRectificationEnabled: isRectActive,
|
||||
date: activeDate,
|
||||
stats: newStats,
|
||||
});
|
||||
}
|
||||
@@ -86,7 +106,7 @@ export const Dashboard: React.FC = () => {
|
||||
fetchStats();
|
||||
}, [currentFilterKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const progressPercent = stats.total > 0 ? Math.round((stats.audited / stats.total) * 100) : 0;
|
||||
const progressPercent = stats.batchTotal > 0 ? Math.round((stats.batchAudited / stats.batchTotal) * 100) : 0;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', maxWidth: '100vw', display: 'flex', flexDirection: 'column' }}>
|
||||
@@ -96,13 +116,16 @@ export const Dashboard: React.FC = () => {
|
||||
|
||||
{/* Stats Row */}
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
{/* Total Items */}
|
||||
{/* Active Batch */}
|
||||
<Paper sx={{
|
||||
flex: 1, p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Total Items</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Active Batch</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#3b82f6', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : stats.total.toLocaleString()}
|
||||
{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>
|
||||
|
||||
@@ -112,7 +135,10 @@ export const Dashboard: React.FC = () => {
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Audited</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#22c55e', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : stats.audited.toLocaleString()}
|
||||
{loading ? '...' : stats.batchAudited.toLocaleString()}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#475569', mt: 0.5, display: 'block' }}>
|
||||
in current batch
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
@@ -122,7 +148,10 @@ export const Dashboard: React.FC = () => {
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Pending</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#eab308', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : stats.pending.toLocaleString()}
|
||||
{loading ? '...' : stats.batchPending.toLocaleString()}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#713f12', mt: 0.5, display: 'block' }}>
|
||||
in current batch
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
@@ -132,16 +161,19 @@ export const Dashboard: React.FC = () => {
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Anomalies Found</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#ef4444', fontWeight: 'bold' }}>
|
||||
{loading ? '...' : stats.anomaliesFound.toLocaleString()}
|
||||
{loading ? '...' : stats.batchAnomaliesFound.toLocaleString()}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#7f1d1d', mt: 0.5, display: 'block' }}>
|
||||
in current batch
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* Overall Progress */}
|
||||
{/* Active Batch Progress */}
|
||||
<Paper sx={{ p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2, mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#f8fafc' }}>Overall Progress</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8' }}>{stats.audited} / {stats.total} completed</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#f8fafc' }}>Active Batch Progress</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8' }}>{stats.batchAudited} / {stats.batchTotal} completed</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
@@ -155,22 +187,56 @@ export const Dashboard: React.FC = () => {
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: '#64748b' }}>{progressPercent}% complete</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#64748b' }}>~{Math.ceil(stats.pending / 50)} sessions remaining at 50/session</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#64748b' }}>~{Math.ceil(stats.batchPending / 50)} sessions remaining at 50/session</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Next Session Info */}
|
||||
<Paper sx={{ p: 2, px: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2, mb: 2 }}>
|
||||
<Typography variant="body1" sx={{ color: '#f8fafc' }}>
|
||||
<Typography component="span" sx={{ color: '#3b82f6', fontWeight: 'bold' }}>Next session:</Typography> 50 items will be queued ({stats.pending} pending total)
|
||||
{/* Session Mechanics Explanation Box */}
|
||||
<Paper sx={{
|
||||
p: 3,
|
||||
bgcolor: 'rgba(30, 41, 59, 0.4)',
|
||||
border: '1px solid rgba(148, 163, 184, 0.1)',
|
||||
borderRadius: 3,
|
||||
mb: 3,
|
||||
backdropFilter: 'blur(8px)'
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#60a5fa', mb: 2, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
Audit Workflow & Session Mechanics
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr 1fr' }, gap: 2.5, mb: 1 }}>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5, fontWeight: 'bold' }}>
|
||||
1. GLOBAL SITE POOL
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#f8fafc' }}>
|
||||
There are <b style={{ color: '#3b82f6' }}>{loading ? '...' : stats.globalTotal.toLocaleString()}</b> total items matching your active filter criteria on this site.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5, fontWeight: 'bold' }}>
|
||||
2. ACTIVE WORK BATCH
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#f8fafc' }}>
|
||||
An active queue of up to <b style={{ color: '#10b981' }}>{loading ? '...' : stats.batchTotal}</b> items is loaded into your immediate workspace for auditing.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5, fontWeight: 'bold' }}>
|
||||
3. BITE-SIZED SESSIONS (50 items)
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#f8fafc' }}>
|
||||
To keep image downloads fast and prevent browser lag, you audit in sessions of <b>50 items</b>.
|
||||
You have <b style={{ color: '#facc15' }}>~{loading ? '...' : Math.ceil(stats.batchPending / 50)} sessions</b> remaining.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Warning Box */}
|
||||
<Paper sx={{ p: 2, px: 3, bgcolor: '#450a0a', border: '1px solid #7f1d1d', borderRadius: 2, mb: 4, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<WarningAmberIcon sx={{ color: '#ef4444', fontSize: 20 }} />
|
||||
<Typography variant="body1" sx={{ color: '#f8fafc' }}>
|
||||
<Typography component="span" sx={{ color: '#ef4444', fontWeight: 'bold' }}>181 high-priority items</Typography> will appear first in the queue
|
||||
<Typography component="span" sx={{ color: '#ef4444', fontWeight: 'bold' }}>Priority items</Typography> will appear first in the session queue
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
@@ -178,18 +244,24 @@ export const Dashboard: React.FC = () => {
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={() => navigate('/audit-session')}
|
||||
onClick={() => navigate('/audit-session', { state: { isReviewMode: stats.batchPending === 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
|
||||
}}
|
||||
>
|
||||
Start Session — Audit Next 50 Items
|
||||
{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 links */}
|
||||
|
||||
Reference in New Issue
Block a user