diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab.tsx index 290ec603..c44465b3 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab.tsx @@ -6,7 +6,7 @@ import type { UploadableMediaCategory } from "@/domain/work-orders/mappers/work- import type { WorkOrderBoardCompletion } from "@/domain/work-orders/types/work-order-board-detail"; import type { MediaFile } from "@/domain/work-orders/types/work-order-media"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; -import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; +import { hasCompletionMediaGallery } from "@/domain/work-orders/utils/partition-slide-over-media"; import { formatMediaSizeLimit, isWorkOrderFileTooLarge, @@ -16,10 +16,6 @@ import { SlideOverCompletionMedia } from "./slide-over-completion-media"; import { SlideOverCompletionReadonlyStatus } from "./slide-over-completion-readonly-status"; import { SlideOverCompletionSignoffs } from "./slide-over-completion-signoffs"; -function isMediaWO(type: WOType | ""): boolean { - return type === "Emergency" || type === "Reactive"; -} - type SlideOverCompletionTabProps = { row: WorkOrderTableRow; isReadOnly: boolean; @@ -56,7 +52,7 @@ export function SlideOverCompletionTab({ const templateUrl = completion?.template?.templateUrl; const signOffAttachment = completion?.signOffAttachment; - if (isMediaWO(row.type)) { + if (hasCompletionMediaGallery(row.type)) { return ( = { other: Paperclip, }; +const FILETYPE_LABEL: Record = { + image: "Photo", + video: "Video", + pdf: "PDF", + doc: "Document", + other: "File", +}; + const EXTRA_DOC_PURPOSES: Array> = [ "Extra", "Aveta", @@ -92,8 +100,13 @@ function ExtraDocRow({ const purpose = file.purpose === "Aveta" || isAveta ? "Aveta" : "Extra"; return ( -
+
(() => loadOwnership(workOrderId), @@ -116,16 +118,16 @@ export function useSlideOverMediaState({ useMediaUploadSession(detailForRow, persistCompletionUpload); const completionMedia = useMemo( - () => uploads.filter((item) => isCompletionSurfaceMedia(item, ownership)), - [uploads, ownership], + () => uploads.filter((item) => isCompletionSurfaceMedia(item, ownership, completionHasGallery)), + [uploads, ownership, completionHasGallery], ); const extrasUploads = useMemo( () => boardMediaItemsToMediaFiles(detailForRow?.media ?? []).filter((item) => - isExtrasSurfaceMedia(item, ownership), + isExtrasSurfaceMedia(item, ownership, completionHasGallery), ), - [detailForRow?.media, ownership], + [detailForRow?.media, ownership, completionHasGallery], ); const hasLocalUploading = uploads.some((item) => item.uploadStatus === "uploading"); diff --git a/src/domain/work-orders/utils/partition-slide-over-media.ts b/src/domain/work-orders/utils/partition-slide-over-media.ts index 5de1e4ad..7092c696 100644 --- a/src/domain/work-orders/utils/partition-slide-over-media.ts +++ b/src/domain/work-orders/utils/partition-slide-over-media.ts @@ -12,6 +12,11 @@ const EMPTY_OWNERSHIP: MediaSurfaceOwnership = { extraOwnedIds: new Set(), }; +/** Only Reactive and Emergency work orders render a media gallery on the Completion tab. */ +export function hasCompletionMediaGallery(type: string | null | undefined): boolean { + return type === "Emergency" || type === "Reactive"; +} + function isPhotoOrVideo(item: MediaFile): boolean { return item.fileType === "image" || item.fileType === "video"; } @@ -24,20 +29,25 @@ function isInFlight(item: MediaFile): boolean { export function isCompletionSurfaceMedia( item: MediaFile, ownership: MediaSurfaceOwnership = EMPTY_OWNERSHIP, + completionHasGallery = true, ): boolean { if (isInFlight(item)) return true; if (COMPLETION_PURPOSES.has(item.purpose ?? "")) return true; + if (!completionHasGallery && item.purpose === "Extra") return false; if (ownership.completionOwnedIds.has(item.id)) return true; if (ownership.extraOwnedIds.has(item.id)) return false; return item.purpose === "Extra" && isPhotoOrVideo(item); } +/** Without a Completion gallery, every Extra item (including photos/videos) belongs to Extras. */ export function isExtrasSurfaceMedia( item: MediaFile, ownership: MediaSurfaceOwnership = EMPTY_OWNERSHIP, + completionHasGallery = true, ): boolean { if (item.purpose === "Aveta") return true; if (item.purpose !== "Extra") return false; + if (!completionHasGallery) return true; if (ownership.completionOwnedIds.has(item.id)) return false; if (ownership.extraOwnedIds.has(item.id)) return true; return !isPhotoOrVideo(item); diff --git a/src/test/app/(protected)/workorders/slide-over-extras-aveta.test.tsx b/src/test/app/(protected)/workorders/slide-over-extras-aveta.test.tsx index f788e250..ba674966 100644 --- a/src/test/app/(protected)/workorders/slide-over-extras-aveta.test.tsx +++ b/src/test/app/(protected)/workorders/slide-over-extras-aveta.test.tsx @@ -161,4 +161,40 @@ describe("SlideOverExtrasTab Aveta", () => { expect(screen.getByRole("button", { name: "safety-form.pdf" })).toBeDisabled(); }); + + it("labels each extra item with its type and category", () => { + const sitePhoto: MediaFile = { + id: "m3", + name: "site.jpg", + fileType: "image", + url: "blob:3", + category: "Photo", + purpose: "Extra", + }; + const walkthrough: MediaFile = { + id: "m4", + name: "walkthrough.mp4", + fileType: "video", + url: "blob:4", + category: "Video", + purpose: "Extra", + }; + + renderWithProviders( + , + ); + + expect(screen.getByRole("group", { name: "Photo, Extra: site.jpg" })).toBeInTheDocument(); + expect( + screen.getByRole("group", { name: "Video, Extra: walkthrough.mp4" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("group", { name: "PDF, Aveta: aveta-clearance.pdf" }), + ).toBeInTheDocument(); + }); }); diff --git a/src/test/app/(protected)/workorders/use-slide-over-media-state.test.tsx b/src/test/app/(protected)/workorders/use-slide-over-media-state.test.tsx index 87a74de5..dab9e3bd 100644 --- a/src/test/app/(protected)/workorders/use-slide-over-media-state.test.tsx +++ b/src/test/app/(protected)/workorders/use-slide-over-media-state.test.tsx @@ -278,4 +278,53 @@ describe("useSlideOverMediaState completion filter", () => { expect(remounted.result.current.completionMedia).toHaveLength(0); expect(remounted.result.current.extrasUploads.map((item) => item.id)).toEqual(["10"]); }); + + it("shows Extra photos and videos in Extra Docs on PM work orders", () => { + const detail = detailWithMedia( + [ + { + id: "10", + category: "Extra", + url: "https://cdn.example.com/site.jpg", + fileName: "site.jpg", + uploadedAt: "2026-07-15T18:00:00.0000000Z", + isLegacy: false, + }, + { + id: "11", + category: "Extra", + url: "https://cdn.example.com/walkthrough.mp4", + fileName: "walkthrough.mp4", + uploadedAt: "2026-07-15T18:00:00.0000000Z", + isLegacy: false, + }, + { + id: "4", + category: "Extra", + url: "https://cdn.example.com/extra.pdf", + fileName: "extra.pdf", + uploadedAt: "2026-07-15T18:00:00.0000000Z", + isLegacy: false, + }, + ], + { id: 200, type: "PM" }, + ); + + const { result } = renderHook(() => + useSlideOverMediaState({ + detailForRow: detail, + uploadMediaMutation: idleMutation(), + deleteMediaMutation: idleMutation(), + categorizeMediaMutation: idleMutation(), + }), + ); + + expect(result.current.completionMedia).toHaveLength(0); + expect(result.current.extrasUploads.map((item) => item.id)).toEqual(["10", "11", "4"]); + expect(result.current.extrasUploads.map((item) => item.fileType)).toEqual([ + "image", + "video", + "pdf", + ]); + }); }); diff --git a/src/test/domain/work-orders/utils/partition-slide-over-media.test.ts b/src/test/domain/work-orders/utils/partition-slide-over-media.test.ts index 077eff57..1970db61 100644 --- a/src/test/domain/work-orders/utils/partition-slide-over-media.test.ts +++ b/src/test/domain/work-orders/utils/partition-slide-over-media.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import type { MediaFile } from "@/domain/work-orders/types/work-order-media"; import { + hasCompletionMediaGallery, isCompletionSurfaceMedia, isExtrasSurfaceMedia, } from "@/domain/work-orders/utils/partition-slide-over-media"; @@ -76,4 +77,33 @@ describe("partition-slide-over-media", () => { }), ).toBe(true); }); + + it("renders a Completion gallery only for Reactive and Emergency work orders", () => { + expect(hasCompletionMediaGallery("Reactive")).toBe(true); + expect(hasCompletionMediaGallery("Emergency")).toBe(true); + expect(hasCompletionMediaGallery("PM")).toBe(false); + expect(hasCompletionMediaGallery("Overdue")).toBe(false); + expect(hasCompletionMediaGallery("")).toBe(false); + }); + + it("puts Extra photos and videos on Extra Docs when Completion has no gallery", () => { + const photo = file({ id: "10", fileType: "image", purpose: "Extra" }); + const video = file({ id: "11", fileType: "video", purpose: "Extra", category: "Video" }); + + for (const item of [photo, video]) { + expect(isCompletionSurfaceMedia(item, undefined, false)).toBe(false); + expect(isExtrasSurfaceMedia(item, undefined, false)).toBe(true); + } + }); + + it("ignores stale completion ownership when Completion has no gallery", () => { + const photo = file({ id: "10", fileType: "image", purpose: "Extra" }); + const ownership = { + completionOwnedIds: new Set(["10"]), + extraOwnedIds: new Set(), + }; + + expect(isCompletionSurfaceMedia(photo, ownership, false)).toBe(false); + expect(isExtrasSurfaceMedia(photo, ownership, false)).toBe(true); + }); });