aboutsummaryrefslogtreecommitdiff
path: root/web/src/components/layout
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
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')
-rw-r--r--web/src/components/layout/AppLayout.module.css194
-rw-r--r--web/src/components/layout/AppLayout.tsx199
2 files changed, 393 insertions, 0 deletions
diff --git a/web/src/components/layout/AppLayout.module.css b/web/src/components/layout/AppLayout.module.css
new file mode 100644
index 0000000..a6d32ce
--- /dev/null
+++ b/web/src/components/layout/AppLayout.module.css
@@ -0,0 +1,194 @@
+.shell {
+ display: grid;
+ grid-template-columns: var(--sidebar-width) 1fr;
+ min-height: 100vh;
+}
+
+/* ── Sidebar ───────────────────────────────────────────────────────────────── */
+.sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: var(--sidebar-width);
+ height: 100vh;
+ background: var(--color-surface);
+ border-right: 1px solid var(--color-border);
+ display: flex;
+ flex-direction: column;
+ z-index: 100;
+ overflow-y: auto;
+}
+
+.sidebarHeader {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-5) var(--space-5);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.sidebarTitle {
+ font-size: var(--text-sm);
+ font-weight: 700;
+ color: var(--color-text-primary);
+ letter-spacing: -0.01em;
+ line-height: 1.2;
+}
+
+.sidebarSubtitle {
+ font-size: var(--text-xs);
+ color: var(--color-text-muted);
+ margin-top: 1px;
+}
+
+/* ── Navegação ─────────────────────────────────────────────────────────────── */
+.nav {
+ flex: 1;
+ padding: var(--space-4) var(--space-3);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+}
+
+.navLabel {
+ font-size: var(--text-xs);
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ padding: 0 var(--space-3);
+ margin-bottom: var(--space-2);
+}
+
+.navLink {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-2) var(--space-3);
+ border-radius: var(--radius-md);
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ transition: all var(--transition-fast);
+ text-decoration: none;
+}
+
+.navLink:hover {
+ background: var(--color-surface-2);
+ color: var(--color-text-primary);
+}
+
+.navLinkActive {
+ background: var(--color-accent-subtle);
+ color: var(--color-accent);
+}
+
+.navLinkActive:hover {
+ background: var(--color-accent-subtle);
+ color: var(--color-accent-hover);
+}
+
+/* ── Footer da sidebar ─────────────────────────────────────────────────────── */
+.sidebarFooter {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-4) var(--space-4);
+ border-top: 1px solid var(--color-border);
+ margin-top: auto;
+}
+
+.userAvatar {
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-full);
+ background: var(--color-accent-subtle);
+ color: var(--color-accent);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.userInfo {
+ min-width: 0;
+}
+
+.userName {
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.userRole {
+ font-size: var(--text-xs);
+ color: var(--color-text-muted);
+}
+
+.logoutBtn {
+ margin-left: auto;
+ flex-shrink: 0;
+ color: var(--color-text-muted);
+ padding: var(--space-1);
+ border-radius: var(--radius-sm);
+ transition: color var(--transition-fast), background var(--transition-fast);
+}
+
+.logoutBtn:hover {
+ color: var(--color-priority-critical);
+ background: rgba(248, 113, 113, 0.1);
+}
+
+
+/* ── Área principal ────────────────────────────────────────────────────────── */
+.main {
+ grid-column: 2;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ── Header ────────────────────────────────────────────────────────────────── */
+.header {
+ position: sticky;
+ top: 0;
+ height: var(--header-height);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 var(--space-8);
+ background: rgba(15, 17, 23, 0.85);
+ backdrop-filter: blur(12px);
+ border-bottom: 1px solid var(--color-border);
+ z-index: 50;
+}
+
+.pageTitle {
+ font-size: var(--text-lg);
+ font-weight: 600;
+ color: var(--color-text-primary);
+ letter-spacing: -0.02em;
+}
+
+.headerActions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+}
+
+/* ── Conteúdo ──────────────────────────────────────────────────────────────── */
+.content {
+ flex: 1;
+ padding: var(--space-8);
+ max-width: var(--content-max-w);
+ width: 100%;
+ animation: fadeIn var(--transition-normal) ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
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>
+ );
+}