From ef55d9bb24430610b60c1b77821307cd72c08c55 Mon Sep 17 00:00:00 2001 From: zwlucas Date: Sat, 25 Jul 2026 18:23:29 -0300 Subject: feat: upload of the project Signed-off-by: zwlucas --- web/src/components/tickets/PriorityBadge.tsx | 25 +++++++ web/src/components/tickets/StatusBadge.tsx | 26 +++++++ web/src/components/tickets/TicketCard.module.css | 94 ++++++++++++++++++++++++ web/src/components/tickets/TicketCard.tsx | 66 +++++++++++++++++ 4 files changed, 211 insertions(+) create mode 100644 web/src/components/tickets/PriorityBadge.tsx create mode 100644 web/src/components/tickets/StatusBadge.tsx create mode 100644 web/src/components/tickets/TicketCard.module.css create mode 100644 web/src/components/tickets/TicketCard.tsx (limited to 'web/src/components/tickets') diff --git a/web/src/components/tickets/PriorityBadge.tsx b/web/src/components/tickets/PriorityBadge.tsx new file mode 100644 index 0000000..acd07a8 --- /dev/null +++ b/web/src/components/tickets/PriorityBadge.tsx @@ -0,0 +1,25 @@ +/** + * PriorityBadge.tsx + * Badge visual para a prioridade do ticket. + */ + +import type { TicketPriority } from '@/lib/api'; + +const PRIORITY_LABELS: Record = { + low: '↓ Baixa', + medium: '→ Média', + high: '↑ Alta', + critical: '⚡ Crítica', +}; + +interface PriorityBadgeProps { + priority: TicketPriority; +} + +export function PriorityBadge({ priority }: PriorityBadgeProps) { + return ( + + {PRIORITY_LABELS[priority]} + + ); +} diff --git a/web/src/components/tickets/StatusBadge.tsx b/web/src/components/tickets/StatusBadge.tsx new file mode 100644 index 0000000..e24876d --- /dev/null +++ b/web/src/components/tickets/StatusBadge.tsx @@ -0,0 +1,26 @@ +/** + * StatusBadge.tsx + * Badge visual para o status do ticket — usa classes do design system global. + */ + +import type { TicketStatus } from '@/lib/api'; + +const STATUS_LABELS: Record = { + open: 'Aberto', + in_progress: 'Em Andamento', + waiting: 'Aguardando', + resolved: 'Resolvido', + closed: 'Fechado', +}; + +interface StatusBadgeProps { + status: TicketStatus; +} + +export function StatusBadge({ status }: StatusBadgeProps) { + return ( + + {STATUS_LABELS[status]} + + ); +} diff --git a/web/src/components/tickets/TicketCard.module.css b/web/src/components/tickets/TicketCard.module.css new file mode 100644 index 0000000..494e3d3 --- /dev/null +++ b/web/src/components/tickets/TicketCard.module.css @@ -0,0 +1,94 @@ +.card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-left-width: 3px; + border-radius: var(--radius-lg); + padding: var(--space-4) var(--space-5); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.card:hover { + border-color: var(--color-border-hover); + border-left-color: inherit; + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +/* Borda esquerda colorida por prioridade */ +.priority_low { border-left-color: var(--color-priority-low); } +.priority_medium { border-left-color: var(--color-priority-medium); } +.priority_high { border-left-color: var(--color-priority-high); } +.priority_critical { border-left-color: var(--color-priority-critical); } + +.top { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); +} + +.badges { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; +} + +.id { + font-size: var(--text-xs); + color: var(--color-text-muted); + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + +.title { + font-size: var(--text-base); + font-weight: 600; + color: var(--color-text-primary); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.description { + font-size: var(--text-sm); + color: var(--color-text-secondary); + line-height: 1.6; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: var(--space-1); + padding-top: var(--space-3); + border-top: 1px solid var(--color-border); +} + +.meta { + font-size: var(--text-xs); + color: var(--color-text-muted); +} + +.assigned { + display: flex; + align-items: center; + gap: 4px; + font-size: var(--text-xs); + color: var(--color-resolved); +} + +.unassigned { + font-size: var(--text-xs); + color: var(--color-text-muted); + font-style: italic; +} diff --git a/web/src/components/tickets/TicketCard.tsx b/web/src/components/tickets/TicketCard.tsx new file mode 100644 index 0000000..3b3926b --- /dev/null +++ b/web/src/components/tickets/TicketCard.tsx @@ -0,0 +1,66 @@ +/** + * TicketCard.tsx + * Card de ticket para uso na lista do dashboard. + */ + +import { useNavigate } from 'react-router-dom'; +import type { Ticket } from '@/lib/api'; +import { StatusBadge } from './StatusBadge'; +import { PriorityBadge } from './PriorityBadge'; +import styles from './TicketCard.module.css'; + +interface TicketCardProps { + ticket: Ticket; +} + +function formatDate(iso: string): string { + return new Intl.DateTimeFormat('pt-BR', { + day: '2-digit', + month: 'short', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(iso)); +} + +export function TicketCard({ ticket }: TicketCardProps) { + const navigate = useNavigate(); + + return ( +
navigate(`/tickets/${ticket.id}`)} + role="button" + tabIndex={0} + onKeyDown={(e) => e.key === 'Enter' && navigate(`/tickets/${ticket.id}`)} + aria-label={`Ticket #${ticket.id}: ${ticket.title}`} + > +
+
+ + +
+ #{ticket.id} +
+ +

{ticket.title}

+ +

{ticket.description}

+ +
+ + Aberto em {formatDate(ticket.createdAt)} + + {ticket.assigneeId ? ( + + + Agente #{ticket.assigneeId} + + ) : ( + Sem agente + )} +
+
+ ); +} -- cgit v1.2.3