shoc-frontend-new/src/app/(protected)/workorders/_hooks/use-media-upload-session.ts
arthur.bassi 65d75ca372 fix(work-orders): persist CompDoc prerequisites before Generated status
Also register retry AbortControllers so cancel aborts the active upload attempt.
2026-08-04 15:03:28 -03:00

132 lines
4.2 KiB
TypeScript

import { useMemo, useState } from "react";
import type { MediaUploadRequest } from "@/app/(protected)/workorders/_components/media/media-uploader";
import { boardMediaItemsToMediaFiles } from "@/domain/work-orders/mappers/work-order-board-detail-mapper";
import type { MediaFile } from "@/domain/work-orders/types/work-order-media";
import type { WorkOrderBoardDetail } from "@/domain/work-orders/types/work-order-board-detail";
import { classifyUpload } from "@/domain/work-orders/utils/work-order-media-utils";
type LocalUpload = MediaFile & { sourceFile?: File; signal?: AbortSignal };
type UploadMutate = (input: {
file: File;
signal?: AbortSignal;
onProgress?: (percent: number) => void;
}) => Promise<unknown>;
function toLocalUpload(request: MediaUploadRequest): LocalUpload {
const classified = classifyUpload(request.file);
return {
id: request.localId,
name: request.file.name,
fileType: classified.fileType,
url: URL.createObjectURL(request.file),
category: classified.category,
purpose: null,
progress: 0,
uploadStatus: "uploading" as const,
sourceFile: request.file,
signal: request.signal,
};
}
function isSameAttempt(item: LocalUpload, request: MediaUploadRequest) {
return item.id === request.localId && item.signal === request.signal;
}
export function useMediaUploadSession(
detail: WorkOrderBoardDetail | undefined,
uploadMutateAsync: UploadMutate,
) {
const [localUploads, setLocalUploads] = useState<LocalUpload[]>([]);
const serverUploads = useMemo(
() => boardMediaItemsToMediaFiles(detail?.media ?? []),
[detail?.media],
);
const uploads = useMemo(() => {
const serverIds = new Set(serverUploads.map((item) => item.id));
const pendingLocals = localUploads.filter(
(item) => item.uploadStatus === "uploading" || item.uploadStatus === "failed",
);
return [...serverUploads, ...pendingLocals.filter((item) => !serverIds.has(item.id))];
}, [serverUploads, localUploads]);
function startUploads(requests: MediaUploadRequest[]) {
setLocalUploads((prev) => {
const next = [...prev];
for (const request of requests) {
const entry = toLocalUpload(request);
const index = next.findIndex((item) => item.id === request.localId);
if (index >= 0) {
const previous = next[index];
if (previous?.url.startsWith("blob:")) {
URL.revokeObjectURL(previous.url);
}
next[index] = entry;
} else {
next.push(entry);
}
}
return next;
});
for (const request of requests) {
void uploadMutateAsync({
file: request.file,
signal: request.signal,
onProgress: (percent) => {
setLocalUploads((prev) =>
prev.map((item) =>
isSameAttempt(item, request) ? { ...item, progress: percent } : item,
),
);
},
})
.then(() => {
setLocalUploads((prev) => prev.filter((item) => !isSameAttempt(item, request)));
})
.catch((error: Error) => {
if (error.name === "AbortError") {
setLocalUploads((prev) => prev.filter((item) => !isSameAttempt(item, request)));
return;
}
setLocalUploads((prev) =>
prev.map((item) =>
isSameAttempt(item, request)
? {
...item,
uploadStatus: "failed",
errorMessage: error.message || "Upload failed",
progress: undefined,
}
: item,
),
);
});
}
}
function retryUpload(localId: string, signal: AbortSignal) {
const target = localUploads.find((item) => item.id === localId);
if (!target?.sourceFile) return;
startUploads([
{
localId,
file: target.sourceFile,
signal,
onProgress: () => undefined,
},
]);
}
function removeLocal(id: string) {
setLocalUploads((prev) => prev.filter((item) => item.id !== id));
}
function isLocal(id: string) {
return localUploads.some((item) => item.id === id);
}
return { uploads, startUploads, retryUpload, removeLocal, isLocal };
}