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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -1,5 +1,6 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { MainLayout } from './components/layout/MainLayout';
import { ProtectedRoute } from './components/ProtectedRoute';
@@ -46,6 +47,7 @@ import { AuditSession } from './pages/audit-session/AuditSession';
export default function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<BrowserRouter>
<Routes>
{/* Public Routes */}

View File

@@ -7,17 +7,45 @@ export const activityFeedsService = {
},
getHistoryDates: async (isRectificationEnabled: boolean) => {
// Requires dbName and siteIds, which should be handled by axios interceptors or passed in
const params = new URLSearchParams({
isRectificationEnabled: String(isRectificationEnabled)
const response = await axiosClient.get('/api/audit/anomaly/get_history_dates', {
params: { isRectificationEnabled }
});
const response = await axiosClient.get(`/api/audit/anomaly/get_history_dates?${params.toString()}`);
return response;
},
getHistory: async (params: any) => {
const queryParams = new URLSearchParams(params);
const response = await axiosClient.get(`/api/audit/anomaly/history?${queryParams.toString()}`);
if (params.isRectificationEnabled) {
const cleanParams = { ...params };
delete cleanParams.auditStatus;
const response = await axiosClient.get('/api/audit/anomaly/history', { params: cleanParams });
return response;
}
if (params.auditStatus === 'False Audits' || params.isTrueFalse === 'true') {
const pageIndex = params.pageSize ? Math.floor(params.pageNo / params.pageSize) + 1 : 1;
const falseParams: any = {
fromDate: params.fromDate || '',
toDate: params.tillDate || '',
asset_id: params.assetsIds || '',
page: pageIndex,
limit: params.pageSize || 20,
site_id: params.siteIds || ''
};
if (params.dbName) {
falseParams.dbName = params.dbName;
}
const response: any = await activityFeedsService.getFalseAnomaly(falseParams);
const resultObj = response?.result || response;
return {
anomalies: resultObj?.data || [],
count: resultObj?.totalCount || 0
};
}
const cleanParams = { ...params };
delete cleanParams.auditStatus;
const response = await axiosClient.get('/api/audit/anomaly/history', { params: cleanParams });
return response;
},
@@ -27,9 +55,27 @@ export const activityFeedsService = {
return response;
},
updateAnomaly: async (anomalies: any[], isRectificationEnabled: boolean) => {
const response = await axiosClient.post('/api/audit/anomaly', anomalies, {
params: { isRectificationEnabled }
});
return response;
},
updateAnomalyInDashboard: async (payload: { site_id: string, anomaly_id: number[] }) => {
const response = await axiosClient.post(`/api/audit/auditor/anomaly/update`, payload);
return response;
},
getOrganizationSites: async (org_id: string) => {
const params = new URLSearchParams({ org_id });
const response = await axiosClient.get(`/api/dashboard/Master/site?${params.toString()}`);
const response = await axiosClient.get('/api/dashboard/Master/site', {
params: { org_id }
});
return response;
},
getFalseAnomaly: async (params: any) => {
const response = await axiosClient.get('/api/audit/asset/get-false-Anomaly', { params });
return response;
}
};

View File

@@ -1,9 +1,10 @@
import React, { useState, useEffect, useRef } from 'react';
import { Box, Button, FormControl, Select, MenuItem } from '@mui/material';
import { Box, Button, FormControl, Select, MenuItem, Chip, Typography, TextField, CircularProgress, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
import FilterListIcon from '@mui/icons-material/FilterList';
import HistoryIcon from '@mui/icons-material/History';
import { useFeedStore } from '../../store/feedStore';
import { FeedFilters } from '../../pages/activity-feeds/FeedFilters';
import { activityFeedsService } from '../../api/activityFeedsService';
const DEBOUNCE_MS = 600; // wait 600ms after user stops changing before committing to store
@@ -12,22 +13,42 @@ export const TopFilterBar: React.FC = () => {
sites,
selectedSite, setSelectedSite,
auditStatus, setAuditStatus,
setIsFiltersModalOpen
setIsFiltersModalOpen,
filterFromDate, setFilterFromDate,
filterTillDate, setFilterTillDate,
filterMinChainage, setFilterMinChainage,
filterMaxChainage, setFilterMaxChainage,
selectedAssetIds, setSelectedAssetIds,
assetTypes,
searchAnomalyId, setSearchAnomalyId,
filterPlaza, setFilterPlaza,
isRectificationEnabled, setIsRectificationEnabled,
addTab, selectedTabIndex, setSelectedTabIndex, tabs
} = useFeedStore();
// Local draft values — these do NOT trigger API calls
const [localAuditStatus, setLocalAuditStatus] = useState(auditStatus || 'Auto Audits');
const [localAuditStatus, setLocalAuditStatus] = useState(auditStatus || 'Manual');
const [localSiteId, setLocalSiteId] = useState<string>(selectedSite?.site_id || 'All Sites');
const [localSearchId, setLocalSearchId] = useState(searchAnomalyId || '');
const [localPlaza, setLocalPlaza] = useState(filterPlaza || '');
// Sync local state when store changes externally (e.g., on initial load)
useEffect(() => {
setLocalAuditStatus(auditStatus || 'Auto Audits');
setLocalAuditStatus(auditStatus || 'Manual');
}, [auditStatus]);
useEffect(() => {
setLocalSiteId(selectedSite?.site_id || 'All Sites');
}, [selectedSite?.site_id]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
setLocalSearchId(searchAnomalyId || '');
}, [searchAnomalyId]);
useEffect(() => {
setLocalPlaza(filterPlaza || '');
}, [filterPlaza]);
// Debounced commit for auditStatus
const auditDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleAuditChange = (value: string) => {
@@ -51,14 +72,114 @@ export const TopFilterBar: React.FC = () => {
}, DEBOUNCE_MS);
};
// Debounced commit for Search ID
const searchDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleSearchChange = (value: string) => {
setLocalSearchId(value);
if (searchDebounce.current) clearTimeout(searchDebounce.current);
searchDebounce.current = setTimeout(() => {
setSearchAnomalyId(value); // triggers API refetch
}, DEBOUNCE_MS);
};
// Debounced commit for Plaza
const plazaDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
const handlePlazaChange = (value: string) => {
setLocalPlaza(value);
if (plazaDebounce.current) clearTimeout(plazaDebounce.current);
plazaDebounce.current = setTimeout(() => {
setFilterPlaza(value); // triggers API refetch
}, DEBOUNCE_MS);
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (auditDebounce.current) clearTimeout(auditDebounce.current);
if (siteDebounce.current) clearTimeout(siteDebounce.current);
if (searchDebounce.current) clearTimeout(searchDebounce.current);
if (plazaDebounce.current) clearTimeout(plazaDebounce.current);
};
}, []);
// Map asset IDs to human-readable names for displaying in filter chips
const getAssetName = (id: number) => {
for (const type of assetTypes) {
const found = type.assets.find((a: any) => a.asset_id === id);
if (found) return found.asset_name.replace(/_/g, ' ');
}
return `Asset: ${id}`;
};
const [historyDates, setHistoryDates] = useState<any[]>([]);
const [loadingHistory, setLoadingHistory] = useState(false);
const [isHistoryDialogOpen, setIsHistoryDialogOpen] = useState(false);
const [selectedHistoryDate, setSelectedHistoryDate] = useState('2026-06-12');
const handleHistoryClick = async () => {
setIsHistoryDialogOpen(true);
setLoadingHistory(true);
try {
const dates: any = await activityFeedsService.getHistoryDates(true);
setHistoryDates(Array.isArray(dates) ? dates : (dates?.responseData || []));
} catch (err) {
console.error("Failed to load history dates", err);
} finally {
setLoadingHistory(false);
}
};
const handleSelectDate = (date: string) => {
const existingIndex = tabs.findIndex(t => t.date === date && t.isRectificationTab);
if (existingIndex !== -1) {
setSelectedTabIndex(existingIndex);
} else {
addTab({
date: date,
isRectificationTab: true,
totalAnomalyCount: 0,
anomalies: [],
anomaliesCopy: [],
selectedAnomalyIndex: 0,
filters: {},
page: 0,
rowsPerPage: 20
});
}
};
const handleViewHistory = () => {
if (!selectedHistoryDate) return;
handleSelectDate(selectedHistoryDate);
setIsHistoryDialogOpen(false);
};
const activeTab = tabs[selectedTabIndex];
const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab;
// Filter check
const isSiteFiltered = selectedSite && selectedSite.site_id !== 'All Sites';
const isStatusFiltered = auditStatus !== 'Manual'; // Treat Manual as default, remove 'All' check
const isDateFiltered = !!(filterFromDate || filterTillDate);
const isChainageFiltered = !!(filterMinChainage || filterMaxChainage);
const isAssetsFiltered = selectedAssetIds.length > 0;
const isSearchFiltered = !!searchAnomalyId;
const isPlazaFiltered = !!filterPlaza;
const isAnyFilterActive = isSiteFiltered || isStatusFiltered || isDateFiltered || isChainageFiltered || isAssetsFiltered || isSearchFiltered || isPlazaFiltered;
const handleClearAllFilters = () => {
setSelectedSite({ site_id: 'All Sites', site_name: 'All Sites' });
setAuditStatus('Manual');
setFilterFromDate('');
setFilterTillDate('');
setFilterMinChainage('');
setFilterMaxChainage('');
setSelectedAssetIds([]);
setSearchAnomalyId('');
setFilterPlaza('');
};
return (
<>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 1.5, px: 2, py: 1.5, bgcolor: '#0b1121' }}>
@@ -73,12 +194,113 @@ export const TopFilterBar: React.FC = () => {
<Button
variant="contained"
startIcon={<HistoryIcon />}
onClick={handleHistoryClick}
sx={{ bgcolor: 'rgba(255,255,255,0.1)', color: '#fff', '&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }, borderRadius: 2, textTransform: 'none', fontWeight: 'bold', boxShadow: 'none' }}
>
RECTIFICATION HISTORY
</Button>
<Dialog
open={isHistoryDialogOpen}
onClose={() => setIsHistoryDialogOpen(false)}
slotProps={{
paper: {
sx: {
bgcolor: '#0f172a',
color: '#f8fafc',
border: '1px solid #1e293b',
minWidth: 320,
maxWidth: 450,
p: 1,
}
}
}}
>
<DialogTitle sx={{ fontWeight: 'bold', pb: 1 }}>Rectification History</DialogTitle>
<DialogContent sx={{ py: 1 }}>
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 2 }}>
Enter a date (YYYY-MM-DD) or pick one from the list to view historical rectification records.
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#94a3b8', mb: 1 }}>History Date</Typography>
<TextField
type="date"
value={selectedHistoryDate}
onChange={(e) => setSelectedHistoryDate(e.target.value)}
fullWidth
sx={{
mb: 3,
'& .MuiOutlinedInput-root': {
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',
}
}}
/>
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 1, fontWeight: 'bold' }}>
Quick Select (Audited Dates)
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, maxHeight: 150, overflowY: 'auto', p: 0.5, border: '1px solid #1e293b', borderRadius: 1, bgcolor: '#0b1121' }}>
{loadingHistory ? (
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'center', py: 2 }}>
<CircularProgress size={20} color="inherit" />
</Box>
) : historyDates.length === 0 ? (
<Typography variant="body2" sx={{ color: '#64748b', p: 1 }}>No dates found</Typography>
) : (
historyDates.map((d) => {
const dateStr = d.audited_on ? d.audited_on.split(' ')[0] : '';
return (
<Chip
key={d.audited_on}
label={dateStr || d.audited_on}
onClick={() => setSelectedHistoryDate(dateStr || d.audited_on)}
sx={{
bgcolor: selectedHistoryDate === (dateStr || d.audited_on) ? '#3b82f6' : '#1e293b',
color: '#f8fafc',
'&:hover': { bgcolor: '#334155' },
borderRadius: 1,
cursor: 'pointer',
border: '1px solid transparent',
borderColor: selectedHistoryDate === (dateStr || d.audited_on) ? '#60a5fa' : 'transparent',
}}
/>
);
})
)}
</Box>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button
onClick={() => setIsHistoryDialogOpen(false)}
sx={{ color: '#94a3b8', textTransform: 'none', fontWeight: 'bold' }}
>
Cancel
</Button>
<Button
onClick={handleViewHistory}
variant="contained"
disabled={!selectedHistoryDate}
sx={{
bgcolor: '#3b82f6',
color: '#fff',
textTransform: 'none',
fontWeight: 'bold',
'&:hover': { bgcolor: '#2563eb' },
'&.Mui-disabled': { bgcolor: 'rgba(59, 130, 246, 0.3)', color: 'rgba(255,255,255,0.3)' }
}}
>
View History
</Button>
</DialogActions>
</Dialog>
{/* Audit Status — local state, debounced commit */}
{!isRectActive && (
<FormControl size="small" sx={{ bgcolor: 'rgba(255,255,255,0.1)', borderRadius: 2, minWidth: 140 }}>
<Select
value={localAuditStatus}
@@ -88,9 +310,9 @@ export const TopFilterBar: React.FC = () => {
<MenuItem value="Manual">Manual</MenuItem>
<MenuItem value="Auto Audits">Auto Audits</MenuItem>
<MenuItem value="False Audits">False Audits</MenuItem>
<MenuItem value="All">All</MenuItem>
</Select>
</FormControl>
)}
{/* Site — local state, debounced commit */}
<FormControl size="small" sx={{ bgcolor: 'rgba(255,255,255,0.1)', borderRadius: 2, minWidth: 140 }}>
@@ -108,13 +330,161 @@ export const TopFilterBar: React.FC = () => {
</Select>
</FormControl>
{/* Search ID Textfield */}
<TextField
size="small"
placeholder="Search ID..."
value={localSearchId}
onChange={(e) => handleSearchChange(e.target.value)}
slotProps={{
input: {
sx: {
borderRadius: 2,
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
fontWeight: 'bold',
width: 150,
'& fieldset': { border: 'none' },
'& .MuiInputBase-input': { py: 1, px: 2, '&::placeholder': { color: 'rgba(255,255,255,0.5)', opacity: 1 } }
}
}
}}
/>
{/* Search Plaza Textfield */}
<TextField
size="small"
placeholder="Search Plaza..."
value={localPlaza}
onChange={(e) => handlePlazaChange(e.target.value)}
slotProps={{
input: {
sx: {
borderRadius: 2,
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
fontWeight: 'bold',
width: 150,
'& fieldset': { border: 'none' },
'& .MuiInputBase-input': { py: 1, px: 2, '&::placeholder': { color: 'rgba(255,255,255,0.5)', opacity: 1 } }
}
}
}}
/>
<Button
variant="contained"
sx={{ bgcolor: 'rgba(255,255,255,0.1)', color: '#fff', '&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }, borderRadius: 2, textTransform: 'none', fontWeight: 'bold', boxShadow: 'none' }}
onClick={() => setIsRectificationEnabled(!isRectificationEnabled)}
sx={{
bgcolor: isRectificationEnabled ? '#ff4d4f' : 'rgba(255,255,255,0.1)',
color: '#fff',
'&:hover': { bgcolor: isRectificationEnabled ? '#ff7875' : 'rgba(255,255,255,0.2)' },
borderRadius: 2,
textTransform: 'none',
fontWeight: 'bold',
boxShadow: 'none'
}}
>
ENABLE RECTIFICATION
{isRectificationEnabled ? 'DISABLE RECTIFICATION' : 'ENABLE RECTIFICATION'}
</Button>
</Box>
{/* Active Filter Chips Bar */}
{isAnyFilterActive && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 1, px: 2, py: 1, bgcolor: '#0b1121', borderTop: '1px solid rgba(255,255,255,0.05)', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 0.5 }}>
Active Filters:
</Typography>
{isSiteFiltered && (
<Chip
label={`Site: ${selectedSite.site_name}`}
size="small"
onDelete={() => setSelectedSite({ site_id: 'All Sites', site_name: 'All Sites' })}
sx={{ bgcolor: 'rgba(59, 130, 246, 0.1)', color: '#60a5fa', border: '1px solid rgba(59, 130, 246, 0.2)', '& .MuiChip-deleteIcon': { color: '#60a5fa' } }}
/>
)}
{isStatusFiltered && (
<Chip
label={`Status: ${auditStatus}`}
size="small"
onDelete={() => setAuditStatus('Manual')}
sx={{ bgcolor: 'rgba(16, 185, 129, 0.1)', color: '#34d399', border: '1px solid rgba(16, 185, 129, 0.2)', '& .MuiChip-deleteIcon': { color: '#34d399' } }}
/>
)}
{isDateFiltered && (
<Chip
label={
filterFromDate && filterTillDate
? `Date: ${filterFromDate} to ${filterTillDate}`
: filterFromDate
? `Date: From ${filterFromDate}`
: `Date: Till ${filterTillDate}`
}
size="small"
onDelete={() => { setFilterFromDate(''); setFilterTillDate(''); }}
sx={{ bgcolor: 'rgba(234, 179, 8, 0.1)', color: '#facc15', border: '1px solid rgba(234, 179, 8, 0.2)', '& .MuiChip-deleteIcon': { color: '#facc15' } }}
/>
)}
{isChainageFiltered && (
<Chip
label={
filterMinChainage && filterMaxChainage
? `Chainage: ${filterMinChainage} - ${filterMaxChainage}`
: filterMinChainage
? `Chainage: >= ${filterMinChainage}`
: `Chainage: <= ${filterMaxChainage}`
}
size="small"
onDelete={() => { setFilterMinChainage(''); setFilterMaxChainage(''); }}
sx={{ bgcolor: 'rgba(168, 85, 247, 0.1)', color: '#c084fc', border: '1px solid rgba(168, 85, 247, 0.2)', '& .MuiChip-deleteIcon': { color: '#c084fc' } }}
/>
)}
{isAssetsFiltered && (
<Chip
label={
selectedAssetIds.length <= 2
? `Assets: ${selectedAssetIds.map(getAssetName).join(', ')}`
: `Assets: ${selectedAssetIds.length} selected`
}
size="small"
onDelete={() => setSelectedAssetIds([])}
sx={{ bgcolor: 'rgba(239, 68, 68, 0.1)', color: '#f87171', border: '1px solid rgba(239, 68, 68, 0.2)', '& .MuiChip-deleteIcon': { color: '#f87171' } }}
/>
)}
{isSearchFiltered && (
<Chip
label={`ID: ${searchAnomalyId}`}
size="small"
onDelete={() => setSearchAnomalyId('')}
sx={{ bgcolor: 'rgba(59, 130, 246, 0.1)', color: '#60a5fa', border: '1px solid rgba(59, 130, 246, 0.2)', '& .MuiChip-deleteIcon': { color: '#60a5fa' } }}
/>
)}
{isPlazaFiltered && (
<Chip
label={`Plaza: ${filterPlaza}`}
size="small"
onDelete={() => setFilterPlaza('')}
sx={{ bgcolor: 'rgba(59, 130, 246, 0.1)', color: '#60a5fa', border: '1px solid rgba(59, 130, 246, 0.2)', '& .MuiChip-deleteIcon': { color: '#60a5fa' } }}
/>
)}
<Button
size="small"
onClick={handleClearAllFilters}
sx={{ color: '#94a3b8', fontSize: '0.75rem', textTransform: 'none', '&:hover': { color: '#f8fafc', bgcolor: 'transparent' }, ml: 'auto' }}
>
Clear All
</Button>
</Box>
)}
<FeedFilters />
</>
);

View File

@@ -1,11 +1,8 @@
import React, { useState } from 'react';
import { AppBar, Toolbar, Typography, Box, IconButton, Menu, MenuItem, Button, Select, FormControl } from '@mui/material';
import { AppBar, Toolbar, Box, IconButton, Menu, MenuItem, Button } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import AccountCircle from '@mui/icons-material/AccountCircle';
import FilterListIcon from '@mui/icons-material/FilterList';
import HistoryIcon from '@mui/icons-material/History';
import { useAuthStore } from '../../store/authStore';
import { useFeedStore } from '../../store/feedStore';
import { useNavigate, useLocation } from 'react-router-dom';
interface HeaderProps {
@@ -14,14 +11,11 @@ interface HeaderProps {
export const Header: React.FC<HeaderProps> = ({ toggleSidebar }) => {
const { user, logout } = useAuthStore();
const { sites, selectedSite, setSelectedSite, auditStatus, setAuditStatus, setIsFiltersModalOpen } = useFeedStore();
const navigate = useNavigate();
const location = useLocation();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const orgName = user?.org_id || user?.db_name || 'TakeLeep'; // Adjust based on your User model
const showNav = !location.pathname.includes('dashboard');
const isActivityFeeds = location.pathname.includes('activity-feeds');
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);

View File

@@ -1,18 +1,103 @@
import React from 'react';
import { Box, Toolbar, CssBaseline } from '@mui/material';
import { Outlet, useLocation } from 'react-router-dom';
import React, { useEffect } from 'react';
import { Box, Toolbar } from '@mui/material';
import { Outlet } from 'react-router-dom';
import { Header } from './Header';
import { useFeedStore } from '../../store/feedStore';
import { useAuthStore } from '../../store/authStore';
import { activityFeedsService } from '../../api/activityFeedsService';
export const MainLayout: React.FC = () => {
const location = useLocation();
const { isInitialized, setAssets, setAssetTypes, setSites, setSelectedSite, setIsInitialized } = useFeedStore();
const { user } = useAuthStore();
useEffect(() => {
if (isInitialized) return;
const loadInitialMetadata = async () => {
try {
// Fetch Assets
const assetRes: any = await activityFeedsService.getAssets();
if (assetRes && assetRes.asset_types) {
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];
});
setAssets(flattenedAssets);
setAssetTypes(assetRes.asset_types);
}
// Fetch Sites
if (user?.org_id) {
const sitesRes: any = await activityFeedsService.getOrganizationSites(user.org_id);
let parsedSites: any[] = [];
if (sitesRes?.responseData?.[0]?.records) {
parsedSites = sitesRes.responseData[0].records;
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) {
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 => {
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);
// ONLY override selectedSite if there isn't a valid one already in the store
const currentSelectedSite = useFeedStore.getState().selectedSite;
if (currentSelectedSite && currentSelectedSite.site_id) {
if (currentSelectedSite.site_id !== 'All Sites') {
// Verify it still exists in the fetched sites list
const exists = parsedSites.some(s => String(s.site_id) === String(currentSelectedSite.site_id));
if (!exists) {
setSelectedSite({ site_id: 'All Sites', site_name: 'All Sites' });
}
}
} else {
setSelectedSite({ site_id: 'All Sites', site_name: 'All Sites' });
}
}
}
} catch (err) {
console.error("Failed to load initial metadata in MainLayout", err);
} finally {
setIsInitialized(true);
}
};
loadInitialMetadata();
}, [isInitialized, user?.org_id, setAssets, setAssetTypes, setSites, setSelectedSite, setIsInitialized]);
return (
<Box sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'background.default' }}>
<CssBaseline />
<Header />
<Box component="main" sx={{ flexGrow: 1, p: location.pathname.includes('activity-feeds') ? 0 : 0, height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Box component="main" sx={{ flexGrow: 1, p: 0, height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Toolbar /> {/* Spacer for Header */}
{/* Child routes get injected here */}

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,32 +76,66 @@ export const Login: React.FC = () => {
};
return (
<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">
<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'; }} />
<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={{ mt: 1, width: '100%' }}>
<Box component="form" sx={{ width: '100%' }}>
<TextField
margin="normal"
required
fullWidth
id="username"
placeholder="Admin"
placeholder="Username"
name="username"
autoFocus
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
InputProps={{
sx: { borderRadius: '50px', '& fieldset': { borderColor: '#738ab8' } }
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,
'& .MuiInputBase-input': { padding: '10px 14px', fontSize: '0.9rem', color: '#333' }
mb: 1.5,
'& .MuiInputBase-input': { padding: '12px 18px', fontSize: '0.9rem' }
}}
/>
<TextField
@@ -115,49 +149,65 @@ export const Login: React.FC = () => {
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
InputProps={{
sx: { borderRadius: '50px', '& fieldset': { borderColor: '#738ab8' } }
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,
mt: 1.5,
mb: 3,
'& .MuiInputBase-input': { padding: '10px 14px', fontSize: '0.9rem', color: '#333' }
'& .MuiInputBase-input': { padding: '12px 18px', fontSize: '0.9rem' }
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3, mb: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1, mb: 1 }}>
<Button
variant="contained"
disabled={!username || !password || loading}
onClick={handleLogin}
sx={{
borderRadius: '50px',
backgroundColor: '#738ab8',
width: '140px',
pt: 1, pb: 1,
backgroundColor: '#3b82f6',
color: '#fff',
fontWeight: 'bold',
width: '100%',
py: 1.5,
boxShadow: 'none',
textTransform: 'none',
fontSize: '1rem',
'&:hover': {
backgroundColor: '#5c729c',
backgroundColor: '#2563eb',
boxShadow: 'none',
},
'&.Mui-disabled': {
backgroundColor: 'rgba(59, 130, 246, 0.3)',
color: 'rgba(255, 255, 255, 0.3)'
}
}}
>
{loading ? '...' : 'LOGIN'}
{loading ? 'Logging in...' : 'LOGIN'}
</Button>
</Box>
{isInvalid && (
<Alert severity="error" sx={{ mt: 2 }}>
<Alert severity="error" sx={{ mt: 3, borderRadius: 2 }}>
Invalid Credentials
</Alert>
)}
</Box>
</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,18 +96,19 @@ 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 }}>
<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}
@@ -105,14 +118,25 @@ export const AnomalyTable: React.FC = () => {
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,7 +221,8 @@ export const FeedFilters: React.FC = () => {
fullWidth
// Zero transition — opens instantly
transitionDuration={0}
PaperProps={{
slotProps={{
paper: {
sx: {
borderRadius: 3,
bgcolor: '#0f172a',
@@ -215,6 +230,7 @@ export const FeedFilters: React.FC = () => {
border: '1px solid #1e293b',
maxHeight: '90vh'
}
}
}}
>
<DialogContent sx={{ p: 3 }}>

View File

@@ -1,93 +1,30 @@
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];
});
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);
}
// 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({
@@ -102,15 +39,27 @@ export const GlobalFeed: React.FC = () => {
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: defaultSite?.site_id === 'All Sites' ? '' : (defaultSite?.site_id || ''),
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
auto_audit: false,
isRectificationEnabled: false
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, {
@@ -123,76 +72,87 @@ export const GlobalFeed: React.FC = () => {
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>

View File

@@ -1,11 +1,37 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Typography, Button, IconButton, Chip } from '@mui/material';
import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
import { useNavigate, useLocation } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { activityFeedsService } from '../../api/activityFeedsService';
import { useAuthStore } from '../../store/authStore';
import { useFeedStore } from '../../store/feedStore';
const SAFE_CATEGORIES = [
{ id: 1, value: "not an anomaly", label: "Not an Anomaly", color: "#10b981", key: "1" },
{ id: 2, value: "vehicle occlusion", label: "Vehicle Occlusion", color: "#3b82f6", key: "2" },
{ id: 3, value: "low light condition", label: "Low Light Condition", color: "#6366f1", key: "3" },
{ id: 4, value: "duplicate", label: "Duplicate", color: "#a855f7", key: "4" },
{ id: 5, value: "gps issue", label: "GPS Issue", color: "#ec4899", key: "5" },
{ id: 6, value: "mismatch", label: "Mismatch", color: "#f43f5e", key: "6" },
{ id: 7, value: "bad image", label: "Bad Image", color: "#f97316", key: "7" },
{ id: 8, value: "patch", label: "Patch", color: "#eab308", key: "8" },
{ id: 9, value: "out of range", label: "Out of Range", color: "#84cc16", key: "9" },
{ id: 10, value: "false others", label: "False Others", color: "#64748b", key: "0" }
];
const ANOMALY_CATEGORIES = [
{ id: 1, value: "plant", label: "Plant", color: "#10b981", key: "1" },
{ id: 2, value: "bent", label: "Bent", color: "#3b82f6", key: "2" },
{ id: 3, value: "broken", label: "Broken", color: "#ef4444", key: "3" },
{ id: 4, value: "missing", label: "Missing", color: "#f97316", key: "4" },
{ id: 5, value: "plant overgrown", label: "Plant Overgrown", color: "#84cc16", key: "5" },
{ id: 6, value: "paint worn off", label: "Paint Worn Off", color: "#eab308", key: "6" },
{ id: 7, value: "dirt", label: "Dirt", color: "#78350f", key: "7" },
{ id: 8, value: "not working", label: "Not Working", color: "#ec4899", key: "8" },
{ id: 9, value: "others", label: "Others", color: "#64748b", key: "9" },
{ id: 10, value: "occlusion", label: "Occlusion", color: "#6366f1", key: "0" }
];
export const AuditSession: React.FC = () => {
const navigate = useNavigate();
const { user } = useAuthStore();
@@ -14,54 +40,177 @@ export const AuditSession: React.FC = () => {
const [loading, setLoading] = useState(true);
const [carouselIndex, setCarouselIndex] = useState(0);
// For the right sidebar state
// Form states
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
const [notes, setNotes] = useState('');
const [isAnomaly, setIsAnomaly] = useState<boolean | null>(null); // true = Anomaly, false = Safe
const [plantSubCategory, setPlantSubCategory] = useState<'in grown' | 'out grown' | null>(null);
const [notAnAnomalySubCategory, setNotAnAnomalySubCategory] = useState<'comparison error' | 'low light' | 'far asset' | 'no detection' | 'image mismatch' | null>(null);
// Premium Audit UX states
const [itemAuditStates, setItemAuditStates] = useState<('pending' | 'audited' | 'skipped')[]>([]);
const [isQuickAudit, setIsQuickAudit] = useState(() => {
const cached = localStorage.getItem('isQuickAudit');
return cached === null ? true : cached === 'true';
});
const [activeKeyFlash, setActiveKeyFlash] = useState<string | null>(null);
const [isNotesFocused, setIsNotesFocused] = useState(false);
const [showCompletionModal, setShowCompletionModal] = useState(false);
const location = useLocation();
const { tabs, selectedTabIndex } = useFeedStore();
const {
tabs, selectedTabIndex, updateTab,
selectedSite, filterFromDate, filterTillDate,
filterMinChainage, filterMaxChainage, selectedAssetIds,
searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza
} = useFeedStore();
useEffect(() => {
const navState = location.state as any;
const activeTab = tabs[selectedTabIndex] || { date: '', isRectificationTab: false, page: 0, rowsPerPage: 20 };
const activeDate = activeTab.date || '';
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
if (navState?.useExistingFeed) {
// Use anomalies from the global feed store
const activeTab = tabs[selectedTabIndex];
if (activeTab && activeTab.anomalies) {
setAnomalies(activeTab.anomalies);
setCurrentIndex(navState.startIndex || 0);
setLoading(false);
} else {
setLoading(false);
}
} else {
// Fetch 50 items for the session (default behavior from Dashboard)
const fetchSessionData = async () => {
const loadSessionData = useCallback(async () => {
try {
setLoading(true);
const selectedSiteId = selectedSite?.site_id ?? '';
const assetIdsKey = selectedAssetIds.join(',');
const isReviewMode = location.state?.isReviewMode;
const res: any = await activityFeedsService.getHistory({
date: '',
date: activeDate,
pageNo: 0,
pageSize: 50,
isTrueFalse: 'false', // Fetch unaudited
auto_audit: false,
isRectificationEnabled: false,
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
auditStatus: auditStatus,
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
isRectificationEnabled: isRectActive,
siteIds: (!selectedSiteId || selectedSiteId === 'All Sites') ? '' : selectedSiteId,
assetsIds: assetIdsKey,
fromDate: filterFromDate,
tillDate: filterTillDate,
fromChainage: filterMinChainage,
toChainage: filterMaxChainage,
anomalyIds: searchAnomalyId,
plaza: filterPlaza,
});
if (res && res.anomalies) {
setAnomalies(res.anomalies);
setCurrentIndex(0);
setCarouselIndex(0);
setSelectedCategory(null);
setNotes('');
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
} else {
setLoading(false);
}
} catch (err) {
console.error('Failed to fetch session data', err);
console.error('Failed to load session anomalies:', err);
} finally {
setLoading(false);
}
};
fetchSessionData();
}
}, [location.state, tabs, selectedTabIndex]);
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state]);
const IMAGES_AWS_PREFIX = "https://auditor-master-images.seekright.com/SeekRight/";
const handleLoadNextFeedPage = useCallback(async () => {
try {
setLoading(true);
const nextPage = (activeTab.page || 0) + 1;
const selectedSiteId = selectedSite?.site_id ?? '';
const assetIdsKey = selectedAssetIds.join(',');
const isReviewMode = location.state?.isReviewMode;
const feedPageSize = activeTab.rowsPerPage || 20;
const res: any = await activityFeedsService.getHistory({
date: activeDate,
pageNo: nextPage * feedPageSize,
pageSize: feedPageSize,
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
auditStatus: auditStatus,
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
isRectificationEnabled: isRectActive,
siteIds: (!selectedSiteId || selectedSiteId === 'All Sites') ? '' : selectedSiteId,
assetsIds: assetIdsKey,
fromDate: filterFromDate,
tillDate: filterTillDate,
fromChainage: filterMinChainage,
toChainage: filterMaxChainage,
anomalyIds: searchAnomalyId,
plaza: filterPlaza,
});
if (res && res.anomalies && res.anomalies.length > 0) {
updateTab(selectedTabIndex, {
anomalies: res.anomalies,
anomaliesCopy: res.anomalies,
page: nextPage
});
setAnomalies(res.anomalies);
setCurrentIndex(0);
setCarouselIndex(0);
setSelectedCategory(null);
setNotes('');
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
} else {
alert("No more anomalies left in the feed.");
navigate('/activity-feeds');
}
} catch (err) {
console.error('Failed to load next page of feed:', err);
} finally {
setLoading(false);
}
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, activeTab, selectedTabIndex, updateTab, navigate]);
useEffect(() => {
const navState = location.state as any;
if (navState?.useExistingFeed) {
const activeTab = tabs[selectedTabIndex];
if (activeTab && activeTab.anomalies) {
// Client-side Plaza filtering fallback check just like in AnomalyTable.tsx
const filteredAnomalies = activeTab.anomalies.filter((anomaly: any) => {
if (!filterPlaza) return true;
return String(anomaly.Plaza || '').toLowerCase().includes(filterPlaza.toLowerCase());
});
setAnomalies(filteredAnomalies);
// Find correct index in filtered list
const origSelectedAnomaly = activeTab.anomalies[navState.startIndex || 0];
const newIdx = origSelectedAnomaly
? filteredAnomalies.findIndex((a: any) => (a.id === origSelectedAnomaly.id || a._id === origSelectedAnomaly._id))
: 0;
setCurrentIndex(newIdx !== -1 ? newIdx : 0);
setItemAuditStates(filteredAnomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
setLoading(false);
} else {
setLoading(false);
}
} else if (location.state && location.state.anomalies) {
setAnomalies(location.state.anomalies);
setCurrentIndex(location.state.startIndex || 0);
setCarouselIndex(0);
setSelectedCategory(null);
setNotes('');
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
setItemAuditStates(location.state.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
if (location.state.anomalies.length > 0) {
setLoading(false);
} else {
setLoading(false);
}
} else {
loadSessionData();
}
}, [location.state, tabs, selectedTabIndex, loadSessionData, filterPlaza]);
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/";
const currentAnomaly = anomalies[currentIndex];
@@ -76,7 +225,6 @@ export const AuditSession: React.FC = () => {
}, [currentAnomaly]);
let masterImageSrc = '';
let anomalyImageSrc = '';
let latStr = 'N/A';
let longStr = 'E/A';
@@ -89,14 +237,13 @@ export const AuditSession: React.FC = () => {
masterImageSrc = '/assets/images/only_master_image_not_available.png';
}
// Parse Position
if (currentAnomaly.Pos) {
const match = String(currentAnomaly.Pos).match(/([NS][0-9.]+)([EW][0-9.]+)/i);
if (match) {
latStr = match[1];
longStr = match[2];
} else {
latStr = currentAnomaly.Pos; // Fallback
latStr = currentAnomaly.Pos;
longStr = '';
}
}
@@ -124,81 +271,269 @@ export const AuditSession: React.FC = () => {
const handleNext = () => {
if (currentIndex < anomalies.length - 1) {
setCurrentIndex(prev => prev + 1);
// Reset form
setSelectedCategory(null);
setNotes('');
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
} else {
// Done with session
const navState = location.state as any;
if (navState?.useExistingFeed) {
navigate('/activity-feeds');
} else {
navigate('/dashboard');
}
setShowCompletionModal(true);
}
};
const handlePrev = () => {
if (currentIndex > 0) {
setCurrentIndex(prev => prev - 1);
// We would ideally preserve form state for previously visited items
setSelectedCategory(null);
setNotes('');
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
}
};
const handleSaveAndNext = async () => {
const handleSkip = () => {
setItemAuditStates(prev => {
const next = [...prev];
next[currentIndex] = 'skipped';
return next;
});
handleNext();
}; const saveAuditState = useCallback(async (
anomalyVal: boolean | null,
catVal: number | null,
notesVal: string,
plantSub?: string | null,
notAnAnomalySub?: string | null
) => {
if (!currentAnomaly || !user) return;
try {
const isAnomalyBool = anomalyVal ?? true;
const auditStatusVal = isAnomalyBool ? 1 : 0;
const categories = isAnomalyBool ? ANOMALY_CATEGORIES : SAFE_CATEGORIES;
const matchedCategory = categories.find(c => c.id === catVal);
let auditValueVal = matchedCategory ? matchedCategory.value : '';
if (isAnomalyBool && catVal === 1 && plantSub) {
auditValueVal = plantSub;
} else if (!isAnomalyBool && catVal === 1 && notAnAnomalySub) {
auditValueVal = notAnAnomalySub;
}
// Construct properties matching Angular properties list:
// ["dbName", "isRectificationEnabled", "Audit_value", "Comments", "Audit_status", "Frame_Test", "Frame_Master", "Chainage", "Pos", "id", "road", "user_id", "Algorithm"]
const auditPayload = {
dbName: user.db_name || '',
isRectificationEnabled: isRectActive,
Audit_value: auditValueVal,
Comments: notesVal || '',
Audit_status: auditStatusVal,
Frame_Test: currentAnomaly.Frame_Test || '',
Frame_Master: currentAnomaly.Frame_Master || '',
Chainage: currentAnomaly.Chainage || '',
Pos: currentAnomaly.Pos || '',
id: currentAnomaly.id,
road: currentAnomaly.road || 'mcw',
user_id: user.user_id || 0,
Algorithm: currentAnomaly.Algorithm || ''
};
// Call API 1: Update anomaly
await activityFeedsService.updateAnomaly([auditPayload], isRectActive);
// Call API 2: If true anomaly, update anomaly status in dashboard to notify client portal
if (auditStatusVal === 1) {
await activityFeedsService.updateAnomalyInDashboard({
site_id: String(currentAnomaly.site_id),
anomaly_id: [currentAnomaly.id]
});
}
} catch (err) {
console.error('Failed to submit audit state:', err);
}
}, [currentAnomaly, user]);
const handleSaveAndNext = useCallback(async () => {
if (!currentAnomaly) return;
// Placeholder for save logic
// await activityFeedsService.updateAuditedAnomaly(...)
console.log('Saved:', { id: currentAnomaly.anomaly_id, isAnomaly, selectedCategory, notes });
handleNext();
};
// Keyboard shortcuts
const handleKeyDown = useCallback((e: KeyboardEvent) => {
// Prevent default on shortcuts if focused on inputs?
if (document.activeElement?.tagName === 'TEXTAREA' || document.activeElement?.tagName === 'INPUT') {
return; // Don't trigger shortcuts when typing notes
if (selectedCategory === 1) {
if ((isAnomaly === null || isAnomaly === true) && !plantSubCategory) {
alert("Please select feature before submit.");
return;
}
if (isAnomaly === false && !notAnAnomalySubCategory) {
alert("Please select feature before submit.");
return;
}
}
switch (e.key.toLowerCase()) {
setItemAuditStates(prev => {
const next = [...prev];
next[currentIndex] = 'audited';
return next;
});
// Call the API
await saveAuditState(isAnomaly, selectedCategory, notes, plantSubCategory, notAnAnomalySubCategory);
handleNext();
}, [currentIndex, currentAnomaly, isAnomaly, selectedCategory, notes, plantSubCategory, notAnAnomalySubCategory, saveAuditState, handleNext]);
// Quick Auto Advance save trigger
const handleQuickSaveAndNext = useCallback(async (
anomalyVal: boolean,
catVal: number,
plantSub?: string | null,
notAnAnomalySub?: string | null
) => {
if (!currentAnomaly) return;
setItemAuditStates(prev => {
const next = [...prev];
next[currentIndex] = 'audited';
return next;
});
// Call the API
await saveAuditState(anomalyVal, catVal, notes, plantSub, notAnAnomalySub);
if (currentIndex < anomalies.length - 1) {
setCurrentIndex(prev => prev + 1);
setSelectedCategory(null);
setNotes('');
setIsAnomaly(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
} else {
setShowCompletionModal(true);
}
}, [currentIndex, anomalies.length, currentAnomaly, notes, saveAuditState]);
// Keyboard shortcuts
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (isNotesFocused) return;
const key = e.key.toLowerCase();
// Register active flash pulse
setActiveKeyFlash(key);
setTimeout(() => setActiveKeyFlash(null), 150);
switch (key) {
case 'a':
setIsAnomaly(true);
setSelectedCategory(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
break;
case 's':
setIsAnomaly(false);
setSelectedCategory(null);
setPlantSubCategory(null);
setNotAnAnomalySubCategory(null);
break;
case '1': setSelectedCategory(1); break;
case '2': setSelectedCategory(2); break;
case '3': setSelectedCategory(3); break;
case '4': setSelectedCategory(4); break;
case '5': setSelectedCategory(5); break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '0': {
const keyMap: { [key: string]: number } = {
'1': 1, '2': 2, '3': 3, '4': 4, '5': 5,
'6': 6, '7': 7, '8': 8, '9': 9, '0': 10
};
const catId = keyMap[key];
// Special nested case: Plant is already active (under Anomaly)
if (isAnomaly === true && selectedCategory === 1) {
if (key === '1') {
setPlantSubCategory('in grown');
if (isQuickAudit) {
setTimeout(() => handleQuickSaveAndNext(true, 1, 'in grown', null), 120);
}
break;
} else if (key === '2') {
setPlantSubCategory('out grown');
if (isQuickAudit) {
setTimeout(() => handleQuickSaveAndNext(true, 1, 'out grown', null), 120);
}
break;
}
}
// Special nested case: Not an Anomaly is already active (under Safe)
if (isAnomaly === false && selectedCategory === 1) {
const subMap: { [key: string]: 'comparison error' | 'low light' | 'far asset' | 'no detection' | 'image mismatch' } = {
'1': 'comparison error',
'2': 'low light',
'3': 'far asset',
'4': 'no detection',
'5': 'image mismatch'
};
const subVal = subMap[key];
if (subVal) {
setNotAnAnomalySubCategory(subVal);
if (isQuickAudit) {
setTimeout(() => handleQuickSaveAndNext(false, 1, null, subVal), 120);
}
break;
}
}
const currentIsAnomaly = isAnomaly === null ? true : isAnomaly;
if (isAnomaly === null) {
setIsAnomaly(true);
}
setSelectedCategory(catId);
// If selecting Plant (Anomaly 1) or Not an Anomaly (Safe 1), do not auto-advance; wait for nested sub-options
if (catId === 1) {
break;
}
if (isQuickAudit) {
setTimeout(() => handleQuickSaveAndNext(currentIsAnomaly, catId), 120);
}
break;
}
case 'enter':
handleSaveAndNext();
break;
case 'arrowright':
handleSaveAndNext();
break;
case 'arrowleft':
handlePrev();
break;
case 'tab':
e.preventDefault();
handleSkip();
break;
}
}, [currentIndex, anomalies, isAnomaly, selectedCategory, notes]);
}, [currentIndex, anomalies, isAnomaly, selectedCategory, notes, plantSubCategory, isQuickAudit, isNotesFocused, handleQuickSaveAndNext, handleSaveAndNext, handleSkip]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
const handleToggleQuickAudit = (val: boolean) => {
setIsQuickAudit(val);
localStorage.setItem('isQuickAudit', String(val));
};
if (loading) {
return <Box sx={{ p: 4, color: 'white' }}>Loading session...</Box>;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh', bgcolor: '#0b1121', color: 'white', gap: 2 }}>
<Typography variant="h6">Loading session...</Typography>
</Box>
);
}
if (anomalies.length === 0) {
return (
<Box sx={{ p: 4, color: 'white' }}>
<Typography mb={2}>No unaudited anomalies found to audit.</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh', bgcolor: '#0b1121', color: 'white', gap: 2 }}>
<Typography sx={{ mb: 2 }}>No unaudited anomalies found to audit.</Typography>
<Button variant="contained" onClick={() => {
const navState = location.state as any;
navigate(navState?.useExistingFeed ? '/activity-feeds' : '/dashboard');
@@ -208,7 +543,6 @@ export const AuditSession: React.FC = () => {
}
const getSeverityBadge = () => {
// Placeholder badge logic
return <Chip label="HIGH" color="error" size="small" sx={{ borderRadius: 1, fontWeight: 'bold' }} />;
};
@@ -246,11 +580,37 @@ export const AuditSession: React.FC = () => {
{getSeverityBadge()}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography sx={{ fontWeight: 'bold', fontSize: '0.9rem' }}>{user?.first_name} {user?.last_name}</Typography>
<Typography sx={{ fontWeight: 'bold', fontSize: '0.9rem' }}>{user?.username || user?.email}</Typography>
<Button size="small" sx={{ color: '#94a3b8', border: '1px solid #334155', textTransform: 'none', borderRadius: 2 }}>? Shortcuts</Button>
</Box>
</Box>
{/* Segmented Progress Visualizer */}
<Box sx={{ display: 'flex', width: '100%', height: '4px', bgcolor: '#1e293b' }}>
{itemAuditStates.map((state, index) => {
let color = '#334155'; // pending
if (index === currentIndex) {
color = '#3b82f6'; // active current item (blue)
} else if (state === 'audited') {
color = '#22c55e'; // audited (green)
} else if (state === 'skipped') {
color = '#eab308'; // skipped (orange)
}
return (
<Box
key={index}
sx={{
flex: 1,
height: '100%',
bgcolor: color,
mr: index === itemAuditStates.length - 1 ? 0 : '1px',
transition: 'background-color 0.2s ease'
}}
/>
);
})}
</Box>
{/* Main Content Area */}
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden', p: 2, gap: 2 }}>
@@ -266,7 +626,15 @@ export const AuditSession: React.FC = () => {
</Box>
<Box sx={{ flex: 1, position: 'relative' }}>
{masterImageSrc ? (
<img src={masterImageSrc} alt="Master" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
<img
src={masterImageSrc}
alt="Master"
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.src = '/assets/images/master_image_not_available.png';
}}
/>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#334155' }}>No Master Image</Box>
)}
@@ -285,7 +653,15 @@ export const AuditSession: React.FC = () => {
{mediaItems[carouselIndex]?.type === 'video' ? (
<video src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
) : (
<img src={mediaItems[carouselIndex]?.url} alt="Anomaly" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
<img
src={mediaItems[carouselIndex]?.url}
alt="Anomaly"
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.src = '/assets/images/anomaly-placeholder.png';
}}
/>
)}
{mediaItems.length > 1 && (
@@ -322,7 +698,24 @@ export const AuditSession: React.FC = () => {
</Box>
{/* Right Side: Sidebar */}
<Box sx={{ width: 320, display: 'flex', flexDirection: 'column', gap: 3 }}>
<Box sx={{ width: 320, display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Quick-Audit Auto-Advance Toggle */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: '#0f172a', p: 1.5, borderRadius: 2, border: '1px solid #1e293b' }}>
<Box>
<Typography sx={{ fontSize: '0.8rem', fontWeight: 'bold', color: '#f8fafc' }}>Quick Audit</Typography>
<Typography sx={{ fontSize: '0.65rem', color: '#64748b' }}>Auto-advances on keypress</Typography>
</Box>
<Switch
size="small"
checked={isQuickAudit}
onChange={(e) => handleToggleQuickAudit(e.target.checked)}
sx={{
'& .MuiSwitch-switchBase.Mui-checked': { color: '#3b82f6' },
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: '#3b82f6' }
}}
/>
</Box>
{/* Anomaly/Safe Toggle */}
<Box>
@@ -336,7 +729,15 @@ export const AuditSession: React.FC = () => {
variant={isAnomaly === true ? "contained" : "outlined"}
color={isAnomaly === true ? "error" : "inherit"}
onClick={() => setIsAnomaly(true)}
sx={{ borderRadius: 2, textTransform: 'none', borderColor: '#334155', color: isAnomaly === true ? 'white' : '#cbd5e1' }}
sx={{
borderRadius: 2,
textTransform: 'none',
borderColor: '#334155',
color: isAnomaly === true ? 'white' : '#cbd5e1',
transition: 'all 0.15s ease',
border: activeKeyFlash === 'a' ? '2px solid #ef4444' : '1px solid #334155',
boxShadow: activeKeyFlash === 'a' ? '0 0 12px #ef4444' : 'none'
}}
>
Anomaly
</Button>
@@ -344,28 +745,48 @@ export const AuditSession: React.FC = () => {
fullWidth
variant={isAnomaly === false ? "contained" : "outlined"}
color={isAnomaly === false ? "success" : "inherit"}
onClick={() => setIsAnomaly(false)}
sx={{ borderRadius: 2, textTransform: 'none', borderColor: '#334155', color: isAnomaly === false ? 'white' : '#cbd5e1' }}
onClick={() => {
setIsAnomaly(false);
setSelectedCategory(null);
setPlantSubCategory(null);
}}
sx={{
borderRadius: 2,
textTransform: 'none',
borderColor: '#334155',
color: isAnomaly === false ? 'white' : '#cbd5e1',
transition: 'all 0.15s ease',
border: activeKeyFlash === 's' ? '2px solid #22c55e' : '1px solid #334155',
boxShadow: activeKeyFlash === 's' ? '0 0 12px #22c55e' : 'none'
}}
>
Safe
</Button>
</Box>
</Box>
{/* Category */}
<Box>
<Box sx={{ flex: 1.5, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<Typography sx={{ fontSize: '0.75rem', fontWeight: 'bold', color: '#94a3b8', letterSpacing: 1, mb: 1 }}>CATEGORY</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{[
{ id: 1, label: 'No Issue', color: '#22c55e' },
{ id: 2, label: 'Minor Crack', color: '#eab308' },
{ id: 3, label: 'Major Crack', color: '#f97316' },
{ id: 4, label: 'Structural Damage', color: '#ef4444' },
{ id: 5, label: 'Needs Investigation', color: '#8b5cf6' },
].map(cat => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, overflowY: 'auto', flex: 1, pr: 0.5, '&::-webkit-scrollbar': { width: '4px' }, '&::-webkit-scrollbar-thumb': { bgcolor: '#1e293b', borderRadius: '4px' } }}>
{(isAnomaly === false ? SAFE_CATEGORIES : ANOMALY_CATEGORIES).map(cat => (
<React.Fragment key={cat.id}>
<Button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
onClick={() => {
setSelectedCategory(cat.id);
const currentIsAnom = isAnomaly === null ? true : isAnomaly;
if (isAnomaly === null) {
setIsAnomaly(true);
}
// If selecting Plant (Anomaly 1) or Not an Anomaly (Safe 1), do not auto-advance; wait for nested sub-options
if (cat.id === 1) {
return;
}
if (isQuickAudit) {
setTimeout(() => handleQuickSaveAndNext(currentIsAnom, cat.id), 120);
}
}}
sx={{
justifyContent: 'flex-start',
textTransform: 'none',
@@ -373,16 +794,92 @@ export const AuditSession: React.FC = () => {
bgcolor: selectedCategory === cat.id ? 'rgba(255,255,255,0.05)' : 'transparent',
borderRadius: 2,
p: 1,
transition: 'all 0.15s ease',
border: activeKeyFlash === cat.key ? `2px solid ${cat.color}` : '1px solid transparent',
boxShadow: activeKeyFlash === cat.key ? `0 0 12px ${cat.color}` : 'none',
'&:hover': { bgcolor: 'rgba(255,255,255,0.1)' }
}}
>
<Box sx={{ width: 24, height: 24, borderRadius: '50%', bgcolor: cat.color, display: 'flex', alignItems: 'center', justifyContent: 'center', mr: 2, fontSize: '0.75rem', fontWeight: 'bold', color: 'white' }}>
{cat.id}
<Box sx={{ width: 24, height: 24, borderRadius: '50%', bgcolor: cat.color, display: 'flex', alignItems: 'center', justifyContent: 'center', mr: 2, fontSize: '0.75rem', fontWeight: 'bold', color: 'white', flexShrink: 0 }}>
{cat.key}
</Box>
{cat.label}
<Typography sx={{ fontSize: '0.875rem', textAlign: 'left' }}>{cat.label}</Typography>
</Button>
{/* Plant Nested Subcategories (Anomaly Mode) */}
{cat.id === 1 && isAnomaly === true && selectedCategory === 1 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, pl: 4, mt: 0.5, mb: 0.5 }}>
{[
{ val: 'in grown', label: 'In Grown', key: '1' },
{ val: 'out grown', label: 'Out Grown', key: '2' }
].map(sub => (
<Button
key={sub.val}
onClick={async () => {
setPlantSubCategory(sub.val as any);
if (isQuickAudit) {
setTimeout(() => handleQuickSaveAndNext(true, 1, sub.val, null), 120);
}
}}
sx={{
justifyContent: 'flex-start',
textTransform: 'none',
fontSize: '0.8rem',
color: plantSubCategory === sub.val ? 'white' : '#94a3b8',
bgcolor: plantSubCategory === sub.val ? 'rgba(59, 130, 246, 0.2)' : 'rgba(255,255,255,0.02)',
borderRadius: 1.5,
py: 0.5,
px: 1.5,
border: plantSubCategory === sub.val ? '1px solid #3b82f6' : '1px solid transparent',
'&:hover': { bgcolor: 'rgba(255,255,255,0.05)' }
}}
>
<Box sx={{ mr: 1, color: '#60a5fa', fontWeight: 'bold' }}>{sub.key}.</Box>
{sub.label}
</Button>
))}
</Box>
)}
{/* Not an Anomaly Nested Subcategories (Safe Mode) */}
{cat.id === 1 && isAnomaly === false && selectedCategory === 1 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, pl: 4, mt: 0.5, mb: 0.5 }}>
{[
{ val: 'comparison error', label: 'Comparison Error', key: '1' },
{ val: 'low light', label: 'Low Light', key: '2' },
{ val: 'far asset', label: 'Far Asset', key: '3' },
{ val: 'no detection', label: 'No Detection', key: '4' },
{ val: 'image mismatch', label: 'Image Mismatch', key: '5' }
].map(sub => (
<Button
key={sub.val}
onClick={async () => {
setNotAnAnomalySubCategory(sub.val as any);
if (isQuickAudit) {
setTimeout(() => handleQuickSaveAndNext(false, 1, null, sub.val), 120);
}
}}
sx={{
justifyContent: 'flex-start',
textTransform: 'none',
fontSize: '0.8rem',
color: notAnAnomalySubCategory === sub.val ? 'white' : '#94a3b8',
bgcolor: notAnAnomalySubCategory === sub.val ? 'rgba(59, 130, 246, 0.2)' : 'rgba(255,255,255,0.02)',
borderRadius: 1.5,
py: 0.5,
px: 1.5,
border: notAnAnomalySubCategory === sub.val ? '1px solid #3b82f6' : '1px solid transparent',
'&:hover': { bgcolor: 'rgba(255,255,255,0.05)' }
}}
>
<Box sx={{ mr: 1, color: '#10b981', fontWeight: 'bold' }}>{sub.key}.</Box>
{sub.label}
</Button>
))}
</Box>
)}
</React.Fragment>
))} </Box>
</Box>
{/* Notes */}
@@ -392,6 +889,8 @@ export const AuditSession: React.FC = () => {
component="textarea"
value={notes}
onChange={(e) => setNotes(e.target.value)}
onFocus={() => setIsNotesFocused(true)}
onBlur={() => setIsNotesFocused(false)}
placeholder="Optional observations..."
sx={{
width: '100%',
@@ -415,7 +914,17 @@ export const AuditSession: React.FC = () => {
variant="contained"
color="primary"
onClick={handleSaveAndNext}
sx={{ borderRadius: 2, py: 1.5, textTransform: 'none', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', px: 3 }}
sx={{
borderRadius: 2,
py: 1.5,
textTransform: 'none',
fontWeight: 'bold',
display: 'flex',
justifyContent: 'space-between',
px: 3,
transition: 'all 0.15s ease',
border: (activeKeyFlash === 'enter' || activeKeyFlash === 'arrowright') ? '2px solid #3b82f6' : 'none'
}}
>
Save & Next <Typography component="span" sx={{ fontSize: '0.75rem', opacity: 0.7 }}>Enter / </Typography>
</Button>
@@ -424,14 +933,32 @@ export const AuditSession: React.FC = () => {
variant="outlined"
onClick={handlePrev}
disabled={currentIndex === 0}
sx={{ flex: 1, borderRadius: 2, textTransform: 'none', borderColor: '#1e293b', color: '#94a3b8', '&:hover': { borderColor: '#334155' } }}
sx={{
flex: 1,
borderRadius: 2,
textTransform: 'none',
borderColor: '#1e293b',
color: '#94a3b8',
transition: 'all 0.15s ease',
border: activeKeyFlash === 'arrowleft' ? '2px solid #64748b' : '1px solid #1e293b',
'&:hover': { borderColor: '#334155' }
}}
>
Prev
</Button>
<Button
variant="outlined"
onClick={handleNext}
sx={{ flex: 1, borderRadius: 2, textTransform: 'none', borderColor: '#1e293b', color: '#94a3b8', '&:hover': { borderColor: '#334155' } }}
onClick={handleSkip}
sx={{
flex: 1,
borderRadius: 2,
textTransform: 'none',
borderColor: '#1e293b',
color: '#94a3b8',
transition: 'all 0.15s ease',
border: activeKeyFlash === 'tab' ? '2px solid #64748b' : '1px solid #1e293b',
'&:hover': { borderColor: '#334155' }
}}
>
Skip <span style={{ fontSize: '0.75rem', opacity: 0.7, marginLeft: 8 }}>Tab</span>
</Button>
@@ -442,7 +969,11 @@ export const AuditSession: React.FC = () => {
{/* Footer Shortcuts */}
<Box sx={{ borderTop: '1px solid #1e293b', p: 1, px: 2, display: 'flex', gap: 3, alignItems: 'center', overflowX: 'auto', '& > div': { display: 'flex', alignItems: 'center', gap: 1, whiteSpace: 'nowrap' } }}>
<Typography sx={{ fontSize: '0.75rem', color: '#64748b' }}>Keys:</Typography>
<Typography sx={{ fontSize: '0.75rem', color: '#64748b' }}>
{isNotesFocused ? "Shortcuts Paused (Typing notes)" : "Keys:"}
</Typography>
{!isNotesFocused && (
<>
<Box>
<Chip label="A" size="small" sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#1e293b', color: '#cbd5e1' }} />
<Typography sx={{ fontSize: '0.75rem', color: '#94a3b8' }}>Anomaly</Typography>
@@ -471,7 +1002,83 @@ export const AuditSession: React.FC = () => {
<Chip label="?" size="small" sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#1e293b', color: '#cbd5e1' }} />
<Typography sx={{ fontSize: '0.75rem', color: '#94a3b8' }}>Help</Typography>
</Box>
</>
)}
</Box>
{/* Session Completion In-place Modal */}
<Dialog
open={showCompletionModal}
onClose={() => setShowCompletionModal(false)}
slotProps={{
paper: {
sx: {
bgcolor: '#0f172a',
color: '#f8fafc',
border: '1px solid #1e293b',
borderRadius: 3,
p: 2,
maxWidth: 400
}
}
}}
>
<DialogTitle sx={{ textAlign: 'center', fontWeight: 'bold', fontSize: '1.4rem', pb: 1 }}>
Session Completed! 🎉
</DialogTitle>
<DialogContent sx={{ py: 2, textAlign: 'center' }}>
<Typography variant="body1" sx={{ color: '#cbd5e1', mb: 2 }}>
{(() => {
const navState = location.state as any;
return navState?.useExistingFeed
? `You have successfully completed this feed session.`
: `You have successfully audited this session of 50 items.`;
})()}
</Typography>
<Typography variant="body2" sx={{ color: '#94a3b8' }}>
{(() => {
const navState = location.state as any;
const feedPageSize = activeTab?.rowsPerPage || 20;
return navState?.useExistingFeed
? `Ready to continue? You can audit the next page of ${feedPageSize} anomalies from the active feed in-place, or return to the active feeds page.`
: `Ready to continue? You can start the next session immediately in-place, or return to the dashboard.`;
})()}
</Typography>
</DialogContent>
<DialogActions sx={{ flexDirection: 'column', gap: 1.5, px: 3, pb: 2 }}>
{(() => {
const navState = location.state as any;
const isFeedSession = !!navState?.useExistingFeed;
const feedPageSize = activeTab?.rowsPerPage || 20;
return (
<>
<Button
fullWidth
variant="contained"
onClick={isFeedSession ? handleLoadNextFeedPage : async () => {
setShowCompletionModal(false);
await loadSessionData();
}}
sx={{ bgcolor: '#3b82f6', '&:hover': { bgcolor: '#2563eb' }, textTransform: 'none', fontWeight: 'bold', py: 1.2, borderRadius: 2 }}
>
{isFeedSession ? `Audit Next ${feedPageSize} Anomalies` : 'Audit Next 50 Items'}
</Button>
<Button
fullWidth
variant="outlined"
onClick={() => {
setShowCompletionModal(false);
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 }}
>
{isFeedSession ? 'Return to Active Feeds' : 'Return to Dashboard'}
</Button>
</>
);
})()}
</DialogActions>
</Dialog>
</Box>
);
};

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

View File

@@ -10,6 +10,7 @@ export interface User {
user_roles: string;
db_name?: string;
org_id?: string;
user_id?: string | number;
}
interface AuthState {

View File

@@ -1,4 +1,5 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface FeedTab {
date: string;
@@ -21,11 +22,16 @@ interface DashboardCache {
assetIds: string;
minChainage: string;
maxChainage: string;
searchAnomalyId: string;
filterPlaza: string;
isRectificationEnabled?: boolean;
date?: string;
stats: {
total: number;
audited: number;
pending: number;
anomaliesFound: number;
globalTotal: number;
batchTotal: number;
batchAudited: number;
batchPending: number;
batchAnomaliesFound: number;
} | null;
}
@@ -38,6 +44,7 @@ interface FeedState {
filterTillDate: string;
filterMinChainage: string;
filterMaxChainage: string;
filterPlaza: string;
selectedAssetIds: number[];
sites: any[];
auditStatus: string;
@@ -48,6 +55,8 @@ interface FeedState {
lastFetchedFilterKey: string;
// Dashboard stats cache
dashboardCache: DashboardCache;
searchAnomalyId: string;
isRectificationEnabled: boolean;
// Actions
setTabs: (tabs: FeedTab[]) => void;
@@ -69,6 +78,9 @@ interface FeedState {
setFilterTillDate: (date: string) => void;
setFilterMinChainage: (val: string) => void;
setFilterMaxChainage: (val: string) => void;
setFilterPlaza: (val: string) => void;
setSearchAnomalyId: (id: string) => void;
setIsRectificationEnabled: (val: boolean) => void;
resetStore: () => void;
}
@@ -80,6 +92,10 @@ const EMPTY_DASHBOARD_CACHE: DashboardCache = {
assetIds: '__unset__',
minChainage: '__unset__',
maxChainage: '__unset__',
searchAnomalyId: '__unset__',
filterPlaza: '__unset__',
isRectificationEnabled: false,
date: '',
stats: null,
};
@@ -92,17 +108,22 @@ const initialFeedState = {
filterTillDate: '',
filterMinChainage: '',
filterMaxChainage: '',
filterPlaza: '',
selectedAssetIds: [],
selectedSite: null,
sites: [],
auditStatus: 'Auto Audits',
auditStatus: 'Manual',
isFiltersModalOpen: false,
isInitialized: false,
lastFetchedFilterKey: '',
dashboardCache: EMPTY_DASHBOARD_CACHE,
searchAnomalyId: '',
isRectificationEnabled: false,
};
export const useFeedStore = create<FeedState>((set) => ({
export const useFeedStore = create<FeedState>()(
persist(
(set) => ({
...initialFeedState,
resetStore: () => set(initialFeedState),
@@ -123,6 +144,9 @@ export const useFeedStore = create<FeedState>((set) => ({
setFilterTillDate: (filterTillDate) => set({ filterTillDate }),
setFilterMinChainage: (filterMinChainage) => set({ filterMinChainage }),
setFilterMaxChainage: (filterMaxChainage) => set({ filterMaxChainage }),
setFilterPlaza: (filterPlaza) => set({ filterPlaza }),
setSearchAnomalyId: (searchAnomalyId) => set({ searchAnomalyId }),
setIsRectificationEnabled: (isRectificationEnabled) => set({ isRectificationEnabled }),
addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })),
closeTab: (index) => set((state) => {
const newTabs = [...state.tabs];
@@ -135,4 +159,23 @@ export const useFeedStore = create<FeedState>((set) => ({
newTabs[index] = { ...newTabs[index], ...partialTab };
return { tabs: newTabs };
})
}));
}),
{
name: 'feed-store-storage',
partialize: (state) => ({
tabs: state.tabs,
selectedTabIndex: state.selectedTabIndex,
filterFromDate: state.filterFromDate,
filterTillDate: state.filterTillDate,
filterMinChainage: state.filterMinChainage,
filterMaxChainage: state.filterMaxChainage,
filterPlaza: state.filterPlaza,
selectedAssetIds: state.selectedAssetIds,
selectedSite: state.selectedSite,
auditStatus: state.auditStatus,
searchAnomalyId: state.searchAnomalyId,
isRectificationEnabled: state.isRectificationEnabled,
}),
}
)
);

View File

@@ -10,11 +10,19 @@ export default defineConfig({
target: 'https://sr-backend-api.takeleap.in',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/dashboard/, ''),
headers: {
Origin: 'https://auditor-qa.seekright.com',
Referer: 'https://auditor-qa.seekright.com/'
}
},
'/api/audit': {
target: 'https://sr-audit.takeleap.in',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/audit/, ''),
headers: {
Origin: 'https://auditor-qa.seekright.com',
Referer: 'https://auditor-qa.seekright.com/'
}
},
},
},