shoc-frontend-new/src/app/(protected)/workorders/_hooks/use-media-upload-session.ts
Alexandre Brandizzi e7fb8d6122 fix(work-orders): persist completedDate via board and media sh-116
leave EditWorkorder for completedDate; await mutations before draft saved
toast; upload-then-categorize with MIME allowlist, progress, cancel, retry.
2026-08-04 15:10:37 -03:00

113 lines
3.6 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 };
type UploadMutate = (input: {
file: File;
signal?: AbortSignal;
onProgress?: (percent: number) => void;
}) => Promise<unknown>;
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) => [
...prev,
...requests.map((request) => {
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,
};
}),
]);
for (const request of requests) {
void uploadMutateAsync({
file: request.file,
signal: request.signal,
onProgress: (percent) => {
setLocalUploads((prev) =>
prev.map((item) =>
item.id === request.localId ? { ...item, progress: percent } : item,
),
);
},
})
.then(() => {
setLocalUploads((prev) => prev.filter((item) => item.id !== request.localId));
})
.catch((error: Error) => {
if (error.name === "AbortError") {
setLocalUploads((prev) => prev.filter((item) => item.id !== request.localId));
return;
}
setLocalUploads((prev) =>
prev.map((item) =>
item.id === request.localId
? {
...item,
uploadStatus: "failed",
errorMessage: error.message || "Upload failed",
progress: undefined,
}
: item,
),
);
});
}
}
function retryUpload(localId: string) {
const target = localUploads.find((item) => item.id === localId);
if (!target?.sourceFile) return;
const controller = new AbortController();
startUploads([
{
localId,
file: target.sourceFile,
signal: controller.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 };
}