aboutsummaryrefslogtreecommitdiff
path: root/web/src/components/tickets
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/tickets
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/tickets')
-rw-r--r--web/src/components/tickets/PriorityBadge.tsx25
-rw-r--r--web/src/components/tickets/StatusBadge.tsx26
-rw-r--r--web/src/components/tickets/TicketCard.module.css94
-rw-r--r--web/src/components/tickets/TicketCard.tsx66
4 files changed, 211 insertions, 0 deletions
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<TicketPriority, string> = {
+ low: '↓ Baixa',
+ medium: '→ Média',
+ high: '↑ Alta',
+ critical: '⚡ Crítica',
+};
+
+interface PriorityBadgeProps {
+ priority: TicketPriority;
+}
+
+export function PriorityBadge({ priority }: PriorityBadgeProps) {
+ return (
+ <span className={`badge badge--${priority}`}>
+ {PRIORITY_LABELS[priority]}
+ </span>
+ );
+}
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<TicketStatus, string> = {
+ open: 'Aberto',
+ in_progress: 'Em Andamento',
+ waiting: 'Aguardando',
+ resolved: 'Resolvido',
+ closed: 'Fechado',
+};
+
+interface StatusBadgeProps {
+ status: TicketStatus;
+}
+
+export function StatusBadge({ status }: StatusBadgeProps) {
+ return (
+ <span className={`badge badge--${status}`}>
+ {STATUS_LABELS[status]}
+ </span>
+ );
+}
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 (
+ <article
+ className={`${styles.card} ${styles[`priority_${ticket.priority}`]}`}
+ onClick={() => navigate(`/tickets/${ticket.id}`)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => e.key === 'Enter' && navigate(`/tickets/${ticket.id}`)}
+ aria-label={`Ticket #${ticket.id}: ${ticket.title}`}
+ >
+ <div className={styles.top}>
+ <div className={styles.badges}>
+ <StatusBadge status={ticket.status} />
+ <PriorityBadge priority={ticket.priority} />
+ </div>
+ <span className={styles.id}>#{ticket.id}</span>
+ </div>
+
+ <h3 className={styles.title}>{ticket.title}</h3>
+
+ <p className={styles.description}>{ticket.description}</p>
+
+ <div className={styles.footer}>
+ <span className={styles.meta}>
+ Aberto em {formatDate(ticket.createdAt)}
+ </span>
+ {ticket.assigneeId ? (
+ <span className={styles.assigned}>
+ <svg width="12" height="12" 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>
+ Agente #{ticket.assigneeId}
+ </span>
+ ) : (
+ <span className={styles.unassigned}>Sem agente</span>
+ )}
+ </div>
+ </article>
+ );
+}