feat: implement activity feed module with dynamic filtering, state management, and asset selection components

This commit is contained in:
2026-06-22 12:47:01 +05:30
parent 0a35bba378
commit 514a656fa8
11 changed files with 505 additions and 150 deletions

View File

@@ -44,8 +44,8 @@ export const DbSelectionDialog: React.FC<DbSelectionDialogProps> = ({ open, onCl
</Box>
) : (
<List>
{organizations.map((org) => (
<ListItem disablePadding key={org.org_id || org.org_name}>
{organizations.map((org, index) => (
<ListItem disablePadding key={`${org.org_id || org.org_name}-${index}`}>
<ListItemButton onClick={() => onClose(org)}>
<ListItemText primary={org.org_name} />
</ListItemButton>

View File

@@ -219,8 +219,9 @@ export const FeedFilters: React.FC = () => {
onClose={() => setIsFiltersModalOpen(false)}
maxWidth="lg"
fullWidth
// Zero transition — opens instantly
keepMounted // pre-renders in background — no mount delay on open
transitionDuration={0}
disableScrollLock // prevents layout shift stutter
slotProps={{
paper: {
sx: {

View File

@@ -25,7 +25,7 @@ export const GlobalFeed: React.FC = () => {
}
const initTabs = async () => {
// Add default Unaudited tab if empty and fetch data
// Add default Unaudited tab if empty and trigger second effect
if (useFeedStore.getState().tabs.length === 0) {
addTab({
date: '', // Unaudited
@@ -38,39 +38,6 @@ export const GlobalFeed: React.FC = () => {
page: 0,
rowsPerPage: 20
});
const currentSite = useFeedStore.getState().selectedSite;
const siteIdParam = (!currentSite || currentSite?.site_id === 'All Sites') ? '' : String(currentSite.site_id || '');
const assetIdsParam = useFeedStore.getState().selectedAssetIds.join(',');
try {
const historyRes: any = await activityFeedsService.getHistory({
date: '',
pageNo: 0,
pageSize: 20,
siteIds: siteIdParam,
auditStatus: auditStatus,
isTrueFalse: isRectificationEnabled ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
auto_audit: isRectificationEnabled ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
isRectificationEnabled: isRectificationEnabled,
assetsIds: assetIdsParam,
fromDate: useFeedStore.getState().filterFromDate,
tillDate: useFeedStore.getState().filterTillDate,
fromChainage: useFeedStore.getState().filterMinChainage,
toChainage: useFeedStore.getState().filterMaxChainage,
anomalyIds: searchAnomalyId,
plaza: filterPlaza
});
if (historyRes && historyRes.anomalies) {
updateTab(0, {
anomalies: historyRes.anomalies,
anomaliesCopy: historyRes.anomalies,
totalAnomalyCount: historyRes.count || historyRes.anomalies.length
});
}
} catch (historyErr) {
console.error("[GlobalFeed] Failed to load history", historyErr);
}
}
};

View File

@@ -1,7 +1,9 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions, IconButton } from '@mui/material';
import { useNavigate, useLocation } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ZoomInIcon from '@mui/icons-material/ZoomIn';
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
import { activityFeedsService } from '../../api/activityFeedsService';
import { useAuthStore } from '../../store/authStore';
import { useFeedStore } from '../../store/feedStore';
@@ -62,24 +64,41 @@ export const AuditSession: React.FC = () => {
tabs, selectedTabIndex, updateTab,
selectedSite, filterFromDate, filterTillDate,
filterMinChainage, filterMaxChainage, selectedAssetIds,
searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza
searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza,
setDashboardCache
} = useFeedStore();
// Call this before every navigate('/dashboard') so the dashboard re-fetches fresh stats
const invalidateDashboardCache = () => {
setDashboardCache({
siteId: '__unset__', auditStatus: '__unset__', fromDate: '__unset__',
tillDate: '__unset__', assetIds: '__unset__', minChainage: '__unset__',
maxChainage: '__unset__', searchAnomalyId: '__unset__', filterPlaza: '__unset__',
isRectificationEnabled: false, date: '', stats: null,
});
};
const [sessionPageNo, setSessionPageNo] = useState(() => location.state?.startPage || 0);
const hasInitializedSession = useRef(false);
const [masterZoom, setMasterZoom] = useState(1);
const [anomalyZoom, setAnomalyZoom] = useState(1);
const activeTab = tabs[selectedTabIndex] || { date: '', isRectificationTab: false, page: 0, rowsPerPage: 20 };
const activeDate = activeTab.date || '';
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
const loadSessionData = useCallback(async () => {
const loadSessionData = useCallback(async (customPageNo?: number) => {
try {
setLoading(true);
const selectedSiteId = selectedSite?.site_id ?? '';
const assetIdsKey = selectedAssetIds.join(',');
const isReviewMode = location.state?.isReviewMode;
const targetPage = typeof customPageNo === 'number' ? customPageNo : sessionPageNo;
const res: any = await activityFeedsService.getHistory({
date: activeDate,
pageNo: 0,
pageNo: targetPage * 50,
pageSize: 50,
sortByDateAsc: false, // must match dashboard sort so firstPendingIndex page alignment is correct
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
auditStatus: auditStatus,
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
@@ -104,17 +123,20 @@ export const AuditSession: React.FC = () => {
setNotAnAnomalySubCategory(null);
setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
} else {
setLoading(false);
alert("No more anomalies left in the feed.");
setShowCompletionModal(false);
navigate('/activity-feeds');
}
} catch (err) {
console.error('Failed to load session anomalies:', err);
} finally {
setLoading(false);
}
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state]);
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, filterPlaza, sessionPageNo, navigate]);
const handleLoadNextFeedPage = useCallback(async () => {
try {
setShowCompletionModal(false);
setLoading(true);
const nextPage = (activeTab.page || 0) + 1;
const selectedSiteId = selectedSite?.site_id ?? '';
@@ -154,6 +176,8 @@ export const AuditSession: React.FC = () => {
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
setMasterZoom(1);
setAnomalyZoom(1);
setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
} else {
alert("No more anomalies left in the feed.");
@@ -167,6 +191,7 @@ export const AuditSession: React.FC = () => {
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, activeTab, selectedTabIndex, updateTab, navigate]);
useEffect(() => {
if (hasInitializedSession.current) return;
const navState = location.state as any;
if (navState?.useExistingFeed) {
const activeTab = tabs[selectedTabIndex];
@@ -188,6 +213,7 @@ export const AuditSession: React.FC = () => {
} else {
setLoading(false);
}
hasInitializedSession.current = true;
} else if (location.state && location.state.anomalies) {
setAnomalies(location.state.anomalies);
setCurrentIndex(location.state.startIndex || 0);
@@ -203,8 +229,10 @@ export const AuditSession: React.FC = () => {
} else {
setLoading(false);
}
hasInitializedSession.current = true;
} else {
loadSessionData();
hasInitializedSession.current = true;
}
}, [location.state, tabs, selectedTabIndex, loadSessionData, filterPlaza]);
@@ -222,6 +250,8 @@ export const AuditSession: React.FC = () => {
} else {
setCarouselIndex(0);
}
setMasterZoom(1);
setAnomalyZoom(1);
}, [currentAnomaly]);
let masterImageSrc = '';
@@ -238,13 +268,21 @@ export const AuditSession: React.FC = () => {
}
if (currentAnomaly.Pos) {
const match = String(currentAnomaly.Pos).match(/([NS][0-9.]+)([EW][0-9.]+)/i);
if (match) {
latStr = match[1];
longStr = match[2];
const matchWithComma = String(currentAnomaly.Pos).match(/([NS])\s*([0-9.]+)[,\s]*([EW])\s*([0-9.]+)/i);
if (matchWithComma) {
const latVal = parseFloat(matchWithComma[2]).toFixed(5);
const longVal = parseFloat(matchWithComma[4]).toFixed(5);
latStr = `${matchWithComma[1].toUpperCase()} ${latVal}`;
longStr = `${matchWithComma[3].toUpperCase()} ${longVal}`;
} else {
latStr = currentAnomaly.Pos;
longStr = '';
const matchOld = String(currentAnomaly.Pos).match(/([NS][0-9.]+)([EW][0-9.]+)/i);
if (matchOld) {
latStr = matchOld[1];
longStr = matchOld[2];
} else {
latStr = currentAnomaly.Pos;
longStr = '';
}
}
}
}
@@ -276,6 +314,8 @@ export const AuditSession: React.FC = () => {
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
setMasterZoom(1);
setAnomalyZoom(1);
} else {
setShowCompletionModal(true);
}
@@ -289,6 +329,8 @@ export const AuditSession: React.FC = () => {
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
setMasterZoom(1);
setAnomalyZoom(1);
}
};
@@ -499,8 +541,14 @@ export const AuditSession: React.FC = () => {
break;
}
case 'enter':
if (selectedCategory !== null) {
handleSaveAndNext();
} else {
handleSkip();
}
break;
case 'arrowright':
handleSaveAndNext();
handleNext();
break;
case 'arrowleft':
handlePrev();
@@ -536,6 +584,7 @@ export const AuditSession: React.FC = () => {
<Typography sx={{ mb: 2 }}>No unaudited anomalies found to audit.</Typography>
<Button variant="contained" onClick={() => {
const navState = location.state as any;
if (!navState?.useExistingFeed) invalidateDashboardCache();
navigate(navState?.useExistingFeed ? '/activity-feeds' : '/dashboard');
}}>Back</Button>
</Box>
@@ -568,6 +617,7 @@ export const AuditSession: React.FC = () => {
startIcon={<ArrowBackIcon fontSize="small" />}
onClick={() => {
const navState = location.state as any;
if (!navState?.useExistingFeed) invalidateDashboardCache();
navigate(navState?.useExistingFeed ? '/activity-feeds' : '/dashboard');
}}
sx={{ color: '#94a3b8', textTransform: 'none', minWidth: 'auto', p: 0, '&:hover': { color: 'white', bgcolor: 'transparent' } }}
@@ -624,12 +674,26 @@ export const AuditSession: React.FC = () => {
<Box sx={{ px: 2, py: 1, borderBottom: '1px solid #1e293b', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography sx={{ fontSize: '0.75rem', fontWeight: 'bold', color: '#94a3b8', letterSpacing: 1 }}>MASTER IMAGE</Typography>
</Box>
<Box sx={{ flex: 1, position: 'relative' }}>
<Box sx={{ flex: 1, position: 'relative', overflow: 'auto', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Box sx={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 1, zIndex: 10 }}>
<IconButton size="small" onClick={() => setMasterZoom(z => z + 0.5)} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}>
<ZoomInIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={() => setMasterZoom(z => Math.max(1, z - 0.5))} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}>
<ZoomOutIcon fontSize="small" />
</IconButton>
</Box>
{masterImageSrc ? (
<img
src={masterImageSrc}
alt="Master"
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
style={{
width: masterZoom === 1 ? '100%' : `${masterZoom * 100}%`,
height: masterZoom === 1 ? '100%' : 'auto',
objectFit: masterZoom === 1 ? 'contain' : 'cover',
transition: 'width 0.2s ease, height 0.2s ease',
transformOrigin: 'top left'
}}
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.src = '/assets/images/master_image_not_available.png';
@@ -647,16 +711,35 @@ export const AuditSession: React.FC = () => {
<Typography sx={{ fontSize: '0.75rem', fontWeight: 'bold', color: '#60a5fa', letterSpacing: 1 }}>ANOMALY IMAGE</Typography>
<Typography sx={{ fontSize: '0.75rem', color: '#93c5fd' }}>{currentAnomaly?.date_of_audit || currentAnomaly?.Created_on}</Typography>
</Box>
<Box sx={{ flex: 1, position: 'relative' }}>
<Box sx={{ flex: 1, position: 'relative', overflow: 'auto', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Box sx={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 1, zIndex: 10 }}>
<IconButton size="small" onClick={() => setAnomalyZoom(z => z + 0.5)} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}>
<ZoomInIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={() => setAnomalyZoom(z => Math.max(1, z - 0.5))} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}>
<ZoomOutIcon fontSize="small" />
</IconButton>
</Box>
{mediaItems.length > 0 ? (
<>
{mediaItems[carouselIndex]?.type === 'video' ? (
<video src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
<video src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{
width: anomalyZoom === 1 ? '100%' : `${anomalyZoom * 100}%`,
height: anomalyZoom === 1 ? '100%' : 'auto',
objectFit: anomalyZoom === 1 ? 'contain' : 'cover',
transition: 'width 0.2s ease, height 0.2s ease'
}} />
) : (
<img
src={mediaItems[carouselIndex]?.url}
alt="Anomaly"
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
style={{
width: anomalyZoom === 1 ? '100%' : `${anomalyZoom * 100}%`,
height: anomalyZoom === 1 ? '100%' : 'auto',
objectFit: anomalyZoom === 1 ? 'contain' : 'cover',
transition: 'width 0.2s ease, height 0.2s ease',
transformOrigin: 'top left'
}}
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.src = '/assets/images/anomaly-placeholder.png';
@@ -685,14 +768,28 @@ export const AuditSession: React.FC = () => {
</Box>
</Box>
{/* Bottom Bar: Location Overlay */}
<Box sx={{ bgcolor: '#0f172a', borderRadius: 2, border: '1px solid #1e293b', p: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ width: 12, height: 12, borderRadius: '50%', bgcolor: '#ef4444' }} />
<Box>
<Typography sx={{ fontWeight: 'bold', fontSize: '0.9rem' }}>{currentAnomaly?.Plaza || 'Unknown Location'}</Typography>
<Typography sx={{ fontSize: '0.8rem', color: '#94a3b8' }}>
{latStr}, {longStr} ID #{currentAnomaly?.id || currentAnomaly?.anomaly_id || 'N/A'}
</Typography>
{/* Bottom Bar: Metadata Overlay */}
<Box sx={{ bgcolor: '#0f172a', borderRadius: 2, border: '1px solid #1e293b', p: 2, display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: '#ef4444', boxShadow: '0 0 8px #ef4444' }} />
<Typography sx={{ fontWeight: 'bold', fontSize: '1.1rem', color: '#f8fafc', letterSpacing: 0.5 }}>
{currentAnomaly?.Plaza || 'Unknown Location'}
</Typography>
</Box>
<Chip
label={`ID #${currentAnomaly?.id || currentAnomaly?.anomaly_id || 'N/A'}`}
size="small"
sx={{ bgcolor: 'rgba(59, 130, 246, 0.15)', color: '#60a5fa', fontWeight: 'bold', border: '1px solid rgba(59, 130, 246, 0.3)' }}
/>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
<Chip size="small" label={currentAnomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset'} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5, fontWeight: 'medium' }} />
<Chip size="small" label={currentAnomaly?.date_of_audit || currentAnomaly?.Created_on ? new Date(currentAnomaly?.date_of_audit || currentAnomaly?.Created_on).toLocaleString() : 'No Date'} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
<Chip size="small" label={`CH: ${currentAnomaly?.Chainage || 'N/A'}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
<Chip size="small" label={`Side: ${currentAnomaly?.Side || 'N/A'}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
<Chip size="small" label={`${latStr}${longStr ? `, ${longStr}` : ''}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
</Box>
</Box>
</Box>
@@ -912,8 +1009,8 @@ export const AuditSession: React.FC = () => {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button
variant="contained"
color="primary"
onClick={handleSaveAndNext}
color={selectedCategory !== null ? "primary" : "inherit"}
onClick={selectedCategory !== null ? handleSaveAndNext : handleSkip}
sx={{
borderRadius: 2,
py: 1.5,
@@ -923,10 +1020,13 @@ export const AuditSession: React.FC = () => {
justifyContent: 'space-between',
px: 3,
transition: 'all 0.15s ease',
border: (activeKeyFlash === 'enter' || activeKeyFlash === 'arrowright') ? '2px solid #3b82f6' : 'none'
bgcolor: selectedCategory !== null ? '' : '#1e293b',
color: selectedCategory !== null ? '' : '#94a3b8',
border: (activeKeyFlash === 'enter') ? (selectedCategory !== null ? '2px solid #3b82f6' : '2px solid #64748b') : 'none',
'&:hover': { bgcolor: selectedCategory !== null ? '' : '#334155' }
}}
>
Save & Next <Typography component="span" sx={{ fontSize: '0.75rem', opacity: 0.7 }}>Enter / </Typography>
{selectedCategory !== null ? "Save & Next" : "Next (Skip)"} <Typography component="span" sx={{ fontSize: '0.75rem', opacity: 0.7 }}>Enter</Typography>
</Button>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
@@ -1010,6 +1110,7 @@ export const AuditSession: React.FC = () => {
<Dialog
open={showCompletionModal}
onClose={() => setShowCompletionModal(false)}
sx={{ zIndex: 10000 }}
slotProps={{
paper: {
sx: {
@@ -1057,7 +1158,9 @@ export const AuditSession: React.FC = () => {
variant="contained"
onClick={isFeedSession ? handleLoadNextFeedPage : async () => {
setShowCompletionModal(false);
await loadSessionData();
const nextPage = sessionPageNo + 1;
setSessionPageNo(nextPage);
await loadSessionData(nextPage);
}}
sx={{ bgcolor: '#3b82f6', '&:hover': { bgcolor: '#2563eb' }, textTransform: 'none', fontWeight: 'bold', py: 1.2, borderRadius: 2 }}
>
@@ -1068,6 +1171,7 @@ export const AuditSession: React.FC = () => {
variant="outlined"
onClick={() => {
setShowCompletionModal(false);
if (!isFeedSession) invalidateDashboardCache();
navigate(isFeedSession ? '/activity-feeds' : '/dashboard');
}}
sx={{ borderColor: '#334155', color: '#94a3b8', '&:hover': { borderColor: '#475569', bgcolor: 'rgba(255,255,255,0.03)' }, textTransform: 'none', py: 1.2, borderRadius: 2 }}

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Typography, Button, Paper, LinearProgress } from '@mui/material';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import { Box, Typography, Button, Paper } from '@mui/material';
import { activityFeedsService } from '../../api/activityFeedsService';
import { TopFilterBar } from '../../components/common/TopFilterBar';
import { useFeedStore } from '../../store/feedStore';
@@ -78,6 +77,7 @@ export const Dashboard: React.FC = () => {
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
@@ -169,82 +169,19 @@ export const Dashboard: React.FC = () => {
</Paper>
</Box>
{/* 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' }}>Active Batch Progress</Typography>
<Typography variant="body2" sx={{ color: '#94a3b8' }}>{stats.batchAudited} / {stats.batchTotal} completed</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progressPercent}
sx={{
height: 10,
borderRadius: 5,
bgcolor: '#1e293b',
'& .MuiLinearProgress-bar': { bgcolor: '#3b82f6', borderRadius: 5 }
}}
/>
<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.batchPending / 50)} sessions remaining at 50/session</Typography>
</Box>
</Paper>
{/* 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' }}>Priority items</Typography> will appear first in the session queue
</Typography>
</Paper>
{/* Start Button */}
<Button
variant="contained"
fullWidth
onClick={() => navigate('/audit-session', { state: { isReviewMode: stats.batchPending === 0 } })}
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,
@@ -264,10 +201,9 @@ export const Dashboard: React.FC = () => {
: `Start Session — Audit Next ${Math.min(stats.batchPending, 50)} Items`}
</Button>
{/* Footer links */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
{/* Footer */}
<Box sx={{ mt: 2 }}>
<Typography variant="caption" sx={{ color: '#475569' }}>Keyboard shortcuts available inside the audit view</Typography>
<Typography variant="caption" sx={{ color: '#64748b', textDecoration: 'underline', cursor: 'pointer', '&:hover': { color: '#f8fafc' } }}>Reset demo data</Typography>
</Box>
</Box>