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)
This commit is contained in:
2026-07-17 11:27:36 +05:30
parent c872566e83
commit ee25bb6065
2 changed files with 184 additions and 35 deletions

View File

@@ -84,6 +84,15 @@ export const activityFeedsService = {
return response; 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) => { getOrganizationSites: async (org_id: string) => {
const response = await axiosClient.get('/api/audit/Master/site', { const response = await axiosClient.get('/api/audit/Master/site', {
params: { org_id } params: { org_id }

View File

@@ -37,6 +37,14 @@ const ANOMALY_CATEGORIES = [
{ id: 10, value: "occlusion", label: "Occlusion", color: "#6366f1", key: "0" } { 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 - // The backend soft-deletes (deleted = 1) for these exact Audit_values -
// mirror of the list in anomalyModel.update. Note the UI's "low light // 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 // 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. // A verdict staged in Bulk Mode - held in memory until Bulk Submit.
interface StagedVerdict { interface StagedVerdict {
isAnomaly: boolean; isAnomaly: boolean;
category: number; category: number | null;
notes: string; notes: string;
plantSub: PlantSub | null; plantSub: PlantSub | null;
notAnAnomalySub: NotAnAnomalySub | null; notAnAnomalySub: NotAnAnomalySub | null;
@@ -61,12 +69,20 @@ const backendMsg = (err: any) =>
// Same Audit_value resolution the single-record save uses: category value, // Same Audit_value resolution the single-record save uses: category value,
// overridden by the nested sub-category for category 1 (Plant / Not an Anomaly). // 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 = ( const computeAuditValue = (
isAnomalyBool: boolean, isAnomalyBool: boolean,
catVal: number | null, catVal: number | null,
plantSub?: string | null, plantSub?: string | null,
notAnAnomalySub?: string | null notAnAnomalySub?: string | null,
isRect = false
): string => { ): 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 categories = isAnomalyBool ? ANOMALY_CATEGORIES : SAFE_CATEGORIES;
const matchedCategory = categories.find(c => c.id === catVal); const matchedCategory = categories.find(c => c.id === catVal);
let auditValueVal = matchedCategory ? matchedCategory.value : ''; let auditValueVal = matchedCategory ? matchedCategory.value : '';
@@ -633,7 +649,7 @@ export const AuditSession: React.FC = () => {
const toast = useToastStore.getState().showToast; const toast = useToastStore.getState().showToast;
const isAnomalyBool = anomalyVal ?? true; 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) // Body matches exactly what the API expects (dbName + isRectificationEnabled go in URL params via axiosClient)
const auditPayload = { const auditPayload = {
@@ -677,6 +693,37 @@ export const AuditSession: React.FC = () => {
const softDeleted = SOFT_DELETE_AUDIT_VALUES.includes(auditValueVal); const softDeleted = SOFT_DELETE_AUDIT_VALUES.includes(auditValueVal);
if (!isAnomalyBool) { 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( toast(
softDeleted softDeleted
? `#${recordId} saved as "${auditValueVal}" — record soft-deleted` ? `#${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. // Build the exact POST /anomaly body the single-record flow sends.
const buildAuditPayload = (rec: any, v: StagedVerdict) => ({ 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 || '', Comments: v.notes || '',
Audit_status: v.isAnomaly, // boolean true/false, not 1/0 Audit_status: v.isAnomaly, // boolean true/false, not 1/0
Frame_Test: rec.Frame_Test || '', Frame_Test: rec.Frame_Test || '',
@@ -748,7 +795,7 @@ export const AuditSession: React.FC = () => {
const successOf = (e: { rec: any; v: StagedVerdict }) => ({ const successOf = (e: { rec: any; v: StagedVerdict }) => ({
id: e.rec.id as number, id: e.rec.id as number,
isAnomaly: e.v.isAnomaly, 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 || '', 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 // had saved earlier ("locked", the same 409 retry path the single-record
// flow uses) - continue to anomaly processing; safe verdicts are final. // flow uses) - continue to anomaly processing; safe verdicts are final.
const toProcessBySite = new Map<string, { rec: any; v: StagedVerdict }[]>(); const toProcessBySite = new Map<string, { rec: any; v: StagedVerdict }[]>();
// 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<string, { rec: any; v: StagedVerdict }[]>();
for (const e of entries) { for (const e of entries) {
const r = resultById.get(Number(e.rec.id)); const r = resultById.get(Number(e.rec.id));
const ok = r?.ok === true; const ok = r?.ok === true;
@@ -805,6 +856,14 @@ export const AuditSession: React.FC = () => {
} else { } else {
failures.push({ id: e.rec.id, reason: r?.error || 'saving audit failed' }); 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) { } else if (ok) {
succeeded.push(successOf(e)); succeeded.push(successOf(e));
} else { } 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 // Phase 3: apply successes locally; failures STAY STAGED so a retry only
// re-sends what actually failed. // re-sends what actually failed.
applyAuditedLocally(succeeded); applyAuditedLocally(succeeded);
@@ -862,12 +960,12 @@ export const AuditSession: React.FC = () => {
let anomalyCnt = 0, safeCnt = 0, softDeleteCnt = 0; let anomalyCnt = 0, safeCnt = 0, softDeleteCnt = 0;
Object.values(stagedAudits).forEach(v => { Object.values(stagedAudits).forEach(v => {
if (v.isAnomaly) anomalyCnt++; else safeCnt++; 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++; softDeleteCnt++;
} }
}); });
return { anomalies: anomalyCnt, safe: safeCnt, softDeletes: softDeleteCnt }; return { anomalies: anomalyCnt, safe: safeCnt, softDeletes: softDeleteCnt };
}, [stagedAudits]); }, [stagedAudits, isRectActive]);
const handleToggleBulkMode = (val: boolean) => { const handleToggleBulkMode = (val: boolean) => {
if (!val && stagedCount > 0) { if (!val && stagedCount > 0) {
@@ -885,9 +983,10 @@ export const AuditSession: React.FC = () => {
const auditAllTargetCount = anomalies.reduce((n, a, i) => const auditAllTargetCount = anomalies.reduce((n, a, i) =>
n + ((a.IsAudited === 1 || itemAuditStates[i] === 'audited') ? 0 : 1), 0); n + ((a.IsAudited === 1 || itemAuditStates[i] === 'audited') ? 0 : 1), 0);
const isAuditAllVerdictValid = const isAuditAllVerdictValid = isRectActive
aaCategory !== null && ? (aaIsAnomaly || aaCategory !== null)
(aaCategory !== 1 || (aaIsAnomaly ? aaPlantSub !== null : aaNotAnAnomalySub !== null)); : aaCategory !== null &&
(aaCategory !== 1 || (aaIsAnomaly ? aaPlantSub !== null : aaNotAnAnomalySub !== null));
const openAuditAllDialog = () => { const openAuditAllDialog = () => {
setAaIsAnomaly(true); setAaIsAnomaly(true);
@@ -899,7 +998,8 @@ export const AuditSession: React.FC = () => {
}; };
const handleAuditAllApply = () => { const handleAuditAllApply = () => {
if (!isAuditAllVerdictValid || aaCategory === null) return; if (!isAuditAllVerdictValid) return;
if (aaCategory === null && !(isRectActive && aaIsAnomaly)) return;
const verdict: StagedVerdict = { const verdict: StagedVerdict = {
isAnomaly: aaIsAnomaly, isAnomaly: aaIsAnomaly,
category: aaCategory, category: aaCategory,
@@ -933,7 +1033,7 @@ export const AuditSession: React.FC = () => {
setIsAuditAllOpen(false); setIsAuditAllOpen(false);
useToastStore.getState().showToast( 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' 'success'
); );
}; };
@@ -1008,6 +1108,8 @@ export const AuditSession: React.FC = () => {
// Category 1 nests a sub-category (Plant / Not an Anomaly) - a verdict // Category 1 nests a sub-category (Plant / Not an Anomaly) - a verdict
// without it is incomplete, both for immediate save and for staging. // without it is incomplete, both for immediate save and for staging.
const validateSubCategory = (): boolean => { const validateSubCategory = (): boolean => {
// Rectification vocabulary has no nested sub-categories.
if (isRectActive) return true;
if (selectedCategory !== 1) return true; if (selectedCategory !== 1) return true;
if ((isAnomaly === null || isAnomaly === true) && !plantSubCategory) { if ((isAnomaly === null || isAnomaly === true) && !plantSubCategory) {
useToastStore.getState().showToast('Please select a feature before submitting.', 'warning'); useToastStore.getState().showToast('Please select a feature before submitting.', 'warning');
@@ -1029,7 +1131,7 @@ export const AuditSession: React.FC = () => {
handleNext(); handleNext();
return; return;
} }
if (selectedCategory === null) { if (selectedCategory === null && !(isRectActive && isAnomaly === true)) {
handleSkip(); handleSkip();
return; return;
} }
@@ -1106,7 +1208,7 @@ export const AuditSession: React.FC = () => {
// Quick Auto Advance save trigger // Quick Auto Advance save trigger
const handleQuickSaveAndNext = useCallback(async ( const handleQuickSaveAndNext = useCallback(async (
anomalyVal: boolean, anomalyVal: boolean,
catVal: number, catVal: number | null,
plantSub?: string | null, plantSub?: string | null,
notAnAnomalySub?: string | null notAnAnomalySub?: string | null
) => { ) => {
@@ -1177,6 +1279,10 @@ export const AuditSession: React.FC = () => {
setSelectedCategory(null); setSelectedCategory(null);
setPlantSubCategory(null); setPlantSubCategory(null);
setNotAnAnomalySubCategory(null); setNotAnAnomalySubCategory(null);
// Rectification TRUE has no category step - Quick mode saves now.
if (isRectActive && isQuickAudit && !isBulkMode) {
setTimeout(() => handleQuickSaveAndNext(true, null), 120);
}
break; break;
case 's': case 's':
setIsAnomaly(false); setIsAnomaly(false);
@@ -1200,6 +1306,19 @@ export const AuditSession: React.FC = () => {
}; };
const catId = keyMap[key]; 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) // Special nested case: Plant is already active (under Anomaly)
if (isAnomaly === true && selectedCategory === 1) { if (isAnomaly === true && selectedCategory === 1) {
if (key === '1') { if (key === '1') {
@@ -1253,7 +1372,7 @@ export const AuditSession: React.FC = () => {
break; break;
} }
case 'enter': case 'enter':
if (selectedCategory !== null) { if (selectedCategory !== null || (isRectActive && isAnomaly === true)) {
handleSaveAndNext(); handleSaveAndNext();
} else { } else {
handleSkip(); handleSkip();
@@ -1270,7 +1389,7 @@ export const AuditSession: React.FC = () => {
handleSkip(); handleSkip();
break; 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(() => { useEffect(() => {
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
@@ -1737,11 +1856,18 @@ export const AuditSession: React.FC = () => {
</Button> </Button>
</Box> </Box>
</Box> </Box>
{/* 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). */}
<Box sx={{ flex: 1.5, display: 'flex', flexDirection: 'column', minHeight: 0 }}> <Box sx={{ flex: 1.5, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<Typography sx={{ fontSize: '0.75rem', fontWeight: 'bold', color: '#94a3b8', letterSpacing: 1, mb: 1 }}>CATEGORY</Typography> <Typography sx={{ fontSize: '0.75rem', fontWeight: 'bold', color: '#94a3b8', letterSpacing: 1, mb: 1 }}>CATEGORY</Typography>
{isRectActive && isAnomaly !== false ? (
<Typography sx={{ fontSize: '0.8rem', color: '#64748b', pr: 1 }}>
Rectified will be saved as "Others". Press Enter (or A in Quick mode) to save.
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, overflowY: 'auto', flex: 1, pr: 0.5, '&::-webkit-scrollbar': { width: '4px' }, '&::-webkit-scrollbar-thumb': { bgcolor: '#1e293b', borderRadius: '4px' } }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, overflowY: 'auto', flex: 1, pr: 0.5, '&::-webkit-scrollbar': { width: '4px' }, '&::-webkit-scrollbar-thumb': { bgcolor: '#1e293b', borderRadius: '4px' } }}>
{(isAnomaly === false ? SAFE_CATEGORIES : ANOMALY_CATEGORIES).map(cat => ( {(isAnomaly === false ? (isRectActive ? RECT_SAFE_CATEGORIES : SAFE_CATEGORIES) : ANOMALY_CATEGORIES).map(cat => (
<React.Fragment key={cat.id}> <React.Fragment key={cat.id}>
<Button <Button
disabled={isLocked} disabled={isLocked}
@@ -1753,7 +1879,8 @@ export const AuditSession: React.FC = () => {
} }
// If selecting Plant (Anomaly 1) or Not an Anomaly (Safe 1), do not auto-advance; wait for nested sub-options // If selecting Plant (Anomaly 1) or Not an Anomaly (Safe 1), do not auto-advance; wait for nested sub-options
if (cat.id === 1) { // (rect mode has no nested sub-options - its id 1 is "true others")
if (cat.id === 1 && !isRectActive) {
return; return;
} }
@@ -1781,7 +1908,7 @@ export const AuditSession: React.FC = () => {
</Button> </Button>
{/* Plant Nested Subcategories (Anomaly Mode) */} {/* Plant Nested Subcategories (Anomaly Mode) */}
{cat.id === 1 && isAnomaly === true && selectedCategory === 1 && ( {!isRectActive && cat.id === 1 && isAnomaly === true && selectedCategory === 1 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, pl: 4, mt: 0.5, mb: 0.5 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, pl: 4, mt: 0.5, mb: 0.5 }}>
{[ {[
{ val: 'in grown', label: 'In Grown', key: '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) */} {/* Not an Anomaly Nested Subcategories (Safe Mode) */}
{cat.id === 1 && isAnomaly === false && selectedCategory === 1 && ( {!isRectActive && cat.id === 1 && isAnomaly === false && selectedCategory === 1 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, pl: 4, mt: 0.5, mb: 0.5 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, pl: 4, mt: 0.5, mb: 0.5 }}>
{[ {[
{ val: 'comparison error', label: 'Comparison Error', key: '1' }, { val: 'comparison error', label: 'Comparison Error', key: '1' },
@@ -1856,6 +1983,7 @@ export const AuditSession: React.FC = () => {
)} )}
</React.Fragment> </React.Fragment>
))} </Box> ))} </Box>
)}
</Box> </Box>
{/* Notes */} {/* Notes */}
@@ -1894,7 +2022,8 @@ export const AuditSession: React.FC = () => {
stagedAudits[currentAnomaly.id].isAnomaly, stagedAudits[currentAnomaly.id].isAnomaly,
stagedAudits[currentAnomaly.id].category, stagedAudits[currentAnomaly.id].category,
stagedAudits[currentAnomaly.id].plantSub, stagedAudits[currentAnomaly.id].plantSub,
stagedAudits[currentAnomaly.id].notAnAnomalySub stagedAudits[currentAnomaly.id].notAnAnomalySub,
isRectActive
)}" )}"
</Typography> </Typography>
<Button size="small" onClick={handleUnstage} sx={{ color: '#94a3b8', textTransform: 'none', fontSize: '0.7rem', minWidth: 'auto', p: 0.25 }}> <Button size="small" onClick={handleUnstage} sx={{ color: '#94a3b8', textTransform: 'none', fontSize: '0.7rem', minWidth: 'auto', p: 0.25 }}>
@@ -1941,10 +2070,13 @@ export const AuditSession: React.FC = () => {
Audit All ({auditAllTargetCount}) same verdict Audit All ({auditAllTargetCount}) same verdict
</Button> </Button>
)} )}
{/* 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 (
<Button <Button
variant="contained" variant="contained"
color={!isLocked && selectedCategory !== null ? "primary" : "inherit"} color={!isLocked && saveReady ? "primary" : "inherit"}
onClick={isLocked ? handleNext : selectedCategory !== null ? handleSaveAndNext : handleSkip} onClick={isLocked ? handleNext : saveReady ? handleSaveAndNext : handleSkip}
sx={{ sx={{
borderRadius: 2, borderRadius: 2,
py: 1.5, py: 1.5,
@@ -1954,14 +2086,15 @@ export const AuditSession: React.FC = () => {
justifyContent: 'space-between', justifyContent: 'space-between',
px: 3, px: 3,
transition: 'all 0.15s ease', transition: 'all 0.15s ease',
bgcolor: selectedCategory !== null ? '' : '#1e293b', bgcolor: saveReady ? '' : '#1e293b',
color: selectedCategory !== null ? '' : '#94a3b8', color: saveReady ? '' : '#94a3b8',
border: (activeKeyFlash === 'enter') ? (selectedCategory !== null ? '2px solid #3b82f6' : '2px solid #64748b') : 'none', border: (activeKeyFlash === 'enter') ? (saveReady ? '2px solid #3b82f6' : '2px solid #64748b') : 'none',
'&:hover': { bgcolor: selectedCategory !== null ? '' : '#334155' } '&:hover': { bgcolor: saveReady ? '' : '#334155' }
}} }}
> >
{isLocked ? "Next (Read-only)" : selectedCategory !== null ? (isBulkMode ? "Stage & Next" : "Save & Next") : "Next (Skip)"} <Typography component="span" sx={{ fontSize: '0.75rem', opacity: 0.7 }}>Enter</Typography> {isLocked ? "Next (Read-only)" : saveReady ? (isBulkMode ? "Stage & Next" : "Save & Next") : "Next (Skip)"} <Typography component="span" sx={{ fontSize: '0.75rem', opacity: 0.7 }}>Enter</Typography>
</Button> </Button>
); })()}
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button <Button
variant="outlined" variant="outlined"
@@ -2018,7 +2151,7 @@ export const AuditSession: React.FC = () => {
<Typography sx={{ fontSize: '0.75rem', color: '#94a3b8' }}>Safe</Typography> <Typography sx={{ fontSize: '0.75rem', color: '#94a3b8' }}>Safe</Typography>
</Box> </Box>
<Box> <Box>
<Chip label="1-5" size="small" sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#1e293b', color: '#cbd5e1' }} /> <Chip label={isRectActive ? "1-2" : "1-5"} size="small" sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#1e293b', color: '#cbd5e1' }} />
<Typography sx={{ fontSize: '0.75rem', color: '#94a3b8' }}>Category</Typography> <Typography sx={{ fontSize: '0.75rem', color: '#94a3b8' }}>Category</Typography>
</Box> </Box>
<Box> <Box>
@@ -2273,9 +2406,14 @@ export const AuditSession: React.FC = () => {
</Button> </Button>
</Box> </Box>
{/* Category */} {/* Category (rect: TRUE has none - auto "Others"; FALSE has two) */}
{isRectActive && aaIsAnomaly ? (
<Typography sx={{ fontSize: '0.8rem', color: '#64748b' }}>
Rectified every record will be saved as "Others".
</Typography>
) : (
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 0.5 }}> <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 0.5 }}>
{(aaIsAnomaly ? ANOMALY_CATEGORIES : SAFE_CATEGORIES).map(cat => ( {(aaIsAnomaly ? ANOMALY_CATEGORIES : (isRectActive ? RECT_SAFE_CATEGORIES : SAFE_CATEGORIES)).map(cat => (
<Button <Button
key={cat.id} key={cat.id}
onClick={() => { setAaCategory(cat.id); setAaPlantSub(null); setAaNotAnAnomalySub(null); }} onClick={() => { setAaCategory(cat.id); setAaPlantSub(null); setAaNotAnAnomalySub(null); }}
@@ -2295,9 +2433,11 @@ export const AuditSession: React.FC = () => {
</Button> </Button>
))} ))}
</Box> </Box>
)}
{/* Nested sub-category for category 1 (Plant / Not an Anomaly) */} {/* Nested sub-category for category 1 (Plant / Not an Anomaly) -
{aaCategory === 1 && ( never applies in rect mode (its id 1 is "true others") */}
{!isRectActive && aaCategory === 1 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{(aaIsAnomaly {(aaIsAnomaly
? [{ val: 'in grown', label: 'In Grown' }, { val: 'out grown', label: 'Out Grown' }] ? [{ 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)) && (
<Typography variant="body2" sx={{ color: '#eab308' }}> <Typography variant="body2" sx={{ color: '#eab308' }}>
This verdict soft-deletes every record it's applied to. This verdict soft-deletes every record it's applied to.
</Typography> </Typography>