mvp-1
This commit is contained in:
@@ -6,9 +6,9 @@ export const activityFeedsService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
getHistoryDates: async (isRectificationEnabled: boolean) => {
|
getHistoryDates: async (isRectificationEnabled: boolean, siteIds?: string) => {
|
||||||
const response = await axiosClient.get('/api/audit/anomaly/get_history_dates', {
|
const response = await axiosClient.get('/api/audit/anomaly/get_history_dates', {
|
||||||
params: { isRectificationEnabled }
|
params: { isRectificationEnabled, siteIds: siteIds || '' }
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -112,22 +112,46 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
return `Asset: ${id}`;
|
return `Asset: ${id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [historyDates, setHistoryDates] = useState<any[]>([]);
|
const [historyDates, setHistoryDates] = useState<any[]>([]); // normal history (isRectificationEnabled=false)
|
||||||
|
const [rectHistoryDates, setRectHistoryDates] = useState<any[]>([]); // rectification history (isRectificationEnabled=true)
|
||||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||||
|
const [loadingRectHistory, setLoadingRectHistory] = useState(false);
|
||||||
const [isHistoryDialogOpen, setIsHistoryDialogOpen] = useState(false);
|
const [isHistoryDialogOpen, setIsHistoryDialogOpen] = useState(false);
|
||||||
const [selectedHistoryDate, setSelectedHistoryDate] = useState('2026-06-12');
|
const [selectedHistoryDate, setSelectedHistoryDate] = useState('2026-06-12');
|
||||||
|
const [historyDropdownValue, setHistoryDropdownValue] = useState('');
|
||||||
|
|
||||||
|
// Fetch normal history dates (isRectificationEnabled=false) for the History dropdown
|
||||||
|
// Rectification history (isRectificationEnabled=true) is fetched separately by the RECTIFICATION HISTORY button
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedSite || selectedSite.site_id === 'All Sites') return;
|
||||||
|
const fetchDates = async () => {
|
||||||
|
setLoadingHistory(true);
|
||||||
|
try {
|
||||||
|
const siteIds = String(selectedSite.site_id);
|
||||||
|
const dates: any = await activityFeedsService.getHistoryDates(false, siteIds);
|
||||||
|
setHistoryDates(Array.isArray(dates) ? dates : (dates?.responseData || []));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load history dates", err);
|
||||||
|
} finally {
|
||||||
|
setLoadingHistory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchDates();
|
||||||
|
}, [selectedSite?.site_id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const handleHistoryClick = async () => {
|
const handleHistoryClick = async () => {
|
||||||
setIsHistoryDialogOpen(true);
|
setIsHistoryDialogOpen(true);
|
||||||
setLoadingHistory(true);
|
if (!selectedSite || selectedSite.site_id === 'All Sites') return;
|
||||||
|
setLoadingRectHistory(true);
|
||||||
try {
|
try {
|
||||||
const dates: any = await activityFeedsService.getHistoryDates(true);
|
const siteIds = String(selectedSite.site_id);
|
||||||
setHistoryDates(Array.isArray(dates) ? dates : (dates?.responseData || []));
|
const dates: any = await activityFeedsService.getHistoryDates(true, siteIds);
|
||||||
|
setRectHistoryDates(Array.isArray(dates) ? dates : (dates?.responseData || []));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load history dates", err);
|
console.error("Failed to load rectification history dates", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingHistory(false);
|
setLoadingRectHistory(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -208,6 +232,48 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
>
|
>
|
||||||
BULK DELETE
|
BULK DELETE
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* History dropdown — only visible when a specific site is selected */}
|
||||||
|
{localSiteId !== 'All Sites' && <FormControl size="small" sx={{ bgcolor: 'rgba(255,255,255,0.1)', borderRadius: 2, minWidth: 160 }}>
|
||||||
|
<Select
|
||||||
|
value={historyDropdownValue}
|
||||||
|
displayEmpty
|
||||||
|
onChange={(e) => {
|
||||||
|
const date = e.target.value as string;
|
||||||
|
if (date) {
|
||||||
|
handleSelectDate(date);
|
||||||
|
setHistoryDropdownValue('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderValue={(val) => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
{loadingHistory && <CircularProgress size={12} sx={{ color: '#fff' }} />}
|
||||||
|
<span>{val || 'History'}</span>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
sx={{ '& .MuiSelect-select': { py: 1, px: 2 }, border: 'none', '& fieldset': { border: 'none' }, fontWeight: 'bold', color: '#fff', '& .MuiSvgIcon-root': { color: '#fff' } }}
|
||||||
|
MenuProps={{
|
||||||
|
slotProps: {
|
||||||
|
paper: {
|
||||||
|
sx: { bgcolor: '#0f172a', color: '#f8fafc', border: '1px solid #1e293b', maxHeight: 300 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem disabled value="">
|
||||||
|
<em style={{ color: '#64748b' }}>{loadingHistory ? 'Loading...' : historyDates.length === 0 ? 'No dates available' : 'Select a date'}</em>
|
||||||
|
</MenuItem>
|
||||||
|
{historyDates.map((d) => {
|
||||||
|
const dateStr = d.audited_on ? d.audited_on.split(' ')[0] : d.audited_on;
|
||||||
|
return (
|
||||||
|
<MenuItem key={dateStr} value={dateStr} sx={{ '&:hover': { bgcolor: '#1e293b' } }}>
|
||||||
|
{dateStr}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>}
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isHistoryDialogOpen}
|
open={isHistoryDialogOpen}
|
||||||
onClose={() => setIsHistoryDialogOpen(false)}
|
onClose={() => setIsHistoryDialogOpen(false)}
|
||||||
@@ -254,14 +320,14 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
Quick Select (Audited Dates)
|
Quick Select (Audited Dates)
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, maxHeight: 150, overflowY: 'auto', p: 0.5, border: '1px solid #1e293b', borderRadius: 1, bgcolor: '#0b1121' }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, maxHeight: 150, overflowY: 'auto', p: 0.5, border: '1px solid #1e293b', borderRadius: 1, bgcolor: '#0b1121' }}>
|
||||||
{loadingHistory ? (
|
{loadingRectHistory ? (
|
||||||
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'center', py: 2 }}>
|
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'center', py: 2 }}>
|
||||||
<CircularProgress size={20} color="inherit" />
|
<CircularProgress size={20} color="inherit" />
|
||||||
</Box>
|
</Box>
|
||||||
) : historyDates.length === 0 ? (
|
) : rectHistoryDates.length === 0 ? (
|
||||||
<Typography variant="body2" sx={{ color: '#64748b', p: 1 }}>No dates found</Typography>
|
<Typography variant="body2" sx={{ color: '#64748b', p: 1 }}>No dates found</Typography>
|
||||||
) : (
|
) : (
|
||||||
historyDates.map((d) => {
|
rectHistoryDates.map((d) => {
|
||||||
const dateStr = d.audited_on ? d.audited_on.split(' ')[0] : '';
|
const dateStr = d.audited_on ? d.audited_on.split(' ')[0] : '';
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
@@ -339,47 +405,49 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{/* Search ID Textfield */}
|
{/* Search ID and Search Plaza — only visible when a specific site is selected */}
|
||||||
<TextField
|
{localSiteId !== 'All Sites' && (
|
||||||
size="small"
|
<>
|
||||||
placeholder="Search ID..."
|
<TextField
|
||||||
value={localSearchId}
|
size="small"
|
||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
placeholder="Search ID..."
|
||||||
slotProps={{
|
value={localSearchId}
|
||||||
input: {
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
sx: {
|
slotProps={{
|
||||||
borderRadius: 2,
|
input: {
|
||||||
bgcolor: 'rgba(255,255,255,0.1)',
|
sx: {
|
||||||
color: 'white',
|
borderRadius: 2,
|
||||||
fontWeight: 'bold',
|
bgcolor: 'rgba(255,255,255,0.1)',
|
||||||
width: 150,
|
color: 'white',
|
||||||
'& fieldset': { border: 'none' },
|
fontWeight: 'bold',
|
||||||
'& .MuiInputBase-input': { py: 1, px: 2, '&::placeholder': { color: 'rgba(255,255,255,0.5)', opacity: 1 } }
|
width: 150,
|
||||||
}
|
'& fieldset': { border: 'none' },
|
||||||
}
|
'& .MuiInputBase-input': { py: 1, px: 2, '&::placeholder': { color: 'rgba(255,255,255,0.5)', opacity: 1 } }
|
||||||
}}
|
}
|
||||||
/>
|
}
|
||||||
|
}}
|
||||||
{/* Search Plaza Textfield */}
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="Search Plaza..."
|
placeholder="Search Plaza..."
|
||||||
value={localPlaza}
|
value={localPlaza}
|
||||||
onChange={(e) => handlePlazaChange(e.target.value)}
|
onChange={(e) => handlePlazaChange(e.target.value)}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
sx: {
|
sx: {
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
bgcolor: 'rgba(255,255,255,0.1)',
|
bgcolor: 'rgba(255,255,255,0.1)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
width: 150,
|
width: 150,
|
||||||
'& fieldset': { border: 'none' },
|
'& fieldset': { border: 'none' },
|
||||||
'& .MuiInputBase-input': { py: 1, px: 2, '&::placeholder': { color: 'rgba(255,255,255,0.5)', opacity: 1 } }
|
'& .MuiInputBase-input': { py: 1, px: 2, '&::placeholder': { color: 'rgba(255,255,255,0.5)', opacity: 1 } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
const [activeKeyFlash, setActiveKeyFlash] = useState<string | null>(null);
|
const [activeKeyFlash, setActiveKeyFlash] = useState<string | null>(null);
|
||||||
const [isNotesFocused, setIsNotesFocused] = useState(false);
|
const [isNotesFocused, setIsNotesFocused] = useState(false);
|
||||||
const [showCompletionModal, setShowCompletionModal] = useState(false);
|
const [showCompletionModal, setShowCompletionModal] = useState(false);
|
||||||
|
const [isShortcutsDialogOpen, setIsShortcutsDialogOpen] = useState(false);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const {
|
const {
|
||||||
@@ -630,7 +631,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<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" 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>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -1105,9 +1106,60 @@ export const AuditSession: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Keyboard Shortcuts Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={isShortcutsDialogOpen}
|
||||||
|
onClose={() => setIsShortcutsDialogOpen(false)}
|
||||||
|
sx={{ zIndex: 10000 }}
|
||||||
|
slotProps={{ paper: { sx: { bgcolor: '#0f172a', color: '#f8fafc', border: '1px solid #1e293b', minWidth: 420 } } }}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontWeight: 'bold', pb: 1, borderBottom: '1px solid #1e293b' }}>
|
||||||
|
Keyboard Shortcuts
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent sx={{ pt: 2 }}>
|
||||||
|
{[
|
||||||
|
{ section: 'Classification' },
|
||||||
|
{ key: 'A', desc: 'Mark as Anomaly' },
|
||||||
|
{ key: 'S', desc: 'Mark as Safe (Not an Anomaly)' },
|
||||||
|
{ section: 'Category Selection' },
|
||||||
|
{ key: '1 – 0', desc: 'Select category (0 = category 10)' },
|
||||||
|
{ section: 'Plant sub-categories (Anomaly → Category 1)' },
|
||||||
|
{ key: '1', desc: 'In Grown' },
|
||||||
|
{ key: '2', desc: 'Out Grown' },
|
||||||
|
{ section: 'Not-an-Anomaly sub-categories (Safe → Category 1)' },
|
||||||
|
{ key: '1', desc: 'Comparison Error' },
|
||||||
|
{ key: '2', desc: 'Low Light' },
|
||||||
|
{ key: '3', desc: 'Far Asset' },
|
||||||
|
{ key: '4', desc: 'No Detection' },
|
||||||
|
{ key: '5', desc: 'Image Mismatch' },
|
||||||
|
{ section: 'Navigation' },
|
||||||
|
{ key: '→', desc: 'Next item' },
|
||||||
|
{ key: '←', desc: 'Previous item' },
|
||||||
|
{ key: 'Tab', desc: 'Skip current item' },
|
||||||
|
{ key: 'Enter', desc: 'Save & Next (or Skip if no category)' },
|
||||||
|
].map((row, i) =>
|
||||||
|
'section' in row ? (
|
||||||
|
<Typography key={i} variant="caption" sx={{ display: 'block', color: '#3b82f6', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 0.8, mt: i === 0 ? 0 : 2, mb: 0.5 }}>
|
||||||
|
{row.section}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 0.5 }}>
|
||||||
|
<Chip label={row.key} size="small" sx={{ minWidth: 52, fontWeight: 'bold', bgcolor: '#1e293b', color: '#e2e8f0', border: '1px solid #334155', borderRadius: 1, fontSize: '0.75rem' }} />
|
||||||
|
<Typography variant="body2" sx={{ color: '#94a3b8' }}>{row.desc}</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ borderTop: '1px solid #1e293b', px: 3, py: 1.5 }}>
|
||||||
|
<Button onClick={() => setIsShortcutsDialogOpen(false)} sx={{ color: '#94a3b8', textTransform: 'none', fontWeight: 'bold' }}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Session Completion In-place Modal */}
|
{/* Session Completion In-place Modal */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showCompletionModal}
|
open={showCompletionModal}
|
||||||
onClose={() => setShowCompletionModal(false)}
|
onClose={() => setShowCompletionModal(false)}
|
||||||
sx={{ zIndex: 10000 }}
|
sx={{ zIndex: 10000 }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
|||||||
Reference in New Issue
Block a user