fix(work-orders): count detail media for slide-over completion gate

Detail info omits MediaCount, so the slide-over closability gate always
reported a missing photo on Reactive/Emergency work orders. Refs SH-339.
This commit is contained in:
Codex Review Integration 2026-09-16 10:56:17 -03:00
parent fc3e7591dc
commit e7f8772802
2 changed files with 70 additions and 2 deletions

View file

@ -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,

View file

@ -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);
});
});