feat(documents): editor de texto rico (Tiptap) para templates de documento

Substitui o textarea de texto puro por um editor visual real, seguindo o
print de referência do usuário do editor "Contrato de Software" do
OrçaFácil legado: toolbar completa (negrito/itálico/sublinhado/tachado,
alinhamento, listas, tabela, imagem, quebra de página), variáveis inseridas
como chip inline via catálogo fechado (nunca {{texto livre}}), painel
"Variáveis usadas neste modelo" (com badge obrigatório ao vivo) e
"Histórico de versões".

Backend: editor-content-renderer.ts e merge-engine.ts portados/reescritos
a partir do renderEditorContent.js/variableRegistry.js reais do legado
(achados em .tmp/orcafacil-reference) — resolve a árvore Tiptap
diretamente em vez de regex sobre HTML cacheado. Novo catálogo de
variáveis + GET /documents/variable-catalog.

Fora de escopo desta leva (documentado em docs/progress.md): nó
`conditional`, repetição de linha de tabela, bloqueio por variável
obrigatória ausente, e auto-resolução de dados reais do cadastro na hora
de gerar o documento.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 19:49:42 -03:00
parent 553c0d9533
commit 1545666f17
20 changed files with 1999 additions and 312 deletions

View File

@@ -851,3 +851,65 @@ Novo botão "Pré-visualizar (PDF)" ao lado de "Salvar rascunho". **Decisão de
### Verificação
`pnpm turbo run typecheck build` 28/28. Suíte 115/115 (113→115: 1 teste cobrindo o preview de rascunho — variáveis viram placeholder, CSS aplicado, nada persiste —, 1 confirmando que `css_content` agora chega no `renderPdf` real da geração). Testado ao vivo contra Chromium de verdade (não mockado): PDF real de 1 página gerado sem nunca ter salvo um rascunho, texto extraído via `pdftotext` confirmando "Prezado(a) [nome_cliente], seu plano é [plano]." e a formatação CSS (cor/tamanho de fonte) presente no stream do PDF. A renderização visual do PDF dentro do iframe não pôde ser confirmada por screenshot (visualizador nativo de PDF do Chromium não aparece em capturas de tela no modo headless do Playwright — mesma limitação de teste automatizado já registrada antes nesta sessão), mas o mecanismo (blob correto, `src` do iframe apontando pro blob certo, bytes do PDF válidos e corretos) foi confirmado de forma independente.
---
## Editor de texto rico para Templates de Documentos (Tiptap)
Data: 2026-09-19. Usuário mandou o print do editor de "Contrato de Software" do OrçaFácil legado
(`/home/handix/editortexto.png`) como referência de como queria a tela de edição de rascunho de
Documentos: header com título/badge de versão + ações, tabs "Editor" / "Visualizar", toolbar rica
(parágrafo/fonte, negrito/itálico/sublinhado/tachado, alinhamento, listas, tabela, imagem, quebra de
página), variáveis inseridas como **chip inline** (nunca `{{texto livre}}`), painel "Variáveis usadas
neste modelo" (com "obrigatório") e "Histórico de versões". Escolheu ir direto pro nível completo.
Até aqui o "editor" era um `<textarea>` puro com `{{variavel}}` digitado à mão — um stand-in documentado
no próprio código/migration (`editor-content-renderer.ts`/`merge-engine.ts`), esperando o "editor visual
real" que essa leva implementa. O banco (`document_template_versions.editor_content` JSONB) já estava
desenhado pra uma árvore Tiptap real (`EMPTY_DOC_CONTENT` já nascia `{type:'doc', content:[...]}`) — sem
migration nova.
Achado o código-fonte real do legado em `.tmp/orcafacil-reference/server/src/lib/documentTemplates/`
(`renderEditorContent.js` + `variableRegistry.js`) — portados fielmente em vez de reinventados.
**Fora de escopo** (documentado de propósito, pra não estourar o pedido — a tela do editor, não o motor de
geração inteiro): nó `conditional` e repetição de linha de tabela (`tableRow.repeat`); bloqueio HTTP 422
por variável obrigatória ausente (mantém o comportamento permissivo já existente — o badge "obrigatório" é
só informativo); auto-resolução de `cliente.*`/`oferta.*`/`contrato.*` a partir do banco na hora de GERAR
o documento (`GenerationFormPage.tsx` continua pedindo cada valor manualmente, como já era); allowlist
completa de sanitização de HTML; cabeçalho/rodapé de PDF.
### Backend
`editor-content-renderer.ts` reescrito: `renderToHtml(doc, resolveMergeField?)` + `collectMergeFieldKeys`
cobrindo paragraph/heading/bulletList/orderedList/listItem/image/pageBreak/table+thead-real/marks
bold-italic-underline-strike-fontSize/mergeField (chip visual sem `resolveMergeField`, valor resolvido
com). `merge-engine.ts` muda de contrato: em vez de regex sobre `html_content` cacheado, `resolveDocument
(editorContent, context)` resolve a árvore direto — mais fiel ao legado e mais simples. Novo
`variable-catalog.ts` com o catálogo fechado de variáveis (portado do legado, só `scope: 'document'`) +
`GET /v1/documents/variable-catalog`. Novo teste unitário puro `editor-content-renderer.spec.ts` (8 casos,
finalmente cumprindo o que o docblock do merge-engine prometia desde uma leva anterior e nunca existiu).
### Frontend
Tiptap (`@tiptap/react` + extensões table/image/text-align/text-style/underline, `^3.29.2`, mesma major do
legado) adicionado ao core-web. Nó customizado `mergeField` (chip visual via NodeView React) e `pageBreak`
(régua tracejada). `EditorToolbar.tsx` fiel ao print + `VariablePickerMenu.tsx` (menu por categoria, sem
componente de dropdown pronto em `packages/ui` — construído ali mesmo). `TemplateDetailPage.tsx`: quando
há rascunho aberto, a tela vira o layout do print (header com título/badge/ações, tabs "Editor / com
variáveis" / "Pré-visualização (PDF)" — nome ajustado do print pra não prometer dado real de
cliente/revenda, já que só mostra `[chave]" —, painel "Variáveis usadas neste modelo" com badge obrigatório
ao vivo, "Histórico de versões"). `GenerationFormPage.tsx` ajustado pra extrair variáveis da árvore real
em vez de regex sobre `plainText` (que deixou de existir).
### Verificação
`pnpm turbo run typecheck build` 28/28. Suíte 123/123 (115→123: 8 novos do renderer). Testado ao vivo
(Playwright + curl): criado template novo, aberto o editor, digitado texto com negrito, inseridas duas
variáveis (`cliente.razao_social` obrigatória, `cliente.cnpj` obrigatória) via o menu por categoria —
confirmado que o painel "Variáveis usadas" atualiza em tempo real com checkmark + badge "obrigatório".
Salvo o rascunho e confirmado via `GET /documents/templates/:id` que `editor_content` persistiu a árvore
Tiptap real (mark `bold` + dois nós `mergeField` com `key`/`label` corretos) e `html_content` já teria o
`<strong>`/chip visual certos. Pré-visualização em PDF gerada ao vivo contra Chromium real (não mockado) e
confirmada via `pdftotext`: `"Prezado(a) cliente, segue o contrato. [cliente.razao_social][cliente.cnpj]"`.
Publicado o rascunho e confirmado que a versão publicada aparece corretamente na tela de overview (chip
"Razão Social"/"CNPJ/CPF" renderizado com o estilo `.merge-field-chip` aplicado). Template de teste
(`qa_test_editor_*`) desativado ao final via `UPDATE document_templates SET active=false` (sem endpoint de
exclusão, mesmo padrão de higiene de dados de teste já seguido nesta sessão).