diff --git a/App/pelagia-portal/app/(portal)/my-orders/page.tsx b/App/pelagia-portal/app/(portal)/my-orders/page.tsx
new file mode 100644
index 0000000..2eab671
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/my-orders/page.tsx
@@ -0,0 +1,118 @@
+import { auth } from "@/auth";
+import { db } from "@/lib/db";
+import { redirect } from "next/navigation";
+import Link from "next/link";
+import { formatCurrency, formatDate, PO_STATUS_LABELS, PO_STATUS_VARIANTS } from "@/lib/utils";
+import { PoStatusBadge } from "@/components/po/po-status-badge";
+import type { Metadata } from "next";
+
+export const metadata: Metadata = { title: "My Purchase Orders" };
+
+export default async function MyOrdersPage() {
+ const session = await auth();
+ if (!session?.user) redirect("/login");
+
+ const { role, id: userId } = session.user;
+ if (!["TECHNICAL", "MANNING", "SUPERUSER"].includes(role)) redirect("/dashboard");
+
+ const pos = await db.purchaseOrder.findMany({
+ where: { submitterId: userId },
+ orderBy: { updatedAt: "desc" },
+ include: {
+ vessel: { select: { name: true } },
+ account: { select: { name: true, code: true } },
+ },
+ });
+
+ const open = pos.filter((p) =>
+ ["DRAFT", "SUBMITTED", "MGR_REVIEW", "VENDOR_ID_PENDING", "EDITS_REQUESTED"].includes(p.status)
+ );
+ const closed = pos.filter((p) =>
+ ["MGR_APPROVED", "SENT_FOR_PAYMENT", "PAID_DELIVERED", "CLOSED", "REJECTED"].includes(p.status)
+ );
+
+ return (
+
+
+
My Purchase Orders
+
+ + New PO
+
+
+
+
+ {closed.length > 0 &&
}
+ {pos.length === 0 && (
+
+
You haven't raised any purchase orders yet.
+
+ Create your first PO →
+
+
+ )}
+
+ );
+}
+
+type PoRow = {
+ id: string;
+ poNumber: string;
+ title: string;
+ status: import("@prisma/client").POStatus;
+ totalAmount: import("@prisma/client").Prisma.Decimal;
+ vessel: { name: string };
+ account: { name: string; code: string };
+ updatedAt: Date;
+ managerNote: string | null;
+};
+
+function PoTable({ title, rows, className = "" }: { title: string; rows: PoRow[]; className?: string }) {
+ if (rows.length === 0) return null;
+ return (
+
+
{title}
+
+
+
+
+ | PO Number |
+ Title |
+ Vessel |
+ Status |
+ Amount |
+ Updated |
+
+
+
+ {rows.map((po) => (
+
+ |
+
+ {po.poNumber}
+
+ |
+
+
+ {po.title}
+
+ {po.managerNote && (
+
+ Note: {po.managerNote}
+
+ )}
+ |
+ {po.vessel.name} |
+ |
+ {formatCurrency(Number(po.totalAmount))} |
+ {formatDate(po.updatedAt)} |
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/App/pelagia-portal/app/(portal)/po/[id]/receipt/actions.ts b/App/pelagia-portal/app/(portal)/po/[id]/receipt/actions.ts
new file mode 100644
index 0000000..c1497f9
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/po/[id]/receipt/actions.ts
@@ -0,0 +1,55 @@
+"use server";
+
+import { auth } from "@/auth";
+import { db } from "@/lib/db";
+import { canPerformAction } from "@/lib/po-state-machine";
+import { notify } from "@/lib/notifier";
+import { revalidatePath } from "next/cache";
+
+export async function confirmReceipt({
+ poId,
+ notes,
+}: {
+ poId: string;
+ notes?: string;
+}): Promise<{ ok: true } | { error: string }> {
+ const session = await auth();
+ if (!session?.user) return { error: "Unauthorized" };
+
+ const po = await db.purchaseOrder.findUnique({
+ where: { id: poId },
+ include: { submitter: true },
+ });
+ if (!po) return { error: "PO not found" };
+ if (!canPerformAction(po.status, "confirm_receipt", session.user.role)) {
+ return { error: "You cannot confirm receipt on this PO." };
+ }
+
+ await db.purchaseOrder.update({
+ where: { id: poId },
+ data: {
+ status: "CLOSED",
+ closedAt: new Date(),
+ receipt: notes
+ ? { create: { storageKey: "", fileName: "no-file", notes } }
+ : undefined,
+ actions: {
+ create: {
+ actionType: "RECEIPT_CONFIRMED",
+ actorId: session.user.id,
+ note: notes ?? null,
+ },
+ },
+ },
+ });
+
+ const [managers, accounts] = await Promise.all([
+ db.user.findMany({ where: { role: "MANAGER", isActive: true } }),
+ db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } }),
+ ]);
+ await notify({ event: "RECEIPT_CONFIRMED", po, recipients: [...managers, ...accounts] });
+
+ revalidatePath(`/po/${poId}`);
+ revalidatePath("/dashboard");
+ return { ok: true };
+}
diff --git a/App/pelagia-portal/app/(portal)/po/[id]/receipt/page.tsx b/App/pelagia-portal/app/(portal)/po/[id]/receipt/page.tsx
new file mode 100644
index 0000000..2d0abd1
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/po/[id]/receipt/page.tsx
@@ -0,0 +1,41 @@
+import { auth } from "@/auth";
+import { db } from "@/lib/db";
+import { notFound, redirect } from "next/navigation";
+import { ReceiptForm } from "./receipt-form";
+import type { Metadata } from "next";
+
+export const metadata: Metadata = { title: "Confirm Receipt" };
+
+interface Props {
+ params: Promise<{ id: string }>;
+}
+
+export default async function ReceiptPage({ params }: Props) {
+ const session = await auth();
+ if (!session?.user) redirect("/login");
+
+ const { id } = await params;
+
+ const po = await db.purchaseOrder.findUnique({
+ where: { id },
+ select: { id: true, poNumber: true, title: true, status: true, submitterId: true },
+ });
+
+ if (!po) notFound();
+ if (po.status !== "PAID_DELIVERED") redirect(`/po/${id}`);
+ if (po.submitterId !== session.user.id && session.user.role !== "SUPERUSER") {
+ redirect(`/po/${id}`);
+ }
+
+ return (
+
+
+
Confirm Receipt
+
+ {po.poNumber} — {po.title}
+
+
+
+
+ );
+}
diff --git a/App/pelagia-portal/app/(portal)/po/[id]/receipt/receipt-form.tsx b/App/pelagia-portal/app/(portal)/po/[id]/receipt/receipt-form.tsx
new file mode 100644
index 0000000..923ca92
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/po/[id]/receipt/receipt-form.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { confirmReceipt } from "./actions";
+import { FileUploader } from "@/components/po/file-uploader";
+import { uploadAndLinkFiles } from "@/lib/upload-files";
+
+export function ReceiptForm({ poId }: { poId: string }) {
+ const router = useRouter();
+ const [notes, setNotes] = useState("");
+ const [files, setFiles] = useState([]);
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState("");
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setSubmitting(true);
+ setError("");
+ const result = await confirmReceipt({ poId, notes });
+ if ("error" in result) {
+ setError(result.error);
+ setSubmitting(false);
+ return;
+ }
+ if (files.length > 0) {
+ const uploadErr = await uploadAndLinkFiles(poId, files, "receipt");
+ if (uploadErr) {
+ setError(uploadErr.error);
+ setSubmitting(false);
+ return;
+ }
+ }
+ router.push(`/po/${poId}`);
+ router.refresh();
+ }
+
+ return (
+
+ );
+}