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:
parent
d27ec9152c
commit
3f3e1e6423
7 changed files with 141 additions and 69 deletions
|
|
@ -1,12 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useTableControls } from "@/components/ui/use-table-controls";
|
||||
import { TableControls, SortableTh } from "@/components/ui/table-controls";
|
||||
import { AddAccountButton, EditAccountButton } from "./account-form";
|
||||
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
import { deleteAccount, toggleAccountActive } from "./actions";
|
||||
|
||||
export type AccountRow = {
|
||||
|
|
@ -20,23 +20,15 @@ export type AccountRow = {
|
|||
const CHIPS = ["Active", "Inactive"];
|
||||
|
||||
function AccountActionsMenu({ account }: { account: AccountRow }) {
|
||||
const router = useRouter();
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleToggle() {
|
||||
startTransition(async () => {
|
||||
await toggleAccountActive(account.id);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
const [toggleOpen, setToggleOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowActionsMenu>
|
||||
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
|
||||
<RowActionsItem onClick={handleToggle} disabled={isPending}>
|
||||
<RowActionsItem onClick={() => setToggleOpen(true)}>
|
||||
{account.isActive ? "Deactivate" : "Activate"}
|
||||
</RowActionsItem>
|
||||
<RowActionsSeparator />
|
||||
|
|
@ -60,6 +52,14 @@ function AccountActionsMenu({ account }: { account: AccountRow }) {
|
|||
label={account.name}
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { formatCurrency, formatDate } from "@/lib/utils";
|
||||
import { useTableControls } from "@/components/ui/use-table-controls";
|
||||
import { TableControls, SortableTh } from "@/components/ui/table-controls";
|
||||
import { AddProductButton } from "./product-form";
|
||||
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
import { deleteProduct, toggleProductActive } from "./actions";
|
||||
|
||||
export type ProductRow = {
|
||||
|
|
@ -26,21 +26,13 @@ export type ProductRow = {
|
|||
const CHIPS = ["Active", "Inactive"];
|
||||
|
||||
function ProductActionsMenu({ product }: { product: ProductRow }) {
|
||||
const router = useRouter();
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleToggle() {
|
||||
startTransition(async () => {
|
||||
await toggleProductActive(product.id);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
const [toggleOpen, setToggleOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowActionsMenu>
|
||||
<RowActionsItem onClick={handleToggle} disabled={isPending}>
|
||||
<RowActionsItem onClick={() => setToggleOpen(true)}>
|
||||
{product.isActive ? "Deactivate" : "Activate"}
|
||||
</RowActionsItem>
|
||||
<RowActionsSeparator />
|
||||
|
|
@ -53,6 +45,14 @@ function ProductActionsMenu({ product }: { product: ProductRow }) {
|
|||
label={product.name}
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useTableControls } from "@/components/ui/use-table-controls";
|
||||
import { TableControls, SortableTh } from "@/components/ui/table-controls";
|
||||
import { AddSiteButton, EditSiteButton } from "./site-form";
|
||||
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
import { deleteSite, toggleSiteActive } from "./actions";
|
||||
|
||||
export type SiteRow = {
|
||||
|
|
@ -25,23 +25,15 @@ export type SiteRow = {
|
|||
const CHIPS = ["Active", "Inactive"];
|
||||
|
||||
function SiteActionsMenu({ site }: { site: SiteRow }) {
|
||||
const router = useRouter();
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleToggle() {
|
||||
startTransition(async () => {
|
||||
await toggleSiteActive(site.id);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
const [toggleOpen, setToggleOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowActionsMenu>
|
||||
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
|
||||
<RowActionsItem onClick={handleToggle} disabled={isPending}>
|
||||
<RowActionsItem onClick={() => setToggleOpen(true)}>
|
||||
{site.isActive ? "Deactivate" : "Activate"}
|
||||
</RowActionsItem>
|
||||
<RowActionsSeparator />
|
||||
|
|
@ -67,6 +59,14 @@ function SiteActionsMenu({ site }: { site: SiteRow }) {
|
|||
label={site.name}
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { TableControls, SortableTh } from "@/components/ui/table-controls";
|
|||
import { AddUserButton, EditUserButton } from "./user-form";
|
||||
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
import { deleteUser, toggleUserActive } from "./actions";
|
||||
import { grantSuperUser } from "../superuser-requests/actions";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
|
|
@ -37,16 +38,9 @@ function UserActionsMenu({ user }: { user: UserRow }) {
|
|||
const router = useRouter();
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [toggleOpen, setToggleOpen] = useState(false);
|
||||
const [grantPending, startGrantTransition] = useTransition();
|
||||
|
||||
function handleToggle() {
|
||||
startTransition(async () => {
|
||||
await toggleUserActive(user.id);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
function handleGrantSuperUser() {
|
||||
startGrantTransition(async () => {
|
||||
await grantSuperUser(user.id);
|
||||
|
|
@ -58,7 +52,7 @@ function UserActionsMenu({ user }: { user: UserRow }) {
|
|||
<>
|
||||
<RowActionsMenu>
|
||||
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
|
||||
<RowActionsItem onClick={handleToggle} disabled={isPending}>
|
||||
<RowActionsItem onClick={() => setToggleOpen(true)}>
|
||||
{user.isActive ? "Deactivate" : "Activate"}
|
||||
</RowActionsItem>
|
||||
{user.role !== "SUPERUSER" && user.role !== "ADMIN" && (
|
||||
|
|
@ -89,6 +83,14 @@ function UserActionsMenu({ user }: { user: UserRow }) {
|
|||
label={user.name}
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
24
App/app/(portal)/admin/vendors/vendors-table.tsx
vendored
24
App/app/(portal)/admin/vendors/vendors-table.tsx
vendored
|
|
@ -1,13 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useTableControls } from "@/components/ui/use-table-controls";
|
||||
import { TableControls, SortableTh } from "@/components/ui/table-controls";
|
||||
import { AddVendorButton, EditVendorButton } from "./vendor-form";
|
||||
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
import { deleteVendor, toggleVendorActive } from "./actions";
|
||||
|
||||
type ContactRow = {
|
||||
|
|
@ -34,23 +34,15 @@ export type VendorRow = {
|
|||
const CHIPS = ["Verified", "Unverified", "Active", "Inactive"];
|
||||
|
||||
function VendorActionsMenu({ vendor }: { vendor: VendorRow }) {
|
||||
const router = useRouter();
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleToggle() {
|
||||
startTransition(async () => {
|
||||
await toggleVendorActive(vendor.id);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
const [toggleOpen, setToggleOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowActionsMenu>
|
||||
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
|
||||
<RowActionsItem onClick={handleToggle} disabled={isPending}>
|
||||
<RowActionsItem onClick={() => setToggleOpen(true)}>
|
||||
{vendor.isActive ? "Deactivate" : "Activate"}
|
||||
</RowActionsItem>
|
||||
<RowActionsSeparator />
|
||||
|
|
@ -77,6 +69,14 @@ function VendorActionsMenu({ vendor }: { vendor: VendorRow }) {
|
|||
label={vendor.name}
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useTableControls } from "@/components/ui/use-table-controls";
|
||||
import { TableControls, SortableTh } from "@/components/ui/table-controls";
|
||||
import { AddVesselButton, EditVesselButton } from "./vessel-form";
|
||||
import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
import { deleteVessel, toggleVesselActive } from "./actions";
|
||||
|
||||
export type VesselRow = {
|
||||
|
|
@ -20,23 +20,15 @@ export type VesselRow = {
|
|||
const CHIPS = ["Active", "Inactive"];
|
||||
|
||||
function VesselActionsMenu({ vessel }: { vessel: VesselRow }) {
|
||||
const router = useRouter();
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleToggle() {
|
||||
startTransition(async () => {
|
||||
await toggleVesselActive(vessel.id);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
const [toggleOpen, setToggleOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowActionsMenu>
|
||||
<RowActionsItem onClick={() => setEditOpen(true)}>Edit</RowActionsItem>
|
||||
<RowActionsItem onClick={handleToggle} disabled={isPending}>
|
||||
<RowActionsItem onClick={() => setToggleOpen(true)}>
|
||||
{vessel.isActive ? "Deactivate" : "Activate"}
|
||||
</RowActionsItem>
|
||||
<RowActionsSeparator />
|
||||
|
|
@ -59,6 +51,14 @@ function VesselActionsMenu({ vessel }: { vessel: VesselRow }) {
|
|||
label={vessel.name}
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
70
App/components/ui/confirm-dialog.tsx
Normal file
70
App/components/ui/confirm-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue