mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 20:53:12 +00:00
Merge branch 'dev' into fix/ab/sh-320-inline-sh-prefix-wo-number
This commit is contained in:
commit
cd611fd09c
7 changed files with 147 additions and 11 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)" }}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue