feat: initialize dashboard structure with auth, feed management, and activity feed filtering components
This commit is contained in:
BIN
public/assets/images/anomaly-placeholder.png
Normal file
BIN
public/assets/images/anomaly-placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/assets/images/arrow-up.png
Normal file
BIN
public/assets/images/arrow-up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 362 B |
@@ -1,5 +1,6 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
import { MainLayout } from './components/layout/MainLayout';
|
import { MainLayout } from './components/layout/MainLayout';
|
||||||
import { ProtectedRoute } from './components/ProtectedRoute';
|
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ import { AuditSession } from './pages/audit-session/AuditSession';
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Public Routes */}
|
{/* Public Routes */}
|
||||||
|
|||||||
@@ -7,17 +7,45 @@ export const activityFeedsService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getHistoryDates: async (isRectificationEnabled: boolean) => {
|
getHistoryDates: async (isRectificationEnabled: boolean) => {
|
||||||
// Requires dbName and siteIds, which should be handled by axios interceptors or passed in
|
const response = await axiosClient.get('/api/audit/anomaly/get_history_dates', {
|
||||||
const params = new URLSearchParams({
|
params: { isRectificationEnabled }
|
||||||
isRectificationEnabled: String(isRectificationEnabled)
|
|
||||||
});
|
});
|
||||||
const response = await axiosClient.get(`/api/audit/anomaly/get_history_dates?${params.toString()}`);
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
getHistory: async (params: any) => {
|
getHistory: async (params: any) => {
|
||||||
const queryParams = new URLSearchParams(params);
|
if (params.isRectificationEnabled) {
|
||||||
const response = await axiosClient.get(`/api/audit/anomaly/history?${queryParams.toString()}`);
|
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;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -27,9 +55,27 @@ export const activityFeedsService = {
|
|||||||
return response;
|
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) => {
|
getOrganizationSites: async (org_id: string) => {
|
||||||
const params = new URLSearchParams({ org_id });
|
const response = await axiosClient.get('/api/dashboard/Master/site', {
|
||||||
const response = await axiosClient.get(`/api/dashboard/Master/site?${params.toString()}`);
|
params: { org_id }
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
getFalseAnomaly: async (params: any) => {
|
||||||
|
const response = await axiosClient.get('/api/audit/asset/get-false-Anomaly', { params });
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
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 FilterListIcon from '@mui/icons-material/FilterList';
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
import { useFeedStore } from '../../store/feedStore';
|
import { useFeedStore } from '../../store/feedStore';
|
||||||
import { FeedFilters } from '../../pages/activity-feeds/FeedFilters';
|
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
|
const DEBOUNCE_MS = 600; // wait 600ms after user stops changing before committing to store
|
||||||
|
|
||||||
@@ -12,22 +13,42 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
sites,
|
sites,
|
||||||
selectedSite, setSelectedSite,
|
selectedSite, setSelectedSite,
|
||||||
auditStatus, setAuditStatus,
|
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();
|
} = useFeedStore();
|
||||||
|
|
||||||
// Local draft values — these do NOT trigger API calls
|
// 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 [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)
|
// Sync local state when store changes externally (e.g., on initial load)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalAuditStatus(auditStatus || 'Auto Audits');
|
setLocalAuditStatus(auditStatus || 'Manual');
|
||||||
}, [auditStatus]);
|
}, [auditStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalSiteId(selectedSite?.site_id || 'All Sites');
|
setLocalSiteId(selectedSite?.site_id || 'All Sites');
|
||||||
}, [selectedSite?.site_id]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [selectedSite?.site_id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalSearchId(searchAnomalyId || '');
|
||||||
|
}, [searchAnomalyId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalPlaza(filterPlaza || '');
|
||||||
|
}, [filterPlaza]);
|
||||||
|
|
||||||
// Debounced commit for auditStatus
|
// Debounced commit for auditStatus
|
||||||
const auditDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const auditDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const handleAuditChange = (value: string) => {
|
const handleAuditChange = (value: string) => {
|
||||||
@@ -51,14 +72,114 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
}, DEBOUNCE_MS);
|
}, 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
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (auditDebounce.current) clearTimeout(auditDebounce.current);
|
if (auditDebounce.current) clearTimeout(auditDebounce.current);
|
||||||
if (siteDebounce.current) clearTimeout(siteDebounce.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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 1.5, px: 2, py: 1.5, bgcolor: '#0b1121' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 1.5, px: 2, py: 1.5, bgcolor: '#0b1121' }}>
|
||||||
@@ -73,24 +194,125 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<HistoryIcon />}
|
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' }}
|
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
|
RECTIFICATION HISTORY
|
||||||
</Button>
|
</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 */}
|
{/* Audit Status — local state, debounced commit */}
|
||||||
<FormControl size="small" sx={{ bgcolor: 'rgba(255,255,255,0.1)', borderRadius: 2, minWidth: 140 }}>
|
{!isRectActive && (
|
||||||
<Select
|
<FormControl size="small" sx={{ bgcolor: 'rgba(255,255,255,0.1)', borderRadius: 2, minWidth: 140 }}>
|
||||||
value={localAuditStatus}
|
<Select
|
||||||
onChange={(e) => handleAuditChange(e.target.value as string)}
|
value={localAuditStatus}
|
||||||
sx={{ '& .MuiSelect-select': { py: 1, px: 2 }, border: 'none', '& fieldset': { border: 'none' }, fontWeight: 'bold', color: '#fff' }}
|
onChange={(e) => handleAuditChange(e.target.value as string)}
|
||||||
>
|
sx={{ '& .MuiSelect-select': { py: 1, px: 2 }, border: 'none', '& fieldset': { border: 'none' }, fontWeight: 'bold', color: '#fff' }}
|
||||||
<MenuItem value="Manual">Manual</MenuItem>
|
>
|
||||||
<MenuItem value="Auto Audits">Auto Audits</MenuItem>
|
<MenuItem value="Manual">Manual</MenuItem>
|
||||||
<MenuItem value="False Audits">False Audits</MenuItem>
|
<MenuItem value="Auto Audits">Auto Audits</MenuItem>
|
||||||
<MenuItem value="All">All</MenuItem>
|
<MenuItem value="False Audits">False Audits</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Site — local state, debounced commit */}
|
{/* Site — local state, debounced commit */}
|
||||||
<FormControl size="small" sx={{ bgcolor: 'rgba(255,255,255,0.1)', borderRadius: 2, minWidth: 140 }}>
|
<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>
|
</Select>
|
||||||
</FormControl>
|
</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
|
<Button
|
||||||
variant="contained"
|
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>
|
</Button>
|
||||||
</Box>
|
</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 />
|
<FeedFilters />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
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 MenuIcon from '@mui/icons-material/Menu';
|
||||||
import AccountCircle from '@mui/icons-material/AccountCircle';
|
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 { useAuthStore } from '../../store/authStore';
|
||||||
import { useFeedStore } from '../../store/feedStore';
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
@@ -14,14 +11,11 @@ interface HeaderProps {
|
|||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({ toggleSidebar }) => {
|
export const Header: React.FC<HeaderProps> = ({ toggleSidebar }) => {
|
||||||
const { user, logout } = useAuthStore();
|
const { user, logout } = useAuthStore();
|
||||||
const { sites, selectedSite, setSelectedSite, auditStatus, setAuditStatus, setIsFiltersModalOpen } = useFeedStore();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
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 showNav = !location.pathname.includes('dashboard');
|
||||||
const isActivityFeeds = location.pathname.includes('activity-feeds');
|
|
||||||
|
|
||||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
|
|||||||
@@ -1,18 +1,103 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Box, Toolbar, CssBaseline } from '@mui/material';
|
import { Box, Toolbar } from '@mui/material';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
|
import { useFeedStore } from '../../store/feedStore';
|
||||||
|
import { useAuthStore } from '../../store/authStore';
|
||||||
|
import { activityFeedsService } from '../../api/activityFeedsService';
|
||||||
|
|
||||||
export const MainLayout: React.FC = () => {
|
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 (
|
return (
|
||||||
<Box sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'background.default' }}>
|
<Box sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'background.default' }}>
|
||||||
<CssBaseline />
|
|
||||||
|
|
||||||
<Header />
|
<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 */}
|
<Toolbar /> {/* Spacer for Header */}
|
||||||
|
|
||||||
{/* Child routes get injected here */}
|
{/* Child routes get injected here */}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
import { accountService } from '../../api/accountService';
|
import { accountService } from '../../api/accountService';
|
||||||
@@ -76,88 +76,138 @@ export const Login: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container component="main" maxWidth="xs">
|
<Box sx={{
|
||||||
<Box sx={{ marginTop: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
{/* Placeholder for Logo */}
|
display: 'flex',
|
||||||
<Box sx={{ mb: 6, display: 'flex', justifyContent: 'center' }}>
|
justifyContent: 'center',
|
||||||
<img src="/images/SeekRightLogo.png" alt="Logo" width="250" onError={(e) => { e.currentTarget.style.display = 'none'; }} />
|
alignItems: 'center',
|
||||||
</Box>
|
background: 'radial-gradient(circle at center, #0f172a 0%, #0b1121 100%)',
|
||||||
|
overflow: 'hidden'
|
||||||
<Box component="form" sx={{ mt: 1, width: '100%' }}>
|
}}>
|
||||||
<TextField
|
<Container component="main" maxWidth="xs">
|
||||||
margin="normal"
|
<Paper sx={{
|
||||||
required
|
p: 4,
|
||||||
fullWidth
|
display: 'flex',
|
||||||
id="username"
|
flexDirection: 'column',
|
||||||
placeholder="Admin"
|
alignItems: 'center',
|
||||||
name="username"
|
bgcolor: 'rgba(15, 23, 42, 0.65)',
|
||||||
autoFocus
|
backdropFilter: 'blur(12px)',
|
||||||
value={username}
|
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
borderRadius: 4,
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.5)'
|
||||||
InputProps={{
|
}}>
|
||||||
sx: { borderRadius: '50px', '& fieldset': { borderColor: '#738ab8' } }
|
{/* Logo */}
|
||||||
}}
|
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'center' }}>
|
||||||
sx={{
|
<img
|
||||||
mb: 1,
|
src="/assets/images/logo/side-bar-logo.png"
|
||||||
'& .MuiInputBase-input': { padding: '10px 14px', fontSize: '0.9rem', color: '#333' }
|
alt="Logo"
|
||||||
}}
|
width="220"
|
||||||
/>
|
onError={(e) => {
|
||||||
<TextField
|
e.currentTarget.src = "/images/SeekRightLogo.png";
|
||||||
margin="normal"
|
}}
|
||||||
required
|
/>
|
||||||
fullWidth
|
|
||||||
name="password"
|
|
||||||
placeholder="Password"
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
|
||||||
InputProps={{
|
|
||||||
sx: { borderRadius: '50px', '& fieldset': { borderColor: '#738ab8' } }
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
mt: 1,
|
|
||||||
mb: 3,
|
|
||||||
'& .MuiInputBase-input': { padding: '10px 14px', fontSize: '0.9rem', color: '#333' }
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3, mb: 4 }}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
disabled={!username || !password || loading}
|
|
||||||
onClick={handleLogin}
|
|
||||||
sx={{
|
|
||||||
borderRadius: '50px',
|
|
||||||
backgroundColor: '#738ab8',
|
|
||||||
width: '140px',
|
|
||||||
pt: 1, pb: 1,
|
|
||||||
boxShadow: 'none',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: '#5c729c',
|
|
||||||
boxShadow: 'none',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? '...' : 'LOGIN'}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box component="form" sx={{ width: '100%' }}>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="username"
|
||||||
|
placeholder="Username"
|
||||||
|
name="username"
|
||||||
|
autoFocus
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
sx: {
|
||||||
|
borderRadius: '50px',
|
||||||
|
color: '#f8fafc',
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.02)',
|
||||||
|
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.12)' },
|
||||||
|
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.25)' },
|
||||||
|
'&.Mui-focused fieldset': { borderColor: '#3b82f6' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mb: 1.5,
|
||||||
|
'& .MuiInputBase-input': { padding: '12px 18px', fontSize: '0.9rem' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="password"
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
sx: {
|
||||||
|
borderRadius: '50px',
|
||||||
|
color: '#f8fafc',
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.02)',
|
||||||
|
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.12)' },
|
||||||
|
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.25)' },
|
||||||
|
'&.Mui-focused fieldset': { borderColor: '#3b82f6' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mt: 1.5,
|
||||||
|
mb: 3,
|
||||||
|
'& .MuiInputBase-input': { padding: '12px 18px', fontSize: '0.9rem' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1, mb: 1 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={!username || !password || loading}
|
||||||
|
onClick={handleLogin}
|
||||||
|
sx={{
|
||||||
|
borderRadius: '50px',
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
width: '100%',
|
||||||
|
py: 1.5,
|
||||||
|
boxShadow: 'none',
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '1rem',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.3)',
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Logging in...' : 'LOGIN'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{isInvalid && (
|
||||||
{isInvalid && (
|
<Alert severity="error" sx={{ mt: 3, borderRadius: 2 }}>
|
||||||
<Alert severity="error" sx={{ mt: 2 }}>
|
Invalid Credentials
|
||||||
Invalid Credentials
|
</Alert>
|
||||||
</Alert>
|
)}
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
</Paper>
|
||||||
</Box>
|
</Container>
|
||||||
|
|
||||||
{/* Database Selection Dialog for SR_AUDITOR */}
|
{/* Database Selection Dialog for SR_AUDITOR */}
|
||||||
<DbSelectionDialog open={dialogOpen} onClose={handleDbSelection} />
|
<DbSelectionDialog open={dialogOpen} onClose={handleDbSelection} />
|
||||||
</Container>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,14 +3,20 @@ import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead
|
|||||||
import { useFeedStore } from '../../store/feedStore';
|
import { useFeedStore } from '../../store/feedStore';
|
||||||
|
|
||||||
export const AnomalyTable: React.FC = () => {
|
export const AnomalyTable: React.FC = () => {
|
||||||
const { tabs, selectedTabIndex, updateTab } = useFeedStore();
|
const { tabs, selectedTabIndex, updateTab, filterPlaza, isRectificationEnabled } = useFeedStore();
|
||||||
|
|
||||||
if (tabs.length === 0 || selectedTabIndex < 0) {
|
if (tabs.length === 0 || selectedTabIndex < 0) {
|
||||||
return <Typography sx={{ p: 2 }}>Loading feeds...</Typography>;
|
return <Typography sx={{ p: 2 }}>Loading feeds...</Typography>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTab = tabs[selectedTabIndex];
|
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) {
|
if (anomalies.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -33,30 +39,36 @@ export const AnomalyTable: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 }}>
|
<Table stickyHeader size="small" sx={{ minWidth: 1200 }}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>#ID</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={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>PLAZA</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={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>ROAD</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={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>DATE</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' }}>
|
||||||
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>CHAINAGE</TableCell>
|
<Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||||
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>LAT/LONG</TableCell>
|
DATE
|
||||||
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>ANOMALY</TableCell>
|
<Box component="img" src="/assets/images/arrow-up.png" sx={{ width: 20, height: 20, ml: 0.5 }} />
|
||||||
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>SIDE</TableCell>
|
</Box>
|
||||||
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>ANOMALY True/False</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>FEATURES</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={{ fontWeight: 'bold', bgcolor: 'background.paper', color: 'text.primary', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>COMMENTS</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)' }}>CHECKED</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={{ 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' }}>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>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{anomalies.map((anomaly: any, index: number) => {
|
{anomalies.map((anomaly: any, _filteredIndex: number) => {
|
||||||
const isSelected = activeTab.selectedAnomalyIndex === index;
|
const realIndex = rawAnomalies.indexOf(anomaly);
|
||||||
|
const isSelected = activeTab.selectedAnomalyIndex === realIndex;
|
||||||
const assetName = anomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset';
|
const assetName = anomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset';
|
||||||
const rowColor = getRowColor(index, assetName);
|
const rowColor = getRowColor(realIndex, assetName);
|
||||||
|
|
||||||
// Parse Position
|
// Parse Position
|
||||||
let latStr = 'N/A';
|
let latStr = 'N/A';
|
||||||
@@ -74,8 +86,8 @@ export const AnomalyTable: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={anomaly._id || index}
|
key={anomaly._id || anomaly.id || _filteredIndex}
|
||||||
onClick={() => handleRowClick(index)}
|
onClick={() => handleRowClick(realIndex)}
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.2)' : rowColor,
|
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.2)' : rowColor,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -84,35 +96,47 @@ export const AnomalyTable: React.FC = () => {
|
|||||||
mb: 1,
|
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={{ 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={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Plaza || '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={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.road || anomaly.Road || '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={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Created_on ? new Date(anomaly.Created_on).toLocaleDateString() : '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={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Chainage || '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={{ borderBottom: '1px solid rgba(255,255,255,0.05)', fontSize: '0.75rem' }}>
|
<TableCell sx={{ width: '8%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
|
||||||
{latStr}<br/>
|
{latStr}<br/>
|
||||||
{longStr}
|
{longStr}
|
||||||
</TableCell>
|
</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)', verticalAlign: 'middle' }}>{assetName}</TableCell>
|
||||||
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Side || 'N/A'}</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={{ borderBottom: '1px solid rgba(255,255,255,0.05)', width: 120 }}>
|
<TableCell sx={{ width: '10%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle' }}>
|
||||||
<Slider
|
<Box sx={{ width: '100%', minWidth: 60 }}>
|
||||||
size="small"
|
<Slider
|
||||||
value={anomaly.Audit_status || anomaly.anomaly_status_slider || 50}
|
size="small"
|
||||||
step={100}
|
value={anomaly.Audit_status || anomaly.anomaly_status_slider || 50}
|
||||||
marks
|
step={100}
|
||||||
min={0}
|
marks
|
||||||
max={100}
|
min={0}
|
||||||
sx={{ color: '#ff4d4f' }}
|
max={100}
|
||||||
/>
|
sx={{ color: '#ff4d4f' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Features || 'N/A'}</TableCell>
|
<TableCell sx={{ width: '6%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.Features || 'N/A'}</TableCell>
|
||||||
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{anomaly.Comments || '-'}</TableCell>
|
<TableCell sx={{ width: '6%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{anomaly.Comments || '-'}</TableCell>
|
||||||
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
<TableCell sx={{ width: '5%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', textAlign: 'center' }}>
|
||||||
<Checkbox size="small" checked={!!anomaly.IsAudited} />
|
<Checkbox size="small" checked={!!anomaly.IsAudited} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
<TableCell sx={{ width: '8%', borderBottom: '1px solid rgba(255,255,255,0.05)', verticalAlign: 'middle', textAlign: 'center' }}>
|
||||||
<Checkbox size="small" checked={!!anomaly.auto_audit} />
|
<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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useMemo, useEffect, useCallback, memo } from 'react';
|
import React, { useState, useMemo, useCallback, memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, TextField, Checkbox, FormControlLabel, Button, Typography, Dialog,
|
Box, TextField, Checkbox, FormControlLabel, Button, Typography, Dialog,
|
||||||
DialogContent, DialogActions, CircularProgress, Divider, InputAdornment, Chip
|
DialogContent, DialogActions, CircularProgress, Divider, InputAdornment, Chip
|
||||||
@@ -153,7 +153,7 @@ export const FeedFilters: React.FC = () => {
|
|||||||
const handleApplyFilter = async () => {
|
const handleApplyFilter = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const { selectedSite, auditStatus, setFilterFromDate, setFilterTillDate } = useFeedStore.getState();
|
const { selectedSite, auditStatus, setFilterFromDate, setFilterTillDate, filterPlaza, isRectificationEnabled, searchAnomalyId } = useFeedStore.getState();
|
||||||
const draftAssetArr = Array.from(draftSet);
|
const draftAssetArr = Array.from(draftSet);
|
||||||
|
|
||||||
// Commit ALL filter state to the global store
|
// 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')
|
const siteIdParam = (!selectedSite || selectedSite?.site_id === 'All Sites')
|
||||||
? '' : String(selectedSite.site_id || '');
|
? '' : 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({
|
const historyRes: any = await activityFeedsService.getHistory({
|
||||||
date: activeTab.date,
|
date: activeTab.date,
|
||||||
fromDate: draftFromDate,
|
fromDate: draftFromDate,
|
||||||
@@ -175,19 +181,27 @@ export const FeedFilters: React.FC = () => {
|
|||||||
pageNo: 0,
|
pageNo: 0,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
siteIds: siteIdParam,
|
siteIds: siteIdParam,
|
||||||
assetsIds: draftAssetArr.join(','),
|
assetsIds: assetIdsKey,
|
||||||
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
|
auditStatus: auditStatus,
|
||||||
isRectificationEnabled: false,
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
auto_audit: false,
|
isRectificationEnabled: isRectActive,
|
||||||
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
fromChainage: draftMinChainage,
|
fromChainage: draftMinChainage,
|
||||||
toChainage: draftMaxChainage,
|
toChainage: draftMaxChainage,
|
||||||
|
plaza: filterPlaza,
|
||||||
});
|
});
|
||||||
if (historyRes?.anomalies) {
|
if (historyRes?.anomalies) {
|
||||||
updateTab(selectedTabIndex, {
|
updateTab(selectedTabIndex, {
|
||||||
anomalies: historyRes.anomalies,
|
anomalies: historyRes.anomalies,
|
||||||
anomaliesCopy: historyRes.anomalies,
|
anomaliesCopy: historyRes.anomalies,
|
||||||
totalAnomalyCount: historyRes.count || historyRes.anomalies.length,
|
totalAnomalyCount: historyRes.count || historyRes.anomalies.length,
|
||||||
filters: { fromDate: draftFromDate, tillDate: draftTillDate, fromChainage: draftMinChainage, toChainage: draftMaxChainage }
|
filters: {
|
||||||
|
fromDate: draftFromDate,
|
||||||
|
tillDate: draftTillDate,
|
||||||
|
fromChainage: draftMinChainage,
|
||||||
|
toChainage: draftMaxChainage,
|
||||||
|
lastFetchedFilterKey: filterKey
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,13 +221,15 @@ export const FeedFilters: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
// Zero transition — opens instantly
|
// Zero transition — opens instantly
|
||||||
transitionDuration={0}
|
transitionDuration={0}
|
||||||
PaperProps={{
|
slotProps={{
|
||||||
sx: {
|
paper: {
|
||||||
borderRadius: 3,
|
sx: {
|
||||||
bgcolor: '#0f172a',
|
borderRadius: 3,
|
||||||
color: '#f8fafc',
|
bgcolor: '#0f172a',
|
||||||
border: '1px solid #1e293b',
|
color: '#f8fafc',
|
||||||
maxHeight: '90vh'
|
border: '1px solid #1e293b',
|
||||||
|
maxHeight: '90vh'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,198 +1,158 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Box, Typography, TablePagination, Button, FormControl, Select, MenuItem } from '@mui/material';
|
import { Box, TablePagination } from '@mui/material';
|
||||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
|
||||||
import { TopFilterBar } from '../../components/common/TopFilterBar';
|
import { TopFilterBar } from '../../components/common/TopFilterBar';
|
||||||
import { FeedTabs } from './FeedTabs';
|
import { FeedTabs } from './FeedTabs';
|
||||||
import { AnomalyTable } from './AnomalyTable';
|
import { AnomalyTable } from './AnomalyTable';
|
||||||
import { PreviewPanel } from './PreviewPanel';
|
import { PreviewPanel } from './PreviewPanel';
|
||||||
import { useFeedStore } from '../../store/feedStore';
|
import { useFeedStore } from '../../store/feedStore';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
|
||||||
import { activityFeedsService } from '../../api/activityFeedsService';
|
import { activityFeedsService } from '../../api/activityFeedsService';
|
||||||
export const GlobalFeed: React.FC = () => {
|
export const GlobalFeed: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
setAssets, setAssetTypes, addTab, tabs, selectedTabIndex, updateTab,
|
addTab, tabs, selectedTabIndex, updateTab,
|
||||||
sites, setSites, selectedSite, setSelectedSite, auditStatus,
|
selectedSite, auditStatus,
|
||||||
isInitialized, setIsInitialized,
|
isInitialized,
|
||||||
lastFetchedFilterKey, setLastFetchedFilterKey
|
searchAnomalyId, filterPlaza,
|
||||||
|
isRectificationEnabled,
|
||||||
|
filterFromDate, filterTillDate,
|
||||||
|
filterMinChainage, filterMaxChainage,
|
||||||
|
selectedAssetIds
|
||||||
} = useFeedStore();
|
} = useFeedStore();
|
||||||
const { user } = useAuthStore();
|
|
||||||
// Ref to skip the first run of the site/auditStatus effect
|
|
||||||
const isFirstRender = React.useRef(true);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initial Load
|
// GUARD: Wait until global metadata is initialized in MainLayout
|
||||||
const loadInitialData = async () => {
|
if (!isInitialized) {
|
||||||
try {
|
return;
|
||||||
const assetRes: any = await activityFeedsService.getAssets();
|
}
|
||||||
if (assetRes && assetRes.asset_types) {
|
|
||||||
// Map assets mimicking the Angular logic
|
const initTabs = async () => {
|
||||||
let flattenedAssets: any[] = [];
|
// Add default Unaudited tab if empty and fetch data
|
||||||
assetRes.asset_types.forEach((type: any) => {
|
if (useFeedStore.getState().tabs.length === 0) {
|
||||||
const mapped = type.assets.map((a: any) => ({
|
addTab({
|
||||||
id: a.asset_id,
|
date: '', // Unaudited
|
||||||
name: a.asset_name,
|
isRectificationTab: false,
|
||||||
asset_type_id: type.type_id
|
totalAnomalyCount: 0,
|
||||||
}));
|
anomalies: [],
|
||||||
flattenedAssets = [...flattenedAssets, ...mapped];
|
anomaliesCopy: [],
|
||||||
|
selectedAnomalyIndex: 0,
|
||||||
|
filters: {},
|
||||||
|
page: 0,
|
||||||
|
rowsPerPage: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentSite = useFeedStore.getState().selectedSite;
|
||||||
|
const siteIdParam = (!currentSite || currentSite?.site_id === 'All Sites') ? '' : String(currentSite.site_id || '');
|
||||||
|
const assetIdsParam = useFeedStore.getState().selectedAssetIds.join(',');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const historyRes: any = await activityFeedsService.getHistory({
|
||||||
|
date: '',
|
||||||
|
pageNo: 0,
|
||||||
|
pageSize: 20,
|
||||||
|
siteIds: siteIdParam,
|
||||||
|
auditStatus: auditStatus,
|
||||||
|
isTrueFalse: isRectificationEnabled ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
|
auto_audit: isRectificationEnabled ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
|
isRectificationEnabled: isRectificationEnabled,
|
||||||
|
assetsIds: assetIdsParam,
|
||||||
|
fromDate: useFeedStore.getState().filterFromDate,
|
||||||
|
tillDate: useFeedStore.getState().filterTillDate,
|
||||||
|
fromChainage: useFeedStore.getState().filterMinChainage,
|
||||||
|
toChainage: useFeedStore.getState().filterMaxChainage,
|
||||||
|
anomalyIds: searchAnomalyId,
|
||||||
|
plaza: filterPlaza
|
||||||
});
|
});
|
||||||
setAssets(flattenedAssets);
|
if (historyRes && historyRes.anomalies) {
|
||||||
setAssetTypes(assetRes.asset_types); // store raw grouped data for filters dialog
|
updateTab(0, {
|
||||||
}
|
anomalies: historyRes.anomalies,
|
||||||
|
anomaliesCopy: historyRes.anomalies,
|
||||||
// Fetch Sites
|
totalAnomalyCount: historyRes.count || historyRes.anomalies.length
|
||||||
let defaultSite = null;
|
|
||||||
if (user?.org_id) {
|
|
||||||
try {
|
|
||||||
const sitesRes: any = await activityFeedsService.getOrganizationSites(user.org_id);
|
|
||||||
let parsedSites: any[] = [];
|
|
||||||
if (sitesRes?.responseData?.[0]?.records) {
|
|
||||||
parsedSites = sitesRes.responseData[0].records;
|
|
||||||
// Handle nested array response [ [ {site1}, {site2} ] ]
|
|
||||||
if (parsedSites.length === 1 && Array.isArray(parsedSites[0])) {
|
|
||||||
parsedSites = parsedSites[0];
|
|
||||||
} else if (parsedSites.length > 0 && Array.isArray(parsedSites[0])) {
|
|
||||||
parsedSites = parsedSites.flat();
|
|
||||||
}
|
|
||||||
} else if (Array.isArray(sitesRes)) {
|
|
||||||
parsedSites = sitesRes;
|
|
||||||
} else if (sitesRes?.data && Array.isArray(sitesRes.data)) {
|
|
||||||
parsedSites = sitesRes.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedSites.length > 0) {
|
|
||||||
// Filter to ensure we only show sites for the currently selected org
|
|
||||||
parsedSites = parsedSites.filter(site => {
|
|
||||||
let siteOrgId = site.org_id;
|
|
||||||
if (siteOrgId === undefined && site.sub_sites && site.sub_sites.length > 0) {
|
|
||||||
siteOrgId = site.sub_sites[0].org_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siteOrgId !== undefined && siteOrgId !== null) {
|
|
||||||
return String(siteOrgId) === String(user.org_id);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}).map(site => {
|
|
||||||
// Normalize site_id: if missing, aggregate from sub_sites
|
|
||||||
if (!site.site_id && site.sub_sites && site.sub_sites.length > 0) {
|
|
||||||
site.site_id = site.sub_sites.map((sub: any) => sub.site_id).filter(Boolean).join(',');
|
|
||||||
}
|
|
||||||
return site;
|
|
||||||
});
|
|
||||||
|
|
||||||
setSites(parsedSites);
|
|
||||||
defaultSite = { site_id: 'All Sites', site_name: 'All Sites' };
|
|
||||||
setSelectedSite(defaultSite);
|
|
||||||
}
|
|
||||||
} catch (siteErr) {
|
|
||||||
console.error("Failed to fetch sites", siteErr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add default Unaudited tab if empty and fetch data
|
|
||||||
if (useFeedStore.getState().tabs.length === 0) {
|
|
||||||
addTab({
|
|
||||||
date: '', // Unaudited
|
|
||||||
isRectificationTab: false,
|
|
||||||
totalAnomalyCount: 0,
|
|
||||||
anomalies: [],
|
|
||||||
anomaliesCopy: [],
|
|
||||||
selectedAnomalyIndex: 0,
|
|
||||||
filters: {},
|
|
||||||
page: 0,
|
|
||||||
rowsPerPage: 20
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const historyRes: any = await activityFeedsService.getHistory({
|
|
||||||
date: '',
|
|
||||||
pageNo: 0,
|
|
||||||
pageSize: 20,
|
|
||||||
siteIds: defaultSite?.site_id === 'All Sites' ? '' : (defaultSite?.site_id || ''),
|
|
||||||
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
|
|
||||||
auto_audit: false,
|
|
||||||
isRectificationEnabled: false
|
|
||||||
});
|
});
|
||||||
if (historyRes && historyRes.anomalies) {
|
|
||||||
updateTab(0, {
|
|
||||||
anomalies: historyRes.anomalies,
|
|
||||||
anomaliesCopy: historyRes.anomalies,
|
|
||||||
totalAnomalyCount: historyRes.count || historyRes.anomalies.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (historyErr) {
|
|
||||||
console.error("[GlobalFeed] Failed to load history", historyErr);
|
|
||||||
}
|
}
|
||||||
|
} catch (historyErr) {
|
||||||
|
console.error("[GlobalFeed] Failed to load history", historyErr);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load initial feed data", error);
|
|
||||||
} finally {
|
|
||||||
setIsInitialized(true); // Mark as initialized regardless, to prevent retry loops
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// GUARD: Only run initial load once across tab switches
|
initTabs();
|
||||||
if (useFeedStore.getState().isInitialized) {
|
}, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadInitialData();
|
|
||||||
}, []); // 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(() => {
|
useEffect(() => {
|
||||||
// GUARD: Skip if tabs are not initialized (initial load hasn't finished)
|
// GUARD: Skip if tabs are not initialized (initial load hasn't finished)
|
||||||
if (useFeedStore.getState().tabs.length === 0) {
|
if (useFeedStore.getState().tabs.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a key for the current filter state
|
const currentTabIndex = selectedTabIndex > -1 ? selectedTabIndex : 0;
|
||||||
const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}`;
|
const activeTab = tabs[currentTabIndex];
|
||||||
|
if (!activeTab) return;
|
||||||
|
|
||||||
// CACHE HIT: This exact filter combo was already fetched — skip re-fetch on remount
|
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
|
||||||
if (filterKey === lastFetchedFilterKey) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFilteredHistory = async () => {
|
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')
|
const siteIdParam = (!selectedSite || selectedSite?.site_id === 'All Sites')
|
||||||
? '' : String(selectedSite.site_id || '');
|
? '' : String(selectedSite.site_id || '');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historyRes: any = await activityFeedsService.getHistory({
|
const historyRes: any = await activityFeedsService.getHistory({
|
||||||
date: activeTab?.date || '',
|
date: activeTab.date || '',
|
||||||
pageNo: 0,
|
pageNo: 0,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
siteIds: siteIdParam,
|
siteIds: siteIdParam,
|
||||||
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
|
auditStatus: auditStatus,
|
||||||
auto_audit: false,
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
isRectificationEnabled: false,
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
...(activeTab?.filters || {})
|
isRectificationEnabled: isRectActive,
|
||||||
|
anomalyIds: searchAnomalyId,
|
||||||
|
plaza: filterPlaza,
|
||||||
|
assetsIds: assetIdsKey,
|
||||||
|
fromDate: filterFromDate,
|
||||||
|
tillDate: filterTillDate,
|
||||||
|
fromChainage: filterMinChainage,
|
||||||
|
toChainage: filterMaxChainage,
|
||||||
});
|
});
|
||||||
if (historyRes && historyRes.anomalies) {
|
if (historyRes && historyRes.anomalies) {
|
||||||
useFeedStore.getState().updateTab(currentTabIndex, {
|
updateTab(currentTabIndex, {
|
||||||
anomalies: historyRes.anomalies,
|
anomalies: historyRes.anomalies,
|
||||||
anomaliesCopy: 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) {
|
} 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();
|
fetchFilteredHistory();
|
||||||
// Use primitive/stable values to avoid spurious re-runs.
|
}, [
|
||||||
// selectedSite?.site_id and auditStatus are both strings/primitives.
|
selectedSite?.site_id, auditStatus, searchAnomalyId, filterPlaza, isRectificationEnabled, selectedTabIndex, tabs.length,
|
||||||
}, [selectedSite?.site_id, auditStatus]); // eslint-disable-line react-hooks/exhaustive-deps
|
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;
|
if (selectedTabIndex < 0 || tabs.length === 0) return;
|
||||||
const activeTab = tabs[selectedTabIndex];
|
const activeTab = tabs[selectedTabIndex];
|
||||||
const allSiteIds = sites.map(s => s.site_id).filter(Boolean).join(',');
|
|
||||||
const rowsPerPage = activeTab.rowsPerPage || 20;
|
const rowsPerPage = activeTab.rowsPerPage || 20;
|
||||||
|
const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab;
|
||||||
|
const assetIdsKey = selectedAssetIds.join(',');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historyRes: any = await activityFeedsService.getHistory({
|
const historyRes: any = await activityFeedsService.getHistory({
|
||||||
@@ -200,10 +160,17 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
pageNo: newPage * rowsPerPage,
|
pageNo: newPage * rowsPerPage,
|
||||||
pageSize: rowsPerPage,
|
pageSize: rowsPerPage,
|
||||||
siteIds: selectedSite?.site_id === 'All Sites' ? '' : (selectedSite?.site_id || ''),
|
siteIds: selectedSite?.site_id === 'All Sites' ? '' : (selectedSite?.site_id || ''),
|
||||||
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
|
auditStatus: auditStatus,
|
||||||
auto_audit: false,
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
isRectificationEnabled: false,
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
...(activeTab?.filters || {})
|
isRectificationEnabled: isRectActive,
|
||||||
|
anomalyIds: searchAnomalyId,
|
||||||
|
plaza: filterPlaza,
|
||||||
|
assetsIds: assetIdsKey,
|
||||||
|
fromDate: filterFromDate,
|
||||||
|
tillDate: filterTillDate,
|
||||||
|
fromChainage: filterMinChainage,
|
||||||
|
toChainage: filterMaxChainage,
|
||||||
});
|
});
|
||||||
if (historyRes && historyRes.anomalies) {
|
if (historyRes && historyRes.anomalies) {
|
||||||
updateTab(selectedTabIndex, {
|
updateTab(selectedTabIndex, {
|
||||||
@@ -221,9 +188,10 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
const handleRowsPerPageChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleRowsPerPageChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (selectedTabIndex < 0 || tabs.length === 0) return;
|
if (selectedTabIndex < 0 || tabs.length === 0) return;
|
||||||
const activeTab = tabs[selectedTabIndex];
|
const activeTab = tabs[selectedTabIndex];
|
||||||
const allSiteIds = sites.map(s => s.site_id).filter(Boolean).join(',');
|
|
||||||
const newRowsPerPage = parseInt(event.target.value, 10);
|
const newRowsPerPage = parseInt(event.target.value, 10);
|
||||||
const newPage = 0;
|
const newPage = 0;
|
||||||
|
const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab;
|
||||||
|
const assetIdsKey = selectedAssetIds.join(',');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historyRes: any = await activityFeedsService.getHistory({
|
const historyRes: any = await activityFeedsService.getHistory({
|
||||||
@@ -231,10 +199,17 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
pageNo: newPage * newRowsPerPage,
|
pageNo: newPage * newRowsPerPage,
|
||||||
pageSize: newRowsPerPage,
|
pageSize: newRowsPerPage,
|
||||||
siteIds: selectedSite?.site_id === 'All Sites' ? '' : (selectedSite?.site_id || ''),
|
siteIds: selectedSite?.site_id === 'All Sites' ? '' : (selectedSite?.site_id || ''),
|
||||||
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
|
auditStatus: auditStatus,
|
||||||
auto_audit: false,
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
isRectificationEnabled: false,
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
...(activeTab?.filters || {})
|
isRectificationEnabled: isRectActive,
|
||||||
|
anomalyIds: searchAnomalyId,
|
||||||
|
plaza: filterPlaza,
|
||||||
|
assetsIds: assetIdsKey,
|
||||||
|
fromDate: filterFromDate,
|
||||||
|
tillDate: filterTillDate,
|
||||||
|
fromChainage: filterMinChainage,
|
||||||
|
toChainage: filterMaxChainage,
|
||||||
});
|
});
|
||||||
if (historyRes && historyRes.anomalies) {
|
if (historyRes && historyRes.anomalies) {
|
||||||
updateTab(selectedTabIndex, {
|
updateTab(selectedTabIndex, {
|
||||||
@@ -258,7 +233,7 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
|
|
||||||
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden', bgcolor: 'background.default' }}>
|
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden', bgcolor: 'background.default' }}>
|
||||||
{/* Left side Table */}
|
{/* 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 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', py: 0 }}>
|
||||||
<TablePagination
|
<TablePagination
|
||||||
component="div"
|
component="div"
|
||||||
@@ -270,7 +245,7 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
rowsPerPageOptions={[10, 20, 50, 100]}
|
rowsPerPageOptions={[10, 20, 50, 100]}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
<Box sx={{ flex: 1, overflow: 'hidden' }}>
|
||||||
<AnomalyTable />
|
<AnomalyTable />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useFeedStore } from '../../store/feedStore';
|
import { useFeedStore } from '../../store/feedStore';
|
||||||
|
|
||||||
export const PreviewPanel: React.FC = () => {
|
export const PreviewPanel: React.FC = () => {
|
||||||
const { tabs, selectedTabIndex } = useFeedStore();
|
const { tabs, selectedTabIndex, isRectificationEnabled } = useFeedStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (tabs.length === 0 || selectedTabIndex < 0) return null;
|
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/";
|
const IMAGES_TEST_AUDIT_PREFIX = "https://dashboard-images.seekright.com/SeekRight/";
|
||||||
|
|
||||||
let masterImageSrc = '';
|
let masterImageSrc = '';
|
||||||
@@ -41,13 +44,17 @@ export const PreviewPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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' }}>
|
<Box sx={{ mb: 2, border: '1px solid rgba(255,255,255,0.1)', borderRadius: 2, overflow: 'hidden' }}>
|
||||||
<img
|
<img
|
||||||
src={masterImageSrc}
|
src={masterImageSrc}
|
||||||
alt="Master"
|
alt="Master"
|
||||||
style={{ width: '100%', height: 'auto', display: 'block' }}
|
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 }}>
|
<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>
|
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>MASTER IMAGE</Typography>
|
||||||
@@ -59,6 +66,10 @@ export const PreviewPanel: React.FC = () => {
|
|||||||
src={anomalyImageSrc}
|
src={anomalyImageSrc}
|
||||||
alt="Anomaly"
|
alt="Anomaly"
|
||||||
style={{ width: '100%', height: 'auto', display: 'block' }}
|
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 }}>
|
<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>
|
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>ANOMALY IMAGE</Typography>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||||
import { activityFeedsService } from '../../api/activityFeedsService';
|
import { activityFeedsService } from '../../api/activityFeedsService';
|
||||||
import { TopFilterBar } from '../../components/common/TopFilterBar';
|
import { TopFilterBar } from '../../components/common/TopFilterBar';
|
||||||
@@ -10,7 +10,9 @@ export const Dashboard: React.FC = () => {
|
|||||||
const {
|
const {
|
||||||
selectedSite, auditStatus, filterFromDate, filterTillDate,
|
selectedSite, auditStatus, filterFromDate, filterTillDate,
|
||||||
filterMinChainage, filterMaxChainage,
|
filterMinChainage, filterMaxChainage,
|
||||||
selectedAssetIds, dashboardCache, setDashboardCache
|
selectedAssetIds, dashboardCache, setDashboardCache,
|
||||||
|
searchAnomalyId, filterPlaza, isRectificationEnabled,
|
||||||
|
tabs, selectedTabIndex
|
||||||
} = useFeedStore();
|
} = useFeedStore();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -18,19 +20,29 @@ export const Dashboard: React.FC = () => {
|
|||||||
const selectedSiteId = selectedSite?.site_id ?? '';
|
const selectedSiteId = selectedSite?.site_id ?? '';
|
||||||
const assetIdsKey = selectedAssetIds.join(',');
|
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
|
// 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)
|
// Initialise stats from cache immediately (no flicker on tab switch)
|
||||||
const [stats, setStats] = useState(() =>
|
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
|
// Only show loading spinner if there's no cached result for these filters
|
||||||
const [loading, setLoading] = useState(dashboardCache.stats === null);
|
const [loading, setLoading] = useState(dashboardCache.stats === null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// CACHE HIT: filters unchanged since last fetch — skip API call entirely
|
// 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);
|
setStats(dashboardCache.stats);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -40,28 +52,32 @@ export const Dashboard: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res: any = await activityFeedsService.getHistory({
|
const res: any = await activityFeedsService.getHistory({
|
||||||
date: '',
|
date: activeDate,
|
||||||
pageNo: 0,
|
pageNo: 0,
|
||||||
pageSize: 200,
|
pageSize: 200,
|
||||||
sortByDateAsc: false,
|
sortByDateAsc: false,
|
||||||
isRectificationEnabled: false,
|
isRectificationEnabled: isRectActive,
|
||||||
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
|
auditStatus: auditStatus,
|
||||||
auto_audit: false,
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
siteIds: (!selectedSiteId || selectedSiteId === 'All Sites') ? '' : selectedSiteId,
|
siteIds: (!selectedSiteId || selectedSiteId === 'All Sites') ? '' : selectedSiteId,
|
||||||
assetsIds: assetIdsKey,
|
assetsIds: assetIdsKey,
|
||||||
fromDate: filterFromDate,
|
fromDate: filterFromDate,
|
||||||
tillDate: filterTillDate,
|
tillDate: filterTillDate,
|
||||||
fromChainage: filterMinChainage,
|
fromChainage: filterMinChainage,
|
||||||
toChainage: filterMaxChainage,
|
toChainage: filterMaxChainage,
|
||||||
|
anomalyIds: searchAnomalyId,
|
||||||
|
plaza: filterPlaza,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
const anomalies = res.anomalies || [];
|
const anomalies = res.anomalies || [];
|
||||||
const newStats = {
|
const newStats = {
|
||||||
total: res.count || anomalies.length,
|
globalTotal: res.count || anomalies.length,
|
||||||
audited: anomalies.filter((a: any) => a.IsAudited === 1).length,
|
batchTotal: anomalies.length,
|
||||||
pending: anomalies.filter((a: any) => a.IsAudited === 0).length,
|
batchAudited: anomalies.filter((a: any) => a.IsAudited === 1).length,
|
||||||
anomaliesFound: anomalies.filter((a: any) => a.Audit_status === 1).length,
|
batchPending: anomalies.filter((a: any) => a.IsAudited === 0).length,
|
||||||
|
batchAnomaliesFound: anomalies.filter((a: any) => a.Audit_status === 1).length,
|
||||||
};
|
};
|
||||||
setStats(newStats);
|
setStats(newStats);
|
||||||
// Persist to store cache so re-mounting the Dashboard skips this fetch
|
// Persist to store cache so re-mounting the Dashboard skips this fetch
|
||||||
@@ -73,6 +89,10 @@ export const Dashboard: React.FC = () => {
|
|||||||
assetIds: assetIdsKey,
|
assetIds: assetIdsKey,
|
||||||
minChainage: filterMinChainage,
|
minChainage: filterMinChainage,
|
||||||
maxChainage: filterMaxChainage,
|
maxChainage: filterMaxChainage,
|
||||||
|
searchAnomalyId,
|
||||||
|
filterPlaza,
|
||||||
|
isRectificationEnabled: isRectActive,
|
||||||
|
date: activeDate,
|
||||||
stats: newStats,
|
stats: newStats,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -86,7 +106,7 @@ export const Dashboard: React.FC = () => {
|
|||||||
fetchStats();
|
fetchStats();
|
||||||
}, [currentFilterKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [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 (
|
return (
|
||||||
<Box sx={{ width: '100%', maxWidth: '100vw', display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ width: '100%', maxWidth: '100vw', display: 'flex', flexDirection: 'column' }}>
|
||||||
@@ -96,13 +116,16 @@ export const Dashboard: React.FC = () => {
|
|||||||
|
|
||||||
{/* Stats Row */}
|
{/* Stats Row */}
|
||||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||||
{/* Total Items */}
|
{/* Active Batch */}
|
||||||
<Paper sx={{
|
<Paper sx={{
|
||||||
flex: 1, p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2
|
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' }}>
|
<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>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -112,7 +135,10 @@ export const Dashboard: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Audited</Typography>
|
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Audited</Typography>
|
||||||
<Typography variant="h4" sx={{ color: '#22c55e', fontWeight: 'bold' }}>
|
<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>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -122,7 +148,10 @@ export const Dashboard: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Pending</Typography>
|
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Pending</Typography>
|
||||||
<Typography variant="h4" sx={{ color: '#eab308', fontWeight: 'bold' }}>
|
<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>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -132,16 +161,19 @@ export const Dashboard: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Anomalies Found</Typography>
|
<Typography variant="body2" sx={{ color: '#94a3b8', mb: 1 }}>Anomalies Found</Typography>
|
||||||
<Typography variant="h4" sx={{ color: '#ef4444', fontWeight: 'bold' }}>
|
<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>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Overall Progress */}
|
{/* Active Batch Progress */}
|
||||||
<Paper sx={{ p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2, mb: 3 }}>
|
<Paper sx={{ p: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2, mb: 3 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#f8fafc' }}>Overall Progress</Typography>
|
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#f8fafc' }}>Active Batch Progress</Typography>
|
||||||
<Typography variant="body2" sx={{ color: '#94a3b8' }}>{stats.audited} / {stats.total} completed</Typography>
|
<Typography variant="body2" sx={{ color: '#94a3b8' }}>{stats.batchAudited} / {stats.batchTotal} completed</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
@@ -155,22 +187,56 @@ export const Dashboard: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
|
||||||
<Typography variant="caption" sx={{ color: '#64748b' }}>{progressPercent}% complete</Typography>
|
<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>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Next Session Info */}
|
{/* Session Mechanics Explanation Box */}
|
||||||
<Paper sx={{ p: 2, px: 3, bgcolor: '#0f172a', border: '1px solid #1e293b', borderRadius: 2, mb: 2 }}>
|
<Paper sx={{
|
||||||
<Typography variant="body1" sx={{ color: '#f8fafc' }}>
|
p: 3,
|
||||||
<Typography component="span" sx={{ color: '#3b82f6', fontWeight: 'bold' }}>Next session:</Typography> 50 items will be queued ({stats.pending} pending total)
|
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>
|
</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>
|
</Paper>
|
||||||
|
|
||||||
{/* Warning Box */}
|
{/* Warning Box */}
|
||||||
<Paper sx={{ p: 2, px: 3, bgcolor: '#450a0a', border: '1px solid #7f1d1d', borderRadius: 2, mb: 4, display: 'flex', alignItems: 'center', gap: 1 }}>
|
<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 }} />
|
<WarningAmberIcon sx={{ color: '#ef4444', fontSize: 20 }} />
|
||||||
<Typography variant="body1" sx={{ color: '#f8fafc' }}>
|
<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>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -178,18 +244,24 @@ export const Dashboard: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => navigate('/audit-session')}
|
onClick={() => navigate('/audit-session', { state: { isReviewMode: stats.batchPending === 0 } })}
|
||||||
|
disabled={stats.batchTotal === 0}
|
||||||
sx={{
|
sx={{
|
||||||
py: 2,
|
py: 2,
|
||||||
bgcolor: '#3b82f6',
|
bgcolor: '#3b82f6',
|
||||||
'&:hover': { bgcolor: '#2563eb' },
|
'&:hover': { bgcolor: '#2563eb' },
|
||||||
|
'&.Mui-disabled': { bgcolor: 'rgba(59, 130, 246, 0.3)', color: 'rgba(255,255,255,0.3)' },
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontSize: '1.1rem',
|
fontSize: '1.1rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
borderRadius: 2
|
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>
|
</Button>
|
||||||
|
|
||||||
{/* Footer links */}
|
{/* Footer links */}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface User {
|
|||||||
user_roles: string;
|
user_roles: string;
|
||||||
db_name?: string;
|
db_name?: string;
|
||||||
org_id?: string;
|
org_id?: string;
|
||||||
|
user_id?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
export interface FeedTab {
|
export interface FeedTab {
|
||||||
date: string;
|
date: string;
|
||||||
@@ -21,11 +22,16 @@ interface DashboardCache {
|
|||||||
assetIds: string;
|
assetIds: string;
|
||||||
minChainage: string;
|
minChainage: string;
|
||||||
maxChainage: string;
|
maxChainage: string;
|
||||||
|
searchAnomalyId: string;
|
||||||
|
filterPlaza: string;
|
||||||
|
isRectificationEnabled?: boolean;
|
||||||
|
date?: string;
|
||||||
stats: {
|
stats: {
|
||||||
total: number;
|
globalTotal: number;
|
||||||
audited: number;
|
batchTotal: number;
|
||||||
pending: number;
|
batchAudited: number;
|
||||||
anomaliesFound: number;
|
batchPending: number;
|
||||||
|
batchAnomaliesFound: number;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +44,7 @@ interface FeedState {
|
|||||||
filterTillDate: string;
|
filterTillDate: string;
|
||||||
filterMinChainage: string;
|
filterMinChainage: string;
|
||||||
filterMaxChainage: string;
|
filterMaxChainage: string;
|
||||||
|
filterPlaza: string;
|
||||||
selectedAssetIds: number[];
|
selectedAssetIds: number[];
|
||||||
sites: any[];
|
sites: any[];
|
||||||
auditStatus: string;
|
auditStatus: string;
|
||||||
@@ -48,6 +55,8 @@ interface FeedState {
|
|||||||
lastFetchedFilterKey: string;
|
lastFetchedFilterKey: string;
|
||||||
// Dashboard stats cache
|
// Dashboard stats cache
|
||||||
dashboardCache: DashboardCache;
|
dashboardCache: DashboardCache;
|
||||||
|
searchAnomalyId: string;
|
||||||
|
isRectificationEnabled: boolean;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
setTabs: (tabs: FeedTab[]) => void;
|
setTabs: (tabs: FeedTab[]) => void;
|
||||||
@@ -69,6 +78,9 @@ interface FeedState {
|
|||||||
setFilterTillDate: (date: string) => void;
|
setFilterTillDate: (date: string) => void;
|
||||||
setFilterMinChainage: (val: string) => void;
|
setFilterMinChainage: (val: string) => void;
|
||||||
setFilterMaxChainage: (val: string) => void;
|
setFilterMaxChainage: (val: string) => void;
|
||||||
|
setFilterPlaza: (val: string) => void;
|
||||||
|
setSearchAnomalyId: (id: string) => void;
|
||||||
|
setIsRectificationEnabled: (val: boolean) => void;
|
||||||
resetStore: () => void;
|
resetStore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +92,10 @@ const EMPTY_DASHBOARD_CACHE: DashboardCache = {
|
|||||||
assetIds: '__unset__',
|
assetIds: '__unset__',
|
||||||
minChainage: '__unset__',
|
minChainage: '__unset__',
|
||||||
maxChainage: '__unset__',
|
maxChainage: '__unset__',
|
||||||
|
searchAnomalyId: '__unset__',
|
||||||
|
filterPlaza: '__unset__',
|
||||||
|
isRectificationEnabled: false,
|
||||||
|
date: '',
|
||||||
stats: null,
|
stats: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,47 +108,74 @@ const initialFeedState = {
|
|||||||
filterTillDate: '',
|
filterTillDate: '',
|
||||||
filterMinChainage: '',
|
filterMinChainage: '',
|
||||||
filterMaxChainage: '',
|
filterMaxChainage: '',
|
||||||
|
filterPlaza: '',
|
||||||
selectedAssetIds: [],
|
selectedAssetIds: [],
|
||||||
selectedSite: null,
|
selectedSite: null,
|
||||||
sites: [],
|
sites: [],
|
||||||
auditStatus: 'Auto Audits',
|
auditStatus: 'Manual',
|
||||||
isFiltersModalOpen: false,
|
isFiltersModalOpen: false,
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
lastFetchedFilterKey: '',
|
lastFetchedFilterKey: '',
|
||||||
dashboardCache: EMPTY_DASHBOARD_CACHE,
|
dashboardCache: EMPTY_DASHBOARD_CACHE,
|
||||||
|
searchAnomalyId: '',
|
||||||
|
isRectificationEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFeedStore = create<FeedState>((set) => ({
|
export const useFeedStore = create<FeedState>()(
|
||||||
...initialFeedState,
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
...initialFeedState,
|
||||||
|
|
||||||
resetStore: () => set(initialFeedState),
|
resetStore: () => set(initialFeedState),
|
||||||
|
|
||||||
setTabs: (tabs) => set({ tabs }),
|
setTabs: (tabs) => set({ tabs }),
|
||||||
setSelectedTabIndex: (index) => set({ selectedTabIndex: index }),
|
setSelectedTabIndex: (index) => set({ selectedTabIndex: index }),
|
||||||
setAssets: (assets) => set({ assets }),
|
setAssets: (assets) => set({ assets }),
|
||||||
setAssetTypes: (assetTypes) => set({ assetTypes }),
|
setAssetTypes: (assetTypes) => set({ assetTypes }),
|
||||||
setSelectedAssetIds: (selectedAssetIds) => set({ selectedAssetIds }),
|
setSelectedAssetIds: (selectedAssetIds) => set({ selectedAssetIds }),
|
||||||
setSites: (sites) => set({ sites }),
|
setSites: (sites) => set({ sites }),
|
||||||
setSelectedSite: (selectedSite) => set({ selectedSite }),
|
setSelectedSite: (selectedSite) => set({ selectedSite }),
|
||||||
setAuditStatus: (auditStatus) => set({ auditStatus }),
|
setAuditStatus: (auditStatus) => set({ auditStatus }),
|
||||||
setIsFiltersModalOpen: (isFiltersModalOpen) => set({ isFiltersModalOpen }),
|
setIsFiltersModalOpen: (isFiltersModalOpen) => set({ isFiltersModalOpen }),
|
||||||
setIsInitialized: (isInitialized) => set({ isInitialized }),
|
setIsInitialized: (isInitialized) => set({ isInitialized }),
|
||||||
setLastFetchedFilterKey: (lastFetchedFilterKey) => set({ lastFetchedFilterKey }),
|
setLastFetchedFilterKey: (lastFetchedFilterKey) => set({ lastFetchedFilterKey }),
|
||||||
setDashboardCache: (dashboardCache) => set({ dashboardCache }),
|
setDashboardCache: (dashboardCache) => set({ dashboardCache }),
|
||||||
setFilterFromDate: (filterFromDate) => set({ filterFromDate }),
|
setFilterFromDate: (filterFromDate) => set({ filterFromDate }),
|
||||||
setFilterTillDate: (filterTillDate) => set({ filterTillDate }),
|
setFilterTillDate: (filterTillDate) => set({ filterTillDate }),
|
||||||
setFilterMinChainage: (filterMinChainage) => set({ filterMinChainage }),
|
setFilterMinChainage: (filterMinChainage) => set({ filterMinChainage }),
|
||||||
setFilterMaxChainage: (filterMaxChainage) => set({ filterMaxChainage }),
|
setFilterMaxChainage: (filterMaxChainage) => set({ filterMaxChainage }),
|
||||||
addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })),
|
setFilterPlaza: (filterPlaza) => set({ filterPlaza }),
|
||||||
closeTab: (index) => set((state) => {
|
setSearchAnomalyId: (searchAnomalyId) => set({ searchAnomalyId }),
|
||||||
const newTabs = [...state.tabs];
|
setIsRectificationEnabled: (isRectificationEnabled) => set({ isRectificationEnabled }),
|
||||||
newTabs.splice(index, 1);
|
addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })),
|
||||||
const newIndex = state.selectedTabIndex >= newTabs.length ? newTabs.length - 1 : state.selectedTabIndex;
|
closeTab: (index) => set((state) => {
|
||||||
return { tabs: newTabs, selectedTabIndex: newIndex };
|
const newTabs = [...state.tabs];
|
||||||
}),
|
newTabs.splice(index, 1);
|
||||||
updateTab: (index, partialTab) => set((state) => {
|
const newIndex = state.selectedTabIndex >= newTabs.length ? newTabs.length - 1 : state.selectedTabIndex;
|
||||||
const newTabs = [...state.tabs];
|
return { tabs: newTabs, selectedTabIndex: newIndex };
|
||||||
newTabs[index] = { ...newTabs[index], ...partialTab };
|
}),
|
||||||
return { tabs: newTabs };
|
updateTab: (index, partialTab) => set((state) => {
|
||||||
})
|
const newTabs = [...state.tabs];
|
||||||
}));
|
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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
@@ -10,11 +10,19 @@ export default defineConfig({
|
|||||||
target: 'https://sr-backend-api.takeleap.in',
|
target: 'https://sr-backend-api.takeleap.in',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api\/dashboard/, ''),
|
rewrite: (path) => path.replace(/^\/api\/dashboard/, ''),
|
||||||
|
headers: {
|
||||||
|
Origin: 'https://auditor-qa.seekright.com',
|
||||||
|
Referer: 'https://auditor-qa.seekright.com/'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'/api/audit': {
|
'/api/audit': {
|
||||||
target: 'https://sr-audit.takeleap.in',
|
target: 'https://sr-audit.takeleap.in',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api\/audit/, ''),
|
rewrite: (path) => path.replace(/^\/api\/audit/, ''),
|
||||||
|
headers: {
|
||||||
|
Origin: 'https://auditor-qa.seekright.com',
|
||||||
|
Referer: 'https://auditor-qa.seekright.com/'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user