sorting for records in the ui
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useMemo, useCallback, memo } from 'react';
|
||||
import {
|
||||
Box, TextField, Checkbox, FormControlLabel, Button, Typography, Dialog,
|
||||
DialogContent, DialogActions, CircularProgress, Divider, InputAdornment, Chip
|
||||
DialogContent, DialogActions, CircularProgress, Divider, InputAdornment, Chip,
|
||||
Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { useFeedStore } from '../../store/feedStore';
|
||||
@@ -70,6 +71,7 @@ export const FeedFilters: React.FC = () => {
|
||||
filterFromDate, filterTillDate,
|
||||
filterMinChainage, filterMaxChainage,
|
||||
setFilterMinChainage, setFilterMaxChainage,
|
||||
sortBy, sortDir, setSortBy, setSortDir,
|
||||
} = useFeedStore();
|
||||
|
||||
// ── Use a Set (stored in state) for O(1) has() lookups ────────────────────
|
||||
@@ -77,6 +79,8 @@ export const FeedFilters: React.FC = () => {
|
||||
const [draftSet, setDraftSet] = useState<Set<number>>(() => new Set(selectedAssetIds));
|
||||
const [draftFromDate, setDraftFromDate] = useState('');
|
||||
const [draftTillDate, setDraftTillDate] = useState('');
|
||||
const [draftSortBy, setDraftSortBy] = useState<string>(() => sortBy);
|
||||
const [draftSortDir, setDraftSortDir] = useState<string>(() => sortDir);
|
||||
const [draftMinChainage, setDraftMinChainage] = useState('');
|
||||
const [draftMaxChainage, setDraftMaxChainage] = useState('');
|
||||
const [assetSearch, setAssetSearch] = useState('');
|
||||
@@ -146,6 +150,8 @@ export const FeedFilters: React.FC = () => {
|
||||
setDraftTillDate('');
|
||||
setDraftMinChainage('');
|
||||
setDraftMaxChainage('');
|
||||
setDraftSortBy('');
|
||||
setDraftSortDir('asc');
|
||||
setAssetSearch('');
|
||||
}, []);
|
||||
|
||||
@@ -162,6 +168,8 @@ export const FeedFilters: React.FC = () => {
|
||||
setFilterMinChainage(draftMinChainage); // persist chainage globally
|
||||
setFilterMaxChainage(draftMaxChainage); // persist chainage globally
|
||||
setSelectedAssetIds(draftAssetArr);
|
||||
setSortBy(draftSortBy as any);
|
||||
setSortDir(draftSortDir as any);
|
||||
|
||||
if (selectedTabIndex >= 0) {
|
||||
const activeTab = tabs[selectedTabIndex];
|
||||
@@ -172,7 +180,9 @@ export const FeedFilters: React.FC = () => {
|
||||
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}`;
|
||||
// Must mirror GlobalFeed's filterKey format exactly or the cache
|
||||
// check there misses and double-fetches.
|
||||
const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeTab.date}|${draftFromDate}|${draftTillDate}|${draftMinChainage}|${draftMaxChainage}|${assetIdsKey}|${draftSortBy}|${draftSortDir}`;
|
||||
|
||||
const historyRes: any = await activityFeedsService.getHistory({
|
||||
date: activeTab.date,
|
||||
@@ -189,6 +199,8 @@ export const FeedFilters: React.FC = () => {
|
||||
fromChainage: draftMinChainage,
|
||||
toChainage: draftMaxChainage,
|
||||
plaza: filterPlaza,
|
||||
sortBy: draftSortBy,
|
||||
sortDir: draftSortDir,
|
||||
});
|
||||
if (historyRes?.anomalies) {
|
||||
updateTab(selectedTabIndex, {
|
||||
@@ -246,6 +258,32 @@ export const FeedFilters: React.FC = () => {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Sort - drives feed order, session page order and dashboard batch
|
||||
numbering (one source of truth end to end) */}
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#94a3b8', whiteSpace: 'nowrap' }}>Sort by</Typography>
|
||||
<Select
|
||||
size="small"
|
||||
value={draftSortBy}
|
||||
onChange={(e) => setDraftSortBy(String(e.target.value))}
|
||||
displayEmpty
|
||||
sx={{ minWidth: 140, color: '#f8fafc', bgcolor: 'rgba(255,255,255,0.07)', borderRadius: 1, '& fieldset': { borderColor: '#334155' }, '& .MuiSvgIcon-root': { color: '#94a3b8' } }}
|
||||
MenuProps={{ slotProps: { paper: { sx: { bgcolor: '#0f172a', border: '1px solid #1e293b' } } } }}
|
||||
>
|
||||
{[['', 'Default'], ['id', 'ID'], ['date', 'Date'], ['chainage', 'Chainage'], ['latlong', 'Lat/Long']].map(([v, l]) => (
|
||||
<MenuItem key={v} value={v} sx={{ color: '#f8fafc', bgcolor: '#0f172a', '&:hover': { bgcolor: '#1e293b' } }}>{l}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={!draftSortBy}
|
||||
onClick={() => setDraftSortDir(d => (d === 'asc' ? 'desc' : 'asc'))}
|
||||
sx={{ color: '#94a3b8', border: '1px solid #334155', textTransform: 'none', borderRadius: 1, minWidth: 72, '&.Mui-disabled': { color: '#475569', borderColor: '#1e293b' } }}
|
||||
>
|
||||
{draftSortDir === 'asc' ? '↑ Asc' : '↓ Desc'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Date & Chainage */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 3, alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
|
||||
@@ -16,7 +16,8 @@ export const GlobalFeed: React.FC = () => {
|
||||
filterFromDate, filterTillDate,
|
||||
filterMinChainage, filterMaxChainage,
|
||||
selectedAssetIds,
|
||||
setIsFeedLoading
|
||||
setIsFeedLoading,
|
||||
sortBy, sortDir
|
||||
} = useFeedStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,7 +61,7 @@ export const GlobalFeed: React.FC = () => {
|
||||
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}`;
|
||||
const filterKey = `${selectedSite?.site_id ?? ''}|${auditStatus}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeTab.date}|${filterFromDate}|${filterTillDate}|${filterMinChainage}|${filterMaxChainage}|${assetIdsKey}|${sortBy}|${sortDir}`;
|
||||
|
||||
// CACHE HIT: This exact tab and filter combo was already fetched
|
||||
if (filterKey === activeTab.filters?.lastFetchedFilterKey && activeTab.anomalies && activeTab.anomalies.length > 0) {
|
||||
@@ -83,6 +84,8 @@ export const GlobalFeed: React.FC = () => {
|
||||
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||
isRectificationEnabled: isRectActive,
|
||||
sortByDateAsc: false,
|
||||
sortBy,
|
||||
sortDir,
|
||||
anomalyIds: searchAnomalyId,
|
||||
plaza: filterPlaza,
|
||||
assetsIds: assetIdsKey,
|
||||
@@ -116,7 +119,7 @@ export const GlobalFeed: React.FC = () => {
|
||||
fetchFilteredHistory();
|
||||
}, [
|
||||
selectedSite?.site_id, auditStatus, searchAnomalyId, filterPlaza, isRectificationEnabled, selectedTabIndex, tabs.length,
|
||||
filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, selectedAssetIds
|
||||
filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, selectedAssetIds, sortBy, sortDir
|
||||
]);
|
||||
|
||||
const handlePageChange = async (_event: unknown, newPage: number) => {
|
||||
@@ -137,6 +140,8 @@ export const GlobalFeed: React.FC = () => {
|
||||
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||
isRectificationEnabled: isRectActive,
|
||||
sortByDateAsc: false,
|
||||
sortBy,
|
||||
sortDir,
|
||||
anomalyIds: searchAnomalyId,
|
||||
plaza: filterPlaza,
|
||||
assetsIds: assetIdsKey,
|
||||
|
||||
@@ -199,7 +199,7 @@ export const AuditSession: React.FC = () => {
|
||||
selectedSite, filterFromDate, filterTillDate,
|
||||
filterMinChainage, filterMaxChainage, selectedAssetIds,
|
||||
searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza,
|
||||
setDashboardCache
|
||||
setDashboardCache, sortBy, sortDir, setSortBy, setSortDir
|
||||
} = useFeedStore();
|
||||
|
||||
// Call this before every navigate('/dashboard') so the dashboard re-fetches fresh stats
|
||||
@@ -233,6 +233,8 @@ export const AuditSession: React.FC = () => {
|
||||
pageNo: targetPage * 50,
|
||||
pageSize: 50,
|
||||
sortByDateAsc: false, // must match dashboard sort so firstPendingIndex page alignment is correct
|
||||
sortBy, // user sort: same value the dashboard fetch uses, so page alignment holds
|
||||
sortDir,
|
||||
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||
auditStatus: auditStatus,
|
||||
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||
@@ -267,7 +269,7 @@ export const AuditSession: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, filterPlaza, sessionPageNo, navigate]);
|
||||
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, filterPlaza, sessionPageNo, navigate, sortBy, sortDir]);
|
||||
|
||||
const handleLoadNextFeedPage = useCallback(async () => {
|
||||
try {
|
||||
@@ -282,6 +284,8 @@ export const AuditSession: React.FC = () => {
|
||||
date: activeDate,
|
||||
pageNo: nextPage * feedPageSize,
|
||||
pageSize: feedPageSize,
|
||||
sortBy,
|
||||
sortDir,
|
||||
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||
auditStatus: auditStatus,
|
||||
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||
@@ -323,7 +327,7 @@ export const AuditSession: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, activeTab, selectedTabIndex, updateTab, navigate]);
|
||||
}, [selectedSite, selectedAssetIds, auditStatus, isRectificationEnabled, filterFromDate, filterTillDate, filterMinChainage, filterMaxChainage, searchAnomalyId, activeDate, isRectActive, location.state, activeTab, selectedTabIndex, updateTab, navigate, sortBy, sortDir]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitializedSession.current) return;
|
||||
@@ -893,6 +897,26 @@ export const AuditSession: React.FC = () => {
|
||||
goToIndex(firstSkipped !== -1 ? firstSkipped : anomalies.length - 1);
|
||||
};
|
||||
|
||||
// Changing sort mid-session reloads page 0 in the new order. Dashboard
|
||||
// sessions only - feed sessions inherit the order of the feed tab they
|
||||
// were opened from (the feed page has its own selector). Staged bulk work
|
||||
// can't survive a reorder, so it's discarded after a confirm.
|
||||
const handleSessionSortChange = (by: string, dir: string) => {
|
||||
if (stagedCount > 0 && !window.confirm(`Changing sort reloads the session and discards ${stagedCount} staged audit(s). Continue?`)) return;
|
||||
setSortBy(by as any);
|
||||
setSortDir(dir as any);
|
||||
};
|
||||
|
||||
const skipSortReload = useRef(true);
|
||||
useEffect(() => {
|
||||
if (skipSortReload.current) { skipSortReload.current = false; return; }
|
||||
if (isFeedSession) return;
|
||||
setShowBatchEndPanel(false);
|
||||
setSessionPageNo(0);
|
||||
loadSessionData(0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sortBy, sortDir]);
|
||||
|
||||
// Category 1 nests a sub-category (Plant / Not an Anomaly) - a verdict
|
||||
// without it is incomplete, both for immediate save and for staging.
|
||||
const validateSubCategory = (): boolean => {
|
||||
@@ -1238,6 +1262,31 @@ export const AuditSession: React.FC = () => {
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{!isFeedSession && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Select
|
||||
size="small"
|
||||
value={sortBy}
|
||||
onChange={(e) => handleSessionSortChange(String(e.target.value), sortDir)}
|
||||
displayEmpty
|
||||
sx={{ fontSize: '0.75rem', color: '#94a3b8', height: 30, '& fieldset': { borderColor: '#334155' }, '& .MuiSvgIcon-root': { color: '#94a3b8' } }}
|
||||
MenuProps={{ sx: { zIndex: 10001 }, slotProps: { paper: { sx: { bgcolor: '#0f172a', border: '1px solid #1e293b' } } } }}
|
||||
>
|
||||
{[['', 'Sort: Default'], ['id', 'Sort: ID'], ['date', 'Sort: Date'], ['chainage', 'Sort: Chainage'], ['latlong', 'Sort: Lat/Long']].map(([v, l]) => (
|
||||
<MenuItem key={v} value={v} sx={{ fontSize: '0.8rem', color: '#f8fafc', bgcolor: '#0f172a', '&:hover': { bgcolor: '#1e293b' } }}>{l}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={!sortBy}
|
||||
onClick={() => handleSessionSortChange(sortBy, sortDir === 'asc' ? 'desc' : 'asc')}
|
||||
title="Toggle sort direction"
|
||||
sx={{ minWidth: 52, color: '#94a3b8', border: '1px solid #334155', textTransform: 'none', borderRadius: 2, '&.Mui-disabled': { color: '#475569', borderColor: '#1e293b' } }}
|
||||
>
|
||||
{sortDir === 'asc' ? '↑ Asc' : '↓ Desc'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Typography sx={{ fontWeight: 'bold', fontSize: '0.9rem' }}>{user?.username || user?.email}</Typography>
|
||||
<Button size="small" onClick={() => setIsShortcutsDialogOpen(true)} sx={{ color: '#94a3b8', border: '1px solid #334155', textTransform: 'none', borderRadius: 2 }}>? Shortcuts</Button>
|
||||
</Box>
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Dashboard: React.FC = () => {
|
||||
filterMinChainage, filterMaxChainage,
|
||||
selectedAssetIds, dashboardCache, setDashboardCache,
|
||||
searchAnomalyId, filterPlaza, isRectificationEnabled,
|
||||
tabs, selectedTabIndex
|
||||
tabs, selectedTabIndex, sortBy, sortDir
|
||||
} = useFeedStore();
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -26,7 +26,7 @@ export const Dashboard: React.FC = () => {
|
||||
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
|
||||
|
||||
// Build a key that uniquely identifies the current filter state
|
||||
const currentFilterKey = `${selectedSiteId}|${auditStatus}|${filterFromDate}|${filterTillDate}|${assetIdsKey}|${filterMinChainage}|${filterMaxChainage}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeDate}`;
|
||||
const currentFilterKey = `${selectedSiteId}|${auditStatus}|${filterFromDate}|${filterTillDate}|${assetIdsKey}|${filterMinChainage}|${filterMaxChainage}|${searchAnomalyId}|${filterPlaza}|${isRectActive}|${activeDate}|${sortBy}|${sortDir}`;
|
||||
|
||||
// Session-start info (which page the audit session should open on) - still
|
||||
// derived from the filtered feed, same as before. The headline stats below
|
||||
@@ -49,16 +49,18 @@ export const Dashboard: React.FC = () => {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setStatsLoading(true);
|
||||
activityFeedsService.getUserStats(isRectActive)
|
||||
activityFeedsService.getUserStats(isRectActive, sortBy, sortDir)
|
||||
.then((s) => { if (!cancelled) setUserStats(s); })
|
||||
.catch((err) => console.error('Failed to load user stats', err))
|
||||
.finally(() => { if (!cancelled) setStatsLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [isRectActive]);
|
||||
// Batch numbering follows the chosen sort - refetch when it changes so
|
||||
// "batch N" keeps matching the N-th session page.
|
||||
}, [isRectActive, sortBy, sortDir]);
|
||||
|
||||
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}|${dashboardCache.searchAnomalyId}|${dashboardCache.filterPlaza}|${dashboardCache.isRectificationEnabled ?? false}|${dashboardCache.date ?? ''}`) {
|
||||
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 ?? ''}|${dashboardCache.sortBy ?? ''}|${dashboardCache.sortDir ?? 'asc'}`) {
|
||||
setSessionInfo(dashboardCache.stats);
|
||||
return;
|
||||
}
|
||||
@@ -70,6 +72,8 @@ export const Dashboard: React.FC = () => {
|
||||
pageNo: 0,
|
||||
pageSize: 200,
|
||||
sortByDateAsc: false,
|
||||
sortBy,
|
||||
sortDir,
|
||||
isRectificationEnabled: isRectActive,
|
||||
auditStatus: auditStatus,
|
||||
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||
@@ -108,6 +112,8 @@ export const Dashboard: React.FC = () => {
|
||||
filterPlaza,
|
||||
isRectificationEnabled: isRectActive,
|
||||
date: activeDate,
|
||||
sortBy,
|
||||
sortDir,
|
||||
stats: newStats,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user