From c06745a9f93dd0d5934f4454a6f261ba44085320 Mon Sep 17 00:00:00 2001 From: Hardik Date: Thu, 14 May 2026 17:08:51 +0530 Subject: [PATCH] feat(admin): add delete with guard rails to all entity tables Adds deleteX server actions and ConfirmDeleteButton to the Items, Vessels, Sites, Users, and Accounts tables. Guard rails per entity: - Items: blocked by non-draft PO line items; nulls draft references, cascades inventory, consumption, and vendor prices - Sites: blocked by non-draft POs; nulls draft PO siteId and vessel siteId, cascades inventory and consumption - Vessels, Accounts: blocked by any PO (FK is non-nullable) - Users: blocked by any submitted PO; cascades notifications UI: inline two-step confirm ("Delete X? Confirm / Cancel") using the shared ConfirmDeleteButton component; errors surface inline. Co-Authored-By: Claude Sonnet 4.6 --- .../app/(portal)/admin/accounts/actions.ts | 12 +++++++++++ .../app/(portal)/admin/accounts/page.tsx | 19 ++++++++++------- .../app/(portal)/admin/products/actions.ts | 21 +++++++++++++++++++ .../app/(portal)/admin/products/page.tsx | 19 ++++++++++------- .../app/(portal)/admin/sites/actions.ts | 21 +++++++++++++++++++ .../app/(portal)/admin/sites/page.tsx | 7 ++++++- .../app/(portal)/admin/users/actions.ts | 17 +++++++++++++++ .../app/(portal)/admin/users/page.tsx | 21 ++++++++++++------- .../app/(portal)/admin/vessels/actions.ts | 12 +++++++++++ .../app/(portal)/admin/vessels/page.tsx | 17 +++++++++------ 10 files changed, 137 insertions(+), 29 deletions(-) diff --git a/App/pelagia-portal/app/(portal)/admin/accounts/actions.ts b/App/pelagia-portal/app/(portal)/admin/accounts/actions.ts index 4a52dca..027e7ab 100644 --- a/App/pelagia-portal/app/(portal)/admin/accounts/actions.ts +++ b/App/pelagia-portal/app/(portal)/admin/accounts/actions.ts @@ -61,6 +61,18 @@ export async function updateAccount(formData: FormData): Promise { return { ok: true }; } +export async function deleteAccount(id: string): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_vessels_accounts")) return { error: "Unauthorized" }; + + const inUse = await db.purchaseOrder.findFirst({ where: { accountId: id } }); + if (inUse) return { error: "Cannot delete: account is referenced in purchase orders. Remove those POs first." }; + + await db.account.delete({ where: { id } }); + revalidatePath("/admin/accounts"); + return { ok: true }; +} + export async function toggleAccountActive(accountId: string): Promise { const session = await auth(); if (!session?.user || !hasPermission(session.user.role, "manage_vessels_accounts")) { diff --git a/App/pelagia-portal/app/(portal)/admin/accounts/page.tsx b/App/pelagia-portal/app/(portal)/admin/accounts/page.tsx index 830d68b..21e86a8 100644 --- a/App/pelagia-portal/app/(portal)/admin/accounts/page.tsx +++ b/App/pelagia-portal/app/(portal)/admin/accounts/page.tsx @@ -3,6 +3,8 @@ import { db } from "@/lib/db"; import { hasPermission } from "@/lib/permissions"; import { redirect } from "next/navigation"; import { AddAccountButton, EditAccountButton } from "./account-form"; +import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; +import { deleteAccount } from "./actions"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "Account Management" }; @@ -47,13 +49,16 @@ export default async function AdminAccountsPage() { - + + + + ))} diff --git a/App/pelagia-portal/app/(portal)/admin/products/actions.ts b/App/pelagia-portal/app/(portal)/admin/products/actions.ts index e411ef9..ec6953f 100644 --- a/App/pelagia-portal/app/(portal)/admin/products/actions.ts +++ b/App/pelagia-portal/app/(portal)/admin/products/actions.ts @@ -35,6 +35,27 @@ export async function createProduct(formData: FormData): Promise { return { ok: true }; } +export async function deleteProduct(id: string): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_products")) return { error: "Forbidden" }; + + const blocked = await db.pOLineItem.findFirst({ + where: { productId: id, po: { status: { not: "DRAFT" } } }, + }); + if (blocked) return { error: "Cannot delete: item is referenced in submitted or active purchase orders." }; + + await db.$transaction(async (tx) => { + await tx.pOLineItem.updateMany({ where: { productId: id }, data: { productId: null } }); + await tx.itemConsumption.deleteMany({ where: { productId: id } }); + await tx.itemInventory.deleteMany({ where: { productId: id } }); + await tx.productVendorPrice.deleteMany({ where: { productId: id } }); + await tx.product.delete({ where: { id } }); + }); + + revalidatePath("/admin/products"); + return { ok: true }; +} + export async function toggleProductActive(productId: string): Promise { const session = await auth(); if (!session?.user || !hasPermission(session.user.role, "manage_products")) { diff --git a/App/pelagia-portal/app/(portal)/admin/products/page.tsx b/App/pelagia-portal/app/(portal)/admin/products/page.tsx index 2edd002..ec8c03f 100644 --- a/App/pelagia-portal/app/(portal)/admin/products/page.tsx +++ b/App/pelagia-portal/app/(portal)/admin/products/page.tsx @@ -5,6 +5,8 @@ import { redirect } from "next/navigation"; import Link from "next/link"; import { formatCurrency, formatDate } from "@/lib/utils"; import { AddProductButton, ToggleProductButton } from "./product-form"; +import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; +import { deleteProduct } from "./actions"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "Item Catalogue" }; @@ -91,13 +93,16 @@ export default async function AdminProductsPage() { {canManage && ( - + + + + )} diff --git a/App/pelagia-portal/app/(portal)/admin/sites/actions.ts b/App/pelagia-portal/app/(portal)/admin/sites/actions.ts index 0daf842..2869530 100644 --- a/App/pelagia-portal/app/(portal)/admin/sites/actions.ts +++ b/App/pelagia-portal/app/(portal)/admin/sites/actions.ts @@ -72,6 +72,27 @@ export async function toggleSiteActive(id: string): Promise { return { ok: true }; } +export async function deleteSite(id: string): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_sites")) return { error: "Forbidden" }; + + const blocked = await db.purchaseOrder.findFirst({ + where: { siteId: id, status: { not: "DRAFT" } }, + }); + if (blocked) return { error: "Cannot delete: site is referenced in submitted or active purchase orders." }; + + await db.$transaction(async (tx) => { + await tx.purchaseOrder.updateMany({ where: { siteId: id, status: "DRAFT" }, data: { siteId: null } }); + await tx.itemConsumption.deleteMany({ where: { siteId: id } }); + await tx.itemInventory.deleteMany({ where: { siteId: id } }); + await tx.vessel.updateMany({ where: { siteId: id }, data: { siteId: null } }); + await tx.site.delete({ where: { id } }); + }); + + revalidatePath("/admin/sites"); + return { ok: true }; +} + export async function recordConsumption(formData: FormData): Promise { const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; diff --git a/App/pelagia-portal/app/(portal)/admin/sites/page.tsx b/App/pelagia-portal/app/(portal)/admin/sites/page.tsx index dccc574..f1b3e06 100644 --- a/App/pelagia-portal/app/(portal)/admin/sites/page.tsx +++ b/App/pelagia-portal/app/(portal)/admin/sites/page.tsx @@ -4,6 +4,8 @@ import { hasPermission } from "@/lib/permissions"; import { redirect } from "next/navigation"; import Link from "next/link"; import { AddSiteButton, EditSiteButton } from "./site-form"; +import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; +import { deleteSite } from "./actions"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "Sites" }; @@ -77,7 +79,10 @@ export default async function SitesPage() { {canEdit && ( - + + + + )} diff --git a/App/pelagia-portal/app/(portal)/admin/users/actions.ts b/App/pelagia-portal/app/(portal)/admin/users/actions.ts index c5e3808..9cd545c 100644 --- a/App/pelagia-portal/app/(portal)/admin/users/actions.ts +++ b/App/pelagia-portal/app/(portal)/admin/users/actions.ts @@ -100,6 +100,23 @@ export async function updateUser(formData: FormData): Promise { return { ok: true }; } +export async function deleteUser(id: string): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_users")) return { error: "Unauthorized" }; + if (id === session.user.id) return { error: "Cannot delete your own account." }; + + const inUse = await db.purchaseOrder.findFirst({ where: { submitterId: id } }); + if (inUse) return { error: "Cannot delete: user has submitted purchase orders. Deactivate them instead." }; + + await db.$transaction(async (tx) => { + await tx.notification.deleteMany({ where: { userId: id } }); + await tx.user.delete({ where: { id } }); + }); + + revalidatePath("/admin/users"); + return { ok: true }; +} + export async function toggleUserActive(userId: string): Promise { const session = await auth(); if (!session?.user || !hasPermission(session.user.role, "manage_users")) { diff --git a/App/pelagia-portal/app/(portal)/admin/users/page.tsx b/App/pelagia-portal/app/(portal)/admin/users/page.tsx index 5cdc8fe..2666c3a 100644 --- a/App/pelagia-portal/app/(portal)/admin/users/page.tsx +++ b/App/pelagia-portal/app/(portal)/admin/users/page.tsx @@ -4,6 +4,8 @@ import { hasPermission } from "@/lib/permissions"; import { redirect } from "next/navigation"; import { formatDate } from "@/lib/utils"; import { AddUserButton, EditUserButton } from "./user-form"; +import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; +import { deleteUser } from "./actions"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "User Management" }; @@ -66,14 +68,17 @@ export default async function AdminUsersPage() { {formatDate(user.createdAt)} - + + + + ))} diff --git a/App/pelagia-portal/app/(portal)/admin/vessels/actions.ts b/App/pelagia-portal/app/(portal)/admin/vessels/actions.ts index 9d2b2b2..32da413 100644 --- a/App/pelagia-portal/app/(portal)/admin/vessels/actions.ts +++ b/App/pelagia-portal/app/(portal)/admin/vessels/actions.ts @@ -62,6 +62,18 @@ export async function updateVessel(formData: FormData): Promise { return { ok: true }; } +export async function deleteVessel(id: string): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_vessels_accounts")) return { error: "Unauthorized" }; + + const inUse = await db.purchaseOrder.findFirst({ where: { vesselId: id } }); + if (inUse) return { error: "Cannot delete: vessel is referenced in purchase orders. Remove those POs first." }; + + await db.vessel.delete({ where: { id } }); + revalidatePath("/admin/vessels"); + return { ok: true }; +} + export async function toggleVesselActive(vesselId: string): Promise { const session = await auth(); if (!session?.user || !hasPermission(session.user.role, "manage_vessels_accounts")) { diff --git a/App/pelagia-portal/app/(portal)/admin/vessels/page.tsx b/App/pelagia-portal/app/(portal)/admin/vessels/page.tsx index 035a701..f72598e 100644 --- a/App/pelagia-portal/app/(portal)/admin/vessels/page.tsx +++ b/App/pelagia-portal/app/(portal)/admin/vessels/page.tsx @@ -3,6 +3,8 @@ import { db } from "@/lib/db"; import { hasPermission } from "@/lib/permissions"; import { redirect } from "next/navigation"; import { AddVesselButton, EditVesselButton } from "./vessel-form"; +import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; +import { deleteVessel } from "./actions"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "Vessel Management" }; @@ -47,12 +49,15 @@ export default async function AdminVesselsPage() { - + + + + ))}