Files
ComFi/App.tsx
MMrp89 1a57ac7754 feat: Initialize ComFi project with Vite
Setup project structure, dependencies, and basic configuration for the ComFi application. Includes initial setup for Vite, React, TypeScript, Tailwind CSS, and essential development tools. Defines core types and provides a basic README for local development.
2026-02-09 20:28:37 -03:00

217 lines
16 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import {
LayoutDashboard, Users, KanbanSquare, FileText, Briefcase, UserCircle, Calendar,
Bell, Menu, X, ArrowDownCircle, ArrowUpCircle, LogOut, Settings, ChevronDown,
Search, ChevronRight, Building2, ScrollText
} from 'lucide-react';
import { ViewState } from './types';
import { ComFiProvider, useComFi } from './contexts/ComFiContext';
import { ToastProvider } from './contexts/ToastContext';
import { DashboardView } from './components/DashboardView';
import { FinancialReportsView } from './components/FinancialReportsView';
import { CRMView } from './components/CRMView';
import { CalendarView } from './components/CalendarView';
import { KanbanView } from './components/KanbanView';
import { ManagementView } from './components/ManagementView';
import { AccountsPayableView } from './components/AccountsPayableView';
import { AccountsReceivableView } from './components/AccountsReceivableView';
import { UserManagementView } from './components/UserManagementView';
import { SettingsView } from './components/SettingsView';
import { AIChatAssistant } from './components/AIChatAssistant';
import { ProposalsView } from './components/ProposalsView';
// --- COMPONENTS HELPERS ---
interface SidebarItemProps {
icon: any; label: string; active: boolean; onClick: () => void; collapsed?: boolean;
}
const SidebarItem: React.FC<SidebarItemProps> = ({ icon: Icon, label, active, onClick, collapsed = false }) => (
<button onClick={onClick} className={`w-full flex items-center gap-3 px-6 py-3.5 transition-all duration-200 relative group ${active ? 'text-white bg-slate-800 border-l-4 border-orange-500' : 'text-slate-400 hover:text-white hover:bg-slate-800/50 border-l-4 border-transparent'}`}>
<Icon size={20} strokeWidth={active ? 2.5 : 2} className={`${active ? 'text-orange-500' : 'group-hover:text-white'}`} />
{!collapsed && <span className="font-medium">{label}</span>}
</button>
);
const MainContent: React.FC = () => {
const {
currentUser, setCurrentUser, users, setUsers,
companies, setCompanies, services, setServices,
expenses, setExpenses, receivables, setReceivables,
clients, setClients, financialSummary, addReceivable
} = useComFi();
const [currentView, setCurrentView] = useState<ViewState>('dashboard');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<any[]>([]);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const searchRef = useRef<HTMLDivElement>(null);
const userMenuRef = useRef<HTMLDivElement>(null);
const toggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen);
// Search Logic
useEffect(() => {
if (!searchQuery.trim()) { setSearchResults([]); return; }
const query = searchQuery.toLowerCase();
const results: any[] = [];
companies.forEach(c => {
if (c.name.toLowerCase().includes(query) || c.fantasyName?.toLowerCase().includes(query)) results.push({ id: c.id, type: 'client', title: c.fantasyName || c.name, subtitle: c.email, detail: 'Cliente', targetView: 'crm' });
});
services.forEach(s => {
if (s.name.toLowerCase().includes(query)) results.push({ id: s.id, type: 'service', title: s.name, subtitle: `R$ ${s.price}`, detail: s.category, targetView: 'services' });
});
setSearchResults(results);
}, [searchQuery, companies, services]);
// Click Outside Handlers
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) setIsSearchOpen(false);
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) setIsUserMenuOpen(false);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const menuItems: { id: ViewState, label: string, icon: any }[] = [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'crm', label: 'Clientes (CRM)', icon: Users },
{ id: 'proposals', label: 'Propostas', icon: ScrollText },
{ id: 'services', label: 'Serviços', icon: Briefcase },
{ id: 'receivables', label: 'Contas a Receber', icon: ArrowUpCircle },
{ id: 'payables', label: 'Contas a Pagar', icon: ArrowDownCircle },
{ id: 'kanban', label: 'Kanban', icon: KanbanSquare },
{ id: 'invoicing', label: 'Faturamento', icon: FileText },
{ id: 'calendar', label: 'Agenda', icon: Calendar },
];
const allowedMenuItems = menuItems.filter(item => currentUser.role === 'super_admin' || currentUser.permissions.includes(item.id));
const getPageTitle = (view: ViewState) => {
if (view === 'user') return 'Gestão de Usuários';
if (view === 'settings') return 'Configurações';
return menuItems.find(i => i.id === view)?.label || 'Dashboard';
};
return (
<div className="min-h-screen bg-[#F8FAFC] flex font-sans text-slate-600">
<AIChatAssistant userName={currentUser.name.split(' ')[0]} contextData={{ revenue: financialSummary.totalRevenue, expenses: financialSummary.totalExpenses, profit: financialSummary.profit, pendingReceivables: financialSummary.receivablePending }} />
{/* Sidebar */}
<aside className="hidden lg:flex flex-col bg-[#111827] h-screen sticky top-0 w-72 transition-all duration-300 overflow-y-auto shadow-xl border-r border-slate-800 z-20">
<div className="p-8 mb-2 flex items-center gap-3">
<div className="w-8 h-8 bg-orange-500 rounded-lg flex items-center justify-center"><LayoutDashboard className="text-white" size={20} /></div>
<h1 className="font-bold text-2xl text-white tracking-tight">ComFi<span className="text-orange-500">.</span></h1>
</div>
<div className="px-6 mb-6"><p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Principal</p></div>
<nav className="flex-1 pb-8 space-y-1">
{allowedMenuItems.map((item) => (<SidebarItem key={item.id} icon={item.icon} label={item.label} active={currentView === item.id} onClick={() => setCurrentView(item.id)} />))}
<div className="px-6 mt-8 mb-2"><p className="text-xs font-bold text-slate-500 uppercase tracking-wider">Configurações</p></div>
<SidebarItem icon={Settings} label="Sistema" active={currentView === 'settings'} onClick={() => setCurrentView('settings')} />
{currentUser.role === 'super_admin' && <SidebarItem icon={UserCircle} label="Usuários" active={currentView === 'user'} onClick={() => setCurrentView('user')} />}
</nav>
<div className="p-4 border-t border-slate-800">
<div className="bg-slate-800/50 rounded-xl p-3 flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-orange-500 flex items-center justify-center text-white font-bold text-xs">{currentUser.name.substring(0,2)}</div>
<div className="overflow-hidden"><p className="text-sm font-bold text-white truncate">{currentUser.name}</p><p className="text-xs text-slate-400 truncate">{currentUser.email}</p></div>
</div>
</div>
</aside>
{/* Main Area */}
<main className="flex-1 flex flex-col min-w-0">
<header className="sticky top-0 z-30 bg-white/80 backdrop-blur-md px-8 py-4 flex justify-between items-center border-b border-slate-100">
<div className="flex items-center gap-4 flex-1">
<button onClick={toggleMobileMenu} className="lg:hidden p-2 text-slate-500 hover:bg-slate-100 rounded-lg"><Menu size={24} /></button>
<div className="hidden md:flex items-center relative w-full max-w-xl" ref={searchRef}>
<Search className="absolute left-3 text-slate-400 pointer-events-none" size={18} />
<input type="text" value={searchQuery} onChange={(e) => { setSearchQuery(e.target.value); setIsSearchOpen(true); }} onFocus={() => setIsSearchOpen(true)} placeholder="Pesquisar..." className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border-none rounded-xl text-sm focus:ring-2 focus:ring-orange-200 outline-none text-slate-600 transition-all placeholder:text-slate-400" />
{isSearchOpen && searchQuery.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-2 bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-[500px] overflow-y-auto animate-fade-in z-50">
{searchResults.map(r => (
<div key={r.id} onClick={() => {setCurrentView(r.targetView); setIsSearchOpen(false);}} className="px-4 py-3 hover:bg-slate-50 cursor-pointer flex items-center gap-3 border-b border-slate-50 last:border-0">
<div className="w-8 h-8 rounded-lg bg-indigo-50 text-indigo-500 flex items-center justify-center shrink-0"><Building2 size={16} /></div>
<div className="flex-1 min-w-0"><div className="text-sm font-bold text-slate-800 truncate">{r.title}</div><div className="text-xs text-slate-500 truncate">{r.subtitle}</div></div>
</div>
))}
</div>
)}
</div>
</div>
<div className="flex items-center gap-4">
<button className="relative w-10 h-10 rounded-full flex items-center justify-center hover:bg-slate-50 text-slate-400 hover:text-slate-600 transition-colors"><Bell size={20} /><span className="absolute top-2.5 right-2.5 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span></button>
<div className="h-8 w-px bg-slate-200 mx-1"></div>
<div className="flex items-center gap-3 relative cursor-pointer" ref={userMenuRef} onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}>
<div className="text-right hidden md:block"><div className="text-sm font-bold text-slate-800 leading-tight">{currentUser.name}</div><div className="text-[10px] text-slate-400 font-medium capitalize">{currentUser.role === 'super_admin' ? 'Administrator' : 'User'}</div></div>
<div className="w-10 h-10 rounded-full bg-slate-200 overflow-hidden shadow-sm ring-2 ring-white"><img src={currentUser.avatar} alt="User" className="w-full h-full object-cover" /></div>
<ChevronDown size={16} className="text-slate-400 hidden md:block" />
{isUserMenuOpen && (
<div className="absolute right-0 top-full mt-2 w-56 bg-white rounded-2xl shadow-xl border border-slate-100 p-2 z-50 animate-scale-up origin-top-right">
<div className="px-4 py-3 border-b border-slate-50 mb-2"><p className="text-sm font-bold text-slate-800">{currentUser.name}</p><p className="text-xs text-slate-400">{currentUser.email}</p></div>
{users.map(u => (<button key={u.id} onClick={(e) => { e.stopPropagation(); setCurrentUser(u); setIsUserMenuOpen(false); }} className={`w-full text-left px-4 py-2 text-xs rounded-xl flex items-center gap-2 hover:bg-slate-50 ${u.id === currentUser.id ? 'text-orange-600 font-bold bg-orange-50' : 'text-slate-500'}`}><img src={u.avatar} className="w-5 h-5 rounded-full" /> Trocar para {u.name}</button>))}
<div className="border-t border-slate-50 my-2"></div>
<button onClick={() => { setCurrentView('settings'); setIsUserMenuOpen(false); }} className="w-full text-left px-4 py-2.5 text-sm text-slate-600 hover:bg-slate-50 rounded-xl flex items-center gap-2 font-medium"><Settings size={16} /> Configurações</button>
<button className="w-full text-left px-4 py-2.5 text-sm text-red-500 hover:bg-red-50 rounded-xl flex items-center gap-2 font-medium"><LogOut size={16} /> Sair</button>
</div>
)}
</div>
</div>
</header>
<div className="flex-1 px-8 pb-8 pt-6 overflow-y-auto">
<div className="max-w-[1600px] mx-auto">
<div className="mb-8"><h2 className="text-2xl font-bold text-slate-800 tracking-tight">{getPageTitle(currentView)}</h2><p className="text-slate-400 text-sm mt-1">Bem-vindo ao ComFi Dashboard.</p></div>
{(() => {
const hasAccess = currentUser.role === 'super_admin' || currentUser.permissions.includes(currentView) || currentView === 'settings';
if (!hasAccess && currentView !== 'dashboard' && currentView !== 'user') return <div className="flex flex-col items-center justify-center h-[60vh] text-center"><Settings size={32} className="text-slate-300 mb-4"/><h2 className="text-xl font-bold text-slate-800">Acesso Restrito</h2></div>;
switch(currentView) {
case 'dashboard': return <DashboardView financialSummary={financialSummary} companies={companies} expenses={expenses} receivables={receivables} />;
case 'crm': return <CRMView companies={companies} setCompanies={setCompanies} availableServices={services} />;
case 'proposals': return <ProposalsView />;
case 'services': return <ManagementView type="services" servicesData={services} setServicesData={setServices} clientsData={clients} setClientsData={setClients} />;
case 'receivables': return <AccountsReceivableView receivables={receivables} setReceivables={setReceivables} />;
case 'payables': return <AccountsPayableView expenses={expenses} setExpenses={setExpenses} />;
case 'calendar': return <CalendarView expenses={expenses} receivables={receivables} />;
case 'kanban': return <KanbanView companies={companies} onAddReceivable={addReceivable} />;
case 'invoicing': return <FinancialReportsView expenses={expenses} receivables={receivables} />;
case 'user': return currentUser.role === 'super_admin' ? <UserManagementView users={users} setUsers={setUsers} currentUser={currentUser} availableModules={menuItems} /> : null;
case 'settings': return <SettingsView />;
default: return <div className="p-10 text-center text-slate-400">Em desenvolvimento.</div>;
}
})()}
</div>
</div>
</main>
{/* Mobile Menu */}
{isMobileMenuOpen && (
<div className="fixed inset-0 z-50 lg:hidden">
<div className="absolute inset-0 bg-slate-900/50 backdrop-blur-sm" onClick={toggleMobileMenu}></div>
<div className="absolute left-0 top-0 bottom-0 w-72 bg-[#111827] h-screen shadow-2xl p-6 flex flex-col overflow-y-auto">
<div className="flex justify-between items-center mb-8 px-2"><h1 className="font-bold text-2xl text-white">ComFi<span className="text-orange-500">.</span></h1><button onClick={toggleMobileMenu}><X size={24} className="text-slate-500" /></button></div>
<div className="space-y-1">
{allowedMenuItems.map((item) => (<SidebarItem key={item.id} icon={item.icon} label={item.label} active={currentView === item.id} onClick={() => {setCurrentView(item.id); toggleMobileMenu();}} />))}
<SidebarItem icon={Settings} label="Sistema" active={currentView === 'settings'} onClick={() => {setCurrentView('settings'); toggleMobileMenu();}} />
{currentUser.role === 'super_admin' && <SidebarItem icon={UserCircle} label="Usuários" active={currentView === 'user'} onClick={() => {setCurrentView('user'); toggleMobileMenu();}} />}
</div>
</div>
</div>
)}
</div>
);
};
const App: React.FC = () => {
return (
<ComFiProvider>
<ToastProvider>
<MainContent />
</ToastProvider>
</ComFiProvider>
);
};
export default App;