diff --git a/public/assets/images/anomaly-placeholder.png b/public/assets/images/anomaly-placeholder.png new file mode 100644 index 0000000..ad07ea0 Binary files /dev/null and b/public/assets/images/anomaly-placeholder.png differ diff --git a/public/assets/images/arrow-up.png b/public/assets/images/arrow-up.png new file mode 100644 index 0000000..605b2f9 Binary files /dev/null and b/public/assets/images/arrow-up.png differ diff --git a/src/App.tsx b/src/App.tsx index 2b86d04..0e51362 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { ThemeProvider, createTheme } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; import { MainLayout } from './components/layout/MainLayout'; import { ProtectedRoute } from './components/ProtectedRoute'; @@ -46,6 +47,7 @@ import { AuditSession } from './pages/audit-session/AuditSession'; export default function App() { return ( + {/* Public Routes */} diff --git a/src/api/activityFeedsService.ts b/src/api/activityFeedsService.ts index b55ac19..d9e3abf 100644 --- a/src/api/activityFeedsService.ts +++ b/src/api/activityFeedsService.ts @@ -7,17 +7,45 @@ export const activityFeedsService = { }, getHistoryDates: async (isRectificationEnabled: boolean) => { - // Requires dbName and siteIds, which should be handled by axios interceptors or passed in - const params = new URLSearchParams({ - isRectificationEnabled: String(isRectificationEnabled) + const response = await axiosClient.get('/api/audit/anomaly/get_history_dates', { + params: { isRectificationEnabled } }); - const response = await axiosClient.get(`/api/audit/anomaly/get_history_dates?${params.toString()}`); return response; }, getHistory: async (params: any) => { - const queryParams = new URLSearchParams(params); - const response = await axiosClient.get(`/api/audit/anomaly/history?${queryParams.toString()}`); + if (params.isRectificationEnabled) { + const cleanParams = { ...params }; + delete cleanParams.auditStatus; + const response = await axiosClient.get('/api/audit/anomaly/history', { params: cleanParams }); + return response; + } + + if (params.auditStatus === 'False Audits' || params.isTrueFalse === 'true') { + const pageIndex = params.pageSize ? Math.floor(params.pageNo / params.pageSize) + 1 : 1; + const falseParams: any = { + fromDate: params.fromDate || '', + toDate: params.tillDate || '', + asset_id: params.assetsIds || '', + page: pageIndex, + limit: params.pageSize || 20, + site_id: params.siteIds || '' + }; + if (params.dbName) { + falseParams.dbName = params.dbName; + } + + const response: any = await activityFeedsService.getFalseAnomaly(falseParams); + const resultObj = response?.result || response; + return { + anomalies: resultObj?.data || [], + count: resultObj?.totalCount || 0 + }; + } + + const cleanParams = { ...params }; + delete cleanParams.auditStatus; + const response = await axiosClient.get('/api/audit/anomaly/history', { params: cleanParams }); return response; }, @@ -27,9 +55,27 @@ export const activityFeedsService = { return response; }, + updateAnomaly: async (anomalies: any[], isRectificationEnabled: boolean) => { + const response = await axiosClient.post('/api/audit/anomaly', anomalies, { + params: { isRectificationEnabled } + }); + return response; + }, + + updateAnomalyInDashboard: async (payload: { site_id: string, anomaly_id: number[] }) => { + const response = await axiosClient.post(`/api/audit/auditor/anomaly/update`, payload); + return response; + }, + getOrganizationSites: async (org_id: string) => { - const params = new URLSearchParams({ org_id }); - const response = await axiosClient.get(`/api/dashboard/Master/site?${params.toString()}`); + const response = await axiosClient.get('/api/dashboard/Master/site', { + params: { org_id } + }); + return response; + }, + + getFalseAnomaly: async (params: any) => { + const response = await axiosClient.get('/api/audit/asset/get-false-Anomaly', { params }); return response; } }; diff --git a/src/components/common/TopFilterBar.tsx b/src/components/common/TopFilterBar.tsx index 6b7f2c7..5716ae8 100644 --- a/src/components/common/TopFilterBar.tsx +++ b/src/components/common/TopFilterBar.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Box, Button, FormControl, Select, MenuItem } from '@mui/material'; +import { Box, Button, FormControl, Select, MenuItem, Chip, Typography, TextField, CircularProgress, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; import FilterListIcon from '@mui/icons-material/FilterList'; import HistoryIcon from '@mui/icons-material/History'; import { useFeedStore } from '../../store/feedStore'; import { FeedFilters } from '../../pages/activity-feeds/FeedFilters'; +import { activityFeedsService } from '../../api/activityFeedsService'; const DEBOUNCE_MS = 600; // wait 600ms after user stops changing before committing to store @@ -12,22 +13,42 @@ export const TopFilterBar: React.FC = () => { sites, selectedSite, setSelectedSite, auditStatus, setAuditStatus, - setIsFiltersModalOpen + setIsFiltersModalOpen, + filterFromDate, setFilterFromDate, + filterTillDate, setFilterTillDate, + filterMinChainage, setFilterMinChainage, + filterMaxChainage, setFilterMaxChainage, + selectedAssetIds, setSelectedAssetIds, + assetTypes, + searchAnomalyId, setSearchAnomalyId, + filterPlaza, setFilterPlaza, + isRectificationEnabled, setIsRectificationEnabled, + addTab, selectedTabIndex, setSelectedTabIndex, tabs } = useFeedStore(); // Local draft values — these do NOT trigger API calls - const [localAuditStatus, setLocalAuditStatus] = useState(auditStatus || 'Auto Audits'); + const [localAuditStatus, setLocalAuditStatus] = useState(auditStatus || 'Manual'); const [localSiteId, setLocalSiteId] = useState(selectedSite?.site_id || 'All Sites'); + const [localSearchId, setLocalSearchId] = useState(searchAnomalyId || ''); + const [localPlaza, setLocalPlaza] = useState(filterPlaza || ''); // Sync local state when store changes externally (e.g., on initial load) useEffect(() => { - setLocalAuditStatus(auditStatus || 'Auto Audits'); + setLocalAuditStatus(auditStatus || 'Manual'); }, [auditStatus]); useEffect(() => { setLocalSiteId(selectedSite?.site_id || 'All Sites'); }, [selectedSite?.site_id]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + setLocalSearchId(searchAnomalyId || ''); + }, [searchAnomalyId]); + + useEffect(() => { + setLocalPlaza(filterPlaza || ''); + }, [filterPlaza]); + // Debounced commit for auditStatus const auditDebounce = useRef | null>(null); const handleAuditChange = (value: string) => { @@ -51,14 +72,114 @@ export const TopFilterBar: React.FC = () => { }, DEBOUNCE_MS); }; + // Debounced commit for Search ID + const searchDebounce = useRef | 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 | null>(null); + const handlePlazaChange = (value: string) => { + setLocalPlaza(value); + if (plazaDebounce.current) clearTimeout(plazaDebounce.current); + plazaDebounce.current = setTimeout(() => { + setFilterPlaza(value); // triggers API refetch + }, DEBOUNCE_MS); + }; + // Cleanup on unmount useEffect(() => { return () => { if (auditDebounce.current) clearTimeout(auditDebounce.current); if (siteDebounce.current) clearTimeout(siteDebounce.current); + if (searchDebounce.current) clearTimeout(searchDebounce.current); + if (plazaDebounce.current) clearTimeout(plazaDebounce.current); }; }, []); + // Map asset IDs to human-readable names for displaying in filter chips + const getAssetName = (id: number) => { + for (const type of assetTypes) { + const found = type.assets.find((a: any) => a.asset_id === id); + if (found) return found.asset_name.replace(/_/g, ' '); + } + return `Asset: ${id}`; + }; + + const [historyDates, setHistoryDates] = useState([]); + 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 ( <> @@ -73,24 +194,125 @@ export const TopFilterBar: React.FC = () => { + setIsHistoryDialogOpen(false)} + slotProps={{ + paper: { + sx: { + bgcolor: '#0f172a', + color: '#f8fafc', + border: '1px solid #1e293b', + minWidth: 320, + maxWidth: 450, + p: 1, + } + } + }} + > + Rectification History + + + Enter a date (YYYY-MM-DD) or pick one from the list to view historical rectification records. + + History Date + 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', + } + }} + /> + + + Quick Select (Audited Dates) + + + {loadingHistory ? ( + + + + ) : historyDates.length === 0 ? ( + No dates found + ) : ( + historyDates.map((d) => { + const dateStr = d.audited_on ? d.audited_on.split(' ')[0] : ''; + return ( + 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', + }} + /> + ); + }) + )} + + + + + + + {/* Audit Status — local state, debounced commit */} - - - + {!isRectActive && ( + + + + )} {/* Site — local state, debounced commit */} @@ -108,13 +330,161 @@ export const TopFilterBar: React.FC = () => { + {/* Search ID Textfield */} + 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 */} + 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 } } + } + } + }} + /> + + + {/* Active Filter Chips Bar */} + {isAnyFilterActive && ( + + + Active Filters: + + + {isSiteFiltered && ( + 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 && ( + 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 && ( + { 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 && ( + = ${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 && ( + 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 && ( + 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 && ( + setFilterPlaza('')} + sx={{ bgcolor: 'rgba(59, 130, 246, 0.1)', color: '#60a5fa', border: '1px solid rgba(59, 130, 246, 0.2)', '& .MuiChip-deleteIcon': { color: '#60a5fa' } }} + /> + )} + + + + )} + ); diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 8fba4ca..4353b40 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,11 +1,8 @@ import React, { useState } from 'react'; -import { AppBar, Toolbar, Typography, Box, IconButton, Menu, MenuItem, Button, Select, FormControl } from '@mui/material'; +import { AppBar, Toolbar, Box, IconButton, Menu, MenuItem, Button } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import AccountCircle from '@mui/icons-material/AccountCircle'; -import FilterListIcon from '@mui/icons-material/FilterList'; -import HistoryIcon from '@mui/icons-material/History'; import { useAuthStore } from '../../store/authStore'; -import { useFeedStore } from '../../store/feedStore'; import { useNavigate, useLocation } from 'react-router-dom'; interface HeaderProps { @@ -14,14 +11,11 @@ interface HeaderProps { export const Header: React.FC = ({ toggleSidebar }) => { const { user, logout } = useAuthStore(); - const { sites, selectedSite, setSelectedSite, auditStatus, setAuditStatus, setIsFiltersModalOpen } = useFeedStore(); const navigate = useNavigate(); const location = useLocation(); const [anchorEl, setAnchorEl] = useState(null); - const orgName = user?.org_id || user?.db_name || 'TakeLeep'; // Adjust based on your User model const showNav = !location.pathname.includes('dashboard'); - const isActivityFeeds = location.pathname.includes('activity-feeds'); const handleMenu = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 6ce6b5a..175454f 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -1,18 +1,103 @@ -import React from 'react'; -import { Box, Toolbar, CssBaseline } from '@mui/material'; -import { Outlet, useLocation } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { Box, Toolbar } from '@mui/material'; +import { Outlet } from 'react-router-dom'; import { Header } from './Header'; +import { useFeedStore } from '../../store/feedStore'; +import { useAuthStore } from '../../store/authStore'; +import { activityFeedsService } from '../../api/activityFeedsService'; export const MainLayout: React.FC = () => { - const location = useLocation(); + const { isInitialized, setAssets, setAssetTypes, setSites, setSelectedSite, setIsInitialized } = useFeedStore(); + const { user } = useAuthStore(); + + useEffect(() => { + if (isInitialized) return; + + const loadInitialMetadata = async () => { + try { + // Fetch Assets + const assetRes: any = await activityFeedsService.getAssets(); + if (assetRes && assetRes.asset_types) { + let flattenedAssets: any[] = []; + assetRes.asset_types.forEach((type: any) => { + const mapped = type.assets.map((a: any) => ({ + id: a.asset_id, + name: a.asset_name, + asset_type_id: type.type_id + })); + flattenedAssets = [...flattenedAssets, ...mapped]; + }); + setAssets(flattenedAssets); + setAssetTypes(assetRes.asset_types); + } + + // Fetch Sites + if (user?.org_id) { + const sitesRes: any = await activityFeedsService.getOrganizationSites(user.org_id); + let parsedSites: any[] = []; + if (sitesRes?.responseData?.[0]?.records) { + parsedSites = sitesRes.responseData[0].records; + if (parsedSites.length === 1 && Array.isArray(parsedSites[0])) { + parsedSites = parsedSites[0]; + } else if (parsedSites.length > 0 && Array.isArray(parsedSites[0])) { + parsedSites = parsedSites.flat(); + } + } else if (Array.isArray(sitesRes)) { + parsedSites = sitesRes; + } else if (sitesRes?.data && Array.isArray(sitesRes.data)) { + parsedSites = sitesRes.data; + } + + if (parsedSites.length > 0) { + parsedSites = parsedSites.filter(site => { + let siteOrgId = site.org_id; + if (siteOrgId === undefined && site.sub_sites && site.sub_sites.length > 0) { + siteOrgId = site.sub_sites[0].org_id; + } + if (siteOrgId !== undefined && siteOrgId !== null) { + return String(siteOrgId) === String(user.org_id); + } + return true; + }).map(site => { + if (!site.site_id && site.sub_sites && site.sub_sites.length > 0) { + site.site_id = site.sub_sites.map((sub: any) => sub.site_id).filter(Boolean).join(','); + } + return site; + }); + + setSites(parsedSites); + + // ONLY override selectedSite if there isn't a valid one already in the store + const currentSelectedSite = useFeedStore.getState().selectedSite; + if (currentSelectedSite && currentSelectedSite.site_id) { + if (currentSelectedSite.site_id !== 'All Sites') { + // Verify it still exists in the fetched sites list + const exists = parsedSites.some(s => String(s.site_id) === String(currentSelectedSite.site_id)); + if (!exists) { + setSelectedSite({ site_id: 'All Sites', site_name: 'All Sites' }); + } + } + } else { + setSelectedSite({ site_id: 'All Sites', site_name: 'All Sites' }); + } + } + } + } catch (err) { + console.error("Failed to load initial metadata in MainLayout", err); + } finally { + setIsInitialized(true); + } + }; + + loadInitialMetadata(); + }, [isInitialized, user?.org_id, setAssets, setAssetTypes, setSites, setSelectedSite, setIsInitialized]); return ( -
- + {/* Spacer for Header */} {/* Child routes get injected here */} diff --git a/src/pages/account/Login.tsx b/src/pages/account/Login.tsx index 1ae6165..8a7975d 100644 --- a/src/pages/account/Login.tsx +++ b/src/pages/account/Login.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Box, Button, TextField, Typography, Container, Alert } from '@mui/material'; +import { Box, Button, TextField, Container, Alert, Paper } from '@mui/material'; import { useNavigate } from 'react-router-dom'; import { useAuthStore } from '../../store/authStore'; import { accountService } from '../../api/accountService'; @@ -76,88 +76,138 @@ export const Login: React.FC = () => { }; return ( - - - - {/* Placeholder for Logo */} - - Logo { e.currentTarget.style.display = 'none'; }} /> - - - - setUsername(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleLogin()} - InputProps={{ - sx: { borderRadius: '50px', '& fieldset': { borderColor: '#738ab8' } } - }} - sx={{ - mb: 1, - '& .MuiInputBase-input': { padding: '10px 14px', fontSize: '0.9rem', color: '#333' } - }} - /> - 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' } - }} - /> - - - + + + + {/* Logo */} + + Logo { + e.currentTarget.src = "/images/SeekRightLogo.png"; + }} + /> + + 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' } + }} + /> + 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' } + }} + /> + + + + - - {isInvalid && ( - - Invalid Credentials - - )} - - + {isInvalid && ( + + Invalid Credentials + + )} + + + {/* Database Selection Dialog for SR_AUDITOR */} - + ); }; diff --git a/src/pages/activity-feeds/AnomalyTable.tsx b/src/pages/activity-feeds/AnomalyTable.tsx index 334b16b..dd98b17 100644 --- a/src/pages/activity-feeds/AnomalyTable.tsx +++ b/src/pages/activity-feeds/AnomalyTable.tsx @@ -3,14 +3,20 @@ import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead import { useFeedStore } from '../../store/feedStore'; export const AnomalyTable: React.FC = () => { - const { tabs, selectedTabIndex, updateTab } = useFeedStore(); + const { tabs, selectedTabIndex, updateTab, filterPlaza, isRectificationEnabled } = useFeedStore(); if (tabs.length === 0 || selectedTabIndex < 0) { return Loading feeds...; } const activeTab = tabs[selectedTabIndex]; - const anomalies = activeTab.anomalies || []; + const rawAnomalies = activeTab.anomalies || []; + + // Client-side Plaza filtering fallback + const anomalies = rawAnomalies.filter((anomaly: any) => { + if (!filterPlaza) return true; + return String(anomaly.Plaza || '').toLowerCase().includes(filterPlaza.toLowerCase()); + }); if (anomalies.length === 0) { return ( @@ -33,30 +39,36 @@ export const AnomalyTable: React.FC = () => { }; return ( - + - #ID - PLAZA - ROAD - DATE - CHAINAGE - LAT/LONG - ANOMALY - SIDE - ANOMALY True/False - FEATURES - COMMENTS - CHECKED - AUTO AUDIT + #ID + PLAZA + ROAD + + + DATE + + + + CHAINAGE + LAT/LONG + ANOMALY + SIDE + ANOMALY
True/False
+ FEATURES + {(activeTab?.date === '' && isRectificationEnabled) ? 'ISSUE' : 'COMMENTS'} + CHECKED + BULK AUDIT
- {anomalies.map((anomaly: any, index: number) => { - const isSelected = activeTab.selectedAnomalyIndex === index; + {anomalies.map((anomaly: any, _filteredIndex: number) => { + const realIndex = rawAnomalies.indexOf(anomaly); + const isSelected = activeTab.selectedAnomalyIndex === realIndex; const assetName = anomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset'; - const rowColor = getRowColor(index, assetName); + const rowColor = getRowColor(realIndex, assetName); // Parse Position let latStr = 'N/A'; @@ -74,8 +86,8 @@ export const AnomalyTable: React.FC = () => { return ( handleRowClick(index)} + key={anomaly._id || anomaly.id || _filteredIndex} + onClick={() => handleRowClick(realIndex)} sx={{ bgcolor: isSelected ? 'rgba(59, 130, 246, 0.2)' : rowColor, cursor: 'pointer', @@ -84,35 +96,47 @@ export const AnomalyTable: React.FC = () => { mb: 1, }} > - #{anomaly.id || anomaly._id?.substring(0, 7) || 'N/A'} - {anomaly.Plaza || 'N/A'} - {anomaly.road || anomaly.Road || 'N/A'} - {anomaly.Created_on ? new Date(anomaly.Created_on).toLocaleDateString() : 'N/A'} - {anomaly.Chainage || 'N/A'} - + #{anomaly.id || anomaly._id?.substring(0, 7) || 'N/A'} + {anomaly.Plaza || 'N/A'} + {anomaly.road || anomaly.Road || 'N/A'} + {anomaly.Created_on ? new Date(anomaly.Created_on).toLocaleDateString() : 'N/A'} + {anomaly.Chainage || 'N/A'} + {latStr}
{longStr}
- {assetName} - {anomaly.Side || 'N/A'} - - + {assetName} + {anomaly.Side || 'N/A'} + + + + - {anomaly.Features || 'N/A'} - {anomaly.Comments || '-'} - + {anomaly.Features || 'N/A'} + {anomaly.Comments || '-'} + - - + + { + 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 }); + }} + />
); diff --git a/src/pages/activity-feeds/FeedFilters.tsx b/src/pages/activity-feeds/FeedFilters.tsx index f77bfbf..fbd94dd 100644 --- a/src/pages/activity-feeds/FeedFilters.tsx +++ b/src/pages/activity-feeds/FeedFilters.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useEffect, useCallback, memo } from 'react'; +import React, { useState, useMemo, useCallback, memo } from 'react'; import { Box, TextField, Checkbox, FormControlLabel, Button, Typography, Dialog, DialogContent, DialogActions, CircularProgress, Divider, InputAdornment, Chip @@ -153,7 +153,7 @@ export const FeedFilters: React.FC = () => { const handleApplyFilter = async () => { setLoading(true); try { - const { selectedSite, auditStatus, setFilterFromDate, setFilterTillDate } = useFeedStore.getState(); + const { selectedSite, auditStatus, setFilterFromDate, setFilterTillDate, filterPlaza, isRectificationEnabled, searchAnomalyId } = useFeedStore.getState(); const draftAssetArr = Array.from(draftSet); // Commit ALL filter state to the global store @@ -168,6 +168,12 @@ export const FeedFilters: React.FC = () => { const siteIdParam = (!selectedSite || selectedSite?.site_id === 'All Sites') ? '' : String(selectedSite.site_id || ''); + const isRectActive = isRectificationEnabled || activeTab.isRectificationTab; + const assetIdsKey = draftAssetArr.join(','); + + // Build the correct filter key that will be calculated in GlobalFeed + const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeTab.date}|${draftFromDate}|${draftTillDate}|${draftMinChainage}|${draftMaxChainage}|${assetIdsKey}`; + const historyRes: any = await activityFeedsService.getHistory({ date: activeTab.date, fromDate: draftFromDate, @@ -175,19 +181,27 @@ export const FeedFilters: React.FC = () => { pageNo: 0, pageSize: 20, siteIds: siteIdParam, - assetsIds: draftAssetArr.join(','), - isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both', - isRectificationEnabled: false, - auto_audit: false, + assetsIds: assetIdsKey, + auditStatus: auditStatus, + isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), + isRectificationEnabled: isRectActive, + auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), fromChainage: draftMinChainage, toChainage: draftMaxChainage, + plaza: filterPlaza, }); if (historyRes?.anomalies) { updateTab(selectedTabIndex, { anomalies: historyRes.anomalies, anomaliesCopy: historyRes.anomalies, totalAnomalyCount: historyRes.count || historyRes.anomalies.length, - filters: { fromDate: draftFromDate, tillDate: draftTillDate, fromChainage: draftMinChainage, toChainage: draftMaxChainage } + filters: { + fromDate: draftFromDate, + tillDate: draftTillDate, + fromChainage: draftMinChainage, + toChainage: draftMaxChainage, + lastFetchedFilterKey: filterKey + } }); } } @@ -207,13 +221,15 @@ export const FeedFilters: React.FC = () => { fullWidth // Zero transition — opens instantly transitionDuration={0} - PaperProps={{ - sx: { - borderRadius: 3, - bgcolor: '#0f172a', - color: '#f8fafc', - border: '1px solid #1e293b', - maxHeight: '90vh' + slotProps={{ + paper: { + sx: { + borderRadius: 3, + bgcolor: '#0f172a', + color: '#f8fafc', + border: '1px solid #1e293b', + maxHeight: '90vh' + } } }} > diff --git a/src/pages/activity-feeds/GlobalFeed.tsx b/src/pages/activity-feeds/GlobalFeed.tsx index 517cce3..117c645 100644 --- a/src/pages/activity-feeds/GlobalFeed.tsx +++ b/src/pages/activity-feeds/GlobalFeed.tsx @@ -1,198 +1,158 @@ import React, { useEffect } from 'react'; -import { Box, Typography, TablePagination, Button, FormControl, Select, MenuItem } from '@mui/material'; -import FilterListIcon from '@mui/icons-material/FilterList'; -import HistoryIcon from '@mui/icons-material/History'; +import { Box, TablePagination } from '@mui/material'; import { TopFilterBar } from '../../components/common/TopFilterBar'; import { FeedTabs } from './FeedTabs'; import { AnomalyTable } from './AnomalyTable'; import { PreviewPanel } from './PreviewPanel'; import { useFeedStore } from '../../store/feedStore'; -import { useAuthStore } from '../../store/authStore'; import { activityFeedsService } from '../../api/activityFeedsService'; export const GlobalFeed: React.FC = () => { const { - setAssets, setAssetTypes, addTab, tabs, selectedTabIndex, updateTab, - sites, setSites, selectedSite, setSelectedSite, auditStatus, - isInitialized, setIsInitialized, - lastFetchedFilterKey, setLastFetchedFilterKey + addTab, tabs, selectedTabIndex, updateTab, + selectedSite, auditStatus, + isInitialized, + searchAnomalyId, filterPlaza, + isRectificationEnabled, + filterFromDate, filterTillDate, + filterMinChainage, filterMaxChainage, + selectedAssetIds } = useFeedStore(); - const { user } = useAuthStore(); - // Ref to skip the first run of the site/auditStatus effect - const isFirstRender = React.useRef(true); + useEffect(() => { - // Initial Load - const loadInitialData = async () => { - try { - const assetRes: any = await activityFeedsService.getAssets(); - if (assetRes && assetRes.asset_types) { - // Map assets mimicking the Angular logic - let flattenedAssets: any[] = []; - assetRes.asset_types.forEach((type: any) => { - const mapped = type.assets.map((a: any) => ({ - id: a.asset_id, - name: a.asset_name, - asset_type_id: type.type_id - })); - flattenedAssets = [...flattenedAssets, ...mapped]; + // GUARD: Wait until global metadata is initialized in MainLayout + if (!isInitialized) { + return; + } + + const initTabs = async () => { + // Add default Unaudited tab if empty and fetch data + if (useFeedStore.getState().tabs.length === 0) { + addTab({ + date: '', // Unaudited + isRectificationTab: false, + totalAnomalyCount: 0, + anomalies: [], + anomaliesCopy: [], + selectedAnomalyIndex: 0, + filters: {}, + page: 0, + rowsPerPage: 20 + }); + + const currentSite = useFeedStore.getState().selectedSite; + const siteIdParam = (!currentSite || currentSite?.site_id === 'All Sites') ? '' : String(currentSite.site_id || ''); + const assetIdsParam = useFeedStore.getState().selectedAssetIds.join(','); + + try { + const historyRes: any = await activityFeedsService.getHistory({ + date: '', + pageNo: 0, + pageSize: 20, + siteIds: siteIdParam, + auditStatus: auditStatus, + isTrueFalse: isRectificationEnabled ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), + auto_audit: isRectificationEnabled ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), + isRectificationEnabled: isRectificationEnabled, + assetsIds: assetIdsParam, + fromDate: useFeedStore.getState().filterFromDate, + tillDate: useFeedStore.getState().filterTillDate, + fromChainage: useFeedStore.getState().filterMinChainage, + toChainage: useFeedStore.getState().filterMaxChainage, + anomalyIds: searchAnomalyId, + plaza: filterPlaza }); - setAssets(flattenedAssets); - setAssetTypes(assetRes.asset_types); // store raw grouped data for filters dialog - } - - // Fetch Sites - let defaultSite = null; - if (user?.org_id) { - try { - const sitesRes: any = await activityFeedsService.getOrganizationSites(user.org_id); - let parsedSites: any[] = []; - if (sitesRes?.responseData?.[0]?.records) { - parsedSites = sitesRes.responseData[0].records; - // Handle nested array response [ [ {site1}, {site2} ] ] - if (parsedSites.length === 1 && Array.isArray(parsedSites[0])) { - parsedSites = parsedSites[0]; - } else if (parsedSites.length > 0 && Array.isArray(parsedSites[0])) { - parsedSites = parsedSites.flat(); - } - } else if (Array.isArray(sitesRes)) { - parsedSites = sitesRes; - } else if (sitesRes?.data && Array.isArray(sitesRes.data)) { - parsedSites = sitesRes.data; - } - - if (parsedSites.length > 0) { - // Filter to ensure we only show sites for the currently selected org - parsedSites = parsedSites.filter(site => { - let siteOrgId = site.org_id; - if (siteOrgId === undefined && site.sub_sites && site.sub_sites.length > 0) { - siteOrgId = site.sub_sites[0].org_id; - } - - if (siteOrgId !== undefined && siteOrgId !== null) { - return String(siteOrgId) === String(user.org_id); - } - return true; - }).map(site => { - // Normalize site_id: if missing, aggregate from sub_sites - if (!site.site_id && site.sub_sites && site.sub_sites.length > 0) { - site.site_id = site.sub_sites.map((sub: any) => sub.site_id).filter(Boolean).join(','); - } - return site; - }); - - setSites(parsedSites); - defaultSite = { site_id: 'All Sites', site_name: 'All Sites' }; - setSelectedSite(defaultSite); - } - } catch (siteErr) { - console.error("Failed to fetch sites", siteErr); - } - } - - // Add default Unaudited tab if empty and fetch data - if (useFeedStore.getState().tabs.length === 0) { - addTab({ - date: '', // Unaudited - isRectificationTab: false, - totalAnomalyCount: 0, - anomalies: [], - anomaliesCopy: [], - selectedAnomalyIndex: 0, - filters: {}, - page: 0, - rowsPerPage: 20 - }); - - try { - const historyRes: any = await activityFeedsService.getHistory({ - date: '', - pageNo: 0, - pageSize: 20, - siteIds: defaultSite?.site_id === 'All Sites' ? '' : (defaultSite?.site_id || ''), - isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both', - auto_audit: false, - isRectificationEnabled: false + if (historyRes && historyRes.anomalies) { + updateTab(0, { + anomalies: historyRes.anomalies, + anomaliesCopy: historyRes.anomalies, + totalAnomalyCount: historyRes.count || historyRes.anomalies.length }); - if (historyRes && historyRes.anomalies) { - updateTab(0, { - anomalies: historyRes.anomalies, - anomaliesCopy: historyRes.anomalies, - totalAnomalyCount: historyRes.count || historyRes.anomalies.length - }); - } - } catch (historyErr) { - console.error("[GlobalFeed] Failed to load history", historyErr); } + } catch (historyErr) { + console.error("[GlobalFeed] Failed to load history", historyErr); } - } catch (error) { - console.error("Failed to load initial feed data", error); - } finally { - setIsInitialized(true); // Mark as initialized regardless, to prevent retry loops } }; - // GUARD: Only run initial load once across tab switches - if (useFeedStore.getState().isInitialized) { - return; - } - loadInitialData(); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + initTabs(); + }, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps - // Fetch data when selectedSite or auditStatus changes + // Fetch data when selectedSite, auditStatus, searchAnomalyId, filterPlaza, tab, or metadata filters change useEffect(() => { // GUARD: Skip if tabs are not initialized (initial load hasn't finished) if (useFeedStore.getState().tabs.length === 0) { return; } - // Build a key for the current filter state - const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}`; + const currentTabIndex = selectedTabIndex > -1 ? selectedTabIndex : 0; + const activeTab = tabs[currentTabIndex]; + if (!activeTab) return; - // CACHE HIT: This exact filter combo was already fetched — skip re-fetch on remount - if (filterKey === lastFetchedFilterKey) { + const isRectActive = isRectificationEnabled || activeTab.isRectificationTab; + const assetIdsKey = selectedAssetIds.join(','); + + // Build a key for the current filter state on the active tab + const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeTab.date}|${filterFromDate}|${filterTillDate}|${filterMinChainage}|${filterMaxChainage}|${assetIdsKey}`; + + // CACHE HIT: This exact tab and filter combo was already fetched + if (filterKey === activeTab.filters?.lastFetchedFilterKey && activeTab.anomalies && activeTab.anomalies.length > 0) { return; } const fetchFilteredHistory = async () => { - const state = useFeedStore.getState(); - const currentTabIndex = state.selectedTabIndex > -1 ? state.selectedTabIndex : 0; - const activeTab = state.tabs[currentTabIndex]; const siteIdParam = (!selectedSite || selectedSite?.site_id === 'All Sites') ? '' : String(selectedSite.site_id || ''); try { const historyRes: any = await activityFeedsService.getHistory({ - date: activeTab?.date || '', + date: activeTab.date || '', pageNo: 0, pageSize: 20, siteIds: siteIdParam, - isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both', - auto_audit: false, - isRectificationEnabled: false, - ...(activeTab?.filters || {}) + auditStatus: auditStatus, + isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), + auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), + isRectificationEnabled: isRectActive, + anomalyIds: searchAnomalyId, + plaza: filterPlaza, + assetsIds: assetIdsKey, + fromDate: filterFromDate, + tillDate: filterTillDate, + fromChainage: filterMinChainage, + toChainage: filterMaxChainage, }); if (historyRes && historyRes.anomalies) { - useFeedStore.getState().updateTab(currentTabIndex, { + updateTab(currentTabIndex, { anomalies: historyRes.anomalies, anomaliesCopy: historyRes.anomalies, - totalAnomalyCount: historyRes.count || historyRes.anomalies.length + totalAnomalyCount: historyRes.count || historyRes.anomalies.length, + filters: { + ...(activeTab.filters || {}), + fromDate: filterFromDate, + tillDate: filterTillDate, + fromChainage: filterMinChainage, + toChainage: filterMaxChainage, + lastFetchedFilterKey: filterKey + } }); - setLastFetchedFilterKey(filterKey); // mark this combo as fetched } } catch (err) { - console.error('[GlobalFeed] Failed to load history for site/audit status change', err); + console.error('[GlobalFeed] Failed to load history for active tab/filters change', err); } }; fetchFilteredHistory(); - // Use primitive/stable values to avoid spurious re-runs. - // selectedSite?.site_id and auditStatus are both strings/primitives. - }, [selectedSite?.site_id, auditStatus]); // eslint-disable-line react-hooks/exhaustive-deps + }, [ + selectedSite?.site_id, auditStatus, searchAnomalyId, filterPlaza, isRectificationEnabled, selectedTabIndex, tabs.length, + filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, selectedAssetIds + ]); - const handlePageChange = async (event: unknown, newPage: number) => { + const handlePageChange = async (_event: unknown, newPage: number) => { if (selectedTabIndex < 0 || tabs.length === 0) return; const activeTab = tabs[selectedTabIndex]; - const allSiteIds = sites.map(s => s.site_id).filter(Boolean).join(','); const rowsPerPage = activeTab.rowsPerPage || 20; + const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab; + const assetIdsKey = selectedAssetIds.join(','); try { const historyRes: any = await activityFeedsService.getHistory({ @@ -200,10 +160,17 @@ export const GlobalFeed: React.FC = () => { pageNo: newPage * rowsPerPage, pageSize: rowsPerPage, siteIds: selectedSite?.site_id === 'All Sites' ? '' : (selectedSite?.site_id || ''), - isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both', - auto_audit: false, - isRectificationEnabled: false, - ...(activeTab?.filters || {}) + auditStatus: auditStatus, + isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), + auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), + isRectificationEnabled: isRectActive, + anomalyIds: searchAnomalyId, + plaza: filterPlaza, + assetsIds: assetIdsKey, + fromDate: filterFromDate, + tillDate: filterTillDate, + fromChainage: filterMinChainage, + toChainage: filterMaxChainage, }); if (historyRes && historyRes.anomalies) { updateTab(selectedTabIndex, { @@ -221,9 +188,10 @@ export const GlobalFeed: React.FC = () => { const handleRowsPerPageChange = async (event: React.ChangeEvent) => { if (selectedTabIndex < 0 || tabs.length === 0) return; const activeTab = tabs[selectedTabIndex]; - const allSiteIds = sites.map(s => s.site_id).filter(Boolean).join(','); const newRowsPerPage = parseInt(event.target.value, 10); const newPage = 0; + const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab; + const assetIdsKey = selectedAssetIds.join(','); try { const historyRes: any = await activityFeedsService.getHistory({ @@ -231,10 +199,17 @@ export const GlobalFeed: React.FC = () => { pageNo: newPage * newRowsPerPage, pageSize: newRowsPerPage, siteIds: selectedSite?.site_id === 'All Sites' ? '' : (selectedSite?.site_id || ''), - isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both', - auto_audit: false, - isRectificationEnabled: false, - ...(activeTab?.filters || {}) + auditStatus: auditStatus, + isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), + auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), + isRectificationEnabled: isRectActive, + anomalyIds: searchAnomalyId, + plaza: filterPlaza, + assetsIds: assetIdsKey, + fromDate: filterFromDate, + tillDate: filterTillDate, + fromChainage: filterMinChainage, + toChainage: filterMaxChainage, }); if (historyRes && historyRes.anomalies) { updateTab(selectedTabIndex, { @@ -258,7 +233,7 @@ export const GlobalFeed: React.FC = () => { {/* Left side Table */} - + { rowsPerPageOptions={[10, 20, 50, 100]} /> - + diff --git a/src/pages/activity-feeds/PreviewPanel.tsx b/src/pages/activity-feeds/PreviewPanel.tsx index d3ed11d..2d3aafb 100644 --- a/src/pages/activity-feeds/PreviewPanel.tsx +++ b/src/pages/activity-feeds/PreviewPanel.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { useFeedStore } from '../../store/feedStore'; export const PreviewPanel: React.FC = () => { - const { tabs, selectedTabIndex } = useFeedStore(); + const { tabs, selectedTabIndex, isRectificationEnabled } = useFeedStore(); const navigate = useNavigate(); if (tabs.length === 0 || selectedTabIndex < 0) return null; @@ -23,7 +23,10 @@ export const PreviewPanel: React.FC = () => { ); } - const IMAGES_AWS_PREFIX = "https://auditor-master-images.seekright.com/SeekRight/"; + const isRectActive = isRectificationEnabled || activeTab?.isRectificationTab; + const IMAGES_AWS_PREFIX = isRectActive + ? "https://sr-img.seekright.com/" + : "https://auditor-master-images.seekright.com/SeekRight/"; const IMAGES_TEST_AUDIT_PREFIX = "https://dashboard-images.seekright.com/SeekRight/"; let masterImageSrc = ''; @@ -41,13 +44,17 @@ export const PreviewPanel: React.FC = () => { } return ( - + Master { + e.currentTarget.onerror = null; + e.currentTarget.src = '/assets/images/master_image_not_available.png'; + }} /> MASTER IMAGE @@ -59,6 +66,10 @@ export const PreviewPanel: React.FC = () => { src={anomalyImageSrc} alt="Anomaly" style={{ width: '100%', height: 'auto', display: 'block' }} + onError={(e) => { + e.currentTarget.onerror = null; + e.currentTarget.src = '/assets/images/anomaly-placeholder.png'; + }} /> ANOMALY IMAGE diff --git a/src/pages/audit-session/AuditSession.tsx b/src/pages/audit-session/AuditSession.tsx index d1b6577..33a0b66 100644 --- a/src/pages/audit-session/AuditSession.tsx +++ b/src/pages/audit-session/AuditSession.tsx @@ -1,11 +1,37 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Box, Typography, Button, IconButton, Chip } from '@mui/material'; +import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; import { useNavigate, useLocation } from 'react-router-dom'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { activityFeedsService } from '../../api/activityFeedsService'; import { useAuthStore } from '../../store/authStore'; import { useFeedStore } from '../../store/feedStore'; +const SAFE_CATEGORIES = [ + { id: 1, value: "not an anomaly", label: "Not an Anomaly", color: "#10b981", key: "1" }, + { id: 2, value: "vehicle occlusion", label: "Vehicle Occlusion", color: "#3b82f6", key: "2" }, + { id: 3, value: "low light condition", label: "Low Light Condition", color: "#6366f1", key: "3" }, + { id: 4, value: "duplicate", label: "Duplicate", color: "#a855f7", key: "4" }, + { id: 5, value: "gps issue", label: "GPS Issue", color: "#ec4899", key: "5" }, + { id: 6, value: "mismatch", label: "Mismatch", color: "#f43f5e", key: "6" }, + { id: 7, value: "bad image", label: "Bad Image", color: "#f97316", key: "7" }, + { id: 8, value: "patch", label: "Patch", color: "#eab308", key: "8" }, + { id: 9, value: "out of range", label: "Out of Range", color: "#84cc16", key: "9" }, + { id: 10, value: "false others", label: "False Others", color: "#64748b", key: "0" } +]; + +const ANOMALY_CATEGORIES = [ + { id: 1, value: "plant", label: "Plant", color: "#10b981", key: "1" }, + { id: 2, value: "bent", label: "Bent", color: "#3b82f6", key: "2" }, + { id: 3, value: "broken", label: "Broken", color: "#ef4444", key: "3" }, + { id: 4, value: "missing", label: "Missing", color: "#f97316", key: "4" }, + { id: 5, value: "plant overgrown", label: "Plant Overgrown", color: "#84cc16", key: "5" }, + { id: 6, value: "paint worn off", label: "Paint Worn Off", color: "#eab308", key: "6" }, + { id: 7, value: "dirt", label: "Dirt", color: "#78350f", key: "7" }, + { id: 8, value: "not working", label: "Not Working", color: "#ec4899", key: "8" }, + { id: 9, value: "others", label: "Others", color: "#64748b", key: "9" }, + { id: 10, value: "occlusion", label: "Occlusion", color: "#6366f1", key: "0" } +]; + export const AuditSession: React.FC = () => { const navigate = useNavigate(); const { user } = useAuthStore(); @@ -14,54 +40,177 @@ export const AuditSession: React.FC = () => { const [loading, setLoading] = useState(true); const [carouselIndex, setCarouselIndex] = useState(0); - // For the right sidebar state + // Form states const [selectedCategory, setSelectedCategory] = useState(null); const [notes, setNotes] = useState(''); const [isAnomaly, setIsAnomaly] = useState(null); // true = Anomaly, false = Safe + const [plantSubCategory, setPlantSubCategory] = useState<'in grown' | 'out grown' | null>(null); + const [notAnAnomalySubCategory, setNotAnAnomalySubCategory] = useState<'comparison error' | 'low light' | 'far asset' | 'no detection' | 'image mismatch' | null>(null); + + // Premium Audit UX states + const [itemAuditStates, setItemAuditStates] = useState<('pending' | 'audited' | 'skipped')[]>([]); + const [isQuickAudit, setIsQuickAudit] = useState(() => { + const cached = localStorage.getItem('isQuickAudit'); + return cached === null ? true : cached === 'true'; + }); + const [activeKeyFlash, setActiveKeyFlash] = useState(null); + const [isNotesFocused, setIsNotesFocused] = useState(false); + const [showCompletionModal, setShowCompletionModal] = useState(false); const location = useLocation(); - const { tabs, selectedTabIndex } = useFeedStore(); + const { + tabs, selectedTabIndex, updateTab, + selectedSite, filterFromDate, filterTillDate, + filterMinChainage, filterMaxChainage, selectedAssetIds, + searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza + } = useFeedStore(); + + const activeTab = tabs[selectedTabIndex] || { date: '', isRectificationTab: false, page: 0, rowsPerPage: 20 }; + const activeDate = activeTab.date || ''; + const isRectActive = isRectificationEnabled || activeTab.isRectificationTab; + + const loadSessionData = useCallback(async () => { + try { + setLoading(true); + const selectedSiteId = selectedSite?.site_id ?? ''; + const assetIdsKey = selectedAssetIds.join(','); + const isReviewMode = location.state?.isReviewMode; + + const res: any = await activityFeedsService.getHistory({ + date: activeDate, + pageNo: 0, + pageSize: 50, + isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), + auditStatus: auditStatus, + auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), + isRectificationEnabled: isRectActive, + siteIds: (!selectedSiteId || selectedSiteId === 'All Sites') ? '' : selectedSiteId, + assetsIds: assetIdsKey, + fromDate: filterFromDate, + tillDate: filterTillDate, + fromChainage: filterMinChainage, + toChainage: filterMaxChainage, + anomalyIds: searchAnomalyId, + plaza: filterPlaza, + }); + if (res && res.anomalies) { + setAnomalies(res.anomalies); + setCurrentIndex(0); + setCarouselIndex(0); + setSelectedCategory(null); + setNotes(''); + setIsAnomaly(null); + setPlantSubCategory(null); + setNotAnAnomalySubCategory(null); + setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending')); + } else { + setLoading(false); + } + } catch (err) { + console.error('Failed to load session anomalies:', err); + } finally { + setLoading(false); + } + }, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state]); + + const handleLoadNextFeedPage = useCallback(async () => { + try { + setLoading(true); + const nextPage = (activeTab.page || 0) + 1; + const selectedSiteId = selectedSite?.site_id ?? ''; + const assetIdsKey = selectedAssetIds.join(','); + const isReviewMode = location.state?.isReviewMode; + const feedPageSize = activeTab.rowsPerPage || 20; + + const res: any = await activityFeedsService.getHistory({ + date: activeDate, + pageNo: nextPage * feedPageSize, + pageSize: feedPageSize, + isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), + auditStatus: auditStatus, + auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), + isRectificationEnabled: isRectActive, + siteIds: (!selectedSiteId || selectedSiteId === 'All Sites') ? '' : selectedSiteId, + assetsIds: assetIdsKey, + fromDate: filterFromDate, + tillDate: filterTillDate, + fromChainage: filterMinChainage, + toChainage: filterMaxChainage, + anomalyIds: searchAnomalyId, + plaza: filterPlaza, + }); + + if (res && res.anomalies && res.anomalies.length > 0) { + updateTab(selectedTabIndex, { + anomalies: res.anomalies, + anomaliesCopy: res.anomalies, + page: nextPage + }); + setAnomalies(res.anomalies); + setCurrentIndex(0); + setCarouselIndex(0); + setSelectedCategory(null); + setNotes(''); + setIsAnomaly(null); + setPlantSubCategory(null); + setNotAnAnomalySubCategory(null); + setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending')); + } else { + alert("No more anomalies left in the feed."); + navigate('/activity-feeds'); + } + } catch (err) { + console.error('Failed to load next page of feed:', err); + } finally { + setLoading(false); + } + }, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, activeTab, selectedTabIndex, updateTab, navigate]); useEffect(() => { const navState = location.state as any; - if (navState?.useExistingFeed) { - // Use anomalies from the global feed store const activeTab = tabs[selectedTabIndex]; if (activeTab && activeTab.anomalies) { - setAnomalies(activeTab.anomalies); - setCurrentIndex(navState.startIndex || 0); + // Client-side Plaza filtering fallback check just like in AnomalyTable.tsx + const filteredAnomalies = activeTab.anomalies.filter((anomaly: any) => { + if (!filterPlaza) return true; + return String(anomaly.Plaza || '').toLowerCase().includes(filterPlaza.toLowerCase()); + }); + setAnomalies(filteredAnomalies); + // Find correct index in filtered list + const origSelectedAnomaly = activeTab.anomalies[navState.startIndex || 0]; + const newIdx = origSelectedAnomaly + ? filteredAnomalies.findIndex((a: any) => (a.id === origSelectedAnomaly.id || a._id === origSelectedAnomaly._id)) + : 0; + setCurrentIndex(newIdx !== -1 ? newIdx : 0); + setItemAuditStates(filteredAnomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending')); + setLoading(false); + } else { + setLoading(false); + } + } else if (location.state && location.state.anomalies) { + setAnomalies(location.state.anomalies); + setCurrentIndex(location.state.startIndex || 0); + setCarouselIndex(0); + setSelectedCategory(null); + setNotes(''); + setIsAnomaly(null); + setPlantSubCategory(null); + setNotAnAnomalySubCategory(null); + setItemAuditStates(location.state.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending')); + if (location.state.anomalies.length > 0) { setLoading(false); } else { setLoading(false); } } else { - // Fetch 50 items for the session (default behavior from Dashboard) - const fetchSessionData = async () => { - try { - setLoading(true); - const res: any = await activityFeedsService.getHistory({ - date: '', - pageNo: 0, - pageSize: 50, - isTrueFalse: 'false', // Fetch unaudited - auto_audit: false, - isRectificationEnabled: false, - }); - if (res && res.anomalies) { - setAnomalies(res.anomalies); - } - } catch (err) { - console.error('Failed to fetch session data', err); - } finally { - setLoading(false); - } - }; - fetchSessionData(); + loadSessionData(); } - }, [location.state, tabs, selectedTabIndex]); + }, [location.state, tabs, selectedTabIndex, loadSessionData, filterPlaza]); - const IMAGES_AWS_PREFIX = "https://auditor-master-images.seekright.com/SeekRight/"; + const IMAGES_AWS_PREFIX = isRectActive + ? "https://sr-img.seekright.com/" + : "https://auditor-master-images.seekright.com/SeekRight/"; const IMAGES_TEST_AUDIT_PREFIX = "https://dashboard-images.seekright.com/SeekRight/"; const currentAnomaly = anomalies[currentIndex]; @@ -76,7 +225,6 @@ export const AuditSession: React.FC = () => { }, [currentAnomaly]); let masterImageSrc = ''; - let anomalyImageSrc = ''; let latStr = 'N/A'; let longStr = 'E/A'; @@ -89,14 +237,13 @@ export const AuditSession: React.FC = () => { masterImageSrc = '/assets/images/only_master_image_not_available.png'; } - // Parse Position if (currentAnomaly.Pos) { const match = String(currentAnomaly.Pos).match(/([NS][0-9.]+)([EW][0-9.]+)/i); if (match) { latStr = match[1]; longStr = match[2]; } else { - latStr = currentAnomaly.Pos; // Fallback + latStr = currentAnomaly.Pos; longStr = ''; } } @@ -124,81 +271,269 @@ export const AuditSession: React.FC = () => { const handleNext = () => { if (currentIndex < anomalies.length - 1) { setCurrentIndex(prev => prev + 1); - // Reset form setSelectedCategory(null); setNotes(''); setIsAnomaly(null); + setPlantSubCategory(null); + setNotAnAnomalySubCategory(null); } else { - // Done with session - const navState = location.state as any; - if (navState?.useExistingFeed) { - navigate('/activity-feeds'); - } else { - navigate('/dashboard'); - } + setShowCompletionModal(true); } }; const handlePrev = () => { if (currentIndex > 0) { setCurrentIndex(prev => prev - 1); - // We would ideally preserve form state for previously visited items + setSelectedCategory(null); + setNotes(''); + setIsAnomaly(null); + setPlantSubCategory(null); + setNotAnAnomalySubCategory(null); } }; - const handleSaveAndNext = async () => { - if (!currentAnomaly) return; - - // Placeholder for save logic - // await activityFeedsService.updateAuditedAnomaly(...) - console.log('Saved:', { id: currentAnomaly.anomaly_id, isAnomaly, selectedCategory, notes }); + const handleSkip = () => { + setItemAuditStates(prev => { + const next = [...prev]; + next[currentIndex] = 'skipped'; + return next; + }); handleNext(); - }; + }; const saveAuditState = useCallback(async ( + anomalyVal: boolean | null, + catVal: number | null, + notesVal: string, + plantSub?: string | null, + notAnAnomalySub?: string | null + ) => { + if (!currentAnomaly || !user) return; + try { + const isAnomalyBool = anomalyVal ?? true; + const auditStatusVal = isAnomalyBool ? 1 : 0; + const categories = isAnomalyBool ? ANOMALY_CATEGORIES : SAFE_CATEGORIES; + const matchedCategory = categories.find(c => c.id === catVal); + + let auditValueVal = matchedCategory ? matchedCategory.value : ''; + if (isAnomalyBool && catVal === 1 && plantSub) { + auditValueVal = plantSub; + } else if (!isAnomalyBool && catVal === 1 && notAnAnomalySub) { + auditValueVal = notAnAnomalySub; + } + // Construct properties matching Angular properties list: + // ["dbName", "isRectificationEnabled", "Audit_value", "Comments", "Audit_status", "Frame_Test", "Frame_Master", "Chainage", "Pos", "id", "road", "user_id", "Algorithm"] + const auditPayload = { + dbName: user.db_name || '', + isRectificationEnabled: isRectActive, + Audit_value: auditValueVal, + Comments: notesVal || '', + Audit_status: auditStatusVal, + Frame_Test: currentAnomaly.Frame_Test || '', + Frame_Master: currentAnomaly.Frame_Master || '', + Chainage: currentAnomaly.Chainage || '', + Pos: currentAnomaly.Pos || '', + id: currentAnomaly.id, + road: currentAnomaly.road || 'mcw', + user_id: user.user_id || 0, + Algorithm: currentAnomaly.Algorithm || '' + }; + + // Call API 1: Update anomaly + await activityFeedsService.updateAnomaly([auditPayload], isRectActive); + + // Call API 2: If true anomaly, update anomaly status in dashboard to notify client portal + if (auditStatusVal === 1) { + await activityFeedsService.updateAnomalyInDashboard({ + site_id: String(currentAnomaly.site_id), + anomaly_id: [currentAnomaly.id] + }); + } + } catch (err) { + console.error('Failed to submit audit state:', err); + } + }, [currentAnomaly, user]); + + const handleSaveAndNext = useCallback(async () => { + if (!currentAnomaly) return; + + if (selectedCategory === 1) { + if ((isAnomaly === null || isAnomaly === true) && !plantSubCategory) { + alert("Please select feature before submit."); + return; + } + if (isAnomaly === false && !notAnAnomalySubCategory) { + alert("Please select feature before submit."); + return; + } + } + + setItemAuditStates(prev => { + const next = [...prev]; + next[currentIndex] = 'audited'; + return next; + }); + + // Call the API + await saveAuditState(isAnomaly, selectedCategory, notes, plantSubCategory, notAnAnomalySubCategory); + + handleNext(); + }, [currentIndex, currentAnomaly, isAnomaly, selectedCategory, notes, plantSubCategory, notAnAnomalySubCategory, saveAuditState, handleNext]); + + // Quick Auto Advance save trigger + const handleQuickSaveAndNext = useCallback(async ( + anomalyVal: boolean, + catVal: number, + plantSub?: string | null, + notAnAnomalySub?: string | null + ) => { + if (!currentAnomaly) return; + setItemAuditStates(prev => { + const next = [...prev]; + next[currentIndex] = 'audited'; + return next; + }); + + // Call the API + await saveAuditState(anomalyVal, catVal, notes, plantSub, notAnAnomalySub); + + if (currentIndex < anomalies.length - 1) { + setCurrentIndex(prev => prev + 1); + setSelectedCategory(null); + setNotes(''); + setIsAnomaly(null); + setPlantSubCategory(null); + setNotAnAnomalySubCategory(null); + } else { + setShowCompletionModal(true); + } + }, [currentIndex, anomalies.length, currentAnomaly, notes, saveAuditState]); // Keyboard shortcuts const handleKeyDown = useCallback((e: KeyboardEvent) => { - // Prevent default on shortcuts if focused on inputs? - if (document.activeElement?.tagName === 'TEXTAREA' || document.activeElement?.tagName === 'INPUT') { - return; // Don't trigger shortcuts when typing notes - } + if (isNotesFocused) return; - switch (e.key.toLowerCase()) { + const key = e.key.toLowerCase(); + + // Register active flash pulse + setActiveKeyFlash(key); + setTimeout(() => setActiveKeyFlash(null), 150); + + switch (key) { case 'a': setIsAnomaly(true); + setSelectedCategory(null); + setPlantSubCategory(null); + setNotAnAnomalySubCategory(null); break; case 's': setIsAnomaly(false); + setSelectedCategory(null); + setPlantSubCategory(null); + setNotAnAnomalySubCategory(null); break; - case '1': setSelectedCategory(1); break; - case '2': setSelectedCategory(2); break; - case '3': setSelectedCategory(3); break; - case '4': setSelectedCategory(4); break; - case '5': setSelectedCategory(5); break; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '0': { + const keyMap: { [key: string]: number } = { + '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, + '6': 6, '7': 7, '8': 8, '9': 9, '0': 10 + }; + const catId = keyMap[key]; + + // Special nested case: Plant is already active (under Anomaly) + if (isAnomaly === true && selectedCategory === 1) { + if (key === '1') { + setPlantSubCategory('in grown'); + if (isQuickAudit) { + setTimeout(() => handleQuickSaveAndNext(true, 1, 'in grown', null), 120); + } + break; + } else if (key === '2') { + setPlantSubCategory('out grown'); + if (isQuickAudit) { + setTimeout(() => handleQuickSaveAndNext(true, 1, 'out grown', null), 120); + } + break; + } + } + + // Special nested case: Not an Anomaly is already active (under Safe) + if (isAnomaly === false && selectedCategory === 1) { + const subMap: { [key: string]: 'comparison error' | 'low light' | 'far asset' | 'no detection' | 'image mismatch' } = { + '1': 'comparison error', + '2': 'low light', + '3': 'far asset', + '4': 'no detection', + '5': 'image mismatch' + }; + const subVal = subMap[key]; + if (subVal) { + setNotAnAnomalySubCategory(subVal); + if (isQuickAudit) { + setTimeout(() => handleQuickSaveAndNext(false, 1, null, subVal), 120); + } + break; + } + } + + const currentIsAnomaly = isAnomaly === null ? true : isAnomaly; + if (isAnomaly === null) { + setIsAnomaly(true); + } + setSelectedCategory(catId); + + // If selecting Plant (Anomaly 1) or Not an Anomaly (Safe 1), do not auto-advance; wait for nested sub-options + if (catId === 1) { + break; + } + + if (isQuickAudit) { + setTimeout(() => handleQuickSaveAndNext(currentIsAnomaly, catId), 120); + } + break; + } case 'enter': - handleSaveAndNext(); - break; case 'arrowright': handleSaveAndNext(); break; case 'arrowleft': handlePrev(); break; + case 'tab': + e.preventDefault(); + handleSkip(); + break; } - }, [currentIndex, anomalies, isAnomaly, selectedCategory, notes]); + }, [currentIndex, anomalies, isAnomaly, selectedCategory, notes, plantSubCategory, isQuickAudit, isNotesFocused, handleQuickSaveAndNext, handleSaveAndNext, handleSkip]); useEffect(() => { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [handleKeyDown]); + const handleToggleQuickAudit = (val: boolean) => { + setIsQuickAudit(val); + localStorage.setItem('isQuickAudit', String(val)); + }; + if (loading) { - return Loading session...; + return ( + + Loading session... + + ); } if (anomalies.length === 0) { return ( - - No unaudited anomalies found to audit. + + No unaudited anomalies found to audit. + {/* Segmented Progress Visualizer */} + + {itemAuditStates.map((state, index) => { + let color = '#334155'; // pending + if (index === currentIndex) { + color = '#3b82f6'; // active current item (blue) + } else if (state === 'audited') { + color = '#22c55e'; // audited (green) + } else if (state === 'skipped') { + color = '#eab308'; // skipped (orange) + } + return ( + + ); + })} + + {/* Main Content Area */} @@ -266,7 +626,15 @@ export const AuditSession: React.FC = () => { {masterImageSrc ? ( - Master + Master { + e.currentTarget.onerror = null; + e.currentTarget.src = '/assets/images/master_image_not_available.png'; + }} + /> ) : ( No Master Image )} @@ -285,7 +653,15 @@ export const AuditSession: React.FC = () => { {mediaItems[carouselIndex]?.type === 'video' ? ( {/* Right Side: Sidebar */} - + + {/* Quick-Audit Auto-Advance Toggle */} + + + Quick Audit + Auto-advances on keypress + + handleToggleQuickAudit(e.target.checked)} + sx={{ + '& .MuiSwitch-switchBase.Mui-checked': { color: '#3b82f6' }, + '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: '#3b82f6' } + }} + /> + + {/* Anomaly/Safe Toggle */} @@ -336,7 +729,15 @@ export const AuditSession: React.FC = () => { variant={isAnomaly === true ? "contained" : "outlined"} color={isAnomaly === true ? "error" : "inherit"} onClick={() => setIsAnomaly(true)} - sx={{ borderRadius: 2, textTransform: 'none', borderColor: '#334155', color: isAnomaly === true ? 'white' : '#cbd5e1' }} + sx={{ + borderRadius: 2, + textTransform: 'none', + borderColor: '#334155', + color: isAnomaly === true ? 'white' : '#cbd5e1', + transition: 'all 0.15s ease', + border: activeKeyFlash === 'a' ? '2px solid #ef4444' : '1px solid #334155', + boxShadow: activeKeyFlash === 'a' ? '0 0 12px #ef4444' : 'none' + }} > ⚠️ Anomaly @@ -344,45 +745,141 @@ export const AuditSession: React.FC = () => { fullWidth variant={isAnomaly === false ? "contained" : "outlined"} color={isAnomaly === false ? "success" : "inherit"} - onClick={() => setIsAnomaly(false)} - sx={{ borderRadius: 2, textTransform: 'none', borderColor: '#334155', color: isAnomaly === false ? 'white' : '#cbd5e1' }} + onClick={() => { + setIsAnomaly(false); + setSelectedCategory(null); + setPlantSubCategory(null); + }} + sx={{ + borderRadius: 2, + textTransform: 'none', + borderColor: '#334155', + color: isAnomaly === false ? 'white' : '#cbd5e1', + transition: 'all 0.15s ease', + border: activeKeyFlash === 's' ? '2px solid #22c55e' : '1px solid #334155', + boxShadow: activeKeyFlash === 's' ? '0 0 12px #22c55e' : 'none' + }} > ✓ Safe - {/* Category */} - + CATEGORY - - {[ - { id: 1, label: 'No Issue', color: '#22c55e' }, - { id: 2, label: 'Minor Crack', color: '#eab308' }, - { id: 3, label: 'Major Crack', color: '#f97316' }, - { id: 4, label: 'Structural Damage', color: '#ef4444' }, - { id: 5, label: 'Needs Investigation', color: '#8b5cf6' }, - ].map(cat => ( - - ))} - + + {(isAnomaly === false ? SAFE_CATEGORIES : ANOMALY_CATEGORIES).map(cat => ( + + + + {/* Plant Nested Subcategories (Anomaly Mode) */} + {cat.id === 1 && isAnomaly === true && selectedCategory === 1 && ( + + {[ + { val: 'in grown', label: 'In Grown', key: '1' }, + { val: 'out grown', label: 'Out Grown', key: '2' } + ].map(sub => ( + + ))} + + )} + + {/* Not an Anomaly Nested Subcategories (Safe Mode) */} + {cat.id === 1 && isAnomaly === false && selectedCategory === 1 && ( + + {[ + { val: 'comparison error', label: 'Comparison Error', key: '1' }, + { val: 'low light', label: 'Low Light', key: '2' }, + { val: 'far asset', label: 'Far Asset', key: '3' }, + { val: 'no detection', label: 'No Detection', key: '4' }, + { val: 'image mismatch', label: 'Image Mismatch', key: '5' } + ].map(sub => ( + + ))} + + )} + + ))} {/* Notes */} @@ -392,6 +889,8 @@ export const AuditSession: React.FC = () => { component="textarea" value={notes} onChange={(e) => setNotes(e.target.value)} + onFocus={() => setIsNotesFocused(true)} + onBlur={() => setIsNotesFocused(false)} placeholder="Optional observations..." sx={{ width: '100%', @@ -415,7 +914,17 @@ export const AuditSession: React.FC = () => { variant="contained" color="primary" onClick={handleSaveAndNext} - sx={{ borderRadius: 2, py: 1.5, textTransform: 'none', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', px: 3 }} + sx={{ + borderRadius: 2, + py: 1.5, + textTransform: 'none', + fontWeight: 'bold', + display: 'flex', + justifyContent: 'space-between', + px: 3, + transition: 'all 0.15s ease', + border: (activeKeyFlash === 'enter' || activeKeyFlash === 'arrowright') ? '2px solid #3b82f6' : 'none' + }} > Save & Next Enter / → @@ -424,14 +933,32 @@ export const AuditSession: React.FC = () => { variant="outlined" onClick={handlePrev} disabled={currentIndex === 0} - sx={{ flex: 1, borderRadius: 2, textTransform: 'none', borderColor: '#1e293b', color: '#94a3b8', '&:hover': { borderColor: '#334155' } }} + sx={{ + flex: 1, + borderRadius: 2, + textTransform: 'none', + borderColor: '#1e293b', + color: '#94a3b8', + transition: 'all 0.15s ease', + border: activeKeyFlash === 'arrowleft' ? '2px solid #64748b' : '1px solid #1e293b', + '&:hover': { borderColor: '#334155' } + }} > ← Prev @@ -442,36 +969,116 @@ export const AuditSession: React.FC = () => { {/* Footer Shortcuts */} div': { display: 'flex', alignItems: 'center', gap: 1, whiteSpace: 'nowrap' } }}> - Keys: - - - Anomaly - - - - Safe - - - - Category - - - - Save & Next - - - - Navigate - - - - Skip - - - - Help - + + {isNotesFocused ? "Shortcuts Paused (Typing notes)" : "Keys:"} + + {!isNotesFocused && ( + <> + + + Anomaly + + + + Safe + + + + Category + + + + Save & Next + + + + Navigate + + + + Skip + + + + Help + + + )} + + {/* Session Completion In-place Modal */} + setShowCompletionModal(false)} + slotProps={{ + paper: { + sx: { + bgcolor: '#0f172a', + color: '#f8fafc', + border: '1px solid #1e293b', + borderRadius: 3, + p: 2, + maxWidth: 400 + } + } + }} + > + + Session Completed! 🎉 + + + + {(() => { + const navState = location.state as any; + return navState?.useExistingFeed + ? `You have successfully completed this feed session.` + : `You have successfully audited this session of 50 items.`; + })()} + + + {(() => { + const navState = location.state as any; + const feedPageSize = activeTab?.rowsPerPage || 20; + return navState?.useExistingFeed + ? `Ready to continue? You can audit the next page of ${feedPageSize} anomalies from the active feed in-place, or return to the active feeds page.` + : `Ready to continue? You can start the next session immediately in-place, or return to the dashboard.`; + })()} + + + + {(() => { + const navState = location.state as any; + const isFeedSession = !!navState?.useExistingFeed; + const feedPageSize = activeTab?.rowsPerPage || 20; + return ( + <> + + + + ); + })()} + + ); }; diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index db07330..5d24282 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Box, Typography, Button, Paper, LinearProgress, Container } from '@mui/material'; +import { Box, Typography, Button, Paper, LinearProgress } from '@mui/material'; import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import { activityFeedsService } from '../../api/activityFeedsService'; import { TopFilterBar } from '../../components/common/TopFilterBar'; @@ -10,7 +10,9 @@ export const Dashboard: React.FC = () => { const { selectedSite, auditStatus, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, - selectedAssetIds, dashboardCache, setDashboardCache + selectedAssetIds, dashboardCache, setDashboardCache, + searchAnomalyId, filterPlaza, isRectificationEnabled, + tabs, selectedTabIndex } = useFeedStore(); const navigate = useNavigate(); @@ -18,19 +20,29 @@ export const Dashboard: React.FC = () => { const selectedSiteId = selectedSite?.site_id ?? ''; const assetIdsKey = selectedAssetIds.join(','); + const activeTab = tabs[selectedTabIndex] || { date: '', isRectificationTab: false }; + const activeDate = activeTab.date || ''; + const isRectActive = isRectificationEnabled || activeTab.isRectificationTab; + // Build a key that uniquely identifies the current filter state - const currentFilterKey = `${selectedSiteId}|${auditStatus}|${filterFromDate}|${filterTillDate}|${assetIdsKey}|${filterMinChainage}|${filterMaxChainage}`; + const currentFilterKey = `${selectedSiteId}|${auditStatus}|${filterFromDate}|${filterTillDate}|${assetIdsKey}|${filterMinChainage}|${filterMaxChainage}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeDate}`; // Initialise stats from cache immediately (no flicker on tab switch) const [stats, setStats] = useState(() => - dashboardCache.stats ?? { total: 0, audited: 0, pending: 0, anomaliesFound: 0 } + dashboardCache.stats ?? { + globalTotal: 0, + batchTotal: 0, + batchAudited: 0, + batchPending: 0, + batchAnomaliesFound: 0, + } ); // Only show loading spinner if there's no cached result for these filters const [loading, setLoading] = useState(dashboardCache.stats === null); useEffect(() => { // CACHE HIT: filters unchanged since last fetch — skip API call entirely - if (dashboardCache.stats !== null && currentFilterKey === `${dashboardCache.siteId}|${dashboardCache.auditStatus}|${dashboardCache.fromDate}|${dashboardCache.tillDate}|${dashboardCache.assetIds}|${dashboardCache.minChainage}|${dashboardCache.maxChainage}`) { + if (dashboardCache.stats !== null && currentFilterKey === `${dashboardCache.siteId}|${dashboardCache.auditStatus}|${dashboardCache.fromDate}|${dashboardCache.tillDate}|${dashboardCache.assetIds}|${dashboardCache.minChainage}|${dashboardCache.maxChainage}|${dashboardCache.searchAnomalyId}|${dashboardCache.filterPlaza}|${dashboardCache.isRectificationEnabled ?? false}|${dashboardCache.date ?? ''}`) { setStats(dashboardCache.stats); setLoading(false); return; @@ -40,28 +52,32 @@ export const Dashboard: React.FC = () => { try { setLoading(true); const res: any = await activityFeedsService.getHistory({ - date: '', + date: activeDate, pageNo: 0, pageSize: 200, sortByDateAsc: false, - isRectificationEnabled: false, - isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both', - auto_audit: false, + isRectificationEnabled: isRectActive, + auditStatus: auditStatus, + isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), + auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), siteIds: (!selectedSiteId || selectedSiteId === 'All Sites') ? '' : selectedSiteId, assetsIds: assetIdsKey, fromDate: filterFromDate, tillDate: filterTillDate, fromChainage: filterMinChainage, toChainage: filterMaxChainage, + anomalyIds: searchAnomalyId, + plaza: filterPlaza, }); if (res) { const anomalies = res.anomalies || []; const newStats = { - total: res.count || anomalies.length, - audited: anomalies.filter((a: any) => a.IsAudited === 1).length, - pending: anomalies.filter((a: any) => a.IsAudited === 0).length, - anomaliesFound: anomalies.filter((a: any) => a.Audit_status === 1).length, + globalTotal: res.count || anomalies.length, + batchTotal: anomalies.length, + batchAudited: anomalies.filter((a: any) => a.IsAudited === 1).length, + batchPending: anomalies.filter((a: any) => a.IsAudited === 0).length, + batchAnomaliesFound: anomalies.filter((a: any) => a.Audit_status === 1).length, }; setStats(newStats); // Persist to store cache so re-mounting the Dashboard skips this fetch @@ -73,6 +89,10 @@ export const Dashboard: React.FC = () => { assetIds: assetIdsKey, minChainage: filterMinChainage, maxChainage: filterMaxChainage, + searchAnomalyId, + filterPlaza, + isRectificationEnabled: isRectActive, + date: activeDate, stats: newStats, }); } @@ -86,7 +106,7 @@ export const Dashboard: React.FC = () => { fetchStats(); }, [currentFilterKey]); // eslint-disable-line react-hooks/exhaustive-deps - const progressPercent = stats.total > 0 ? Math.round((stats.audited / stats.total) * 100) : 0; + const progressPercent = stats.batchTotal > 0 ? Math.round((stats.batchAudited / stats.batchTotal) * 100) : 0; return ( @@ -96,13 +116,16 @@ export const Dashboard: React.FC = () => { {/* Stats Row */} - {/* Total Items */} + {/* Active Batch */} - Total Items + Active Batch - {loading ? '...' : stats.total.toLocaleString()} + {loading ? '...' : stats.batchTotal.toLocaleString()} + + + of {loading ? '...' : stats.globalTotal.toLocaleString()} total items on site @@ -112,7 +135,10 @@ export const Dashboard: React.FC = () => { }}> Audited - {loading ? '...' : stats.audited.toLocaleString()} + {loading ? '...' : stats.batchAudited.toLocaleString()} + + + in current batch @@ -122,7 +148,10 @@ export const Dashboard: React.FC = () => { }}> Pending - {loading ? '...' : stats.pending.toLocaleString()} + {loading ? '...' : stats.batchPending.toLocaleString()} + + + in current batch @@ -132,16 +161,19 @@ export const Dashboard: React.FC = () => { }}> Anomalies Found - {loading ? '...' : stats.anomaliesFound.toLocaleString()} + {loading ? '...' : stats.batchAnomaliesFound.toLocaleString()} + + + in current batch - {/* Overall Progress */} + {/* Active Batch Progress */} - Overall Progress - {stats.audited} / {stats.total} completed + Active Batch Progress + {stats.batchAudited} / {stats.batchTotal} completed { /> {progressPercent}% complete - ~{Math.ceil(stats.pending / 50)} sessions remaining at 50/session + ~{Math.ceil(stats.batchPending / 50)} sessions remaining at 50/session - {/* Next Session Info */} - - - Next session: 50 items will be queued ({stats.pending} pending total) + {/* Session Mechanics Explanation Box */} + + + Audit Workflow & Session Mechanics + + + + 1. GLOBAL SITE POOL + + + There are {loading ? '...' : stats.globalTotal.toLocaleString()} total items matching your active filter criteria on this site. + + + + + 2. ACTIVE WORK BATCH + + + An active queue of up to {loading ? '...' : stats.batchTotal} items is loaded into your immediate workspace for auditing. + + + + + 3. BITE-SIZED SESSIONS (50 items) + + + To keep image downloads fast and prevent browser lag, you audit in sessions of 50 items. + You have ~{loading ? '...' : Math.ceil(stats.batchPending / 50)} sessions remaining. + + + {/* Warning Box */} - 181 high-priority items will appear first in the queue + Priority items will appear first in the session queue @@ -178,18 +244,24 @@ export const Dashboard: React.FC = () => { {/* Footer links */} diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 8afcd31..b0c235a 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -10,6 +10,7 @@ export interface User { user_roles: string; db_name?: string; org_id?: string; + user_id?: string | number; } interface AuthState { diff --git a/src/store/feedStore.ts b/src/store/feedStore.ts index d441ac7..01909f5 100644 --- a/src/store/feedStore.ts +++ b/src/store/feedStore.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; export interface FeedTab { date: string; @@ -21,11 +22,16 @@ interface DashboardCache { assetIds: string; minChainage: string; maxChainage: string; + searchAnomalyId: string; + filterPlaza: string; + isRectificationEnabled?: boolean; + date?: string; stats: { - total: number; - audited: number; - pending: number; - anomaliesFound: number; + globalTotal: number; + batchTotal: number; + batchAudited: number; + batchPending: number; + batchAnomaliesFound: number; } | null; } @@ -38,6 +44,7 @@ interface FeedState { filterTillDate: string; filterMinChainage: string; filterMaxChainage: string; + filterPlaza: string; selectedAssetIds: number[]; sites: any[]; auditStatus: string; @@ -48,6 +55,8 @@ interface FeedState { lastFetchedFilterKey: string; // Dashboard stats cache dashboardCache: DashboardCache; + searchAnomalyId: string; + isRectificationEnabled: boolean; // Actions setTabs: (tabs: FeedTab[]) => void; @@ -69,6 +78,9 @@ interface FeedState { setFilterTillDate: (date: string) => void; setFilterMinChainage: (val: string) => void; setFilterMaxChainage: (val: string) => void; + setFilterPlaza: (val: string) => void; + setSearchAnomalyId: (id: string) => void; + setIsRectificationEnabled: (val: boolean) => void; resetStore: () => void; } @@ -80,6 +92,10 @@ const EMPTY_DASHBOARD_CACHE: DashboardCache = { assetIds: '__unset__', minChainage: '__unset__', maxChainage: '__unset__', + searchAnomalyId: '__unset__', + filterPlaza: '__unset__', + isRectificationEnabled: false, + date: '', stats: null, }; @@ -92,47 +108,74 @@ const initialFeedState = { filterTillDate: '', filterMinChainage: '', filterMaxChainage: '', + filterPlaza: '', selectedAssetIds: [], selectedSite: null, sites: [], - auditStatus: 'Auto Audits', + auditStatus: 'Manual', isFiltersModalOpen: false, isInitialized: false, lastFetchedFilterKey: '', dashboardCache: EMPTY_DASHBOARD_CACHE, + searchAnomalyId: '', + isRectificationEnabled: false, }; -export const useFeedStore = create((set) => ({ - ...initialFeedState, +export const useFeedStore = create()( + persist( + (set) => ({ + ...initialFeedState, - resetStore: () => set(initialFeedState), + resetStore: () => set(initialFeedState), - setTabs: (tabs) => set({ tabs }), - setSelectedTabIndex: (index) => set({ selectedTabIndex: index }), - setAssets: (assets) => set({ assets }), - setAssetTypes: (assetTypes) => set({ assetTypes }), - setSelectedAssetIds: (selectedAssetIds) => set({ selectedAssetIds }), - setSites: (sites) => set({ sites }), - setSelectedSite: (selectedSite) => set({ selectedSite }), - setAuditStatus: (auditStatus) => set({ auditStatus }), - setIsFiltersModalOpen: (isFiltersModalOpen) => set({ isFiltersModalOpen }), - setIsInitialized: (isInitialized) => set({ isInitialized }), - setLastFetchedFilterKey: (lastFetchedFilterKey) => set({ lastFetchedFilterKey }), - setDashboardCache: (dashboardCache) => set({ dashboardCache }), - setFilterFromDate: (filterFromDate) => set({ filterFromDate }), - setFilterTillDate: (filterTillDate) => set({ filterTillDate }), - setFilterMinChainage: (filterMinChainage) => set({ filterMinChainage }), - setFilterMaxChainage: (filterMaxChainage) => set({ filterMaxChainage }), - addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })), - closeTab: (index) => set((state) => { - const newTabs = [...state.tabs]; - newTabs.splice(index, 1); - const newIndex = state.selectedTabIndex >= newTabs.length ? newTabs.length - 1 : state.selectedTabIndex; - return { tabs: newTabs, selectedTabIndex: newIndex }; - }), - updateTab: (index, partialTab) => set((state) => { - const newTabs = [...state.tabs]; - newTabs[index] = { ...newTabs[index], ...partialTab }; - return { tabs: newTabs }; - }) -})); + setTabs: (tabs) => set({ tabs }), + setSelectedTabIndex: (index) => set({ selectedTabIndex: index }), + setAssets: (assets) => set({ assets }), + setAssetTypes: (assetTypes) => set({ assetTypes }), + setSelectedAssetIds: (selectedAssetIds) => set({ selectedAssetIds }), + setSites: (sites) => set({ sites }), + setSelectedSite: (selectedSite) => set({ selectedSite }), + setAuditStatus: (auditStatus) => set({ auditStatus }), + setIsFiltersModalOpen: (isFiltersModalOpen) => set({ isFiltersModalOpen }), + setIsInitialized: (isInitialized) => set({ isInitialized }), + setLastFetchedFilterKey: (lastFetchedFilterKey) => set({ lastFetchedFilterKey }), + setDashboardCache: (dashboardCache) => set({ dashboardCache }), + setFilterFromDate: (filterFromDate) => set({ filterFromDate }), + setFilterTillDate: (filterTillDate) => set({ filterTillDate }), + setFilterMinChainage: (filterMinChainage) => set({ filterMinChainage }), + setFilterMaxChainage: (filterMaxChainage) => set({ filterMaxChainage }), + setFilterPlaza: (filterPlaza) => set({ filterPlaza }), + setSearchAnomalyId: (searchAnomalyId) => set({ searchAnomalyId }), + setIsRectificationEnabled: (isRectificationEnabled) => set({ isRectificationEnabled }), + addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })), + closeTab: (index) => set((state) => { + const newTabs = [...state.tabs]; + newTabs.splice(index, 1); + const newIndex = state.selectedTabIndex >= newTabs.length ? newTabs.length - 1 : state.selectedTabIndex; + return { tabs: newTabs, selectedTabIndex: newIndex }; + }), + 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, + }), + } + ) +); diff --git a/vite.config.ts b/vite.config.ts index 87cb23c..ff4e70e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,11 +10,19 @@ export default defineConfig({ target: 'https://sr-backend-api.takeleap.in', changeOrigin: true, rewrite: (path) => path.replace(/^\/api\/dashboard/, ''), + headers: { + Origin: 'https://auditor-qa.seekright.com', + Referer: 'https://auditor-qa.seekright.com/' + } }, '/api/audit': { target: 'https://sr-audit.takeleap.in', changeOrigin: true, rewrite: (path) => path.replace(/^\/api\/audit/, ''), + headers: { + Origin: 'https://auditor-qa.seekright.com', + Referer: 'https://auditor-qa.seekright.com/' + } }, }, },