shoc-frontend-new/src/domain/work-orders/utils/screen-work-order-uploads.ts
Alexandre Brandizzi c99e81d0d7 fix(media): pre-check duration and per-work-order counts on Extra Docs
Extra Docs advertised the 90-second limit but only checked type and size.
Both dispatcher surfaces now share one screening step (type, size, count,
duration), and the Completion Doc media tab and Extra Docs count the whole
work order's photos and videos rather than only their own tab's share.
Failed local uploads no longer count toward the limit.
2026-09-24 22:01:22 -03:00

64 lines
2.3 KiB
TypeScript

import {
MAX_PHOTOS_PER_WORK_ORDER,
MAX_VIDEOS_PER_WORK_ORDER,
MAX_VIDEO_DURATION_SECONDS,
resolveWorkOrderUploadKind,
workOrderFileSizeRejection,
type WorkOrderMediaKindCounts,
} from "@/domain/work-orders/utils/work-order-media-limits";
import { probeVideoDurationSeconds } from "@/lib/probe-video-duration";
export type WorkOrderUploadScreen = {
/** Photos and videos already on the work order, across every surface. */
existing: WorkOrderMediaKindCounts;
isTypeAllowed: (file: File) => boolean;
unsupportedTypeMessage: string;
};
export type WorkOrderUploadScreenResult = { accepted: File[]; rejections: string[] };
/**
* SH-116 browser pre-check shared by every dispatcher upload surface: type, size,
* per-work-order photo/video count (including earlier files in the same selection)
* and video duration. The server enforces the same contract; a video whose duration
* the browser cannot read is never blocked here.
*/
export async function screenWorkOrderUploads(
files: readonly File[],
{ existing, isTypeAllowed, unsupportedTypeMessage }: WorkOrderUploadScreen,
): Promise<WorkOrderUploadScreenResult> {
const counts = { ...existing };
const accepted: File[] = [];
const rejections: string[] = [];
for (const file of files) {
if (!isTypeAllowed(file)) {
rejections.push(unsupportedTypeMessage);
continue;
}
const sizeRejection = workOrderFileSizeRejection(file);
if (sizeRejection) {
rejections.push(sizeRejection);
continue;
}
const kind = resolveWorkOrderUploadKind(file);
if (kind === "photo" && counts.photos >= MAX_PHOTOS_PER_WORK_ORDER) {
rejections.push(`A work order can have at most ${MAX_PHOTOS_PER_WORK_ORDER} photos.`);
continue;
}
if (kind === "video" && counts.videos >= MAX_VIDEOS_PER_WORK_ORDER) {
rejections.push(`A work order can have at most ${MAX_VIDEOS_PER_WORK_ORDER} videos.`);
continue;
}
if (kind === "video") {
const duration = await probeVideoDurationSeconds(file);
if (duration != null && duration > MAX_VIDEO_DURATION_SECONDS) {
rejections.push(`Videos must be ${MAX_VIDEO_DURATION_SECONDS} seconds or shorter.`);
continue;
}
}
if (kind === "photo") counts.photos += 1;
if (kind === "video") counts.videos += 1;
accepted.push(file);
}
return { accepted, rejections };
}