From 6012ebabe72d75238d95d59fb537ecfc0793495f Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 24 Sep 2026 22:51:17 -0300 Subject: [PATCH] fix(media): count HEIC and untyped phone files, guard overlapping picks Persisted HEIC files and local files with an empty or octet-stream type were classified as "other", so they did not count toward the 10 photo / 3 video limit. Both classifiers now recognise them by extension. The Photos & Videos uploader and Extra Docs ignore a new selection while the previous one is still being screened, and the vendor portal drops an earlier pick whose video check finishes after a newer pick. Also removes ticket keys from source comments. --- .../detail/tabs/slide-over-extras-tab.tsx | 41 +++++++++++++------ .../_components/media/media-uploader.tsx | 34 +++++++++++---- .../v/_components/vendor-portal-documents.tsx | 7 ++++ .../vendor-portal/lib/document-validation.ts | 2 +- .../vendor-portal/types/vendor-portal.ts | 2 +- .../mappers/work-order-board-detail-mapper.ts | 2 +- .../utils/screen-work-order-uploads.ts | 2 +- .../utils/work-order-media-limits.ts | 2 +- .../utils/work-order-media-utils.ts | 5 +++ src/lib/probe-video-duration.ts | 2 +- ...slide-over-extras-upload-contract.test.tsx | 32 +++++++++++++++ .../app/v/vendor-portal-documents.test.tsx | 36 ++++++++++++++++ .../work-order-media-kind-counts.test.ts | 31 ++++++++++++++ 13 files changed, 171 insertions(+), 27 deletions(-) create mode 100644 src/test/domain/work-orders/utils/work-order-media-kind-counts.test.ts diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-extras-tab.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-extras-tab.tsx index ec6963e0..a5d2e118 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-extras-tab.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-extras-tab.tsx @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useRef, useState } from "react"; import { File as FileIcon, FileText, @@ -18,7 +18,10 @@ import { hasExplicitAvetaFile, resolveExtraDocCategory, } from "@/domain/work-orders/utils/extra-docs-aveta"; -import { screenWorkOrderUploads } from "@/domain/work-orders/utils/screen-work-order-uploads"; +import { + screenWorkOrderUploads, + type WorkOrderUploadScreenResult, +} from "@/domain/work-orders/utils/screen-work-order-uploads"; import { canDeleteWorkOrderMedia, countWorkOrderMediaKinds, @@ -189,16 +192,30 @@ export function SlideOverExtrasTab({ }: SlideOverExtrasTabProps) { const inputRef = useRef(null); const avetaAttached = hasExplicitAvetaFile(uploads); + // One selection at a time: a second pick during the async screen would read the + // same photo/video counts and could pass the per-work-order limit. + const screeningRef = useRef(false); + const [screening, setScreening] = useState(false); + const busy = pending || screening; const addFiles = async (files: FileList | null) => { - if (isReadOnly || pending || !files || files.length === 0) return; + if (isReadOnly || pending || screeningRef.current || !files || files.length === 0) return; // Snapshot synchronously: the input is reset right after this handler returns. const selected = Array.from(files); - const { accepted, rejections } = await screenWorkOrderUploads(selected, { - existing: mediaCounts ?? countWorkOrderMediaKinds(uploads), - isTypeAllowed: isWorkOrderExtraDocTypeAllowed, - unsupportedTypeMessage: UNSUPPORTED_TYPE_MESSAGE, - }); + screeningRef.current = true; + setScreening(true); + let screened: WorkOrderUploadScreenResult; + try { + screened = await screenWorkOrderUploads(selected, { + existing: mediaCounts ?? countWorkOrderMediaKinds(uploads), + isTypeAllowed: isWorkOrderExtraDocTypeAllowed, + unsupportedTypeMessage: UNSUPPORTED_TYPE_MESSAGE, + }); + } finally { + screeningRef.current = false; + setScreening(false); + } + const { accepted, rejections } = screened; for (const message of rejections) toast.error(message); if (accepted.length > 0) { onUpload(accepted); @@ -231,7 +248,7 @@ export function SlideOverExtrasTab({ <>