"use client" import { useEffect, useState } from "react" import { useMutation, useQuery } from "convex/react" import { toast } from "sonner" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import type { TicketCategory } from "@/lib/schemas/category" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" const PRIORITY_ROWS = [ { value: "URGENT", label: "Crítico" }, { value: "HIGH", label: "Alta" }, { value: "MEDIUM", label: "Média" }, { value: "LOW", label: "Baixa" }, { value: "DEFAULT", label: "Sem prioridade" }, ] as const const TIME_UNITS: Array<{ value: RuleFormState["responseUnit"]; label: string; factor: number }> = [ { value: "minutes", label: "Minutos", factor: 1 }, { value: "hours", label: "Horas", factor: 60 }, { value: "days", label: "Dias", factor: 1440 }, ] const MODE_OPTIONS: Array<{ value: RuleFormState["responseMode"]; label: string }> = [ { value: "calendar", label: "Horas corridas" }, { value: "business", label: "Horas úteis" }, ] const PAUSE_STATUS_OPTIONS = [ { value: "PENDING", label: "Pendente" }, { value: "AWAITING_ATTENDANCE", label: "Em atendimento" }, { value: "PAUSED", label: "Pausado" }, ] as const const DEFAULT_RULE_STATE: RuleFormState = { responseValue: "", responseUnit: "hours", responseMode: "calendar", solutionValue: "", solutionUnit: "hours", solutionMode: "calendar", alertThreshold: 80, pauseStatuses: ["PAUSED"], } type RuleFormState = { responseValue: string responseUnit: "minutes" | "hours" | "days" responseMode: "calendar" | "business" solutionValue: string solutionUnit: "minutes" | "hours" | "days" solutionMode: "calendar" | "business" alertThreshold: number pauseStatuses: string[] } export type CategorySlaDrawerProps = { category: TicketCategory | null tenantId: string viewerId: Id<"users"> | null onClose: () => void } export function CategorySlaDrawer({ category, tenantId, viewerId, onClose }: CategorySlaDrawerProps) { const [rules, setRules] = useState>(() => buildDefaultRuleState()) const [saving, setSaving] = useState(false) const drawerOpen = Boolean(category) const canLoad = Boolean(category && viewerId) const existing = useQuery( api.categorySlas.get, canLoad ? { tenantId, viewerId: viewerId as Id<"users">, categoryId: category!.id as Id<"ticketCategories">, } : "skip" ) as { rules: Array<{ priority: string responseTargetMinutes: number | null responseMode?: string | null solutionTargetMinutes: number | null solutionMode?: string | null alertThreshold?: number | null pauseStatuses?: string[] | null }> } | undefined const saveSla = useMutation(api.categorySlas.save) useEffect(() => { if (!existing?.rules) { setRules(buildDefaultRuleState()) return } const next = buildDefaultRuleState() for (const rule of existing.rules) { const priority = rule.priority?.toUpperCase() ?? "DEFAULT" next[priority] = convertRuleToForm(rule) } setRules(next) }, [existing, category?.id]) const handleChange = (priority: string, patch: Partial) => { setRules((current) => ({ ...current, [priority]: { ...current[priority], ...patch, }, })) } const togglePause = (priority: string, status: string) => { setRules((current) => { const selected = new Set(current[priority].pauseStatuses) if (selected.has(status)) { selected.delete(status) } else { selected.add(status) } if (selected.size === 0) { selected.add("PAUSED") } return { ...current, [priority]: { ...current[priority], pauseStatuses: Array.from(selected), }, } }) } const handleSave = async () => { if (!category || !viewerId) return setSaving(true) toast.loading("Salvando SLA...", { id: "category-sla" }) try { const payload = PRIORITY_ROWS.map((row) => { const form = rules[row.value] return { priority: row.value, responseTargetMinutes: convertToMinutes(form.responseValue, form.responseUnit), responseMode: form.responseMode, solutionTargetMinutes: convertToMinutes(form.solutionValue, form.solutionUnit), solutionMode: form.solutionMode, alertThreshold: Math.min(Math.max(form.alertThreshold, 5), 95) / 100, pauseStatuses: form.pauseStatuses, } }) await saveSla({ tenantId, actorId: viewerId, categoryId: category.id as Id<"ticketCategories">, rules: payload, }) toast.success("SLA atualizado", { id: "category-sla" }) onClose() } catch (error) { console.error(error) toast.error("Não foi possível salvar as regras de SLA.", { id: "category-sla" }) } finally { setSaving(false) } } return ( { if (!open) { onClose() } }} > Configurar SLA — {category?.name ?? ""} Defina metas de resposta e resolução para cada prioridade. Os prazos em horas úteis consideram apenas segunda a sexta, das 8h às 18h.
{PRIORITY_ROWS.map((row) => { const form = rules[row.value] return (

{row.label}

{row.value === "DEFAULT" ? "Aplicado quando o ticket não tem prioridade definida." : "Aplica-se aos tickets desta prioridade."}

handleChange(row.value, { responseValue: value })} onUnitChange={(value) => handleChange(row.value, { responseUnit: value as RuleFormState["responseUnit"] }) } onModeChange={(value) => handleChange(row.value, { responseMode: value as RuleFormState["responseMode"] }) } /> handleChange(row.value, { solutionValue: value })} onUnitChange={(value) => handleChange(row.value, { solutionUnit: value as RuleFormState["solutionUnit"] }) } onModeChange={(value) => handleChange(row.value, { solutionMode: value as RuleFormState["solutionMode"] }) } />

Alertar quando

handleChange(row.value, { alertThreshold: Number(event.target.value) || 0 })} /> % do tempo for consumido.

Estados que pausam

{PAUSE_STATUS_OPTIONS.map((option) => { const selected = form.pauseStatuses.includes(option.value) return ( ) })}
) })}
) } function buildDefaultRuleState() { return PRIORITY_ROWS.reduce>((acc, row) => { acc[row.value] = { ...DEFAULT_RULE_STATE } return acc }, {}) } function convertRuleToForm(rule: { priority: string responseTargetMinutes: number | null responseMode?: string | null solutionTargetMinutes: number | null solutionMode?: string | null alertThreshold?: number | null pauseStatuses?: string[] | null }): RuleFormState { const response = minutesToForm(rule.responseTargetMinutes) const solution = minutesToForm(rule.solutionTargetMinutes) return { responseValue: response.amount, responseUnit: response.unit, responseMode: (rule.responseMode ?? "calendar") as RuleFormState["responseMode"], solutionValue: solution.amount, solutionUnit: solution.unit, solutionMode: (rule.solutionMode ?? "calendar") as RuleFormState["solutionMode"], alertThreshold: Math.round((rule.alertThreshold ?? 0.8) * 100), pauseStatuses: rule.pauseStatuses && rule.pauseStatuses.length > 0 ? rule.pauseStatuses : ["PAUSED"], } } function minutesToForm(input?: number | null) { if (!input || input <= 0) { return { amount: "", unit: "hours" as RuleFormState["responseUnit"] } } 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" as RuleFormState["responseUnit"] } } function convertToMinutes(value: string, unit: RuleFormState["responseUnit"]) { const numeric = Number(value) if (!Number.isFinite(numeric) || numeric <= 0) { return undefined } const factor = TIME_UNITS.find((item) => item.value === unit)?.factor ?? 1 return Math.round(numeric * factor) } type SlaInputGroupProps = { title: string amount: string unit: RuleFormState["responseUnit"] mode: RuleFormState["responseMode"] onAmountChange: (value: string) => void onUnitChange: (value: string) => void onModeChange: (value: string) => void } function SlaInputGroup({ title, amount, unit, mode, onAmountChange, onUnitChange, onModeChange }: SlaInputGroupProps) { return (

{title}

onAmountChange(event.target.value)} placeholder="0" />
) }