mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
fix(work-orders): send workOrderVersion on media categorize/delete
Align CompDoc media mutations with backend PR #47 so categorize and delete no longer return 422 WorkOrderVersionRequired.
This commit is contained in:
parent
f66284d78c
commit
969f99a576
5 changed files with 148 additions and 12 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { type MouseEvent } from "react";
|
||||
import { Dialog, DialogContent, DialogTitle, Skeleton, Stack } from "@mui/material";
|
||||
import { Image as ImageIcon } from "lucide-react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
||||
import { LoadingRegion } from "@/components/ui/loading-region";
|
||||
|
|
@ -23,6 +24,14 @@ type MediaUploadModalProps = {
|
|||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
function resolveWorkOrderVersion(
|
||||
detailRowVersion: string | null | undefined,
|
||||
rowVersion: string | null | undefined,
|
||||
): string | null {
|
||||
const version = detailRowVersion?.trim() || rowVersion?.trim() || "";
|
||||
return version || null;
|
||||
}
|
||||
|
||||
export function MediaUploadModal({ row, open, onOpenChange }: MediaUploadModalProps) {
|
||||
const { data: detail, isLoading } = useWorkOrderBoardDetail(row.id, open);
|
||||
const uploadMutation = useUploadWorkOrderMedia(row.id);
|
||||
|
|
@ -38,6 +47,15 @@ export function MediaUploadModal({ row, open, onOpenChange }: MediaUploadModalPr
|
|||
uploadMutation.isPending || deleteMutation.isPending || categorizeMutation.isPending;
|
||||
const handleClose = guardDialogClose(pending, () => onOpenChange(false));
|
||||
|
||||
const requireVersion = (): string | null => {
|
||||
const version = resolveWorkOrderVersion(detail?.info.rowVersion, row.rowVersion);
|
||||
if (!version) {
|
||||
toast.error("Missing work order version. Refresh and try again.");
|
||||
return null;
|
||||
}
|
||||
return version;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
|
|
@ -81,9 +99,15 @@ export function MediaUploadModal({ row, open, onOpenChange }: MediaUploadModalPr
|
|||
removeLocal(id);
|
||||
return;
|
||||
}
|
||||
deleteMutation.mutate(id);
|
||||
const workOrderVersion = requireVersion();
|
||||
if (!workOrderVersion) return;
|
||||
deleteMutation.mutate({ mediaId: id, workOrderVersion });
|
||||
}}
|
||||
onCategorize={(id, category) => {
|
||||
const workOrderVersion = requireVersion();
|
||||
if (!workOrderVersion) return;
|
||||
categorizeMutation.mutate({ mediaId: id, category, workOrderVersion });
|
||||
}}
|
||||
onCategorize={(id, category) => categorizeMutation.mutate({ mediaId: id, category })}
|
||||
onRetry={retryUpload}
|
||||
onCancel={removeLocal}
|
||||
readOnly={isReadOnly}
|
||||
|
|
|
|||
|
|
@ -126,9 +126,18 @@ export const workOrderBoardDocumentsApi = {
|
|||
workOrderId: string | number,
|
||||
mediaId: string | number,
|
||||
category: UploadableMediaCategory,
|
||||
workOrderVersion: string,
|
||||
): Promise<WorkOrderBoardMediaItem> => {
|
||||
if (!workOrderVersion) {
|
||||
throw new BoardPatchValidationError(
|
||||
"WorkOrderVersionRequired",
|
||||
"workOrderVersion is required to update media category.",
|
||||
);
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("category", category);
|
||||
formData.append("workOrderVersion", workOrderVersion);
|
||||
const response = await api.patch(API_PATHS.workOrder.mediaItem(workOrderId, mediaId), {
|
||||
body: formData,
|
||||
throwHttpErrors: false,
|
||||
|
|
@ -140,7 +149,18 @@ export const workOrderBoardDocumentsApi = {
|
|||
throwDocumentHttpError(response, data);
|
||||
},
|
||||
|
||||
deleteMedia: async (workOrderId: string | number, mediaId: string | number): Promise<void> => {
|
||||
deleteMedia: async (
|
||||
workOrderId: string | number,
|
||||
mediaId: string | number,
|
||||
workOrderVersion: string,
|
||||
): Promise<void> => {
|
||||
if (!workOrderVersion) {
|
||||
throw new BoardPatchValidationError(
|
||||
"WorkOrderVersionRequired",
|
||||
"workOrderVersion is required to delete media.",
|
||||
);
|
||||
}
|
||||
|
||||
const numericId = typeof mediaId === "number" ? mediaId : Number(mediaId);
|
||||
if (!Number.isFinite(numericId) || numericId <= 0) {
|
||||
throw new BoardPatchValidationError(
|
||||
|
|
@ -150,6 +170,7 @@ export const workOrderBoardDocumentsApi = {
|
|||
}
|
||||
|
||||
const response = await api.delete(API_PATHS.workOrder.mediaItem(workOrderId, numericId), {
|
||||
searchParams: { workOrderVersion },
|
||||
throwHttpErrors: false,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ export interface UploadWorkOrderMediaInput {
|
|||
export interface UpdateWorkOrderMediaCategoryInput {
|
||||
mediaId: string | number;
|
||||
category: UploadableMediaCategory;
|
||||
workOrderVersion: string;
|
||||
}
|
||||
|
||||
export interface DeleteWorkOrderMediaInput {
|
||||
mediaId: string | number;
|
||||
workOrderVersion: string;
|
||||
}
|
||||
|
||||
export interface UploadCompletionDocInput {
|
||||
|
|
@ -217,8 +223,8 @@ export function useUpdateWorkOrderMediaCategory(
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ mediaId, category }: UpdateWorkOrderMediaCategoryInput) =>
|
||||
workOrdersApi.updateMediaCategory(workOrderId, mediaId, category),
|
||||
mutationFn: ({ mediaId, category, workOrderVersion }: UpdateWorkOrderMediaCategoryInput) =>
|
||||
workOrdersApi.updateMediaCategory(workOrderId, mediaId, category, workOrderVersion),
|
||||
onSuccess: () => {
|
||||
invalidateBoardDetail(queryClient, workOrderId);
|
||||
invalidateWorkOrders(queryClient);
|
||||
|
|
@ -232,11 +238,12 @@ export function useUpdateWorkOrderMediaCategory(
|
|||
|
||||
export function useDeleteWorkOrderMedia(
|
||||
workOrderId: string | number,
|
||||
): UseMutationResult<void, Error, string | number> {
|
||||
): UseMutationResult<void, Error, DeleteWorkOrderMediaInput> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (mediaId: string | number) => workOrdersApi.deleteMedia(workOrderId, mediaId),
|
||||
mutationFn: ({ mediaId, workOrderVersion }: DeleteWorkOrderMediaInput) =>
|
||||
workOrdersApi.deleteMedia(workOrderId, mediaId, workOrderVersion),
|
||||
onSuccess: () => {
|
||||
invalidateBoardDetail(queryClient, workOrderId);
|
||||
invalidateWorkOrders(queryClient);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const categorizeMutate = vi.fn();
|
|||
vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({
|
||||
useWorkOrderBoardDetail: () => ({
|
||||
data: {
|
||||
info: { status: "Scheduled" },
|
||||
info: { status: "Scheduled", rowVersion: "DETAIL-VER==" },
|
||||
media: [
|
||||
{
|
||||
id: "11",
|
||||
|
|
@ -80,7 +80,7 @@ function baseRow(): WorkOrderTableRow {
|
|||
title: "Emergency",
|
||||
priority: "Sev 1",
|
||||
primaryDispatchId: null,
|
||||
rowVersion: "",
|
||||
rowVersion: "ROW-VER==",
|
||||
dispatchRowVersion: "",
|
||||
flagColor: null,
|
||||
};
|
||||
|
|
@ -111,4 +111,32 @@ describe("MediaUploadModal API wiring", () => {
|
|||
expect(screen.getByText(/categorize after upload/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/category for before\.jpg/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("categorizes with workOrderVersion from detail.info.rowVersion", async () => {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
|
||||
const { fireEvent } = await import("@testing-library/react");
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={client}>
|
||||
<MediaUploadModal row={baseRow()} open onOpenChange={vi.fn()} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/category for before\.jpg/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/category for before\.jpg/i), {
|
||||
target: { value: "After" },
|
||||
});
|
||||
|
||||
expect(categorizeMutate).toHaveBeenCalledWith({
|
||||
mediaId: "11",
|
||||
category: "After",
|
||||
workOrderVersion: "DETAIL-VER==",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -494,12 +494,55 @@ describe("workOrdersApi.uploadMedia", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("workOrdersApi.updateMediaCategory", () => {
|
||||
beforeEach(() => {
|
||||
apiPatchFn.mockReset();
|
||||
});
|
||||
|
||||
it("PATCHes FormData with category and workOrderVersion", async () => {
|
||||
apiPatchFn.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
id: 12,
|
||||
category: 1,
|
||||
url: "https://cdn.example.com/before.jpg",
|
||||
fileName: "before.jpg",
|
||||
uploadedAt: "2026-07-16T12:00:00Z",
|
||||
isLegacy: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const item = await workOrdersApi.updateMediaCategory(10, 12, "Before", "AAAAAAA=");
|
||||
|
||||
expect(apiPatchFn).toHaveBeenCalledWith(
|
||||
API_PATHS.workOrder.mediaItem(10, 12),
|
||||
expect.objectContaining({ throwHttpErrors: false }),
|
||||
);
|
||||
const body = apiPatchFn.mock.calls[0]?.[1]?.body as FormData;
|
||||
expect(body.get("category")).toBe("Before");
|
||||
expect(body.get("workOrderVersion")).toBe("AAAAAAA=");
|
||||
expect(item.category).toBe("Before");
|
||||
});
|
||||
|
||||
it("rejects missing workOrderVersion client-side", async () => {
|
||||
const { BoardPatchValidationError } =
|
||||
await import("@/domain/work-orders/errors/board-patch-errors");
|
||||
|
||||
await expect(workOrdersApi.updateMediaCategory(10, 12, "After", "")).rejects.toMatchObject({
|
||||
name: "BoardPatchValidationError",
|
||||
code: "WorkOrderVersionRequired",
|
||||
});
|
||||
expect(apiPatchFn).not.toHaveBeenCalled();
|
||||
expect(BoardPatchValidationError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workOrdersApi.deleteMedia", () => {
|
||||
beforeEach(() => {
|
||||
apiDeleteFn.mockReset();
|
||||
});
|
||||
|
||||
it("DELETEs media item and accepts 204", async () => {
|
||||
it("DELETEs media item with workOrderVersion query and accepts 204", async () => {
|
||||
apiDeleteFn.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 204,
|
||||
|
|
@ -508,18 +551,31 @@ describe("workOrdersApi.deleteMedia", () => {
|
|||
},
|
||||
});
|
||||
|
||||
await workOrdersApi.deleteMedia(10, 12);
|
||||
await workOrdersApi.deleteMedia(10, 12, "AAAAAAA=");
|
||||
|
||||
expect(apiDeleteFn).toHaveBeenCalledWith(API_PATHS.workOrder.mediaItem(10, 12), {
|
||||
searchParams: { workOrderVersion: "AAAAAAA=" },
|
||||
throwHttpErrors: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects missing workOrderVersion client-side", async () => {
|
||||
const { BoardPatchValidationError } =
|
||||
await import("@/domain/work-orders/errors/board-patch-errors");
|
||||
|
||||
await expect(workOrdersApi.deleteMedia(10, 12, "")).rejects.toMatchObject({
|
||||
name: "BoardPatchValidationError",
|
||||
code: "WorkOrderVersionRequired",
|
||||
});
|
||||
expect(apiDeleteFn).not.toHaveBeenCalled();
|
||||
expect(BoardPatchValidationError).toBeTruthy();
|
||||
});
|
||||
|
||||
it("rejects legacy media ids client-side", async () => {
|
||||
const { BoardPatchValidationError } =
|
||||
await import("@/domain/work-orders/errors/board-patch-errors");
|
||||
|
||||
await expect(workOrdersApi.deleteMedia(10, -1)).rejects.toBeInstanceOf(
|
||||
await expect(workOrdersApi.deleteMedia(10, -1, "AAAAAAA=")).rejects.toBeInstanceOf(
|
||||
BoardPatchValidationError,
|
||||
);
|
||||
expect(apiDeleteFn).not.toHaveBeenCalled();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue