sorting for records in the ui
This commit is contained in:
@@ -84,9 +84,11 @@ export const activityFeedsService = {
|
|||||||
// Dashboard stats scoped to the logged-in user (org-wide for
|
// Dashboard stats scoped to the logged-in user (org-wide for
|
||||||
// ADMIN/MODERATOR): overall workload, today's activity, and the workload
|
// ADMIN/MODERATOR): overall workload, today's activity, and the workload
|
||||||
// split into batches of 50 in the same order the audit session walks.
|
// split into batches of 50 in the same order the audit session walks.
|
||||||
getUserStats: async (isRectificationEnabled: boolean) => {
|
// sortBy/sortDir keep the batch numbering aligned with the feed/session
|
||||||
|
// order when a custom sort is active (backend mirrors the same whitelist).
|
||||||
|
getUserStats: async (isRectificationEnabled: boolean, sortBy?: string, sortDir?: string) => {
|
||||||
const response = await axiosClient.get('/api/audit/anomaly/user-stats', {
|
const response = await axiosClient.get('/api/audit/anomaly/user-stats', {
|
||||||
params: { isRectificationEnabled }
|
params: { isRectificationEnabled, sortBy: sortBy || '', sortDir: sortDir || '' }
|
||||||
});
|
});
|
||||||
return response as unknown as {
|
return response as unknown as {
|
||||||
scoped: boolean;
|
scoped: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useMemo, useCallback, memo } from 'react';
|
import React, { useState, useMemo, useCallback, memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, TextField, Checkbox, FormControlLabel, Button, Typography, Dialog,
|
Box, TextField, Checkbox, FormControlLabel, Button, Typography, Dialog,
|
||||||
DialogContent, DialogActions, CircularProgress, Divider, InputAdornment, Chip
|
DialogContent, DialogActions, CircularProgress, Divider, InputAdornment, Chip,
|
||||||
|
Select, MenuItem
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import { useFeedStore } from '../../store/feedStore';
|
import { useFeedStore } from '../../store/feedStore';
|
||||||
@@ -70,6 +71,7 @@ export const FeedFilters: React.FC = () => {
|
|||||||
filterFromDate, filterTillDate,
|
filterFromDate, filterTillDate,
|
||||||
filterMinChainage, filterMaxChainage,
|
filterMinChainage, filterMaxChainage,
|
||||||
setFilterMinChainage, setFilterMaxChainage,
|
setFilterMinChainage, setFilterMaxChainage,
|
||||||
|
sortBy, sortDir, setSortBy, setSortDir,
|
||||||
} = useFeedStore();
|
} = useFeedStore();
|
||||||
|
|
||||||
// ── Use a Set (stored in state) for O(1) has() lookups ────────────────────
|
// ── 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 [draftSet, setDraftSet] = useState<Set<number>>(() => new Set(selectedAssetIds));
|
||||||
const [draftFromDate, setDraftFromDate] = useState('');
|
const [draftFromDate, setDraftFromDate] = useState('');
|
||||||
const [draftTillDate, setDraftTillDate] = useState('');
|
const [draftTillDate, setDraftTillDate] = useState('');
|
||||||
|
const [draftSortBy, setDraftSortBy] = useState<string>(() => sortBy);
|
||||||
|
const [draftSortDir, setDraftSortDir] = useState<string>(() => sortDir);
|
||||||
const [draftMinChainage, setDraftMinChainage] = useState('');
|
const [draftMinChainage, setDraftMinChainage] = useState('');
|
||||||
const [draftMaxChainage, setDraftMaxChainage] = useState('');
|
const [draftMaxChainage, setDraftMaxChainage] = useState('');
|
||||||
const [assetSearch, setAssetSearch] = useState('');
|
const [assetSearch, setAssetSearch] = useState('');
|
||||||
@@ -146,6 +150,8 @@ export const FeedFilters: React.FC = () => {
|
|||||||
setDraftTillDate('');
|
setDraftTillDate('');
|
||||||
setDraftMinChainage('');
|
setDraftMinChainage('');
|
||||||
setDraftMaxChainage('');
|
setDraftMaxChainage('');
|
||||||
|
setDraftSortBy('');
|
||||||
|
setDraftSortDir('asc');
|
||||||
setAssetSearch('');
|
setAssetSearch('');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -162,6 +168,8 @@ export const FeedFilters: React.FC = () => {
|
|||||||
setFilterMinChainage(draftMinChainage); // persist chainage globally
|
setFilterMinChainage(draftMinChainage); // persist chainage globally
|
||||||
setFilterMaxChainage(draftMaxChainage); // persist chainage globally
|
setFilterMaxChainage(draftMaxChainage); // persist chainage globally
|
||||||
setSelectedAssetIds(draftAssetArr);
|
setSelectedAssetIds(draftAssetArr);
|
||||||
|
setSortBy(draftSortBy as any);
|
||||||
|
setSortDir(draftSortDir as any);
|
||||||
|
|
||||||
if (selectedTabIndex >= 0) {
|
if (selectedTabIndex >= 0) {
|
||||||
const activeTab = tabs[selectedTabIndex];
|
const activeTab = tabs[selectedTabIndex];
|
||||||
@@ -172,7 +180,9 @@ export const FeedFilters: React.FC = () => {
|
|||||||
const assetIdsKey = draftAssetArr.join(',');
|
const assetIdsKey = draftAssetArr.join(',');
|
||||||
|
|
||||||
// Build the correct filter key that will be calculated in GlobalFeed
|
// 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({
|
const historyRes: any = await activityFeedsService.getHistory({
|
||||||
date: activeTab.date,
|
date: activeTab.date,
|
||||||
@@ -189,6 +199,8 @@ export const FeedFilters: React.FC = () => {
|
|||||||
fromChainage: draftMinChainage,
|
fromChainage: draftMinChainage,
|
||||||
toChainage: draftMaxChainage,
|
toChainage: draftMaxChainage,
|
||||||
plaza: filterPlaza,
|
plaza: filterPlaza,
|
||||||
|
sortBy: draftSortBy,
|
||||||
|
sortDir: draftSortDir,
|
||||||
});
|
});
|
||||||
if (historyRes?.anomalies) {
|
if (historyRes?.anomalies) {
|
||||||
updateTab(selectedTabIndex, {
|
updateTab(selectedTabIndex, {
|
||||||
@@ -246,6 +258,32 @@ export const FeedFilters: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</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 */}
|
{/* Date & Chainage */}
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 3, alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 3, alignItems: 'center' }}>
|
||||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
filterFromDate, filterTillDate,
|
filterFromDate, filterTillDate,
|
||||||
filterMinChainage, filterMaxChainage,
|
filterMinChainage, filterMaxChainage,
|
||||||
selectedAssetIds,
|
selectedAssetIds,
|
||||||
setIsFeedLoading
|
setIsFeedLoading,
|
||||||
|
sortBy, sortDir
|
||||||
} = useFeedStore();
|
} = useFeedStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,7 +61,7 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
const assetIdsKey = selectedAssetIds.join(',');
|
const assetIdsKey = selectedAssetIds.join(',');
|
||||||
|
|
||||||
// Build a key for the current filter state on the active tab
|
// 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
|
// CACHE HIT: This exact tab and filter combo was already fetched
|
||||||
if (filterKey === activeTab.filters?.lastFetchedFilterKey && activeTab.anomalies && activeTab.anomalies.length > 0) {
|
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' : '')),
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
isRectificationEnabled: isRectActive,
|
isRectificationEnabled: isRectActive,
|
||||||
sortByDateAsc: false,
|
sortByDateAsc: false,
|
||||||
|
sortBy,
|
||||||
|
sortDir,
|
||||||
anomalyIds: searchAnomalyId,
|
anomalyIds: searchAnomalyId,
|
||||||
plaza: filterPlaza,
|
plaza: filterPlaza,
|
||||||
assetsIds: assetIdsKey,
|
assetsIds: assetIdsKey,
|
||||||
@@ -116,7 +119,7 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
fetchFilteredHistory();
|
fetchFilteredHistory();
|
||||||
}, [
|
}, [
|
||||||
selectedSite?.site_id, auditStatus, searchAnomalyId, filterPlaza, isRectificationEnabled, selectedTabIndex, tabs.length,
|
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) => {
|
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' : '')),
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
isRectificationEnabled: isRectActive,
|
isRectificationEnabled: isRectActive,
|
||||||
sortByDateAsc: false,
|
sortByDateAsc: false,
|
||||||
|
sortBy,
|
||||||
|
sortDir,
|
||||||
anomalyIds: searchAnomalyId,
|
anomalyIds: searchAnomalyId,
|
||||||
plaza: filterPlaza,
|
plaza: filterPlaza,
|
||||||
assetsIds: assetIdsKey,
|
assetsIds: assetIdsKey,
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
selectedSite, filterFromDate, filterTillDate,
|
selectedSite, filterFromDate, filterTillDate,
|
||||||
filterMinChainage, filterMaxChainage, selectedAssetIds,
|
filterMinChainage, filterMaxChainage, selectedAssetIds,
|
||||||
searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza,
|
searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza,
|
||||||
setDashboardCache
|
setDashboardCache, sortBy, sortDir, setSortBy, setSortDir
|
||||||
} = useFeedStore();
|
} = useFeedStore();
|
||||||
|
|
||||||
// Call this before every navigate('/dashboard') so the dashboard re-fetches fresh stats
|
// 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,
|
pageNo: targetPage * 50,
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
sortByDateAsc: false, // must match dashboard sort so firstPendingIndex page alignment is correct
|
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'),
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
auditStatus: auditStatus,
|
auditStatus: auditStatus,
|
||||||
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
@@ -267,7 +269,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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 () => {
|
const handleLoadNextFeedPage = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -282,6 +284,8 @@ export const AuditSession: React.FC = () => {
|
|||||||
date: activeDate,
|
date: activeDate,
|
||||||
pageNo: nextPage * feedPageSize,
|
pageNo: nextPage * feedPageSize,
|
||||||
pageSize: feedPageSize,
|
pageSize: feedPageSize,
|
||||||
|
sortBy,
|
||||||
|
sortDir,
|
||||||
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
auditStatus: auditStatus,
|
auditStatus: auditStatus,
|
||||||
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
auto_audit: isRectActive ? '' : (auditStatus === 'Auto Audits' ? 'true' : (auditStatus === 'Manual' ? 'false' : '')),
|
||||||
@@ -323,7 +327,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (hasInitializedSession.current) return;
|
if (hasInitializedSession.current) return;
|
||||||
@@ -893,6 +897,26 @@ export const AuditSession: React.FC = () => {
|
|||||||
goToIndex(firstSkipped !== -1 ? firstSkipped : anomalies.length - 1);
|
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
|
// Category 1 nests a sub-category (Plant / Not an Anomaly) - a verdict
|
||||||
// without it is incomplete, both for immediate save and for staging.
|
// without it is incomplete, both for immediate save and for staging.
|
||||||
const validateSubCategory = (): boolean => {
|
const validateSubCategory = (): boolean => {
|
||||||
@@ -1238,6 +1262,31 @@ export const AuditSession: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<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>
|
<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>
|
<Button size="small" onClick={() => setIsShortcutsDialogOpen(true)} sx={{ color: '#94a3b8', border: '1px solid #334155', textTransform: 'none', borderRadius: 2 }}>? Shortcuts</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const Dashboard: React.FC = () => {
|
|||||||
filterMinChainage, filterMaxChainage,
|
filterMinChainage, filterMaxChainage,
|
||||||
selectedAssetIds, dashboardCache, setDashboardCache,
|
selectedAssetIds, dashboardCache, setDashboardCache,
|
||||||
searchAnomalyId, filterPlaza, isRectificationEnabled,
|
searchAnomalyId, filterPlaza, isRectificationEnabled,
|
||||||
tabs, selectedTabIndex
|
tabs, selectedTabIndex, sortBy, sortDir
|
||||||
} = useFeedStore();
|
} = useFeedStore();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -26,7 +26,7 @@ export const Dashboard: React.FC = () => {
|
|||||||
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
|
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
|
||||||
|
|
||||||
// Build a key that uniquely identifies the current filter state
|
// Build a key that uniquely identifies the current filter state
|
||||||
const currentFilterKey = `${selectedSiteId}|${auditStatus}|${filterFromDate}|${filterTillDate}|${assetIdsKey}|${filterMinChainage}|${filterMaxChainage}|${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
|
// Session-start info (which page the audit session should open on) - still
|
||||||
// derived from the filtered feed, same as before. The headline stats below
|
// derived from the filtered feed, same as before. The headline stats below
|
||||||
@@ -49,16 +49,18 @@ export const Dashboard: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setStatsLoading(true);
|
setStatsLoading(true);
|
||||||
activityFeedsService.getUserStats(isRectActive)
|
activityFeedsService.getUserStats(isRectActive, sortBy, sortDir)
|
||||||
.then((s) => { if (!cancelled) setUserStats(s); })
|
.then((s) => { if (!cancelled) setUserStats(s); })
|
||||||
.catch((err) => console.error('Failed to load user stats', err))
|
.catch((err) => console.error('Failed to load user stats', err))
|
||||||
.finally(() => { if (!cancelled) setStatsLoading(false); });
|
.finally(() => { if (!cancelled) setStatsLoading(false); });
|
||||||
return () => { cancelled = true; };
|
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(() => {
|
useEffect(() => {
|
||||||
// CACHE HIT: filters unchanged since last fetch — skip API call entirely
|
// CACHE HIT: filters unchanged since last fetch — skip API call entirely
|
||||||
if (dashboardCache.stats !== null && currentFilterKey === `${dashboardCache.siteId}|${dashboardCache.auditStatus}|${dashboardCache.fromDate}|${dashboardCache.tillDate}|${dashboardCache.assetIds}|${dashboardCache.minChainage}|${dashboardCache.maxChainage}|${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);
|
setSessionInfo(dashboardCache.stats);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,6 +72,8 @@ export const Dashboard: React.FC = () => {
|
|||||||
pageNo: 0,
|
pageNo: 0,
|
||||||
pageSize: 200,
|
pageSize: 200,
|
||||||
sortByDateAsc: false,
|
sortByDateAsc: false,
|
||||||
|
sortBy,
|
||||||
|
sortDir,
|
||||||
isRectificationEnabled: isRectActive,
|
isRectificationEnabled: isRectActive,
|
||||||
auditStatus: auditStatus,
|
auditStatus: auditStatus,
|
||||||
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
isTrueFalse: isRectActive ? 'both' : (auditStatus === 'False Audits' ? 'true' : 'both'),
|
||||||
@@ -108,6 +112,8 @@ export const Dashboard: React.FC = () => {
|
|||||||
filterPlaza,
|
filterPlaza,
|
||||||
isRectificationEnabled: isRectActive,
|
isRectificationEnabled: isRectActive,
|
||||||
date: activeDate,
|
date: activeDate,
|
||||||
|
sortBy,
|
||||||
|
sortDir,
|
||||||
stats: newStats,
|
stats: newStats,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
// Feed/session sort. '' = backend default order (id DESC on the pending
|
||||||
|
// feed). One choice drives the whole chain: feed table, session pages,
|
||||||
|
// dashboard batch numbering and the "continue auditing" start page - they
|
||||||
|
// must all use the same order or batch N stops matching session page N.
|
||||||
|
export type FeedSortBy = '' | 'id' | 'date' | 'chainage' | 'latlong';
|
||||||
|
export type FeedSortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
export interface FeedTab {
|
export interface FeedTab {
|
||||||
date: string;
|
date: string;
|
||||||
isRectificationTab: boolean;
|
isRectificationTab: boolean;
|
||||||
@@ -26,6 +33,8 @@ interface DashboardCache {
|
|||||||
filterPlaza: string;
|
filterPlaza: string;
|
||||||
isRectificationEnabled?: boolean;
|
isRectificationEnabled?: boolean;
|
||||||
date?: string;
|
date?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortDir?: string;
|
||||||
stats: {
|
stats: {
|
||||||
globalTotal: number;
|
globalTotal: number;
|
||||||
batchTotal: number;
|
batchTotal: number;
|
||||||
@@ -59,6 +68,8 @@ interface FeedState {
|
|||||||
searchAnomalyId: string;
|
searchAnomalyId: string;
|
||||||
isRectificationEnabled: boolean;
|
isRectificationEnabled: boolean;
|
||||||
isFeedLoading: boolean;
|
isFeedLoading: boolean;
|
||||||
|
sortBy: FeedSortBy;
|
||||||
|
sortDir: FeedSortDir;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
setTabs: (tabs: FeedTab[]) => void;
|
setTabs: (tabs: FeedTab[]) => void;
|
||||||
@@ -84,6 +95,8 @@ interface FeedState {
|
|||||||
setSearchAnomalyId: (id: string) => void;
|
setSearchAnomalyId: (id: string) => void;
|
||||||
setIsRectificationEnabled: (val: boolean) => void;
|
setIsRectificationEnabled: (val: boolean) => void;
|
||||||
setIsFeedLoading: (val: boolean) => void;
|
setIsFeedLoading: (val: boolean) => void;
|
||||||
|
setSortBy: (val: FeedSortBy) => void;
|
||||||
|
setSortDir: (val: FeedSortDir) => void;
|
||||||
resetStore: () => void;
|
resetStore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +136,8 @@ const initialFeedState = {
|
|||||||
searchAnomalyId: '',
|
searchAnomalyId: '',
|
||||||
isRectificationEnabled: false,
|
isRectificationEnabled: false,
|
||||||
isFeedLoading: false,
|
isFeedLoading: false,
|
||||||
|
sortBy: '' as FeedSortBy,
|
||||||
|
sortDir: 'asc' as FeedSortDir,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFeedStore = create<FeedState>()(
|
export const useFeedStore = create<FeedState>()(
|
||||||
@@ -152,6 +167,8 @@ export const useFeedStore = create<FeedState>()(
|
|||||||
setSearchAnomalyId: (searchAnomalyId) => set({ searchAnomalyId }),
|
setSearchAnomalyId: (searchAnomalyId) => set({ searchAnomalyId }),
|
||||||
setIsRectificationEnabled: (isRectificationEnabled) => set({ isRectificationEnabled }),
|
setIsRectificationEnabled: (isRectificationEnabled) => set({ isRectificationEnabled }),
|
||||||
setIsFeedLoading: (isFeedLoading) => set({ isFeedLoading }),
|
setIsFeedLoading: (isFeedLoading) => set({ isFeedLoading }),
|
||||||
|
setSortBy: (sortBy) => set({ sortBy }),
|
||||||
|
setSortDir: (sortDir) => set({ sortDir }),
|
||||||
addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })),
|
addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })),
|
||||||
closeTab: (index) => set((state) => {
|
closeTab: (index) => set((state) => {
|
||||||
const newTabs = [...state.tabs];
|
const newTabs = [...state.tabs];
|
||||||
@@ -180,6 +197,8 @@ export const useFeedStore = create<FeedState>()(
|
|||||||
auditStatus: state.auditStatus,
|
auditStatus: state.auditStatus,
|
||||||
searchAnomalyId: state.searchAnomalyId,
|
searchAnomalyId: state.searchAnomalyId,
|
||||||
isRectificationEnabled: state.isRectificationEnabled,
|
isRectificationEnabled: state.isRectificationEnabled,
|
||||||
|
sortBy: state.sortBy,
|
||||||
|
sortDir: state.sortDir,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user