diff --git a/App/pelagia-portal/components/dashboard/spend-charts.tsx b/App/pelagia-portal/components/dashboard/spend-charts.tsx
new file mode 100644
index 0000000..9ee6fc7
--- /dev/null
+++ b/App/pelagia-portal/components/dashboard/spend-charts.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ Tooltip,
+ ResponsiveContainer,
+ CartesianGrid,
+} from "recharts";
+
+interface VesselData {
+ name: string;
+ amount: number;
+}
+
+interface MonthData {
+ month: string;
+ amount: number;
+}
+
+interface Props {
+ vesselData: VesselData[];
+ monthData: MonthData[];
+}
+
+function currencyTick(value: number): string {
+ if (value >= 1_000_000) return `₹${(value / 1_000_000).toFixed(1)}M`;
+ if (value >= 1_000) return `₹${(value / 1_000).toFixed(0)}K`;
+ return `₹${value}`;
+}
+
+function currencyTooltip(value: number): string {
+ return value.toLocaleString("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 });
+}
+
+export function SpendCharts({ vesselData, monthData }: Props) {
+ return (
+
+ {vesselData.length > 0 && (
+
+
Approved Spend by Vessel (Top 5)
+
+
+
+
+
+ [currencyTooltip(Number(v)), "Spend"]} cursor={{ fill: "#f5f5f5" }} />
+
+
+
+
+ )}
+
+ {monthData.length > 0 && (
+
+
Approved Spend by Month
+
+
+
+
+
+ [currencyTooltip(Number(v)), "Spend"]} cursor={{ fill: "#f5f5f5" }} />
+
+
+
+
+ )}
+
+ );
+}
diff --git a/App/pelagia-portal/components/dashboard/stat-card.tsx b/App/pelagia-portal/components/dashboard/stat-card.tsx
new file mode 100644
index 0000000..6083b32
--- /dev/null
+++ b/App/pelagia-portal/components/dashboard/stat-card.tsx
@@ -0,0 +1,60 @@
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import type { LucideIcon } from "lucide-react";
+
+interface StatCardProps {
+ label: string;
+ value: string | number;
+ icon: LucideIcon;
+ color?: "blue" | "green" | "orange" | "red";
+ href?: string;
+}
+
+const COLOR_MAP = {
+ blue: {
+ bg: "bg-primary-50",
+ icon: "text-primary-600",
+ iconBg: "bg-primary-100",
+ },
+ green: {
+ bg: "bg-success-50",
+ icon: "text-success",
+ iconBg: "bg-success-100",
+ },
+ orange: {
+ bg: "bg-warning-50",
+ icon: "text-warning",
+ iconBg: "bg-warning-100",
+ },
+ red: {
+ bg: "bg-danger-50",
+ icon: "text-danger",
+ iconBg: "bg-danger-100",
+ },
+};
+
+export function StatCard({ label, value, icon: Icon, color = "blue", href }: StatCardProps) {
+ const colors = COLOR_MAP[color];
+
+ const card = (
+
+ );
+
+ if (href) {
+ return {card};
+ }
+ return card;
+}
diff --git a/App/pelagia-portal/components/layout/header.tsx b/App/pelagia-portal/components/layout/header.tsx
new file mode 100644
index 0000000..c8a5497
--- /dev/null
+++ b/App/pelagia-portal/components/layout/header.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { signOut } from "next-auth/react";
+import { LogOut } from "lucide-react";
+import type { Role } from "@prisma/client";
+
+const ROLE_LABELS: Record = {
+ TECHNICAL: "Technical",
+ MANNING: "Manning",
+ ACCOUNTS: "Accounts",
+ MANAGER: "Manager",
+ SUPERUSER: "SuperUser",
+ AUDITOR: "Auditor",
+ ADMIN: "Admin",
+};
+
+interface HeaderProps {
+ user: { name: string; email: string; role: Role };
+}
+
+export function Header({ user }: HeaderProps) {
+ return (
+
+
+
+
+
{user.name}
+
{ROLE_LABELS[user.role]}
+
+
+
+
+ );
+}
diff --git a/App/pelagia-portal/components/layout/sidebar.tsx b/App/pelagia-portal/components/layout/sidebar.tsx
new file mode 100644
index 0000000..6f684fc
--- /dev/null
+++ b/App/pelagia-portal/components/layout/sidebar.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { usePathname } from "next/navigation";
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import {
+ LayoutDashboard,
+ FileText,
+ Plus,
+ CheckSquare,
+ CreditCard,
+ History,
+ Users,
+ Ship,
+ Building2,
+ Store,
+ Anchor,
+ Package,
+} from "lucide-react";
+import type { Role } from "@prisma/client";
+
+interface NavItem {
+ href: string;
+ label: string;
+ icon: React.ElementType;
+ roles?: Role[];
+}
+
+const NAV_ITEMS: NavItem[] = [
+ { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
+ { href: "/po/new", label: "New PO", icon: Plus, roles: ["TECHNICAL", "MANNING", "SUPERUSER"] },
+ { href: "/my-orders", label: "My Purchase Orders", icon: FileText, roles: ["TECHNICAL", "MANNING", "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"] },
+];
+
+const ADMIN_ITEMS: NavItem[] = [
+ { href: "/admin/users", label: "Users", icon: Users },
+ { href: "/admin/vessels", label: "Vessels", icon: Ship },
+ { href: "/admin/accounts", label: "Accounts", icon: Building2 },
+ { href: "/admin/vendors", label: "Vendors", icon: Store },
+ { href: "/admin/products", label: "Products", icon: Package },
+];
+
+export function Sidebar({ userRole }: { userRole: Role }) {
+ const pathname = usePathname();
+ const isAdmin = userRole === "ADMIN";
+
+ const visible = NAV_ITEMS.filter(
+ (item) => !item.roles || item.roles.includes(userRole)
+ );
+
+ return (
+
+ );
+}
+
+function NavLink({ item, pathname }: { item: NavItem; pathname: string }) {
+ const isActive =
+ pathname === item.href || pathname.startsWith(item.href + "/");
+ const Icon = item.icon;
+
+ return (
+
+
+ {item.label}
+
+ );
+}
diff --git a/App/pelagia-portal/components/ui/admin-dialog.tsx b/App/pelagia-portal/components/ui/admin-dialog.tsx
new file mode 100644
index 0000000..ddfec1e
--- /dev/null
+++ b/App/pelagia-portal/components/ui/admin-dialog.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { useEffect } from "react";
+
+interface Props {
+ title: string;
+ open: boolean;
+ onClose: () => void;
+ children: React.ReactNode;
+}
+
+export function AdminDialog({ title, open, onClose, children }: Props) {
+ useEffect(() => {
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") onClose();
+ }
+ if (open) document.addEventListener("keydown", onKey);
+ return () => document.removeEventListener("keydown", onKey);
+ }, [open, onClose]);
+
+ if (!open) return null;
+
+ return (
+ { if (e.target === e.currentTarget) onClose(); }}
+ >
+
+
+
{title}
+
+
+
{children}
+
+
+ );
+}
diff --git a/App/pelagia-portal/components/ui/badge.tsx b/App/pelagia-portal/components/ui/badge.tsx
new file mode 100644
index 0000000..e3ebb26
--- /dev/null
+++ b/App/pelagia-portal/components/ui/badge.tsx
@@ -0,0 +1,29 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary-100 text-primary-700",
+ secondary: "bg-neutral-100 text-neutral-700",
+ success: "bg-success-100 text-success-700",
+ warning: "bg-warning-100 text-warning-700",
+ danger: "bg-danger-100 text-danger-700",
+ outline: "border border-neutral-300 text-neutral-600",
+ },
+ },
+ defaultVariants: { variant: "default" },
+ }
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+export function Badge({ className, variant, ...props }: BadgeProps) {
+ return ;
+}
+
+export { badgeVariants };
diff --git a/App/pelagia-portal/components/ui/button.tsx b/App/pelagia-portal/components/ui/button.tsx
new file mode 100644
index 0000000..1f9258b
--- /dev/null
+++ b/App/pelagia-portal/components/ui/button.tsx
@@ -0,0 +1,40 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import { Slot } from "@radix-ui/react-slot";
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500",
+ secondary: "border border-neutral-300 bg-white text-neutral-700 hover:bg-neutral-50 focus-visible:ring-neutral-400",
+ destructive: "bg-danger text-white hover:opacity-90 focus-visible:ring-danger",
+ success: "bg-success text-white hover:opacity-90 focus-visible:ring-success",
+ warning: "border border-warning bg-warning-50 text-warning-700 hover:bg-warning-100 focus-visible:ring-warning",
+ ghost: "text-neutral-700 hover:bg-neutral-100 focus-visible:ring-neutral-400",
+ link: "text-primary-600 underline-offset-4 hover:underline focus-visible:ring-primary-500",
+ },
+ size: {
+ sm: "h-8 px-3 py-1.5 text-xs",
+ md: "h-10 px-4 py-2.5",
+ lg: "h-11 px-5 py-3",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: { variant: "default", size: "md" },
+ }
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+export function Button({ className, variant, size, asChild, ...props }: ButtonProps) {
+ const Comp = asChild ? Slot : "button";
+ return ;
+}
+
+export { buttonVariants };
diff --git a/App/pelagia-portal/components/ui/card.tsx b/App/pelagia-portal/components/ui/card.tsx
new file mode 100644
index 0000000..6184ca3
--- /dev/null
+++ b/App/pelagia-portal/components/ui/card.tsx
@@ -0,0 +1,32 @@
+import { cn } from "@/lib/utils";
+
+export function Card({ className, ...props }: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+export function CardHeader({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
+export function CardTitle({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
+export function CardDescription({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
+export function CardContent({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
+export function CardFooter({ className, ...props }: React.HTMLAttributes) {
+ return (
+
+ );
+}
diff --git a/App/pelagia-portal/components/ui/input.tsx b/App/pelagia-portal/components/ui/input.tsx
new file mode 100644
index 0000000..6d890c5
--- /dev/null
+++ b/App/pelagia-portal/components/ui/input.tsx
@@ -0,0 +1,18 @@
+import { cn } from "@/lib/utils";
+
+export interface InputProps extends React.InputHTMLAttributes {}
+
+export function Input({ className, type, ...props }: InputProps) {
+ return (
+
+ );
+}
diff --git a/App/pelagia-portal/components/ui/label.tsx b/App/pelagia-portal/components/ui/label.tsx
new file mode 100644
index 0000000..7d2c28a
--- /dev/null
+++ b/App/pelagia-portal/components/ui/label.tsx
@@ -0,0 +1,17 @@
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cn } from "@/lib/utils";
+
+export function Label({
+ className,
+ ...props
+}: React.ComponentPropsWithoutRef) {
+ return (
+
+ );
+}
diff --git a/App/pelagia-portal/components/ui/textarea.tsx b/App/pelagia-portal/components/ui/textarea.tsx
new file mode 100644
index 0000000..fa30a6f
--- /dev/null
+++ b/App/pelagia-portal/components/ui/textarea.tsx
@@ -0,0 +1,17 @@
+import { cn } from "@/lib/utils";
+
+export interface TextareaProps extends React.TextareaHTMLAttributes {}
+
+export function Textarea({ className, ...props }: TextareaProps) {
+ return (
+
+ );
+}