import { API_PATHS } from "@/api/api-paths"; import { ApiError, mapHttpStatusToMessage } from "@/api/api-error"; import { apiGet, apiPost, apiRequestRaw } from "@/api/api"; import { handleApiResponse } from "@/api/handle-api-response"; 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 { try { return await response.json(); } catch { return undefined; } } function throwDocumentHttpError(response: Response, data: unknown): never { const record = typeof data === "object" && data !== null ? (data as Record) : {}; if (response.status === 409) { throw new BoardPatchConflictError( typeof record.message === "string" ? record.message : "The record was modified by another user. Refresh and retry.", null, ); } if (response.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(response.status, data), response.status); } export const workOrderBoardDocumentsApi = { listComments: async (workOrderId: string | number): Promise => { const data = await apiGet(API_PATHS.workOrder.comments(workOrderId)); const items = handleApiResponse(data); return Array.isArray(items) ? items.map(mapBoardDetailComment) : []; }, createComment: async ( workOrderId: string | number, text: string, mentions?: string[], ): Promise => { const payload: CreateWorkOrderCommentPayload = { text }; if (mentions && mentions.length > 0) { payload.mentions = mentions.map(String); } const data = await apiPost(API_PATHS.workOrder.comments(workOrderId), payload); return mapBoardDetailComment(handleApiResponse(data)); }, updateComment: async ( workOrderId: string | number, commentId: string | number, text: string, ): Promise => { const response = await apiRequestRaw( "patch", API_PATHS.workOrder.commentItem(workOrderId, commentId), { json: { text }, throwHttpErrors: false }, "workOrderBoardDocumentsApi.updateComment", ); 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, data); }, /** @deprecated Prefer createComment — modern board endpoint. */ addComment: async ( workOrderId: string | number, text: string, _commentType?: string, ): Promise => { return workOrderBoardDocumentsApi.createComment(workOrderId, text); }, listMedia: async (workOrderId: string | number): Promise => { const data = await apiGet(API_PATHS.workOrder.media(workOrderId)); const items = handleApiResponse(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 => { const formData = buildMediaFormData(file, category); const response = await apiRequestRaw( "post", API_PATHS.workOrder.media(workOrderId), { body: formData, throwHttpErrors: false, signal: options?.signal, onUploadProgress: options?.onProgress ? (progress) => { options.onProgress?.(Math.round(progress.percent * 100)); } : undefined, }, "workOrderBoardDocumentsApi.uploadMedia", ); const data = await readResponseJson(response); if (response.ok) { return mapBoardDetailMediaItem(handleApiResponse(data) ?? data); } throwDocumentHttpError(response, data); }, updateMediaCategory: async ( workOrderId: string | number, mediaId: string | number, category: UploadableMediaCategory, workOrderVersion: string, ): Promise => { 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 apiRequestRaw( "patch", API_PATHS.workOrder.mediaItem(workOrderId, mediaId), { body: formData, throwHttpErrors: false }, "workOrderBoardDocumentsApi.updateMediaCategory", ); const data = await readResponseJson(response); if (response.ok) { return mapBoardDetailMediaItem(handleApiResponse(data) ?? data); } throwDocumentHttpError(response, data); }, deleteMedia: async ( workOrderId: string | number, mediaId: string | number, workOrderVersion: string, ): Promise => { 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 apiRequestRaw( "delete", API_PATHS.workOrder.mediaItem(workOrderId, numericId), { searchParams: { workOrderVersion }, throwHttpErrors: false }, "workOrderBoardDocumentsApi.deleteMedia", ); if (response.status === 204) { return; } const data = await readResponseJson(response); if (response.ok) { return; } throwDocumentHttpError(response, data); }, uploadCompletionDoc: async ( workOrderId: string | number, params: { file: File; workOrderVersion: string; signOffName?: string; signOffSignature?: string; }, ): Promise => { if (!params.workOrderVersion) { throw new BoardPatchValidationError( "WorkOrderVersionRequired", "workOrderVersion is required to upload a completion document.", ); } const formData = buildCompletionDocFormData(params); const response = await apiRequestRaw( "post", API_PATHS.workOrder.completionDoc(workOrderId), { body: formData, throwHttpErrors: false }, "workOrderBoardDocumentsApi.uploadCompletionDoc", ); const data = await readResponseJson(response); if (response.ok) { return mapCompletion(handleApiResponse(data) ?? data); } throwDocumentHttpError(response, data); }, };