shoc-frontend-new/src/domain/work-orders/api/work-order-board-documents-api.ts
Arthur Bassi 74e5c58e5d fix(work-orders): restore completion media uploads over HTTP/1.1
Use XHR for media progress so Ky does not stream the body on HTTP/1.1
(Vite proxy), and keep completion vs extra tiles from mixing after upload.
2026-09-07 15:19:39 -03:00

224 lines
7 KiB
TypeScript

import { API_PATHS } from "@/api/api-paths";
import { ApiError, mapHttpStatusToMessage } from "@/api/api-error";
import { api, apiGet, apiPost } from "@/api/api";
import { handleApiResponse } from "@/api/handle-api-response";
import { uploadFormWithProgress } from "@/api/upload-form-with-progress";
import {
BoardPatchConflictError,
BoardPatchValidationError,
} from "@/domain/work-orders/errors/board-patch-errors";
import {
buildCompletionDocFormData,
buildMediaFormData,
mapBoardDetailComment,
mapBoardDetailMediaItem,
mapCompletion,
type UploadableMediaCategory,
} from "@/domain/work-orders/mappers/work-order-board-detail-mapper";
import type {
WorkOrderBoardCompletion,
WorkOrderBoardMediaItem,
} from "@/domain/work-orders/types/work-order-board-detail";
import type { WorkOrderComment } from "@/domain/work-orders/types/work-order";
export interface CreateWorkOrderCommentPayload {
text: string;
mentions?: string[];
}
async function readResponseJson(response: Response): Promise<unknown> {
try {
return await response.json();
} catch {
return undefined;
}
}
function throwDocumentHttpError(status: number, data: unknown): never {
const record = typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
if (status === 409) {
throw new BoardPatchConflictError(
typeof record.message === "string"
? record.message
: "The record was modified by another user. Refresh and retry.",
null,
);
}
if (status === 422) {
throw new BoardPatchValidationError(
typeof record.code === "string" ? record.code : "ValidationError",
typeof record.message === "string" ? record.message : mapHttpStatusToMessage(422, data),
);
}
throw new ApiError(mapHttpStatusToMessage(status, data), status);
}
export const workOrderBoardDocumentsApi = {
listComments: async (workOrderId: string | number): Promise<WorkOrderComment[]> => {
const data = await apiGet<unknown>(API_PATHS.workOrder.comments(workOrderId));
const items = handleApiResponse<unknown[]>(data);
return Array.isArray(items) ? items.map(mapBoardDetailComment) : [];
},
createComment: async (
workOrderId: string | number,
text: string,
mentions?: string[],
): Promise<WorkOrderComment> => {
const payload: CreateWorkOrderCommentPayload = { text };
if (mentions && mentions.length > 0) {
payload.mentions = mentions.map(String);
}
const data = await apiPost<unknown>(API_PATHS.workOrder.comments(workOrderId), payload);
return mapBoardDetailComment(handleApiResponse(data));
},
updateComment: async (
workOrderId: string | number,
commentId: string | number,
text: string,
): Promise<WorkOrderComment> => {
const response = await api.patch(API_PATHS.workOrder.commentItem(workOrderId, commentId), {
json: { text },
throwHttpErrors: false,
});
const data = await readResponseJson(response);
if (response.ok) {
if (response.status === 204 || data == null) {
return mapBoardDetailComment({ id: commentId, text });
}
return mapBoardDetailComment(handleApiResponse(data) ?? data);
}
throwDocumentHttpError(response.status, data);
},
/** @deprecated Prefer createComment — modern board endpoint. */
addComment: async (
workOrderId: string | number,
text: string,
_commentType?: string,
): Promise<WorkOrderComment> => {
return workOrderBoardDocumentsApi.createComment(workOrderId, text);
},
listMedia: async (workOrderId: string | number): Promise<WorkOrderBoardMediaItem[]> => {
const data = await apiGet<unknown>(API_PATHS.workOrder.media(workOrderId));
const items = handleApiResponse<unknown[]>(data);
return Array.isArray(items) ? items.map(mapBoardDetailMediaItem) : [];
},
uploadMedia: async (
workOrderId: string | number,
file: File,
category?: UploadableMediaCategory,
options?: { signal?: AbortSignal; onProgress?: (percent: number) => void },
): Promise<WorkOrderBoardMediaItem> => {
const formData = buildMediaFormData(file, category);
const { ok, status, data } = await uploadFormWithProgress(
API_PATHS.workOrder.media(workOrderId),
formData,
{ signal: options?.signal, onProgress: options?.onProgress },
);
if (ok) {
return mapBoardDetailMediaItem(handleApiResponse(data) ?? data);
}
throwDocumentHttpError(status, data);
},
updateMediaCategory: async (
workOrderId: string | number,
mediaId: string | number,
category: UploadableMediaCategory,
workOrderVersion: string,
): Promise<WorkOrderBoardMediaItem> => {
if (!workOrderVersion) {
throw new BoardPatchValidationError(
"WorkOrderVersionRequired",
"workOrderVersion is required to update media category.",
);
}
const formData = new FormData();
formData.append("category", category);
formData.append("workOrderVersion", workOrderVersion);
const response = await api.patch(API_PATHS.workOrder.mediaItem(workOrderId, mediaId), {
body: formData,
throwHttpErrors: false,
});
const data = await readResponseJson(response);
if (response.ok) {
return mapBoardDetailMediaItem(handleApiResponse(data) ?? data);
}
throwDocumentHttpError(response.status, data);
},
deleteMedia: async (
workOrderId: string | number,
mediaId: string | number,
workOrderVersion: string,
): Promise<void> => {
if (!workOrderVersion) {
throw new BoardPatchValidationError(
"WorkOrderVersionRequired",
"workOrderVersion is required to delete media.",
);
}
const numericId = typeof mediaId === "number" ? mediaId : Number(mediaId);
if (!Number.isFinite(numericId) || numericId <= 0) {
throw new BoardPatchValidationError(
"InvalidMedia",
"Legacy media cannot be deleted via this endpoint.",
);
}
const response = await api.delete(API_PATHS.workOrder.mediaItem(workOrderId, numericId), {
searchParams: { workOrderVersion },
throwHttpErrors: false,
});
if (response.status === 204) {
return;
}
const data = await readResponseJson(response);
if (response.ok) {
return;
}
throwDocumentHttpError(response.status, data);
},
uploadCompletionDoc: async (
workOrderId: string | number,
params: {
file: File;
workOrderVersion: string;
signOffName?: string;
signOffSignature?: string;
},
): Promise<WorkOrderBoardCompletion> => {
if (!params.workOrderVersion) {
throw new BoardPatchValidationError(
"WorkOrderVersionRequired",
"workOrderVersion is required to upload a completion document.",
);
}
const formData = buildCompletionDocFormData(params);
const response = await api.post(API_PATHS.workOrder.completionDoc(workOrderId), {
body: formData,
throwHttpErrors: false,
});
const data = await readResponseJson(response);
if (response.ok) {
return mapCompletion(handleApiResponse(data) ?? data);
}
throwDocumentHttpError(response.status, data);
},
};