diff --git a/App/pelagia-portal/app/(portal)/dashboard/page.tsx b/App/pelagia-portal/app/(portal)/dashboard/page.tsx new file mode 100644 index 0000000..77b0a7d --- /dev/null +++ b/App/pelagia-portal/app/(portal)/dashboard/page.tsx @@ -0,0 +1,261 @@ +import { auth } from "@/auth"; +import { db } from "@/lib/db"; +import { StatCard } from "@/components/dashboard/stat-card"; +import { SpendCharts } from "@/components/dashboard/spend-charts"; +import { formatCurrency, formatDate, PO_STATUS_LABELS } from "@/lib/utils"; +import { FileText, Clock, CheckCircle, DollarSign } from "lucide-react"; +import Link from "next/link"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { title: "Dashboard" }; + +export default async function DashboardPage() { + const session = await auth(); + if (!session?.user) return null; + + const { role, id: userId } = session.user; + + if (role === "TECHNICAL" || role === "MANNING" || role === "SUPERUSER") { + return ; + } + if (role === "MANAGER") { + return ; + } + if (role === "ACCOUNTS") { + return ; + } + return ; +} + +async function SubmitterDashboard({ userId }: { userId: string }) { + const [openCount, pendingCount, closedCount, recentPos] = await Promise.all([ + db.purchaseOrder.count({ + where: { submitterId: userId, status: { in: ["DRAFT", "SUBMITTED", "MGR_REVIEW", "VENDOR_ID_PENDING", "EDITS_REQUESTED"] } }, + }), + db.purchaseOrder.count({ + where: { submitterId: userId, status: "MGR_REVIEW" }, + }), + db.purchaseOrder.count({ + where: { submitterId: userId, status: "CLOSED" }, + }), + db.purchaseOrder.findMany({ + where: { submitterId: userId }, + orderBy: { updatedAt: "desc" }, + take: 8, + select: { id: true, poNumber: true, title: true, status: true, totalAmount: true, updatedAt: true }, + }), + ]); + + return ( +
+

Dashboard

+
+ + + +
+
+ + + New Purchase Order + + + View all my purchase orders → + +
+ {recentPos.length > 0 && ( +
+

Recent Orders

+
+ + + + + + + + + + + + {recentPos.map((po) => ( + + + + + + + + ))} + +
PO NumberTitleStatusAmountUpdated
+ + {po.poNumber} + + {po.title} + + {PO_STATUS_LABELS[po.status]} + + {formatCurrency(Number(po.totalAmount))}{formatDate(po.updatedAt)}
+
+
+ )} +
+ ); +} + +async function ManagerDashboard() { + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const twelveMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 11, 1); + + const approvedStatuses = ["MGR_APPROVED", "SENT_FOR_PAYMENT", "PAID_DELIVERED", "CLOSED"] as const; + + const [awaitingCount, approvedThisMonth, totalSpendResult, recentApproved, vesselBreakdown, monthlyPos] = await Promise.all([ + db.purchaseOrder.count({ where: { status: "MGR_REVIEW" } }), + db.purchaseOrder.count({ where: { status: "MGR_APPROVED", approvedAt: { gte: startOfMonth } } }), + db.purchaseOrder.aggregate({ + _sum: { totalAmount: true }, + where: { status: { in: [...approvedStatuses] } }, + }), + db.purchaseOrder.findMany({ + where: { status: { in: ["MGR_APPROVED", "SENT_FOR_PAYMENT", "PAID_DELIVERED"] } }, + orderBy: { approvedAt: "desc" }, + take: 8, + select: { id: true, poNumber: true, title: true, status: true, totalAmount: true, approvedAt: true, vessel: { select: { name: true } } }, + }), + db.purchaseOrder.groupBy({ + by: ["vesselId"], + _sum: { totalAmount: true }, + where: { status: { in: [...approvedStatuses] } }, + orderBy: { _sum: { totalAmount: "desc" } }, + take: 5, + }), + db.purchaseOrder.findMany({ + where: { status: { in: [...approvedStatuses] }, approvedAt: { gte: twelveMonthsAgo } }, + select: { totalAmount: true, approvedAt: true }, + }), + ]); + + const vesselIds = vesselBreakdown.map((r) => r.vesselId); + const vessels = await db.vessel.findMany({ where: { id: { in: vesselIds } }, select: { id: true, name: true } }); + const vesselMap = Object.fromEntries(vessels.map((v) => [v.id, v.name])); + + const totalSpend = Number(totalSpendResult._sum.totalAmount ?? 0); + + // Build monthly series for last 12 months + const monthlyMap: Record = {}; + for (let i = 11; i >= 0; i--) { + const d = new Date(now.getFullYear(), now.getMonth() - i, 1); + const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; + monthlyMap[key] = 0; + } + for (const po of monthlyPos) { + if (!po.approvedAt) continue; + const key = `${po.approvedAt.getFullYear()}-${String(po.approvedAt.getMonth() + 1).padStart(2, "0")}`; + if (key in monthlyMap) monthlyMap[key] += Number(po.totalAmount); + } + const monthData = Object.entries(monthlyMap).map(([key, amount]) => { + const [year, month] = key.split("-"); + const label = new Date(Number(year), Number(month) - 1, 1).toLocaleString("en", { month: "short", year: "2-digit" }); + return { month: label, amount }; + }); + + const vesselChartData = vesselBreakdown.map((row) => ({ + name: vesselMap[row.vesselId] ?? "Unknown", + amount: Number(row._sum.totalAmount ?? 0), + })); + + return ( +
+

Dashboard

+
+ + + +
+ + {/* Recent approved POs */} + {recentApproved.length > 0 && ( +
+
+

Recent Approved Orders

+ View all → +
+
+ + + + + + + + + + + + + {recentApproved.map((po) => ( + + + + + + + + + ))} + +
POTitleVesselStatusAmountApproved
+ + {po.poNumber} + + {po.title}{po.vessel.name} + + {PO_STATUS_LABELS[po.status]} + + {formatCurrency(Number(po.totalAmount))}{po.approvedAt ? formatDate(po.approvedAt) : "—"}
+
+
+ )} + + +
+ ); +} + +async function AccountsDashboard() { + const [queueCount, queueValueResult] = await Promise.all([ + db.purchaseOrder.count({ where: { status: "MGR_APPROVED" } }), + db.purchaseOrder.aggregate({ + _sum: { totalAmount: true }, + where: { status: "MGR_APPROVED" }, + }), + ]); + + const queueValue = Number(queueValueResult._sum.totalAmount ?? 0); + + return ( +
+

Dashboard

+
+ + +
+
+ ); +} + +async function GenericDashboard() { + const total = await db.purchaseOrder.count(); + return ( +
+

Dashboard

+
+ +
+
+ ); +} diff --git a/App/pelagia-portal/app/(portal)/layout.tsx b/App/pelagia-portal/app/(portal)/layout.tsx new file mode 100644 index 0000000..d563085 --- /dev/null +++ b/App/pelagia-portal/app/(portal)/layout.tsx @@ -0,0 +1,23 @@ +import { auth } from "@/auth"; +import { redirect } from "next/navigation"; +import { Sidebar } from "@/components/layout/sidebar"; +import { Header } from "@/components/layout/header"; + +export default async function PortalLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + if (!session?.user) redirect("/login"); + + return ( +
+ +
+
+
{children}
+
+
+ ); +}