mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
85 lines
3.2 KiB
TypeScript
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));
|
|
},
|
|
});
|
|
}
|