"use client" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useEditor, EditorContent } from "@tiptap/react" import type { Editor } from "@tiptap/react" import StarterKit from "@tiptap/starter-kit" import Placeholder from "@tiptap/extension-placeholder" import Mention from "@tiptap/extension-mention" import { ReactRenderer } from "@tiptap/react" import tippy, { type Instance, type Props as TippyProps } from "tippy.js" import { Braces, Trash2 } from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectEmptyState, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" // Tipos type EmailCtaTarget = "AUTO" | "PORTAL" | "STAFF" type SendEmailAction = { id: string type: "SEND_EMAIL" subject: string message: string toRequester: boolean toAssignee: boolean toUserId: string toEmails: string ctaTarget: EmailCtaTarget ctaLabel: string } type Agent = { _id: string name: string } type EmailActionConfigProps = { action: SendEmailAction onChange: (action: SendEmailAction) => void onRemove: () => void agents: Agent[] } // Variáveis disponíveis para interpolação const EMAIL_VARIABLES = [ { key: "ticket.reference", label: "Referência", description: "Número do chamado (ex: #1234)" }, { key: "ticket.subject", label: "Assunto", description: "Título do chamado" }, { key: "ticket.status", label: "Status", description: "Status atual do chamado" }, { key: "ticket.priority", label: "Prioridade", description: "Nível de prioridade" }, { key: "company.name", label: "Empresa", description: "Nome da empresa do solicitante" }, { key: "requester.name", label: "Solicitante", description: "Nome de quem abriu o chamado" }, { key: "assignee.name", label: "Responsável", description: "Nome do agente responsável" }, ] as const type EmailVariable = (typeof EMAIL_VARIABLES)[number] const CLEAR_SELECT_VALUE = "__clear__" // Estilos do badge de variável (fundo preto com texto branco) const VARIABLE_BADGE_CLASSES = "inline-flex items-center gap-1 rounded-md bg-neutral-900 border border-neutral-700 px-1.5 py-0.5 text-xs font-mono text-white whitespace-nowrap" // Extensao TipTap para variaveis de e-mail const EmailVariableMentionExtension = Mention.extend({ name: "emailVariable", priority: 1000, group: "inline", inline: true, selectable: true, atom: true, addAttributes() { return { id: { default: null }, label: { default: null }, } }, addKeyboardShortcuts() { return { Backspace: ({ editor }: { editor: Editor }) => { const { state } = editor const { selection } = state if (selection.empty) { const { $from } = selection const nodeBefore = $from.nodeBefore if (nodeBefore?.type?.name === this.name) { const from = $from.pos - nodeBefore.nodeSize const to = $from.pos editor.chain().focus().deleteRange({ from, to }).run() return true } } return false }, } }, parseHTML() { return [ { tag: `span[data-email-variable]`, getAttrs: (dom: HTMLElement | string) => { if (dom instanceof HTMLElement) { return { id: dom.dataset.variableId ?? null, label: dom.dataset.variableLabel ?? dom.textContent ?? null, } } return {} }, }, ] }, renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { const id = String(HTMLAttributes.id ?? "") const label = String(HTMLAttributes.label ?? id) return [ "span", { "data-email-variable": "true", "data-variable-id": id, "data-variable-label": label, class: VARIABLE_BADGE_CLASSES, contenteditable: "false", }, `{{${id}}}`, ] }, addNodeView() { return ({ node }) => { const dom = document.createElement("span") dom.className = VARIABLE_BADGE_CLASSES dom.contentEditable = "false" dom.dataset.emailVariable = "true" dom.dataset.variableId = String(node.attrs.id ?? "") dom.dataset.variableLabel = String(node.attrs.label ?? "") dom.textContent = `{{${node.attrs.id}}}` return { dom } } }, }).configure({ suggestion: { char: "{{", allowSpaces: false, startOfLine: false, render: () => { let component: ReactRenderer | null = null let popup: Instance | null = null let keydownHandler: ((event: KeyboardEvent) => boolean) | null = null const registerHandler = (handler: ((event: KeyboardEvent) => boolean) | null) => { keydownHandler = handler } return { onStart: (props) => { component = new ReactRenderer(EmailVariableList, { props: { ...props, onRegister: registerHandler }, editor: props.editor, }) if (!props.clientRect) return popup = tippy(document.body, { getReferenceClientRect: () => props.clientRect?.() ?? new DOMRect(), appendTo: () => document.body, content: component.element, showOnCreate: true, interactive: true, trigger: "manual", placement: "bottom-start", zIndex: 99999, }) }, onUpdate(props) { component?.updateProps({ ...props, onRegister: registerHandler }) if (!props.clientRect) return popup?.setProps({ getReferenceClientRect: () => props.clientRect?.() ?? new DOMRect(), }) }, onKeyDown(props) { if (props.event.key === "Escape") { popup?.hide() return true } if (keydownHandler?.(props.event)) { return true } return false }, onExit() { popup?.destroy() component?.destroy() keydownHandler = null }, } }, items: ({ query }) => { const q = query.toLowerCase().replace(/^\{*/, "") return EMAIL_VARIABLES.filter( (v) => v.key.toLowerCase().includes(q) || v.label.toLowerCase().includes(q) || v.description.toLowerCase().includes(q) ) }, }, }) // Lista de sugestoes de variaveis type EmailVariableListProps = { items: EmailVariable[] command: (item: { id: string; label: string }) => void onRegister?: (handler: ((event: KeyboardEvent) => boolean) | null) => void } function EmailVariableList({ items, command, onRegister }: EmailVariableListProps) { const [selectedIndex, setSelectedIndex] = useState(0) const selectItem = useCallback( (index: number) => { const item = items[index] if (item) { command({ id: item.key, label: item.label }) } }, [command, items] ) const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key === "ArrowUp") { setSelectedIndex((prev) => (prev + items.length - 1) % items.length) return true } if (event.key === "ArrowDown") { setSelectedIndex((prev) => (prev + 1) % items.length) return true } if (event.key === "Enter") { selectItem(selectedIndex) return true } return false }, [items.length, selectItem, selectedIndex] ) useEffect(() => { onRegister?.(handleKeyDown) return () => onRegister?.(null) }, [handleKeyDown, onRegister]) useEffect(() => { setSelectedIndex(0) }, [items]) if (!items.length) { return (
Nenhuma variavel encontrada
) } return (
{items.map((item, index) => ( ))}
) } // Converte HTML com badges para texto com {{variaveis}} function htmlToPlainVariables(html: string): string { if (!html) return "" // Remove tags HTML mantendo o texto const div = document.createElement("div") div.innerHTML = html // Badges de variavel ja tem o texto {{variavel}} dentro return div.textContent ?? "" } // Converte texto com {{variaveis}} para HTML com badges function plainVariablesToHtml(text: string): string { if (!text) return "" // Escapa HTML primeiro const escaped = text .replace(/&/g, "&") .replace(//g, ">") // Converte {{var}} para badges return escaped.replace(/\{\{([^}]+)\}\}/g, (_, varKey) => { const variable = EMAIL_VARIABLES.find((v) => v.key === varKey) const label = variable?.label ?? varKey return `{{${varKey}}}` }) } // Editor de mensagem com suporte a variaveis type MessageEditorProps = { value: string onChange: (value: string) => void onInsertVariable: (ref: { insertVariable: (key: string) => void } | null) => void } function MessageEditor({ value, onChange, onInsertVariable }: MessageEditorProps) { // eslint-disable-next-line react-hooks/exhaustive-deps -- initialContent deve ser calculado apenas uma vez na montagem const initialContent = useMemo(() => plainVariablesToHtml(value), []) const editor = useEditor({ extensions: [ StarterKit.configure({ bulletList: false, orderedList: false, blockquote: false, codeBlock: false, heading: false, horizontalRule: false, }), Placeholder.configure({ placeholder: "Escreva a mensagem do e-mail..." }), EmailVariableMentionExtension, ], content: initialContent, editorProps: { attributes: { class: "prose prose-sm max-w-none focus:outline-none min-h-[100px] p-3 text-sm", }, }, onUpdate({ editor }) { const html = editor.getHTML() const plain = htmlToPlainVariables(html) onChange(plain) }, immediatelyRender: false, }) // Expoe metodo para inserir variavel externamente useEffect(() => { if (!editor) { onInsertVariable(null) return } onInsertVariable({ insertVariable: (key: string) => { const variable = EMAIL_VARIABLES.find((v) => v.key === key) if (!variable) return editor .chain() .focus() .insertContent({ type: "emailVariable", attrs: { id: key, label: variable.label }, }) .run() }, }) return () => onInsertVariable(null) }, [editor, onInsertVariable]) // Sincroniza valor externo useEffect(() => { if (!editor) return const currentPlain = htmlToPlainVariables(editor.getHTML()) if (currentPlain !== value) { editor.commands.setContent(plainVariablesToHtml(value), { emitUpdate: false }) } }, [value, editor]) if (!editor) return null return (
) } // Componente principal export function EmailActionConfig({ action, onChange, onRemove, agents }: EmailActionConfigProps) { const subjectInputRef = useRef(null) const messageEditorRef = useRef<{ insertVariable: (key: string) => void } | null>(null) const [activeField, setActiveField] = useState<"subject" | "message">("message") const handleChange = useCallback( (key: K, value: SendEmailAction[K]) => { onChange({ ...action, [key]: value }) }, [action, onChange] ) const handleInsertVariable = useCallback( (variable: EmailVariable) => { if (activeField === "subject" && subjectInputRef.current) { const input = subjectInputRef.current const start = input.selectionStart ?? input.value.length const end = input.selectionEnd ?? input.value.length const varText = `{{${variable.key}}}` const newValue = input.value.slice(0, start) + varText + input.value.slice(end) handleChange("subject", newValue) // Reposiciona cursor apos a variavel requestAnimationFrame(() => { input.focus() const newPos = start + varText.length input.setSelectionRange(newPos, newPos) }) } else if (activeField === "message" && messageEditorRef.current) { messageEditorRef.current.insertVariable(variable.key) } }, [activeField, handleChange] ) return (
{/* Header com tipo e botão de remover */}
Enviar e-mail
{/* Assunto */}
handleChange("subject", e.target.value)} onFocus={() => setActiveField("subject")} placeholder="Ex.: Atualizacao do chamado #{{ticket.reference}}" />
{/* Mensagem */}
setActiveField("message")}> handleChange("message", v)} onInsertVariable={(ref) => { messageEditorRef.current = ref }} />
{/* Paleta de variáveis */}
{EMAIL_VARIABLES.map((variable) => ( {variable.description} ))}
{/* Destinatários */}
handleChange("toEmails", e.target.value)} placeholder="cliente@empresa.com, outro@dominio.com" className="bg-white" />
{/* Botão do e-mail */}
handleChange("ctaLabel", e.target.value)} placeholder="Abrir chamado" />
) }