fix(work-orders): prefer stored media over a zero detail media count

The detail endpoint's info inherits MediaCount from the board row DTO but
the service never sets it, so it serializes as 0. The previous guard trusted
any numeric value and returned that 0, leaving the slide-over completion gate
reporting "At least 1 photo" even after an upload. Take the larger of the
reported count and the stored (non-legacy) attachments so a positive API
count still wins while a spurious or omitted 0 no longer masks present media.
Dropping the unused mapped-row argument also removes a flat-payload path that
would have counted legacy items.
This commit is contained in:
Codex Review Integration 2026-09-16 18:05:50 -03:00
parent 74765427bc
commit a9d8416f0f
2 changed files with 23 additions and 18 deletions

View file

@ -6,10 +6,7 @@ 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,
WorkOrderTableRow,
} from "@/domain/work-orders/types/work-order-table-row";
import type { CompletionDocStatus } from "@/domain/work-orders/types/work-order-table-row";
import type {
WorkOrderAuditEntry,
WorkOrderComment,
@ -273,21 +270,17 @@ 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. Keep an explicit count when present; otherwise count the same
* set the board counts — stored (non-legacy) attachments.
* `GET workorders/{id}/detail` info serializes the board row's `MediaCount` as 0 — the detail
* DTO inherits the field but the service never sets it — so trusting it verbatim would leave the
* slide-over closability gate reporting "At least 1 photo" even after an upload. Take the larger
* of the reported count and the stored (non-legacy) attachments: a positive API count still wins,
* but a spurious (or omitted) 0 can no longer mask media that are actually present.
*/
function resolveDetailMediaCount(
infoRaw: unknown,
info: WorkOrderTableRow,
media: WorkOrderBoardMediaItem[],
): number {
function resolveDetailMediaCount(infoRaw: unknown, media: WorkOrderBoardMediaItem[]): number {
const record = asRecord(infoRaw);
if (typeof (record.mediaCount ?? record.MediaCount) === "number") {
return info.mediaCount;
}
return media.filter((item) => !item.isLegacy).length;
const explicit = record.mediaCount ?? record.MediaCount;
const nonLegacyCount = media.filter((item) => !item.isLegacy).length;
return Math.max(typeof explicit === "number" ? explicit : 0, nonLegacyCount);
}
export function mapWorkOrderBoardDetail(raw: unknown): WorkOrderBoardDetail {
@ -307,7 +300,7 @@ export function mapWorkOrderBoardDetail(raw: unknown): WorkOrderBoardDetail {
const info = mapWorkOrderTableRow(infoRaw);
return {
info: { ...info, mediaCount: resolveDetailMediaCount(infoRaw, info, media) },
info: { ...info, mediaCount: resolveDetailMediaCount(infoRaw, media) },
completion: mapCompletion(completionRaw ?? {}),
comments,
audit,

View file

@ -297,6 +297,18 @@ describe("mapWorkOrderBoardDetail media count (SH-339)", () => {
);
});
it("prefers stored media when detail info reports an explicit zero", () => {
const detail = mapWorkOrderBoardDetail({
...SAMPLE_DETAIL,
info: { ...reactiveInfo, mediaCount: 0 },
});
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,