shoc-frontend-new/src/domain/work-orders/use-cases/use-resumable-work-order-media-upload.ts
2026-09-23 04:17:18 -03:00

85 lines
3.2 KiB
TypeScript

import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
import { toast } from "react-toastify";
import {
type MediaUploadPolicy,
type MediaUploadSessionDto,
type UploadCategory,
type UploadSurface,
MediaUploadValidationError,
isUploadCategory,
safeMediaUploadErrorMessage,
} from "@/api/media-upload-contracts";
import { uploadFileResumable } from "@/api/media-upload-engine";
import type { UploadableMediaCategory } from "@/domain/work-orders/mappers/work-order-board-detail-mapper";
import type { UploadWorkOrderMediaInput } from "@/domain/work-orders/use-cases/use-work-order-mutations";
import { workOrdersMediaUploadApi } from "@/domain/work-orders/api/work-order-media-upload-api";
import { validateMediaFileAgainstLimits } from "@/domain/work-orders/utils/emergency-media-limits";
import { queryKeys } from "@/infra/query-key/query-key";
function toUploadCategory(
category: UploadableMediaCategory | undefined,
): UploadCategory | undefined {
return typeof category === "string" && isUploadCategory(category) ? category : undefined;
}
function invalidateMediaQueries(
queryClient: ReturnType<typeof useQueryClient>,
workOrderId: string | number,
): void {
void queryClient.invalidateQueries({
queryKey: queryKeys.workOrders.boardDetail(workOrderId),
});
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.boardMedia(workOrderId) });
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.mediaPolicy(workOrderId) });
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
}
/**
* Emergency/Reactive resumable upload (5 MiB chunks, acknowledged-part progress,
* network-loss recovery by session status). Limits are enforced client-side from the
* server policy; the server re-checks everything at commit. Media surfaces render only
* Completed/scan-passed attachments.
*/
export function useResumableWorkOrderMediaUpload(
workOrderId: string | number,
surface: Extract<UploadSurface, "WorkOrderTable" | "CompletionMedia">,
policy?: MediaUploadPolicy,
): UseMutationResult<MediaUploadSessionDto, Error, UploadWorkOrderMediaInput> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
file,
category,
idempotencyKey,
signal,
onProgress,
}: UploadWorkOrderMediaInput) => {
if (!policy?.canUpload) {
throw new MediaUploadValidationError("Uploads are not available for this work order.");
}
const check = await validateMediaFileAgainstLimits(file, policy);
if (!check.ok) {
throw new MediaUploadValidationError(check.reason);
}
return uploadFileResumable({
file,
endpoints: workOrdersMediaUploadApi.endpoints(workOrderId),
surface,
category: toUploadCategory(category),
idempotencyKey,
signal,
onProgress,
});
},
onSuccess: (session) => {
invalidateMediaQueries(queryClient, workOrderId);
if (session.state === "Completed") {
toast.success("File uploaded");
}
},
onError: (error: Error) => {
toast.error(safeMediaUploadErrorMessage(error));
},
});
}