From e3851a1799337a05184864b0dad9323ba59eaed3 Mon Sep 17 00:00:00 2001 From: Hardik Date: Sun, 31 May 2026 02:53:33 +0530 Subject: [PATCH] feat(sidebar+vessels): Purchasing section, split Administration, Cost Centre rename Sidebar: - Inventory section renamed to Purchasing - Manager gets separate Administration section for Vendors only - Admin gets full Administration (Vendors + Users + Accounting Codes + Companies) - Sites hidden from Manager when NEXT_PUBLIC_INVENTORY_ENABLED=false - Cost Centres replaces Vessels in the Purchasing nav link Admin vessel pages: - All headings, titles, dialogs, breadcrumbs: Vessels -> Cost Centre - Error messages updated accordingly Co-Authored-By: Claude Sonnet 4.6 --- App/app/(portal)/admin/vessels/[id]/page.tsx | 4 +- App/app/(portal)/admin/vessels/actions.ts | 6 +- App/app/(portal)/admin/vessels/page.tsx | 2 +- .../(portal)/admin/vessels/vessel-form.tsx | 10 +- .../(portal)/admin/vessels/vessels-table.tsx | 8 +- App/components/layout/sidebar.tsx | 109 ++++++++++-------- 6 files changed, 79 insertions(+), 60 deletions(-) diff --git a/App/app/(portal)/admin/vessels/[id]/page.tsx b/App/app/(portal)/admin/vessels/[id]/page.tsx index ab990f7..5468416 100644 --- a/App/app/(portal)/admin/vessels/[id]/page.tsx +++ b/App/app/(portal)/admin/vessels/[id]/page.tsx @@ -11,7 +11,7 @@ interface Props { params: Promise<{ id: string }> } export async function generateMetadata({ params }: Props): Promise { const { id } = await params; const v = await db.vessel.findUnique({ where: { id }, select: { name: true } }); - return { title: v?.name ?? "Vessel Detail" }; + return { title: v?.name ?? "Cost Centre Detail" }; } export default async function VesselDetailPage({ params }: Props) { @@ -46,7 +46,7 @@ export default async function VesselDetailPage({ params }: Props) { return (
- Vessels + Cost Centres / {vessel.name}
diff --git a/App/app/(portal)/admin/vessels/actions.ts b/App/app/(portal)/admin/vessels/actions.ts index 698b6cc..d3cbc3a 100644 --- a/App/app/(portal)/admin/vessels/actions.ts +++ b/App/app/(portal)/admin/vessels/actions.ts @@ -10,7 +10,7 @@ import { nextId } from "@/lib/id-generators"; type ActionResult = { ok: true } | { error: string }; const vesselSchema = z.object({ - name: z.string().min(1, "Vessel name is required"), + name: z.string().min(1, "Cost centre name is required"), code: z.string().optional(), }); @@ -66,7 +66,7 @@ export async function deleteVessel(id: string): Promise { 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." }; + if (inUse) return { error: "Cannot delete: this cost centre is referenced in purchase orders." }; await db.vessel.delete({ where: { id } }); revalidatePath("/admin/vessels"); @@ -80,7 +80,7 @@ export async function toggleVesselActive(vesselId: string): Promise
- +
@@ -60,9 +60,9 @@ export function AddVesselButton({ suggestedCode }: { suggestedCode?: string }) { <> - setOpen(false)}> + setOpen(false)}>
{error &&

{error}

} @@ -73,7 +73,7 @@ export function AddVesselButton({ suggestedCode }: { suggestedCode?: string }) { @@ -119,7 +119,7 @@ export function EditVesselButton({ Edit )} - setOpen(false)}> + setOpen(false)}>
{error &&

{error}

} diff --git a/App/app/(portal)/admin/vessels/vessels-table.tsx b/App/app/(portal)/admin/vessels/vessels-table.tsx index 1f33347..80971d3 100644 --- a/App/app/(portal)/admin/vessels/vessels-table.tsx +++ b/App/app/(portal)/admin/vessels/vessels-table.tsx @@ -49,7 +49,7 @@ function VesselActionsMenu({ vessel }: { vessel: VesselRow }) { 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.`} + description={vessel.isActive ? `${vessel.name} will be hidden from new PO cost centre selection.` : `${vessel.name} will become available in PO cost centre selection.`} confirmLabel={vessel.isActive ? "Deactivate" : "Activate"} onConfirm={() => toggleVesselActive(vessel.id)} /> @@ -79,14 +79,14 @@ export function VesselsTable({ vessels, suggestedCode }: { vessels: VesselRow[]; return (
-

Vessel Management

+

Cost Centre Management

- No vessels match your search. + No cost centres match your search. )} diff --git a/App/components/layout/sidebar.tsx b/App/components/layout/sidebar.tsx index d924e4d..98495ce 100644 --- a/App/components/layout/sidebar.tsx +++ b/App/components/layout/sidebar.tsx @@ -35,52 +35,57 @@ interface NavItem { } const NAV_ITEMS: NavItem[] = [ - { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, - { href: "/po/new", label: "New PO", icon: Plus, roles: ["TECHNICAL", "MANNING", "MANAGER", "SUPERUSER"] }, - { href: "/my-orders", label: "Closed Purchase Orders", icon: FileText, roles: ["TECHNICAL", "MANNING", "MANAGER", "SUPERUSER"] }, - { href: "/po/import", label: "Import PO", icon: Upload, roles: ["MANAGER", "SUPERUSER"] }, - { href: "/approvals", label: "Approvals", icon: CheckSquare, roles: ["MANAGER", "SUPERUSER"] }, - { href: "/payments", label: "Payments", icon: CreditCard, roles: ["ACCOUNTS"] }, - { href: "/payments/history", label: "Payment History", icon: Receipt, roles: ["ACCOUNTS", "SUPERUSER"] }, - { href: "/history", label: "History", icon: History, roles: ["MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"] }, - { href: "/profile", label: "My Profile", icon: UserCircle }, + { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, + { href: "/po/new", label: "New PO", icon: Plus, roles: ["TECHNICAL", "MANNING", "MANAGER", "SUPERUSER"] }, + { href: "/my-orders", label: "Closed Purchase Orders", icon: FileText, roles: ["TECHNICAL", "MANNING", "MANAGER", "SUPERUSER"] }, + { href: "/po/import", label: "Import PO", icon: Upload, roles: ["MANAGER", "SUPERUSER"] }, + { href: "/approvals", label: "Approvals", icon: CheckSquare, roles: ["MANAGER", "SUPERUSER"] }, + { href: "/payments", label: "Payments", icon: CreditCard, roles: ["ACCOUNTS"] }, + { href: "/payments/history", label: "Payment History", icon: Receipt, roles: ["ACCOUNTS", "SUPERUSER"] }, + { href: "/history", label: "History", icon: History, roles: ["MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"] }, + { href: "/profile", label: "My Profile", icon: UserCircle }, ]; -// Vendor/product/cart nav — always visible (needed for PO creation) -const PO_CATALOGUE_ITEMS: NavItem[] = [ - { href: "/inventory/items", label: "Items", icon: Package, roles: ["TECHNICAL", "MANNING", "SUPERUSER"] }, - { href: "/inventory/vendors", label: "Vendors", icon: Store, roles: ["TECHNICAL", "MANNING", "SUPERUSER"] }, - { href: "/inventory/cart", label: "Cart", icon: ShoppingCart, roles: ["TECHNICAL", "MANNING", "SUPERUSER", "MANAGER"] }, - { href: "/admin/vendors", label: "Vendors", icon: Store, roles: ["MANAGER", "ACCOUNTS", "ADMIN"] }, - { href: "/admin/products", label: "Items", icon: Package, roles: ["MANAGER", "ADMIN"] }, - { href: "/admin/vessels", label: "Vessels", icon: Ship, roles: ["MANAGER", "ADMIN"] }, - { href: "/admin/sites", label: "Sites", icon: MapPin, roles: ["MANAGER", "ADMIN"] }, +// ── Purchasing section ──────────────────────────────────────────────────────── +// Staff browsing items (product catalogue + cart for PO creation) +const PURCHASING_STAFF: NavItem[] = [ + { href: "/inventory/items", label: "Items", icon: Package, roles: ["TECHNICAL", "MANNING", "SUPERUSER"] }, + { href: "/inventory/vendors", label: "Vendors", icon: Store, roles: ["TECHNICAL", "MANNING", "SUPERUSER"] }, + { href: "/inventory/cart", label: "Cart", icon: ShoppingCart, roles: ["TECHNICAL", "MANNING", "SUPERUSER", "MANAGER"] }, ]; -// Inventory tracking nav — hidden when NEXT_PUBLIC_INVENTORY_ENABLED=false -const INVENTORY_TRACKING_ITEMS: NavItem[] = []; // reserved for future inventory-only links +// Manager / Admin catalogue management — Sites conditionally shown +const PURCHASING_MGMT: NavItem[] = [ + { href: "/admin/products", label: "Items", icon: Package, roles: ["MANAGER", "ADMIN"] }, + { href: "/admin/vessels", label: "Cost Centres", icon: Ship, roles: ["MANAGER", "ADMIN"] }, + ...(INVENTORY_ENABLED + ? [{ href: "/admin/sites", label: "Sites", icon: MapPin, roles: ["MANAGER", "ADMIN"] as Role[] }] + : []), +]; -const INVENTORY_ITEMS: NavItem[] = INVENTORY_ENABLED - ? [...PO_CATALOGUE_ITEMS, ...INVENTORY_TRACKING_ITEMS] - : PO_CATALOGUE_ITEMS; +const PURCHASING_ITEMS: NavItem[] = [...PURCHASING_STAFF, ...PURCHASING_MGMT]; +// ── Administration section ──────────────────────────────────────────────────── +// Vendors shown to MANAGER / ACCOUNTS under their own Administration header +const MANAGER_ADMIN_ITEMS: NavItem[] = [ + { href: "/admin/vendors", label: "Vendors", icon: Store, roles: ["MANAGER", "ACCOUNTS", "ADMIN"] }, +]; + +// Full Administration section (ADMIN only) const ADMIN_ITEMS: NavItem[] = [ - { href: "/admin/users", label: "Users", icon: Users }, - { href: "/admin/superuser-requests", label: "SuperUser Requests", icon: ShieldCheck }, - { href: "/admin/accounts", label: "Accounting Codes", icon: Building2 }, - { href: "/admin/companies", label: "Companies", icon: Briefcase }, + { href: "/admin/users", label: "Users", icon: Users }, + { href: "/admin/superuser-requests", label: "SuperUser Requests",icon: ShieldCheck }, + { href: "/admin/accounts", label: "Accounting Codes", icon: Building2 }, + { href: "/admin/companies", label: "Companies", icon: Briefcase }, ]; export function Sidebar({ userRole }: { userRole: Role }) { const pathname = usePathname(); - const isAdmin = userRole === "ADMIN"; + const isAdmin = userRole === "ADMIN"; - const visible = NAV_ITEMS.filter( - (item) => !item.roles || item.roles.includes(userRole) - ); - const visibleInventory = INVENTORY_ITEMS.filter( - (item) => !item.roles || item.roles.includes(userRole) - ); + const visibleMain = NAV_ITEMS.filter((i) => !i.roles || i.roles.includes(userRole)); + const visiblePurchasing = PURCHASING_ITEMS.filter((i) => !i.roles || i.roles.includes(userRole)); + const visibleMgrAdmin = MANAGER_ADMIN_ITEMS.filter((i) => !i.roles || i.roles.includes(userRole)); return (