From c7c3d46c627031dadd562e586e3d43cdf16b0d48 Mon Sep 17 00:00:00 2001 From: kaushik Date: Tue, 21 Jul 2026 16:04:31 +0530 Subject: [PATCH] audit session: zoomable images, 0-9 categories, prefilled notes, Next != Skip - images: scroll-wheel zoom toward the cursor + mouse-follow pan on both master and anomaly panes (new ZoomableImage); video keeps native controls - categories renumbered 0-9 on both lists (first item = 0) for display and keyboard shortcut, matching the legacy UI; keyboard resolves by key, not id - NOTES prefills from the record's existing Comment so the algorithm comment is visible/editable and no longer wiped on save - Enter/primary button is plain Next (record stays pending); only Skip/Tab marks Skipped, and Skipped records are excluded from Audit All feed table: FEATURES shows Audit_value; verdict pill replaces slider - FEATURES column now renders the record's Audit_value (was a nonexistent field showing N/A) - ANOMALY True/False column: colored pill (green True / red False / neutral when not yet audited) instead of the broken slider; gated on IsAudited so pending rows read neutral --- .env.production | 2 +- src/pages/activity-feeds/AnomalyTable.tsx | 35 ++-- src/pages/audit-session/AuditSession.tsx | 194 ++++++++++------------ src/pages/audit-session/ZoomableImage.tsx | 130 +++++++++++++++ 4 files changed, 236 insertions(+), 125 deletions(-) create mode 100644 src/pages/audit-session/ZoomableImage.tsx diff --git a/.env.production b/.env.production index 6d0c216..f9729d8 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,2 @@ # VITE_AUDIT_API_URL=https://node-audit.seekright.com -VITE_AUDIT_API_URL=https://psb7wrm5-7514.inc1.devtunnels.ms +VITE_AUDIT_API_URL=https://ms31kqw1-7514.inc1.devtunnels.ms diff --git a/src/pages/activity-feeds/AnomalyTable.tsx b/src/pages/activity-feeds/AnomalyTable.tsx index 807eee6..8029cb0 100644 --- a/src/pages/activity-feeds/AnomalyTable.tsx +++ b/src/pages/activity-feeds/AnomalyTable.tsx @@ -1,7 +1,24 @@ import React from 'react'; -import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Slider, Skeleton } from '@mui/material'; +import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Skeleton } from '@mui/material'; import { useFeedStore } from '../../store/feedStore'; +// Anomaly verdict. Audit_status is only a real verdict once a human has +// audited the record (IsAudited = 1) - on pending rows it holds the +// pipeline's pre-audit guess, so it must be ignored. Green True (1 → real +// anomaly), red False (0 → safe), neutral when not yet audited. +const VerdictPill: React.FC<{ isAudited: any; status: any }> = ({ isAudited, status }) => { + const decided = Number(isAudited) === 1; + const isTrue = decided && Number(status) === 1; + const label = !decided ? '—' : isTrue ? 'True' : 'False'; + const color = !decided ? '#64748b' : isTrue ? '#22c55e' : '#ef4444'; + const bg = !decided ? 'rgba(100,116,139,0.12)' : isTrue ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)'; + return ( + + {label} + + ); +}; + const SKELETON_COLS = 11; export const AnomalyTable: React.FC = () => { @@ -116,20 +133,10 @@ export const AnomalyTable: React.FC = () => { {assetName} {anomaly.Side || 'N/A'} - - - - + + - {anomaly.Features || 'N/A'} + {anomaly.Audit_value || '—'} {anomaly.Comments || '-'} ); diff --git a/src/pages/audit-session/AuditSession.tsx b/src/pages/audit-session/AuditSession.tsx index e3415e7..0ad2b3c 100644 --- a/src/pages/audit-session/AuditSession.tsx +++ b/src/pages/audit-session/AuditSession.tsx @@ -1,9 +1,8 @@ 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 { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions, 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 { ZoomableImage } from './ZoomableImage'; import SearchIcon from '@mui/icons-material/Search'; import { activityFeedsService } from '../../api/activityFeedsService'; import { axiosClient } from '../../api/axiosClient'; @@ -11,30 +10,33 @@ import { useToastStore } from '../../store/toastStore'; import { useAuthStore } from '../../store/authStore'; import { useFeedStore } from '../../store/feedStore'; +// key = displayed badge AND keyboard shortcut (0-9, first item = 0, matching +// the legacy UI). id stays the stable internal identity (1-10) that drives +// selection and the id-1 sub-menu logic - id and key deliberately diverge. const SAFE_CATEGORIES = [ - { id: 1, value: "not an anomaly", label: "Not an Anomaly", color: "#10b981", key: "1" }, - { id: 2, value: "vehicle occlusion", label: "Vehicle Occlusion", color: "#3b82f6", key: "2" }, - { id: 3, value: "low light condition", label: "Low Light Condition", color: "#6366f1", key: "3" }, - { id: 4, value: "duplicate", label: "Duplicate", color: "#a855f7", key: "4" }, - { id: 5, value: "gps issue", label: "GPS Issue", color: "#ec4899", key: "5" }, - { id: 6, value: "mismatch", label: "Mismatch", color: "#f43f5e", key: "6" }, - { id: 7, value: "bad image", label: "Bad Image", color: "#f97316", key: "7" }, - { id: 8, value: "patch", label: "Patch", color: "#eab308", key: "8" }, - { id: 9, value: "out of range", label: "Out of Range", color: "#84cc16", key: "9" }, - { id: 10, value: "false others", label: "False Others", color: "#64748b", key: "0" } + { id: 1, value: "not an anomaly", label: "Not an Anomaly", color: "#10b981", key: "0" }, + { id: 2, value: "vehicle occlusion", label: "Vehicle Occlusion", color: "#3b82f6", key: "1" }, + { id: 3, value: "low light condition", label: "Low Light Condition", color: "#6366f1", key: "2" }, + { id: 4, value: "duplicate", label: "Duplicate", color: "#a855f7", key: "3" }, + { id: 5, value: "gps issue", label: "GPS Issue", color: "#ec4899", key: "4" }, + { id: 6, value: "mismatch", label: "Mismatch", color: "#f43f5e", key: "5" }, + { id: 7, value: "bad image", label: "Bad Image", color: "#f97316", key: "6" }, + { id: 8, value: "patch", label: "Patch", color: "#eab308", key: "7" }, + { id: 9, value: "out of range", label: "Out of Range", color: "#84cc16", key: "8" }, + { id: 10, value: "false others", label: "False Others", color: "#64748b", key: "9" } ]; const ANOMALY_CATEGORIES = [ - { id: 1, value: "plant", label: "Plant", color: "#10b981", key: "1" }, - { id: 2, value: "bent", label: "Bent", color: "#3b82f6", key: "2" }, - { id: 3, value: "broken", label: "Broken", color: "#ef4444", key: "3" }, - { id: 4, value: "missing", label: "Missing", color: "#f97316", key: "4" }, - { id: 5, value: "plant overgrown", label: "Plant Overgrown", color: "#84cc16", key: "5" }, - { id: 6, value: "paint worn off", label: "Paint Worn Off", color: "#eab308", key: "6" }, - { id: 7, value: "dirt", label: "Dirt", color: "#78350f", key: "7" }, - { id: 8, value: "not working", label: "Not Working", color: "#ec4899", key: "8" }, - { id: 9, value: "others", label: "Others", color: "#64748b", key: "9" }, - { id: 10, value: "occlusion", label: "Occlusion", color: "#6366f1", key: "0" } + { id: 1, value: "plant", label: "Plant", color: "#10b981", key: "0" }, + { id: 2, value: "bent", label: "Bent", color: "#3b82f6", key: "1" }, + { id: 3, value: "broken", label: "Broken", color: "#ef4444", key: "2" }, + { id: 4, value: "missing", label: "Missing", color: "#f97316", key: "3" }, + { id: 5, value: "plant overgrown", label: "Plant Overgrown", color: "#84cc16", key: "4" }, + { id: 6, value: "paint worn off", label: "Paint Worn Off", color: "#eab308", key: "5" }, + { id: 7, value: "dirt", label: "Dirt", color: "#78350f", key: "6" }, + { id: 8, value: "not working", label: "Not Working", color: "#ec4899", key: "7" }, + { id: 9, value: "others", label: "Others", color: "#64748b", key: "8" }, + { id: 10, value: "occlusion", label: "Occlusion", color: "#6366f1", key: "9" } ]; // Rectification FALSE verdicts - the only two the legacy flow accepts. @@ -260,8 +262,6 @@ export const AuditSession: React.FC = () => { const [sessionPageNo, setSessionPageNo] = useState(() => location.state?.startPage || 0); const hasInitializedSession = useRef(false); - const [masterZoom, setMasterZoom] = useState(1); - const [anomalyZoom, setAnomalyZoom] = useState(1); const activeTab = tabs[selectedTabIndex] || { date: '', isRectificationTab: false, page: 0, rowsPerPage: 20 }; const activeDate = activeTab.date || ''; @@ -347,7 +347,7 @@ export const AuditSession: React.FC = () => { setCurrentIndex(0); setCarouselIndex(0); setSelectedCategory(null); - setNotes(''); + setNotes(res.anomalies[0]?.Comments || ''); setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); @@ -405,12 +405,10 @@ export const AuditSession: React.FC = () => { setCurrentIndex(0); setCarouselIndex(0); setSelectedCategory(null); - setNotes(''); + setNotes(res.anomalies[0]?.Comments || ''); setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); - setMasterZoom(1); - setAnomalyZoom(1); setStagedAudits({}); setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending')); } else { @@ -453,7 +451,7 @@ export const AuditSession: React.FC = () => { setCurrentIndex(location.state.startIndex || 0); setCarouselIndex(0); setSelectedCategory(null); - setNotes(''); + setNotes(location.state.anomalies[location.state.startIndex || 0]?.Comments || ''); setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); @@ -484,6 +482,19 @@ export const AuditSession: React.FC = () => { // itself. const isLocked = currentAnomaly?.IsAudited === 1 || itemAuditStates[currentIndex] === 'audited'; + // Seed the NOTES box with the record's existing Comment (the algorithm's + // "Type: … Area …") whenever the shown record changes, so it's visible and + // editable. Keyed on the record id so it doesn't clobber the auditor's + // typing (that doesn't change the id). Staged records keep their staged + // note; locked records render Comments directly via the textarea, so both + // are skipped here. + useEffect(() => { + if (!currentAnomaly || isLocked) return; + if (stagedAudits[currentAnomaly.id]) return; + setNotes(currentAnomaly.Comments || ''); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentAnomaly?.id]); + useEffect(() => { if (currentAnomaly?.images && currentAnomaly.images.length > 0) { const selectedIdx = currentAnomaly.images.findIndex((img: any) => img.selected); @@ -491,8 +502,6 @@ export const AuditSession: React.FC = () => { } else { setCarouselIndex(0); } - setMasterZoom(1); - setAnomalyZoom(1); }, [currentAnomaly]); let masterImageSrc = ''; @@ -565,13 +574,11 @@ export const AuditSession: React.FC = () => { setNotAnAnomalySubCategory(staged.notAnAnomalySub); } else { setSelectedCategory(null); - setNotes(''); + setNotes(list[idx]?.Comments || ''); setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); } - setMasterZoom(1); - setAnomalyZoom(1); }; const handleNext = () => { @@ -979,9 +986,10 @@ export const AuditSession: React.FC = () => { }; // Records "Audit All" would touch: everything except locked ones (already - // audited server-side or completed earlier this session). + // audited server-side or completed earlier this session) and records the + // auditor intentionally Skipped. const auditAllTargetCount = anomalies.reduce((n, a, i) => - n + ((a.IsAudited === 1 || itemAuditStates[i] === 'audited') ? 0 : 1), 0); + n + ((a.IsAudited === 1 || itemAuditStates[i] === 'audited' || itemAuditStates[i] === 'skipped') ? 0 : 1), 0); const isAuditAllVerdictValid = isRectActive ? (aaIsAnomaly || aaCategory !== null) @@ -1012,7 +1020,8 @@ export const AuditSession: React.FC = () => { const nextStates = [...itemAuditStates]; let stagedNow = 0; anomalies.forEach((a, i) => { - if (a.IsAudited === 1 || itemAuditStates[i] === 'audited') return; + // Skipped records were intentionally passed over - don't bulk-stage them. + if (a.IsAudited === 1 || itemAuditStates[i] === 'audited' || itemAuditStates[i] === 'skipped') return; nextStaged[a.id] = { ...verdict }; nextStates[i] = 'staged'; stagedNow++; @@ -1023,7 +1032,7 @@ export const AuditSession: React.FC = () => { // Keep the visible per-record form in sync when the current record was // part of the sweep, so what's on screen matches what got staged. const cur = anomalies[currentIndex]; - if (cur && nextStaged[cur.id] && !(cur.IsAudited === 1 || itemAuditStates[currentIndex] === 'audited')) { + if (cur && nextStaged[cur.id] && !(cur.IsAudited === 1 || itemAuditStates[currentIndex] === 'audited' || itemAuditStates[currentIndex] === 'skipped')) { setIsAnomaly(verdict.isAnomaly); setSelectedCategory(verdict.category); setNotes(verdict.notes); @@ -1168,7 +1177,7 @@ export const AuditSession: React.FC = () => { return next; }); setSelectedCategory(null); - setNotes(''); + setNotes(currentAnomaly?.Comments || ''); setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); @@ -1232,7 +1241,7 @@ export const AuditSession: React.FC = () => { if (currentIndex < anomalies.length - 1) { setCurrentIndex(prev => prev + 1); setSelectedCategory(null); - setNotes(''); + setNotes(anomalies[currentIndex + 1]?.Comments || ''); setIsAnomaly(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); @@ -1300,26 +1309,25 @@ export const AuditSession: React.FC = () => { case '8': case '9': case '0': { - const keyMap: { [key: string]: number } = { - '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, - '6': 6, '7': 7, '8': 8, '9': 9, '0': 10 - }; - const catId = keyMap[key]; - // Rectification: TRUE has no categories (digits ignored); FALSE only - // accepts 1/2 (true others / false others). Nested sub-category - // handling below never applies in rect mode. + // accepts its two options. Nested sub-category handling below never + // applies in rect mode. if (isRectActive) { - if (isAnomaly === false && (key === '1' || key === '2')) { - setSelectedCategory(catId); - if (isQuickAudit && !isBulkMode) { - setTimeout(() => handleQuickSaveAndNext(false, catId), 120); + if (isAnomaly === false) { + const c = RECT_SAFE_CATEGORIES.find(x => x.key === key); + if (c) { + setSelectedCategory(c.id); + if (isQuickAudit && !isBulkMode) { + setTimeout(() => handleQuickSaveAndNext(false, c.id), 120); + } } } break; } - // Special nested case: Plant is already active (under Anomaly) + // Special nested case: Plant is already active (under Anomaly). + // Sub-menus keep literal keys 1/2 and take precedence over the + // general category lookup below. if (isAnomaly === true && selectedCategory === 1) { if (key === '1') { setPlantSubCategory('in grown'); @@ -1355,13 +1363,20 @@ export const AuditSession: React.FC = () => { } } + // General category select: resolve the pressed key to a category via + // the active list's `key` (0-9), since id and key now diverge. + const activeList = isAnomaly === false ? SAFE_CATEGORIES : ANOMALY_CATEGORIES; + const matched = activeList.find(x => x.key === key); + if (!matched) break; + const catId = matched.id; + const currentIsAnomaly = isAnomaly === null ? true : isAnomaly; if (isAnomaly === null) { setIsAnomaly(true); } setSelectedCategory(catId); - // If selecting Plant (Anomaly 1) or Not an Anomaly (Safe 1), do not auto-advance; wait for nested sub-options + // Plant (Anomaly id 1) / Not an Anomaly (Safe id 1): do not auto-advance; wait for nested sub-options if (catId === 1) { break; } @@ -1375,7 +1390,9 @@ export const AuditSession: React.FC = () => { if (selectedCategory !== null || (isRectActive && isAnomaly === true)) { handleSaveAndNext(); } else { - handleSkip(); + // No verdict: plain Next (advance without marking Skipped - only the + // explicit Skip button / Tab marks a record Skipped). + handleNext(); } break; case 'arrowright': @@ -1612,32 +1629,9 @@ export const AuditSession: React.FC = () => { MASTER IMAGE - - - setMasterZoom(z => z + 0.5)} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}> - - - setMasterZoom(z => Math.max(1, z - 0.5))} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}> - - - + {masterImageSrc ? ( - Master { - if (!e.currentTarget.src.includes('master_image_not_available.png')) { - e.currentTarget.src = '/assets/images/master_image_not_available.png'; - } - }} - /> + ) : ( No Master Image )} @@ -1650,40 +1644,20 @@ export const AuditSession: React.FC = () => { ANOMALY IMAGE {currentAnomaly?.date_of_audit || currentAnomaly?.Created_on} - - - setAnomalyZoom(z => z + 0.5)} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}> - - - setAnomalyZoom(z => Math.max(1, z - 0.5))} sx={{ bgcolor: 'rgba(0,0,0,0.5)', color: 'white', '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }}> - - - + {mediaItems.length > 0 ? ( <> {mediaItems[carouselIndex]?.type === 'video' ? (