mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 10:23:11 +00:00
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.
224 lines
7 KiB
TypeScript
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);
|
|
},
|
|
};
|