diff --git a/index.html b/index.html index 085e1ff..070a639 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - react-revamp + SeekRight
diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..0830af7 Binary files /dev/null and b/public/favicon.png differ diff --git a/src/api/activityFeedsService.ts b/src/api/activityFeedsService.ts index d9e3abf..0ce2555 100644 --- a/src/api/activityFeedsService.ts +++ b/src/api/activityFeedsService.ts @@ -77,5 +77,10 @@ export const activityFeedsService = { getFalseAnomaly: async (params: any) => { const response = await axiosClient.get('/api/audit/asset/get-false-Anomaly', { params }); return response; + }, + + bulkDelete: async (payload: any) => { + const response = await axiosClient.post('/api/audit/bulk/delete', payload); + return response; } }; diff --git a/src/components/common/BulkDeleteDialog.tsx b/src/components/common/BulkDeleteDialog.tsx new file mode 100644 index 0000000..59b466d --- /dev/null +++ b/src/components/common/BulkDeleteDialog.tsx @@ -0,0 +1,335 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, DialogContent, Typography, Box, IconButton, Button, + TextField, RadioGroup, FormControlLabel, Radio, Checkbox, + CircularProgress, Select, MenuItem +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { useFeedStore } from '../../store/feedStore'; +import { useAuthStore } from '../../store/authStore'; +import { activityFeedsService } from '../../api/activityFeedsService'; + +interface BulkDeleteDialogProps { + open: boolean; + onClose: () => void; +} + +const REASONS = ['Low Light', 'Bad Image', 'Wrong Algorithm', 'Out of Range']; + +export const BulkDeleteDialog: React.FC = ({ open, onClose }) => { + const { assetTypes, selectedSite, sites } = useFeedStore(); + const { user } = useAuthStore(); + + const [fromDate, setFromDate] = useState(''); + const [tillDate, setTillDate] = useState(''); + const [auditType, setAuditType] = useState('manual'); + const [reason, setReason] = useState('Bad Image'); + const [selectedAssets, setSelectedAssets] = useState>(new Set()); + const [isLoading, setIsLoading] = useState(false); + const [localSite, setLocalSite] = useState(selectedSite?.site_id || 'All Sites'); + + // Reset state when dialog opens + useEffect(() => { + if (open) { + setFromDate(''); + setTillDate(''); + setAuditType('manual'); + setReason('Bad Image'); + setSelectedAssets(new Set()); + setLocalSite(selectedSite?.site_id || 'All Sites'); + } + }, [open, selectedSite?.site_id]); + + // Extract all asset IDs for "Select All" functionality + const allAssetIds = React.useMemo(() => { + const ids: number[] = []; + assetTypes.forEach((type: any) => { + type.assets?.forEach((a: any) => ids.push(a.asset_id)); + }); + return ids; + }, [assetTypes]); + + const isAllSelected = selectedAssets.size > 0 && selectedAssets.size === allAssetIds.length; + + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedAssets(new Set(allAssetIds)); + } else { + setSelectedAssets(new Set()); + } + }; + + const handleSelectCategory = (type: any, checked: boolean) => { + const newSelected = new Set(selectedAssets); + type.assets?.forEach((a: any) => { + if (checked) { + newSelected.add(a.asset_id); + } else { + newSelected.delete(a.asset_id); + } + }); + setSelectedAssets(newSelected); + }; + + const handleSelectAsset = (assetId: number, checked: boolean) => { + const newSelected = new Set(selectedAssets); + if (checked) { + newSelected.add(assetId); + } else { + newSelected.delete(assetId); + } + setSelectedAssets(newSelected); + }; + + const handleApply = async () => { + if (!fromDate || !tillDate) { + alert("Please select both From and Till dates."); + return; + } + if (selectedAssets.size === 0) { + alert("Please select at least one asset."); + return; + } + + let siteIds: number[] = []; + if (localSite === 'All Sites') { + sites.forEach((s: any) => { + if (s.site_id) { + siteIds.push(...String(s.site_id).split(',').map(id => Number(id.trim()))); + } + }); + } else { + siteIds = String(localSite).split(',').map(id => Number(id.trim())); + } + + const payload = { + dbName: user?.db_name, + asset_id: Array.from(selectedAssets), + site_id: siteIds, + fromDate: fromDate, + toDate: tillDate, + user_id: Number(user?.user_id), + audit_value: reason, + isBulkDelete: true, + audit_type: auditType + }; + + try { + setIsLoading(true); + await activityFeedsService.bulkDelete(payload); + alert("Bulk delete successful!"); + onClose(); + } catch (err: any) { + console.error("Bulk delete failed", err); + alert("Bulk delete failed. " + (err?.response?.data?.message || err.message || "")); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + + + + + + {/* SITE SELECTION */} + SITE + + + {/* DATE RANGE */} + DATE RANGE + + + From + setFromDate(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + bgcolor: 'rgba(255,255,255,0.05)', + borderRadius: 1, + color: '#f8fafc', + '& fieldset': { borderColor: '#334155' }, + '&:hover fieldset': { borderColor: '#475569' }, + '&.Mui-focused fieldset': { borderColor: '#3b82f6' } + }, + '& input[type="date"]::-webkit-calendar-picker-indicator': { filter: 'invert(1)', cursor: 'pointer' } + }} + /> + + + Till + setTillDate(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + bgcolor: 'rgba(255,255,255,0.05)', + borderRadius: 1, + color: '#f8fafc', + '& fieldset': { borderColor: '#334155' }, + '&:hover fieldset': { borderColor: '#475569' }, + '&.Mui-focused fieldset': { borderColor: '#3b82f6' } + }, + '& input[type="date"]::-webkit-calendar-picker-indicator': { filter: 'invert(1)', cursor: 'pointer' } + }} + /> + + + + {/* AUDIT TYPE */} + AUDIT TYPE + setAuditType(e.target.value)} sx={{ mb: 2 }}> + } label={manual} /> + } label={auto audit} /> + + + {/* ASSETS */} + ASSETS + + 0} + onChange={(e) => handleSelectAll(e.target.checked)} + sx={{ color: '#64748b', '&.Mui-checked': { color: '#3b82f6' } }} + /> + } + label={SELECT ALL} + sx={{ width: '100%', m: 0 }} + /> + + {assetTypes.map((type: any) => { + if (!type.assets || type.assets.length === 0) return null; + const categoryAssetIds = type.assets.map((a: any) => a.asset_id); + const isCategorySelected = categoryAssetIds.every((id: number) => selectedAssets.has(id)); + const isCategoryIndeterminate = !isCategorySelected && categoryAssetIds.some((id: number) => selectedAssets.has(id)); + + return ( + + handleSelectCategory(type, e.target.checked)} + sx={{ color: '#64748b', '&.Mui-checked, &.MuiCheckbox-indeterminate': { color: '#3b82f6' } }} + /> + } + label={{type.type_name.replace(/_/g, ' ')}} + sx={{ width: '100%', m: 0 }} + /> + + {type.assets.map((asset: any) => ( + handleSelectAsset(asset.asset_id, e.target.checked)} + sx={{ color: '#475569', '&.Mui-checked': { color: '#3b82f6' }, py: 0.5 }} + /> + } + label={{asset.asset_name.replace(/_/g, ' ')}} + sx={{ m: 0 }} + /> + ))} + + + ); + })} + + + {/* REASON */} + REASON + setReason(e.target.value)} sx={{ mb: 2 }}> + {REASONS.map(r => ( + } + label={{r}} + /> + ))} + + + + + + + + ); +}; diff --git a/src/components/common/TopFilterBar.tsx b/src/components/common/TopFilterBar.tsx index 5716ae8..c210d3b 100644 --- a/src/components/common/TopFilterBar.tsx +++ b/src/components/common/TopFilterBar.tsx @@ -5,6 +5,7 @@ import HistoryIcon from '@mui/icons-material/History'; import { useFeedStore } from '../../store/feedStore'; import { FeedFilters } from '../../pages/activity-feeds/FeedFilters'; import { activityFeedsService } from '../../api/activityFeedsService'; +import { BulkDeleteDialog } from './BulkDeleteDialog'; const DEBOUNCE_MS = 600; // wait 600ms after user stops changing before committing to store @@ -112,6 +113,7 @@ export const TopFilterBar: React.FC = () => { }; const [historyDates, setHistoryDates] = useState([]); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); const [loadingHistory, setLoadingHistory] = useState(false); const [isHistoryDialogOpen, setIsHistoryDialogOpen] = useState(false); const [selectedHistoryDate, setSelectedHistoryDate] = useState('2026-06-12'); @@ -199,6 +201,13 @@ export const TopFilterBar: React.FC = () => { > RECTIFICATION HISTORY + setIsHistoryDialogOpen(false)} @@ -486,6 +495,7 @@ export const TopFilterBar: React.FC = () => { )} + setIsBulkDeleteDialogOpen(false)} /> ); }; diff --git a/src/main.tsx b/src/main.tsx index 4aff025..9b5cb7d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,6 @@ -import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - - - , + , ) diff --git a/src/pages/account/DbSelectionDialog.tsx b/src/pages/account/DbSelectionDialog.tsx index fc72956..fb6f9b5 100644 --- a/src/pages/account/DbSelectionDialog.tsx +++ b/src/pages/account/DbSelectionDialog.tsx @@ -44,8 +44,8 @@ export const DbSelectionDialog: React.FC = ({ open, onCl ) : ( - {organizations.map((org) => ( - + {organizations.map((org, index) => ( + onClose(org)}> diff --git a/src/pages/activity-feeds/FeedFilters.tsx b/src/pages/activity-feeds/FeedFilters.tsx index fbd94dd..0df1bbd 100644 --- a/src/pages/activity-feeds/FeedFilters.tsx +++ b/src/pages/activity-feeds/FeedFilters.tsx @@ -219,8 +219,9 @@ export const FeedFilters: React.FC = () => { onClose={() => setIsFiltersModalOpen(false)} maxWidth="lg" fullWidth - // Zero transition — opens instantly + keepMounted // pre-renders in background — no mount delay on open transitionDuration={0} + disableScrollLock // prevents layout shift stutter slotProps={{ paper: { sx: { diff --git a/src/pages/activity-feeds/GlobalFeed.tsx b/src/pages/activity-feeds/GlobalFeed.tsx index 117c645..eb7e78f 100644 --- a/src/pages/activity-feeds/GlobalFeed.tsx +++ b/src/pages/activity-feeds/GlobalFeed.tsx @@ -25,7 +25,7 @@ export const GlobalFeed: React.FC = () => { } const initTabs = async () => { - // Add default Unaudited tab if empty and fetch data + // Add default Unaudited tab if empty and trigger second effect if (useFeedStore.getState().tabs.length === 0) { addTab({ date: '', // Unaudited @@ -38,39 +38,6 @@ export const GlobalFeed: React.FC = () => { page: 0, rowsPerPage: 20 }); - - const currentSite = useFeedStore.getState().selectedSite; - const siteIdParam = (!currentSite || currentSite?.site_id === 'All Sites') ? '' : String(currentSite.site_id || ''); - const assetIdsParam = useFeedStore.getState().selectedAssetIds.join(','); - - try { - const historyRes: any = await activityFeedsService.getHistory({ - date: '', - pageNo: 0, - pageSize: 20, - siteIds: siteIdParam, - auditStatus: auditStatus, - isTrueFalse: isRectificationEnabled ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), - auto_audit: isRectificationEnabled ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), - isRectificationEnabled: isRectificationEnabled, - assetsIds: assetIdsParam, - fromDate: useFeedStore.getState().filterFromDate, - tillDate: useFeedStore.getState().filterTillDate, - fromChainage: useFeedStore.getState().filterMinChainage, - toChainage: useFeedStore.getState().filterMaxChainage, - anomalyIds: searchAnomalyId, - plaza: filterPlaza - }); - if (historyRes && historyRes.anomalies) { - updateTab(0, { - anomalies: historyRes.anomalies, - anomaliesCopy: historyRes.anomalies, - totalAnomalyCount: historyRes.count || historyRes.anomalies.length - }); - } - } catch (historyErr) { - console.error("[GlobalFeed] Failed to load history", historyErr); - } } }; diff --git a/src/pages/audit-session/AuditSession.tsx b/src/pages/audit-session/AuditSession.tsx index 33a0b66..ad013c6 100644 --- a/src/pages/audit-session/AuditSession.tsx +++ b/src/pages/audit-session/AuditSession.tsx @@ -1,7 +1,9 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions, IconButton } from '@mui/material'; import { useNavigate, useLocation } from 'react-router-dom'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import ZoomInIcon from '@mui/icons-material/ZoomIn'; +import ZoomOutIcon from '@mui/icons-material/ZoomOut'; import { activityFeedsService } from '../../api/activityFeedsService'; import { useAuthStore } from '../../store/authStore'; import { useFeedStore } from '../../store/feedStore'; @@ -62,24 +64,41 @@ export const AuditSession: React.FC = () => { tabs, selectedTabIndex, updateTab, selectedSite, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, selectedAssetIds, - searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza + searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza, + setDashboardCache } = useFeedStore(); + // Call this before every navigate('/dashboard') so the dashboard re-fetches fresh stats + const invalidateDashboardCache = () => { + setDashboardCache({ + siteId: '__unset__', auditStatus: '__unset__', fromDate: '__unset__', + tillDate: '__unset__', assetIds: '__unset__', minChainage: '__unset__', + maxChainage: '__unset__', searchAnomalyId: '__unset__', filterPlaza: '__unset__', + isRectificationEnabled: false, date: '', stats: null, + }); + }; + const [sessionPageNo, setSessionPageNo] = useState(() => location.state?.startPage || 0); + const hasInitializedSession = useRef(false); + + const [masterZoom, setMasterZoom] = useState(1); + const [anomalyZoom, setAnomalyZoom] = useState(1); + const activeTab = tabs[selectedTabIndex] || { date: '', isRectificationTab: false, page: 0, rowsPerPage: 20 }; const activeDate = activeTab.date || ''; const isRectActive = isRectificationEnabled || activeTab.isRectificationTab; - const loadSessionData = useCallback(async () => { + const loadSessionData = useCallback(async (customPageNo?: number) => { try { setLoading(true); const selectedSiteId = selectedSite?.site_id ?? ''; const assetIdsKey = selectedAssetIds.join(','); - const isReviewMode = location.state?.isReviewMode; + const targetPage = typeof customPageNo === 'number' ? customPageNo : sessionPageNo; const res: any = await activityFeedsService.getHistory({ date: activeDate, - pageNo: 0, + pageNo: targetPage * 50, pageSize: 50, + sortByDateAsc: false, // must match dashboard sort so firstPendingIndex page alignment is correct isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'), auditStatus: auditStatus, auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')), @@ -104,17 +123,20 @@ export const AuditSession: React.FC = () => { setNotAnAnomalySubCategory(null); setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending')); } else { - setLoading(false); + alert("No more anomalies left in the feed."); + setShowCompletionModal(false); + navigate('/activity-feeds'); } } catch (err) { console.error('Failed to load session anomalies:', err); } finally { setLoading(false); } - }, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state]); + }, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, filterPlaza, sessionPageNo, navigate]); const handleLoadNextFeedPage = useCallback(async () => { try { + setShowCompletionModal(false); setLoading(true); const nextPage = (activeTab.page || 0) + 1; const selectedSiteId = selectedSite?.site_id ?? ''; @@ -154,6 +176,8 @@ export const AuditSession: React.FC = () => { setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); + setMasterZoom(1); + setAnomalyZoom(1); setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending')); } else { alert("No more anomalies left in the feed."); @@ -167,6 +191,7 @@ export const AuditSession: React.FC = () => { }, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, activeTab, selectedTabIndex, updateTab, navigate]); useEffect(() => { + if (hasInitializedSession.current) return; const navState = location.state as any; if (navState?.useExistingFeed) { const activeTab = tabs[selectedTabIndex]; @@ -188,6 +213,7 @@ export const AuditSession: React.FC = () => { } else { setLoading(false); } + hasInitializedSession.current = true; } else if (location.state && location.state.anomalies) { setAnomalies(location.state.anomalies); setCurrentIndex(location.state.startIndex || 0); @@ -203,8 +229,10 @@ export const AuditSession: React.FC = () => { } else { setLoading(false); } + hasInitializedSession.current = true; } else { loadSessionData(); + hasInitializedSession.current = true; } }, [location.state, tabs, selectedTabIndex, loadSessionData, filterPlaza]); @@ -222,6 +250,8 @@ export const AuditSession: React.FC = () => { } else { setCarouselIndex(0); } + setMasterZoom(1); + setAnomalyZoom(1); }, [currentAnomaly]); let masterImageSrc = ''; @@ -238,13 +268,21 @@ export const AuditSession: React.FC = () => { } if (currentAnomaly.Pos) { - const match = String(currentAnomaly.Pos).match(/([NS][0-9.]+)([EW][0-9.]+)/i); - if (match) { - latStr = match[1]; - longStr = match[2]; + const matchWithComma = String(currentAnomaly.Pos).match(/([NS])\s*([0-9.]+)[,\s]*([EW])\s*([0-9.]+)/i); + if (matchWithComma) { + const latVal = parseFloat(matchWithComma[2]).toFixed(5); + const longVal = parseFloat(matchWithComma[4]).toFixed(5); + latStr = `${matchWithComma[1].toUpperCase()} ${latVal}`; + longStr = `${matchWithComma[3].toUpperCase()} ${longVal}`; } else { - latStr = currentAnomaly.Pos; - longStr = ''; + const matchOld = String(currentAnomaly.Pos).match(/([NS][0-9.]+)([EW][0-9.]+)/i); + if (matchOld) { + latStr = matchOld[1]; + longStr = matchOld[2]; + } else { + latStr = currentAnomaly.Pos; + longStr = ''; + } } } } @@ -276,6 +314,8 @@ export const AuditSession: React.FC = () => { setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); + setMasterZoom(1); + setAnomalyZoom(1); } else { setShowCompletionModal(true); } @@ -289,6 +329,8 @@ export const AuditSession: React.FC = () => { setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); + setMasterZoom(1); + setAnomalyZoom(1); } }; @@ -499,8 +541,14 @@ export const AuditSession: React.FC = () => { break; } case 'enter': + if (selectedCategory !== null) { + handleSaveAndNext(); + } else { + handleSkip(); + } + break; case 'arrowright': - handleSaveAndNext(); + handleNext(); break; case 'arrowleft': handlePrev(); @@ -536,6 +584,7 @@ export const AuditSession: React.FC = () => { No unaudited anomalies found to audit. @@ -568,6 +617,7 @@ export const AuditSession: React.FC = () => { startIcon={} onClick={() => { const navState = location.state as any; + if (!navState?.useExistingFeed) invalidateDashboardCache(); navigate(navState?.useExistingFeed ? '/activity-feeds' : '/dashboard'); }} sx={{ color: '#94a3b8', textTransform: 'none', minWidth: 'auto', p: 0, '&:hover': { color: 'white', bgcolor: 'transparent' } }} @@ -624,12 +674,26 @@ export const AuditSession: React.FC = () => { MASTER IMAGE - + + + setMasterZoom(z => z + 0.5)} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}> + + + setMasterZoom(z => Math.max(1, z - 0.5))} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}> + + + {masterImageSrc ? ( Master { e.currentTarget.onerror = null; e.currentTarget.src = '/assets/images/master_image_not_available.png'; @@ -647,16 +711,35 @@ export const AuditSession: React.FC = () => { ANOMALY IMAGE {currentAnomaly?.date_of_audit || currentAnomaly?.Created_on} - + + + setAnomalyZoom(z => z + 0.5)} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}> + + + setAnomalyZoom(z => Math.max(1, z - 0.5))} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}> + + + {mediaItems.length > 0 ? ( <> {mediaItems[carouselIndex]?.type === 'video' ? ( - - {/* Bottom Bar: Location Overlay */} - - - - {currentAnomaly?.Plaza || 'Unknown Location'} - - {latStr}, {longStr} • ID #{currentAnomaly?.id || currentAnomaly?.anomaly_id || 'N/A'} - + {/* Bottom Bar: Metadata Overlay */} + + + + + + {currentAnomaly?.Plaza || 'Unknown Location'} + + + + + + + + + + + @@ -912,8 +1009,8 @@ export const AuditSession: React.FC = () => { - {/* Footer links */} - + {/* Footer */} + Keyboard shortcuts available inside the audit view - Reset demo data