feat(ui,tickets): aplicar visual Rever (badges revertidas), header com play/pause, edição inline com cancelar, empty states e toasts centralizados; novas mutations Convex (updateSubject/updateSummary/toggleWork)

This commit is contained in:
esdrasrenan 2025-10-04 17:13:13 -03:00
parent 881bb7bfdd
commit 6c57c691f3
14 changed files with 512 additions and 307 deletions

View file

@ -1,68 +1,70 @@
import Link from "next/link"
import { formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale"
import { tickets as ticketsMock } from "@/lib/mocks/tickets"
import type { Ticket } from "@/lib/schemas/ticket"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
import { TicketStatusBadge } from "@/components/tickets/status-badge"
const channelLabel: Record<string, string> = {
EMAIL: "E-mail",
WHATSAPP: "WhatsApp",
CHAT: "Chat",
PHONE: "Telefone",
API: "API",
MANUAL: "Manual",
}
const cellClass = "py-4 align-top"
function AssigneeCell({ ticket }: { ticket: Ticket }) {
if (!ticket.assignee) {
return <span className="text-sm text-muted-foreground">Sem responsável</span>
}
const initials = ticket.assignee.name
.split(" ")
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
return (
<div className="flex items-center gap-2">
<Avatar className="size-8">
<AvatarImage src={ticket.assignee.avatarUrl} alt={ticket.assignee.name} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="text-sm font-medium leading-none text-foreground">
{ticket.assignee.name}
</span>
<span className="text-xs text-muted-foreground">
{ticket.assignee.teams?.[0] ?? "Agente"}
</span>
</div>
</div>
)
}
type TicketsTableProps = {
tickets?: Ticket[]
}
export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
import Link from "next/link"
import { formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale"
import { tickets as ticketsMock } from "@/lib/mocks/tickets"
import type { Ticket } from "@/lib/schemas/ticket"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { NewTicketDialog } from "@/components/tickets/new-ticket-dialog"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
import { TicketStatusBadge } from "@/components/tickets/status-badge"
const channelLabel: Record<string, string> = {
EMAIL: "E-mail",
WHATSAPP: "WhatsApp",
CHAT: "Chat",
PHONE: "Telefone",
API: "API",
MANUAL: "Manual",
}
const cellClass = "py-4 align-top"
function AssigneeCell({ ticket }: { ticket: Ticket }) {
if (!ticket.assignee) {
return <span className="text-sm text-muted-foreground">Sem responsável</span>
}
const initials = ticket.assignee.name
.split(" ")
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
return (
<div className="flex items-center gap-2">
<Avatar className="size-8">
<AvatarImage src={ticket.assignee.avatarUrl} alt={ticket.assignee.name} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="text-sm font-medium leading-none text-foreground">
{ticket.assignee.name}
</span>
<span className="text-xs text-muted-foreground">
{ticket.assignee.teams?.[0] ?? "Agente"}
</span>
</div>
</div>
)
}
type TicketsTableProps = {
tickets?: Ticket[]
}
export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
return (
<Card className="border bg-card/90 shadow-sm">
<CardContent className="px-4 py-4 sm:px-6">
@ -159,14 +161,29 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
</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>
<Empty className="my-6">
<EmptyHeader>
<EmptyMedia variant="icon">
<span className="inline-block size-3 rounded-full bg-muted-foreground/40" />
</EmptyMedia>
<EmptyTitle>Nenhum ticket encontrado</EmptyTitle>
<EmptyDescription>
Ajuste os filtros ou crie um novo ticket.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<NewTicketDialog />
</EmptyContent>
</Empty>
)}
</CardContent>
</Card>
)
}