|
|
|
|
@@ -1,9 +1,10 @@
|
|
|
|
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
|
|
|
import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Select, MenuItem, TextField, CircularProgress } from '@mui/material';
|
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
|
|
|
import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Select, MenuItem, TextField, CircularProgress, InputAdornment, ListItemButton } 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 SearchIcon from '@mui/icons-material/Search';
|
|
|
|
|
import { activityFeedsService } from '../../api/activityFeedsService';
|
|
|
|
|
import { axiosClient } from '../../api/axiosClient';
|
|
|
|
|
import { useToastStore } from '../../store/toastStore';
|
|
|
|
|
@@ -128,6 +129,14 @@ export const AuditSession: React.FC = () => {
|
|
|
|
|
const [reportMessage, setReportMessage] = useState('');
|
|
|
|
|
const [reportSending, setReportSending] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Edit Asset: corrects a misclassified record's asset (metadata edit, not
|
|
|
|
|
// an audit verdict). Picker reads the startup-loaded asset catalog from
|
|
|
|
|
// the feed store; the backend resolves the canonical name from asset_id.
|
|
|
|
|
const [isEditAssetOpen, setIsEditAssetOpen] = useState(false);
|
|
|
|
|
const [editAssetSearch, setEditAssetSearch] = useState('');
|
|
|
|
|
const [editAssetId, setEditAssetId] = useState<number | null>(null);
|
|
|
|
|
const [editAssetSaving, setEditAssetSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
const DEVS = ['aromal', 'divya', 'Poonam', 'ravi', 'Kaushik', 'Saleth', 'Jagan', 'Yash', 'faraz_siddiqui', 'brinda.n', 'SasiVardhan', 'Shubham', 'sohail'];
|
|
|
|
|
|
|
|
|
|
const handleSendReport = async () => {
|
|
|
|
|
@@ -199,7 +208,8 @@ export const AuditSession: React.FC = () => {
|
|
|
|
|
selectedSite, filterFromDate, filterTillDate,
|
|
|
|
|
filterMinChainage, filterMaxChainage, selectedAssetIds,
|
|
|
|
|
searchAnomalyId, auditStatus, isRectificationEnabled, filterPlaza,
|
|
|
|
|
setDashboardCache, sortBy, sortDir, setSortBy, setSortDir
|
|
|
|
|
setDashboardCache, sortBy, sortDir, setSortBy, setSortDir,
|
|
|
|
|
assetTypes
|
|
|
|
|
} = useFeedStore();
|
|
|
|
|
|
|
|
|
|
// Call this before every navigate('/dashboard') so the dashboard re-fetches fresh stats
|
|
|
|
|
@@ -221,6 +231,53 @@ export const AuditSession: React.FC = () => {
|
|
|
|
|
const activeDate = activeTab.date || '';
|
|
|
|
|
const isRectActive = isRectificationEnabled || activeTab.isRectificationTab;
|
|
|
|
|
|
|
|
|
|
// Same search-filter shape as AssetMultiSelect: filter each group's
|
|
|
|
|
// assets by name, drop groups that end up empty.
|
|
|
|
|
const filteredAssetTypes = useMemo(() => {
|
|
|
|
|
if (!editAssetSearch.trim()) return assetTypes;
|
|
|
|
|
const q = editAssetSearch.toLowerCase();
|
|
|
|
|
return assetTypes
|
|
|
|
|
.map((type: any) => ({
|
|
|
|
|
...type,
|
|
|
|
|
assets: type.assets.filter((a: any) => a.asset_name.toLowerCase().includes(q)),
|
|
|
|
|
}))
|
|
|
|
|
.filter((type: any) => type.assets.length > 0);
|
|
|
|
|
}, [assetTypes, editAssetSearch]);
|
|
|
|
|
|
|
|
|
|
const handleSaveAssetEdit = async () => {
|
|
|
|
|
if (!currentAnomaly || editAssetId === null || editAssetId === currentAnomaly.asset_id) return;
|
|
|
|
|
const toast = useToastStore.getState().showToast;
|
|
|
|
|
setEditAssetSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await activityFeedsService.updateAnomalyAsset(currentAnomaly.id, editAssetId, isRectActive);
|
|
|
|
|
const patchOne = (a: any) =>
|
|
|
|
|
a.id === res.id ? { ...a, Asset: res.asset_name, asset_id: res.asset_id } : a;
|
|
|
|
|
// Session state first (drives the metadata chip immediately), then the
|
|
|
|
|
// persisted feed-tab caches - both anomalies AND anomaliesCopy, and only
|
|
|
|
|
// tabs of the same mode: audits and rectification ids come from
|
|
|
|
|
// different tables and can collide (same rule as applyAuditedLocally).
|
|
|
|
|
setAnomalies((prev: any[]) => prev.map(patchOne));
|
|
|
|
|
const feed = useFeedStore.getState();
|
|
|
|
|
feed.setTabs(feed.tabs.map((t: any) =>
|
|
|
|
|
!!t.isRectificationTab === isRectActive
|
|
|
|
|
? { ...t, anomalies: (t.anomalies || []).map(patchOne), anomaliesCopy: (t.anomaliesCopy || []).map(patchOne) }
|
|
|
|
|
: t
|
|
|
|
|
));
|
|
|
|
|
setIsEditAssetOpen(false);
|
|
|
|
|
setEditAssetId(null);
|
|
|
|
|
setEditAssetSearch('');
|
|
|
|
|
toast(`#${res.id}: asset changed to "${res.asset_name.replace(/_/g, ' ')}"`, 'success');
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
if (err?.response?.status === 409) {
|
|
|
|
|
toast(`#${currentAnomaly.id} is already audited — asset is locked.`, 'warning');
|
|
|
|
|
} else {
|
|
|
|
|
toast(`Failed to update asset — ${backendMsg(err)}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setEditAssetSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadSessionData = useCallback(async (customPageNo?: number) => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
@@ -1052,7 +1109,7 @@ export const AuditSession: React.FC = () => {
|
|
|
|
|
}, [currentIndex, anomalies.length, currentAnomaly, isLocked, notes, saveAuditState]);
|
|
|
|
|
// Keyboard shortcuts
|
|
|
|
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
|
|
|
if (isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen || isBulkConfirmOpen || isAuditAllOpen || bulkResult !== null) return;
|
|
|
|
|
if (isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen || isBulkConfirmOpen || isAuditAllOpen || isEditAssetOpen || bulkResult !== null) return;
|
|
|
|
|
|
|
|
|
|
const key = e.key.toLowerCase();
|
|
|
|
|
|
|
|
|
|
@@ -1182,7 +1239,7 @@ export const AuditSession: React.FC = () => {
|
|
|
|
|
handleSkip();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}, [currentIndex, anomalies, isLocked, isAnomaly, selectedCategory, notes, plantSubCategory, isQuickAudit, isBulkMode, isNotesFocused, isReportDialogOpen, isShortcutsDialogOpen, isBulkConfirmOpen, isAuditAllOpen, bulkResult, showBatchEndPanel, stagedCount, handleNextBatch, handleLeaveSession, handleQuickSaveAndNext, handleSaveAndNext, handleSkip, handleNext]);
|
|
|
|
|
}, [currentIndex, anomalies, isLocked, isAnomaly, selectedCategory, notes, plantSubCategory, isQuickAudit, isBulkMode, isNotesFocused, isReportDialogOpen, isShortcutsDialogOpen, isBulkConfirmOpen, isAuditAllOpen, isEditAssetOpen, bulkResult, showBatchEndPanel, stagedCount, handleNextBatch, handleLeaveSession, handleQuickSaveAndNext, handleSaveAndNext, handleSkip, handleNext]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
@@ -1524,7 +1581,16 @@ export const AuditSession: React.FC = () => {
|
|
|
|
|
<Chip size="small" label={`Side: ${currentAnomaly?.Side || 'N/A'}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
|
|
|
|
|
<Chip size="small" label={`${latStr}${longStr ? `, ${longStr}` : ''}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
|
|
|
|
<Button
|
|
|
|
|
size="small"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
disabled={isLocked}
|
|
|
|
|
onClick={() => { setEditAssetId(null); setEditAssetSearch(''); setIsEditAssetOpen(true); }}
|
|
|
|
|
sx={{ borderColor: '#3b82f6', color: '#60a5fa', textTransform: 'none', fontSize: '0.75rem', borderRadius: 1.5, '&:hover': { bgcolor: 'rgba(59,130,246,0.1)', borderColor: '#3b82f6' }, '&.Mui-disabled': { borderColor: '#334155', color: '#475569' } }}
|
|
|
|
|
>
|
|
|
|
|
Edit Asset
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="small"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
@@ -1908,9 +1974,9 @@ export const AuditSession: React.FC = () => {
|
|
|
|
|
{/* Footer Shortcuts */}
|
|
|
|
|
<Box sx={{ borderTop: '1px solid #1e293b', p: 1, px: 2, display: 'flex', gap: 3, alignItems: 'center', overflowX: 'auto', '& > div': { display: 'flex', alignItems: 'center', gap: 1, whiteSpace: 'nowrap' } }}>
|
|
|
|
|
<Typography sx={{ fontSize: '0.75rem', color: '#64748b' }}>
|
|
|
|
|
{(isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen) ? "Shortcuts Paused" : "Keys:"}
|
|
|
|
|
{(isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen || isEditAssetOpen) ? "Shortcuts Paused" : "Keys:"}
|
|
|
|
|
</Typography>
|
|
|
|
|
{!(isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen) && (
|
|
|
|
|
{!(isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen || isEditAssetOpen) && (
|
|
|
|
|
<>
|
|
|
|
|
<Box>
|
|
|
|
|
<Chip label="A" size="small" sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#1e293b', color: '#cbd5e1' }} />
|
|
|
|
|
@@ -2015,6 +2081,77 @@ export const AuditSession: React.FC = () => {
|
|
|
|
|
</DialogActions>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
{/* Edit Asset Dialog */}
|
|
|
|
|
<Dialog
|
|
|
|
|
open={isEditAssetOpen}
|
|
|
|
|
onClose={() => { setIsEditAssetOpen(false); setEditAssetId(null); setEditAssetSearch(''); }}
|
|
|
|
|
sx={{ zIndex: 10000 }}
|
|
|
|
|
slotProps={{ paper: { sx: { bgcolor: '#0f172a', color: '#f8fafc', border: '1px solid #1e293b', minWidth: 400 } } }}
|
|
|
|
|
>
|
|
|
|
|
<DialogTitle sx={{ fontWeight: 'bold', pb: 1, borderBottom: '1px solid #1e293b' }}>
|
|
|
|
|
Edit Asset
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogContent sx={{ pt: 2, display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
|
|
|
|
<Box>
|
|
|
|
|
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5 }}>Anomaly</Typography>
|
|
|
|
|
<Typography variant="body2" sx={{ color: '#f8fafc' }}>
|
|
|
|
|
ID #{currentAnomaly?.id} — current: {currentAnomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown'}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
<TextField
|
|
|
|
|
placeholder="Search assets..."
|
|
|
|
|
variant="outlined"
|
|
|
|
|
size="small"
|
|
|
|
|
fullWidth
|
|
|
|
|
autoFocus
|
|
|
|
|
value={editAssetSearch}
|
|
|
|
|
onChange={(e) => setEditAssetSearch(e.target.value)}
|
|
|
|
|
sx={{ bgcolor: 'rgba(255,255,255,0.05)', borderRadius: 1, input: { color: '#f8fafc' }, '& fieldset': { borderColor: '#334155' } }}
|
|
|
|
|
slotProps={{ input: { startAdornment: <InputAdornment position="start"><SearchIcon sx={{ color: '#64748b' }} fontSize="small" /></InputAdornment> } }}
|
|
|
|
|
/>
|
|
|
|
|
<Box sx={{ maxHeight: 320, overflowY: 'auto', pr: 1 }}>
|
|
|
|
|
{filteredAssetTypes.length === 0 ? (
|
|
|
|
|
<Typography variant="body2" sx={{ color: '#64748b', py: 1 }}>No assets match your search.</Typography>
|
|
|
|
|
) : (
|
|
|
|
|
filteredAssetTypes.map((type: any) => (
|
|
|
|
|
<Box key={type.type_id ?? 'uncategorized'} sx={{ mb: 1 }}>
|
|
|
|
|
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#60a5fa', textTransform: 'uppercase', fontSize: '0.7rem', letterSpacing: 1, py: 0.5 }}>
|
|
|
|
|
{type.type_name || 'Uncategorized'}
|
|
|
|
|
</Typography>
|
|
|
|
|
{type.assets.map((asset: any) => (
|
|
|
|
|
<ListItemButton
|
|
|
|
|
key={asset.asset_id}
|
|
|
|
|
dense
|
|
|
|
|
selected={editAssetId === asset.asset_id}
|
|
|
|
|
onClick={() => setEditAssetId(asset.asset_id)}
|
|
|
|
|
sx={{ color: '#f8fafc', borderRadius: 1, py: 0.5, '&:hover': { bgcolor: '#1e293b' }, '&.Mui-selected': { bgcolor: '#1e3a5f' }, '&.Mui-selected:hover': { bgcolor: '#1e3a5f' } }}
|
|
|
|
|
>
|
|
|
|
|
{asset.asset_name.replace(/_/g, ' ')}
|
|
|
|
|
{asset.asset_id === currentAnomaly?.asset_id && (
|
|
|
|
|
<Typography component="span" sx={{ ml: 1, fontSize: '0.7rem', color: '#64748b' }}>(current)</Typography>
|
|
|
|
|
)}
|
|
|
|
|
</ListItemButton>
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
<DialogActions sx={{ borderTop: '1px solid #1e293b', px: 3, py: 1.5, gap: 1 }}>
|
|
|
|
|
<Button onClick={() => { setIsEditAssetOpen(false); setEditAssetId(null); setEditAssetSearch(''); }} sx={{ color: '#94a3b8', textTransform: 'none' }}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleSaveAssetEdit}
|
|
|
|
|
variant="contained"
|
|
|
|
|
disabled={editAssetId === null || editAssetId === currentAnomaly?.asset_id || editAssetSaving}
|
|
|
|
|
sx={{ bgcolor: '#3b82f6', color: '#fff', fontWeight: 'bold', textTransform: 'none', '&:hover': { bgcolor: '#2563eb' }, '&.Mui-disabled': { bgcolor: 'rgba(59,130,246,0.3)', color: 'rgba(255,255,255,0.4)' } }}
|
|
|
|
|
>
|
|
|
|
|
{editAssetSaving ? <CircularProgress size={18} sx={{ color: '#fff' }} /> : 'Save'}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogActions>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
{/* Keyboard Shortcuts Dialog */}
|
|
|
|
|
<Dialog
|
|
|
|
|
open={isShortcutsDialogOpen}
|
|
|
|
|
|