From cfb16600d75891afba8f1e4f35b71f2c527c81f8 Mon Sep 17 00:00:00 2001 From: Hardik Date: Sat, 16 May 2026 21:27:43 +0530 Subject: [PATCH] feat(mobile): manager approval queue and PO review on small screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a responsive mobile experience scoped to MANAGER and SUPERUSER roles: Layout: - Sidebar hidden on small screens (md: breakpoint) - New MobileHeader (logo + notification bell + sign out) visible on mobile - New MobileBottomNav (Approvals + Profile tabs) pinned to bottom on mobile - New DesktopRequired overlay shown to all other roles on small screens — a fixed full-screen message directing them to use a desktop browser Approvals queue: - Desktop: existing table layout (hidden md:block) - Mobile: tap-to-review card stack (md:hidden) — shows PO number, title, submitter, cost centre, amount, and a full-width Review button PO approval detail: - ManagerEditPoForm (direct field editing) hidden on mobile; still available on desktop (direct edits not required per spec) - ApprovalActions buttons stack full-width on mobile, row on sm+ - Paddings reduced on small screens throughout PO detail component: - Order Details and Vendor grids switch from grid-cols-2 → grid-cols-1 on small screens (sm:grid-cols-2 restores two columns at 640px+) - Section padding reduced on mobile (p-3 md:p-6, p-4 md:p-6) - Line items table already had overflow-x-auto — no change needed Co-Authored-By: Claude Sonnet 4.6 --- .../approvals/[id]/approval-actions.tsx | 14 +-- .../app/(portal)/approvals/[id]/page.tsx | 27 +++-- .../app/(portal)/approvals/page.tsx | 103 ++++++++++++------ App/pelagia-portal/app/(portal)/layout.tsx | 38 ++++++- .../components/layout/desktop-required.tsx | 19 ++++ .../components/layout/mobile-bottom-nav.tsx | 36 ++++++ .../components/layout/mobile-header.tsx | 47 ++++++++ .../components/po/po-detail.tsx | 10 +- 8 files changed, 232 insertions(+), 62 deletions(-) create mode 100644 App/pelagia-portal/components/layout/desktop-required.tsx create mode 100644 App/pelagia-portal/components/layout/mobile-bottom-nav.tsx create mode 100644 App/pelagia-portal/components/layout/mobile-header.tsx diff --git a/App/pelagia-portal/app/(portal)/approvals/[id]/approval-actions.tsx b/App/pelagia-portal/app/(portal)/approvals/[id]/approval-actions.tsx index fee7e5d..0f92e2b 100644 --- a/App/pelagia-portal/app/(portal)/approvals/[id]/approval-actions.tsx +++ b/App/pelagia-portal/app/(portal)/approvals/[id]/approval-actions.tsx @@ -42,7 +42,7 @@ export function ApprovalActions({ } return ( -
+

Decision

{(activeAction === "reject" || activeAction === "request_edits" || activeAction === "approve_note") && ( @@ -70,14 +70,14 @@ export function ApprovalActions({

{error}

)} -
+
@@ -87,14 +87,14 @@ export function ApprovalActions({ if (activeAction === "request_edits") dispatch("request_edits", true); }} disabled={!!pending} - className="rounded-lg border border-warning bg-warning-50 px-4 py-2.5 text-sm font-medium text-warning-700 hover:bg-warning-100 disabled:opacity-60 transition-colors" + className="w-full sm:w-auto rounded-lg border border-warning bg-warning-50 px-4 py-2.5 text-sm font-medium text-warning-700 hover:bg-warning-100 disabled:opacity-60 transition-colors" > {pending === "request_edits" ? "Sending…" : activeAction === "request_edits" ? "Send Edit Request" : "Request Edits"} @@ -104,14 +104,14 @@ export function ApprovalActions({ if (activeAction === "approve_note") dispatch("approve_note"); }} disabled={!!pending} - className="rounded-lg border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-60 transition-colors" + className="w-full sm:w-auto rounded-lg border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-60 transition-colors" > {pending === "approve_note" ? "Approving…" : activeAction === "approve_note" ? "Confirm Approve with Remarks" : "Approve with Remarks"} diff --git a/App/pelagia-portal/app/(portal)/approvals/[id]/page.tsx b/App/pelagia-portal/app/(portal)/approvals/[id]/page.tsx index 42c84fa..e12ee68 100644 --- a/App/pelagia-portal/app/(portal)/approvals/[id]/page.tsx +++ b/App/pelagia-portal/app/(portal)/approvals/[id]/page.tsx @@ -64,10 +64,10 @@ export default async function ApprovalDetailPage({ params }: Props) { return (
-
+
-

Review Purchase Order

-

{po.poNumber} — {po.title}

+

Review Purchase Order

+

{po.poNumber} — {po.title}

@@ -75,25 +75,28 @@ export default async function ApprovalDetailPage({ params }: Props) {

Vendor required before approval

- This PO has no vendor assigned. Use “Request Vendor ID” to route it for vendor selection, or assign a vendor via the Edit PO form. + This PO has no vendor assigned. Use “Request Vendor ID” to route it for vendor selection, or assign a vendor via the Edit PO form on desktop.

)} - + {/* Direct field editing is desktop-only */} +
+ +
-
+
{hasSignature ? ( ) : ( -
+

Signature required to approve POs

diff --git a/App/pelagia-portal/app/(portal)/approvals/page.tsx b/App/pelagia-portal/app/(portal)/approvals/page.tsx index 41dc9c5..a916619 100644 --- a/App/pelagia-portal/app/(portal)/approvals/page.tsx +++ b/App/pelagia-portal/app/(portal)/approvals/page.tsx @@ -68,42 +68,75 @@ export default async function ApprovalsPage({ searchParams }: Props) {

No purchase orders awaiting approval.

) : ( -
- - - - - - - - - - - - - - {pending.map((po) => ( - - - - - - - - + <> + {/* ── Desktop table ─────────────────────────────────────────── */} +
+
PO NumberTitleSubmitterCost CentreAmountSubmitted
{po.poNumber}{po.title}{po.submitter.name}{po.vessel.name} - {formatCurrency(Number(po.totalAmount), po.currency)} - - {po.submittedAt ? formatDate(po.submittedAt) : "—"} - - - Review → - -
+ + + + + + + + + - ))} - -
PO NumberTitleSubmitterCost CentreAmountSubmitted
-
+ + + {pending.map((po) => ( + + {po.poNumber} + {po.title} + {po.submitter.name} + {po.vessel.name} + + {formatCurrency(Number(po.totalAmount), po.currency)} + + + {po.submittedAt ? formatDate(po.submittedAt) : "—"} + + + + Review → + + + + ))} + + +
+ + {/* ── Mobile cards ──────────────────────────────────────────── */} +
+ {pending.map((po) => ( + +
+
+ {po.poNumber} + + {po.submittedAt ? formatDate(po.submittedAt) : "—"} + +
+

{po.title}

+
+ + {po.submitter.name} · {po.vessel.name} + + + {formatCurrency(Number(po.totalAmount), po.currency)} + +
+
+ + Review → + +
+
+ + ))} +
+ )}
); diff --git a/App/pelagia-portal/app/(portal)/layout.tsx b/App/pelagia-portal/app/(portal)/layout.tsx index bae3fe7..77b6567 100644 --- a/App/pelagia-portal/app/(portal)/layout.tsx +++ b/App/pelagia-portal/app/(portal)/layout.tsx @@ -3,6 +3,12 @@ import { db } from "@/lib/db"; import { redirect } from "next/navigation"; import { Sidebar } from "@/components/layout/sidebar"; import { Header } from "@/components/layout/header"; +import { MobileHeader } from "@/components/layout/mobile-header"; +import { MobileBottomNav } from "@/components/layout/mobile-bottom-nav"; +import { DesktopRequired } from "@/components/layout/desktop-required"; + +// Roles that have a useful mobile experience (approval queue + PO review) +const MOBILE_ROLES = ["MANAGER", "SUPERUSER"] as const; export default async function PortalLayout({ children, @@ -12,6 +18,8 @@ export default async function PortalLayout({ const session = await auth(); if (!session?.user) redirect("/login"); + const hasMobile = (MOBILE_ROLES as readonly string[]).includes(session.user.role); + const [notifications, unreadCount] = await Promise.all([ db.notification.findMany({ where: { userId: session.user.id }, @@ -32,14 +40,38 @@ export default async function PortalLayout({ return (
- + {/* Desktop sidebar — hidden on small screens */} +
+ +
+
-
-
{children}
+ + {/* Desktop top bar — hidden on small screens */} +
+
+
+ + {/* Page content — add bottom padding on mobile to clear the bottom nav */} +
+ {children} +
+ + {/* Mobile bottom nav — managers/superusers only */} + {hasMobile && } + + {/* Full-screen overlay for roles without a mobile experience */} + {!hasMobile && }
); diff --git a/App/pelagia-portal/components/layout/desktop-required.tsx b/App/pelagia-portal/components/layout/desktop-required.tsx new file mode 100644 index 0000000..0aa55ef --- /dev/null +++ b/App/pelagia-portal/components/layout/desktop-required.tsx @@ -0,0 +1,19 @@ +import { Monitor } from "lucide-react"; + +export function DesktopRequired() { + return ( +
+
+ +
+

Desktop Required

+

+ PPMS is designed for use on a desktop or laptop browser. Please switch to a larger screen to + access all features. +

+

+ PMS — it runs the ship, from the right seat. +

+
+ ); +} diff --git a/App/pelagia-portal/components/layout/mobile-bottom-nav.tsx b/App/pelagia-portal/components/layout/mobile-bottom-nav.tsx new file mode 100644 index 0000000..db3db9e --- /dev/null +++ b/App/pelagia-portal/components/layout/mobile-bottom-nav.tsx @@ -0,0 +1,36 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { CheckSquare, UserCircle } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const TABS = [ + { href: "/approvals", label: "Approvals", icon: CheckSquare }, + { href: "/profile", label: "Profile", icon: UserCircle }, +]; + +export function MobileBottomNav() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/App/pelagia-portal/components/layout/mobile-header.tsx b/App/pelagia-portal/components/layout/mobile-header.tsx new file mode 100644 index 0000000..1d8aacf --- /dev/null +++ b/App/pelagia-portal/components/layout/mobile-header.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { signOut } from "next-auth/react"; +import { Anchor, LogOut } from "lucide-react"; +import { NotificationBell } from "./notification-bell"; +import type { Role } from "@prisma/client"; + +interface NotificationItem { + id: string; + body: string; + link: string | null; + isRead: boolean; + sentAt: string; + poId: string | null; +} + +interface MobileHeaderProps { + user: { name: string; role: Role }; + initialUnreadCount: number; + initialNotifications: NotificationItem[]; +} + +export function MobileHeader({ user, initialUnreadCount, initialNotifications }: MobileHeaderProps) { + return ( +
+
+
+ +
+ PPMS +
+
+ + +
+
+ ); +} diff --git a/App/pelagia-portal/components/po/po-detail.tsx b/App/pelagia-portal/components/po/po-detail.tsx index 575380e..436af1b 100644 --- a/App/pelagia-portal/components/po/po-detail.tsx +++ b/App/pelagia-portal/components/po/po-detail.tsx @@ -271,9 +271,9 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals })()} {/* Order Details */} -
+

Order Details

-
+
Cost Centre
{po.vessel?.name ?? "—"}
Account
{po.account.name} ({po.account.code})
Requested By
{po.submitter.name}
@@ -298,9 +298,9 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals {/* Vendor */} {po.vendor ? ( -
+

Vendor

-
+
Name
{po.vendor.name}
Vendor ID
@@ -336,7 +336,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals )} {/* Line Items */} -
+

Line Items