diff --git a/src/domain/work-orders/mappers/work-order-board-detail-mapper.ts b/src/domain/work-orders/mappers/work-order-board-detail-mapper.ts index e2ae7411..90aba625 100644 --- a/src/domain/work-orders/mappers/work-order-board-detail-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-board-detail-mapper.ts @@ -6,7 +6,10 @@ import type { WorkOrderBoardMediaItem, } from "@/domain/work-orders/types/work-order-board-detail"; import { BOARD_MEDIA_CATEGORY_API } from "@/domain/work-orders/types/work-order-board-detail"; -import type { CompletionDocStatus } from "@/domain/work-orders/types/work-order-table-row"; +import type { + CompletionDocStatus, + WorkOrderTableRow, +} from "@/domain/work-orders/types/work-order-table-row"; import type { WorkOrderAuditEntry, WorkOrderComment, @@ -269,6 +272,24 @@ export function boardMediaItemsToMediaFiles(items: WorkOrderBoardMediaItem[]): M })); } +/** + * `GET workorders/{id}/detail` info omits the board row's `MediaCount`, so the table-row mapper + * defaults it to 0 and the slide-over closability gate would report "At least 1 photo" even + * after an upload (SH-339). Keep an explicit count when present; otherwise count the same + * set the board counts — stored (non-legacy) attachments. + */ +function resolveDetailMediaCount( + infoRaw: unknown, + info: WorkOrderTableRow, + media: WorkOrderBoardMediaItem[], +): number { + const record = asRecord(infoRaw); + if (typeof (record.mediaCount ?? record.MediaCount) === "number") { + return info.mediaCount; + } + return media.filter((item) => !item.isLegacy).length; +} + export function mapWorkOrderBoardDetail(raw: unknown): WorkOrderBoardDetail { const root = asRecord(raw); const infoRaw = root.info ?? root.Info ?? root; @@ -283,8 +304,10 @@ export function mapWorkOrderBoardDetail(raw: unknown): WorkOrderBoardDetail { : []; const media = Array.isArray(mediaRaw) ? mediaRaw.map(mapBoardDetailMediaItem) : []; + const info = mapWorkOrderTableRow(infoRaw); + return { - info: mapWorkOrderTableRow(infoRaw), + info: { ...info, mediaCount: resolveDetailMediaCount(infoRaw, info, media) }, completion: mapCompletion(completionRaw ?? {}), comments, audit, diff --git a/src/test/domain/work-orders/mappers/work-order-board-detail-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-board-detail-mapper.test.ts index 7f34bcc8..b393f716 100644 --- a/src/test/domain/work-orders/mappers/work-order-board-detail-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-board-detail-mapper.test.ts @@ -6,6 +6,10 @@ import { mapBoardDetailMediaItem, mapWorkOrderBoardDetail, } from "@/domain/work-orders/mappers/work-order-board-detail-mapper"; +import { + getClosabilityGaps, + tableRowToClosabilityInput, +} from "@/domain/work-orders/utils/wo-closability"; const SAMPLE_DETAIL = { info: { @@ -279,3 +283,44 @@ describe("mapWorkOrderBoardDetail", () => { expect(completionForm.get("file")).toBeInstanceOf(File); }); }); + +describe("mapWorkOrderBoardDetail media count (SH-339)", () => { + const reactiveInfo = { ...SAMPLE_DETAIL.info, workOrderType: 6 }; + + it("derives mediaCount from stored media when detail info omits it", () => { + const detail = mapWorkOrderBoardDetail({ ...SAMPLE_DETAIL, info: reactiveInfo }); + + expect(detail.info.type).toBe("Reactive"); + expect(detail.info.mediaCount).toBe(2); + expect(getClosabilityGaps(tableRowToClosabilityInput(detail.info))).not.toContain( + "At least 1 photo", + ); + }); + + it("does not count legacy URL-column media, matching the board row count", () => { + const detail = mapWorkOrderBoardDetail({ + ...SAMPLE_DETAIL, + info: reactiveInfo, + media: [ + { + id: -1, + category: 1, + url: "https://cdn.example.com/legacy.jpg", + fileName: "legacy.jpg", + isLegacy: true, + }, + ], + }); + + expect(detail.info.mediaCount).toBe(0); + }); + + it("keeps an explicit info mediaCount", () => { + const detail = mapWorkOrderBoardDetail({ + ...SAMPLE_DETAIL, + info: { ...reactiveInfo, mediaCount: 5 }, + }); + + expect(detail.info.mediaCount).toBe(5); + }); +});