Merge remote-tracking branch 'origin/feat/ab/sh-292-notification-center' into HEAD

# Conflicts:
#	src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts
This commit is contained in:
Codex Review Integration 2026-09-18 14:46:02 -03:00
commit ff1a946e74
2 changed files with 100 additions and 49 deletions

View file

@ -1,3 +1,4 @@
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useMemo, useState } from "react";
import type {
WorkOrderTablePatch,
@ -19,6 +20,8 @@ function isSlideOverInfoLocked(status: string | null | undefined): boolean {
return isWorkOrderFullyLocked(status) || isWorkOrderCoreLocked(status);
}
type StateSetter<T> = Dispatch<SetStateAction<T>>;
type UseSlideOverEditStateArgs = {
row: WorkOrderTableRow | null;
editMode: boolean;
@ -33,6 +36,47 @@ type UseSlideOverEditStateArgs = {
openRequestKey?: number;
};
function useSlideOverInitialization({
row,
editMode,
initialTab,
setTab,
setDraft,
setBaseline,
setEditing,
setShowUnsaved,
setUnsavedIntent,
}: {
row: WorkOrderTableRow | null;
editMode: boolean;
initialTab: SlideOverTab;
setTab: (tab: SlideOverTab) => void;
setDraft: StateSetter<WorkOrderTableRow | null>;
setBaseline: StateSetter<WorkOrderTableRow | null>;
setEditing: StateSetter<boolean>;
setShowUnsaved: StateSetter<boolean>;
setUnsavedIntent: StateSetter<"close" | "cancel-edit" | null>;
}) {
useEffect(() => {
if (!row) {
setDraft(null);
setBaseline(null);
setEditing(false);
setShowUnsaved(false);
setUnsavedIntent(null);
return;
}
setDraft(row);
setBaseline(row);
setEditing(editMode && !isSlideOverInfoLocked(row.status));
setTab(editMode ? "info" : initialTab);
setShowUnsaved(false);
setUnsavedIntent(null);
// Intentionally identity-only: depending on `row` would absorb optimistic patches.
// eslint-disable-next-line react-hooks/exhaustive-deps -- row.id / editMode gate
}, [row?.id, editMode]);
}
export function useSlideOverEditState({
row,
editMode,
@ -53,24 +97,17 @@ export function useSlideOverEditState({
// Reset draft/baseline only when the opened WO identity or editMode intent changes —
// not on every optimistic parent-row field update during a failed/in-flight save.
useEffect(() => {
if (!row) {
setDraft(null);
setBaseline(null);
setEditing(false);
setShowUnsaved(false);
setUnsavedIntent(null);
return;
}
setDraft(row);
setBaseline(row);
setEditing(editMode && !isSlideOverInfoLocked(row.status));
setTab(editMode ? "info" : initialTab);
setShowUnsaved(false);
setUnsavedIntent(null);
// Intentionally identity-only: depending on `row` would absorb optimistic patches.
// eslint-disable-next-line react-hooks/exhaustive-deps -- row.id / editMode gate
}, [row?.id, editMode]);
useSlideOverInitialization({
row,
editMode,
initialTab,
setTab,
setDraft,
setBaseline,
setEditing,
setShowUnsaved,
setUnsavedIntent,
});
// Re-apply the requested tab on every open, including a second notification for the work order
// already on screen (same row id) where the identity effect above does not re-run. Tab-only so
@ -110,8 +147,7 @@ export function useSlideOverEditState({
});
}, [infoSource, editing, baseline]);
const activeDraft = draft ?? infoSource;
const dirtyBaseline = baseline ?? infoSource;
const [activeDraft, dirtyBaseline] = [draft ?? infoSource, baseline ?? infoSource];
const isDirty = useMemo(() => {
if (!dirtyBaseline || !activeDraft || !editing) return false;
return isSlideOverDraftDirty(activeDraft, dirtyBaseline);

View file

@ -19,40 +19,56 @@ import { guardDialogClose } from "@/lib/dialog-guard";
import { useWorkOrdersListFilters } from "./use-work-orders-list-filters";
import { useWorkOrdersTableData } from "./use-work-orders-table-data";
function useSlideOverState() {
const [row, setRow] = useState<WorkOrderTableRow | null>(null);
const [edit, setEdit] = useState(false);
const [tab, setTab] = useState<SlideOverTab>("info");
const [highlight, setHighlight] = useState<CommentHighlight | null>(null);
function useSlideOverOpenState() {
const [slideOverRow, setSlideOverRow] = useState<WorkOrderTableRow | null>(null);
const [slideOverEdit, setSlideOverEdit] = useState(false);
const [slideOverTab, setSlideOverTab] = useState<SlideOverTab>("info");
const [slideOverHighlight, setSlideOverHighlight] = useState<CommentHighlight | null>(null);
// Bumps on every open() so the slide-over re-applies tab/highlight even when a second
// notification targets the work order already on screen (same row id).
const [openRequestKey, setOpenRequestKey] = useState(0);
const [slideOverOpenRequestKey, setSlideOverOpenRequestKey] = useState(0);
const open = (
next: WorkOrderTableRow,
nextEdit = false,
nextTab: SlideOverTab = "info",
nextHighlight: CommentHighlight | null = null,
const handleOpenSlideOver = (
row: WorkOrderTableRow,
edit = false,
tab: SlideOverTab = "info",
highlight: CommentHighlight | null = null,
) => {
setRow(next);
setEdit(nextEdit);
setTab(nextTab);
setHighlight(nextHighlight);
setOpenRequestKey((key) => key + 1);
setSlideOverRow(row);
setSlideOverEdit(edit);
setSlideOverTab(tab);
setSlideOverHighlight(highlight);
setSlideOverOpenRequestKey((key) => key + 1);
};
const close = () => {
setRow(null);
setEdit(false);
setHighlight(null);
const handleCloseSlideOver = () => {
setSlideOverRow(null);
setSlideOverEdit(false);
setSlideOverHighlight(null);
};
return { row, edit, tab, highlight, openRequestKey, open, close };
return {
slideOverRow,
slideOverEdit,
slideOverTab,
slideOverHighlight,
slideOverOpenRequestKey,
handleOpenSlideOver,
handleCloseSlideOver,
};
}
export function useWorkOrdersListPage() {
const [wizardOpen, setWizardOpen] = useState(false);
const slideOver = useSlideOverState();
const {
slideOverRow,
slideOverEdit,
slideOverTab,
slideOverHighlight,
slideOverOpenRequestKey,
handleOpenSlideOver,
handleCloseSlideOver: closeSlideOverState,
} = useSlideOverOpenState();
const [confirmCancel, setConfirmCancel] = useState<WorkOrderTableRow | null>(null);
const [confirmComplete, setConfirmComplete] = useState<WorkOrderTableRow | null>(null);
const [docRow, setDocRow] = useState<WorkOrderTableRow | null>(null);
@ -86,7 +102,6 @@ export function useWorkOrdersListPage() {
[vendors],
);
const slideOverRow = slideOver.row;
const activeSlideOverRow = useMemo(() => {
if (!slideOverRow) return null;
return tableData.rows.find((row) => row.id === slideOverRow.id) ?? slideOverRow;
@ -108,7 +123,7 @@ export function useWorkOrdersListPage() {
}, [upliftsRow, tableData.rows]);
const handleCloseSlideOver = () => {
slideOver.close();
closeSlideOverState();
tableData.setSelectedId(null);
};
@ -156,10 +171,10 @@ export function useWorkOrdersListPage() {
return {
wizardOpen,
setWizardOpen,
slideOverEdit: slideOver.edit,
slideOverTab: slideOver.tab,
slideOverHighlight: slideOver.highlight,
slideOverOpenRequestKey: slideOver.openRequestKey,
slideOverEdit,
slideOverTab,
slideOverHighlight,
slideOverOpenRequestKey,
confirmCancel,
setConfirmCancel,
confirmComplete,
@ -185,7 +200,7 @@ export function useWorkOrdersListPage() {
activeDocRow,
activeMediaRow,
activeUpliftsRow,
handleOpenSlideOver: slideOver.open,
handleOpenSlideOver,
handleCloseSlideOver,
handleConfirmCancel,
handleConfirmComplete,