fix(vendors): preserve the selected edit state

This commit is contained in:
Alexandre Brandizzi 2026-08-06 22:39:11 -03:00
parent ff2e04b30e
commit ec203e1f5b
5 changed files with 82 additions and 22 deletions

View file

@ -15,7 +15,7 @@ export interface VendorDeactivationState {
confirm: () => void; confirm: () => void;
} }
export function useVendorDeactivation(): VendorDeactivationState { export function useVendorDeactivation(onSuccess?: () => void): VendorDeactivationState {
const [target, setTarget] = useState<VendorListItem | null>(null); const [target, setTarget] = useState<VendorListItem | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const deleteVendor = useDeleteVendor(); const deleteVendor = useDeleteVendor();
@ -39,7 +39,10 @@ export function useVendorDeactivation(): VendorDeactivationState {
if (!target || target.id == null) return; if (!target || target.id == null) return;
setError(null); setError(null);
deleteVendor.mutate(target.id, { deleteVendor.mutate(target.id, {
onSuccess: () => setTarget(null), onSuccess: () => {
setTarget(null);
onSuccess?.();
},
onError: (err: Error) => setError(err.message || "Failed to deactivate vendor"), onError: (err: Error) => setError(err.message || "Failed to deactivate vendor"),
}); });
}; };

View file

@ -1,5 +1,5 @@
import { useEffect, useState, type ReactNode } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { Controller } from "react-hook-form"; import { Controller, useWatch } from "react-hook-form";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import LaunchIcon from "@mui/icons-material/Launch"; import LaunchIcon from "@mui/icons-material/Launch";
@ -250,13 +250,14 @@ function DrawerEditor({
onSuccess: onSaved, onSuccess: onSaved,
}); });
const roster = form.roster; const roster = form.roster;
const selectedIndex = Math.max( const technicians = useWatch({ control: form.control, name: "technicians" }) ?? [];
0, const selectedIndex = technicians.findIndex(
roster?.technicians.findIndex( (technician) => technician.id != null && String(technician.id) === String(vendor.id),
(technician) => technician.id != null && String(technician.id) === String(vendor.id),
) ?? 0,
); );
const selected = roster?.technicians[selectedIndex]; const selectedTotalJobs =
roster?.technicians.find(
(technician) => technician.id != null && String(technician.id) === String(vendor.id),
)?.totalJobs ?? vendor.totalJobs;
if (form.isLoading) { if (form.isLoading) {
return <CircularProgress size={28} sx={{ m: "auto" }} />; return <CircularProgress size={28} sx={{ m: "auto" }} />;
@ -289,12 +290,12 @@ function DrawerEditor({
tradeOptions={form.trades} tradeOptions={form.trades}
showTechnicianStatus={false} showTechnicianStatus={false}
/> />
{selected != null && ( {selectedIndex >= 0 && (
<> <>
<Divider /> <Divider />
<Stack direction="row" sx={{ alignItems: "center", justifyContent: "space-between" }}> <Stack direction="row" sx={{ alignItems: "center", justifyContent: "space-between" }}>
<Box> <Box>
<Text variant="title">{selected.totalJobs ?? 0}</Text> <Text variant="title">{selectedTotalJobs}</Text>
<Text variant="caption" tone="muted"> <Text variant="caption" tone="muted">
Total Jobs Total Jobs
</Text> </Text>

View file

@ -116,7 +116,11 @@ export default function VendorsListPage() {
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [detailTarget, setDetailTarget] = useState<VendorListItem | null>(null); const [detailTarget, setDetailTarget] = useState<VendorListItem | null>(null);
const [detailStartsInEdit, setDetailStartsInEdit] = useState(false); const [detailStartsInEdit, setDetailStartsInEdit] = useState(false);
const deactivation = useVendorDeactivation(); const closeDetail = () => {
setDetailTarget(null);
setDetailStartsInEdit(false);
};
const deactivation = useVendorDeactivation(closeDetail);
const { data: facets } = useVendorFacets(filters.isActive); const { data: facets } = useVendorFacets(filters.isActive);
const { data, isLoading, isFetching, error } = useVendorsList(filters.listParams); const { data, isLoading, isFetching, error } = useVendorsList(filters.listParams);
@ -213,15 +217,8 @@ export default function VendorsListPage() {
open={Boolean(detailTarget)} open={Boolean(detailTarget)}
vendor={detailTarget} vendor={detailTarget}
startInEdit={detailStartsInEdit} startInEdit={detailStartsInEdit}
onRequestDeactivation={(row) => { onRequestDeactivation={deactivation.open}
setDetailTarget(null); onClose={closeDetail}
setDetailStartsInEdit(false);
deactivation.open(row);
}}
onClose={() => {
setDetailTarget(null);
setDetailStartsInEdit(false);
}}
/> />
<VendorDeactivationDialog <VendorDeactivationDialog

View file

@ -110,4 +110,27 @@ describe("VendorDetailDrawer selected-technician display", () => {
expect(screen.getByText("0")).toBeInTheDocument(); expect(screen.getByText("0")).toBeInTheDocument();
expect(screen.queryByText("999-999-9999")).not.toBeInTheDocument(); expect(screen.queryByText("999-999-9999")).not.toBeInTheDocument();
}); });
it("does not expose a status control for a different technician", () => {
useVendorCompanyRoster.mockReturnValue(
rosterWith([
{
id: 7,
contactName: "Different Technician",
phone: "314-555-0101",
email: "",
tradeSpecialties: "Plumbing",
isActive: true,
totalJobs: 10,
},
]),
);
renderWithProviders(
<VendorDetailDrawer open={true} vendor={vendor} startInEdit={true} onClose={vi.fn()} />,
{ route: "/vendors", withAuth: false },
);
expect(screen.queryByRole("switch", { name: "Active status" })).not.toBeInTheDocument();
});
}); });

View file

@ -1,4 +1,4 @@
import { screen } from "@testing-library/react"; import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import VendorsListPage from "@/app/(protected)/vendors/index"; import VendorsListPage from "@/app/(protected)/vendors/index";
@ -220,6 +220,42 @@ describe("VendorsListPage", () => {
expect(mutate).not.toHaveBeenCalled(); expect(mutate).not.toHaveBeenCalled();
}); });
it("preserves inline edits when deactivation is cancelled", async () => {
setupDefaults();
useVendorCompanyRoster.mockReturnValue({
data: activeRoster,
isLoading: false,
isError: false,
error: null,
refetch: vi.fn(),
});
useVendorDeactivationImpact.mockReturnValue({
data: { vendorId: 1, canDeactivate: true, openWorkOrders: [] },
isLoading: false,
error: null,
});
useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => {
if (params.pageSize === 1) return result([], 1);
return params.isActive ? result([activeVendor], 1) : result([], 0);
});
renderWithProviders(<VendorsListPage />, { route: "/vendors", withAuth: false });
await userEvent.click(screen.getByRole("button", { name: "Edit vendor Gateway Plumbing" }));
const company = screen.getByRole("textbox", { name: "Company" });
await userEvent.clear(company);
await userEvent.type(company, "Draft Company Name");
await userEvent.click(screen.getByRole("switch", { name: "Active status" }));
await userEvent.click(
within(screen.getByRole("dialog", { name: "Deactivate Vendor" })).getByRole("button", {
name: "Cancel",
}),
);
expect(company).toBeInTheDocument();
expect(company).toHaveValue("Draft Company Name");
});
it("opens inline edit when the table Edit control is clicked", async () => { it("opens inline edit when the table Edit control is clicked", async () => {
setupDefaults(); setupDefaults();
useVendorCompanyRoster.mockReturnValue({ useVendorCompanyRoster.mockReturnValue({