anton screenshot

This commit is contained in:
2026-07-23 10:27:10 +05:30
parent a76f92dbdd
commit 2d9b80971c
2 changed files with 100 additions and 5 deletions

View File

@@ -93,6 +93,14 @@ export const activityFeedsService = {
return response; return response;
}, },
// Manual test-frame capture: extract the frame at `timestamp` (seconds) from
// the record's video, store it as a new image, and repoint Frame_Test at it.
// Returns { frame_test } - the new relative path to display/use.
captureFrame: async (payload: { site_id: number | string; anomaly_id: number; table: 'audits' | 'rectification'; timestamp: number }) => {
const response = await axiosClient.post('/api/audit/auditor/anomaly/capture-frame', 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

@@ -104,6 +104,10 @@ export const AuditSession: React.FC = () => {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [carouselIndex, setCarouselIndex] = useState(0); const [carouselIndex, setCarouselIndex] = useState(0);
// Manual test-frame capture: ref to read the playing video's currentTime,
// and a flag for the in-flight extraction round-trip.
const videoRef = useRef<HTMLVideoElement>(null);
const [capturing, setCapturing] = useState(false);
// Form states // Form states
const [selectedCategory, setSelectedCategory] = useState<number | null>(null); const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
@@ -564,6 +568,77 @@ export const AuditSession: React.FC = () => {
return items; return items;
}, [currentAnomaly]); }, [currentAnomaly]);
// Grab the frame currently showing in the video and make it the record's
// Frame_Test. The backend extracts it server-side, stores it as a NEW file,
// and repoints Frame_Test (original preserved). We mirror that into local
// state so the carousel shows the new frame immediately - everything else
// (audit → archive → anomaly row) already reads Frame_Test.
const handleCaptureFrame = useCallback(async () => {
const v = videoRef.current;
if (!v || !currentAnomaly) return;
const timestamp = v.currentTime;
const toast = useToastStore.getState().showToast;
setCapturing(true);
try {
const res: any = await activityFeedsService.captureFrame({
site_id: currentAnomaly.site_id,
anomaly_id: currentAnomaly.id,
table: isRectActive ? 'rectification' : 'audits',
timestamp,
});
const newFrame = res?.frame_test;
if (!newFrame) throw new Error('No frame returned');
const newExtra = res?.extra_image;
// Index the new frame will occupy in the carousel: it replaces the
// current test frame in place (or is appended). Switch to it explicitly -
// the auditor clicks while viewing the video, and we don't want to rely
// on the [currentAnomaly] effect to move off the video item.
const oldImages: any[] = Array.isArray(currentAnomaly.images) ? currentAnomaly.images : [];
let targetIdx = 0;
if (oldImages.length > 0) {
const found = oldImages.findIndex((i: any) => i.selected || i.src === currentAnomaly.Frame_Test);
targetIdx = found >= 0 ? found : oldImages.length;
}
// Repoint the record everywhere it's cached. Frame_Test drives the CDN
// URL; Extra_Image drives the carousel; images is the built carousel.
const patchOne = (a: any) => {
if (a.id !== currentAnomaly.id) return a;
const updated: any = { ...a, Frame_Test: newFrame, Extra_Image: newExtra ?? a.Extra_Image };
if (Array.isArray(a.images) && a.images.length > 0) {
let replaced = false;
updated.images = a.images.map((img: any) => {
if (img.selected || img.src === a.Frame_Test) { replaced = true; return { ...img, src: newFrame, selected: true }; }
return { ...img, selected: false };
});
if (!replaced) updated.images = [...a.images.map((i: any) => ({ ...i, selected: false })), { src: newFrame, selected: true }];
} else {
updated.images = [{ src: newFrame, selected: true }];
}
return updated;
};
// Session state first, then the PERSISTED feed-tab caches (both anomalies
// AND anomaliesCopy) - otherwise the feed / re-opening the record shows
// the stale pre-capture image (same sync as handleSaveAssetEdit). Only
// same-mode tabs (audits vs rectification ids can collide).
setAnomalies((prev: any[]) => prev.map(patchOne));
const feed = useFeedStore.getState();
feed.setTabs(feed.tabs.map((t: any) =>
!!t.isRectificationTab === isRectActive
? { ...t, anomalies: (t.anomalies || []).map(patchOne), anomaliesCopy: (t.anomaliesCopy || []).map(patchOne) }
: t
));
setCarouselIndex(targetIdx);
toast('Frame captured — now the test image for this record', 'success');
} catch (e: any) {
toast(e?.response?.data?.error || e?.message || 'Frame capture failed', 'error');
} finally {
setCapturing(false);
}
}, [currentAnomaly, isRectActive]);
// 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.
@@ -1656,11 +1731,23 @@ export const AuditSession: React.FC = () => {
{mediaItems.length > 0 ? ( {mediaItems.length > 0 ? (
<> <>
{mediaItems[carouselIndex]?.type === 'video' ? ( {mediaItems[carouselIndex]?.type === 'video' ? (
<video src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{ <>
width: '100%', <video ref={videoRef} src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{
height: '100%', width: '100%',
objectFit: 'contain' height: '100%',
}} /> objectFit: 'contain'
}} />
<Button
size="small"
variant="contained"
onClick={handleCaptureFrame}
disabled={capturing || isLocked}
startIcon={capturing ? <CircularProgress size={14} sx={{ color: 'white' }} /> : null}
sx={{ position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', zIndex: 20, textTransform: 'none', fontWeight: 'bold', bgcolor: 'rgba(59,130,246,0.92)', '&:hover': { bgcolor: '#3b82f6' } }}
>
{capturing ? 'Capturing…' : 'Use this frame'}
</Button>
</>
) : ( ) : (
<ZoomableImage <ZoomableImage
src={mediaItems[carouselIndex]?.url} src={mediaItems[carouselIndex]?.url}