"use client" import { useMemo, useState } from "react" import { useMutation, useQuery } from "convex/react" import { toast } from "sonner" import { IconAlarm, IconBolt, IconTargetArrow } from "@tabler/icons-react" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { useAuth } from "@/lib/auth-client" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Skeleton } from "@/components/ui/skeleton" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { CategorySlaManager } from "./category-sla-manager" import { CompanySlaManager } from "./company-sla-manager" type SlaPolicy = { id: string name: string description: string timeToFirstResponse: number | null timeToResolution: number | null } function formatMinutes(value: number | null) { if (value === null) return "—" if (value < 60) return `${Math.round(value)} min` const hours = Math.floor(value / 60) const minutes = Math.round(value % 60) if (minutes === 0) return `${hours}h` return `${hours}h ${minutes}min` } export function SlasManager() { const { session, convexUserId } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const slas = useQuery( api.slas.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as SlaPolicy[] | undefined const createSla = useMutation(api.slas.create) const updateSla = useMutation(api.slas.update) const removeSla = useMutation(api.slas.remove) const [name, setName] = useState("") const [description, setDescription] = useState("") const [firstResponse, setFirstResponse] = useState("") const [resolution, setResolution] = useState("") const [saving, setSaving] = useState(false) const [editingSla, setEditingSla] = useState(null) const { bestFirstResponse, bestResolution } = useMemo(() => { if (!slas) return { bestFirstResponse: null, bestResolution: null } const response = slas.reduce((acc, sla) => { if (sla.timeToFirstResponse === null) return acc return acc === null ? sla.timeToFirstResponse : Math.min(acc, sla.timeToFirstResponse) }, null) const resolution = slas.reduce((acc, sla) => { if (sla.timeToResolution === null) return acc return acc === null ? sla.timeToResolution : Math.min(acc, sla.timeToResolution) }, null) return { bestFirstResponse: response, bestResolution: resolution } }, [slas]) const resetForm = () => { setName("") setDescription("") setFirstResponse("") setResolution("") } const parseNumber = (value: string) => { const parsed = Number(value) return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined } const handleCreate = async (event: React.FormEvent) => { event.preventDefault() if (!name.trim()) { toast.error("Informe um nome para a política") return } if (!convexUserId) { toast.error("Sessão não sincronizada com o Convex") return } setSaving(true) toast.loading("Criando SLA...", { id: "sla" }) try { await createSla({ tenantId, actorId: convexUserId as Id<"users">, name: name.trim(), description: description.trim() || undefined, timeToFirstResponse: parseNumber(firstResponse), timeToResolution: parseNumber(resolution), }) toast.success("Política criada", { id: "sla" }) resetForm() } catch (error) { console.error(error) toast.error("Não foi possível criar a política", { id: "sla" }) } finally { setSaving(false) } } const openEdit = (policy: SlaPolicy) => { setEditingSla(policy) setName(policy.name) setDescription(policy.description) setFirstResponse(policy.timeToFirstResponse ? String(policy.timeToFirstResponse) : "") setResolution(policy.timeToResolution ? String(policy.timeToResolution) : "") } const handleUpdate = async () => { if (!editingSla) return if (!name.trim()) { toast.error("Informe um nome para a política") return } if (!convexUserId) { toast.error("Sessão não sincronizada com o Convex") return } setSaving(true) toast.loading("Salvando alterações...", { id: "sla-edit" }) try { await updateSla({ tenantId, policyId: editingSla.id as Id<"slaPolicies">, actorId: convexUserId as Id<"users">, name: name.trim(), description: description.trim() || undefined, timeToFirstResponse: parseNumber(firstResponse), timeToResolution: parseNumber(resolution), }) toast.success("Política atualizada", { id: "sla-edit" }) setEditingSla(null) resetForm() } catch (error) { console.error(error) toast.error("Não foi possível atualizar a política", { id: "sla-edit" }) } finally { setSaving(false) } } const handleRemove = async (policy: SlaPolicy) => { const confirmed = window.confirm(`Excluir a política ${policy.name}?`) if (!confirmed) return if (!convexUserId) { toast.error("Sessão não sincronizada com o Convex") return } toast.loading("Removendo política...", { id: `sla-remove-${policy.id}` }) try { await removeSla({ tenantId, policyId: policy.id as Id<"slaPolicies">, actorId: convexUserId as Id<"users">, }) toast.success("Política removida", { id: `sla-remove-${policy.id}` }) } catch (error) { console.error(error) toast.error("Não foi possível remover a política", { id: `sla-remove-${policy.id}` }) } } return (
Políticas criadas Regras aplicadas às filas e tickets. {slas ? slas.length : } Resposta (média) Tempo mínimo para primeira resposta. {slas ? formatMinutes(bestFirstResponse ?? null) : } Resolução (média) Alvo para encerrar chamados. {slas ? formatMinutes(bestResolution ?? null) : }
Nova política de SLA Defina metas de resposta e resolução para garantir previsibilidade no atendimento.
setName(event.target.value)} required />
setFirstResponse(event.target.value)} placeholder="Opcional" />
setResolution(event.target.value)} placeholder="Opcional" />