387 lines
20 KiB
TypeScript
387 lines
20 KiB
TypeScript
import React, { useState } from 'react';
|
|
import NotificationBell from './NotificationBell';
|
|
import { TeamPresence } from './TeamPresence';
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import { Menu, X, FolderOpen, Layers, ClipboardCheck, LogOut, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer, User as UserIcon } from 'lucide-react';
|
|
import { clsx } from 'clsx';
|
|
import { TechnicalManual } from './TechnicalManual';
|
|
import { useAuth } from '../context/useAuth';
|
|
// import { useSystemSettings } from '../context/SystemSettingsContext';
|
|
import { setApiOrganizationId } from '../services/api';
|
|
|
|
interface LayoutProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
const [isManualOpen, setIsManualOpen] = useState(false);
|
|
const [isDarkMode, setIsDarkMode] = useState(() => {
|
|
const saved = localStorage.getItem('theme');
|
|
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
});
|
|
const location = useLocation();
|
|
const { isAdmin, isUser, isDeveloper, appUser, logout } = useAuth();
|
|
// const { settings } = useSystemSettings();
|
|
|
|
// Sync Organization ID with API client
|
|
React.useEffect(() => {
|
|
if (appUser?.organizationId) {
|
|
setApiOrganizationId(appUser.organizationId);
|
|
} else {
|
|
setApiOrganizationId(null);
|
|
}
|
|
}, [appUser?.organizationId]);
|
|
|
|
// Helper to get role display name
|
|
const getRoleDisplay = () => {
|
|
switch (appUser?.role) {
|
|
case 'admin': return { label: 'Admin', color: 'text-amber-500' };
|
|
case 'user': return { label: 'Usuário', color: 'text-green-500' };
|
|
case 'guest': return { label: 'Convidado', color: 'text-blue-400' };
|
|
default: return { label: 'Carregando...', color: 'text-text-muted' };
|
|
}
|
|
};
|
|
const roleInfo = getRoleDisplay();
|
|
|
|
React.useEffect(() => {
|
|
if (isDarkMode) {
|
|
document.documentElement.classList.add('dark');
|
|
localStorage.setItem('theme', 'dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
localStorage.setItem('theme', 'light');
|
|
}
|
|
}, [isDarkMode]);
|
|
|
|
const toggleTheme = () => setIsDarkMode(!isDarkMode);
|
|
const navigate = useNavigate();
|
|
|
|
// Guest Redirection Logic
|
|
React.useEffect(() => {
|
|
if (appUser?.role === 'guest' && (location.pathname === '/' || location.pathname === '/projects')) {
|
|
navigate('/guest-dashboard');
|
|
}
|
|
}, [appUser, location.pathname, navigate]);
|
|
|
|
interface NavItem {
|
|
icon: React.ElementType;
|
|
label: string;
|
|
path: string;
|
|
adminOnly?: boolean;
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{ icon: LayoutDashboard, label: 'Painel Principal', path: '/guest-dashboard' },
|
|
{ icon: FolderOpen, label: 'Obras & Projetos', path: '/' },
|
|
{ icon: Layers, label: 'Esquemas', path: '/schemes' },
|
|
{ icon: ClipboardCheck, label: 'Inspeções', path: '/inspections' },
|
|
{ icon: FolderOpen, label: 'Biblioteca', path: '/library', adminOnly: true },
|
|
{ icon: Thermometer, label: 'Instrumentos', path: '/instruments' },
|
|
{ icon: TrendingUp, label: 'Estudo Rend.', path: '/yield-study', adminOnly: true },
|
|
{ icon: Package, label: 'Estoque', path: '/stock' },
|
|
{ icon: Wrench, label: 'Calculadora', path: '/calculators' },
|
|
];
|
|
|
|
const isActive = (path: string) => {
|
|
if (path === '/' && location.pathname === '/') return true;
|
|
if (path !== '/' && location.pathname.startsWith(path)) return true;
|
|
return false;
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-surface-soft flex font-sans selection:bg-primary/30">
|
|
{/* Sidebar Desktop - Fixed */}
|
|
<aside className="hidden md:flex flex-col w-68 border-r border-border/40 bg-surface fixed h-full z-20 shadow-xl shadow-black/20">
|
|
{/* Logo Area */}
|
|
<div className="flex items-center gap-3 px-2 mb-8">
|
|
<div className="w-10 h-10 flex items-center justify-center shrink-0">
|
|
<img
|
|
src={isDarkMode ? "/steelpaint_icon.png" : "/steelpaint_iconw.png"}
|
|
alt="SteelPaint Logo"
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<h1 className="font-black text-xl tracking-tight text-text-main leading-none">
|
|
SteelPaint
|
|
</h1>
|
|
<p className="text-[10px] font-bold text-primary uppercase tracking-wider mt-1">
|
|
GESTÃO DE PINTURA INDUSTRIAL
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-6 mb-2">
|
|
<div className="w-full flex items-center gap-3 p-2 rounded-xl border border-border/50 bg-surface-hover/50 text-text-main opacity-80 cursor-default" title="Apenas visualização">
|
|
<div className="w-8 h-8 rounded-lg bg-surface-soft flex items-center justify-center font-bold text-xs uppercase">
|
|
{appUser?.organizationId ? 'ORG' : 'GPI'}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-semibold truncate">{appUser?.organizationId || 'Padrão'}</p>
|
|
<p className="text-[10px] text-text-muted uppercase tracking-wider">Organização</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Team Presence - Shows all members with online/offline status */}
|
|
<TeamPresence />
|
|
|
|
<nav className="flex-1 overflow-y-auto py-6 px-4 space-y-1.5">
|
|
<div className="px-4 mb-2">
|
|
<span className="text-[10px] font-bold text-text-muted uppercase tracking-[0.2em]">Menu Principal</span>
|
|
</div>
|
|
{navItems.map((item) => {
|
|
if (item.adminOnly && !isAdmin() && !isUser()) return null;
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
className={clsx(
|
|
"flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all group",
|
|
isActive(item.path)
|
|
? "bg-primary text-white shadow-lg shadow-primary/20"
|
|
: "text-text-secondary hover:bg-surface-hover hover:text-text-main"
|
|
)}
|
|
>
|
|
<item.icon size={18} className={clsx(
|
|
"transition-colors",
|
|
isActive(item.path) ? "text-white" : "text-text-muted group-hover:text-primary"
|
|
)} />
|
|
{item.label}
|
|
</Link>
|
|
)
|
|
})}
|
|
|
|
{/* Admin Menu Item - Only visible for admins */}
|
|
{isAdmin() && (
|
|
<>
|
|
<div className="px-4 mt-6 mb-2">
|
|
<span className="text-[10px] font-bold text-text-muted uppercase tracking-[0.2em]">Sistema</span>
|
|
</div>
|
|
<Link
|
|
to="/admin"
|
|
className={clsx(
|
|
"flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all group",
|
|
isActive('/admin')
|
|
? "bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-lg shadow-amber-500/20"
|
|
: "text-text-secondary hover:bg-surface-hover hover:text-text-main"
|
|
)}
|
|
>
|
|
<Shield size={18} className={clsx(
|
|
"transition-colors",
|
|
isActive('/admin') ? "text-white" : "text-amber-500 group-hover:text-amber-400"
|
|
)} />
|
|
Administração
|
|
</Link>
|
|
</>
|
|
)}
|
|
|
|
{/* Developer Menu Item - ONLY for admtracksteel@gmail.com */}
|
|
{(isDeveloper() || isAdmin() || isUser()) && (
|
|
<>
|
|
{!isAdmin() && (
|
|
<div className="px-4 mt-6 mb-2">
|
|
<span className="text-[10px] font-bold text-text-muted uppercase tracking-[0.2em]">Desenvolvedor</span>
|
|
</div>
|
|
)}
|
|
<Link
|
|
to="/developer"
|
|
className={clsx(
|
|
"flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all group",
|
|
isActive('/developer')
|
|
? "bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg shadow-indigo-500/20"
|
|
: "text-text-secondary hover:bg-surface-hover hover:text-text-main"
|
|
)}
|
|
>
|
|
<Terminal size={18} className={clsx(
|
|
"transition-colors",
|
|
isActive('/developer') ? "text-white" : "text-indigo-500 group-hover:text-indigo-400"
|
|
)} />
|
|
Área Dev
|
|
</Link>
|
|
</>
|
|
)}
|
|
</nav>
|
|
|
|
<div className="p-6 border-t border-border/40 space-y-4">
|
|
<button
|
|
onClick={toggleTheme}
|
|
className="flex items-center gap-3 w-full px-4 py-3 rounded-xl text-sm font-semibold text-text-secondary hover:bg-surface-hover transition-all"
|
|
>
|
|
{isDarkMode ? (
|
|
<>
|
|
<Sun size={18} className="text-yellow-500" />
|
|
<span>Modo Claro</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Moon size={18} className="text-primary" />
|
|
<span>Modo Escuro</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onClick={logout}
|
|
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-text-secondary hover:text-error hover:bg-error/5 transition-all w-full"
|
|
>
|
|
<LogOut size={18} />
|
|
Sair
|
|
</button>
|
|
|
|
<div className="pt-2 flex items-center justify-between px-2">
|
|
<div className="flex items-center gap-2">
|
|
<NotificationBell />
|
|
<div className="w-px h-6 bg-border/50 mx-1"></div>
|
|
<div className="w-8 h-8 rounded-full bg-surface-soft flex items-center justify-center">
|
|
<UserIcon size={16} className="text-text-muted" />
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] text-text-main font-bold truncate max-w-[100px]">{appUser?.name || 'Usuário'}</span>
|
|
<span className="text-[8px] text-text-muted">v3.0.0</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className={`w-1.5 h-1.5 rounded-full ${appUser?.role === 'admin' ? 'bg-amber-500' : appUser?.role === 'user' ? 'bg-green-500' : 'bg-blue-400'}`}></span>
|
|
<span className={`text-[10px] font-medium ${roleInfo.color}`}>{roleInfo.label}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Mobile Header */}
|
|
<header className="md:hidden fixed top-0 left-0 right-0 h-16 bg-surface/80 backdrop-blur-xl border-b border-border/40 z-30 flex items-center justify-between px-6">
|
|
<div className="flex items-center gap-3">
|
|
<img
|
|
src={isDarkMode ? "/steelpaint_icon.png" : "/steelpaint_iconw.png"}
|
|
alt="Logo"
|
|
className="w-8 h-8 object-contain"
|
|
/>
|
|
<span className="font-bold text-lg text-text-main">SteelPaint</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
|
className="p-2 text-text-main hover:bg-surface-hover rounded-lg transition-colors"
|
|
>
|
|
{isSidebarOpen ? <X size={24} /> : <Menu size={24} />}
|
|
</button>
|
|
</header>
|
|
|
|
{/* Mobile Sidebar Overlay */}
|
|
{isSidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black/60 z-40 md:hidden backdrop-blur-sm transition-all animate-in fade-in"
|
|
onClick={() => setIsSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Mobile Sidebar Drawer */}
|
|
<div className={clsx(
|
|
"fixed inset-y-0 left-0 w-72 bg-surface z-50 transform transition-transform duration-300 ease-out md:hidden flex flex-col border-r border-border/40 shadow-2xl",
|
|
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
)}>
|
|
<div className="p-8 border-b border-border/40 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<img
|
|
src={isDarkMode ? "/steelpaint_icon.png" : "/steelpaint_iconw.png"}
|
|
alt="Logo"
|
|
className="w-8 h-8 object-contain"
|
|
/>
|
|
<span className="font-bold text-lg text-text-main">SteelPaint</span>
|
|
</div>
|
|
<button onClick={() => setIsSidebarOpen(false)} className="p-2 text-text-muted hover:text-text-main" aria-label="Close Menu">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
<nav className="flex-1 overflow-y-auto py-8 px-6 space-y-2">
|
|
{navItems.map((item) => {
|
|
if (item.adminOnly && !isAdmin()) return null;
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
onClick={() => setIsSidebarOpen(false)}
|
|
className={clsx(
|
|
"flex items-center gap-4 px-4 py-4 rounded-2xl text-base font-bold transition-all",
|
|
isActive(item.path)
|
|
? "bg-primary text-white shadow-lg shadow-primary/20"
|
|
: "text-text-secondary hover:bg-surface-hover"
|
|
)}
|
|
>
|
|
<item.icon size={22} />
|
|
{item.label}
|
|
</Link>
|
|
)
|
|
})}
|
|
|
|
{/* Admin Menu Item - Mobile */}
|
|
{isAdmin() && (
|
|
<Link
|
|
to="/admin"
|
|
onClick={() => setIsSidebarOpen(false)}
|
|
className={clsx(
|
|
"flex items-center gap-4 px-4 py-4 rounded-2xl text-base font-bold transition-all mt-4",
|
|
isActive('/admin')
|
|
? "bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-lg shadow-amber-500/20"
|
|
: "text-text-secondary hover:bg-surface-hover"
|
|
)}
|
|
>
|
|
<Shield size={22} className="text-amber-500" />
|
|
Administração
|
|
</Link>
|
|
)}
|
|
|
|
{/* Developer Menu Item - Mobile */}
|
|
{(isDeveloper() || isAdmin() || isUser()) && (
|
|
<Link
|
|
to="/developer"
|
|
onClick={() => setIsSidebarOpen(false)}
|
|
className={clsx(
|
|
"flex items-center gap-4 px-4 py-4 rounded-2xl text-base font-bold transition-all mt-4 font-mono",
|
|
isActive('/developer')
|
|
? "bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg shadow-indigo-500/20"
|
|
: "text-text-secondary hover:bg-surface-hover hover:text-indigo-400"
|
|
)}
|
|
>
|
|
<Terminal size={22} className="text-indigo-500" />
|
|
Área Dev
|
|
</Link>
|
|
)}
|
|
</nav>
|
|
<div className="p-8 border-t border-border/40 space-y-4">
|
|
<button onClick={toggleTheme} className="flex items-center gap-4 w-full text-text-main font-bold">
|
|
{isDarkMode ? <Sun size={20} className="text-yellow-500" /> : <Moon size={20} className="text-primary" />}
|
|
{isDarkMode ? 'Modo Claro' : 'Modo Escuro'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content Area */}
|
|
<main className={clsx(
|
|
"flex-1 min-h-screen transition-all duration-300",
|
|
"md:pl-68", // Push content on desktop
|
|
"pt-16 md:pt-0" // Add padding on mobile for header
|
|
)}>
|
|
<div className="max-w-7xl mx-auto px-6 sm:px-10 lg:px-12 py-10 w-full animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
{children}
|
|
</div>
|
|
</main>
|
|
|
|
{/* Floating Help Button */}
|
|
<button
|
|
onClick={() => setIsManualOpen(true)}
|
|
className="fixed bottom-4 right-4 md:top-6 md:right-6 z-30 w-12 h-12 bg-primary hover:bg-primary-dark text-white rounded-xl shadow-lg shadow-primary/30 flex items-center justify-center transition-all hover:scale-105 group"
|
|
title="Manual Técnico"
|
|
aria-label="Abrir Manual Técnico"
|
|
>
|
|
<HelpCircle size={22} className="group-hover:scale-110 transition-transform" />
|
|
</button>
|
|
|
|
{/* Technical Manual Modal */}
|
|
<TechnicalManual isOpen={isManualOpen} onClose={() => setIsManualOpen(false)} />
|
|
</div>
|
|
);
|
|
};
|
|
|