under review records ui and be
This commit is contained in:
@@ -193,6 +193,28 @@ export interface MissingMediaResult {
|
|||||||
summary: MissingMediaSummary;
|
summary: MissingMediaSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A pending record quarantined by Report to Dev (under_review = 1), with
|
||||||
|
// its latest dev ticket merged in (null if the ticket predates ticket
|
||||||
|
// logging or was raised outside this org).
|
||||||
|
export interface UnderReviewRecord {
|
||||||
|
id: number;
|
||||||
|
Asset: string | null;
|
||||||
|
Plaza: string | null;
|
||||||
|
Chainage: string | null;
|
||||||
|
Side: string | null;
|
||||||
|
video_date: string | null;
|
||||||
|
updated_user_name: string | null;
|
||||||
|
Frame_Test: string | null;
|
||||||
|
ticket: {
|
||||||
|
anomaly_id: number;
|
||||||
|
dev_handle: string;
|
||||||
|
reporter: string | null;
|
||||||
|
created_at: string;
|
||||||
|
git_status: 'pending' | 'success' | 'failed';
|
||||||
|
git_issue_url: string | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const adminService = {
|
export const adminService = {
|
||||||
getUsers: async (): Promise<AdminUser[]> => {
|
getUsers: async (): Promise<AdminUser[]> => {
|
||||||
return (await axiosClient.get('/api/audit/admin/users')) as unknown as AdminUser[];
|
return (await axiosClient.get('/api/audit/admin/users')) as unknown as AdminUser[];
|
||||||
@@ -309,6 +331,30 @@ export const adminService = {
|
|||||||
return (await axiosClient.get('/api/audit/admin/media-status', { params })) as unknown as MissingMediaResult;
|
return (await axiosClient.get('/api/audit/admin/media-status', { params })) as unknown as MissingMediaResult;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Pending records quarantined by Report to Dev (under_review = 1) -
|
||||||
|
// hidden from auditors until released here.
|
||||||
|
getUnderReviewRecords: async (params: {
|
||||||
|
dbName: string;
|
||||||
|
type: 'audits' | 'rectification';
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}): Promise<{ records: UnderReviewRecord[]; total: number }> => {
|
||||||
|
return (await axiosClient.get('/api/audit/admin/under-review', { params })) as unknown as {
|
||||||
|
records: UnderReviewRecord[]; total: number;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Put up to 50 quarantined records back into the auditors' pending feed.
|
||||||
|
releaseUnderReview: async (payload: {
|
||||||
|
dbName: string;
|
||||||
|
type: 'audits' | 'rectification';
|
||||||
|
ids: number[];
|
||||||
|
}): Promise<{ released: number }> => {
|
||||||
|
return (await axiosClient.post('/api/audit/admin/under-review/release', payload)) as unknown as {
|
||||||
|
released: number;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
// Immediate CDN re-verify for up to 50 records (also resets the cron's
|
// Immediate CDN re-verify for up to 50 records (also resets the cron's
|
||||||
// give-up counter for them).
|
// give-up counter for them).
|
||||||
recheckMedia: async (payload: {
|
recheckMedia: async (payload: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Allocation } from './Allocation';
|
|||||||
import { AuditHistory } from './AuditHistory';
|
import { AuditHistory } from './AuditHistory';
|
||||||
import { DevTickets } from './DevTickets';
|
import { DevTickets } from './DevTickets';
|
||||||
import { MissingMedia } from './MissingMedia';
|
import { MissingMedia } from './MissingMedia';
|
||||||
|
import { UnderReview } from './UnderReview';
|
||||||
|
|
||||||
export const AdminConsole: React.FC = () => {
|
export const AdminConsole: React.FC = () => {
|
||||||
const [tab, setTab] = useState(0);
|
const [tab, setTab] = useState(0);
|
||||||
@@ -26,6 +27,7 @@ export const AdminConsole: React.FC = () => {
|
|||||||
<Tab label="Audit & Rectification History" />
|
<Tab label="Audit & Rectification History" />
|
||||||
<Tab label="Dev Tickets" />
|
<Tab label="Dev Tickets" />
|
||||||
<Tab label="Missing Media" />
|
<Tab label="Missing Media" />
|
||||||
|
<Tab label="Under Review" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{tab === 0 && <ClientsOverview />}
|
{tab === 0 && <ClientsOverview />}
|
||||||
@@ -35,6 +37,7 @@ export const AdminConsole: React.FC = () => {
|
|||||||
{tab === 4 && <AuditHistory />}
|
{tab === 4 && <AuditHistory />}
|
||||||
{tab === 5 && <DevTickets />}
|
{tab === 5 && <DevTickets />}
|
||||||
{tab === 6 && <MissingMedia />}
|
{tab === 6 && <MissingMedia />}
|
||||||
|
{tab === 7 && <UnderReview />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
209
src/pages/admin/UnderReview.tsx
Normal file
209
src/pages/admin/UnderReview.tsx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
||||||
|
Paper, Checkbox, FormControl, InputLabel, Select, MenuItem, ToggleButtonGroup,
|
||||||
|
ToggleButton, Button, CircularProgress, TablePagination, Typography, Chip, Link, Tooltip,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { adminService } from '../../api/adminService';
|
||||||
|
import type { UnderReviewRecord } from '../../api/adminService';
|
||||||
|
import { accountService } from '../../api/accountService';
|
||||||
|
import { useToastStore } from '../../store/toastStore';
|
||||||
|
|
||||||
|
interface Organization {
|
||||||
|
org_id: string;
|
||||||
|
org_name: string;
|
||||||
|
db_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
const gitChipColor = (status: string): 'default' | 'success' | 'error' => {
|
||||||
|
if (status === 'success') return 'success';
|
||||||
|
if (status === 'failed') return 'error';
|
||||||
|
return 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UnderReview: React.FC = () => {
|
||||||
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
|
const [dbName, setDbName] = useState('');
|
||||||
|
const [type, setType] = useState<'audits' | 'rectification'>('audits');
|
||||||
|
const [records, setRecords] = useState<UnderReviewRecord[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [releasing, setReleasing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const orgs = ((await accountService.getOrganizations()) as any) || [];
|
||||||
|
setOrganizations(orgs);
|
||||||
|
if (orgs.length > 0) setDbName(orgs[0].db_name);
|
||||||
|
} catch {
|
||||||
|
useToastStore.getState().showToast('Failed to load organizations.', 'error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
if (!dbName) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await adminService.getUnderReviewRecords({
|
||||||
|
dbName, type, limit: PAGE_SIZE, offset: page * PAGE_SIZE,
|
||||||
|
});
|
||||||
|
setRecords(result.records);
|
||||||
|
setTotal(result.total);
|
||||||
|
// Releasing can shrink the total below the current page - snap back.
|
||||||
|
if (page > 0 && result.total <= page * PAGE_SIZE) setPage(0);
|
||||||
|
} catch {
|
||||||
|
useToastStore.getState().showToast('Failed to load under-review records.', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [dbName, type, page]);
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(0);
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
}, [dbName, type]);
|
||||||
|
|
||||||
|
const toggleRow = (id: number) => {
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id); else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAll = () => {
|
||||||
|
setSelectedIds(selectedIds.size === records.length ? new Set() : new Set(records.map((r) => r.id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRelease = async (ids: number[]) => {
|
||||||
|
setReleasing(true);
|
||||||
|
try {
|
||||||
|
const result = await adminService.releaseUnderReview({ dbName, type, ids });
|
||||||
|
useToastStore.getState().showToast(
|
||||||
|
`Released ${result.released} record(s) back to the auditors' feed.`,
|
||||||
|
result.released > 0 ? 'success' : 'info'
|
||||||
|
);
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
load();
|
||||||
|
} catch (err: any) {
|
||||||
|
useToastStore.getState().showToast(err?.response?.data?.message || 'Release failed.', 'error');
|
||||||
|
} finally {
|
||||||
|
setReleasing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||||
|
<InputLabel>Organization</InputLabel>
|
||||||
|
<Select label="Organization" value={dbName} onChange={(e) => setDbName(e.target.value)}>
|
||||||
|
{organizations.map((o) => <MenuItem key={o.org_id} value={o.db_name}>{o.org_name}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<ToggleButtonGroup value={type} exclusive onChange={(_, v) => v && setType(v)} size="small">
|
||||||
|
<ToggleButton value="audits">Audits</ToggleButton>
|
||||||
|
<ToggleButton value="rectification">Rectification</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={selectedIds.size === 0 || selectedIds.size > 50 || releasing}
|
||||||
|
onClick={() => handleRelease(Array.from(selectedIds))}
|
||||||
|
>
|
||||||
|
{releasing ? <CircularProgress size={20} sx={{ color: 'white' }} /> : `Release selected (${selectedIds.size})`}
|
||||||
|
</Button>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||||
|
records reported to a dev are hidden from auditors until released here (max 50 at a time)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>
|
||||||
|
) : (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell padding="checkbox">
|
||||||
|
<Checkbox
|
||||||
|
checked={records.length > 0 && selectedIds.size === records.length}
|
||||||
|
indeterminate={selectedIds.size > 0 && selectedIds.size < records.length}
|
||||||
|
onChange={toggleAll}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>Record ID</TableCell>
|
||||||
|
<TableCell>Asset</TableCell>
|
||||||
|
<TableCell>Plaza</TableCell>
|
||||||
|
<TableCell>Chainage</TableCell>
|
||||||
|
<TableCell>Side</TableCell>
|
||||||
|
<TableCell>Video Date</TableCell>
|
||||||
|
<TableCell>Reported By</TableCell>
|
||||||
|
<TableCell>Dev</TableCell>
|
||||||
|
<TableCell>Reported On</TableCell>
|
||||||
|
<TableCell>Git Issue</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{records.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={11} align="center" sx={{ color: 'text.secondary' }}>
|
||||||
|
No records under review — nothing is currently reported to the devs.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{records.map((r) => (
|
||||||
|
<TableRow key={r.id} selected={selectedIds.has(r.id)}>
|
||||||
|
<TableCell padding="checkbox">
|
||||||
|
<Checkbox checked={selectedIds.has(r.id)} onChange={() => toggleRow(r.id)} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{r.id}</TableCell>
|
||||||
|
<TableCell>{r.Asset?.replace(/_/g, ' ') || '—'}</TableCell>
|
||||||
|
<TableCell>{r.Plaza || '—'}</TableCell>
|
||||||
|
<TableCell>{r.Chainage || '—'}</TableCell>
|
||||||
|
<TableCell>{r.Side || '—'}</TableCell>
|
||||||
|
<TableCell>{r.video_date ? new Date(r.video_date).toLocaleDateString() : '—'}</TableCell>
|
||||||
|
<TableCell>{r.ticket?.reporter || r.updated_user_name || '—'}</TableCell>
|
||||||
|
<TableCell>{r.ticket ? `@${r.ticket.dev_handle}` : '—'}</TableCell>
|
||||||
|
<TableCell>{r.ticket?.created_at ? new Date(r.ticket.created_at).toLocaleString() : '—'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{r.ticket ? (
|
||||||
|
r.ticket.git_issue_url ? (
|
||||||
|
<Link href={r.ticket.git_issue_url} target="_blank" rel="noopener" underline="hover">
|
||||||
|
<Chip size="small" label={r.ticket.git_status} color={gitChipColor(r.ticket.git_status)} clickable />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Chip size="small" label={r.ticket.git_status} color={gitChipColor(r.ticket.git_status)} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Tooltip title="No dev ticket found for this record in this org">
|
||||||
|
<Chip size="small" label="no ticket" variant="outlined" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<TablePagination
|
||||||
|
component="div"
|
||||||
|
count={total}
|
||||||
|
page={page}
|
||||||
|
onPageChange={(_, newPage) => setPage(newPage)}
|
||||||
|
rowsPerPage={PAGE_SIZE}
|
||||||
|
rowsPerPageOptions={[PAGE_SIZE]}
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -181,7 +181,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
reportMessage.trim(),
|
reportMessage.trim(),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
await axiosClient.post('/api/audit/report', {
|
const res: any = await axiosClient.post('/api/audit/report', {
|
||||||
channel: `@${reportDev}`,
|
channel: `@${reportDev}`,
|
||||||
text: context,
|
text: context,
|
||||||
anomalyId: a?.id,
|
anomalyId: a?.id,
|
||||||
@@ -189,11 +189,31 @@ export const AuditSession: React.FC = () => {
|
|||||||
plaza: a?.Plaza || null,
|
plaza: a?.Plaza || null,
|
||||||
devHandle: reportDev,
|
devHandle: reportDev,
|
||||||
reporter: user?.username || user?.email || null,
|
reporter: user?.username || user?.email || null,
|
||||||
|
isRectificationEnabled: isRectActive,
|
||||||
});
|
});
|
||||||
setIsReportDialogOpen(false);
|
setIsReportDialogOpen(false);
|
||||||
setReportDev('');
|
setReportDev('');
|
||||||
setReportMessage('');
|
setReportMessage('');
|
||||||
|
if (res?.ticket?.underReview && a?.id != null) {
|
||||||
|
// The backend quarantined the record (under_review = 1): remove it
|
||||||
|
// from the session and the pending caches - auditors must not see
|
||||||
|
// it again until an admin releases it from the Under Review tab.
|
||||||
|
const reportedId = a.id;
|
||||||
|
const removedIdx = anomalies.findIndex((x: any) => x.id === reportedId);
|
||||||
|
const remaining = anomalies.filter((x: any) => x.id !== reportedId);
|
||||||
|
setAnomalies(remaining);
|
||||||
|
// itemAuditStates is index-aligned with anomalies - splice in lockstep.
|
||||||
|
setItemAuditStates(prev => prev.filter((_, i) => i !== removedIdx));
|
||||||
|
purgePendingCaches([reportedId]);
|
||||||
|
if (remaining.length === 0) {
|
||||||
|
setShowBatchEndPanel(true);
|
||||||
|
} else {
|
||||||
|
goToIndex(Math.min(removedIdx, remaining.length - 1), remaining);
|
||||||
|
}
|
||||||
|
useToastStore.getState().showToast(`#${reportedId} reported to @${reportDev} — moved to Under Review (admin console)`, 'success');
|
||||||
|
} else {
|
||||||
useToastStore.getState().showToast(`Message sent to @${reportDev}`, 'success');
|
useToastStore.getState().showToast(`Message sent to @${reportDev}`, 'success');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to send report:', err);
|
console.error('Failed to send report:', err);
|
||||||
useToastStore.getState().showToast('Failed to send message. Please try again.', 'error');
|
useToastStore.getState().showToast('Failed to send message. Please try again.', 'error');
|
||||||
@@ -512,9 +532,12 @@ export const AuditSession: React.FC = () => {
|
|||||||
// Single navigation entry point (arrows, Enter-advance, progress-tile
|
// Single navigation entry point (arrows, Enter-advance, progress-tile
|
||||||
// clicks). If the target record has a staged verdict, pre-fill the form
|
// clicks). If the target record has a staged verdict, pre-fill the form
|
||||||
// with it so it can be reviewed/overwritten before Bulk Submit.
|
// with it so it can be reviewed/overwritten before Bulk Submit.
|
||||||
const goToIndex = (idx: number) => {
|
// `list` defaults to the current session array; pass the post-removal
|
||||||
if (idx < 0 || idx >= anomalies.length) return;
|
// array when navigating right after deleting a record from state (state
|
||||||
const staged = anomalies[idx] ? stagedAudits[anomalies[idx].id] : undefined;
|
// updates haven't flushed yet, so the closure still sees the old array).
|
||||||
|
const goToIndex = (idx: number, list: any[] = anomalies) => {
|
||||||
|
if (idx < 0 || idx >= list.length) return;
|
||||||
|
const staged = list[idx] ? stagedAudits[list[idx].id] : undefined;
|
||||||
setCurrentIndex(idx);
|
setCurrentIndex(idx);
|
||||||
if (staged) {
|
if (staged) {
|
||||||
setIsAnomaly(staged.isAnomaly);
|
setIsAnomaly(staged.isAnomaly);
|
||||||
@@ -556,15 +579,33 @@ export const AuditSession: React.FC = () => {
|
|||||||
handleNext();
|
handleNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Remove records from the cached PENDING feed tabs (the feeds table
|
||||||
|
// renders from this cache - a stale entry lets users re-open a record
|
||||||
|
// that has left the pending queue), decrement the tab count, and drop the
|
||||||
|
// cached dashboard stats so they refetch. Only tabs of the same mode:
|
||||||
|
// audits and rectification ids come from different tables and can collide.
|
||||||
|
const purgePendingCaches = (ids: number[]) => {
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
const idSet = new Set(ids);
|
||||||
|
const feed = useFeedStore.getState();
|
||||||
|
feed.setTabs(feed.tabs.map((t: any) =>
|
||||||
|
(t.date === '' || t.date === 'reviewTab') && !!t.isRectificationTab === isRectActive
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
anomalies: (t.anomalies || []).filter((a: any) => !idSet.has(a.id)),
|
||||||
|
anomaliesCopy: (t.anomaliesCopy || []).filter((a: any) => !idSet.has(a.id)),
|
||||||
|
totalAnomalyCount: Math.max(0, (t.totalAnomalyCount || 0) - ids.length),
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
));
|
||||||
|
feed.setDashboardCache({ ...feed.dashboardCache, stats: null });
|
||||||
|
};
|
||||||
|
|
||||||
// Sync the in-memory copies with what the backend just wrote, for one or
|
// Sync the in-memory copies with what the backend just wrote, for one or
|
||||||
// many records at once. Without this the lock banner shows the record's OLD
|
// many records at once. Without this the lock banner shows the record's OLD
|
||||||
// Audited_on - the ingest pipeline pre-stamps that column on unaudited
|
// Audited_on - the ingest pipeline pre-stamps that column on unaudited
|
||||||
// rows, so it can be months in the past - instead of the NOW() the save
|
// rows, so it can be months in the past - instead of the NOW() the save
|
||||||
// just set. Also purges the records from the cached PENDING feed tabs - the
|
// just set.
|
||||||
// feeds table renders from this cache, and a stale entry let users re-open
|
|
||||||
// an already-audited record from the table. Only tabs of the same mode:
|
|
||||||
// audits and rectification ids come from different tables and can collide.
|
|
||||||
// Also drops the cached dashboard stats so they refetch.
|
|
||||||
const applyAuditedLocally = (records: { id: number; isAnomaly: boolean; auditValue: string; comments: string }[]) => {
|
const applyAuditedLocally = (records: { id: number; isAnomaly: boolean; auditValue: string; comments: string }[]) => {
|
||||||
if (records.length === 0) return;
|
if (records.length === 0) return;
|
||||||
const nowIso = new Date().toISOString();
|
const nowIso = new Date().toISOString();
|
||||||
@@ -575,19 +616,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
? { ...a, IsAudited: 1, Audit_status: r.isAnomaly ? 1 : 0, Audit_value: r.auditValue, Comments: r.comments, Audited_on: nowIso }
|
? { ...a, IsAudited: 1, Audit_status: r.isAnomaly ? 1 : 0, Audit_value: r.auditValue, Comments: r.comments, Audited_on: nowIso }
|
||||||
: a;
|
: a;
|
||||||
}));
|
}));
|
||||||
|
purgePendingCaches(records.map(r => r.id));
|
||||||
const feed = useFeedStore.getState();
|
|
||||||
feed.setTabs(feed.tabs.map((t: any) =>
|
|
||||||
(t.date === '' || t.date === 'reviewTab') && !!t.isRectificationTab === isRectActive
|
|
||||||
? {
|
|
||||||
...t,
|
|
||||||
anomalies: (t.anomalies || []).filter((a: any) => !byId.has(a.id)),
|
|
||||||
anomaliesCopy: (t.anomaliesCopy || []).filter((a: any) => !byId.has(a.id)),
|
|
||||||
totalAnomalyCount: Math.max(0, (t.totalAnomalyCount || 0) - records.length),
|
|
||||||
}
|
|
||||||
: t
|
|
||||||
));
|
|
||||||
feed.setDashboardCache({ ...feed.dashboardCache, stats: null });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveAuditState = useCallback(async (
|
const saveAuditState = useCallback(async (
|
||||||
|
|||||||
Reference in New Issue
Block a user