From a54fcaae9fc953727c3d635035c6467c1b34823f Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Wed, 9 Sep 2026 17:09:28 -0300 Subject: [PATCH] fix(work-orders): persist extra media placement --- .../detail/use-slide-over-media-state.ts | 100 ++++++++++++++---- .../media/media-uploader-grid-item-meta.tsx | 2 +- .../media-uploader-grid-item-meta.test.tsx | 35 ++++++ .../use-slide-over-media-state.test.tsx | 40 +++++-- 4 files changed, 146 insertions(+), 31 deletions(-) create mode 100644 src/test/app/(protected)/workorders/media-uploader-grid-item-meta.test.tsx diff --git a/src/app/(protected)/workorders/_components/detail/use-slide-over-media-state.ts b/src/app/(protected)/workorders/_components/detail/use-slide-over-media-state.ts index fe011400..346ecb57 100644 --- a/src/app/(protected)/workorders/_components/detail/use-slide-over-media-state.ts +++ b/src/app/(protected)/workorders/_components/detail/use-slide-over-media-state.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "react-toastify"; import type { MediaUploadRequest } from "@/app/(protected)/workorders/_components/media/media-uploader"; import { useMediaUploadSession } from "@/app/(protected)/workorders/_hooks/use-media-upload-session"; @@ -14,6 +14,7 @@ import type { import { isCompletionSurfaceMedia, isExtrasSurfaceMedia, + type MediaSurfaceOwnership, } from "@/domain/work-orders/utils/partition-slide-over-media"; type UseSlideOverMediaStateArgs = { @@ -37,34 +38,83 @@ function dropOwnedId(previous: Set, id: string): Set { return next; } +const OWNERSHIP_STORAGE_PREFIX = "work-order-media-surface:"; + +function loadOwnership(workOrderId: string | number | undefined): MediaSurfaceOwnership { + if (workOrderId == null) return { completionOwnedIds: new Set(), extraOwnedIds: new Set() }; + try { + const stored = sessionStorage.getItem(`${OWNERSHIP_STORAGE_PREFIX}${workOrderId}`); + if (!stored) return { completionOwnedIds: new Set(), extraOwnedIds: new Set() }; + const parsed = JSON.parse(stored) as { completion?: unknown; extra?: unknown }; + return { + completionOwnedIds: new Set(Array.isArray(parsed.completion) ? parsed.completion : []), + extraOwnedIds: new Set(Array.isArray(parsed.extra) ? parsed.extra : []), + }; + } catch { + return { completionOwnedIds: new Set(), extraOwnedIds: new Set() }; + } +} + +function saveOwnership( + workOrderId: string | number | undefined, + ownership: MediaSurfaceOwnership, +): void { + if (workOrderId == null) return; + try { + sessionStorage.setItem( + `${OWNERSHIP_STORAGE_PREFIX}${workOrderId}`, + JSON.stringify({ + completion: [...ownership.completionOwnedIds], + extra: [...ownership.extraOwnedIds], + }), + ); + } catch { + return; + } +} + export function useSlideOverMediaState({ detailForRow, uploadMediaMutation, deleteMediaMutation, categorizeMediaMutation, }: UseSlideOverMediaStateArgs) { + const workOrderId = detailForRow?.info.id; const [mediaBatchPending, setMediaBatchPending] = useState(false); - const [completionOwnedIds, setCompletionOwnedIds] = useState>(() => new Set()); - const [extraOwnedIds, setExtraOwnedIds] = useState>(() => new Set()); + const [ownership, setOwnershipState] = useState(() => + loadOwnership(workOrderId), + ); + + useEffect(() => { + setOwnershipState(loadOwnership(workOrderId)); + }, [workOrderId]); + + const updateOwnership = useCallback( + (update: (current: MediaSurfaceOwnership) => MediaSurfaceOwnership) => { + setOwnershipState((current) => { + const next = update(current); + saveOwnership(workOrderId, next); + return next; + }); + }, + [workOrderId], + ); const persistCompletionUpload = useCallback( async (input: UploadWorkOrderMediaInput) => { const item = await uploadMediaMutation.mutateAsync(input); - setCompletionOwnedIds((previous) => addOwnedId(previous, item.id)); - setExtraOwnedIds((previous) => dropOwnedId(previous, item.id)); + updateOwnership((current) => ({ + completionOwnedIds: addOwnedId(new Set(current.completionOwnedIds), item.id), + extraOwnedIds: dropOwnedId(new Set(current.extraOwnedIds), item.id), + })); return item; }, - [uploadMediaMutation], + [updateOwnership, uploadMediaMutation], ); const { uploads, startUploads, retryUpload, cancelUpload, removeLocal, isLocal } = useMediaUploadSession(detailForRow, persistCompletionUpload); - const ownership = useMemo( - () => ({ completionOwnedIds, extraOwnedIds }), - [completionOwnedIds, extraOwnedIds], - ); - const completionMedia = useMemo( () => uploads.filter((item) => isCompletionSurfaceMedia(item, ownership)), [uploads, ownership], @@ -96,8 +146,10 @@ export function useSlideOverMediaState({ for (const file of files) { // Upload stays Extra; Aveta is applied only by an explicit Extra Docs PATCH. const item = await uploadMediaMutation.mutateAsync({ file, category: "Extra" }); - setExtraOwnedIds((previous) => addOwnedId(previous, item.id)); - setCompletionOwnedIds((previous) => dropOwnedId(previous, item.id)); + updateOwnership((current) => ({ + completionOwnedIds: dropOwnedId(new Set(current.completionOwnedIds), item.id), + extraOwnedIds: addOwnedId(new Set(current.extraOwnedIds), item.id), + })); } } finally { setMediaBatchPending(false); @@ -110,14 +162,20 @@ export function useSlideOverMediaState({ toast.error("Missing work order version. Refresh and try again."); return; } - if (category === "Extra") { - setExtraOwnedIds((previous) => addOwnedId(previous, mediaId)); - setCompletionOwnedIds((previous) => dropOwnedId(previous, mediaId)); - } else { - setExtraOwnedIds((previous) => dropOwnedId(previous, mediaId)); - setCompletionOwnedIds((previous) => dropOwnedId(previous, mediaId)); - } - categorizeMediaMutation.mutate({ mediaId, category, workOrderVersion }); + categorizeMediaMutation.mutate( + { mediaId, category, workOrderVersion }, + { + onSuccess: () => { + updateOwnership((current) => ({ + completionOwnedIds: dropOwnedId(new Set(current.completionOwnedIds), mediaId), + extraOwnedIds: + category === "Extra" + ? addOwnedId(new Set(current.extraOwnedIds), mediaId) + : dropOwnedId(new Set(current.extraOwnedIds), mediaId), + })); + }, + }, + ); }; const removeMedia = (id: string) => { diff --git a/src/app/(protected)/workorders/_components/media/media-uploader-grid-item-meta.tsx b/src/app/(protected)/workorders/_components/media/media-uploader-grid-item-meta.tsx index 324538ab..209e5a2e 100644 --- a/src/app/(protected)/workorders/_components/media/media-uploader-grid-item-meta.tsx +++ b/src/app/(protected)/workorders/_components/media/media-uploader-grid-item-meta.tsx @@ -38,7 +38,7 @@ export function MediaUploaderGridItemMeta({ {needsCategory && (