diff --git a/web/src/components/tickets/play-next-ticket-card.tsx b/web/src/components/tickets/play-next-ticket-card.tsx index 944a5ff..693953f 100644 --- a/web/src/components/tickets/play-next-ticket-card.tsx +++ b/web/src/components/tickets/play-next-ticket-card.tsx @@ -5,7 +5,6 @@ import { useState } from "react" import { useRouter } from "next/navigation" import { IconArrowRight, IconPlayerPlayFilled } from "@tabler/icons-react" import { useMutation, useQuery } from "convex/react" -// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" @@ -21,11 +20,15 @@ import { TicketPriorityPill } from "@/components/tickets/priority-pill" import { TicketStatusBadge } from "@/components/tickets/status-badge" import { Spinner } from "@/components/ui/spinner" import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select" - -interface PlayNextTicketCardProps { - context?: TicketPlayContext -} - + +interface PlayNextTicketCardProps { + context?: TicketPlayContext +} + +const queueBadgeClass = "inline-flex items-center rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-semibold text-neutral-700" +const startButtonClass = "inline-flex items-center gap-2 rounded-lg border border-black bg-[#00e8ff] px-3 py-2 text-sm font-semibold text-black transition hover:bg-[#00d6eb]" +const secondaryButtonClass = "inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-neutral-700 hover:bg-slate-100" + export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) { const router = useRouter() const { userId } = useAuth() @@ -43,85 +46,110 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) { })?.[0] const nextTicketUi = nextTicketFromServer ? mapTicketFromServer(nextTicketFromServer as unknown) : null - const cardContext: TicketPlayContext | null = context ?? (nextTicketUi ? { queue: { id: "default", name: "Geral", pending: queueSummary.reduce((a, b) => a + b.pending, 0), waiting: queueSummary.reduce((a, b) => a + b.waiting, 0), breached: 0 }, nextTicket: nextTicketUi } : null) + const cardContext: TicketPlayContext | null = + context ?? + (nextTicketUi + ? { + queue: { + id: "default", + name: "Geral", + pending: queueSummary.reduce((acc, item) => acc + item.pending, 0), + waiting: queueSummary.reduce((acc, item) => acc + item.waiting, 0), + breached: 0, + }, + nextTicket: nextTicketUi, + } + : null) if (!cardContext || !cardContext.nextTicket) { return ( - + - Fila sem tickets pendentes + Fila sem tickets pendentes - - Nenhum ticket disponivel no momento. Excelente trabalho! + + Nenhum ticket disponível no momento. Excelente trabalho! ) } const ticket = cardContext.nextTicket - + return ( - - - - Proximo ticket • #{ticket.reference} - - - - + + + + Próximo ticket • #{ticket.reference} + + + +
- Fila: - setSelectedQueueId(value === "ALL" ? undefined : value)}> + + + + Todas - {queueSummary.map((q) => ( - {q.name} + {queueSummary.map((queue) => ( + + {queue.name} + ))}
-

{ticket.subject}

-

{ticket.summary}

+

{ticket.subject}

+

{ticket.summary}

-
- {ticket.queue ?? "Sem fila"} - - Solicitante: {ticket.requester.name} -
- -
+
+ {ticket.queue ?? "Sem fila"} + + Solicitante: {ticket.requester.name} +
+ +
Pendentes na fila - {cardContext.queue.pending} + {cardContext.queue.pending}
Em espera - {cardContext.queue.waiting} + {cardContext.queue.waiting}
SLA violado - {cardContext.queue.breached} + {cardContext.queue.breached}
- - - - ) + + + + ) } diff --git a/web/src/components/tickets/priority-pill.tsx b/web/src/components/tickets/priority-pill.tsx index 2985e92..376965f 100644 --- a/web/src/components/tickets/priority-pill.tsx +++ b/web/src/components/tickets/priority-pill.tsx @@ -1,40 +1,21 @@ -import { cn } from "@/lib/utils" +import { type TicketPriority } from "@/lib/schemas/ticket" import { Badge } from "@/components/ui/badge" +import { cn } from "@/lib/utils" -const priorityConfig = { - LOW: { - label: "Baixa", - className: "bg-slate-100 text-slate-600 border-transparent", - }, - MEDIUM: { - label: "Media", - className: "bg-blue-100 text-blue-600 border-transparent", - }, - HIGH: { - label: "Alta", - className: "bg-amber-100 text-amber-700 border-transparent", - }, - URGENT: { - label: "Urgente", - className: "bg-red-100 text-red-700 border-transparent", - }, -} satisfies Record - -type TicketPriorityPillProps = { - priority: keyof typeof priorityConfig -} - -export function TicketPriorityPill({ priority }: TicketPriorityPillProps) { - const config = priorityConfig[priority] +const priorityStyles: Record = { + LOW: { label: "Baixa", className: "border border-slate-200 bg-slate-100 text-slate-700" }, + MEDIUM: { label: "Média", className: "border border-slate-200 bg-[#dff1fb] text-[#0a4760]" }, + HIGH: { label: "Alta", className: "border border-slate-200 bg-[#fde8d1] text-[#7d3b05]" }, + URGENT: { label: "Urgente", className: "border border-slate-200 bg-[#fbd9dd] text-[#8b0f1c]" }, +} + +const baseClass = "inline-flex items-center rounded-full px-3 py-0.5 text-xs font-semibold" + +export function TicketPriorityPill({ priority }: { priority: TicketPriority }) { + const styles = priorityStyles[priority] return ( - - {config?.label ?? priority} + + {styles?.label ?? priority} ) } diff --git a/web/src/components/tickets/priority-select.tsx b/web/src/components/tickets/priority-select.tsx index cb0872c..d3e82dd 100644 --- a/web/src/components/tickets/priority-select.tsx +++ b/web/src/components/tickets/priority-select.tsx @@ -11,72 +11,68 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Badge } from "@/components/ui/badge" import { toast } from "sonner" import { ArrowDown, ArrowRight, ArrowUp, ChevronsUp } from "lucide-react" +import { cn } from "@/lib/utils" -const labels: Record = { - LOW: "Baixa", - MEDIUM: "Média", - HIGH: "Alta", - URGENT: "Urgente", +const priorityStyles: Record = { + LOW: { label: "Baixa", badgeClass: "border border-slate-200 bg-slate-100 text-slate-700" }, + MEDIUM: { label: "Média", badgeClass: "border border-slate-200 bg-[#dff1fb] text-[#0a4760]" }, + HIGH: { label: "Alta", badgeClass: "border border-slate-200 bg-[#fde8d1] text-[#7d3b05]" }, + URGENT: { label: "Urgente", badgeClass: "border border-slate-200 bg-[#fbd9dd] text-[#8b0f1c]" }, } -function badgeClass(p: TicketPriority) { - switch (p) { - case "URGENT": - return "bg-red-100 text-red-700" - case "HIGH": - return "bg-amber-100 text-amber-700" - case "MEDIUM": - return "bg-blue-100 text-blue-700" - default: - return "bg-slate-100 text-slate-700" - } -} +const triggerClass = "h-8 w-[160px] rounded-full border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" +const itemClass = "flex items-center gap-2 rounded-md px-2 py-2 text-sm text-neutral-800 transition data-[state=checked]:bg-[#00e8ff]/15 data-[state=checked]:text-neutral-900 focus:bg-[#00e8ff]/10" +const iconClass = "size-4 text-neutral-700" +const baseBadgeClass = "inline-flex items-center gap-2 rounded-full px-3 py-0.5 text-xs font-semibold" -function PriorityIcon({ p }: { p: TicketPriority }) { - const cls = "size-3.5 text-cyan-600" - if (p === "LOW") return - if (p === "MEDIUM") return - if (p === "HIGH") return - return +function PriorityIcon({ value }: { value: TicketPriority }) { + if (value === "LOW") return + if (value === "MEDIUM") return + if (value === "HIGH") return + return } export function PrioritySelect({ ticketId, value }: { ticketId: string; value: TicketPriority }) { const updatePriority = useMutation(api.tickets.updatePriority) const [priority, setPriority] = useState(value) const { userId } = useAuth() + return ( ) } - diff --git a/web/src/components/tickets/recent-tickets-panel.tsx b/web/src/components/tickets/recent-tickets-panel.tsx index aa124e6..207da13 100644 --- a/web/src/components/tickets/recent-tickets-panel.tsx +++ b/web/src/components/tickets/recent-tickets-panel.tsx @@ -6,19 +6,17 @@ import { api } from "@/convex/_generated/api"; import { DEFAULT_TENANT_ID } from "@/lib/constants"; import { mapTicketsFromServerList } from "@/lib/mappers/ticket"; import { TicketsTable } from "@/components/tickets/tickets-table"; -import { Spinner } from "@/components/ui/spinner"; -import type { Ticket } from "@/lib/schemas/ticket"; export function RecentTicketsPanel() { const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 10 }); if (ticketsRaw === undefined) { return ( -
+
{Array.from({ length: 4 }).map((_, i) => (
-
-
+
+
))}
@@ -27,7 +25,7 @@ export function RecentTicketsPanel() { } const tickets = mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]); return ( -
+
); diff --git a/web/src/components/tickets/status-badge.tsx b/web/src/components/tickets/status-badge.tsx index 7c4223a..814abf2 100644 --- a/web/src/components/tickets/status-badge.tsx +++ b/web/src/components/tickets/status-badge.tsx @@ -1,27 +1,26 @@ "use client" import { ticketStatusSchema, type TicketStatus } from "@/lib/schemas/ticket" -import { Badge } from "@/components/ui/badge" - -const statusConfig = { - NEW: { label: "Novo", className: "bg-slate-100 text-slate-700 border-transparent" }, - OPEN: { label: "Aberto", className: "bg-blue-100 text-blue-700 border-transparent" }, - PENDING: { label: "Pendente", className: "bg-amber-100 text-amber-700 border-transparent" }, - ON_HOLD: { label: "Em espera", className: "bg-purple-100 text-purple-700 border-transparent" }, - RESOLVED: { label: "Resolvido", className: "bg-emerald-100 text-emerald-700 border-transparent" }, - CLOSED: { label: "Fechado", className: "bg-slate-100 text-slate-700 border-transparent" }, -} satisfies Record - +import { Badge } from "@/components/ui/badge" +import { cn } from "@/lib/utils" + +const statusStyles: Record = { + NEW: { label: "Novo", className: "border border-slate-200 bg-slate-100 text-slate-700" }, + OPEN: { label: "Aberto", className: "border border-slate-200 bg-[#dff1fb] text-[#0a4760]" }, + PENDING: { label: "Pendente", className: "border border-slate-200 bg-[#fdebd6] text-[#7b4107]" }, + ON_HOLD: { label: "Em espera", className: "border border-slate-200 bg-[#ede8ff] text-[#4f2f96]" }, + RESOLVED: { label: "Resolvido", className: "border border-slate-200 bg-[#dcf4eb] text-[#1f6a45]" }, + CLOSED: { label: "Fechado", className: "border border-slate-200 bg-slate-200 text-slate-700" }, +} + type TicketStatusBadgeProps = { status: TicketStatus } - -export function TicketStatusBadge({ status }: TicketStatusBadgeProps) { - const config = statusConfig[status] - return ( - - {config?.label ?? status} + +export function TicketStatusBadge({ status }: TicketStatusBadgeProps) { + const parsed = ticketStatusSchema.parse(status) + const styles = statusStyles[parsed] + return ( + + {styles?.label ?? parsed} - ) -} + ) +} diff --git a/web/src/components/tickets/status-select.tsx b/web/src/components/tickets/status-select.tsx index f227972..518ff24 100644 --- a/web/src/components/tickets/status-select.tsx +++ b/web/src/components/tickets/status-select.tsx @@ -10,63 +10,56 @@ import { useAuth } from "@/lib/auth-client" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Badge } from "@/components/ui/badge" import { toast } from "sonner" +import { cn } from "@/lib/utils" -const labels: Record = { - NEW: "Novo", - OPEN: "Aberto", - PENDING: "Pendente", - ON_HOLD: "Em espera", - RESOLVED: "Resolvido", - CLOSED: "Fechado", +const statusStyles: Record = { + NEW: { label: "Novo", badgeClass: "border border-slate-200 bg-slate-100 text-slate-700" }, + OPEN: { label: "Aberto", badgeClass: "border border-slate-200 bg-[#dff1fb] text-[#0a4760]" }, + PENDING: { label: "Pendente", badgeClass: "border border-slate-200 bg-[#fdebd6] text-[#7b4107]" }, + ON_HOLD: { label: "Em espera", badgeClass: "border border-slate-200 bg-[#ede8ff] text-[#4f2f96]" }, + RESOLVED: { label: "Resolvido", badgeClass: "border border-slate-200 bg-[#dcf4eb] text-[#1f6a45]" }, + CLOSED: { label: "Fechado", badgeClass: "border border-slate-200 bg-slate-200 text-slate-700" }, } -function badgeClass(s: TicketStatus) { - switch (s) { - case "OPEN": - return "bg-blue-100 text-blue-700" - case "PENDING": - return "bg-amber-100 text-amber-700" - case "ON_HOLD": - return "bg-purple-100 text-purple-700" - case "RESOLVED": - return "bg-emerald-100 text-emerald-700" - case "CLOSED": - return "bg-slate-100 text-slate-700" - default: - return "bg-slate-100 text-slate-700" - } -} +const triggerClass = "h-8 w-[180px] rounded-full border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" +const itemClass = "rounded-md px-2 py-2 text-sm text-neutral-800 transition hover:bg-slate-100 data-[state=checked]:bg-[#00e8ff]/15 data-[state=checked]:text-neutral-900 focus:bg-[#00e8ff]/10" +const baseBadgeClass = "inline-flex items-center rounded-full px-3 py-0.5 text-xs font-semibold" export function StatusSelect({ ticketId, value }: { ticketId: string; value: TicketStatus }) { const updateStatus = useMutation(api.tickets.updateStatus) const [status, setStatus] = useState(value) const { userId } = useAuth() + return ( diff --git a/web/src/components/tickets/ticket-comments.rich.tsx b/web/src/components/tickets/ticket-comments.rich.tsx index 7b86312..97ad8c9 100644 --- a/web/src/components/tickets/ticket-comments.rich.tsx +++ b/web/src/components/tickets/ticket-comments.rich.tsx @@ -5,8 +5,7 @@ import { formatDistanceToNow } from "date-fns" import { ptBR } from "date-fns/locale" import { IconLock, IconMessage } from "@tabler/icons-react" import { Download, FileIcon } from "lucide-react" -import { useAction, useMutation } from "convex/react" -// eslint-disable-next-line @typescript-eslint/ban-ts-comment +import { useMutation } from "convex/react" // @ts-ignore import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" @@ -27,59 +26,74 @@ interface TicketCommentsProps { ticket: TicketWithDetails } +const badgeInternal = "gap-1 rounded-full border border-slate-300 bg-neutral-900 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-white" +const selectTriggerClass = "h-8 w-[140px] rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" +const submitButtonClass = "inline-flex items-center gap-2 rounded-lg border border-black bg-[#00e8ff] px-3 py-2 text-sm font-semibold text-black transition hover:bg-[#00d6eb]" + export function TicketComments({ ticket }: TicketCommentsProps) { const { userId } = useAuth() const addComment = useMutation(api.tickets.addComment) - const generateUploadUrl = useAction(api.files.generateUploadUrl) const [body, setBody] = useState("") const [attachmentsToSend, setAttachmentsToSend] = useState>([]) const [preview, setPreview] = useState(null) - const [pending, setPending] = useState[]>([]) + const [pending, setPending] = useState[]>([]) const [visibility, setVisibility] = useState<"PUBLIC" | "INTERNAL">("PUBLIC") const commentsAll = useMemo(() => { return [...pending, ...ticket.comments] }, [pending, ticket.comments]) - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() + async function handleSubmit(event: React.FormEvent) { + event.preventDefault() if (!userId) return - const attachments = attachmentsToSend const now = new Date() + const attachments = attachmentsToSend const optimistic = { id: `temp-${now.getTime()}`, author: ticket.requester, visibility, body: sanitizeEditorHtml(body), - attachments: attachments.map((a) => ({ id: a.storageId, name: a.name, url: a.previewUrl })), + attachments: attachments.map((attachment) => ({ + id: attachment.storageId, + name: attachment.name, + url: attachment.previewUrl, + })), createdAt: now, updatedAt: now, } - setPending((p) => [optimistic, ...p]) + + setPending((current) => [optimistic, ...current]) setBody("") setAttachmentsToSend([]) toast.loading("Enviando comentário...", { id: "comment" }) + try { - const typedAttachments = attachments.map((a) => ({ - storageId: a.storageId as unknown as Id<"_storage">, - name: a.name, - size: a.size, - type: a.type, + const payload = attachments.map((attachment) => ({ + storageId: attachment.storageId as unknown as Id<"_storage">, + name: attachment.name, + size: attachment.size, + type: attachment.type, })) - await addComment({ ticketId: ticket.id as Id<"tickets">, authorId: userId as Id<"users">, visibility, body: optimistic.body, attachments: typedAttachments }) + await addComment({ + ticketId: ticket.id as Id<"tickets">, + authorId: userId as Id<"users">, + visibility, + body: optimistic.body, + attachments: payload, + }) setPending([]) toast.success("Comentário enviado!", { id: "comment" }) - } catch (err) { + } catch { setPending([]) toast.error("Falha ao enviar comentário.", { id: "comment" }) } } return ( - - - - Conversa + + + + Conversa @@ -87,10 +101,10 @@ export function TicketComments({ ticket }: TicketCommentsProps) { - + - Nenhum comentário ainda - Registre o próximo passo abaixo. + Nenhum comentário ainda + Registre o próximo passo abaixo. ) : ( @@ -100,51 +114,57 @@ export function TicketComments({ ticket }: TicketCommentsProps) { .slice(0, 2) .map((part) => part[0]?.toUpperCase()) .join("") + return (
- + {initials}
- {comment.author.name} + {comment.author.name} {comment.visibility === "INTERNAL" ? ( - - Interno + + Interno ) : null} - + {formatDistanceToNow(comment.createdAt, { addSuffix: true, locale: ptBR })}
-
+
{comment.attachments?.length ? (
- {comment.attachments.map((att) => { - const isImg = (att?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i) - if (isImg && att.url) { + {comment.attachments.map((attachment) => { + const isImage = (attachment?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i) + if (isImage && attachment.url) { return ( ) } return ( - - {att.name} - {att.url ? : null} + + {attachment.name} + {attachment.url ? : null} ) })} @@ -159,25 +179,26 @@ export function TicketComments({ ticket }: TicketCommentsProps) { setAttachmentsToSend((prev) => [...prev, ...files])} />
-
+
Visibilidade: - setVisibility(value as "PUBLIC" | "INTERNAL")}> + + + + Pública Interna
- +
- !o && setPreview(null)}> + !open && setPreview(null)}> - {preview ? ( - // eslint-disable-next-line @next/next/no-img-element - Preview - ) : null} + {preview ? Preview : null} diff --git a/web/src/components/tickets/ticket-detail-view.tsx b/web/src/components/tickets/ticket-detail-view.tsx index 25f39a4..ce66e8a 100644 --- a/web/src/components/tickets/ticket-detail-view.tsx +++ b/web/src/components/tickets/ticket-detail-view.tsx @@ -10,7 +10,6 @@ import type { Id } from "@/convex/_generated/dataModel"; import type { TicketWithDetails } from "@/lib/schemas/ticket"; import { getTicketById } from "@/lib/mocks/tickets"; import { Card, CardContent } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { TicketComments } from "@/components/tickets/ticket-comments.rich"; import { TicketDetailsPanel } from "@/components/tickets/ticket-details-panel"; @@ -28,7 +27,7 @@ export function TicketDetailView({ id }: { id: string }) { } if (!ticket) return (
- +
@@ -36,7 +35,7 @@ export function TicketDetailView({ id }: { id: string }) {
- + {Array.from({ length: 3 }).map((_, i) => (
@@ -46,7 +45,7 @@ export function TicketDetailView({ id }: { id: string }) { ))} - + {Array.from({ length: 5 }).map((_, i) => ())} diff --git a/web/src/components/tickets/ticket-details-panel.tsx b/web/src/components/tickets/ticket-details-panel.tsx index f3bc082..5c8a052 100644 --- a/web/src/components/tickets/ticket-details-panel.tsx +++ b/web/src/components/tickets/ticket-details-panel.tsx @@ -1,85 +1,85 @@ -import { format } from "date-fns" -import { ptBR } from "date-fns/locale" -import { IconAlertTriangle, IconClockHour4, IconTags } from "@tabler/icons-react" - -import type { TicketWithDetails } from "@/lib/schemas/ticket" -import { Badge } from "@/components/ui/badge" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Separator } from "@/components/ui/separator" - -interface TicketDetailsPanelProps { - ticket: TicketWithDetails -} - +import { format } from "date-fns" +import { ptBR } from "date-fns/locale" +import { IconAlertTriangle, IconClockHour4, IconTags } from "@tabler/icons-react" + +import type { TicketWithDetails } from "@/lib/schemas/ticket" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Separator } from "@/components/ui/separator" + +interface TicketDetailsPanelProps { + ticket: TicketWithDetails +} + +const queueBadgeClass = "inline-flex items-center rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-semibold text-neutral-700" +const tagBadgeClass = "inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-neutral-700" +const iconAccentClass = "size-3 text-neutral-700" + export function TicketDetailsPanel({ ticket }: TicketDetailsPanelProps) { return ( - - - Detalhes + + + Detalhes - +
-

Fila

- {ticket.queue ?? "Sem fila"} +

Fila

+ {ticket.queue ?? "Sem fila"}
- +
-

SLA

+

SLA

{ticket.slaPolicy ? ( -
- {ticket.slaPolicy.name} -
+
+ {ticket.slaPolicy.name} +
{ticket.slaPolicy.targetMinutesToFirstResponse ? ( - - Resposta inicial: {ticket.slaPolicy.targetMinutesToFirstResponse} min - + Resposta inicial: {ticket.slaPolicy.targetMinutesToFirstResponse} min ) : null} {ticket.slaPolicy.targetMinutesToResolution ? ( - - Resolução: {ticket.slaPolicy.targetMinutesToResolution} min - + Resolução: {ticket.slaPolicy.targetMinutesToResolution} min ) : null}
) : ( - Sem política atribuída. + Sem política atribuída. )}
- +
-

Métricas

+

Métricas

{ticket.metrics ? ( -
+
- Tempo aguardando: {ticket.metrics.timeWaitingMinutes ?? "-"} min + Tempo aguardando: {ticket.metrics.timeWaitingMinutes ?? "-"} min - Tempo aberto: {ticket.metrics.timeOpenedMinutes ?? "-"} min + Tempo aberto: {ticket.metrics.timeOpenedMinutes ?? "-"} min
) : ( - Sem dados de SLA ainda. + Sem dados de SLA ainda. )}
- +
-

Tags

+

Tags

{ticket.tags?.length ? ( ticket.tags.map((tag) => ( - - {tag} + + {tag} )) ) : ( - Sem tags. + Sem tags. )}
- +
-

Histórico

-
+

Histórico

+
Criado: {format(ticket.createdAt, "dd/MM/yyyy HH:mm", { locale: ptBR })} Atualizado: {format(ticket.updatedAt, "dd/MM/yyyy HH:mm", { locale: ptBR })} {ticket.resolvedAt ? ( @@ -90,4 +90,4 @@ export function TicketDetailsPanel({ ticket }: TicketDetailsPanelProps) { ) -} +} diff --git a/web/src/components/tickets/ticket-queue-summary.tsx b/web/src/components/tickets/ticket-queue-summary.tsx index adeeb01..fbc60a2 100644 --- a/web/src/components/tickets/ticket-queue-summary.tsx +++ b/web/src/components/tickets/ticket-queue-summary.tsx @@ -1,67 +1,68 @@ "use client" import { useQuery } from "convex/react" -// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" import type { TicketQueueSummary } from "@/lib/schemas/ticket" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Progress } from "@/components/ui/progress" - -interface TicketQueueSummaryProps { - queues?: TicketQueueSummary[] -} - + +interface TicketQueueSummaryProps { + queues?: TicketQueueSummary[] +} + export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) { const fromServer = useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID }) const data: TicketQueueSummary[] = (queues ?? (fromServer as TicketQueueSummary[] | undefined) ?? []) + if (!queues && fromServer === undefined) { return (
- {Array.from({ length: 3 }).map((_, i) => ( -
-
-
+ {Array.from({ length: 3 }).map((_, index) => ( +
+
+
))}
) } + return (
{data.map((queue) => { const total = queue.pending + queue.waiting const breachPercent = total === 0 ? 0 : Math.round((queue.breached / total) * 100) return ( - - - Fila - {queue.name} - - -
- Pendentes - {queue.pending} -
-
- Aguardando resposta - {queue.waiting} -
-
- Violados - {queue.breached} -
-
- - - {breachPercent}% com SLA violado nesta fila - -
-
-
- ) - })} -
- ) + + + Fila + {queue.name} + + +
+ Pendentes + {queue.pending} +
+
+ Aguardando resposta + {queue.waiting} +
+
+ Violados + {queue.breached} +
+
+ + + {breachPercent}% com SLA violado nesta fila + +
+
+
+ ) + })} +
+ ) } diff --git a/web/src/components/tickets/ticket-summary-header.tsx b/web/src/components/tickets/ticket-summary-header.tsx index 4d40c78..3f2e37d 100644 --- a/web/src/components/tickets/ticket-summary-header.tsx +++ b/web/src/components/tickets/ticket-summary-header.tsx @@ -26,6 +26,16 @@ interface TicketHeaderProps { ticket: TicketWithDetails } +const cardClass = "space-y-4 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm" +const referenceBadgeClass = "inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold text-neutral-700" +const startButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-[#00e8ff] px-3 py-1.5 text-sm font-semibold text-black transition hover:bg-[#00d6eb]" +const pauseButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-black/90" +const editButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-black/90" +const selectTriggerClass = "h-8 w-[220px] rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" +const smallSelectTriggerClass = "h-8 w-[180px] rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" +const sectionLabelClass = "text-xs font-semibold uppercase tracking-wide text-neutral-500" +const sectionValueClass = "font-medium text-neutral-900" + export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { const { userId } = useAuth() const changeAssignee = useMutation(api.tickets.changeAssignee) @@ -40,7 +50,10 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { const [editing, setEditing] = useState(false) const [subject, setSubject] = useState(ticket.subject) const [summary, setSummary] = useState(ticket.summary ?? "") - const dirty = useMemo(() => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""), [subject, summary, ticket.subject, ticket.summary]) + const dirty = useMemo( + () => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""), + [subject, summary, ticket.subject, ticket.summary] + ) async function handleSave() { if (!userId) return @@ -69,19 +82,16 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { const isPlaying = lastWork?.type === "WORK_STARTED" return ( -
+
-
-
- - #{ticket.reference} - +
+
+ #{ticket.reference}
{editing ? (
- setSubject(e.target.value)} className="h-9 text-base font-semibold" /> + setSubject(e.target.value)} + className="h-10 rounded-lg border border-slate-300 text-lg font-semibold text-neutral-900" + />