feat: enforce ticket ownership during work sessions

This commit is contained in:
Esdras Renan 2025-10-20 19:46:20 -03:00
parent 81657e52d8
commit 3972f66c92
6 changed files with 397 additions and 43 deletions

View file

@ -1,6 +1,7 @@
import { useMemo } from "react"
import { format } from "date-fns"
import { ptBR } from "date-fns/locale"
import { IconAlertTriangle, IconClockHour4, IconTags } from "@tabler/icons-react"
import { IconAlertTriangle, IconClockHour4 } from "@tabler/icons-react"
import type { TicketWithDetails } from "@/lib/schemas/ticket"
import { Badge } from "@/components/ui/badge"
@ -12,12 +13,38 @@ interface TicketDetailsPanelProps {
}
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"
function formatDuration(ms?: number | null) {
if (!ms || ms <= 0) return "0s"
const totalSeconds = Math.floor(ms / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
if (hours > 0) {
return `${hours}h ${minutes.toString().padStart(2, "0")}m`
}
if (minutes > 0) {
return `${minutes}m ${seconds.toString().padStart(2, "0")}s`
}
return `${seconds}s`
}
export function TicketDetailsPanel({ ticket }: TicketDetailsPanelProps) {
const isAvulso = Boolean(ticket.company?.isAvulso)
const companyLabel = ticket.company?.name ?? (isAvulso ? "Cliente avulso" : "Sem empresa vinculada")
const agentTotals = useMemo(() => {
const totals = ticket.workSummary?.perAgentTotals ?? []
return totals
.map((item) => ({
agentId: String(item.agentId),
agentName: item.agentName ?? null,
totalWorkedMs: item.totalWorkedMs ?? 0,
internalWorkedMs: item.internalWorkedMs ?? 0,
externalWorkedMs: item.externalWorkedMs ?? 0,
}))
.sort((a, b) => b.totalWorkedMs - a.totalWorkedMs)
}, [ticket.workSummary?.perAgentTotals])
return (
<Card className="rounded-2xl border border-slate-200 bg-white shadow-sm">
@ -70,21 +97,31 @@ export function TicketDetailsPanel({ ticket }: TicketDetailsPanelProps) {
)}
</div>
<Separator className="bg-slate-200" />
<div className="space-y-2 break-words">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">Tags</p>
<div className="flex flex-wrap gap-2">
{ticket.tags?.length ? (
ticket.tags.map((tag) => (
<Badge key={tag} className={tagBadgeClass}>
<IconTags className={iconAccentClass} /> {tag}
</Badge>
))
) : (
<span className="text-neutral-600">Sem tags.</span>
)}
</div>
</div>
<Separator className="bg-slate-200" />
{agentTotals.length > 0 ? (
<>
<div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">Tempo por agente</p>
<div className="flex flex-col gap-2">
{agentTotals.map((agent) => (
<div
key={agent.agentId}
className="flex flex-col gap-1 rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs text-neutral-700 shadow-sm"
>
<div className="flex items-center justify-between text-sm font-semibold text-neutral-900">
<span>{agent.agentName ?? "Agente removido"}</span>
<span>{formatDuration(agent.totalWorkedMs)}</span>
</div>
<div className="flex flex-wrap gap-4 text-xs text-neutral-600">
<span>Interno: {formatDuration(agent.internalWorkedMs)}</span>
<span>Externo: {formatDuration(agent.externalWorkedMs)}</span>
</div>
</div>
))}
</div>
</div>
<Separator className="bg-slate-200" />
</>
) : null}
<div className="space-y-1">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">Histórico</p>
<div className="flex flex-col gap-1 text-xs text-neutral-600">