From 3cb086d67bae08a64d7146a5206be7b054e0bd40 Mon Sep 17 00:00:00 2001 From: kaushik Date: Mon, 13 Jul 2026 21:43:10 +0530 Subject: [PATCH] sorting for records in the ui --- src/api/activityFeedsService.ts | 6 ++- src/pages/activity-feeds/FeedFilters.tsx | 42 +++++++++++++++++- src/pages/activity-feeds/GlobalFeed.tsx | 11 +++-- src/pages/audit-session/AuditSession.tsx | 55 ++++++++++++++++++++++-- src/pages/dashboard/Dashboard.tsx | 16 ++++--- src/store/feedStore.ts | 19 ++++++++ 6 files changed, 134 insertions(+), 15 deletions(-) diff --git a/src/api/activityFeedsService.ts b/src/api/activityFeedsService.ts index f433b2c..dffabd9 100644 --- a/src/api/activityFeedsService.ts +++ b/src/api/activityFeedsService.ts @@ -84,9 +84,11 @@ export const activityFeedsService = { // Dashboard stats scoped to the logged-in user (org-wide for // ADMIN/MODERATOR): overall workload, today's activity, and the workload // 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', { - params: { isRectificationEnabled } + params: { isRectificationEnabled, sortBy: sortBy || '', sortDir: sortDir || '' } }); return response as unknown as { scoped: boolean; diff --git a/src/pages/activity-feeds/FeedFilters.tsx b/src/pages/activity-feeds/FeedFilters.tsx index 0df1bbd..6230c88 100644 --- a/src/pages/activity-feeds/FeedFilters.tsx +++ b/src/pages/activity-feeds/FeedFilters.tsx @@ -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>(() => new Set(selectedAssetIds)); const [draftFromDate, setDraftFromDate] = useState(''); const [draftTillDate, setDraftTillDate] = useState(''); + const [draftSortBy, setDraftSortBy] = useState(() => sortBy); + const [draftSortDir, setDraftSortDir] = useState(() => 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 = () => { )} + {/* Sort - drives feed order, session page order and dashboard batch + numbering (one source of truth end to end) */} + + Sort by + + + + {/* Date & Chainage */} diff --git a/src/pages/activity-feeds/GlobalFeed.tsx b/src/pages/activity-feeds/GlobalFeed.tsx index 4c149b4..f160077 100644 --- a/src/pages/activity-feeds/GlobalFeed.tsx +++ b/src/pages/activity-feeds/GlobalFeed.tsx @@ -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, diff --git a/src/pages/audit-session/AuditSession.tsx b/src/pages/audit-session/AuditSession.tsx index 9c1156f..73f3185 100644 --- a/src/pages/audit-session/AuditSession.tsx +++ b/src/pages/audit-session/AuditSession.tsx @@ -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 = () => { )} + {!isFeedSession && ( + + + + + )} {user?.username || user?.email} diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index fbf863b..0c07bd1 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -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, }); } diff --git a/src/store/feedStore.ts b/src/store/feedStore.ts index 5ae7ec0..e76fe4b 100644 --- a/src/store/feedStore.ts +++ b/src/store/feedStore.ts @@ -1,6 +1,13 @@ import { create } from 'zustand'; 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 { date: string; isRectificationTab: boolean; @@ -26,6 +33,8 @@ interface DashboardCache { filterPlaza: string; isRectificationEnabled?: boolean; date?: string; + sortBy?: string; + sortDir?: string; stats: { globalTotal: number; batchTotal: number; @@ -59,6 +68,8 @@ interface FeedState { searchAnomalyId: string; isRectificationEnabled: boolean; isFeedLoading: boolean; + sortBy: FeedSortBy; + sortDir: FeedSortDir; // Actions setTabs: (tabs: FeedTab[]) => void; @@ -84,6 +95,8 @@ interface FeedState { setSearchAnomalyId: (id: string) => void; setIsRectificationEnabled: (val: boolean) => void; setIsFeedLoading: (val: boolean) => void; + setSortBy: (val: FeedSortBy) => void; + setSortDir: (val: FeedSortDir) => void; resetStore: () => void; } @@ -123,6 +136,8 @@ const initialFeedState = { searchAnomalyId: '', isRectificationEnabled: false, isFeedLoading: false, + sortBy: '' as FeedSortBy, + sortDir: 'asc' as FeedSortDir, }; export const useFeedStore = create()( @@ -152,6 +167,8 @@ export const useFeedStore = create()( setSearchAnomalyId: (searchAnomalyId) => set({ searchAnomalyId }), setIsRectificationEnabled: (isRectificationEnabled) => set({ isRectificationEnabled }), setIsFeedLoading: (isFeedLoading) => set({ isFeedLoading }), + setSortBy: (sortBy) => set({ sortBy }), + setSortDir: (sortDir) => set({ sortDir }), addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })), closeTab: (index) => set((state) => { const newTabs = [...state.tabs]; @@ -180,6 +197,8 @@ export const useFeedStore = create()( auditStatus: state.auditStatus, searchAnomalyId: state.searchAnomalyId, isRectificationEnabled: state.isRectificationEnabled, + sortBy: state.sortBy, + sortDir: state.sortDir, }), } )