"use client" import { useMemo, useState } from "react" import { useMutation, useQuery } from "convex/react" import { toast } from "sonner" import { Globe, Plus } from "lucide-react" 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 { Badge } from "@/components/ui/badge" 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Skeleton } from "@/components/ui/skeleton" import { Dialog, DialogContent, DialogDescription, 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 } type TimeUnit = "minutes" | "hours" | "days" const TIME_UNITS: Array<{ value: TimeUnit; label: string; factor: number }> = [ { value: "minutes", label: "Minutos", factor: 1 }, { value: "hours", label: "Horas", factor: 60 }, { value: "days", label: "Dias", factor: 1440 }, ] 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` } function minutesToForm(input: number | null): { amount: string; unit: TimeUnit } { if (!input || input <= 0) return { amount: "", unit: "hours" } for (const option of [...TIME_UNITS].reverse()) { if (input % option.factor === 0) { return { amount: String(Math.round(input / option.factor)), unit: option.value } } } return { amount: String(input), unit: "minutes" } } function convertToMinutes(amount: string, unit: TimeUnit): number | undefined { const numeric = Number(amount) if (!Number.isFinite(numeric) || numeric <= 0) return undefined const factor = TIME_UNITS.find((item) => item.value === unit)?.factor ?? 1 return Math.round(numeric * factor) } 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 [dialogOpen, setDialogOpen] = useState(false) const [editingSla, setEditingSla] = useState(null) const [name, setName] = useState("") const [description, setDescription] = useState("") const [responseAmount, setResponseAmount] = useState("") const [responseUnit, setResponseUnit] = useState("hours") const [resolutionAmount, setResolutionAmount] = useState("") const [resolutionUnit, setResolutionUnit] = useState("hours") const [saving, setSaving] = useState(false) 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("") setResponseAmount("") setResponseUnit("hours") setResolutionAmount("") setResolutionUnit("hours") } const openCreateDialog = () => { resetForm() setEditingSla(null) setDialogOpen(true) } const openEditDialog = (policy: SlaPolicy) => { const response = minutesToForm(policy.timeToFirstResponse) const resolution = minutesToForm(policy.timeToResolution) setEditingSla(policy) setName(policy.name) setDescription(policy.description) setResponseAmount(response.amount) setResponseUnit(response.unit) setResolutionAmount(resolution.amount) setResolutionUnit(resolution.unit) setDialogOpen(true) } const closeDialog = () => { setDialogOpen(false) setEditingSla(null) resetForm() } const handleSave = async () => { if (!name.trim()) { toast.error("Informe um nome para a politica") return } if (!convexUserId) { toast.error("Sessao nao sincronizada com o Convex") return } const timeToFirstResponse = convertToMinutes(responseAmount, responseUnit) const timeToResolution = convertToMinutes(resolutionAmount, resolutionUnit) setSaving(true) const toastId = editingSla ? "sla-edit" : "sla-create" toast.loading(editingSla ? "Salvando alteracoes..." : "Criando politica...", { id: toastId }) try { if (editingSla) { await updateSla({ tenantId, policyId: editingSla.id as Id<"slaPolicies">, actorId: convexUserId as Id<"users">, name: name.trim(), description: description.trim() || undefined, timeToFirstResponse, timeToResolution, }) toast.success("Politica atualizada", { id: toastId }) } else { await createSla({ tenantId, actorId: convexUserId as Id<"users">, name: name.trim(), description: description.trim() || undefined, timeToFirstResponse, timeToResolution, }) toast.success("Politica criada", { id: toastId }) } closeDialog() } catch (error) { console.error(error) toast.error(editingSla ? "Nao foi possivel atualizar a politica" : "Nao foi possivel criar a politica", { id: toastId }) } 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 (
{/* Cards de resumo */}
Politicas globais Regras que valem para todas as empresas. {slas ? slas.length : } Melhor resposta Menor meta de primeira resposta. {slas ? formatMinutes(bestFirstResponse ?? null) : } Melhor resolucao Menor meta para encerrar chamados. {slas ? formatMinutes(bestResolution ?? null) : }
{/* Politicas globais de SLA */}
Politicas globais de SLA Estas regras valem para todas as empresas e categorias. Sao sobrescritas por regras mais especificas (por empresa ou por categoria).
{slas === undefined ? (
{Array.from({ length: 2 }).map((_, index) => ( ))}
) : slas.length === 0 ? (

Nenhuma politica global cadastrada

Crie politicas de SLA para definir metas de resposta e resolucao para os chamados.

) : (
{slas.map((policy) => (

{policy.name}

Global
{policy.description && (

{policy.description}

)}
Resposta: {formatMinutes(policy.timeToFirstResponse)} Resolucao: {formatMinutes(policy.timeToResolution)}
))}
)}
{/* SLA por empresa */} {/* SLA por categoria */} {/* Modal unificado de criar/editar */} !open && closeDialog()}> {editingSla ? "Editar politica de SLA" : "Nova politica de SLA"} {editingSla ? "Altere os dados da politica. Ela continua valendo para todas as empresas e categorias." : "Crie uma politica global que vale para todas as empresas e categorias. Voce pode criar regras mais especificas depois."}
setName(event.target.value)} autoFocus />