feat: implement activity feed module with dynamic filtering, state management, and asset selection components
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>react-revamp</title>
|
||||
<title>SeekRight</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -77,5 +77,10 @@ export const activityFeedsService = {
|
||||
getFalseAnomaly: async (params: any) => {
|
||||
const response = await axiosClient.get('/api/audit/asset/get-false-Anomaly', { params });
|
||||
return response;
|
||||
},
|
||||
|
||||
bulkDelete: async (payload: any) => {
|
||||
const response = await axiosClient.post('/api/audit/bulk/delete', payload);
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
335
src/components/common/BulkDeleteDialog.tsx
Normal file
335
src/components/common/BulkDeleteDialog.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog, DialogContent, Typography, Box, IconButton, Button,
|
||||
TextField, RadioGroup, FormControlLabel, Radio, Checkbox,
|
||||
CircularProgress, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useFeedStore } from '../../store/feedStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { activityFeedsService } from '../../api/activityFeedsService';
|
||||
|
||||
interface BulkDeleteDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const REASONS = ['Low Light', 'Bad Image', 'Wrong Algorithm', 'Out of Range'];
|
||||
|
||||
export const BulkDeleteDialog: React.FC<BulkDeleteDialogProps> = ({ open, onClose }) => {
|
||||
const { assetTypes, selectedSite, sites } = useFeedStore();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const [fromDate, setFromDate] = useState('');
|
||||
const [tillDate, setTillDate] = useState('');
|
||||
const [auditType, setAuditType] = useState('manual');
|
||||
const [reason, setReason] = useState('Bad Image');
|
||||
const [selectedAssets, setSelectedAssets] = useState<Set<number>>(new Set());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [localSite, setLocalSite] = useState<string>(selectedSite?.site_id || 'All Sites');
|
||||
|
||||
// Reset state when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFromDate('');
|
||||
setTillDate('');
|
||||
setAuditType('manual');
|
||||
setReason('Bad Image');
|
||||
setSelectedAssets(new Set());
|
||||
setLocalSite(selectedSite?.site_id || 'All Sites');
|
||||
}
|
||||
}, [open, selectedSite?.site_id]);
|
||||
|
||||
// Extract all asset IDs for "Select All" functionality
|
||||
const allAssetIds = React.useMemo(() => {
|
||||
const ids: number[] = [];
|
||||
assetTypes.forEach((type: any) => {
|
||||
type.assets?.forEach((a: any) => ids.push(a.asset_id));
|
||||
});
|
||||
return ids;
|
||||
}, [assetTypes]);
|
||||
|
||||
const isAllSelected = selectedAssets.size > 0 && selectedAssets.size === allAssetIds.length;
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedAssets(new Set(allAssetIds));
|
||||
} else {
|
||||
setSelectedAssets(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectCategory = (type: any, checked: boolean) => {
|
||||
const newSelected = new Set(selectedAssets);
|
||||
type.assets?.forEach((a: any) => {
|
||||
if (checked) {
|
||||
newSelected.add(a.asset_id);
|
||||
} else {
|
||||
newSelected.delete(a.asset_id);
|
||||
}
|
||||
});
|
||||
setSelectedAssets(newSelected);
|
||||
};
|
||||
|
||||
const handleSelectAsset = (assetId: number, checked: boolean) => {
|
||||
const newSelected = new Set(selectedAssets);
|
||||
if (checked) {
|
||||
newSelected.add(assetId);
|
||||
} else {
|
||||
newSelected.delete(assetId);
|
||||
}
|
||||
setSelectedAssets(newSelected);
|
||||
};
|
||||
|
||||
const handleApply = async () => {
|
||||
if (!fromDate || !tillDate) {
|
||||
alert("Please select both From and Till dates.");
|
||||
return;
|
||||
}
|
||||
if (selectedAssets.size === 0) {
|
||||
alert("Please select at least one asset.");
|
||||
return;
|
||||
}
|
||||
|
||||
let siteIds: number[] = [];
|
||||
if (localSite === 'All Sites') {
|
||||
sites.forEach((s: any) => {
|
||||
if (s.site_id) {
|
||||
siteIds.push(...String(s.site_id).split(',').map(id => Number(id.trim())));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
siteIds = String(localSite).split(',').map(id => Number(id.trim()));
|
||||
}
|
||||
|
||||
const payload = {
|
||||
dbName: user?.db_name,
|
||||
asset_id: Array.from(selectedAssets),
|
||||
site_id: siteIds,
|
||||
fromDate: fromDate,
|
||||
toDate: tillDate,
|
||||
user_id: Number(user?.user_id),
|
||||
audit_value: reason,
|
||||
isBulkDelete: true,
|
||||
audit_type: auditType
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await activityFeedsService.bulkDelete(payload);
|
||||
alert("Bulk delete successful!");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
console.error("Bulk delete failed", err);
|
||||
alert("Bulk delete failed. " + (err?.response?.data?.message || err.message || ""));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={!isLoading ? onClose : undefined}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
borderRadius: 2,
|
||||
bgcolor: '#0f172a',
|
||||
color: '#f8fafc',
|
||||
border: '1px solid #1e293b',
|
||||
p: 1
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', px: 2, pt: 1 }}>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
endIcon={<CloseIcon sx={{ fontSize: 16 }} />}
|
||||
sx={{ color: '#FCC320', fontSize: '0.8rem', fontWeight: 'bold' }}
|
||||
disabled={isLoading}
|
||||
>
|
||||
CLOSE
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ px: 2, pb: 1 }}>
|
||||
<Box sx={{ borderBottom: '1px solid #1e293b', mb: 2 }} />
|
||||
</Box>
|
||||
|
||||
<DialogContent sx={{ py: 0, '&::-webkit-scrollbar': { width: '6px' }, '&::-webkit-scrollbar-thumb': { bgcolor: '#334155', borderRadius: '4px' } }}>
|
||||
{/* SITE SELECTION */}
|
||||
<Typography sx={{ color: '#FCC320', fontWeight: 'bold', mb: 1, fontSize: '0.9rem' }}>SITE</Typography>
|
||||
<Select
|
||||
value={localSite}
|
||||
onChange={(e) => setLocalSite(e.target.value as string)}
|
||||
size="small"
|
||||
fullWidth
|
||||
sx={{
|
||||
mb: 3,
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
color: '#f8fafc',
|
||||
borderRadius: 1,
|
||||
'& fieldset': { borderColor: '#334155' },
|
||||
'&:hover fieldset': { borderColor: '#475569' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#3b82f6' },
|
||||
'& .MuiSelect-icon': { color: '#f8fafc' }
|
||||
}}
|
||||
>
|
||||
<MenuItem value="All Sites">All Sites</MenuItem>
|
||||
{sites.map((s: any) => (
|
||||
<MenuItem key={s.site_id} value={s.site_id}>{s.site_name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{/* DATE RANGE */}
|
||||
<Typography sx={{ color: '#FCC320', fontWeight: 'bold', mb: 1, fontSize: '0.9rem' }}>DATE RANGE</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5 }}>From</Typography>
|
||||
<TextField
|
||||
type="date"
|
||||
fullWidth
|
||||
size="small"
|
||||
value={fromDate}
|
||||
onChange={(e) => setFromDate(e.target.value)}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 1,
|
||||
color: '#f8fafc',
|
||||
'& fieldset': { borderColor: '#334155' },
|
||||
'&:hover fieldset': { borderColor: '#475569' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#3b82f6' }
|
||||
},
|
||||
'& input[type="date"]::-webkit-calendar-picker-indicator': { filter: 'invert(1)', cursor: 'pointer' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5 }}>Till</Typography>
|
||||
<TextField
|
||||
type="date"
|
||||
fullWidth
|
||||
size="small"
|
||||
value={tillDate}
|
||||
onChange={(e) => setTillDate(e.target.value)}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 1,
|
||||
color: '#f8fafc',
|
||||
'& fieldset': { borderColor: '#334155' },
|
||||
'&:hover fieldset': { borderColor: '#475569' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#3b82f6' }
|
||||
},
|
||||
'& input[type="date"]::-webkit-calendar-picker-indicator': { filter: 'invert(1)', cursor: 'pointer' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* AUDIT TYPE */}
|
||||
<Typography sx={{ color: '#FCC320', fontWeight: 'bold', mb: 0.5, fontSize: '0.9rem' }}>AUDIT TYPE</Typography>
|
||||
<RadioGroup row value={auditType} onChange={(e) => setAuditType(e.target.value)} sx={{ mb: 2 }}>
|
||||
<FormControlLabel value="manual" control={<Radio size="small" sx={{ color: '#64748b', '&.Mui-checked': { color: '#3b82f6' } }} />} label={<Typography variant="body2" color="#cbd5e1">manual</Typography>} />
|
||||
<FormControlLabel value="auto audit" control={<Radio size="small" sx={{ color: '#64748b', '&.Mui-checked': { color: '#3b82f6' } }} />} label={<Typography variant="body2" color="#cbd5e1">auto audit</Typography>} />
|
||||
</RadioGroup>
|
||||
|
||||
{/* ASSETS */}
|
||||
<Typography sx={{ color: '#FCC320', fontWeight: 'bold', mb: 1, fontSize: '0.9rem' }}>ASSETS</Typography>
|
||||
<Box sx={{ height: 250, overflowY: 'auto', border: '1px solid #1e293b', borderRadius: 1, p: 1, mb: 3, bgcolor: '#0b1121', '&::-webkit-scrollbar': { width: '6px' }, '&::-webkit-scrollbar-thumb': { bgcolor: '#334155', borderRadius: '4px' } }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={isAllSelected && allAssetIds.length > 0}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
sx={{ color: '#64748b', '&.Mui-checked': { color: '#3b82f6' } }}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2" fontWeight="bold" color="#f8fafc">SELECT ALL</Typography>}
|
||||
sx={{ width: '100%', m: 0 }}
|
||||
/>
|
||||
|
||||
{assetTypes.map((type: any) => {
|
||||
if (!type.assets || type.assets.length === 0) return null;
|
||||
const categoryAssetIds = type.assets.map((a: any) => a.asset_id);
|
||||
const isCategorySelected = categoryAssetIds.every((id: number) => selectedAssets.has(id));
|
||||
const isCategoryIndeterminate = !isCategorySelected && categoryAssetIds.some((id: number) => selectedAssets.has(id));
|
||||
|
||||
return (
|
||||
<Box key={type.type_id} sx={{ ml: 1, mt: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={isCategorySelected}
|
||||
indeterminate={isCategoryIndeterminate}
|
||||
onChange={(e) => handleSelectCategory(type, e.target.checked)}
|
||||
sx={{ color: '#64748b', '&.Mui-checked, &.MuiCheckbox-indeterminate': { color: '#3b82f6' } }}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2" fontWeight="bold" color="#cbd5e1" sx={{ textTransform: 'uppercase' }}>{type.type_name.replace(/_/g, ' ')}</Typography>}
|
||||
sx={{ width: '100%', m: 0 }}
|
||||
/>
|
||||
<Box sx={{ ml: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
{type.assets.map((asset: any) => (
|
||||
<FormControlLabel
|
||||
key={asset.asset_id}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectedAssets.has(asset.asset_id)}
|
||||
onChange={(e) => handleSelectAsset(asset.asset_id, e.target.checked)}
|
||||
sx={{ color: '#475569', '&.Mui-checked': { color: '#3b82f6' }, py: 0.5 }}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2" color="#94a3b8">{asset.asset_name.replace(/_/g, ' ')}</Typography>}
|
||||
sx={{ m: 0 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* REASON */}
|
||||
<Typography sx={{ color: '#FCC320', fontWeight: 'bold', mb: 0.5, fontSize: '0.9rem' }}>REASON</Typography>
|
||||
<RadioGroup value={reason} onChange={(e) => setReason(e.target.value)} sx={{ mb: 2 }}>
|
||||
{REASONS.map(r => (
|
||||
<FormControlLabel
|
||||
key={r}
|
||||
value={r}
|
||||
control={<Radio size="small" sx={{ color: '#64748b', '&.Mui-checked': { color: '#3b82f6' } }} />}
|
||||
label={<Typography variant="body2" color="#cbd5e1">{r}</Typography>}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1, mb: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleApply}
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
bgcolor: '#3b82f6',
|
||||
color: '#fff',
|
||||
'&:hover': { bgcolor: '#2563eb' },
|
||||
textTransform: 'none',
|
||||
fontWeight: 'bold',
|
||||
px: 4,
|
||||
boxShadow: 'none',
|
||||
'&.Mui-disabled': { bgcolor: 'rgba(59, 130, 246, 0.3)', color: 'rgba(255,255,255,0.3)' }
|
||||
}}
|
||||
>
|
||||
{isLoading ? <CircularProgress size={24} color="inherit" /> : 'Apply'}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import HistoryIcon from '@mui/icons-material/History';
|
||||
import { useFeedStore } from '../../store/feedStore';
|
||||
import { FeedFilters } from '../../pages/activity-feeds/FeedFilters';
|
||||
import { activityFeedsService } from '../../api/activityFeedsService';
|
||||
import { BulkDeleteDialog } from './BulkDeleteDialog';
|
||||
|
||||
const DEBOUNCE_MS = 600; // wait 600ms after user stops changing before committing to store
|
||||
|
||||
@@ -112,6 +113,7 @@ export const TopFilterBar: React.FC = () => {
|
||||
};
|
||||
|
||||
const [historyDates, setHistoryDates] = useState<any[]>([]);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
const [isHistoryDialogOpen, setIsHistoryDialogOpen] = useState(false);
|
||||
const [selectedHistoryDate, setSelectedHistoryDate] = useState('2026-06-12');
|
||||
@@ -199,6 +201,13 @@ export const TopFilterBar: React.FC = () => {
|
||||
>
|
||||
RECTIFICATION HISTORY
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
sx={{ borderColor: '#FCC320', color: '#FCC320', '&:hover': { bgcolor: 'rgba(252, 195, 32, 0.1)', borderColor: '#FCC320' }, borderRadius: 2, textTransform: 'none', fontWeight: 'bold' }}
|
||||
>
|
||||
BULK DELETE
|
||||
</Button>
|
||||
<Dialog
|
||||
open={isHistoryDialogOpen}
|
||||
onClose={() => setIsHistoryDialogOpen(false)}
|
||||
@@ -486,6 +495,7 @@ export const TopFilterBar: React.FC = () => {
|
||||
)}
|
||||
|
||||
<FeedFilters />
|
||||
<BulkDeleteDialog open={isBulkDeleteDialogOpen} onClose={() => setIsBulkDeleteDialogOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
<App />,
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user