284 lines
11 KiB
TypeScript
284 lines
11 KiB
TypeScript
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 { 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
|
|
} = 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];
|
|
});
|
|
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
|
|
});
|
|
}
|
|
} 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
|
|
|
|
// Fetch data when selectedSite or auditStatus changes
|
|
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}`;
|
|
|
|
// CACHE HIT: This exact filter combo was already fetched — skip re-fetch on remount
|
|
if (filterKey === lastFetchedFilterKey) {
|
|
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 || '',
|
|
pageNo: 0,
|
|
pageSize: 20,
|
|
siteIds: siteIdParam,
|
|
isTrueFalse: auditStatus === 'False Audits' ? 'true' : 'both',
|
|
auto_audit: false,
|
|
isRectificationEnabled: false,
|
|
...(activeTab?.filters || {})
|
|
});
|
|
if (historyRes && historyRes.anomalies) {
|
|
useFeedStore.getState().updateTab(currentTabIndex, {
|
|
anomalies: historyRes.anomalies,
|
|
anomaliesCopy: historyRes.anomalies,
|
|
totalAnomalyCount: historyRes.count || historyRes.anomalies.length
|
|
});
|
|
setLastFetchedFilterKey(filterKey); // mark this combo as fetched
|
|
}
|
|
} catch (err) {
|
|
console.error('[GlobalFeed] Failed to load history for site/audit status 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
|
|
|
|
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;
|
|
|
|
try {
|
|
const historyRes: any = await activityFeedsService.getHistory({
|
|
date: activeTab?.date || '',
|
|
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 || {})
|
|
});
|
|
if (historyRes && historyRes.anomalies) {
|
|
updateTab(selectedTabIndex, {
|
|
anomalies: historyRes.anomalies,
|
|
anomaliesCopy: historyRes.anomalies,
|
|
totalAnomalyCount: historyRes.count || historyRes.anomalies.length,
|
|
page: newPage,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load history for page change", err);
|
|
}
|
|
};
|
|
|
|
const handleRowsPerPageChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
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;
|
|
|
|
try {
|
|
const historyRes: any = await activityFeedsService.getHistory({
|
|
date: activeTab?.date || '',
|
|
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 || {})
|
|
});
|
|
if (historyRes && historyRes.anomalies) {
|
|
updateTab(selectedTabIndex, {
|
|
anomalies: historyRes.anomalies,
|
|
anomaliesCopy: historyRes.anomalies,
|
|
totalAnomalyCount: historyRes.count || historyRes.anomalies.length,
|
|
page: newPage,
|
|
rowsPerPage: newRowsPerPage,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load history for rowsPerPage change", err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
|
|
<TopFilterBar />
|
|
<FeedTabs />
|
|
|
|
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden', bgcolor: 'background.default' }}>
|
|
{/* Left side Table */}
|
|
<Box sx={{ flex: 1, overflow: 'auto', px: 2, display: 'flex', flexDirection: 'column' }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', py: 0 }}>
|
|
<TablePagination
|
|
component="div"
|
|
count={tabs[selectedTabIndex]?.totalAnomalyCount || 0}
|
|
page={tabs[selectedTabIndex]?.page || 0}
|
|
onPageChange={handlePageChange}
|
|
rowsPerPage={tabs[selectedTabIndex]?.rowsPerPage || 20}
|
|
onRowsPerPageChange={handleRowsPerPageChange}
|
|
rowsPerPageOptions={[10, 20, 50, 100]}
|
|
/>
|
|
</Box>
|
|
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
|
<AnomalyTable />
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Right side Preview Panel */}
|
|
<PreviewPanel />
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|