From ee25bb6065ab5658ac5eaf8351d8af099b5040c3 Mon Sep 17 00:00:00 2001 From: kaushik Date: Fri, 17 Jul 2026 11:27:36 +0530 Subject: [PATCH] rectification audit options: TRUE auto-"Others", FALSE limited to true/false others - rect TRUE: no category menu, verdict saved as "Others" (legacy behavior); Save/Enter work without a category, A auto-saves in Quick mode - rect FALSE: only "true others" / "false others" (keys 1-2); audits safe menu no longer selectable in rect mode (closes the "duplicate" soft-delete trap) - FALSE + "true others" now calls POST /auditor/rectified/false to re-open the defect (asset DAMAGED) - wired into single save, Bulk Mode phase 2b, and Audit All; per-record errors surfaced in toasts - audits mode untouched (all changes gated on isRectActive) --- src/api/activityFeedsService.ts | 9 + src/pages/audit-session/AuditSession.tsx | 210 +++++++++++++++++++---- 2 files changed, 184 insertions(+), 35 deletions(-) diff --git a/src/api/activityFeedsService.ts b/src/api/activityFeedsService.ts index 328f6a6..3a9ad8e 100644 --- a/src/api/activityFeedsService.ts +++ b/src/api/activityFeedsService.ts @@ -84,6 +84,15 @@ export const activityFeedsService = { return response; }, + // Called after a rectification FALSE + 'true others' audit - the defect is + // NOT fixed, so this re-opens/creates it in the work-order layer with + // asset status DAMAGED. Backend only accepts rows already saved with + // Audit_value = 'true others' (exact lowercase). + rectifiedFalse: async (payload: { site_id: number; anomaly_id: number[] }) => { + const response = await axiosClient.post('/api/audit/auditor/rectified/false', payload); + return response; + }, + getOrganizationSites: async (org_id: string) => { const response = await axiosClient.get('/api/audit/Master/site', { params: { org_id } diff --git a/src/pages/audit-session/AuditSession.tsx b/src/pages/audit-session/AuditSession.tsx index 58f9a29..e3415e7 100644 --- a/src/pages/audit-session/AuditSession.tsx +++ b/src/pages/audit-session/AuditSession.tsx @@ -37,6 +37,14 @@ const ANOMALY_CATEGORIES = [ { id: 10, value: "occlusion", label: "Occlusion", color: "#6366f1", key: "0" } ]; +// Rectification FALSE verdicts - the only two the legacy flow accepts. +// 'true others' (exact lowercase) is what POST /auditor/rectified/false +// matches on; anything else would silently skip the defect re-opening. +const RECT_SAFE_CATEGORIES = [ + { id: 1, value: "true others", label: "True Others", color: "#f97316", key: "1" }, + { id: 2, value: "false others", label: "False Others", color: "#64748b", key: "2" }, +]; + // The backend soft-deletes (deleted = 1) for these exact Audit_values - // mirror of the list in anomalyModel.update. Note the UI's "low light // condition" does NOT match the backend's "low light", so from this @@ -49,7 +57,7 @@ type NotAnAnomalySub = 'comparison error' | 'low light' | 'far asset' | 'no dete // A verdict staged in Bulk Mode - held in memory until Bulk Submit. interface StagedVerdict { isAnomaly: boolean; - category: number; + category: number | null; notes: string; plantSub: PlantSub | null; notAnAnomalySub: NotAnAnomalySub | null; @@ -61,12 +69,20 @@ const backendMsg = (err: any) => // Same Audit_value resolution the single-record save uses: category value, // overridden by the nested sub-category for category 1 (Plant / Not an Anomaly). +// Rectification mode has its own legacy vocabulary: TRUE (rectified) is +// always 'Others' with no category menu; FALSE picks from the two +// RECT_SAFE_CATEGORIES options. const computeAuditValue = ( isAnomalyBool: boolean, catVal: number | null, plantSub?: string | null, - notAnAnomalySub?: string | null + notAnAnomalySub?: string | null, + isRect = false ): string => { + if (isRect) { + if (isAnomalyBool) return 'Others'; + return RECT_SAFE_CATEGORIES.find(c => c.id === catVal)?.value || ''; + } const categories = isAnomalyBool ? ANOMALY_CATEGORIES : SAFE_CATEGORIES; const matchedCategory = categories.find(c => c.id === catVal); let auditValueVal = matchedCategory ? matchedCategory.value : ''; @@ -633,7 +649,7 @@ export const AuditSession: React.FC = () => { const toast = useToastStore.getState().showToast; const isAnomalyBool = anomalyVal ?? true; - const auditValueVal = computeAuditValue(isAnomalyBool, catVal, plantSub, notAnAnomalySub); + const auditValueVal = computeAuditValue(isAnomalyBool, catVal, plantSub, notAnAnomalySub, isRectActive); // Body matches exactly what the API expects (dbName + isRectificationEnabled go in URL params via axiosClient) const auditPayload = { @@ -677,6 +693,37 @@ export const AuditSession: React.FC = () => { const softDeleted = SOFT_DELETE_AUDIT_VALUES.includes(auditValueVal); if (!isAnomalyBool) { + // Rectification "true others" = the defect is NOT fixed - re-open it + // in the work-order layer (asset DAMAGED). The audit itself already + // saved, so failures here inform but never roll back. + if (isRectActive && auditValueVal === 'true others') { + try { + const res: any = await activityFeedsService.rectifiedFalse({ + site_id: currentAnomaly.site_id, + anomaly_id: [recordId], + }); + const ignored = res?.data?.ignoredAnomalies?.length > 0; + toast( + ignored + ? `#${recordId} saved as "true others" — duplicate, defect already open` + : `#${recordId} saved as "true others" — defect re-opened for rectification`, + 'success' + ); + } catch (err: any) { + // The 404 body still carries per-record errorMessages when the + // record WAS eligible but processing failed (e.g. media copy). + const perRecordError = err?.response?.data?.data?.errorMessages?.[recordId]; + if (perRecordError) { + toast(`#${recordId} saved, but defect re-opening failed — ${perRecordError}`, 'error'); + } else if (err?.response?.status === 404) { + toast(`#${recordId} saved, but not eligible for defect re-opening (already processed or non linear/fixed algorithm)`, 'warning'); + } else { + toast(`#${recordId} saved, but defect re-opening failed — ${backendMsg(err)}`, 'error'); + } + } + markLocalAudited(); + return true; + } toast( softDeleted ? `#${recordId} saved as "${auditValueVal}" — record soft-deleted` @@ -732,7 +779,7 @@ export const AuditSession: React.FC = () => { // Build the exact POST /anomaly body the single-record flow sends. const buildAuditPayload = (rec: any, v: StagedVerdict) => ({ - Audit_value: computeAuditValue(v.isAnomaly, v.category, v.plantSub, v.notAnAnomalySub), + Audit_value: computeAuditValue(v.isAnomaly, v.category, v.plantSub, v.notAnAnomalySub, isRectActive), Comments: v.notes || '', Audit_status: v.isAnomaly, // boolean true/false, not 1/0 Frame_Test: rec.Frame_Test || '', @@ -748,7 +795,7 @@ export const AuditSession: React.FC = () => { const successOf = (e: { rec: any; v: StagedVerdict }) => ({ id: e.rec.id as number, isAnomaly: e.v.isAnomaly, - auditValue: computeAuditValue(e.v.isAnomaly, e.v.category, e.v.plantSub, e.v.notAnAnomalySub), + auditValue: computeAuditValue(e.v.isAnomaly, e.v.category, e.v.plantSub, e.v.notAnAnomalySub, isRectActive), comments: e.v.notes || '', }); @@ -793,6 +840,10 @@ export const AuditSession: React.FC = () => { // had saved earlier ("locked", the same 409 retry path the single-record // flow uses) - continue to anomaly processing; safe verdicts are final. const toProcessBySite = new Map(); + // Rectification FALSE + "true others" = the defect is NOT fixed: those + // records need a second call too (/auditor/rectified/false re-opens the + // defect in the work-order layer), grouped per site like phase 2. + const toRectifyFalseBySite = new Map(); for (const e of entries) { const r = resultById.get(Number(e.rec.id)); const ok = r?.ok === true; @@ -805,6 +856,14 @@ export const AuditSession: React.FC = () => { } else { failures.push({ id: e.rec.id, reason: r?.error || 'saving audit failed' }); } + } else if (isRectActive && computeAuditValue(false, e.v.category, e.v.plantSub, e.v.notAnAnomalySub, true) === 'true others') { + if (ok || locked) { + const key = String(e.rec.site_id); + if (!toRectifyFalseBySite.has(key)) toRectifyFalseBySite.set(key, []); + toRectifyFalseBySite.get(key)!.push(e); + } else { + failures.push({ id: e.rec.id, reason: r?.error || 'saving audit failed' }); + } } else if (ok) { succeeded.push(successOf(e)); } else { @@ -841,6 +900,45 @@ export const AuditSession: React.FC = () => { } } + // Phase 2b (rectification only): re-open the defects for FALSE + + // "true others" verdicts, one call per site. + for (const [, group] of toRectifyFalseBySite) { + const ids = group.map(e => e.rec.id); + try { + const res: any = await activityFeedsService.rectifiedFalse({ + site_id: group[0].rec.site_id, + anomaly_id: ids, + }); + const errMsgs = res?.data?.errorMessages || {}; + const notFound = new Set((res?.data?.notFound || []).map(Number)); + for (const e of group) { + const id = Number(e.rec.id); + if (errMsgs[id]) { + failures.push({ id: e.rec.id, reason: `audit saved, but defect re-opening failed — ${errMsgs[id]}` }); + } else if (notFound.has(id)) { + // Not eligible (already processed / non linear-fixed) - the + // audit itself stands, same as the single-record flow. + succeeded.push(successOf(e)); + } else { + succeeded.push(successOf(e)); + } + } + } catch (err: any) { + // A 404 body still carries per-record errorMessages when records + // WERE eligible but processing failed - split accordingly. + const errMsgs = err?.response?.data?.data?.errorMessages || {}; + if (err?.response?.status === 404) { + for (const e of group) { + const msg = errMsgs[Number(e.rec.id)]; + if (msg) failures.push({ id: e.rec.id, reason: `audit saved, but defect re-opening failed — ${msg}` }); + else succeeded.push(successOf(e)); + } + } else { + group.forEach(e => failures.push({ id: e.rec.id, reason: `audit saved, but defect re-opening failed — ${backendMsg(err)}` })); + } + } + } + // Phase 3: apply successes locally; failures STAY STAGED so a retry only // re-sends what actually failed. applyAuditedLocally(succeeded); @@ -862,12 +960,12 @@ export const AuditSession: React.FC = () => { let anomalyCnt = 0, safeCnt = 0, softDeleteCnt = 0; Object.values(stagedAudits).forEach(v => { if (v.isAnomaly) anomalyCnt++; else safeCnt++; - if (SOFT_DELETE_AUDIT_VALUES.includes(computeAuditValue(v.isAnomaly, v.category, v.plantSub, v.notAnAnomalySub))) { + if (SOFT_DELETE_AUDIT_VALUES.includes(computeAuditValue(v.isAnomaly, v.category, v.plantSub, v.notAnAnomalySub, isRectActive))) { softDeleteCnt++; } }); return { anomalies: anomalyCnt, safe: safeCnt, softDeletes: softDeleteCnt }; - }, [stagedAudits]); + }, [stagedAudits, isRectActive]); const handleToggleBulkMode = (val: boolean) => { if (!val && stagedCount > 0) { @@ -885,9 +983,10 @@ export const AuditSession: React.FC = () => { const auditAllTargetCount = anomalies.reduce((n, a, i) => n + ((a.IsAudited === 1 || itemAuditStates[i] === 'audited') ? 0 : 1), 0); - const isAuditAllVerdictValid = - aaCategory !== null && - (aaCategory !== 1 || (aaIsAnomaly ? aaPlantSub !== null : aaNotAnAnomalySub !== null)); + const isAuditAllVerdictValid = isRectActive + ? (aaIsAnomaly || aaCategory !== null) + : aaCategory !== null && + (aaCategory !== 1 || (aaIsAnomaly ? aaPlantSub !== null : aaNotAnAnomalySub !== null)); const openAuditAllDialog = () => { setAaIsAnomaly(true); @@ -899,7 +998,8 @@ export const AuditSession: React.FC = () => { }; const handleAuditAllApply = () => { - if (!isAuditAllVerdictValid || aaCategory === null) return; + if (!isAuditAllVerdictValid) return; + if (aaCategory === null && !(isRectActive && aaIsAnomaly)) return; const verdict: StagedVerdict = { isAnomaly: aaIsAnomaly, category: aaCategory, @@ -933,7 +1033,7 @@ export const AuditSession: React.FC = () => { setIsAuditAllOpen(false); useToastStore.getState().showToast( - `Staged ${stagedNow} record${stagedNow === 1 ? '' : 's'} as "${computeAuditValue(verdict.isAnomaly, verdict.category, verdict.plantSub, verdict.notAnAnomalySub)}" — review any tile, then press Bulk Submit.`, + `Staged ${stagedNow} record${stagedNow === 1 ? '' : 's'} as "${computeAuditValue(verdict.isAnomaly, verdict.category, verdict.plantSub, verdict.notAnAnomalySub, isRectActive)}" — review any tile, then press Bulk Submit.`, 'success' ); }; @@ -1008,6 +1108,8 @@ export const AuditSession: React.FC = () => { // 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 => { + // Rectification vocabulary has no nested sub-categories. + if (isRectActive) return true; if (selectedCategory !== 1) return true; if ((isAnomaly === null || isAnomaly === true) && !plantSubCategory) { useToastStore.getState().showToast('Please select a feature before submitting.', 'warning'); @@ -1029,7 +1131,7 @@ export const AuditSession: React.FC = () => { handleNext(); return; } - if (selectedCategory === null) { + if (selectedCategory === null && !(isRectActive && isAnomaly === true)) { handleSkip(); return; } @@ -1106,7 +1208,7 @@ export const AuditSession: React.FC = () => { // Quick Auto Advance save trigger const handleQuickSaveAndNext = useCallback(async ( anomalyVal: boolean, - catVal: number, + catVal: number | null, plantSub?: string | null, notAnAnomalySub?: string | null ) => { @@ -1177,6 +1279,10 @@ export const AuditSession: React.FC = () => { setSelectedCategory(null); setPlantSubCategory(null); setNotAnAnomalySubCategory(null); + // Rectification TRUE has no category step - Quick mode saves now. + if (isRectActive && isQuickAudit && !isBulkMode) { + setTimeout(() => handleQuickSaveAndNext(true, null), 120); + } break; case 's': setIsAnomaly(false); @@ -1200,6 +1306,19 @@ export const AuditSession: React.FC = () => { }; 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. + if (isRectActive) { + if (isAnomaly === false && (key === '1' || key === '2')) { + setSelectedCategory(catId); + if (isQuickAudit && !isBulkMode) { + setTimeout(() => handleQuickSaveAndNext(false, catId), 120); + } + } + break; + } + // Special nested case: Plant is already active (under Anomaly) if (isAnomaly === true && selectedCategory === 1) { if (key === '1') { @@ -1253,7 +1372,7 @@ export const AuditSession: React.FC = () => { break; } case 'enter': - if (selectedCategory !== null) { + if (selectedCategory !== null || (isRectActive && isAnomaly === true)) { handleSaveAndNext(); } else { handleSkip(); @@ -1270,7 +1389,7 @@ export const AuditSession: React.FC = () => { handleSkip(); break; } - }, [currentIndex, anomalies, isLocked, isAnomaly, selectedCategory, notes, plantSubCategory, isQuickAudit, isBulkMode, isNotesFocused, isReportDialogOpen, isShortcutsDialogOpen, isBulkConfirmOpen, isAuditAllOpen, isEditAssetOpen, bulkResult, showBatchEndPanel, stagedCount, handleNextBatch, handleLeaveSession, handleQuickSaveAndNext, handleSaveAndNext, handleSkip, handleNext]); + }, [currentIndex, anomalies, isLocked, isAnomaly, selectedCategory, notes, plantSubCategory, isQuickAudit, isBulkMode, isRectActive, isNotesFocused, isReportDialogOpen, isShortcutsDialogOpen, isBulkConfirmOpen, isAuditAllOpen, isEditAssetOpen, bulkResult, showBatchEndPanel, stagedCount, handleNextBatch, handleLeaveSession, handleQuickSaveAndNext, handleSaveAndNext, handleSkip, handleNext]); useEffect(() => { window.addEventListener('keydown', handleKeyDown); @@ -1737,11 +1856,18 @@ export const AuditSession: React.FC = () => { - {/* Category */} + {/* Category. Rectification mode: TRUE (rectified) has no category + menu - the verdict is always saved as "Others"; FALSE shows only + the two legacy options (true others / false others). */} CATEGORY + {isRectActive && isAnomaly !== false ? ( + + Rectified — will be saved as "Others". Press Enter (or A in Quick mode) to save. + + ) : ( - {(isAnomaly === false ? SAFE_CATEGORIES : ANOMALY_CATEGORIES).map(cat => ( + {(isAnomaly === false ? (isRectActive ? RECT_SAFE_CATEGORIES : SAFE_CATEGORIES) : ANOMALY_CATEGORIES).map(cat => ( {/* Plant Nested Subcategories (Anomaly Mode) */} - {cat.id === 1 && isAnomaly === true && selectedCategory === 1 && ( + {!isRectActive && cat.id === 1 && isAnomaly === true && selectedCategory === 1 && ( {[ { val: 'in grown', label: 'In Grown', key: '1' }, @@ -1817,7 +1944,7 @@ export const AuditSession: React.FC = () => { )} {/* Not an Anomaly Nested Subcategories (Safe Mode) */} - {cat.id === 1 && isAnomaly === false && selectedCategory === 1 && ( + {!isRectActive && cat.id === 1 && isAnomaly === false && selectedCategory === 1 && ( {[ { val: 'comparison error', label: 'Comparison Error', key: '1' }, @@ -1856,6 +1983,7 @@ export const AuditSession: React.FC = () => { )} ))} + )} {/* Notes */} @@ -1894,7 +2022,8 @@ export const AuditSession: React.FC = () => { stagedAudits[currentAnomaly.id].isAnomaly, stagedAudits[currentAnomaly.id].category, stagedAudits[currentAnomaly.id].plantSub, - stagedAudits[currentAnomaly.id].notAnAnomalySub + stagedAudits[currentAnomaly.id].notAnAnomalySub, + isRectActive )}" )} + {/* Rect TRUE needs no category - the verdict is complete as soon + as "Anomaly" is picked (saved as "Others"). */} + {(() => { const saveReady = selectedCategory !== null || (isRectActive && isAnomaly === true); return ( + ); })()} - {/* Category */} + {/* Category (rect: TRUE has none - auto "Others"; FALSE has two) */} + {isRectActive && aaIsAnomaly ? ( + + Rectified — every record will be saved as "Others". + + ) : ( - {(aaIsAnomaly ? ANOMALY_CATEGORIES : SAFE_CATEGORIES).map(cat => ( + {(aaIsAnomaly ? ANOMALY_CATEGORIES : (isRectActive ? RECT_SAFE_CATEGORIES : SAFE_CATEGORIES)).map(cat => ( ))} + )} - {/* Nested sub-category for category 1 (Plant / Not an Anomaly) */} - {aaCategory === 1 && ( + {/* Nested sub-category for category 1 (Plant / Not an Anomaly) - + never applies in rect mode (its id 1 is "true others") */} + {!isRectActive && aaCategory === 1 && ( {(aaIsAnomaly ? [{ val: 'in grown', label: 'In Grown' }, { val: 'out grown', label: 'Out Grown' }] @@ -2351,7 +2491,7 @@ export const AuditSession: React.FC = () => { }} /> - {isAuditAllVerdictValid && SOFT_DELETE_AUDIT_VALUES.includes(computeAuditValue(aaIsAnomaly, aaCategory, aaPlantSub, aaNotAnAnomalySub)) && ( + {isAuditAllVerdictValid && SOFT_DELETE_AUDIT_VALUES.includes(computeAuditValue(aaIsAnomaly, aaCategory, aaPlantSub, aaNotAnAnomalySub, isRectActive)) && ( ⚠ This verdict soft-deletes every record it's applied to.