Auto-deploy: 2026-01-13 12:14:29 | 1 arquivo(s) alterado(s)

This commit is contained in:
daivid.alves 2026-01-13 12:14:30 -03:00
parent 4a33cbafce
commit 096dac3e0d
1 changed files with 189 additions and 99 deletions

View File

@ -1,139 +1,229 @@
import React from 'react'; import React, { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { import {
LayoutDashboard, ChevronLeft,
Car, ChevronRight,
Wrench, Search,
BarChart3, ChevronDown,
ClipboardList, LayoutDashboard,
Activity, Car,
Radio,
AlertTriangle,
Store,
Users, Users,
Wrench,
Activity,
Radio,
AlertTriangle,
Store,
ClipboardList,
LogOut, LogOut,
Menu, ShieldAlert,
ChevronLeft Building2,
Award,
GitBranch,
Lock
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAuthContext } from '@/components/shared/AuthProvider'; import { useAuthContext } from '@/components/shared/AuthProvider';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import './PrafrotSidebar.css'; import './PrafrotSidebar.css';
export const PrafrotSidebar = ({ isCollapsed, onToggle }) => { const MENU_ITEMS = [
const { user, logout } = useAuthContext(); {
const location = useLocation(); id: 'dashboard',
label: 'Estatísticas',
icon: LayoutDashboard,
path: '/plataforma/prafrot/estatisticas'
},
{
id: 'cadastros',
label: 'Cadastros',
icon: ClipboardList,
children: [
{ id: 'c-veiculos', label: 'Veículos', path: '/plataforma/prafrot/veiculos', icon: Car },
{ id: 'c-dispatcher', label: 'Dispatcher', path: '/plataforma/prafrot/dispatcher', icon: ClipboardList },
{ id: 'c-motoristas', label: 'Motoristas', path: '/plataforma/prafrot/motoristas', icon: Users, disabled: true, disabledReason: 'Funcionalidade em manutenção' },
{ id: 'c-oficinas', label: 'Oficinas', path: '/plataforma/prafrot/oficinas', icon: Store }
]
},
{
id: 'gerencia',
label: 'Gerência',
icon: Activity,
children: [
{ id: 'g-monitoramento', label: 'Monitoramento', path: '/plataforma/prafrot/monitoramento', icon: Radio },
{ id: 'g-manutencao', label: 'Manutenção', path: '/plataforma/prafrot/manutencao', icon: Wrench },
{ id: 'g-sinistros', label: 'Sinistros', path: '/plataforma/prafrot/sinistros', icon: AlertTriangle }
]
}
];
const MENU_GROUPS = [ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
{ const [searchTerm, setSearchTerm] = useState('');
label: 'Geral', const [expandedItems, setExpandedItems] = useState({ cadastros: true, gerencia: true });
items: [ const location = useLocation();
{ icon: LayoutDashboard, label: 'Estatísticas', path: '/plataforma/prafrot/estatisticas' }, const { user, logout } = useAuthContext();
]
}, const toggleExpand = (id) => {
{ setExpandedItems(prev => ({
label: 'Cadastros', ...prev,
items: [ [id]: !prev[id]
{ icon: Car, label: 'Veículos', path: '/plataforma/prafrot/veiculos' }, }));
{ icon: ClipboardList, label: 'Dispatcher', path: '/plataforma/prafrot/dispatcher' }, };
{ icon: Users, label: 'Motoristas', path: '/plataforma/prafrot/motoristas', disabled: true },
{ icon: Store, label: 'Oficinas', path: '/plataforma/prafrot/oficinas' }, const filteredItems = useMemo(() => {
] if (!searchTerm) return MENU_ITEMS;
},
{ return MENU_ITEMS.filter(item => {
label: 'Gerência', const matchParent = item.label.toLowerCase().includes(searchTerm.toLowerCase());
items: [ const matchChildren = item.children?.some(child =>
{ icon: Radio, label: 'Monitoramento', path: '/plataforma/prafrot/monitoramento' }, child.label.toLowerCase().includes(searchTerm.toLowerCase())
{ icon: Wrench, label: 'Manutenção', path: '/plataforma/prafrot/manutencao' }, );
{ icon: AlertTriangle, label: 'Sinistros', path: '/plataforma/prafrot/sinistros' }, return matchParent || matchChildren;
] });
} }, [searchTerm]);
];
const handleLogout = () => { const handleLogout = () => {
logout(); logout();
window.location.href = '/plataforma/prafrot/login'; window.location.href = '/plataforma/prafrot/login';
}; };
const NavItem = ({ item }) => { const MenuItem = ({ item, isSub = false }) => {
const isActive = location.pathname.startsWith(item.path);
const Icon = item.icon; const Icon = item.icon;
const hasChildren = item.children && item.children.length > 0;
const isExpanded = expandedItems[item.id];
const isActive = location.pathname.startsWith(item.path) || (hasChildren && item.children.some(c => location.pathname.startsWith(c.path)));
const isLocked = item.disabled;
if (item.disabled) { // Filter sub-items if searching
// Motoristas option is commented out/disabled as requested const subItems = item.children?.filter(child =>
// I'll keep it in the code but won't render or will render disabled !searchTerm || child.label.toLowerCase().includes(searchTerm.toLowerCase())
return ( );
/*
<div className="pfs-item disabled" title="Em breve"> const content = (
<Icon size={20} className="pfs-icon" /> <div
{!isCollapsed && <span className="pfs-label">{item.label}</span>} className={cn(
</div> isSub ? "pfs-sublink" : "pfs-link",
*/ isActive && !hasChildren && "active",
null isLocked && "pfs-locked"
); )}
} onClick={() => {
if (isLocked) return;
if (hasChildren) toggleExpand(item.id);
}}
title={isLocked ? item.disabledReason : (isCollapsed ? item.label : '')}
>
<Icon size={isSub ? 16 : 20} className="pfs-icon" />
{(!isCollapsed || isSub) && <span className="pfs-label">{item.label}</span>}
{hasChildren && !isCollapsed && (
<ChevronDown
size={14}
className={cn("pfs-chevron", isExpanded && "expanded")}
/>
)}
{isLocked && !isCollapsed && (
<Lock size={12} className="pfs-lock-icon" />
)}
</div>
);
return ( return (
<Link <div className="pfs-item-wrapper">
to={item.path} {item.path && !hasChildren && !isLocked ? (
className={cn( <Link to={item.path} style={{ textDecoration: 'none' }}>
"pfs-item", {content}
isActive && "active" </Link>
) : content}
{hasChildren && isExpanded && !isCollapsed && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="pfs-submenu"
>
{subItems.map(child => (
<MenuItem key={child.id} item={child} isSub />
))}
</motion.div>
)} )}
> </div>
<Icon size={20} className="pfs-icon" strokeWidth={isActive ? 2.5 : 2} />
{!isCollapsed && <span className="pfs-label">{item.label}</span>}
</Link>
); );
}; };
return ( return (
<aside className={cn("pfs-container", isCollapsed && "collapsed")}> <aside className={cn("pfs-container", isCollapsed && "collapsed")}>
{/* Header */} <div className="pfs-toggle-container">
<div className="pfs-header"> <button className="pfs-toggle-btn" onClick={onToggle}>
<div className="pfs-logo-box">PF</div> {isCollapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
{!isCollapsed && (
<div className="pfs-brand-info">
<span className="pfs-brand-name">PRA<span>FROTA</span></span>
<span className="pfs-brand-sub">Fleet Management</span>
</div>
)}
<button onClick={onToggle} className="pfs-toggle-btn">
{isCollapsed ? <Menu size={18} /> : <ChevronLeft size={18} />}
</button> </button>
</div> </div>
{/* Navigation */} <div className="pfs-search-wrapper">
<nav className="pfs-nav custom-scrollbar"> <div className="relative">
{MENU_GROUPS.map((group, gIdx) => ( <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500" />
<React.Fragment key={gIdx}> <input
{!isCollapsed && <div className="pfs-group-label">{group.label}</div>} type="text"
{group.items.map((item, iIdx) => ( className="pfs-search-input"
<NavItem key={iIdx} item={item} /> placeholder="Buscar..."
))} value={searchTerm}
</React.Fragment> onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<nav className="pfs-nav-content custom-scrollbar">
{filteredItems.map(item => (
<MenuItem key={item.id} item={item} />
))} ))}
</nav> </nav>
{/* Footer */} <footer className="pfs-footer">
<div className="pfs-footer"> <div className="pfs-brand">
<div className="pfs-user-card"> <div className="pfs-brand-logo">
<Avatar className="h-8 w-8 border border-[#2a2a2a]"> PF
<AvatarImage src={`https://ui-avatars.com/api/?name=${user?.name || 'User'}&background=10b981&color=141414`} /> </div>
<AvatarFallback className="bg-[#10b981] text-[#141414] font-bold text-xs">
{(user?.name || 'U').charAt(0)}
</AvatarFallback>
</Avatar>
{!isCollapsed && ( {!isCollapsed && (
<div className="pfs-user-info"> <div className="pfs-brand-info">
<div className="pfs-user-name truncate">{user?.name || 'Usuário'}</div> <span className="pfs-brand-name">PRA<span>FROTA</span></span>
<button onClick={handleLogout} className="pfs-logout-btn"> <span className="pfs-app-sub">Fleet Management</span>
<LogOut size={12} /> Sair
</button>
</div> </div>
)} )}
</div> </div>
</div>
<div className="pfs-user-section">
<div className={cn("pfs-user-card", isCollapsed && "justify-center")}>
<Avatar className="h-8 w-8 border border-zinc-800">
<AvatarImage src={`https://ui-avatars.com/api/?name=${user?.name || 'User'}&background=10b981&color=141414`} />
<AvatarFallback className="bg-emerald-500 text-zinc-950 font-bold text-xs">
{(user?.name || 'U').charAt(0)}
</AvatarFallback>
</Avatar>
{!isCollapsed && (
<div className="pfs-user-data">
<span className="pfs-user-name">{user?.name || 'Usuário'}</span>
<button onClick={handleLogout} className="pfs-logout-link">
<LogOut size={10} /> Sair
</button>
</div>
)}
</div>
</div>
{!isCollapsed && (
<div className="pfs-legal">
<div className="pfs-legal-item">
<Building2 size={10} />
<span>CNPJ: 31.882.636/0001-38</span>
</div>
<div className="pfs-version">
<GitBranch size={10} />
<span>v1.3.0</span>
</div>
</div>
)}
</footer>
</aside> </aside>
); );
}; };