feat: implement activity feed module with dynamic filtering, state management, and asset selection components
This commit is contained in:
@@ -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