feat: CSV exports, PDF improvements, play internal/external with hour split, roles cleanup, admin companies with 'Cliente avulso', ticket list spacing/alignment fixes, status translations and mappings
This commit is contained in:
parent
addd4ce6e8
commit
3bafcc5a0a
45 changed files with 1401 additions and 256 deletions
|
|
@ -9,7 +9,6 @@ const statusStyles: Record<TicketStatus, { label: string; className: string }> =
|
|||
AWAITING_ATTENDANCE: { label: "Aguardando atendimento", className: "border border-slate-200 bg-[#dff1fb] text-[#0a4760]" },
|
||||
PAUSED: { label: "Pausado", 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 }
|
||||
|
|
|
|||
|
|
@ -14,14 +14,13 @@ import { ChevronDown } from "lucide-react"
|
|||
|
||||
type StatusKey = TicketStatus | "NEW" | "OPEN" | "ON_HOLD";
|
||||
|
||||
const STATUS_OPTIONS: TicketStatus[] = ["PENDING", "AWAITING_ATTENDANCE", "PAUSED", "RESOLVED", "CLOSED"];
|
||||
const STATUS_OPTIONS: TicketStatus[] = ["PENDING", "AWAITING_ATTENDANCE", "PAUSED", "RESOLVED"];
|
||||
|
||||
const statusStyles: Record<StatusKey, { label: string; badgeClass: string }> = {
|
||||
PENDING: { label: "Pendente", badgeClass: "bg-slate-100 text-slate-700" },
|
||||
AWAITING_ATTENDANCE: { label: "Aguardando atendimento", badgeClass: "bg-[#dff1fb] text-[#0a4760]" },
|
||||
PAUSED: { label: "Pausado", badgeClass: "bg-[#ede8ff] text-[#4f2f96]" },
|
||||
RESOLVED: { label: "Resolvido", badgeClass: "bg-[#dcf4eb] text-[#1f6a45]" },
|
||||
CLOSED: { label: "Fechado", badgeClass: "bg-slate-200 text-slate-700" },
|
||||
NEW: { label: "Pendente", badgeClass: "bg-slate-100 text-slate-700" },
|
||||
OPEN: { label: "Aguardando atendimento", badgeClass: "bg-[#dff1fb] text-[#0a4760]" },
|
||||
ON_HOLD: { label: "Pausado", badgeClass: "bg-[#ede8ff] text-[#4f2f96]" },
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { format, formatDistanceToNow } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
import { IconClock, IconFileDownload, IconPlayerPause, IconPlayerPlay } from "@tabler/icons-react"
|
||||
import { IconClock, IconFileTypePdf, IconPlayerPause, IconPlayerPlay } from "@tabler/icons-react"
|
||||
import { useMutation, useQuery } from "convex/react"
|
||||
import { toast } from "sonner"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
|
@ -24,6 +24,12 @@ import { Textarea } from "@/components/ui/textarea"
|
|||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { useTicketCategories } from "@/hooks/use-ticket-categories"
|
||||
import { useDefaultQueues } from "@/hooks/use-default-queues"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
interface TicketHeaderProps {
|
||||
ticket: TicketWithDetails
|
||||
|
|
@ -128,6 +134,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
return selectedCategoryId !== currentCategoryId || selectedSubcategoryId !== currentSubcategoryId
|
||||
}, [selectedCategoryId, selectedSubcategoryId, currentCategoryId, currentSubcategoryId])
|
||||
const currentQueueName = ticket.queue ?? ""
|
||||
const isAvulso = Boolean((ticket as any).company?.isAvulso ?? false)
|
||||
const [queueSelection, setQueueSelection] = useState(currentQueueName)
|
||||
const queueDirty = useMemo(() => queueSelection !== currentQueueName, [queueSelection, currentQueueName])
|
||||
const formDirty = dirty || categoryDirty || queueDirty
|
||||
|
|
@ -263,11 +270,14 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
return {
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
totalWorkedMs: ticket.workSummary.totalWorkedMs,
|
||||
internalWorkedMs: ticket.workSummary.internalWorkedMs ?? 0,
|
||||
externalWorkedMs: ticket.workSummary.externalWorkedMs ?? 0,
|
||||
activeSession: ticket.workSummary.activeSession
|
||||
? {
|
||||
id: ticket.workSummary.activeSession.id as Id<"ticketWorkSessions">,
|
||||
agentId: ticket.workSummary.activeSession.agentId as Id<"users">,
|
||||
startedAt: ticket.workSummary.activeSession.startedAt.getTime(),
|
||||
workType: (ticket.workSummary.activeSession as any).workType ?? "INTERNAL",
|
||||
}
|
||||
: null,
|
||||
}
|
||||
|
|
@ -294,6 +304,12 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
|
||||
const currentSessionMs = workSummary?.activeSession ? Math.max(0, now - workSummary.activeSession.startedAt) : 0
|
||||
const totalWorkedMs = workSummary ? workSummary.totalWorkedMs + currentSessionMs : 0
|
||||
const internalWorkedMs = workSummary
|
||||
? (((workSummary as any).internalWorkedMs ?? 0) + (((workSummary?.activeSession as any)?.workType === "INTERNAL") ? currentSessionMs : 0))
|
||||
: 0
|
||||
const externalWorkedMs = workSummary
|
||||
? (((workSummary as any).externalWorkedMs ?? 0) + (((workSummary?.activeSession as any)?.workType === "EXTERNAL") ? currentSessionMs : 0))
|
||||
: 0
|
||||
|
||||
const formattedTotalWorked = useMemo(() => formatDuration(totalWorkedMs), [totalWorkedMs])
|
||||
const updatedRelative = useMemo(
|
||||
|
|
@ -301,12 +317,12 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
[ticket.updatedAt]
|
||||
)
|
||||
|
||||
const handleStartWork = async () => {
|
||||
const handleStartWork = async (workType: "INTERNAL" | "EXTERNAL") => {
|
||||
if (!convexUserId) return
|
||||
toast.dismiss("work")
|
||||
toast.loading("Iniciando atendimento...", { id: "work" })
|
||||
try {
|
||||
const result = await startWork({ ticketId: ticket.id as Id<"tickets">, actorId: convexUserId as Id<"users"> })
|
||||
const result = await startWork({ ticketId: ticket.id as Id<"tickets">, actorId: convexUserId as Id<"users">, workType } as any)
|
||||
if (result?.status === "already_started") {
|
||||
toast.info("O atendimento já estava em andamento", { id: "work" })
|
||||
} else {
|
||||
|
|
@ -347,7 +363,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
setExportingPdf(true)
|
||||
toast.dismiss("ticket-export")
|
||||
toast.loading("Gerando PDF...", { id: "ticket-export" })
|
||||
const response = await fetch(`/api/tickets/${ticket.id}/export/pdf`)
|
||||
const response = await fetch(`/api/tickets/${ticket.id}/export/pdf`, { credentials: "include" })
|
||||
if (!response.ok) {
|
||||
throw new Error(`failed: ${response.status}`)
|
||||
}
|
||||
|
|
@ -373,9 +389,17 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
<div className={cardClass}>
|
||||
<div className="absolute right-6 top-6 flex items-center gap-3">
|
||||
{workSummary ? (
|
||||
<Badge className="inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700">
|
||||
<IconClock className="size-4 text-neutral-700" /> Tempo total: {formattedTotalWorked}
|
||||
</Badge>
|
||||
<>
|
||||
<Badge className="inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700">
|
||||
<IconClock className="size-4 text-neutral-700" /> Interno: {formatDuration(internalWorkedMs)}
|
||||
</Badge>
|
||||
<Badge className="inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700">
|
||||
<IconClock className="size-4 text-neutral-700" /> Externo: {formatDuration(externalWorkedMs)}
|
||||
</Badge>
|
||||
<Badge className="inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700">
|
||||
<IconClock className="size-4 text-neutral-700" /> Total: {formattedTotalWorked}
|
||||
</Badge>
|
||||
</>
|
||||
) : null}
|
||||
{!editing ? (
|
||||
<Button size="sm" className={editButtonClass} onClick={() => setEditing(true)}>
|
||||
|
|
@ -383,45 +407,53 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm font-semibold text-neutral-800 hover:bg-slate-50"
|
||||
aria-label="Exportar PDF"
|
||||
className="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white text-neutral-800 hover:bg-slate-50"
|
||||
onClick={handleExportPdf}
|
||||
disabled={exportingPdf}
|
||||
title="Exportar PDF"
|
||||
>
|
||||
{exportingPdf ? <Spinner className="size-3 text-neutral-700" /> : <IconFileDownload className="size-4" />}
|
||||
Exportar PDF
|
||||
{exportingPdf ? <Spinner className="size-4 text-neutral-700" /> : <IconFileTypePdf className="size-5" />}
|
||||
</Button>
|
||||
<DeleteTicketDialog ticketId={ticket.id as Id<"tickets">} />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge className={referenceBadgeClass}>#{ticket.reference}</Badge>
|
||||
<PrioritySelect ticketId={ticket.id} value={ticket.priority} />
|
||||
<StatusSelect ticketId={ticket.id} value={status} />
|
||||
<Button
|
||||
size="sm"
|
||||
className={isPlaying ? pauseButtonClass : startButtonClass}
|
||||
onClick={() => {
|
||||
if (!convexUserId) return
|
||||
if (isPlaying) {
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge className={referenceBadgeClass}>#{ticket.reference}</Badge>
|
||||
{isAvulso ? (
|
||||
<Badge className="inline-flex h-9 items-center gap-2 rounded-full border border-rose-200 bg-rose-50 px-3 text-sm font-semibold text-rose-700">
|
||||
Cliente avulso
|
||||
</Badge>
|
||||
) : null}
|
||||
<PrioritySelect ticketId={ticket.id} value={ticket.priority} />
|
||||
<StatusSelect ticketId={ticket.id} value={status} />
|
||||
{isPlaying ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className={pauseButtonClass}
|
||||
onClick={() => {
|
||||
if (!convexUserId) return
|
||||
setPauseDialogOpen(true)
|
||||
} else {
|
||||
void handleStartWork()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<>
|
||||
<IconPlayerPause className="size-4 text-white" /> Pausar
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconPlayerPlay className="size-4 text-white" /> Iniciar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
}}
|
||||
>
|
||||
<IconPlayerPause className="size-4 text-white" /> Pausar
|
||||
</Button>
|
||||
) : (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" className={startButtonClass}>
|
||||
<IconPlayerPlay className="size-4 text-white" /> Iniciar
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="rounded-lg border border-slate-200 bg-white text-neutral-800 shadow-sm">
|
||||
<DropdownMenuItem onSelect={() => void handleStartWork("INTERNAL")}>Iniciar (interno)</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => void handleStartWork("EXTERNAL")}>Iniciar (externo)</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
{editing ? (
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ const statusOptions: Array<{ value: TicketStatus; label: string }> = [
|
|||
{ value: "AWAITING_ATTENDANCE", label: "Aguardando atendimento" },
|
||||
{ value: "PAUSED", label: "Pausado" },
|
||||
{ value: "RESOLVED", label: "Resolvido" },
|
||||
{ value: "CLOSED", label: "Fechado" },
|
||||
]
|
||||
|
||||
const statusLabelMap = statusOptions.reduce<Record<TicketStatus, string>>((acc, option) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
import { format, formatDistanceToNow, formatDistanceToNowStrict } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
import { type LucideIcon, Code, FileText, Mail, MessageCircle, MessageSquare, Phone } from "lucide-react"
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ const channelIcon: Record<TicketChannel, LucideIcon> = {
|
|||
MANUAL: FileText,
|
||||
}
|
||||
|
||||
const cellClass = "px-6 py-5 align-top text-sm text-neutral-700 first:pl-8 last:pr-8"
|
||||
const cellClass = "px-4 py-4 align-middle text-sm text-neutral-700 whitespace-normal first:pl-5 last:pr-6"
|
||||
const channelIconBadgeClass = "inline-flex size-8 items-center justify-center rounded-full border border-slate-200 bg-slate-50 text-neutral-700"
|
||||
const categoryChipClass = "inline-flex items-center gap-1 rounded-full bg-slate-200/60 px-2.5 py-1 text-[11px] font-medium text-neutral-700"
|
||||
const tableRowClass =
|
||||
|
|
@ -53,7 +53,6 @@ const statusLabel: Record<TicketStatus, string> = {
|
|||
AWAITING_ATTENDANCE: "Aguardando atendimento",
|
||||
PAUSED: "Pausado",
|
||||
RESOLVED: "Resolvido",
|
||||
CLOSED: "Fechado",
|
||||
}
|
||||
|
||||
const statusTone: Record<TicketStatus, string> = {
|
||||
|
|
@ -61,7 +60,6 @@ const statusTone: Record<TicketStatus, string> = {
|
|||
AWAITING_ATTENDANCE: "text-sky-700",
|
||||
PAUSED: "text-violet-700",
|
||||
RESOLVED: "text-emerald-700",
|
||||
CLOSED: "text-slate-600",
|
||||
}
|
||||
|
||||
function formatDuration(ms?: number) {
|
||||
|
|
@ -135,34 +133,34 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
|||
return (
|
||||
<Card className="gap-0 rounded-3xl border border-slate-200 bg-white py-0 shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<Table className="min-w-full overflow-hidden rounded-3xl">
|
||||
<Table className="min-w-full overflow-hidden rounded-3xl table-fixed">
|
||||
<TableHeader className="bg-slate-100/80">
|
||||
<TableRow className="bg-transparent text-[11px] uppercase tracking-wide text-neutral-600">
|
||||
<TableHead className="w-[120px] px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8">
|
||||
<TableHead className="w-[120px] px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6">
|
||||
Ticket
|
||||
</TableHead>
|
||||
<TableHead className="px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8">
|
||||
<TableHead className="w-[40%] px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6">
|
||||
Assunto
|
||||
</TableHead>
|
||||
<TableHead className="hidden px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8 lg:table-cell">
|
||||
<TableHead className="hidden w-[120px] pl-1 pr-3 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6 lg:table-cell">
|
||||
Fila
|
||||
</TableHead>
|
||||
<TableHead className="hidden px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8 md:table-cell">
|
||||
<TableHead className="hidden w-[80px] pl-1 pr-3 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6 md:table-cell">
|
||||
Canal
|
||||
</TableHead>
|
||||
<TableHead className="hidden px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8 md:table-cell">
|
||||
<TableHead className="hidden w-[100px] pl-1 pr-3 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6 md:table-cell">
|
||||
Prioridade
|
||||
</TableHead>
|
||||
<TableHead className="px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8">
|
||||
<TableHead className="w-[230px] pl-14 pr-3 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="hidden px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8 lg:table-cell">
|
||||
<TableHead className="hidden w-[110px] px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6 lg:table-cell">
|
||||
Tempo
|
||||
</TableHead>
|
||||
<TableHead className="hidden px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8 xl:table-cell">
|
||||
<TableHead className="hidden w-[200px] px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6 xl:table-cell">
|
||||
Responsável
|
||||
</TableHead>
|
||||
<TableHead className="px-6 py-4 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-8 last:pr-8">
|
||||
<TableHead className="w-[140px] px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6">
|
||||
Atualizado
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
|
|
@ -196,11 +194,11 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={cellClass}>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="line-clamp-1 text-[15px] font-semibold text-neutral-900">
|
||||
<div className="flex flex-col gap-1.5 min-w-0">
|
||||
<span className="text-[15px] font-semibold text-neutral-900 line-clamp-2 md:line-clamp-1 break-words">
|
||||
{ticket.subject}
|
||||
</span>
|
||||
<span className="line-clamp-1 text-sm text-neutral-600">
|
||||
<span className="text-sm text-neutral-600 line-clamp-1 break-words max-w-[52ch]">
|
||||
{ticket.summary ?? "Sem resumo"}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 text-xs text-neutral-500">
|
||||
|
|
@ -216,12 +214,12 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
|||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={`${cellClass} hidden lg:table-cell`}>
|
||||
<TableCell className={`${cellClass} hidden lg:table-cell pl-0`}>
|
||||
<span className="text-sm font-semibold text-neutral-800">
|
||||
{ticket.queue ?? "Sem fila"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className={`${cellClass} hidden md:table-cell`}>
|
||||
<TableCell className={`${cellClass} hidden md:table-cell pl-1`}>
|
||||
<div className="flex items-center">
|
||||
<span className="sr-only">Canal {channelLabel[ticket.channel]}</span>
|
||||
<span
|
||||
|
|
@ -233,7 +231,7 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
|||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={`${cellClass} hidden md:table-cell`}>
|
||||
<TableCell className={`${cellClass} hidden md:table-cell pl-1 pr-8`}>
|
||||
<div
|
||||
className="inline-flex"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
|
|
@ -242,9 +240,9 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
|||
<PrioritySelect ticketId={ticket.id} value={ticket.priority} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={cellClass}>
|
||||
<TableCell className={`${cellClass} pl-14`}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className={cn("text-sm font-semibold", statusTone[ticket.status])}>
|
||||
<span className={cn("text-sm font-semibold break-words leading-tight max-w-[140px] sm:max-w-[180px]", statusTone[ticket.status])}>
|
||||
{statusLabel[ticket.status]}
|
||||
</span>
|
||||
{ticket.metrics?.timeWaitingMinutes ? (
|
||||
|
|
@ -266,9 +264,14 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
|||
<AssigneeCell ticket={ticket} />
|
||||
</TableCell>
|
||||
<TableCell className={cellClass}>
|
||||
<span className="text-sm text-neutral-600">
|
||||
{formatDistanceToNow(ticket.updatedAt, { addSuffix: true, locale: ptBR })}
|
||||
</span>
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="text-sm text-neutral-700">
|
||||
{`há cerca de ${formatDistanceToNowStrict(ticket.updatedAt, { locale: ptBR })}`}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500">
|
||||
{format(ticket.updatedAt, "dd/MM/yyyy HH:mm")}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export function TicketsView() {
|
|||
const tickets = useMemo(() => mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]), [ticketsRaw])
|
||||
|
||||
const filteredTickets = useMemo(() => {
|
||||
const completedStatuses = new Set<Ticket["status"]>(["RESOLVED", "CLOSED"])
|
||||
const completedStatuses = new Set<Ticket["status"]>(["RESOLVED"])
|
||||
let working = tickets
|
||||
|
||||
if (!filters.status) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue