feat: initialize dashboard structure with auth, feed management, and activity feed filtering components

This commit is contained in:
2026-06-16 16:42:48 +05:30
parent a4c0892741
commit 0a35bba378
17 changed files with 1840 additions and 536 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Box, Button, TextField, Typography, Container, Alert } from '@mui/material';
import { Box, Button, TextField, Container, Alert, Paper } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../../store/authStore';
import { accountService } from '../../api/accountService';
@@ -76,88 +76,138 @@ export const Login: React.FC = () => {
};
return (
<Container component="main" maxWidth="xs">
<Box sx={{ marginTop: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{/* Placeholder for Logo */}
<Box sx={{ mb: 6, display: 'flex', justifyContent: 'center' }}>
<img src="/images/SeekRightLogo.png" alt="Logo" width="250" onError={(e) => { e.currentTarget.style.display = 'none'; }} />
</Box>
<Box component="form" sx={{ mt: 1, width: '100%' }}>
<TextField
margin="normal"
required
fullWidth
id="username"
placeholder="Admin"
name="username"
autoFocus
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
InputProps={{
sx: { borderRadius: '50px', '& fieldset': { borderColor: '#738ab8' } }
}}
sx={{
mb: 1,
'& .MuiInputBase-input': { padding: '10px 14px', fontSize: '0.9rem', color: '#333' }
}}
/>
<TextField
margin="normal"
required
fullWidth
name="password"
placeholder="Password"
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
InputProps={{
sx: { borderRadius: '50px', '& fieldset': { borderColor: '#738ab8' } }
}}
sx={{
mt: 1,
mb: 3,
'& .MuiInputBase-input': { padding: '10px 14px', fontSize: '0.9rem', color: '#333' }
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3, mb: 4 }}>
<Button
variant="contained"
disabled={!username || !password || loading}
onClick={handleLogin}
sx={{
borderRadius: '50px',
backgroundColor: '#738ab8',
width: '140px',
pt: 1, pb: 1,
boxShadow: 'none',
'&:hover': {
backgroundColor: '#5c729c',
boxShadow: 'none',
}
}}
>
{loading ? '...' : 'LOGIN'}
</Button>
<Box sx={{
width: '100vw',
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: 'radial-gradient(circle at center, #0f172a 0%, #0b1121 100%)',
overflow: 'hidden'
}}>
<Container component="main" maxWidth="xs">
<Paper sx={{
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
bgcolor: 'rgba(15, 23, 42, 0.65)',
backdropFilter: 'blur(12px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: 4,
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.5)'
}}>
{/* Logo */}
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'center' }}>
<img
src="/assets/images/logo/side-bar-logo.png"
alt="Logo"
width="220"
onError={(e) => {
e.currentTarget.src = "/images/SeekRightLogo.png";
}}
/>
</Box>
<Box component="form" sx={{ width: '100%' }}>
<TextField
margin="normal"
required
fullWidth
id="username"
placeholder="Username"
name="username"
autoFocus
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
slotProps={{
input: {
sx: {
borderRadius: '50px',
color: '#f8fafc',
bgcolor: 'rgba(255, 255, 255, 0.02)',
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.12)' },
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.25)' },
'&.Mui-focused fieldset': { borderColor: '#3b82f6' }
}
}
}}
sx={{
mb: 1.5,
'& .MuiInputBase-input': { padding: '12px 18px', fontSize: '0.9rem' }
}}
/>
<TextField
margin="normal"
required
fullWidth
name="password"
placeholder="Password"
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
slotProps={{
input: {
sx: {
borderRadius: '50px',
color: '#f8fafc',
bgcolor: 'rgba(255, 255, 255, 0.02)',
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.12)' },
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.25)' },
'&.Mui-focused fieldset': { borderColor: '#3b82f6' }
}
}
}}
sx={{
mt: 1.5,
mb: 3,
'& .MuiInputBase-input': { padding: '12px 18px', fontSize: '0.9rem' }
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1, mb: 1 }}>
<Button
variant="contained"
disabled={!username || !password || loading}
onClick={handleLogin}
sx={{
borderRadius: '50px',
backgroundColor: '#3b82f6',
color: '#fff',
fontWeight: 'bold',
width: '100%',
py: 1.5,
boxShadow: 'none',
textTransform: 'none',
fontSize: '1rem',
'&:hover': {
backgroundColor: '#2563eb',
boxShadow: 'none',
},
'&.Mui-disabled': {
backgroundColor: 'rgba(59, 130, 246, 0.3)',
color: 'rgba(255, 255, 255, 0.3)'
}
}}
>
{loading ? 'Logging in...' : 'LOGIN'}
</Button>
</Box>
{isInvalid && (
<Alert severity="error" sx={{ mt: 2 }}>
Invalid Credentials
</Alert>
)}
</Box>
</Box>
{isInvalid && (
<Alert severity="error" sx={{ mt: 3, borderRadius: 2 }}>
Invalid Credentials
</Alert>
)}
</Box>
</Paper>
</Container>
{/* Database Selection Dialog for SR_AUDITOR */}
<DbSelectionDialog open={dialogOpen} onClose={handleDbSelection} />
</Container>
</Box>
);
};

View File

@@ -3,14 +3,20 @@ import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead
import { useFeedStore } from '../../store/feedStore';
export const AnomalyTable: React.FC = () => {
const { tabs, selectedTabIndex, updateTab } = useFeedStore();
const { tabs, selectedTabIndex, updateTab, filterPlaza, isRectificationEnabled } = useFeedStore();
if (tabs.length === 0 || selectedTabIndex < 0) {
return <Typography sx={{ p: 2 }}>Loading feeds...</Typography>;
}
const activeTab = tabs[selectedTabIndex];
const anomalies = activeTab.anomalies || [];
const rawAnomalies = activeTab.anomalies || [];
// Client-side Plaza filtering fallback
const anomalies = rawAnomalies.filter((anomaly: any) => {
if (!filterPlaza) return true;
return String(anomaly.Plaza || '').toLowerCase().includes(filterPlaza.toLowerCase());
});
if (anomalies.length === 0) {
return (
@@ -33,30 +39,36 @@ export const AnomalyTable: React.FC = () => {
};
return (
<TableContainer component={Paper} sx={{ borderRadius: 0, boxShadow: 'none', height: 'calc(100vh - 180px)', bgcolor: 'transparent' }}>
<TableContainer component={Paper} sx={{ borderRadius: 0, boxShadow: 'none', height: '100%', bgcolor: 'transparent', overflow: 'auto' }}>
<Table stickyHeader size="small" sx={{ minWidth: 1200 }}>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>#ID</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>PLAZA</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>ROAD</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>DATE</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>CHAINAGE</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>LAT/LONG</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>ANOMALY</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>SIDE</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>ANOMALY True/False</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>FEATURES</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>COMMENTS</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>CHECKED</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>AUTO AUDIT</TableCell>
<TableCell sx={{ width: '6%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>#ID</TableCell>
<TableCell sx={{ width: '9%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>PLAZA</TableCell>
<TableCell sx={{ width: '5%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>ROAD</TableCell>
<TableCell sx={{ width: '9%', cursor: 'pointer', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>
<Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
DATE
<Box component="img" src="/assets/images/arrow-up.png" sx={{ width: 20, height: 20, ml: 0.5 }} />
</Box>
</TableCell>
<TableCell sx={{ width: '7%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>CHAINAGE</TableCell>
<TableCell sx={{ width: '8%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>LAT/LONG</TableCell>
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle' }}>ANOMALY</TableCell>
<TableCell sx={{ width: '6%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>SIDE</TableCell>
<TableCell sx={{ width: '10%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>ANOMALY<br/>True/False</TableCell>
<TableCell sx={{ width: '6%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>FEATURES</TableCell>
<TableCell sx={{ width: '6%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{(activeTab?.date === '' && isRectificationEnabled) ? 'ISSUE' : 'COMMENTS'}</TableCell>
<TableCell sx={{ width: '5%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', textAlign: 'center', whiteSpace: 'nowrap' }}>CHECKED</TableCell>
<TableCell sx={{ width: '8%', fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)', verticalAlign: 'middle', textAlign: 'center', whiteSpace: 'nowrap' }}>BULK AUDIT</TableCell>
</TableRow>
</TableHead>
<TableBody>
{anomalies.map((anomaly: any, index: number) => {
const isSelected = activeTab.selectedAnomalyIndex === index;
{anomalies.map((anomaly: any, _filteredIndex: number) => {
const realIndex = rawAnomalies.indexOf(anomaly);
const isSelected = activeTab.selectedAnomalyIndex === realIndex;
const assetName = anomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset';
const rowColor = getRowColor(index, assetName);
const rowColor = getRowColor(realIndex, assetName);
// Parse Position
let latStr = 'N/A';
@@ -74,8 +86,8 @@ export const AnomalyTable: React.FC = () => {
return (
<TableRow
key={anomaly._id || index}
onClick={() => handleRowClick(index)}
key={anomaly._id || anomaly.id || _filteredIndex}
onClick={() => handleRowClick(realIndex)}
sx={{
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.2)' : rowColor,
cursor: 'pointer',
@@ -84,35 +96,47 @@ export const AnomalyTable: React.FC = () => {
mb: 1,
}}
>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>#{anomaly.id || anomaly._id?.substring(0, 7) || 'N/A'}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Plaza || 'N/A'}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.road || anomaly.Road || 'N/A'}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Created_on ? new Date(anomaly.Created_on).toLocaleDateString() : 'N/A'}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Chainage || 'N/A'}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)', fontSize: '0.75rem' }}>
<TableCell sx={{ width: '6%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>#{anomaly.id || anomaly._id?.substring(0, 7) || 'N/A'}</TableCell>
<TableCell sx={{ width: '9%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.Plaza || 'N/A'}</TableCell>
<TableCell sx={{ width: '5%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.road || anomaly.Road || 'N/A'}</TableCell>
<TableCell sx={{ width: '9%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.Created_on ? new Date(anomaly.Created_on).toLocaleDateString() : 'N/A'}</TableCell>
<TableCell sx={{ width: '7%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.Chainage || 'N/A'}</TableCell>
<TableCell sx={{ width: '8%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
{latStr}<br/>
{longStr}
</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{assetName}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Side || 'N/A'}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)', width: 120 }}>
<Slider
size="small"
value={anomaly.Audit_status || anomaly.anomaly_status_slider || 50}
step={100}
marks
min={0}
max={100}
sx={{ color: '#ff4d4f' }}
/>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle' }}>{assetName}</TableCell>
<TableCell sx={{ width: '6%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.Side || 'N/A'}</TableCell>
<TableCell sx={{ width: '10%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle' }}>
<Box sx={{ width: '100%', minWidth: 60 }}>
<Slider
size="small"
value={anomaly.Audit_status || anomaly.anomaly_status_slider || 50}
step={100}
marks
min={0}
max={100}
sx={{ color: '#ff4d4f' }}
/>
</Box>
</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Features || 'N/A'}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Comments || '-'}</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<TableCell sx={{ width: '6%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.Features || 'N/A'}</TableCell>
<TableCell sx={{ width: '6%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.Comments || '-'}</TableCell>
<TableCell sx={{ width: '5%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', textAlign: 'center' }}>
<Checkbox size="small" checked={!!anomaly.IsAudited} />
</TableCell>
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<Checkbox size="small" checked={!!anomaly.auto_audit} />
<TableCell sx={{ width: '8%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', textAlign: 'center' }}>
<Checkbox
size="small"
checked={!!anomaly.isSelectedForBulkAudit}
onChange={(e) => {
e.stopPropagation(); // prevent row click selection
anomaly.isSelectedForBulkAudit = e.target.checked;
// Trigger a re-render of the current active tab
const updatedAnomalies = [...rawAnomalies];
updateTab(selectedTabIndex, { anomalies: updatedAnomalies });
}}
/>
</TableCell>
</TableRow>
);

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo, useEffect, useCallback, memo } from 'react';
import React, { useState, useMemo, useCallback, memo } from 'react';
import {
Box, TextField, Checkbox, FormControlLabel, Button, Typography, Dialog,
DialogContent, DialogActions, CircularProgress, Divider, InputAdornment, Chip
@@ -153,7 +153,7 @@ export const FeedFilters: React.FC = () => {
const handleApplyFilter = async () => {
setLoading(true);
try {
const { selectedSite, auditStatus, setFilterFromDate, setFilterTillDate } = useFeedStore.getState();
const { selectedSite, auditStatus, setFilterFromDate, setFilterTillDate, filterPlaza, isRectificationEnabled, searchAnomalyId } = useFeedStore.getState();
const draftAssetArr = Array.from(draftSet);
// Commit ALL filter state to the global store
@@ -168,6 +168,12 @@ export const FeedFilters: React.FC = () => {
const siteIdParam = (!selectedSite || selectedSite?.site_id === 'All Sites')
? '' : String(selectedSite.site_id || '');
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
const assetIdsKey = draftAssetArr.join(',');
// Build the correct filter key that will be calculated in GlobalFeed
const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeTab.date}|${draftFromDate}|${draftTillDate}|${draftMinChainage}|${draftMaxChainage}|${assetIdsKey}`;
const historyRes: any = await activityFeedsService.getHistory({
date: activeTab.date,
fromDate: draftFromDate,
@@ -175,19 +181,27 @@ export const FeedFilters: React.FC = () => {
pageNo: 0,
pageSize: 20,
siteIds: siteIdParam,
assetsIds: draftAssetArr.join(','),
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
isRectificationEnabled: false,
auto_audit: false,
assetsIds: assetIdsKey,
auditStatus: auditStatus,
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
isRectificationEnabled: isRectActive,
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
fromChainage: draftMinChainage,
toChainage: draftMaxChainage,
plaza: filterPlaza,
});
if (historyRes?.anomalies) {
updateTab(selectedTabIndex, {
anomalies: historyRes.anomalies,
anomaliesCopy: historyRes.anomalies,
totalAnomalyCount: historyRes.count || historyRes.anomalies.length,
filters: { fromDate: draftFromDate, tillDate: draftTillDate, fromChainage: draftMinChainage, toChainage: draftMaxChainage }
filters: {
fromDate: draftFromDate,
tillDate: draftTillDate,
fromChainage: draftMinChainage,
toChainage: draftMaxChainage,
lastFetchedFilterKey: filterKey
}
});
}
}
@@ -207,13 +221,15 @@ export const FeedFilters: React.FC = () => {
fullWidth
// Zero transition — opens instantly
transitionDuration={0}
PaperProps={{
sx: {
borderRadius: 3,
bgcolor: '#0f172a',
color: '#f8fafc',
border: '1px solid #1e293b',
maxHeight: '90vh'
slotProps={{
paper: {
sx: {
borderRadius: 3,
bgcolor: '#0f172a',
color: '#f8fafc',
border: '1px solid #1e293b',
maxHeight: '90vh'
}
}
}}
>

View File

@@ -1,198 +1,158 @@
import React, { useEffect } from 'react';
import { Box, Typography, TablePagination, Button, FormControl, Select, MenuItem } from '@mui/material';
import FilterListIcon from '@mui/icons-material/FilterList';
import HistoryIcon from '@mui/icons-material/History';
import { Box, TablePagination } from '@mui/material';
import { TopFilterBar } from '../../components/common/TopFilterBar';
import { FeedTabs } from './FeedTabs';
import { AnomalyTable } from './AnomalyTable';
import { PreviewPanel } from './PreviewPanel';
import { useFeedStore } from '../../store/feedStore';
import { useAuthStore } from '../../store/authStore';
import { activityFeedsService } from '../../api/activityFeedsService';
export const GlobalFeed: React.FC = () => {
const {
setAssets, setAssetTypes, addTab, tabs, selectedTabIndex, updateTab,
sites, setSites, selectedSite, setSelectedSite, auditStatus,
isInitialized, setIsInitialized,
lastFetchedFilterKey, setLastFetchedFilterKey
addTab, tabs, selectedTabIndex, updateTab,
selectedSite, auditStatus,
isInitialized,
searchAnomalyId, filterPlaza,
isRectificationEnabled,
filterFromDate, filterTillDate,
filterMinChainage, filterMaxChainage,
selectedAssetIds
} = useFeedStore();
const { user } = useAuthStore();
// Ref to skip the first run of the site/auditStatus effect
const isFirstRender = React.useRef(true);
useEffect(() => {
// Initial Load
const loadInitialData = async () => {
try {
const assetRes: any = await activityFeedsService.getAssets();
if (assetRes && assetRes.asset_types) {
// Map assets mimicking the Angular logic
let flattenedAssets: any[] = [];
assetRes.asset_types.forEach((type: any) => {
const mapped = type.assets.map((a: any) => ({
id: a.asset_id,
name: a.asset_name,
asset_type_id: type.type_id
}));
flattenedAssets = [...flattenedAssets, ...mapped];
// GUARD: Wait until global metadata is initialized in MainLayout
if (!isInitialized) {
return;
}
const initTabs = async () => {
// Add default Unaudited tab if empty and fetch data
if (useFeedStore.getState().tabs.length === 0) {
addTab({
date: '', // Unaudited
isRectificationTab: false,
totalAnomalyCount: 0,
anomalies: [],
anomaliesCopy: [],
selectedAnomalyIndex: 0,
filters: {},
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
});
setAssets(flattenedAssets);
setAssetTypes(assetRes.asset_types); // store raw grouped data for filters dialog
}
// Fetch Sites
let defaultSite = null;
if (user?.org_id) {
try {
const sitesRes: any = await activityFeedsService.getOrganizationSites(user.org_id);
let parsedSites: any[] = [];
if (sitesRes?.responseData?.[0]?.records) {
parsedSites = sitesRes.responseData[0].records;
// Handle nested array response [ [ {site1}, {site2} ] ]
if (parsedSites.length === 1 && Array.isArray(parsedSites[0])) {
parsedSites = parsedSites[0];
} else if (parsedSites.length > 0 && Array.isArray(parsedSites[0])) {
parsedSites = parsedSites.flat();
}
} else if (Array.isArray(sitesRes)) {
parsedSites = sitesRes;
} else if (sitesRes?.data && Array.isArray(sitesRes.data)) {
parsedSites = sitesRes.data;
}
if (parsedSites.length > 0) {
// Filter to ensure we only show sites for the currently selected org
parsedSites = parsedSites.filter(site => {
let siteOrgId = site.org_id;
if (siteOrgId === undefined && site.sub_sites && site.sub_sites.length > 0) {
siteOrgId = site.sub_sites[0].org_id;
}
if (siteOrgId !== undefined && siteOrgId !== null) {
return String(siteOrgId) === String(user.org_id);
}
return true;
}).map(site => {
// Normalize site_id: if missing, aggregate from sub_sites
if (!site.site_id && site.sub_sites && site.sub_sites.length > 0) {
site.site_id = site.sub_sites.map((sub: any) => sub.site_id).filter(Boolean).join(',');
}
return site;
});
setSites(parsedSites);
defaultSite = { site_id: 'All Sites', site_name: 'All Sites' };
setSelectedSite(defaultSite);
}
} catch (siteErr) {
console.error("Failed to fetch sites", siteErr);
}
}
// Add default Unaudited tab if empty and fetch data
if (useFeedStore.getState().tabs.length === 0) {
addTab({
date: '', // Unaudited
isRectificationTab: false,
totalAnomalyCount: 0,
anomalies: [],
anomaliesCopy: [],
selectedAnomalyIndex: 0,
filters: {},
page: 0,
rowsPerPage: 20
});
try {
const historyRes: any = await activityFeedsService.getHistory({
date: '',
pageNo: 0,
pageSize: 20,
siteIds: defaultSite?.site_id === 'All Sites' ? '' : (defaultSite?.site_id || ''),
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
auto_audit: false,
isRectificationEnabled: false
if (historyRes && historyRes.anomalies) {
updateTab(0, {
anomalies: historyRes.anomalies,
anomaliesCopy: historyRes.anomalies,
totalAnomalyCount: historyRes.count || historyRes.anomalies.length
});
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);
}
} catch (historyErr) {
console.error("[GlobalFeed] Failed to load history", historyErr);
}
} catch (error) {
console.error("Failed to load initial feed data", error);
} finally {
setIsInitialized(true); // Mark as initialized regardless, to prevent retry loops
}
};
// GUARD: Only run initial load once across tab switches
if (useFeedStore.getState().isInitialized) {
return;
}
loadInitialData();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
initTabs();
}, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps
// Fetch data when selectedSite or auditStatus changes
// Fetch data when selectedSite, auditStatus, searchAnomalyId, filterPlaza, tab, or metadata filters change
useEffect(() => {
// GUARD: Skip if tabs are not initialized (initial load hasn't finished)
if (useFeedStore.getState().tabs.length === 0) {
return;
}
// Build a key for the current filter state
const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}`;
const currentTabIndex = selectedTabIndex > -1 ? selectedTabIndex : 0;
const activeTab = tabs[currentTabIndex];
if (!activeTab) return;
// CACHE HIT: This exact filter combo was already fetched — skip re-fetch on remount
if (filterKey === lastFetchedFilterKey) {
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
const assetIdsKey = selectedAssetIds.join(',');
// Build a key for the current filter state on the active tab
const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeTab.date}|${filterFromDate}|${filterTillDate}|${filterMinChainage}|${filterMaxChainage}|${assetIdsKey}`;
// CACHE HIT: This exact tab and filter combo was already fetched
if (filterKey === activeTab.filters?.lastFetchedFilterKey && activeTab.anomalies && activeTab.anomalies.length > 0) {
return;
}
const fetchFilteredHistory = async () => {
const state = useFeedStore.getState();
const currentTabIndex = state.selectedTabIndex > -1 ? state.selectedTabIndex : 0;
const activeTab = state.tabs[currentTabIndex];
const siteIdParam = (!selectedSite || selectedSite?.site_id === 'All Sites')
? '' : String(selectedSite.site_id || '');
try {
const historyRes: any = await activityFeedsService.getHistory({
date: activeTab?.date || '',
date: activeTab.date || '',
pageNo: 0,
pageSize: 20,
siteIds: siteIdParam,
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
auto_audit: false,
isRectificationEnabled: false,
...(activeTab?.filters || {})
auditStatus: auditStatus,
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
isRectificationEnabled: isRectActive,
anomalyIds: searchAnomalyId,
plaza: filterPlaza,
assetsIds: assetIdsKey,
fromDate: filterFromDate,
tillDate: filterTillDate,
fromChainage: filterMinChainage,
toChainage: filterMaxChainage,
});
if (historyRes && historyRes.anomalies) {
useFeedStore.getState().updateTab(currentTabIndex, {
updateTab(currentTabIndex, {
anomalies: historyRes.anomalies,
anomaliesCopy: historyRes.anomalies,
totalAnomalyCount: historyRes.count || historyRes.anomalies.length
totalAnomalyCount: historyRes.count || historyRes.anomalies.length,
filters: {
...(activeTab.filters || {}),
fromDate: filterFromDate,
tillDate: filterTillDate,
fromChainage: filterMinChainage,
toChainage: filterMaxChainage,
lastFetchedFilterKey: filterKey
}
});
setLastFetchedFilterKey(filterKey); // mark this combo as fetched
}
} catch (err) {
console.error('[GlobalFeed] Failed to load history for site/audit status change', err);
console.error('[GlobalFeed] Failed to load history for active tab/filters change', err);
}
};
fetchFilteredHistory();
// Use primitive/stable values to avoid spurious re-runs.
// selectedSite?.site_id and auditStatus are both strings/primitives.
}, [selectedSite?.site_id, auditStatus]); // eslint-disable-line react-hooks/exhaustive-deps
}, [
selectedSite?.site_id, auditStatus, searchAnomalyId, filterPlaza, isRectificationEnabled, selectedTabIndex, tabs.length,
filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, selectedAssetIds
]);
const handlePageChange = async (event: unknown, newPage: number) => {
const handlePageChange = async (_event: unknown, newPage: number) => {
if (selectedTabIndex < 0 || tabs.length === 0) return;
const activeTab = tabs[selectedTabIndex];
const allSiteIds = sites.map(s => s.site_id).filter(Boolean).join(',');
const rowsPerPage = activeTab.rowsPerPage || 20;
const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab;
const assetIdsKey = selectedAssetIds.join(',');
try {
const historyRes: any = await activityFeedsService.getHistory({
@@ -200,10 +160,17 @@ export const GlobalFeed: React.FC = () => {
pageNo: newPage * rowsPerPage,
pageSize: rowsPerPage,
siteIds: selectedSite?.site_id === 'All Sites' ? '' : (selectedSite?.site_id || ''),
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
auto_audit: false,
isRectificationEnabled: false,
...(activeTab?.filters || {})
auditStatus: auditStatus,
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
isRectificationEnabled: isRectActive,
anomalyIds: searchAnomalyId,
plaza: filterPlaza,
assetsIds: assetIdsKey,
fromDate: filterFromDate,
tillDate: filterTillDate,
fromChainage: filterMinChainage,
toChainage: filterMaxChainage,
});
if (historyRes && historyRes.anomalies) {
updateTab(selectedTabIndex, {
@@ -221,9 +188,10 @@ export const GlobalFeed: React.FC = () => {
const handleRowsPerPageChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (selectedTabIndex < 0 || tabs.length === 0) return;
const activeTab = tabs[selectedTabIndex];
const allSiteIds = sites.map(s => s.site_id).filter(Boolean).join(',');
const newRowsPerPage = parseInt(event.target.value, 10);
const newPage = 0;
const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab;
const assetIdsKey = selectedAssetIds.join(',');
try {
const historyRes: any = await activityFeedsService.getHistory({
@@ -231,10 +199,17 @@ export const GlobalFeed: React.FC = () => {
pageNo: newPage * newRowsPerPage,
pageSize: newRowsPerPage,
siteIds: selectedSite?.site_id === 'All Sites' ? '' : (selectedSite?.site_id || ''),
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
auto_audit: false,
isRectificationEnabled: false,
...(activeTab?.filters || {})
auditStatus: auditStatus,
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
isRectificationEnabled: isRectActive,
anomalyIds: searchAnomalyId,
plaza: filterPlaza,
assetsIds: assetIdsKey,
fromDate: filterFromDate,
tillDate: filterTillDate,
fromChainage: filterMinChainage,
toChainage: filterMaxChainage,
});
if (historyRes && historyRes.anomalies) {
updateTab(selectedTabIndex, {
@@ -258,7 +233,7 @@ export const GlobalFeed: React.FC = () => {
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden', bgcolor: 'background.default' }}>
{/* Left side Table */}
<Box sx={{ flex: 1, overflow: 'auto', px: 2, display: 'flex', flexDirection: 'column' }}>
<Box sx={{ flex: 1, overflow: 'hidden', px: 2, display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', py: 0 }}>
<TablePagination
component="div"
@@ -270,7 +245,7 @@ export const GlobalFeed: React.FC = () => {
rowsPerPageOptions={[10, 20, 50, 100]}
/>
</Box>
<Box sx={{ flex: 1, overflow: 'auto' }}>
<Box sx={{ flex: 1, overflow: 'hidden' }}>
<AnomalyTable />
</Box>
</Box>

View File

@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { useFeedStore } from '../../store/feedStore';
export const PreviewPanel: React.FC = () => {
const { tabs, selectedTabIndex } = useFeedStore();
const { tabs, selectedTabIndex, isRectificationEnabled } = useFeedStore();
const navigate = useNavigate();
if (tabs.length === 0 || selectedTabIndex < 0) return null;
@@ -23,7 +23,10 @@ export const PreviewPanel: React.FC = () => {
);
}
const IMAGES_AWS_PREFIX = "https://auditor-master-images.seekright.com/SeekRight/";
const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab;
const IMAGES_AWS_PREFIX = isRectActive
? "https://sr-img.seekright.com/"
: "https://auditor-master-images.seekright.com/SeekRight/";
const IMAGES_TEST_AUDIT_PREFIX = "https://dashboard-images.seekright.com/SeekRight/";
let masterImageSrc = '';
@@ -41,13 +44,17 @@ export const PreviewPanel: React.FC = () => {
}
return (
<Box sx={{ width: 400, p: 2, bgcolor: 'background.paper', height: 'calc(100vh - 120px)', borderLeft: '1px solid rgba(255,255,255,0.1)', overflowY: 'auto' }}>
<Box sx={{ width: 400, p: 2, bgcolor: 'background.paper', height: '100%', borderLeft: '1px solid rgba(255,255,255,0.1)', overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ mb: 2, border: '1px solid rgba(255,255,255,0.1)', borderRadius: 2, overflow: 'hidden' }}>
<img
src={masterImageSrc}
alt="Master"
style={{ width: '100%', height: 'auto', display: 'block' }}
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.src = '/assets/images/master_image_not_available.png';
}}
/>
<Box sx={{ bgcolor: 'rgba(255,255,255,0.05)', color: 'text.primary', textAlign: 'center', py: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>MASTER IMAGE</Typography>
@@ -59,6 +66,10 @@ export const PreviewPanel: React.FC = () => {
src={anomalyImageSrc}
alt="Anomaly"
style={{ width: '100%', height: 'auto', display: 'block' }}
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.src = '/assets/images/anomaly-placeholder.png';
}}
/>
<Box sx={{ bgcolor: 'rgba(255,255,255,0.05)', color: 'text.primary', textAlign: 'center', py: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>ANOMALY IMAGE</Typography>

File diff suppressed because it is too large Load Diff

View File

@@ -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 */}