feat(admin): confirm activate/deactivate via modal popup across all tables

Replace immediate server action calls with ConfirmDialog modals for
activate/deactivate on all 6 admin tables (users, vendors, vessels,
sites, accounts, products). Delete already used DeleteConfirmDialog;
this adds the same pattern for reversible toggle actions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-05-29 03:07:04 +05:30
parent d27ec9152c
commit 3f3e1e6423
7 changed files with 141 additions and 69 deletions

View file

@ -1,12 +1,12 @@
"use client"; "use client";
import { useState, useTransition } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTableControls } from "@/components/ui/use-table-controls"; import { useTableControls } from "@/components/ui/use-table-controls";
import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls";
import { AddAccountButton, EditAccountButton } from "./account-form"; import { AddAccountButton, EditAccountButton } from "./account-form";
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { deleteAccount, toggleAccountActive } from "./actions"; import { deleteAccount, toggleAccountActive } from "./actions";
export type AccountRow = { export type AccountRow = {
@ -20,23 +20,15 @@ export type AccountRow = {
const CHIPS = ["Active", "Inactive"]; const CHIPS = ["Active", "Inactive"];
function AccountActionsMenu({ account }: { account: AccountRow }) { function AccountActionsMenu({ account }: { account: AccountRow }) {
const router = useRouter();
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [isPending, startTransition] = useTransition(); const [toggleOpen, setToggleOpen] = useState(false);
function handleToggle() {
startTransition(async () => {
await toggleAccountActive(account.id);
router.refresh();
});
}
return ( return (
<> <>
<RowActionsMenu> <RowActionsMenu>
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem> <RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
<RowActionsItem onClick={handleToggle} disabled={isPending}> <RowActionsItem onClick={() => setToggleOpen(true)}>
{account.isActive ? "Deactivate" : "Activate"} {account.isActive ? "Deactivate" : "Activate"}
</RowActionsItem> </RowActionsItem>
<RowActionsSeparator /> <RowActionsSeparator />
@ -60,6 +52,14 @@ function AccountActionsMenu({ account }: { account: AccountRow }) {
label={account.name} label={account.name}
onConfirm={() => deleteAccount(account.id)} onConfirm={() => deleteAccount(account.id)}
/> />
<ConfirmDialog
open={toggleOpen}
onOpenChange={setToggleOpen}
title={account.isActive ? `Deactivate ${account.name}?` : `Activate ${account.name}?`}
description={account.isActive ? `${account.name} will be hidden from account selections.` : `${account.name} will become available for account selections.`}
confirmLabel={account.isActive ? "Deactivate" : "Activate"}
onConfirm={() => toggleAccountActive(account.id)}
/>
</> </>
); );
} }

View file

@ -1,14 +1,14 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useState, useTransition } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
import { formatCurrency, formatDate } from "@/lib/utils"; import { formatCurrency, formatDate } from "@/lib/utils";
import { useTableControls } from "@/components/ui/use-table-controls"; import { useTableControls } from "@/components/ui/use-table-controls";
import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls";
import { AddProductButton } from "./product-form"; import { AddProductButton } from "./product-form";
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { deleteProduct, toggleProductActive } from "./actions"; import { deleteProduct, toggleProductActive } from "./actions";
export type ProductRow = { export type ProductRow = {
@ -26,21 +26,13 @@ export type ProductRow = {
const CHIPS = ["Active", "Inactive"]; const CHIPS = ["Active", "Inactive"];
function ProductActionsMenu({ product }: { product: ProductRow }) { function ProductActionsMenu({ product }: { product: ProductRow }) {
const router = useRouter();
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [isPending, startTransition] = useTransition(); const [toggleOpen, setToggleOpen] = useState(false);
function handleToggle() {
startTransition(async () => {
await toggleProductActive(product.id);
router.refresh();
});
}
return ( return (
<> <>
<RowActionsMenu> <RowActionsMenu>
<RowActionsItem onClick={handleToggle} disabled={isPending}> <RowActionsItem onClick={() => setToggleOpen(true)}>
{product.isActive ? "Deactivate" : "Activate"} {product.isActive ? "Deactivate" : "Activate"}
</RowActionsItem> </RowActionsItem>
<RowActionsSeparator /> <RowActionsSeparator />
@ -53,6 +45,14 @@ function ProductActionsMenu({ product }: { product: ProductRow }) {
label={product.name} label={product.name}
onConfirm={() => deleteProduct(product.id)} onConfirm={() => deleteProduct(product.id)}
/> />
<ConfirmDialog
open={toggleOpen}
onOpenChange={setToggleOpen}
title={product.isActive ? `Deactivate ${product.name}?` : `Activate ${product.name}?`}
description={product.isActive ? `${product.name} will be hidden from new purchase orders.` : `${product.name} will become available for new purchase orders.`}
confirmLabel={product.isActive ? "Deactivate" : "Activate"}
onConfirm={() => toggleProductActive(product.id)}
/>
</> </>
); );
} }

View file

@ -1,13 +1,13 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useState, useTransition } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTableControls } from "@/components/ui/use-table-controls"; import { useTableControls } from "@/components/ui/use-table-controls";
import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls";
import { AddSiteButton, EditSiteButton } from "./site-form"; import { AddSiteButton, EditSiteButton } from "./site-form";
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { deleteSite, toggleSiteActive } from "./actions"; import { deleteSite, toggleSiteActive } from "./actions";
export type SiteRow = { export type SiteRow = {
@ -25,23 +25,15 @@ export type SiteRow = {
const CHIPS = ["Active", "Inactive"]; const CHIPS = ["Active", "Inactive"];
function SiteActionsMenu({ site }: { site: SiteRow }) { function SiteActionsMenu({ site }: { site: SiteRow }) {
const router = useRouter();
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [isPending, startTransition] = useTransition(); const [toggleOpen, setToggleOpen] = useState(false);
function handleToggle() {
startTransition(async () => {
await toggleSiteActive(site.id);
router.refresh();
});
}
return ( return (
<> <>
<RowActionsMenu> <RowActionsMenu>
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem> <RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
<RowActionsItem onClick={handleToggle} disabled={isPending}> <RowActionsItem onClick={() => setToggleOpen(true)}>
{site.isActive ? "Deactivate" : "Activate"} {site.isActive ? "Deactivate" : "Activate"}
</RowActionsItem> </RowActionsItem>
<RowActionsSeparator /> <RowActionsSeparator />
@ -67,6 +59,14 @@ function SiteActionsMenu({ site }: { site: SiteRow }) {
label={site.name} label={site.name}
onConfirm={() => deleteSite(site.id)} onConfirm={() => deleteSite(site.id)}
/> />
<ConfirmDialog
open={toggleOpen}
onOpenChange={setToggleOpen}
title={site.isActive ? `Deactivate ${site.name}?` : `Activate ${site.name}?`}
description={site.isActive ? `${site.name} will be hidden from cost centre selections.` : `${site.name} will become available for cost centre selections.`}
confirmLabel={site.isActive ? "Deactivate" : "Activate"}
onConfirm={() => toggleSiteActive(site.id)}
/>
</> </>
); );
} }

View file

@ -7,6 +7,7 @@ import { TableControls, SortableTh } from "@/components/ui/table-controls";
import { AddUserButton, EditUserButton } from "./user-form"; import { AddUserButton, EditUserButton } from "./user-form";
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { deleteUser, toggleUserActive } from "./actions"; import { deleteUser, toggleUserActive } from "./actions";
import { grantSuperUser } from "../superuser-requests/actions"; import { grantSuperUser } from "../superuser-requests/actions";
import { ShieldCheck } from "lucide-react"; import { ShieldCheck } from "lucide-react";
@ -37,16 +38,9 @@ function UserActionsMenu({ user }: { user: UserRow }) {
const router = useRouter(); const router = useRouter();
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [isPending, startTransition] = useTransition(); const [toggleOpen, setToggleOpen] = useState(false);
const [grantPending, startGrantTransition] = useTransition(); const [grantPending, startGrantTransition] = useTransition();
function handleToggle() {
startTransition(async () => {
await toggleUserActive(user.id);
router.refresh();
});
}
function handleGrantSuperUser() { function handleGrantSuperUser() {
startGrantTransition(async () => { startGrantTransition(async () => {
await grantSuperUser(user.id); await grantSuperUser(user.id);
@ -58,7 +52,7 @@ function UserActionsMenu({ user }: { user: UserRow }) {
<> <>
<RowActionsMenu> <RowActionsMenu>
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem> <RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
<RowActionsItem onClick={handleToggle} disabled={isPending}> <RowActionsItem onClick={() => setToggleOpen(true)}>
{user.isActive ? "Deactivate" : "Activate"} {user.isActive ? "Deactivate" : "Activate"}
</RowActionsItem> </RowActionsItem>
{user.role !== "SUPERUSER" && user.role !== "ADMIN" && ( {user.role !== "SUPERUSER" && user.role !== "ADMIN" && (
@ -89,6 +83,14 @@ function UserActionsMenu({ user }: { user: UserRow }) {
label={user.name} label={user.name}
onConfirm={() => deleteUser(user.id)} onConfirm={() => deleteUser(user.id)}
/> />
<ConfirmDialog
open={toggleOpen}
onOpenChange={setToggleOpen}
title={user.isActive ? `Deactivate ${user.name}?` : `Activate ${user.name}?`}
description={user.isActive ? `This will prevent ${user.name} from signing in.` : `This will restore ${user.name}'s access to PPMS.`}
confirmLabel={user.isActive ? "Deactivate" : "Activate"}
onConfirm={() => toggleUserActive(user.id)}
/>
</> </>
); );
} }

View file

@ -1,13 +1,13 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useState, useTransition } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTableControls } from "@/components/ui/use-table-controls"; import { useTableControls } from "@/components/ui/use-table-controls";
import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls";
import { AddVendorButton, EditVendorButton } from "./vendor-form"; import { AddVendorButton, EditVendorButton } from "./vendor-form";
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { deleteVendor, toggleVendorActive } from "./actions"; import { deleteVendor, toggleVendorActive } from "./actions";
type ContactRow = { type ContactRow = {
@ -34,23 +34,15 @@ export type VendorRow = {
const CHIPS = ["Verified", "Unverified", "Active", "Inactive"]; const CHIPS = ["Verified", "Unverified", "Active", "Inactive"];
function VendorActionsMenu({ vendor }: { vendor: VendorRow }) { function VendorActionsMenu({ vendor }: { vendor: VendorRow }) {
const router = useRouter();
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [isPending, startTransition] = useTransition(); const [toggleOpen, setToggleOpen] = useState(false);
function handleToggle() {
startTransition(async () => {
await toggleVendorActive(vendor.id);
router.refresh();
});
}
return ( return (
<> <>
<RowActionsMenu> <RowActionsMenu>
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem> <RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
<RowActionsItem onClick={handleToggle} disabled={isPending}> <RowActionsItem onClick={() => setToggleOpen(true)}>
{vendor.isActive ? "Deactivate" : "Activate"} {vendor.isActive ? "Deactivate" : "Activate"}
</RowActionsItem> </RowActionsItem>
<RowActionsSeparator /> <RowActionsSeparator />
@ -77,6 +69,14 @@ function VendorActionsMenu({ vendor }: { vendor: VendorRow }) {
label={vendor.name} label={vendor.name}
onConfirm={() => deleteVendor(vendor.id)} onConfirm={() => deleteVendor(vendor.id)}
/> />
<ConfirmDialog
open={toggleOpen}
onOpenChange={setToggleOpen}
title={vendor.isActive ? `Deactivate ${vendor.name}?` : `Activate ${vendor.name}?`}
description={vendor.isActive ? `${vendor.name} will be hidden from new purchase orders.` : `${vendor.name} will become available for new purchase orders.`}
confirmLabel={vendor.isActive ? "Deactivate" : "Activate"}
onConfirm={() => toggleVendorActive(vendor.id)}
/>
</> </>
); );
} }

View file

@ -1,12 +1,12 @@
"use client"; "use client";
import { useState, useTransition } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTableControls } from "@/components/ui/use-table-controls"; import { useTableControls } from "@/components/ui/use-table-controls";
import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls";
import { AddVesselButton, EditVesselButton } from "./vessel-form"; import { AddVesselButton, EditVesselButton } from "./vessel-form";
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { deleteVessel, toggleVesselActive } from "./actions"; import { deleteVessel, toggleVesselActive } from "./actions";
export type VesselRow = { export type VesselRow = {
@ -20,23 +20,15 @@ export type VesselRow = {
const CHIPS = ["Active", "Inactive"]; const CHIPS = ["Active", "Inactive"];
function VesselActionsMenu({ vessel }: { vessel: VesselRow }) { function VesselActionsMenu({ vessel }: { vessel: VesselRow }) {
const router = useRouter();
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [isPending, startTransition] = useTransition(); const [toggleOpen, setToggleOpen] = useState(false);
function handleToggle() {
startTransition(async () => {
await toggleVesselActive(vessel.id);
router.refresh();
});
}
return ( return (
<> <>
<RowActionsMenu> <RowActionsMenu>
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem> <RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
<RowActionsItem onClick={handleToggle} disabled={isPending}> <RowActionsItem onClick={() => setToggleOpen(true)}>
{vessel.isActive ? "Deactivate" : "Activate"} {vessel.isActive ? "Deactivate" : "Activate"}
</RowActionsItem> </RowActionsItem>
<RowActionsSeparator /> <RowActionsSeparator />
@ -59,6 +51,14 @@ function VesselActionsMenu({ vessel }: { vessel: VesselRow }) {
label={vessel.name} label={vessel.name}
onConfirm={() => deleteVessel(vessel.id)} onConfirm={() => deleteVessel(vessel.id)}
/> />
<ConfirmDialog
open={toggleOpen}
onOpenChange={setToggleOpen}
title={vessel.isActive ? `Deactivate ${vessel.name}?` : `Activate ${vessel.name}?`}
description={vessel.isActive ? `${vessel.name} will be hidden from new purchase orders.` : `${vessel.name} will become available for new purchase orders.`}
confirmLabel={vessel.isActive ? "Deactivate" : "Activate"}
onConfirm={() => toggleVesselActive(vessel.id)}
/>
</> </>
); );
} }

View file

@ -0,0 +1,70 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { AdminDialog } from "@/components/ui/admin-dialog";
type ActionResult = { ok: true } | { error: string };
interface Props {
open: boolean;
onOpenChange: (v: boolean) => void;
title: string;
description: string;
confirmLabel: string;
onConfirm: () => Promise<ActionResult>;
}
export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, onConfirm }: Props) {
const router = useRouter();
const [error, setError] = useState("");
const [isPending, startTransition] = useTransition();
function handleClose() {
if (isPending) return;
setError("");
onOpenChange(false);
}
function handleConfirm() {
setError("");
startTransition(async () => {
const result = await onConfirm();
if ("error" in result) {
setError(result.error);
} else {
onOpenChange(false);
router.refresh();
}
});
}
return (
<AdminDialog title={title} open={open} onClose={handleClose}>
<div className="space-y-4">
<p className="text-sm text-neutral-600">{description}</p>
{error && (
<p className="text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>
)}
<div className="flex justify-end gap-3 pt-1">
<button
type="button"
onClick={handleClose}
disabled={isPending}
className="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-60"
>
Cancel
</button>
<button
type="button"
onClick={handleConfirm}
disabled={isPending}
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors"
>
{isPending ? `${confirmLabel}` : confirmLabel}
</button>
</div>
</div>
</AdminDialog>
);
}