chore: synchronize local fixes to gitea

This commit is contained in:
2026-03-14 00:25:56 +00:00
commit b4ffe72b3e
393 changed files with 71657 additions and 0 deletions

View File

@@ -0,0 +1,418 @@
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 } from 'lucide-react';
import { clsx } from 'clsx';
import { useClerk, UserButton, useUser, OrganizationSwitcher, useOrganization } from '@clerk/clerk-react';
import { TechnicalManual } from './TechnicalManual';
import { useAuth } from '../context/useAuth';
// import { useSystemSettings } from '../context/SystemSettingsContext';
import { setApiOrgData } 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 { signOut } = useClerk();
const { user } = useUser();
const { organization } = useOrganization();
const { isAdmin, isUser, isDeveloper, appUser } = useAuth();
// const { settings } = useSystemSettings();
// Sync Organization ID with API client
React.useEffect(() => {
if (organization?.id) {
setApiOrgData(organization.id, organization.name);
} else {
setApiOrgData(null);
}
}, [organization]);
// 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">
{isAdmin() ? (
<OrganizationSwitcher
hidePersonal={true}
afterSelectOrganizationUrl="/"
afterCreateOrganizationUrl="/"
afterLeaveOrganizationUrl="/"
appearance={{
elements: {
rootBox: "w-full",
organizationSwitcherTrigger: "w-full justify-between bg-surface-hover/50 hover:bg-surface-hover p-2 rounded-xl border border-border/50 text-text-main transition-all",
organizationPreviewTextContainer: "text-text-main",
organizationPreviewMainIdentifier: "text-text-main font-semibold",
organizationSwitcherPopoverCard: "bg-surface border border-border/40 shadow-2xl",
organizationSwitcherPopoverActions: "bg-surface-soft/50",
organizationSwitcherPopoverActionButton: "text-text-main hover:bg-surface-hover transition-colors",
organizationPreview: "hover:bg-surface-hover cursor-pointer transition-colors px-4 py-3",
organizationPreviewSecondaryIdentifier: "text-text-muted",
organizationSwitcherPopoverFooter: "hidden",
userPreviewMainIdentifier: "text-text-main font-bold",
userPreviewSecondaryIdentifier: "text-text-muted",
}
}}
/>
) : (
<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">
{organization?.imageUrl ? (
<img src={organization.imageUrl} alt={organization.name} className="w-8 h-8 rounded-lg object-cover bg-surface-soft" />
) : (
<div className="w-8 h-8 rounded-lg bg-surface-soft flex items-center justify-center font-bold text-xs">
{organization?.name?.substring(0, 2).toUpperCase()}
</div>
)}
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold truncate">{organization?.name || 'Carregando...'}</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={() => signOut()}
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>
<UserButton afterSignOutUrl="/" />
<div className="flex flex-col">
<span className="text-[10px] text-text-main font-bold truncate max-w-[100px]">{user?.firstName || 'Usuário'}</span>
<span className="text-[8px] text-text-muted">v2.1.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>
);
};