feat(mobile): manager approval queue and PO review on small screens
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 <noreply@anthropic.com>
This commit is contained in:
parent
3646af5c64
commit
cfb16600d7
8 changed files with 232 additions and 62 deletions
|
|
@ -42,7 +42,7 @@ export function ApprovalActions({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-neutral-200 bg-white p-6">
|
<div className="rounded-lg border border-neutral-200 bg-white p-4 md:p-6">
|
||||||
<h3 className="text-base font-semibold text-neutral-900 mb-4">Decision</h3>
|
<h3 className="text-base font-semibold text-neutral-900 mb-4">Decision</h3>
|
||||||
|
|
||||||
{(activeAction === "reject" || activeAction === "request_edits" || activeAction === "approve_note") && (
|
{(activeAction === "reject" || activeAction === "request_edits" || activeAction === "approve_note") && (
|
||||||
|
|
@ -70,14 +70,14 @@ export function ApprovalActions({
|
||||||
<p className="mb-4 text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>
|
<p className="mb-4 text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-col sm:flex-row flex-wrap items-stretch sm:items-center gap-2 sm:gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveAction("reject");
|
setActiveAction("reject");
|
||||||
if (activeAction === "reject") dispatch("reject", true);
|
if (activeAction === "reject") dispatch("reject", true);
|
||||||
}}
|
}}
|
||||||
disabled={!!pending}
|
disabled={!!pending}
|
||||||
className="rounded-lg border border-danger bg-danger-50 px-4 py-2.5 text-sm font-medium text-danger-700 hover:bg-danger-100 disabled:opacity-60 transition-colors"
|
className="w-full sm:w-auto rounded-lg border border-danger bg-danger-50 px-4 py-2.5 text-sm font-medium text-danger-700 hover:bg-danger-100 disabled:opacity-60 transition-colors"
|
||||||
>
|
>
|
||||||
{pending === "reject" ? "Rejecting…" : activeAction === "reject" ? "Confirm Reject" : "Reject"}
|
{pending === "reject" ? "Rejecting…" : activeAction === "reject" ? "Confirm Reject" : "Reject"}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -87,14 +87,14 @@ export function ApprovalActions({
|
||||||
if (activeAction === "request_edits") dispatch("request_edits", true);
|
if (activeAction === "request_edits") dispatch("request_edits", true);
|
||||||
}}
|
}}
|
||||||
disabled={!!pending}
|
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"}
|
{pending === "request_edits" ? "Sending…" : activeAction === "request_edits" ? "Send Edit Request" : "Request Edits"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch("request_vendor_id")}
|
onClick={() => dispatch("request_vendor_id")}
|
||||||
disabled={!!pending}
|
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 === "request_vendor_id" ? "Sending…" : "Request Vendor ID"}
|
{pending === "request_vendor_id" ? "Sending…" : "Request Vendor ID"}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -104,14 +104,14 @@ export function ApprovalActions({
|
||||||
if (activeAction === "approve_note") dispatch("approve_note");
|
if (activeAction === "approve_note") dispatch("approve_note");
|
||||||
}}
|
}}
|
||||||
disabled={!!pending}
|
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"}
|
{pending === "approve_note" ? "Approving…" : activeAction === "approve_note" ? "Confirm Approve with Remarks" : "Approve with Remarks"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch("approve")}
|
onClick={() => dispatch("approve")}
|
||||||
disabled={!!pending}
|
disabled={!!pending}
|
||||||
className="rounded-lg bg-success px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-60 transition-opacity"
|
className="w-full sm:w-auto rounded-lg bg-success px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-60 transition-opacity"
|
||||||
>
|
>
|
||||||
{pending === "approve" ? "Approving…" : "Approve"}
|
{pending === "approve" ? "Approving…" : "Approve"}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,10 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl">
|
<div className="max-w-6xl">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-4 md:mb-6 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold text-neutral-900">Review Purchase Order</h1>
|
<h1 className="text-lg md:text-2xl font-semibold text-neutral-900">Review Purchase Order</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-500">{po.poNumber} — {po.title}</p>
|
<p className="mt-0.5 text-sm text-neutral-500">{po.poNumber} — {po.title}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -75,25 +75,28 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
||||||
<div className="mb-4 rounded-lg border border-danger-100 bg-danger-50 px-4 py-3">
|
<div className="mb-4 rounded-lg border border-danger-100 bg-danger-50 px-4 py-3">
|
||||||
<p className="text-sm font-medium text-danger-700">Vendor required before approval</p>
|
<p className="text-sm font-medium text-danger-700">Vendor required before approval</p>
|
||||||
<p className="text-sm text-danger-600 mt-0.5">
|
<p className="text-sm text-danger-600 mt-0.5">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PoDetail po={po} currentUserId={session.user.id} currentRole={session.user.role} readOnly />
|
<PoDetail po={po} currentUserId={session.user.id} currentRole={session.user.role} readOnly />
|
||||||
|
|
||||||
<ManagerEditPoForm
|
{/* Direct field editing is desktop-only */}
|
||||||
po={serializedPo}
|
<div className="hidden md:block">
|
||||||
vessels={vessels}
|
<ManagerEditPoForm
|
||||||
accounts={accounts}
|
po={serializedPo}
|
||||||
vendors={vendors}
|
vessels={vessels}
|
||||||
/>
|
accounts={accounts}
|
||||||
|
vendors={vendors}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-4 md:mt-6">
|
||||||
{hasSignature ? (
|
{hasSignature ? (
|
||||||
<ApprovalActions poId={po.id} poStatus={po.status} />
|
<ApprovalActions poId={po.id} poStatus={po.status} />
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border border-warning-200 bg-warning-50 p-5 flex items-start gap-3">
|
<div className="rounded-lg border border-warning-200 bg-warning-50 p-4 md:p-5 flex items-start gap-3">
|
||||||
<span className="text-warning-500 text-xl leading-none mt-0.5">✎</span>
|
<span className="text-warning-500 text-xl leading-none mt-0.5">✎</span>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-warning-800">Signature required to approve POs</p>
|
<p className="text-sm font-semibold text-warning-800">Signature required to approve POs</p>
|
||||||
|
|
|
||||||
|
|
@ -68,42 +68,75 @@ export default async function ApprovalsPage({ searchParams }: Props) {
|
||||||
<p className="text-neutral-500">No purchase orders awaiting approval.</p>
|
<p className="text-neutral-500">No purchase orders awaiting approval.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border border-neutral-200 bg-white overflow-hidden">
|
<>
|
||||||
<table className="w-full text-sm">
|
{/* ── Desktop table ─────────────────────────────────────────── */}
|
||||||
<thead className="bg-neutral-50 border-b border-neutral-200">
|
<div className="hidden md:block rounded-lg border border-neutral-200 bg-white overflow-hidden">
|
||||||
<tr>
|
<table className="w-full text-sm">
|
||||||
<th className="px-4 py-3 text-left font-medium text-neutral-600">PO Number</th>
|
<thead className="bg-neutral-50 border-b border-neutral-200">
|
||||||
<th className="px-4 py-3 text-left font-medium text-neutral-600">Title</th>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left font-medium text-neutral-600">Submitter</th>
|
<th className="px-4 py-3 text-left font-medium text-neutral-600">PO Number</th>
|
||||||
<th className="px-4 py-3 text-left font-medium text-neutral-600">Cost Centre</th>
|
<th className="px-4 py-3 text-left font-medium text-neutral-600">Title</th>
|
||||||
<th className="px-4 py-3 text-right font-medium text-neutral-600">Amount</th>
|
<th className="px-4 py-3 text-left font-medium text-neutral-600">Submitter</th>
|
||||||
<th className="px-4 py-3 text-left font-medium text-neutral-600">Submitted</th>
|
<th className="px-4 py-3 text-left font-medium text-neutral-600">Cost Centre</th>
|
||||||
<th className="px-4 py-3"></th>
|
<th className="px-4 py-3 text-right font-medium text-neutral-600">Amount</th>
|
||||||
</tr>
|
<th className="px-4 py-3 text-left font-medium text-neutral-600">Submitted</th>
|
||||||
</thead>
|
<th className="px-4 py-3"></th>
|
||||||
<tbody className="divide-y divide-neutral-100">
|
|
||||||
{pending.map((po) => (
|
|
||||||
<tr key={po.id} className="hover:bg-neutral-50">
|
|
||||||
<td className="px-4 py-3 font-mono text-xs text-neutral-600">{po.poNumber}</td>
|
|
||||||
<td className="px-4 py-3 font-medium text-neutral-900 max-w-xs truncate">{po.title}</td>
|
|
||||||
<td className="px-4 py-3 text-neutral-600">{po.submitter.name}</td>
|
|
||||||
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
|
||||||
<td className="px-4 py-3 text-right font-mono text-sm">
|
|
||||||
{formatCurrency(Number(po.totalAmount), po.currency)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-neutral-500">
|
|
||||||
{po.submittedAt ? formatDate(po.submittedAt) : "—"}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<Link href={`/approvals/${po.id}`} className="text-primary-600 hover:text-primary-700 font-medium">
|
|
||||||
Review →
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody className="divide-y divide-neutral-100">
|
||||||
</table>
|
{pending.map((po) => (
|
||||||
</div>
|
<tr key={po.id} className="hover:bg-neutral-50">
|
||||||
|
<td className="px-4 py-3 font-mono text-xs text-neutral-600">{po.poNumber}</td>
|
||||||
|
<td className="px-4 py-3 font-medium text-neutral-900 max-w-xs truncate">{po.title}</td>
|
||||||
|
<td className="px-4 py-3 text-neutral-600">{po.submitter.name}</td>
|
||||||
|
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
||||||
|
<td className="px-4 py-3 text-right font-mono text-sm">
|
||||||
|
{formatCurrency(Number(po.totalAmount), po.currency)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-neutral-500">
|
||||||
|
{po.submittedAt ? formatDate(po.submittedAt) : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<Link href={`/approvals/${po.id}`} className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Review →
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Mobile cards ──────────────────────────────────────────── */}
|
||||||
|
<div className="md:hidden space-y-3">
|
||||||
|
{pending.map((po) => (
|
||||||
|
<Link key={po.id} href={`/approvals/${po.id}`} className="block">
|
||||||
|
<div className="rounded-xl border border-neutral-200 bg-white p-4 shadow-sm active:bg-neutral-50 transition-colors">
|
||||||
|
<div className="flex items-start justify-between gap-2 mb-1">
|
||||||
|
<span className="font-mono text-xs text-neutral-500">{po.poNumber}</span>
|
||||||
|
<span className="text-xs text-neutral-400 shrink-0">
|
||||||
|
{po.submittedAt ? formatDate(po.submittedAt) : "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-semibold text-neutral-900 leading-snug mb-2">{po.title}</p>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-neutral-500 truncate max-w-[55%]">
|
||||||
|
{po.submitter.name} · {po.vessel.name}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono font-semibold text-neutral-900 shrink-0">
|
||||||
|
{formatCurrency(Number(po.totalAmount), po.currency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<span className="block w-full rounded-lg bg-primary-600 py-2 text-center text-sm font-semibold text-white">
|
||||||
|
Review →
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ import { db } from "@/lib/db";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
import { Header } from "@/components/layout/header";
|
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({
|
export default async function PortalLayout({
|
||||||
children,
|
children,
|
||||||
|
|
@ -12,6 +18,8 @@ export default async function PortalLayout({
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user) redirect("/login");
|
if (!session?.user) redirect("/login");
|
||||||
|
|
||||||
|
const hasMobile = (MOBILE_ROLES as readonly string[]).includes(session.user.role);
|
||||||
|
|
||||||
const [notifications, unreadCount] = await Promise.all([
|
const [notifications, unreadCount] = await Promise.all([
|
||||||
db.notification.findMany({
|
db.notification.findMany({
|
||||||
where: { userId: session.user.id },
|
where: { userId: session.user.id },
|
||||||
|
|
@ -32,14 +40,38 @@ export default async function PortalLayout({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-neutral-50">
|
<div className="flex h-screen overflow-hidden bg-neutral-50">
|
||||||
<Sidebar userRole={session.user.role} />
|
{/* Desktop sidebar — hidden on small screens */}
|
||||||
|
<div className="hidden md:flex">
|
||||||
|
<Sidebar userRole={session.user.role} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
<Header
|
{/* Mobile top bar */}
|
||||||
|
<MobileHeader
|
||||||
user={session.user}
|
user={session.user}
|
||||||
initialUnreadCount={unreadCount}
|
initialUnreadCount={unreadCount}
|
||||||
initialNotifications={serialisedNotifications}
|
initialNotifications={serialisedNotifications}
|
||||||
/>
|
/>
|
||||||
<main className="flex-1 overflow-y-auto p-6">{children}</main>
|
|
||||||
|
{/* Desktop top bar — hidden on small screens */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<Header
|
||||||
|
user={session.user}
|
||||||
|
initialUnreadCount={unreadCount}
|
||||||
|
initialNotifications={serialisedNotifications}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page content — add bottom padding on mobile to clear the bottom nav */}
|
||||||
|
<main className="flex-1 overflow-y-auto p-4 md:p-6 pb-20 md:pb-6">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Mobile bottom nav — managers/superusers only */}
|
||||||
|
{hasMobile && <MobileBottomNav />}
|
||||||
|
|
||||||
|
{/* Full-screen overlay for roles without a mobile experience */}
|
||||||
|
{!hasMobile && <DesktopRequired />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
19
App/pelagia-portal/components/layout/desktop-required.tsx
Normal file
19
App/pelagia-portal/components/layout/desktop-required.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Monitor } from "lucide-react";
|
||||||
|
|
||||||
|
export function DesktopRequired() {
|
||||||
|
return (
|
||||||
|
<div className="md:hidden fixed inset-0 z-40 flex flex-col items-center justify-center bg-white px-8 text-center">
|
||||||
|
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary-50 mb-5">
|
||||||
|
<Monitor className="h-8 w-8 text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold text-neutral-900 mb-2">Desktop Required</h2>
|
||||||
|
<p className="text-sm text-neutral-500 max-w-xs leading-relaxed">
|
||||||
|
PPMS is designed for use on a desktop or laptop browser. Please switch to a larger screen to
|
||||||
|
access all features.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-xs text-neutral-400 italic">
|
||||||
|
PMS — it runs the ship, from the right seat.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
App/pelagia-portal/components/layout/mobile-bottom-nav.tsx
Normal file
36
App/pelagia-portal/components/layout/mobile-bottom-nav.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<nav className="md:hidden flex h-16 shrink-0 items-stretch border-t border-neutral-200 bg-white">
|
||||||
|
{TABS.map(({ href, label, icon: Icon }) => {
|
||||||
|
const active = pathname === href || pathname.startsWith(href + "/");
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={href}
|
||||||
|
href={href}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 flex-col items-center justify-center gap-0.5 text-xs font-medium transition-colors",
|
||||||
|
active ? "text-primary-600" : "text-neutral-500 hover:text-neutral-700"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className={cn("h-5 w-5", active ? "stroke-[2.5]" : "")} />
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
App/pelagia-portal/components/layout/mobile-header.tsx
Normal file
47
App/pelagia-portal/components/layout/mobile-header.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<header className="flex h-14 items-center justify-between border-b border-neutral-200 bg-white px-4 md:hidden">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary-600">
|
||||||
|
<Anchor className="h-3.5 w-3.5 text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-semibold text-neutral-900">PPMS</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<NotificationBell
|
||||||
|
initialUnreadCount={initialUnreadCount}
|
||||||
|
initialNotifications={initialNotifications}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => signOut({ callbackUrl: "/login" })}
|
||||||
|
className="rounded-lg p-2 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 transition-colors"
|
||||||
|
title="Sign out"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -271,9 +271,9 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Order Details */}
|
{/* Order Details */}
|
||||||
<div className="rounded-lg border border-neutral-200 bg-white p-6">
|
<div className="rounded-lg border border-neutral-200 bg-white p-4 md:p-6">
|
||||||
<h3 className="text-sm font-semibold text-neutral-900 mb-4">Order Details</h3>
|
<h3 className="text-sm font-semibold text-neutral-900 mb-4">Order Details</h3>
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||||
<div><dt className="text-neutral-500">Cost Centre</dt><dd className="font-medium text-neutral-900">{po.vessel?.name ?? "—"}</dd></div>
|
<div><dt className="text-neutral-500">Cost Centre</dt><dd className="font-medium text-neutral-900">{po.vessel?.name ?? "—"}</dd></div>
|
||||||
<div><dt className="text-neutral-500">Account</dt><dd className="font-medium text-neutral-900">{po.account.name} ({po.account.code})</dd></div>
|
<div><dt className="text-neutral-500">Account</dt><dd className="font-medium text-neutral-900">{po.account.name} ({po.account.code})</dd></div>
|
||||||
<div><dt className="text-neutral-500">Requested By</dt><dd className="font-medium text-neutral-900">{po.submitter.name}</dd></div>
|
<div><dt className="text-neutral-500">Requested By</dt><dd className="font-medium text-neutral-900">{po.submitter.name}</dd></div>
|
||||||
|
|
@ -298,9 +298,9 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
||||||
|
|
||||||
{/* Vendor */}
|
{/* Vendor */}
|
||||||
{po.vendor ? (
|
{po.vendor ? (
|
||||||
<div className="rounded-lg border border-neutral-200 bg-white p-6">
|
<div className="rounded-lg border border-neutral-200 bg-white p-4 md:p-6">
|
||||||
<h3 className="text-sm font-semibold text-neutral-900 mb-3">Vendor</h3>
|
<h3 className="text-sm font-semibold text-neutral-900 mb-3">Vendor</h3>
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
||||||
<div><dt className="text-neutral-500">Name</dt><dd className="font-medium text-neutral-900">{po.vendor.name}</dd></div>
|
<div><dt className="text-neutral-500">Name</dt><dd className="font-medium text-neutral-900">{po.vendor.name}</dd></div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-neutral-500">Vendor ID</dt>
|
<dt className="text-neutral-500">Vendor ID</dt>
|
||||||
|
|
@ -336,7 +336,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Line Items */}
|
{/* Line Items */}
|
||||||
<div className="rounded-lg border border-neutral-200 bg-white p-6">
|
<div className="rounded-lg border border-neutral-200 bg-white p-3 md:p-6">
|
||||||
<h3 className="text-sm font-semibold text-neutral-900 mb-4">Line Items</h3>
|
<h3 className="text-sm font-semibold text-neutral-900 mb-4">Line Items</h3>
|
||||||
<LineItemsEditor
|
<LineItemsEditor
|
||||||
items={lineItemsForEditor}
|
items={lineItemsForEditor}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue