From 4c1a41fe611fedc15f72c1a7e27460c9bd513d46 Mon Sep 17 00:00:00 2001 From: Hardik Date: Sat, 16 May 2026 15:56:07 +0530 Subject: [PATCH] feat(accounts): add payment history page at /payments/history Accounts users had no way to review POs that had already been processed through the payment pipeline. The existing /history page requires the export_reports permission which accounts does not hold. New page at /payments/history: - Scoped to PAID_DELIVERED and CLOSED statuses only - Gated on view_all_pos permission (held by ACCOUNTS, MANAGER, SUPERUSER, etc.) - Filterable by paid-date range (paidAt) and cost centre - Columns: PO number, title, cost centre, vendor, submitter, status badge, payment ref, amount, paid date - Summary bar at top showing total paid amount and order count - 200-row soft limit with prompt to refine filters Sidebar: - Added "Payment History" link (Receipt icon) visible to ACCOUNTS and SUPERUSER - Removed ACCOUNTS from the /history nav item since that page requires export_reports which accounts does not have (fixes the dead sidebar link) Co-Authored-By: Claude Sonnet 4.6 --- .../app/(portal)/payments/history/page.tsx | 152 ++++++++++++++++++ .../history/payment-history-filters.tsx | 88 ++++++++++ .../components/layout/sidebar.tsx | 4 +- 3 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 App/pelagia-portal/app/(portal)/payments/history/page.tsx create mode 100644 App/pelagia-portal/app/(portal)/payments/history/payment-history-filters.tsx diff --git a/App/pelagia-portal/app/(portal)/payments/history/page.tsx b/App/pelagia-portal/app/(portal)/payments/history/page.tsx new file mode 100644 index 0000000..a5b7e91 --- /dev/null +++ b/App/pelagia-portal/app/(portal)/payments/history/page.tsx @@ -0,0 +1,152 @@ +import { auth } from "@/auth"; +import { db } from "@/lib/db"; +import { hasPermission } from "@/lib/permissions"; +import { redirect } from "next/navigation"; +import Link from "next/link"; +import { formatCurrency, formatDate } from "@/lib/utils"; +import { PoStatusBadge } from "@/components/po/po-status-badge"; +import { PaymentHistoryFilters } from "./payment-history-filters"; +import { Suspense } from "react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { title: "Payment History" }; + +interface Props { + searchParams: Promise<{ + dateFrom?: string; + dateTo?: string; + vesselId?: string; + }>; +} + +export default async function PaymentHistoryPage({ searchParams }: Props) { + const session = await auth(); + if (!session?.user) redirect("/login"); + + if (!hasPermission(session.user.role, "view_all_pos")) redirect("/dashboard"); + + const { dateFrom, dateTo, vesselId } = await searchParams; + + const where: Parameters[0]["where"] = { + status: { in: ["PAID_DELIVERED", "CLOSED"] }, + }; + + if (dateFrom) where.paidAt = { ...where.paidAt as object, gte: new Date(dateFrom) }; + if (dateTo) { + const end = new Date(dateTo); + end.setDate(end.getDate() + 1); + where.paidAt = { ...where.paidAt as object, lt: end }; + } + if (vesselId) where.vesselId = vesselId; + + const [orders, vessels, totalResult] = await Promise.all([ + db.purchaseOrder.findMany({ + where, + include: { + submitter: true, + vessel: true, + account: true, + vendor: true, + }, + orderBy: { paidAt: "desc" }, + take: 200, + }), + db.vessel.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true } }), + db.purchaseOrder.aggregate({ + _sum: { totalAmount: true }, + where, + }), + ]); + + const totalPaid = Number(totalResult._sum.totalAmount ?? 0); + + return ( +
+
+

Payment History

+

+ POs marked as paid or fully closed +

+
+ + {/* Summary stat */} +
+
+

Total Paid

+

+ {formatCurrency(totalPaid)} +

+
+
+

Orders

+

{orders.length}

+
+
+ + + + + +
+ + + + + + + + + + + + + + + + {orders.map((po) => ( + + + + + + + + + + + + ))} + +
PO NumberTitleCost CentreVendorSubmitterStatusPayment RefAmountPaid
+ + {po.poNumber} + + + {po.title} + {po.vessel.name}{po.vendor?.name ?? "—"}{po.submitter.name} + + + {po.paymentRef ?? "—"} + + {formatCurrency(Number(po.totalAmount), po.currency)} + + {po.paidAt ? formatDate(po.paidAt) : "—"} +
+ {orders.length === 0 && ( +
+ No paid orders found. +
+ )} +
+ + {orders.length === 200 && ( +

+ Showing first 200 results — refine filters to narrow results. +

+ )} +
+ ); +} diff --git a/App/pelagia-portal/app/(portal)/payments/history/payment-history-filters.tsx b/App/pelagia-portal/app/(portal)/payments/history/payment-history-filters.tsx new file mode 100644 index 0000000..135a214 --- /dev/null +++ b/App/pelagia-portal/app/(portal)/payments/history/payment-history-filters.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { useState } from "react"; + +interface Props { + vessels: { id: string; name: string }[]; +} + +export function PaymentHistoryFilters({ vessels }: Props) { + const router = useRouter(); + const sp = useSearchParams(); + + const [dateFrom, setDateFrom] = useState(sp.get("dateFrom") ?? ""); + const [dateTo, setDateTo] = useState(sp.get("dateTo") ?? ""); + const [vesselId, setVesselId] = useState(sp.get("vesselId") ?? ""); + + function apply() { + const params = new URLSearchParams(); + if (dateFrom) params.set("dateFrom", dateFrom); + if (dateTo) params.set("dateTo", dateTo); + if (vesselId) params.set("vesselId", vesselId); + router.push(`/payments/history?${params.toString()}`); + } + + function clear() { + setDateFrom(""); + setDateTo(""); + setVesselId(""); + router.push("/payments/history"); + } + + const hasFilters = dateFrom || dateTo || vesselId; + + return ( +
+
+
+ + setDateFrom(e.target.value)} + className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20" + /> +
+
+ + setDateTo(e.target.value)} + className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20" + /> +
+
+ + +
+
+
+ + {hasFilters && ( + + )} +
+
+ ); +} diff --git a/App/pelagia-portal/components/layout/sidebar.tsx b/App/pelagia-portal/components/layout/sidebar.tsx index 39bdaa6..9f326e3 100644 --- a/App/pelagia-portal/components/layout/sidebar.tsx +++ b/App/pelagia-portal/components/layout/sidebar.tsx @@ -10,6 +10,7 @@ import { CheckSquare, CreditCard, History, + Receipt, Users, Ship, Building2, @@ -37,7 +38,8 @@ const NAV_ITEMS: NavItem[] = [ { 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: "/history", label: "History", icon: History, roles: ["MANAGER", "SUPERUSER", "ACCOUNTS", "AUDITOR", "ADMIN"] }, + { href: "/payments/history", label: "Payment History", icon: Receipt, roles: ["ACCOUNTS", "SUPERUSER"] }, + { href: "/history", label: "History", icon: History, roles: ["MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"] }, ]; const INVENTORY_ITEMS: NavItem[] = [