krishna_sales/src/screens/ConsoleLayout.tsx
2026-11-06 13:30:59 +05:30

211 lines
9.0 KiB
TypeScript

import { useEffect, useState } from 'react';
import { NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom';
import { LogOut, ChevronDown, Database, Users, KeyRound } from 'lucide-react';
import { cn } from '../lib/cn';
import { useAuth } from '../auth/context';
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
import { APP_VERSION } from '../api/config';
import { NoticeStrips } from '../components/common/NoticeBanner';
import { ChangePasswordModal } from '../components/auth/ChangePasswordModal';
import { REPORT_MAP } from './ReportPage';
import { routeConfig } from '../routesConfig';
import logo from '../assets/logo.png';
const PATH_LABELS: Record<string, string> = {
"/my-orders": "My Orders",
"/my-calls": "My Calls",
"/my-daily": "My Daily Logs",
"/orders": "Orders",
"/calls": "Calls",
"/daily": "Daily Logs",
"/stores": "Stores",
"/sales-report": "DSR",
};
/** Auth-guarded shell: navy top bar + tab nav + routed <Outlet>. */
export function ConsoleLayout() {
const { authed, logout, isAdmin, roles: userRoles, userEmail } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const user = orderBookingClient.currentUser();
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
const isReportsActive = location.pathname.startsWith('/reports');
// 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 />;
const reportRoute = routeConfig.find(r => r.path === '/reports/:reportType');
const canSeeReports = reportRoute && (
(reportRoute.adminOnly && isAdmin) ||
(reportRoute.roles && reportRoute.roles.some(r => userRoles.includes(r))) ||
(!reportRoute.adminOnly && !reportRoute.roles) // open to all
);
return (
<div className="h-screen bg-[var(--secondary-color)] flex flex-col">
<header className="sticky top-0 z-50 shrink-0 flex items-center justify-between gap-6 px-6 h-[56px] shadow-sm" style={{ background: 'var(--nav-bg-color)' }}>
<div className="flex items-center min-w-max w-48 gap-3">
<img src={logo} alt="Logo" className="h-8 w-auto object-contain" />
<span className="text-lg font-bold text-white tracking-wide leading-none">Krishna Sales</span>
</div>
<nav className="flex items-center justify-end gap-2 h-full flex-1 mr-4">
{routeConfig.filter(route => {
if (route.path.includes('/:')) return false; // Skip detail pages
if (!PATH_LABELS[route.path]) return false; // Skip unmapped routes
if (route.adminOnly && !isAdmin) return false;
if (!route.adminOnly && route.roles) {
return route.roles.some(r => userRoles.includes(r));
}
return true;
}).map((route) => {
const key = route.path.replace('/', '');
return (
<NavLink
key={key}
to={route.path}
className={({ isActive }) =>
cn(
'flex items-center gap-1.5 no-underline font-sans text-[13px] font-semibold px-3 py-1.5 rounded-md transition-all duration-150',
isActive ? 'bg-white/20 text-white' : 'text-white/90 hover:bg-white/10 hover:text-white',
)
}
>
{PATH_LABELS[route.path]}
</NavLink>
);
})}
{canSeeReports && (
<div className="relative group flex items-center h-full">
<button className={cn(
"flex items-center gap-1.5 no-underline font-sans text-[13px] font-semibold px-3 py-1.5 rounded-md transition-all duration-150 cursor-pointer",
isReportsActive ? "bg-white/20 text-white" : "text-white/90 hover:bg-white/10 hover:text-white"
)}>
Reports
<ChevronDown size={14} className="ml-0.5 opacity-70" />
</button>
<div className="absolute top-[80%] right-0 mt-1 w-56 bg-white rounded-md shadow-lg py-1 border border-gray-200 hidden group-hover:block z-50">
{Object.entries(REPORT_MAP).map(([key, report]) => (
<NavLink
key={key}
to={`/reports/${key}`}
className={({ isActive }) =>
cn(
"block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100",
isActive && "bg-gray-100 font-semibold"
)
}
>
{report.title}
</NavLink>
))}
</div>
</div>
)}
</nav>
<div className="relative group flex items-center justify-end shrink-0 w-48 h-full py-2">
<button
className="w-8 h-8 rounded-full bg-black/20 text-white flex items-center justify-center text-sm font-bold hover:bg-black/30 transition-colors cursor-pointer"
>
{user?.name ? user.name.charAt(0).toUpperCase() : 'S'}
</button>
<div className="absolute top-[85%] right-0 mt-1 w-60 bg-white rounded-md shadow-xl py-2 border border-gray-200 hidden group-hover:block z-50 text-left">
<div className="px-4 py-3 border-b border-gray-100 mb-1">
<p className="text-sm font-bold text-gray-900 truncate">{user?.name || 'User'}</p>
<p className="text-xs text-gray-500 truncate mt-0.5">{user?.email || 'user@example.com'}</p>
{userRoles && userRoles.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{userRoles.map(role => (
<span key={role} className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium bg-blue-100 text-blue-800">
{role}
</span>
))}
</div>
)}
</div>
{(isAdmin || userRoles?.includes('Admin') || userRoles?.includes('Manager')) && (
<div className="px-2 pb-1 border-b border-gray-100 mb-1 flex flex-col gap-1">
{isAdmin && (
<NavLink
to="/admin/datasets"
className={({ isActive }) => cn("w-full text-left px-3 py-2 text-sm font-medium hover:bg-indigo-50 hover:text-indigo-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer", isActive ? "text-indigo-700 bg-indigo-50" : "text-gray-700")}
>
<Database size={16} />
Manage Datasets
</NavLink>
)}
<NavLink
to="/admin/users"
className={({ isActive }) => cn("w-full text-left px-3 py-2 text-sm font-medium hover:bg-indigo-50 hover:text-indigo-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer", isActive ? "text-indigo-700 bg-indigo-50" : "text-gray-700")}
>
<Users size={16} />
Manage Users
</NavLink>
</div>
)}
<div className="px-2 pb-2 flex flex-col gap-1.5">
<button
type="button"
onClick={() => {
setIsChangePasswordOpen(true);
}}
className="w-full text-left px-3 py-2 text-sm text-gray-700 font-medium hover:bg-emerald-50 hover:text-emerald-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer"
>
<KeyRound size={16} className="text-emerald-600" />
Change Password
</button>
<button
onClick={() => {
logout();
navigate('/login', { replace: true });
}}
className="w-full text-left px-3 py-2 text-sm text-red-600 font-medium hover:bg-red-50 rounded-md transition-colors flex items-center gap-2 cursor-pointer"
>
<LogOut size={16} />
Sign out
</button>
<div className="px-3 pt-2 border-t border-gray-100">
<span className="text-xs font-semibold text-gray-500 tracking-wide">Version {APP_VERSION}</span>
</div>
</div>
</div>
</div>
</header>
<NoticeStrips />
<main className="flex-1 overflow-y-auto p-6">
<div className="max-w-[1240px] w-full mx-auto flex flex-col gap-5">
<Outlet />
</div>
</main>
<ChangePasswordModal
open={isChangePasswordOpen}
onClose={() => setIsChangePasswordOpen(false)}
userEmail={userEmail || user?.email}
userName={user?.name}
onSuccessLogout={() => {
setIsChangePasswordOpen(false);
logout();
navigate('/login', { replace: true });
}}
/>
</div>
);
}