Merge branch 'dev' into fix/ab/sh-320-inline-sh-prefix-wo-number

This commit is contained in:
Alexandre Brandizzi 2026-09-16 18:52:23 -03:00 • committed by GitHub
commit cd611fd09c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 147 additions and 11 deletions

View file

@ -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 (
<SlideOverCompletionMedia
isReadOnly={isReadOnly}

View file

@ -34,6 +34,14 @@ const FILETYPE_ICON: Record<FileCategory, typeof ImageIcon> = {
other: Paperclip,
};
const FILETYPE_LABEL: Record<FileCategory, string> = {
image: "Photo",
video: "Video",
pdf: "PDF",
doc: "Document",
other: "File",
};
const EXTRA_DOC_PURPOSES: Array<Extract<UploadableMediaCategory, "Extra" | "Aveta">> = [
"Extra",
"Aveta",
@ -92,8 +100,13 @@ function ExtraDocRow({
const purpose = file.purpose === "Aveta" || isAveta ? "Aveta" : "Extra";
return (
<div className="flex items-center gap-2 py-1">
<div
role="group"
aria-label={`${FILETYPE_LABEL[file.fileType]}, ${purpose}: ${file.name}`}
className="flex items-center gap-2 py-1"
>
<ItemIcon
aria-hidden
size={12}
className="shrink-0"
style={{ color: isAveta ? "var(--warning)" : "var(--color-text-muted)" }}

View file

@ -12,6 +12,7 @@ import type {
UploadWorkOrderMediaInput,
} from "@/domain/work-orders/use-cases/use-work-order-mutations";
import {
hasCompletionMediaGallery,
isCompletionSurfaceMedia,
isExtrasSurfaceMedia,
type MediaSurfaceOwnership,
@ -80,6 +81,7 @@ export function useSlideOverMediaState({
categorizeMediaMutation,
}: UseSlideOverMediaStateArgs) {
const workOrderId = detailForRow?.info.id;
const completionHasGallery = hasCompletionMediaGallery(detailForRow?.info.type);
const [mediaBatchPending, setMediaBatchPending] = useState(false);
const [ownership, setOwnershipState] = useState<MediaSurfaceOwnership>(() =>
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");

View file

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

View file

@ -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(
<SlideOverExtrasTab
uploads={[sitePhoto, walkthrough, avetaDoc]}
isReadOnly
onUpload={vi.fn()}
onRemove={vi.fn()}
/>,
);
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();
});
});

View file

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

View file

@ -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<string>(),
};
expect(isCompletionSurfaceMedia(photo, ownership, false)).toBe(false);
expect(isExtrasSurfaceMedia(photo, ownership, false)).toBe(true);
});
});