aboutsummaryrefslogtreecommitdiff
path: root/web/src/components/layout/AppLayout.tsx
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
commitef55d9bb24430610b60c1b77821307cd72c08c55 (patch)
treee6935a810b34370b0a4c821e55f6397aac50c832 /web/src/components/layout/AppLayout.tsx
downloadhelpdesk-ef55d9bb24430610b60c1b77821307cd72c08c55.tar.gz
helpdesk-ef55d9bb24430610b60c1b77821307cd72c08c55.zip
feat: upload of the projectHEADmaster
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'web/src/components/layout/AppLayout.tsx')
-rw-r--r--web/src/components/layout/AppLayout.tsx199
1 files changed, 199 insertions, 0 deletions
diff --git a/web/src/components/layout/AppLayout.tsx b/web/src/components/layout/AppLayout.tsx
new file mode 100644
index 0000000..78a1795
--- /dev/null
+++ b/web/src/components/layout/AppLayout.tsx
@@ -0,0 +1,199 @@
+/**
+ * AppLayout.tsx
+ *
+ * Shell principal da aplicação: sidebar + header + área de conteúdo.
+ * Todas as páginas são renderizadas dentro deste layout via <Outlet />.
+ */
+
+import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
+import { useAuth } from '@/context/AuthContext';
+import { useTicketEvents } from '@/hooks/useTicketEvents';
+import styles from './AppLayout.module.css';
+
+
+// ─── Ícones inline (SVG) ──────────────────────────────────────────────────────
+const Icons = {
+ Dashboard: () => (
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+ <rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" />
+ <rect x="3" y="14" width="7" height="7" /><rect x="14" y="14" width="7" height="7" />
+ </svg>
+ ),
+ Plus: () => (
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+ <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
+ </svg>
+ ),
+ Ticket: () => (
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+ <path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z" />
+ </svg>
+ ),
+ Logo: () => (
+ <svg width="28" height="28" viewBox="0 0 32 32" fill="none" aria-hidden="true">
+ <rect width="32" height="32" rx="8" fill="var(--color-accent)" fillOpacity="0.2" />
+ <path d="M8 11h16M8 16h10M8 21h12" stroke="var(--color-accent)" strokeWidth="2.2" strokeLinecap="round" />
+ </svg>
+ ),
+ User: () => (
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+ <circle cx="12" cy="8" r="4" /><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
+ </svg>
+ ),
+ Users: () => (
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" />
+ </svg>
+ ),
+ Settings: () => (
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+ <circle cx="12" cy="12" r="3"></circle>
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
+ </svg>
+ ),
+};
+
+// ─── Mapa de títulos por rota ─────────────────────────────────────────────────
+const ROUTE_TITLES: Record<string, string> = {
+ '/dashboard': 'Dashboard',
+ '/tickets/new': 'Abrir Chamado',
+ '/users': 'Usuários',
+};
+
+function getPageTitle(pathname: string): string {
+ if (ROUTE_TITLES[pathname]) return ROUTE_TITLES[pathname];
+ if (pathname.startsWith('/tickets/')) return 'Detalhe do Chamado';
+ return 'Helpdesk TI';
+}
+
+// ─── Componente ───────────────────────────────────────────────────────────────
+export default function AppLayout() {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const { user, logout } = useAuth();
+ const pageTitle = getPageTitle(location.pathname);
+
+ // Inicializa o listener de eventos em tempo real
+ useTicketEvents();
+
+ function handleLogout() {
+ logout();
+ navigate('/login', { replace: true });
+ }
+
+
+ return (
+ <div className={styles.shell}>
+
+ {/* ── Sidebar ─────────────────────────────────────────────────────── */}
+ <aside className={styles.sidebar} aria-label="Navegação principal">
+ {/* Logo */}
+ <div className={styles.sidebarHeader}>
+ <Icons.Logo />
+ <div>
+ <p className={styles.sidebarTitle}>Helpdesk TI</p>
+ <p className={styles.sidebarSubtitle}>Central de Chamados</p>
+ </div>
+ </div>
+
+ {/* Navegação */}
+ <nav className={styles.nav}>
+ <p className={styles.navLabel}>Menu</p>
+
+ <NavLink
+ to="/dashboard"
+ className={({ isActive }) =>
+ `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`
+ }
+ >
+ <Icons.Dashboard />
+ <span>Dashboard</span>
+ </NavLink>
+
+ <NavLink
+ to="/tickets/new"
+ className={({ isActive }) =>
+ `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`
+ }
+ >
+ <Icons.Plus />
+ <span>Abrir Chamado</span>
+ </NavLink>
+
+ <NavLink
+ to="/dashboard?status=open"
+ className={({ isActive }) =>
+ `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`
+ }
+ >
+ <Icons.Ticket />
+ <span>Meus Chamados</span>
+ </NavLink>
+
+ {user?.role === 'admin' && (
+ <>
+ <p className={styles.navLabel}>Administração</p>
+ <NavLink
+ to="/users"
+ className={({ isActive }) => `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`}
+ >
+ <Icons.Users />
+ <span>Usuários</span>
+ </NavLink>
+ <NavLink
+ to="/settings"
+ className={({ isActive }) => `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`}
+ >
+ <Icons.Settings />
+ <span>Configurações</span>
+ </NavLink>
+ </>
+ )}
+ </nav>
+
+ {/* Footer da sidebar */}
+ <div className={styles.sidebarFooter}>
+ <div className={styles.userAvatar}>
+ <Icons.User />
+ </div>
+ <div className={styles.userInfo}>
+ <p className={styles.userName}>{user?.name ?? 'Usuário'}</p>
+ <p className={styles.userRole}>{user?.role ?? 'agent'}</p>
+ </div>
+ <button
+ onClick={handleLogout}
+ className={styles.logoutBtn}
+ title="Sair"
+ aria-label="Sair da conta"
+ >
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
+ <polyline points="16 17 21 12 16 7" />
+ <line x1="21" y1="12" x2="9" y2="12" />
+ </svg>
+ </button>
+ </div>
+ </aside>
+
+ {/* ── Área principal ───────────────────────────────────────────────── */}
+ <div className={styles.main}>
+
+ {/* Header */}
+ <header className={styles.header}>
+ <h1 className={styles.pageTitle}>{pageTitle}</h1>
+ <div className={styles.headerActions}>
+ <NavLink to="/tickets/new" className="btn btn--primary">
+ <Icons.Plus />
+ Novo Chamado
+ </NavLink>
+ </div>
+ </header>
+
+ {/* Conteúdo da página */}
+ <main className={styles.content} id="main-content">
+ <Outlet />
+ </main>
+ </div>
+ </div>
+ );
+}