feat(mobile): extend mobile experience to Accounts role for payment actions
- Add ACCOUNTS to MOBILE_ROLES so the desktop-required wall no longer blocks them on small screens. - MobileBottomNav is now role-aware: ACCOUNTS gets Payments + Profile tabs; MANAGER/SUPERUSER keep Approvals + Profile. Role prop threaded from layout → MobileBottomNav. - PaymentActions (both MGR_APPROVED and SENT_FOR_PAYMENT states) stacks vertically on small screens — input takes full width, button below it — then reverts to the horizontal inline layout at sm+ breakpoint. - Payments page card bottom row (status badge + action) stacks on mobile (flex-col sm:flex-row) so the reference input isn't squashed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cfb16600d7
commit
f60f249c96
4 changed files with 25 additions and 13 deletions
|
|
@ -7,8 +7,8 @@ import { MobileHeader } from "@/components/layout/mobile-header";
|
||||||
import { MobileBottomNav } from "@/components/layout/mobile-bottom-nav";
|
import { MobileBottomNav } from "@/components/layout/mobile-bottom-nav";
|
||||||
import { DesktopRequired } from "@/components/layout/desktop-required";
|
import { DesktopRequired } from "@/components/layout/desktop-required";
|
||||||
|
|
||||||
// Roles that have a useful mobile experience (approval queue + PO review)
|
// Roles that have a useful mobile experience
|
||||||
const MOBILE_ROLES = ["MANAGER", "SUPERUSER"] as const;
|
const MOBILE_ROLES = ["MANAGER", "SUPERUSER", "ACCOUNTS"] as const;
|
||||||
|
|
||||||
export default async function PortalLayout({
|
export default async function PortalLayout({
|
||||||
children,
|
children,
|
||||||
|
|
@ -67,8 +67,8 @@ export default async function PortalLayout({
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Mobile bottom nav — managers/superusers only */}
|
{/* Mobile bottom nav — roles with a mobile experience */}
|
||||||
{hasMobile && <MobileBottomNav />}
|
{hasMobile && <MobileBottomNav role={session.user.role} />}
|
||||||
|
|
||||||
{/* Full-screen overlay for roles without a mobile experience */}
|
{/* Full-screen overlay for roles without a mobile experience */}
|
||||||
{!hasMobile && <DesktopRequired />}
|
{!hasMobile && <DesktopRequired />}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ export default async function PaymentsPage() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 border-t border-neutral-100 pt-4 flex items-center justify-between gap-4">
|
<div className="mt-4 border-t border-neutral-100 pt-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
<span className={`text-xs font-medium rounded-full px-2.5 py-0.5 ${
|
<span className={`text-xs font-medium rounded-full px-2.5 py-0.5 ${
|
||||||
po.status === "SENT_FOR_PAYMENT"
|
po.status === "SENT_FOR_PAYMENT"
|
||||||
? "bg-primary-50 text-primary-700"
|
? "bg-primary-50 text-primary-700"
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,12 @@ export function PaymentActions({ poId, poStatus }: { poId: string; poStatus: POS
|
||||||
|
|
||||||
if (poStatus === "MGR_APPROVED") {
|
if (poStatus === "MGR_APPROVED") {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||||
{error && <span className="text-xs text-danger-700">{error}</span>}
|
{error && <span className="text-xs text-danger-700">{error}</span>}
|
||||||
<button
|
<button
|
||||||
onClick={handleProcessPayment}
|
onClick={handleProcessPayment}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors whitespace-nowrap"
|
className="w-full sm:w-auto rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors"
|
||||||
>
|
>
|
||||||
{pending ? "Processing…" : "Start Payment Processing"}
|
{pending ? "Processing…" : "Start Payment Processing"}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -46,7 +46,7 @@ export function PaymentActions({ poId, poStatus }: { poId: string; poStatus: POS
|
||||||
|
|
||||||
if (poStatus === "SENT_FOR_PAYMENT") {
|
if (poStatus === "SENT_FOR_PAYMENT") {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleMarkPaid} className="flex items-center gap-3">
|
<form onSubmit={handleMarkPaid} className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 w-full">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Payment reference / transaction ID"
|
placeholder="Payment reference / transaction ID"
|
||||||
|
|
@ -58,7 +58,7 @@ export function PaymentActions({ poId, poStatus }: { poId: string; poStatus: POS
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
className="rounded-lg bg-success px-4 py-2 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-60 transition-opacity whitespace-nowrap"
|
className="w-full sm:w-auto rounded-lg bg-success px-4 py-2 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-60 transition-opacity"
|
||||||
>
|
>
|
||||||
{pending ? "Confirming…" : "Confirm Payment Sent"}
|
{pending ? "Confirming…" : "Confirm Payment Sent"}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,32 @@
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { CheckSquare, UserCircle } from "lucide-react";
|
import { CheckSquare, CreditCard, UserCircle } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { Role } from "@prisma/client";
|
||||||
|
|
||||||
const TABS = [
|
const MANAGER_TABS = [
|
||||||
{ href: "/approvals", label: "Approvals", icon: CheckSquare },
|
{ href: "/approvals", label: "Approvals", icon: CheckSquare },
|
||||||
{ href: "/profile", label: "Profile", icon: UserCircle },
|
{ href: "/profile", label: "Profile", icon: UserCircle },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function MobileBottomNav() {
|
const ACCOUNTS_TABS = [
|
||||||
|
{ href: "/payments", label: "Payments", icon: CreditCard },
|
||||||
|
{ href: "/profile", label: "Profile", icon: UserCircle },
|
||||||
|
];
|
||||||
|
|
||||||
|
function tabsForRole(role: Role) {
|
||||||
|
if (role === "ACCOUNTS") return ACCOUNTS_TABS;
|
||||||
|
return MANAGER_TABS; // MANAGER, SUPERUSER
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MobileBottomNav({ role }: { role: Role }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const tabs = tabsForRole(role);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="md:hidden flex h-16 shrink-0 items-stretch border-t border-neutral-200 bg-white">
|
<nav className="md:hidden flex h-16 shrink-0 items-stretch border-t border-neutral-200 bg-white">
|
||||||
{TABS.map(({ href, label, icon: Icon }) => {
|
{tabs.map(({ href, label, icon: Icon }) => {
|
||||||
const active = pathname === href || pathname.startsWith(href + "/");
|
const active = pathname === href || pathname.startsWith(href + "/");
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue