anton screenshot
This commit is contained in:
@@ -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 }
|
||||||
|
|||||||
@@ -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={{
|
<>
|
||||||
|
<video ref={videoRef} src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
objectFit: 'contain'
|
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user