krishnasales_mobile/src/screens/ConsoleLayout.tsx
2026-07-30 18:50:59 +05:30

207 lines
8.4 KiB
TypeScript

import { useEffect, useState } from 'react';
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom';
import { LogOut, Menu, X, Database, Users } from 'lucide-react';
import { cn } from '../lib/cn';
import { useAuth } from '../auth/context';
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
import { SCREENS } from './tabs';
import { routeConfig } from '../routesConfig';
import logoUrl from '../assets/logo.png';
import './screen.css';
const canAccessScreen = (key: string, isAdmin: boolean, roles: string[]) => {
const route = routeConfig.find(r => r.path === `/${key}`);
if (!route) return true;
if (route.adminOnly && !isAdmin) return false;
if (!route.adminOnly && route.roles) {
return route.roles.some(r => roles.includes(r));
}
return true;
};
/** Auth-guarded shell: slim navy top bar + fixed bottom tab nav + routed <Outlet>. */
export function ConsoleLayout() {
const { authed, logout, isAdmin, roles: userRoles } = useAuth();
const navigate = useNavigate();
const user = orderBookingClient.currentUser();
const [isMenuOpen, setIsMenuOpen] = useState(false);
// A 401 from any workflow client bounces back to login.
useEffect(() => {
onAuthErrorAll(() => {
logout();
navigate('/login', { replace: true });
});
}, [logout, navigate]);
if (!authed) return <Navigate to="/login" replace />;
return (
<div className="h-[100dvh] overflow-hidden bg-app flex flex-col">
<header className="print:hidden sticky top-0 z-20 shrink-0 flex items-center justify-between gap-3 px-4 h-14 pt-[env(safe-area-inset-top)] top-navbar shadow-md">
<div className="flex items-center gap-3 flex-1">
<button
type="button"
aria-label="Toggle Menu"
onClick={() => setIsMenuOpen(true)}
className="inline-flex items-center justify-center size-9 rounded-md text-white hover:text-white hover:bg-white/10 transition-colors duration-150 cursor-pointer nav-menu-icon"
>
<Menu size={20} />
</button>
<div className="flex flex-col justify-center leading-none">
<span className="text-white font-extrabold text-on-navy tracking-[-0.01em] leading-none nav-title">Krishna Sales</span>
{/* <span className="text-2xs text-on-navy-muted">Sandbox {APP_ID}{user?.name ? ` · ${user.name}` : ''}</span> */}
</div>
</div>
</header>
<main className="flex-1 overflow-y-auto px-4 pt-4 pb-24">
<div className="w-full max-w-[1200px] mx-auto flex flex-col gap-5">
<Outlet />
</div>
</main>
<nav
className="print:hidden fixed bottom-0 inset-x-0 z-20 h-16 pb-[env(safe-area-inset-bottom)] bg-app border-t border-border-subtle grid shadow-[0_-2px_16px_rgba(11,27,59,0.06)]"
style={{ gridTemplateColumns: `repeat(${SCREENS.filter(t => t.key !== 'analytics' && t.key !== 'all-daily' && t.key !== 'sales-report' && canAccessScreen(t.key, isAdmin, userRoles)).length}, minmax(0, 1fr))` }}
>
{SCREENS.filter(t => t.key !== 'analytics' && t.key !== 'all-daily' && t.key !== 'sales-report' && canAccessScreen(t.key, isAdmin, userRoles)).map((t) => {
const Icon = t.icon;
return (
<NavLink
key={t.key}
to={`/${t.key}`}
className="flex flex-col items-center justify-center gap-1 no-underline min-h-[48px]"
>
{({ isActive }) => (
<>
<span
className={cn(
'flex items-center justify-center h-7 w-14 rounded-pill transition-colors duration-150 bottom-nav-icon',
isActive ? 'active' : 'text-faint',
)}
>
<Icon size={20} />
</span>
<span
className={cn(
'text-2xs leading-none transition-colors duration-150 bottom-nav-text',
isActive ? 'font-semibold active' : 'font-medium text-faint',
)}
>
{t.label}
</span>
</>
)}
</NavLink>
);
})}
</nav>
{/* Sidebar Overlay */}
{isMenuOpen && (
<div
className="fixed inset-0 z-40 bg-black/50 transition-opacity"
onClick={() => setIsMenuOpen(false)}
/>
)}
<div
className={cn(
"fixed inset-y-0 left-0 z-50 w-64 bg-app border-r border-border-subtle shadow-xl transform transition-transform duration-300 ease-in-out flex flex-col",
isMenuOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<div className="h-14 flex items-center justify-between px-4 border-b border-border-subtle pt-[env(safe-area-inset-top)] shrink-0">
<div className="flex items-center gap-2.5">
<img src={logoUrl} alt="Krishna" className="w-7 h-7 rounded-full object-cover shadow-sm" />
<span className="font-bold text-foreground tracking-tight">Krishna Field Sales</span>
</div>
<button
type="button"
onClick={() => setIsMenuOpen(false)}
className="inline-flex items-center justify-center size-8 rounded-md text-faint hover:text-foreground hover:bg-black/5 transition-colors cursor-pointer"
>
<X size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto py-2">
{SCREENS.filter(t => canAccessScreen(t.key, isAdmin, userRoles)).map((t) => {
const Icon = t.icon;
return (
<NavLink
key={t.key}
to={`/${t.key}`}
onClick={() => setIsMenuOpen(false)}
className={({ isActive }) => cn(
"flex items-center gap-3 px-4 py-3 mx-2 rounded-lg no-underline transition-colors duration-150 sidebar-item",
isActive ? "active" : ""
)}
>
<Icon size={20} className="shrink-0" />
<span>{t.label}</span>
</NavLink>
);
})}
<div className="my-2 border-t border-border-subtle" />
{isAdmin && (
<>
<NavLink
to="/admin/users"
onClick={() => setIsMenuOpen(false)}
className={({ isActive }) => cn(
"flex items-center gap-3 px-4 py-3 mx-2 rounded-lg no-underline transition-colors duration-150 sidebar-item",
isActive ? "active" : ""
)}
>
<Users size={20} className="shrink-0" />
<span>Sales Officers</span>
</NavLink>
<NavLink
to="/admin/datasets"
onClick={() => setIsMenuOpen(false)}
className={({ isActive }) => cn(
"flex items-center gap-3 px-4 py-3 mx-2 rounded-lg no-underline transition-colors duration-150 sidebar-item",
isActive ? "active" : ""
)}
>
<Database size={20} className="shrink-0" />
<span>Manage Datasets</span>
</NavLink>
</>
)}
</div>
<div className="p-4 border-t border-border-subtle shrink-0 flex flex-col gap-3 mb-[env(safe-area-inset-bottom)]">
{user && (
<div className="flex flex-col px-1">
<span className="text-sm font-medium text-foreground truncate">{user.name || 'User'}</span>
<span className="text-xs text-foreground/70 truncate">{user.email || 'user@email.com'}</span>
{userRoles && userRoles.length > 0 && (
<span className="text-xs font-semibold text-primary mt-0.5 truncate">{userRoles.join(', ')}</span>
)}
</div>
)}
<button
type="button"
onClick={() => {
setIsMenuOpen(false);
logout();
navigate('/login', { replace: true });
}}
className="flex items-center justify-center gap-2 w-full py-2.5 px-4 rounded-lg bg-black/5 hover:bg-black/10 text-foreground transition-colors font-medium text-sm cursor-pointer"
>
<LogOut size={16} />
<span>Sign out</span>
</button>
</div>
</div>
</div>
);
}