diff --git a/App/pelagia-portal/app/(portal)/history/history-filters.tsx b/App/pelagia-portal/app/(portal)/history/history-filters.tsx
new file mode 100644
index 0000000..f3bdd28
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/history/history-filters.tsx
@@ -0,0 +1,92 @@
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+import { useState } from "react";
+
+const STATUSES = [
+ { value: "", label: "All statuses" },
+ { value: "DRAFT", label: "Draft" },
+ { value: "SUBMITTED", label: "Submitted" },
+ { value: "MGR_REVIEW", label: "Pending Approval" },
+ { value: "VENDOR_ID_PENDING", label: "Vendor ID Pending" },
+ { value: "EDITS_REQUESTED", label: "Edits Requested" },
+ { value: "MGR_APPROVED", label: "Approved" },
+ { value: "SENT_FOR_PAYMENT", label: "Sent for Payment" },
+ { value: "PAID_DELIVERED", label: "Paid / Delivered" },
+ { value: "CLOSED", label: "Closed" },
+ { value: "REJECTED", label: "Rejected" },
+];
+
+interface Props {
+ vessels: { id: string; name: string }[];
+}
+
+export function HistoryFilters({ 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") ?? "");
+ const [status, setStatus] = useState(sp.get("status") ?? "");
+
+ function apply() {
+ const params = new URLSearchParams();
+ if (dateFrom) params.set("dateFrom", dateFrom);
+ if (dateTo) params.set("dateTo", dateTo);
+ if (vesselId) params.set("vesselId", vesselId);
+ if (status) params.set("status", status);
+ router.push(`/history?${params.toString()}`);
+ }
+
+ function clear() {
+ setDateFrom(""); setDateTo(""); setVesselId(""); setStatus("");
+ router.push("/history");
+ }
+
+ const hasFilters = dateFrom || dateTo || vesselId || status;
+
+ 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/app/(portal)/history/page.tsx b/App/pelagia-portal/app/(portal)/history/page.tsx
new file mode 100644
index 0000000..f87c73a
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/history/page.tsx
@@ -0,0 +1,128 @@
+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, PO_STATUS_LABELS } from "@/lib/utils";
+import { PoStatusBadge } from "@/components/po/po-status-badge";
+import { HistoryFilters } from "./history-filters";
+import { Suspense } from "react";
+import type { Metadata } from "next";
+import type { POStatus } from "@prisma/client";
+
+export const metadata: Metadata = { title: "History" };
+
+interface Props {
+ searchParams: Promise<{
+ dateFrom?: string;
+ dateTo?: string;
+ vesselId?: string;
+ status?: string;
+ }>;
+}
+
+export default async function HistoryPage({ searchParams }: Props) {
+ const session = await auth();
+ if (!session?.user) redirect("/login");
+
+ if (!hasPermission(session.user.role, "export_reports")) redirect("/dashboard");
+
+ const { dateFrom, dateTo, vesselId, status } = await searchParams;
+
+ const where: Parameters[0]["where"] = {};
+ if (dateFrom) where.createdAt = { ...where.createdAt, gte: new Date(dateFrom) };
+ if (dateTo) {
+ const end = new Date(dateTo);
+ end.setDate(end.getDate() + 1);
+ where.createdAt = { ...where.createdAt, lt: end };
+ }
+ if (vesselId) where.vesselId = vesselId;
+ if (status) where.status = status as POStatus;
+
+ const [orders, vessels] = await Promise.all([
+ db.purchaseOrder.findMany({
+ where,
+ include: { submitter: true, vessel: true, account: true },
+ orderBy: { createdAt: "desc" },
+ take: 200,
+ }),
+ db.vessel.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true } }),
+ ]);
+
+ const exportParams = new URLSearchParams({ format: "csv" });
+ if (dateFrom) exportParams.set("dateFrom", dateFrom);
+ if (dateTo) exportParams.set("dateTo", dateTo);
+ if (vesselId) exportParams.set("vesselId", vesselId);
+ if (status) exportParams.set("status", status);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ | PO Number |
+ Title |
+ Vessel |
+ Submitter |
+ Status |
+ Amount |
+ Created |
+
+
+
+ {orders.map((po) => (
+
+ |
+
+ {po.poNumber}
+
+ |
+ {po.title} |
+ {po.vessel.name} |
+ {po.submitter.name} |
+
+
+ |
+
+ {formatCurrency(Number(po.totalAmount), po.currency)}
+ |
+ {formatDate(po.createdAt)} |
+
+ ))}
+
+
+ {orders.length === 0 && (
+
No purchase orders found.
+ )}
+
+ {orders.length === 200 && (
+
Showing first 200 results — refine filters to narrow results.
+ )}
+
+ );
+}
diff --git a/App/pelagia-portal/app/api/reports/export/route.ts b/App/pelagia-portal/app/api/reports/export/route.ts
new file mode 100644
index 0000000..1012d87
--- /dev/null
+++ b/App/pelagia-portal/app/api/reports/export/route.ts
@@ -0,0 +1,123 @@
+import { auth } from "@/auth";
+import { db } from "@/lib/db";
+import { hasPermission } from "@/lib/permissions";
+import { NextRequest, NextResponse } from "next/server";
+import type { POStatus } from "@prisma/client";
+
+const PO_STATUS_LABELS: Record = {
+ DRAFT: "Draft", SUBMITTED: "Submitted", MGR_REVIEW: "Pending Approval",
+ VENDOR_ID_PENDING: "Vendor ID Pending", EDITS_REQUESTED: "Edits Requested",
+ REJECTED: "Rejected", MGR_APPROVED: "Approved", SENT_FOR_PAYMENT: "Sent for Payment",
+ PAID_DELIVERED: "Paid / Delivered", CLOSED: "Closed",
+};
+
+export async function GET(request: NextRequest) {
+ const session = await auth();
+ if (!session?.user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ if (!hasPermission(session.user.role, "export_reports")) {
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
+ }
+
+ const sp = request.nextUrl.searchParams;
+ const format = sp.get("format") ?? "csv";
+ const dateFrom = sp.get("dateFrom");
+ const dateTo = sp.get("dateTo");
+ const vesselId = sp.get("vesselId");
+ const status = sp.get("status");
+
+ const where: Parameters[0]["where"] = {};
+ if (dateFrom) where.createdAt = { ...where.createdAt, gte: new Date(dateFrom) };
+ if (dateTo) {
+ const end = new Date(dateTo);
+ end.setDate(end.getDate() + 1);
+ where.createdAt = { ...where.createdAt, lt: end };
+ }
+ if (vesselId) where.vesselId = vesselId;
+ if (status) where.status = status as POStatus;
+
+ const orders = await db.purchaseOrder.findMany({
+ where,
+ include: { submitter: true, vessel: true, account: true, vendor: true },
+ orderBy: { createdAt: "desc" },
+ });
+
+ if (format === "pdf") {
+ const rows = orders.map((po) => `
+
+ | ${po.poNumber} |
+ ${po.title} |
+ ${PO_STATUS_LABELS[po.status] ?? po.status} |
+ ${po.vessel.name} |
+ ${po.submitter.name} |
+ ${po.vendor?.name ?? "—"} |
+ ${Number(po.totalAmount).toLocaleString("en-IN", { style: "currency", currency: "INR" })} |
+ ${po.createdAt.toLocaleDateString("en-IN")} |
+
`).join("");
+
+ const html = `
+
+
+
+PO Export — Pelagia Portal
+
+
+
+
+
+
+Purchase Order Report — Pelagia Portal
+Generated: ${new Date().toLocaleString("en-IN")} · ${orders.length} orders
+
+
+
+ | PO Number | Title | Status | Vessel |
+ Submitter | Vendor | Amount | Created |
+
+
+ ${rows}
+
+
+
+`;
+
+ return new NextResponse(html, {
+ headers: { "Content-Type": "text/html; charset=utf-8" },
+ });
+ }
+
+ // Default: CSV
+ const headers = ["PO Number", "Title", "Status", "Vessel", "Account", "Vendor", "Submitter", "Amount", "Currency", "Created"];
+ const csvRows = orders.map((po) => [
+ po.poNumber,
+ `"${po.title.replace(/"/g, '""')}"`,
+ po.status,
+ po.vessel.name,
+ po.account.name,
+ po.vendor?.name ?? "",
+ po.submitter.name,
+ po.totalAmount.toString(),
+ po.currency,
+ po.createdAt.toISOString(),
+ ]);
+
+ const csv = [headers.join(","), ...csvRows.map((r) => r.join(","))].join("\n");
+
+ return new NextResponse(csv, {
+ headers: {
+ "Content-Type": "text/csv",
+ "Content-Disposition": `attachment; filename="pelagia-po-export-${Date.now()}.csv"`,
+ },
+ });
+}