feat: enforce ticket ownership during work sessions
This commit is contained in:
parent
81657e52d8
commit
3972f66c92
6 changed files with 397 additions and 43 deletions
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue