mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 11:22:05 +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 { WorkOrderBoardCompletion } from "@/domain/work-orders/types/work-order-board-detail";
|
||||||
import type { MediaFile } from "@/domain/work-orders/types/work-order-media";
|
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 { 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 {
|
import {
|
||||||
formatMediaSizeLimit,
|
formatMediaSizeLimit,
|
||||||
isWorkOrderFileTooLarge,
|
isWorkOrderFileTooLarge,
|
||||||
|
|
@ -16,10 +16,6 @@ import { SlideOverCompletionMedia } from "./slide-over-completion-media";
|
||||||
import { SlideOverCompletionReadonlyStatus } from "./slide-over-completion-readonly-status";
|
import { SlideOverCompletionReadonlyStatus } from "./slide-over-completion-readonly-status";
|
||||||
import { SlideOverCompletionSignoffs } from "./slide-over-completion-signoffs";
|
import { SlideOverCompletionSignoffs } from "./slide-over-completion-signoffs";
|
||||||
|
|
||||||
function isMediaWO(type: WOType | ""): boolean {
|
|
||||||
return type === "Emergency" || type === "Reactive";
|
|
||||||
}
|
|
||||||
|
|
||||||
type SlideOverCompletionTabProps = {
|
type SlideOverCompletionTabProps = {
|
||||||
row: WorkOrderTableRow;
|
row: WorkOrderTableRow;
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
|
|
@ -56,7 +52,7 @@ export function SlideOverCompletionTab({
|
||||||
const templateUrl = completion?.template?.templateUrl;
|
const templateUrl = completion?.template?.templateUrl;
|
||||||
const signOffAttachment = completion?.signOffAttachment;
|
const signOffAttachment = completion?.signOffAttachment;
|
||||||
|
|
||||||
if (isMediaWO(row.type)) {
|
if (hasCompletionMediaGallery(row.type)) {
|
||||||
return (
|
return (
|
||||||
<SlideOverCompletionMedia
|
<SlideOverCompletionMedia
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,14 @@ const FILETYPE_ICON: Record<FileCategory, typeof ImageIcon> = {
|
||||||
other: Paperclip,
|
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">> = [
|
const EXTRA_DOC_PURPOSES: Array<Extract<UploadableMediaCategory, "Extra" | "Aveta">> = [
|
||||||
"Extra",
|
"Extra",
|
||||||
"Aveta",
|
"Aveta",
|
||||||
|
|
@ -92,8 +100,13 @@ function ExtraDocRow({
|
||||||
const purpose = file.purpose === "Aveta" || isAveta ? "Aveta" : "Extra";
|
const purpose = file.purpose === "Aveta" || isAveta ? "Aveta" : "Extra";
|
||||||
|
|
||||||
return (
|
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
|
<ItemIcon
|
||||||
|
aria-hidden
|
||||||
size={12}
|
size={12}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
style={{ color: isAveta ? "var(--warning)" : "var(--color-text-muted)" }}
|
style={{ color: isAveta ? "var(--warning)" : "var(--color-text-muted)" }}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
UploadWorkOrderMediaInput,
|
UploadWorkOrderMediaInput,
|
||||||
} from "@/domain/work-orders/use-cases/use-work-order-mutations";
|
} from "@/domain/work-orders/use-cases/use-work-order-mutations";
|
||||||
import {
|
import {
|
||||||
|
hasCompletionMediaGallery,
|
||||||
isCompletionSurfaceMedia,
|
isCompletionSurfaceMedia,
|
||||||
isExtrasSurfaceMedia,
|
isExtrasSurfaceMedia,
|
||||||
type MediaSurfaceOwnership,
|
type MediaSurfaceOwnership,
|
||||||
|
|
@ -80,6 +81,7 @@ export function useSlideOverMediaState({
|
||||||
categorizeMediaMutation,
|
categorizeMediaMutation,
|
||||||
}: UseSlideOverMediaStateArgs) {
|
}: UseSlideOverMediaStateArgs) {
|
||||||
const workOrderId = detailForRow?.info.id;
|
const workOrderId = detailForRow?.info.id;
|
||||||
|
const completionHasGallery = hasCompletionMediaGallery(detailForRow?.info.type);
|
||||||
const [mediaBatchPending, setMediaBatchPending] = useState(false);
|
const [mediaBatchPending, setMediaBatchPending] = useState(false);
|
||||||
const [ownership, setOwnershipState] = useState<MediaSurfaceOwnership>(() =>
|
const [ownership, setOwnershipState] = useState<MediaSurfaceOwnership>(() =>
|
||||||
loadOwnership(workOrderId),
|
loadOwnership(workOrderId),
|
||||||
|
|
@ -116,16 +118,16 @@ export function useSlideOverMediaState({
|
||||||
useMediaUploadSession(detailForRow, persistCompletionUpload);
|
useMediaUploadSession(detailForRow, persistCompletionUpload);
|
||||||
|
|
||||||
const completionMedia = useMemo(
|
const completionMedia = useMemo(
|
||||||
() => uploads.filter((item) => isCompletionSurfaceMedia(item, ownership)),
|
() => uploads.filter((item) => isCompletionSurfaceMedia(item, ownership, completionHasGallery)),
|
||||||
[uploads, ownership],
|
[uploads, ownership, completionHasGallery],
|
||||||
);
|
);
|
||||||
|
|
||||||
const extrasUploads = useMemo(
|
const extrasUploads = useMemo(
|
||||||
() =>
|
() =>
|
||||||
boardMediaItemsToMediaFiles(detailForRow?.media ?? []).filter((item) =>
|
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");
|
const hasLocalUploading = uploads.some((item) => item.uploadStatus === "uploading");
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,11 @@ const EMPTY_OWNERSHIP: MediaSurfaceOwnership = {
|
||||||
extraOwnedIds: new Set(),
|
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 {
|
function isPhotoOrVideo(item: MediaFile): boolean {
|
||||||
return item.fileType === "image" || item.fileType === "video";
|
return item.fileType === "image" || item.fileType === "video";
|
||||||
}
|
}
|
||||||
|
|
@ -24,20 +29,25 @@ function isInFlight(item: MediaFile): boolean {
|
||||||
export function isCompletionSurfaceMedia(
|
export function isCompletionSurfaceMedia(
|
||||||
item: MediaFile,
|
item: MediaFile,
|
||||||
ownership: MediaSurfaceOwnership = EMPTY_OWNERSHIP,
|
ownership: MediaSurfaceOwnership = EMPTY_OWNERSHIP,
|
||||||
|
completionHasGallery = true,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (isInFlight(item)) return true;
|
if (isInFlight(item)) return true;
|
||||||
if (COMPLETION_PURPOSES.has(item.purpose ?? "")) 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.completionOwnedIds.has(item.id)) return true;
|
||||||
if (ownership.extraOwnedIds.has(item.id)) return false;
|
if (ownership.extraOwnedIds.has(item.id)) return false;
|
||||||
return item.purpose === "Extra" && isPhotoOrVideo(item);
|
return item.purpose === "Extra" && isPhotoOrVideo(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Without a Completion gallery, every Extra item (including photos/videos) belongs to Extras. */
|
||||||
export function isExtrasSurfaceMedia(
|
export function isExtrasSurfaceMedia(
|
||||||
item: MediaFile,
|
item: MediaFile,
|
||||||
ownership: MediaSurfaceOwnership = EMPTY_OWNERSHIP,
|
ownership: MediaSurfaceOwnership = EMPTY_OWNERSHIP,
|
||||||
|
completionHasGallery = true,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (item.purpose === "Aveta") return true;
|
if (item.purpose === "Aveta") return true;
|
||||||
if (item.purpose !== "Extra") return false;
|
if (item.purpose !== "Extra") return false;
|
||||||
|
if (!completionHasGallery) return true;
|
||||||
if (ownership.completionOwnedIds.has(item.id)) return false;
|
if (ownership.completionOwnedIds.has(item.id)) return false;
|
||||||
if (ownership.extraOwnedIds.has(item.id)) return true;
|
if (ownership.extraOwnedIds.has(item.id)) return true;
|
||||||
return !isPhotoOrVideo(item);
|
return !isPhotoOrVideo(item);
|
||||||
|
|
|
||||||
|
|
@ -161,4 +161,40 @@ describe("SlideOverExtrasTab Aveta", () => {
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: "safety-form.pdf" })).toBeDisabled();
|
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.completionMedia).toHaveLength(0);
|
||||||
expect(remounted.result.current.extrasUploads.map((item) => item.id)).toEqual(["10"]);
|
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 { describe, expect, it } from "vitest";
|
||||||
import type { MediaFile } from "@/domain/work-orders/types/work-order-media";
|
import type { MediaFile } from "@/domain/work-orders/types/work-order-media";
|
||||||
import {
|
import {
|
||||||
|
hasCompletionMediaGallery,
|
||||||
isCompletionSurfaceMedia,
|
isCompletionSurfaceMedia,
|
||||||
isExtrasSurfaceMedia,
|
isExtrasSurfaceMedia,
|
||||||
} from "@/domain/work-orders/utils/partition-slide-over-media";
|
} from "@/domain/work-orders/utils/partition-slide-over-media";
|
||||||
|
|
@ -76,4 +77,33 @@ describe("partition-slide-over-media", () => {
|
||||||
}),
|
}),
|
||||||
).toBe(true);
|
).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