"use client" import { useEffect, useMemo, useState } from "react" import { useMutation, useQuery } from "convex/react" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Spinner } from "@/components/ui/spinner" import { RichTextEditor, sanitizeEditorHtml } from "@/components/ui/rich-text-editor" import { toast } from "sonner" type ClosingTemplate = { id: string; title: string; body: string } const DEFAULT_PHONE_NUMBER = "(11) 4173-5368" const DEFAULT_CLOSING_TEMPLATES: ClosingTemplate[] = [ { id: "default-standard", title: "Encerramento padrão", body: sanitizeEditorHtml(`

Olá {{cliente}},

A equipe da Raven agradece o contato. Este ticket está sendo encerrado.

Se surgirem novas questões, você pode reabrir o ticket em até 7 dias ou nos contatar pelo número ${DEFAULT_PHONE_NUMBER}. Obrigado.

👍 👀 🙌
Gabriel Henrique · Raven

`), }, { id: "default-no-contact", title: "Tentativa de contato sem sucesso", body: sanitizeEditorHtml(`

Prezado(a) {{cliente}},

Realizamos uma tentativa de contato, mas não obtivemos sucesso.

Por favor, retorne assim que possível para seguirmos com as verificações necessárias.

Este ticket será encerrado após 3 tentativas realizadas sem sucesso.

Telefone para contato: ${DEFAULT_PHONE_NUMBER}.

👍 👀 🙌
Gabriel Henrique · Raven

`), }, { id: "default-closed-after-attempts", title: "Encerramento após 3 tentativas", body: sanitizeEditorHtml(`

Prezado(a) {{cliente}},

Esse ticket está sendo encerrado pois realizamos 3 tentativas sem retorno.

Você pode reabrir este ticket em até 7 dias ou entrar em contato pelo telefone ${DEFAULT_PHONE_NUMBER} quando preferir.

👍 👀 🙌
Gabriel Henrique · Raven

`), }, ] function applyTemplatePlaceholders(html: string, customerName?: string | null) { const normalizedName = customerName?.trim() const fallback = normalizedName && normalizedName.length > 0 ? normalizedName : "cliente" return html.replace(/{{\s*(cliente|customer|customername|nome|nomecliente)\s*}}/gi, fallback) } export function CloseTicketDialog({ open, onOpenChange, ticketId, tenantId, actorId, requesterName, onSuccess, }: { open: boolean onOpenChange: (open: boolean) => void ticketId: string tenantId: string actorId: Id<"users"> | null requesterName?: string | null onSuccess: () => void }) { const updateStatus = useMutation(api.tickets.updateStatus) const addComment = useMutation(api.tickets.addComment) const closingTemplates = useQuery( actorId && open ? api.commentTemplates.list : "skip", actorId && open ? { tenantId, viewerId: actorId, kind: "closing" as const } : "skip" ) as { id: string; title: string; body: string }[] | undefined const templatesLoading = Boolean(actorId && open && closingTemplates === undefined) const templates = useMemo(() => { if (closingTemplates && closingTemplates.length > 0) { return closingTemplates.map((t) => ({ id: t.id, title: t.title, body: t.body })) } return DEFAULT_CLOSING_TEMPLATES }, [closingTemplates]) const [selectedTemplateId, setSelectedTemplateId] = useState(null) const [message, setMessage] = useState("") const [isSubmitting, setIsSubmitting] = useState(false) useEffect(() => { if (!open) { setSelectedTemplateId(null) setMessage("") setIsSubmitting(false) return } if (templates.length > 0 && !selectedTemplateId && !message) { const first = templates[0] const hydrated = sanitizeEditorHtml(applyTemplatePlaceholders(first.body, requesterName)) setSelectedTemplateId(first.id) setMessage(hydrated) } }, [open, templates, requesterName, selectedTemplateId, message]) const handleTemplateSelect = (template: ClosingTemplate) => { setSelectedTemplateId(template.id) const filled = sanitizeEditorHtml(applyTemplatePlaceholders(template.body, requesterName)) setMessage(filled) } const handleSubmit = async () => { if (!actorId) { toast.error("É necessário estar autenticado para encerrar o ticket.") return } setIsSubmitting(true) toast.loading("Encerrando ticket...", { id: "close-ticket" }) try { await updateStatus({ ticketId: ticketId as unknown as Id<"tickets">, status: "RESOLVED", actorId }) const withPlaceholders = applyTemplatePlaceholders(message, requesterName) const sanitized = sanitizeEditorHtml(withPlaceholders) const hasContent = sanitized.replace(/<[^>]*>/g, "").trim().length > 0 if (hasContent) { await addComment({ ticketId: ticketId as unknown as Id<"tickets">, authorId: actorId, visibility: "PUBLIC", body: sanitized, attachments: [], }) } toast.success("Ticket encerrado com sucesso!", { id: "close-ticket" }) onOpenChange(false) onSuccess() } catch (error) { console.error(error) toast.error("Não foi possível encerrar o ticket.", { id: "close-ticket" }) } finally { setIsSubmitting(false) } } return ( Encerrar ticket Confirme a mensagem de encerramento que será enviada ao cliente. Você pode personalizar o texto antes de concluir.

Modelos rápidos

{templatesLoading ? (
Carregando templates...
) : (
{templates.map((template) => ( ))}
)}

Use {"{{cliente}}"} dentro do template para inserir automaticamente o nome do solicitante.

Mensagem de encerramento

Você pode editar o conteúdo antes de enviar. Deixe em branco para encerrar sem comentário adicional.

O comentário será público e ficará registrado no histórico do ticket.
) }