feat: preview de imagens com modal, download com nome correto; cartões (Conversa/Detalhes/Timeline) com sombra e padding; alias '@/convex/_generated/api'; payloads legíveis (nome de fila/responsável, label de status) e timeline amigável; Dropzone no 'Novo ticket' com comentário inicial; microtipografia refinada

This commit is contained in:
esdrasrenan 2025-10-04 01:23:34 -03:00
parent 90c3c8e4d6
commit 44c98fec4a
24 changed files with 1409 additions and 301 deletions

View file

@ -63,110 +63,110 @@ type TicketsTableProps = {
}
export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
return (
<Card className="border-none shadow-none">
<CardContent className="px-4 py-4 sm:px-6">
<Table className="min-w-full">
<TableHeader>
<TableRow className="text-xs uppercase text-muted-foreground">
<TableHead className="w-[110px]">Ticket</TableHead>
<TableHead>Assunto</TableHead>
<TableHead className="hidden lg:table-cell">Fila</TableHead>
<TableHead className="hidden md:table-cell">Canal</TableHead>
<TableHead className="hidden md:table-cell">Prioridade</TableHead>
<TableHead>Status</TableHead>
<TableHead className="hidden xl:table-cell">Responsável</TableHead>
<TableHead className="w-[140px]">Atualizado</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tickets.map((ticket) => (
<TableRow key={ticket.id} className="group">
<TableCell className={cellClass}>
<div className="flex flex-col gap-0.5">
<Link
href={`/tickets/${ticket.id}`}
className="font-semibold tracking-tight text-primary hover:underline"
>
#{ticket.reference}
</Link>
<span className="text-xs text-muted-foreground">
{ticket.queue ?? "Sem fila"}
</span>
</div>
</TableCell>
<TableCell className={cellClass}>
<div className="flex flex-col gap-1">
<Link
href={`/tickets/${ticket.id}`}
className="line-clamp-1 font-medium text-foreground hover:underline"
>
{ticket.subject}
</Link>
<span className="line-clamp-1 text-sm text-muted-foreground">
{ticket.summary ?? "Sem resumo"}
</span>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<span>{ticket.requester.name}</span>
{ticket.tags?.map((tag) => (
<Badge
key={tag}
variant="outline"
className="rounded-full border-transparent bg-slate-100 px-2 py-1 text-xs text-slate-600"
>
{tag}
</Badge>
))}
</div>
</div>
</TableCell>
<TableCell className={`${cellClass} hidden lg:table-cell`}>
<Badge className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600">
{ticket.queue ?? "Sem fila"}
</Badge>
</TableCell>
<TableCell className={`${cellClass} hidden md:table-cell`}>
<Badge className="rounded-full bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-600">
{channelLabel[ticket.channel] ?? ticket.channel}
</Badge>
</TableCell>
<TableCell className={`${cellClass} hidden md:table-cell`}>
<TicketPriorityPill priority={ticket.priority} />
</TableCell>
<TableCell className={cellClass}>
<div className="flex flex-col gap-1">
<TicketStatusBadge status={ticket.status} />
{ticket.metrics?.timeWaitingMinutes ? (
<span className="text-xs text-muted-foreground">
Espera {ticket.metrics.timeWaitingMinutes} min
</span>
) : null}
</div>
</TableCell>
<TableCell className={`${cellClass} hidden xl:table-cell`}>
<AssigneeCell ticket={ticket} />
</TableCell>
<TableCell className={cellClass}>
<span className="text-sm text-muted-foreground">
{formatDistanceToNow(ticket.updatedAt, {
addSuffix: true,
locale: ptBR,
})}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{tickets.length === 0 && (
<div className="flex flex-col items-center justify-center gap-2 py-10 text-center">
<p className="text-sm font-medium">Nenhum ticket encontrado</p>
<p className="text-sm text-muted-foreground">
Ajuste os filtros ou selecione outra fila.
</p>
</div>
)}
</CardContent>
</Card>
)
}
return (
<Card className="border bg-card/90 shadow-sm">
<CardContent className="px-4 py-4 sm:px-6">
<Table className="min-w-full">
<TableHeader>
<TableRow className="text-xs uppercase tracking-wide text-muted-foreground">
<TableHead className="w-[110px]">Ticket</TableHead>
<TableHead>Assunto</TableHead>
<TableHead className="hidden lg:table-cell">Fila</TableHead>
<TableHead className="hidden md:table-cell">Canal</TableHead>
<TableHead className="hidden md:table-cell">Prioridade</TableHead>
<TableHead>Status</TableHead>
<TableHead className="hidden xl:table-cell">Responsável</TableHead>
<TableHead className="w-[140px]">Atualizado</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tickets.map((ticket) => (
<TableRow key={ticket.id} className="group hover:bg-muted/40">
<TableCell className={cellClass}>
<div className="flex flex-col gap-0.5">
<Link
href={`/tickets/${ticket.id}`}
className="font-semibold tracking-tight text-primary hover:underline"
>
#{ticket.reference}
</Link>
<span className="text-xs text-muted-foreground">
{ticket.queue ?? "Sem fila"}
</span>
</div>
</TableCell>
<TableCell className={cellClass}>
<div className="flex flex-col gap-1">
<Link
href={`/tickets/${ticket.id}`}
className="line-clamp-1 font-medium text-foreground hover:underline"
>
{ticket.subject}
</Link>
<span className="line-clamp-1 text-sm text-muted-foreground">
{ticket.summary ?? "Sem resumo"}
</span>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<span>{ticket.requester.name}</span>
{ticket.tags?.map((tag) => (
<Badge
key={tag}
variant="outline"
className="rounded-full border-transparent bg-slate-100 px-2 py-1 text-xs text-slate-600"
>
{tag}
</Badge>
))}
</div>
</div>
</TableCell>
<TableCell className={`${cellClass} hidden lg:table-cell`}>
<Badge className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600">
{ticket.queue ?? "Sem fila"}
</Badge>
</TableCell>
<TableCell className={`${cellClass} hidden md:table-cell`}>
<Badge className="rounded-full bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-600">
{channelLabel[ticket.channel] ?? ticket.channel}
</Badge>
</TableCell>
<TableCell className={`${cellClass} hidden md:table-cell`}>
<TicketPriorityPill priority={ticket.priority} />
</TableCell>
<TableCell className={cellClass}>
<div className="flex flex-col gap-1">
<TicketStatusBadge status={ticket.status} />
{ticket.metrics?.timeWaitingMinutes ? (
<span className="text-xs text-muted-foreground">
Espera {ticket.metrics.timeWaitingMinutes} min
</span>
) : null}
</div>
</TableCell>
<TableCell className={`${cellClass} hidden xl:table-cell`}>
<AssigneeCell ticket={ticket} />
</TableCell>
<TableCell className={cellClass}>
<span className="text-sm text-muted-foreground">
{formatDistanceToNow(ticket.updatedAt, {
addSuffix: true,
locale: ptBR,
})}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{tickets.length === 0 && (
<div className="flex flex-col items-center justify-center gap-2 py-10 text-center text-sm">
<p className="text-sm font-medium">Nenhum ticket encontrado</p>
<p className="text-sm text-muted-foreground">
Ajuste os filtros ou selecione outra fila.
</p>
</div>
)}
</CardContent>
</Card>
)
}