anton screenshot
This commit is contained in:
@@ -93,6 +93,14 @@ export const activityFeedsService = {
|
||||
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) => {
|
||||
const response = await axiosClient.get('/api/audit/Master/site', {
|
||||
params: { org_id }
|
||||
|
||||
@@ -104,6 +104,10 @@ export const AuditSession: React.FC = () => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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
|
||||
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
|
||||
@@ -564,6 +568,77 @@ export const AuditSession: React.FC = () => {
|
||||
return items;
|
||||
}, [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
|
||||
// clicks). If the target record has a staged verdict, pre-fill the form
|
||||
// with it so it can be reviewed/overwritten before Bulk Submit.
|
||||
@@ -1656,11 +1731,23 @@ export const AuditSession: React.FC = () => {
|
||||
{mediaItems.length > 0 ? (
|
||||
<>
|
||||
{mediaItems[carouselIndex]?.type === 'video' ? (
|
||||
<video src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain'
|
||||
}} />
|
||||
<>
|
||||
<video ref={videoRef} src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{
|
||||
width: '100%',
|
||||
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
|
||||
src={mediaItems[carouselIndex]?.url}
|
||||
|
||||
Reference in New Issue
Block a user