diff options
| author | zwlucas <lucas.fariamo08@gmail.com> | 2026-07-25 21:23:29 +0000 |
|---|---|---|
| committer | zwlucas <lucas.fariamo08@gmail.com> | 2026-07-25 21:23:29 +0000 |
| commit | ef55d9bb24430610b60c1b77821307cd72c08c55 (patch) | |
| tree | e6935a810b34370b0a4c821e55f6397aac50c832 /web/src/pages/DashboardPage.tsx | |
| download | helpdesk-master.tar.gz helpdesk-master.zip | |
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'web/src/pages/DashboardPage.tsx')
| -rw-r--r-- | web/src/pages/DashboardPage.tsx | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..fa7c5b5 --- /dev/null +++ b/web/src/pages/DashboardPage.tsx @@ -0,0 +1,224 @@ +/** + * DashboardPage.tsx + * + * Painel de fila de tickets — visão principal do agente de TI. + * Exibe todos os chamados com filtros por status e prioridade. + */ + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { api, type TicketStatus, type TicketPriority } from '@/lib/api'; +import { TicketCard } from '@/components/tickets/TicketCard'; +import styles from './DashboardPage.module.css'; + +// ─── Opções dos filtros ─────────────────────────────────────────────────────── + +const STATUS_OPTIONS: { value: TicketStatus | ''; label: string }[] = [ + { value: '', label: 'Todos os status' }, + { value: 'open', label: '🔵 Abertos' }, + { value: 'in_progress', label: '🟣 Em Andamento' }, + { value: 'waiting', label: '🟡 Aguardando' }, + { value: 'resolved', label: '🟢 Resolvidos' }, + { value: 'closed', label: '⚫ Fechados' }, +]; + +const PRIORITY_OPTIONS: { value: TicketPriority | ''; label: string }[] = [ + { value: '', label: 'Todas as prioridades' }, + { value: 'critical', label: '⚡ Crítica' }, + { value: 'high', label: '↑ Alta' }, + { value: 'medium', label: '→ Média' }, + { value: 'low', label: '↓ Baixa' }, +]; + +// ─── Skeleton de carregamento ───────────────────────────────────────────────── + +function TicketSkeleton() { + return ( + <div className={styles.skeleton} aria-hidden="true"> + <div className={styles.skeletonLine} style={{ width: '30%', height: 20 }} /> + <div className={styles.skeletonLine} style={{ width: '80%', height: 18 }} /> + <div className={styles.skeletonLine} style={{ width: '60%', height: 14 }} /> + </div> + ); +} + +// ─── Componente principal ───────────────────────────────────────────────────── + +export default function DashboardPage() { + // ── Filtros ──────────────────────────────────────────────────────────────── + const [status, setStatus] = useState<TicketStatus | ''>(''); + const [priority, setPriority] = useState<TicketPriority | ''>(''); + const [page, setPage] = useState(1); + + const LIMIT = 12; + + // ── Query ────────────────────────────────────────────────────────────────── + // A query re-executa automaticamente sempre que os filtros ou a página mudam + const { data, isLoading, isError, error } = useQuery({ + queryKey: ['tickets', { status, priority, page }], + queryFn: () => + api.tickets.list({ + status: status || undefined, + priority: priority || undefined, + page, + limit: LIMIT, + }), + }); + + const { data: stats } = useQuery({ + queryKey: ['stats'], + queryFn: () => api.stats.get(), + }); + + const tickets = data?.data ?? []; + const meta = data?.meta; + const totalPages = meta?.totalPages ?? 1; + + // Reseta para página 1 ao mudar filtros + function handleStatusChange(value: TicketStatus | '') { + setStatus(value); + setPage(1); + } + + function handlePriorityChange(value: TicketPriority | '') { + setPriority(value); + setPage(1); + } + + return ( + <div className={styles.page}> + + {/* ── Cabeçalho + stats ─────────────────────────────────────────────── */} + <div className={styles.pageHeader}> + <div> + <h2 className={styles.heading}>Fila de Chamados</h2> + {meta && ( + <p className={styles.total}> + {meta.total} chamado{meta.total !== 1 ? 's' : ''} encontrado{meta.total !== 1 ? 's' : ''} + </p> + )} + </div> + </div> + + {/* ── KPI Cards ─────────────────────────────────────────────────────── */} + {stats && ( + <div className={styles.kpiGrid}> + <div className={styles.kpiCard}> + <span className={styles.kpiLabel}>Pendentes</span> + <strong className={styles.kpiValue}>{stats.totalPending}</strong> + </div> + <div className={styles.kpiCard}> + <span className={styles.kpiLabel}>Aguardando</span> + <strong className={styles.kpiValue}>{stats.totalWaiting}</strong> + </div> + <div className={styles.kpiCard}> + <span className={styles.kpiLabel}>Resolvidos</span> + <strong className={styles.kpiValue}>{stats.totalResolved}</strong> + </div> + </div> + )} + + {/* ── Filtros ───────────────────────────────────────────────────────── */} + <div className={styles.filters} role="search" aria-label="Filtros de chamados"> + <select + className="input" + value={status} + onChange={(e) => handleStatusChange(e.target.value as TicketStatus | '')} + aria-label="Filtrar por status" + style={{ maxWidth: 220 }} + > + {STATUS_OPTIONS.map((o) => ( + <option key={o.value} value={o.value}>{o.label}</option> + ))} + </select> + + <select + className="input" + value={priority} + onChange={(e) => handlePriorityChange(e.target.value as TicketPriority | '')} + aria-label="Filtrar por prioridade" + style={{ maxWidth: 220 }} + > + {PRIORITY_OPTIONS.map((o) => ( + <option key={o.value} value={o.value}>{o.label}</option> + ))} + </select> + + {(status || priority) && ( + <button + className="btn btn--ghost" + onClick={() => { handleStatusChange(''); handlePriorityChange(''); }} + > + Limpar filtros + </button> + )} + </div> + + {/* ── Estados: loading / erro / vazio / lista ───────────────────────── */} + {isLoading && ( + <div className={styles.grid} aria-busy="true" aria-label="Carregando chamados"> + {Array.from({ length: 6 }).map((_, i) => ( + <TicketSkeleton key={i} /> + ))} + </div> + )} + + {isError && ( + <div className={styles.errorState} role="alert"> + <p className={styles.errorTitle}>Falha ao carregar chamados</p> + <p className={styles.errorMessage}> + {error instanceof Error ? error.message : 'Erro desconhecido'} + </p> + </div> + )} + + {!isLoading && !isError && tickets.length === 0 && ( + <div className={styles.emptyState}> + <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-muted)" strokeWidth="1.5" 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" /> + <path d="M13 5v2M13 17v2M13 11v2" /> + </svg> + <p className={styles.emptyTitle}>Nenhum chamado encontrado</p> + <p className={styles.emptyMessage}> + {status || priority + ? 'Tente ajustar os filtros acima.' + : 'Nenhum chamado aberto no momento.'} + </p> + </div> + )} + + {!isLoading && tickets.length > 0 && ( + <div className={styles.grid}> + {tickets.map((ticket) => ( + <TicketCard key={ticket.id} ticket={ticket} /> + ))} + </div> + )} + + {/* ── Paginação ─────────────────────────────────────────────────────── */} + {totalPages > 1 && ( + <div className={styles.pagination} aria-label="Paginação"> + <button + className="btn btn--ghost" + onClick={() => setPage((p) => Math.max(1, p - 1))} + disabled={page <= 1} + > + ← Anterior + </button> + + <span className={styles.pageInfo}> + Página {page} de {totalPages} + </span> + + <button + className="btn btn--ghost" + onClick={() => setPage((p) => Math.min(totalPages, p + 1))} + disabled={page >= totalPages} + > + Próxima → + </button> + </div> + )} + </div> + ); +} |