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:
@@ -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 }
|
||||||
|
|||||||
@@ -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,8 +983,9 @@ 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 !== null &&
|
||||||
(aaCategory !== 1 || (aaIsAnomaly ? aaPlantSub !== null : aaNotAnAnomalySub !== null));
|
(aaCategory !== 1 || (aaIsAnomaly ? aaPlantSub !== null : aaNotAnAnomalySub !== null));
|
||||||
|
|
||||||
const openAuditAllDialog = () => {
|
const openAuditAllDialog = () => {
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user