diff --git a/src/components/tickets/close-ticket-dialog.tsx b/src/components/tickets/close-ticket-dialog.tsx index 3c974df..2ae6d4f 100644 --- a/src/components/tickets/close-ticket-dialog.tsx +++ b/src/components/tickets/close-ticket-dialog.tsx @@ -1,58 +1,66 @@ "use client" -import { useEffect, useMemo, useState } from "react" +import { useCallback, 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 { RichTextEditor, sanitizeEditorHtml, stripLeadingEmptyParagraphs } 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_COMPANY_NAME = "Rever Tecnologia" + +const sanitizeTemplate = (html: string) => stripLeadingEmptyParagraphs(sanitizeEditorHtml(html.trim())) const DEFAULT_CLOSING_TEMPLATES: ClosingTemplate[] = [ { id: "default-standard", title: "Encerramento padrão", - body: sanitizeEditorHtml(` + body: sanitizeTemplate(`

Olá {{cliente}},

-

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

+

A equipe da ${DEFAULT_COMPANY_NAME} 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

+

{{agente}} · ${DEFAULT_COMPANY_NAME}

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

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

+

{{agente}} · ${DEFAULT_COMPANY_NAME}

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

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

+

{{agente}} · ${DEFAULT_COMPANY_NAME}

`), }, ] -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) +function applyTemplatePlaceholders(html: string, customerName?: string | null, agentName?: string | null) { + const normalizedCustomer = customerName?.trim() + const customerFallback = normalizedCustomer && normalizedCustomer.length > 0 ? normalizedCustomer : "cliente" + const normalizedAgent = agentName?.trim() + const agentFallback = normalizedAgent && normalizedAgent.length > 0 ? normalizedAgent : "Equipe Rever" + return html + .replace(/{{\s*(cliente|customer|customername|nome|nomecliente)\s*}}/gi, customerFallback) + .replace(/{{\s*(agente|agent|atendente|responsavel|usu[aá]rio|usuario)\s*}}/gi, agentFallback) + .replace(/{{\s*(empresa|company|companhia)\s*}}/gi, DEFAULT_COMPANY_NAME) } export function CloseTicketDialog({ @@ -62,6 +70,7 @@ export function CloseTicketDialog({ tenantId, actorId, requesterName, + agentName, onSuccess, }: { open: boolean @@ -70,6 +79,7 @@ export function CloseTicketDialog({ tenantId: string actorId: Id<"users"> | null requesterName?: string | null + agentName?: string | null onSuccess: () => void }) { const updateStatus = useMutation(api.tickets.updateStatus) @@ -92,6 +102,11 @@ export function CloseTicketDialog({ const [message, setMessage] = useState("") const [isSubmitting, setIsSubmitting] = useState(false) + const hydrateTemplateBody = useCallback((templateHtml: string) => { + const withPlaceholders = applyTemplatePlaceholders(templateHtml, requesterName, agentName) + return stripLeadingEmptyParagraphs(sanitizeEditorHtml(withPlaceholders)) + }, [requesterName, agentName]) + useEffect(() => { if (!open) { setSelectedTemplateId(null) @@ -101,16 +116,15 @@ export function CloseTicketDialog({ } if (templates.length > 0 && !selectedTemplateId && !message) { const first = templates[0] - const hydrated = sanitizeEditorHtml(applyTemplatePlaceholders(first.body, requesterName)) + const hydrated = hydrateTemplateBody(first.body) setSelectedTemplateId(first.id) setMessage(hydrated) } - }, [open, templates, requesterName, selectedTemplateId, message]) + }, [open, templates, selectedTemplateId, message, hydrateTemplateBody]) const handleTemplateSelect = (template: ClosingTemplate) => { setSelectedTemplateId(template.id) - const filled = sanitizeEditorHtml(applyTemplatePlaceholders(template.body, requesterName)) - setMessage(filled) + setMessage(hydrateTemplateBody(template.body)) } const handleSubmit = async () => { @@ -122,8 +136,8 @@ export function CloseTicketDialog({ 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 withPlaceholders = applyTemplatePlaceholders(message, requesterName, agentName) + const sanitized = stripLeadingEmptyParagraphs(sanitizeEditorHtml(withPlaceholders)) const hasContent = sanitized.replace(/<[^>]*>/g, "").trim().length > 0 if (hasContent) { await addComment({ diff --git a/src/components/tickets/status-select.tsx b/src/components/tickets/status-select.tsx index e247fc2..31d1102 100644 --- a/src/components/tickets/status-select.tsx +++ b/src/components/tickets/status-select.tsx @@ -1,8 +1,6 @@ "use client" -import { useEffect, useMemo, useState } from "react" -import { useMutation, useQuery } from "convex/react" -import { api } from "@/convex/_generated/api" +import { useEffect, useState } from "react" import type { Id } from "@/convex/_generated/dataModel" import type { TicketStatus } from "@/lib/schemas/ticket" import { useAuth } from "@/lib/auth-client" @@ -12,7 +10,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip import { CheckCircle2 } from "lucide-react" import { CloseTicketDialog } from "@/components/tickets/close-ticket-dialog" import { cn } from "@/lib/utils" -import { sanitizeEditorHtml } from "@/components/ui/rich-text-editor" type StatusKey = TicketStatus | "NEW" | "OPEN" | "ON_HOLD"; @@ -29,54 +26,6 @@ const statusStyles: Record = { const baseBadgeClass = "inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 px-3 text-sm font-semibold transition hover:border-slate-300" -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 StatusSelect({ ticketId, @@ -91,8 +40,16 @@ export function StatusSelect({ requesterName?: string | null showCloseButton?: boolean }) { - const { convexUserId } = useAuth() + const { convexUserId, session, machineContext } = useAuth() const actorId = (convexUserId ?? null) as Id<"users"> | null + const sessionName = session?.user?.name?.trim() + const machineAssignedName = machineContext?.assignedUserName?.trim() + const agentName = + sessionName && sessionName.length > 0 + ? sessionName + : machineAssignedName && machineAssignedName.length > 0 + ? machineAssignedName + : null const [status, setStatus] = useState(value) const [closeDialogOpen, setCloseDialogOpen] = useState(false) @@ -136,6 +93,7 @@ export function StatusSelect({ tenantId={tenantId} actorId={actorId} requesterName={requesterName} + agentName={agentName} onSuccess={() => { setStatus("RESOLVED") setCloseDialogOpen(false) diff --git a/src/components/tickets/ticket-comments.rich.tsx b/src/components/tickets/ticket-comments.rich.tsx index 7db871c..d85e2ea 100644 --- a/src/components/tickets/ticket-comments.rich.tsx +++ b/src/components/tickets/ticket-comments.rich.tsx @@ -16,7 +16,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { toast } from "sonner" import { Dropzone } from "@/components/ui/dropzone" -import { RichTextEditor, RichTextContent, sanitizeEditorHtml } from "@/components/ui/rich-text-editor" +import { RichTextEditor, RichTextContent, sanitizeEditorHtml, stripLeadingEmptyParagraphs } from "@/components/ui/rich-text-editor" import { Dialog, DialogClose, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select" @@ -71,11 +71,12 @@ export function TicketComments({ ticket }: TicketCommentsProps) { const canUseTemplates = Boolean(convexUserId && isStaff) const insertTemplateIntoBody = (html: string) => { - const sanitized = sanitizeEditorHtml(html) + const sanitizedTemplate = stripLeadingEmptyParagraphs(sanitizeEditorHtml(html)) setBody((current) => { - if (!current) return sanitized - const merged = `${current}


${sanitized}` - return sanitizeEditorHtml(merged) + if (!sanitizedTemplate) return current + if (!current) return sanitizedTemplate + const merged = `${current}


${sanitizedTemplate}` + return stripLeadingEmptyParagraphs(sanitizeEditorHtml(merged)) }) } diff --git a/src/components/tickets/ticket-summary-header.tsx b/src/components/tickets/ticket-summary-header.tsx index e7f5454..52f7e84 100644 --- a/src/components/tickets/ticket-summary-header.tsx +++ b/src/components/tickets/ticket-summary-header.tsx @@ -95,10 +95,18 @@ function formatDuration(durationMs: number) { } export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { - const { convexUserId, role, isStaff } = useAuth() + const { convexUserId, role, isStaff, session, machineContext } = useAuth() const normalizedRole = (role ?? "").toLowerCase() const isManager = normalizedRole === "manager" const isAdmin = normalizedRole === "admin" + const sessionName = session?.user?.name?.trim() + const machineAssignedName = machineContext?.assignedUserName?.trim() + const agentName = + sessionName && sessionName.length > 0 + ? sessionName + : machineAssignedName && machineAssignedName.length > 0 + ? machineAssignedName + : null useDefaultQueues(ticket.tenantId) const changeAssignee = useMutation(api.tickets.changeAssignee) const changeQueue = useMutation(api.tickets.changeQueue) @@ -621,7 +629,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {