Pular para o conteúdo principal

Editor de templates de e-mail — UI e integração com o backend

Guia para replicar a tela de edição de templates de e-mail (como a do Módulo 1 — e-mail de boas-vindas) noutro frontend ou sistema, consumindo as APIs deste backend.

Nota: o backend não envia e-mail (sem SMTP). Expõe apenas leitura, gravação e renderização de assunto + corpo. O envio real fica a cargo do cliente ou de outro serviço.

Documentação complementar de referência API: FRONTEND_EMAIL_TEMPLATES.md.


1. O que a tela faz

A interface é um editor de template por módulo. Cada módulo corresponde a um tipo de e-mail transacional. O utilizador edita texto com placeholders {{chave}}, grava na empresa e pode pré-visualizar o resultado renderizado.

1.1 Componentes da UI (exemplo: boas-vindas)

Área da telaComportamentoOrigem dos dados
Título — ex.: «Módulo 1 — e-mail de boas-vindas»Label fixo no frontend; identifica o tipo de templateConfiguração local do frontend (ver tabela §2)
Badge «Padrão do sistema»Visível quando isDefault === trueCampo isDefault do GET
Descrição — ex.: «Modelo de e-mail enviado ao novo utilizador…»Texto explicativo fixoFrontend (i18n / config)
Campo AssuntoInput de linha única, max 200 caracteresdata.assunto do GET
Contador assunto — ex.: 29 / 200 caracteresValidação client-side antes do PATCHLimite por tipo (§2)
Campo CorpoTextarea multilinha, max 5000 (ou 20000 para proposta)data.corpo do GET
Contador corpo — ex.: 86 / 5000 caracteresValidação client-sideLimite por tipo (§2)
Dica de placeholders«Use placeholders como {{nome_usuario}}…»Texto fixo + lista dinâmica
Chips «Placeholders disponíveis»Botões clicáveis que inserem {{chave}} no corpo (ou assunto)data.placeholders[] do GET
Guardar templateEnvia PATCH com assunto e/ou corpoAPI §3.2
Pré-visualizarChama render com dados de exemplo e mostra modal/painelAPI §3.3

1.2 Estado local recomendado (React / Vue / Angular)

interface EmailTemplateEditorState {
assunto: string;
corpo: string;
isDefault: boolean;
placeholders: string[]; // sem {{ }}
updatedAt: string | null;
updatedBy: string | null;
isLoading: boolean;
isSaving: boolean;
saveError: string | null;
isDirty: boolean; // alterações não gravadas
}

1.3 Fluxo de dados

sequenceDiagram
participant UI as Editor (frontend)
participant API as Backend CRM

UI->>API: GET /companies/{uuid}/email-templates/welcome
API-->>UI: assunto, corpo, isDefault, placeholders

Note over UI: Utilizador edita campos / clica chips

UI->>API: PATCH /companies/{uuid}/email-templates/welcome
API-->>UI: template atualizado (isDefault=false)

UI->>API: GET .../welcome/render?nomeUsuario=...&linkPrimeiroAcesso=...
API-->>UI: assunto e corpo finais (sem {{ }})
Note over UI: Modal de preview ou envio via SMTP externo

2. Os cinco módulos e mapeamento API

O frontend numera os módulos (1–5); o backend usa templateKey na URL.

Módulo UITítulo sugeridotemplateKey (URL)Chave em company.metadataAssunto maxCorpo max
1E-mail de boas-vindaswelcomeemailTemplates.welcome2005 000
2Reset de senhapassword-resetemailTemplates.passwordReset2005 000
3Primeiro acessofirst-accessemailTemplates.firstAccess2005 000
4Proposta comercialproposalemailTemplates.proposal20020 000
5Envio de documentodocument-deliveryemailTemplates.documentDelivery2005 000

Prefixo comum por empresa:

/api/companies/{companyUuid}/email-templates/{templateKey}

Substitua {companyUuid} pelo UUID da empresa (do JWT ou contexto da sessão).


3. Chamadas HTTP (detalhe por operação)

3.0 Autenticação e envelope

ItemValor
Base URLhttp://localhost:3000 (ou ambiente)
Prefixo/api
HeaderAuthorization: Bearer <sessionHash>
Content-Type (PATCH/POST)application/json

Sucesso:

{
"status": "success",
"message": "Mensagem descritiva",
"data": { }
}

Erro:

{
"status": "error",
"message": "Descrição do erro"
}

Permissões: perfis N0 / admin / super_admin acedem a qualquer empresa; os restantes só à empresa do JWT.


3.1 Carregar o formulário — GET /{templateKey}

Exemplo (Módulo 1):

GET /api/companies/63d884d1-773a-4ced-b51c-9ba57d04ead6/email-templates/welcome
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Resposta data:

{
"assunto": "Bem-vindo à {{nome_empresa}}!",
"corpo": "Olá {{nome_usuario}}, sua conta foi criada. Acesse pelo link: {{link_primeiro_acesso}}",
"isDefault": true,
"placeholders": ["nome_empresa", "nome_usuario", "link_primeiro_acesso"],
"updated_at": null,
"updated_by": null
}

Implementação no editor:

  1. Ao montar a página, chamar GET com companyUuid + templateKey.
  2. Preencher assunto e corpo.
  3. Renderizar chips a partir de placeholders → mostrar {{nome_empresa}}, etc.
  4. Se isDefault === true, exibir badge «Padrão do sistema».

3.2 Guardar — PATCH /{templateKey}

Exemplo:

PATCH /api/companies/63d884d1-773a-4ced-b51c-9ba57d04ead6/email-templates/welcome
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
"assunto": "Bem-vindo à {{nome_empresa}}!",
"corpo": "Olá {{nome_usuario}}, acesse: {{link_primeiro_acesso}}"
}

Regras:

  • Body parcial: envie assunto, corpo ou ambos (pelo menos um).
  • Strings não vazias após trim.
  • Respeitar limites de caracteres (§2).
  • Só placeholders listados em placeholders são aceites; outros → 400.

Resposta: mesmo formato do GET; após customização, isDefault passa a false e updated_at / updated_by são preenchidos.

Validação client-side (espelhar o backend):

const PLACEHOLDER_REGEX = /\{\{[^}]*\}\}/g;

function findInvalidPlaceholders(text: string, allowed: string[]): string[] {
const invalid = new Set<string>();
for (const match of text.matchAll(PLACEHOLDER_REGEX)) {
const inner = match[0].slice(2, -2).trim();
if (!allowed.includes(inner)) invalid.add(match[0]);
}
return [...invalid];
}

3.3 Pré-visualizar — GET ou POST .../render

Devolve assunto e corpo sem placeholders, prontos para envio ou preview.

Módulo 1 — welcome (GET)

GET /api/companies/{companyUuid}/email-templates/welcome/render?nomeUsuario=Maria%20Silva&linkPrimeiroAcesso=https%3A%2F%2Fapp.exemplo.com%2Fprimeiro-acesso
Authorization: Bearer ...
Query paramPlaceholderQuem fornece
nomeUsuario{{nome_usuario}}Frontend (valor de teste no preview)
linkPrimeiroAcesso{{link_primeiro_acesso}}Frontend
{{nome_empresa}}Backend (nome fantasia / razão social da empresa)

Resposta:

{
"status": "success",
"message": "Template de boas-vindas renderizado com sucesso",
"data": {
"assunto": "Bem-vindo à Acme Pagamentos!",
"corpo": "Olá Maria Silva, sua conta foi criada. Acesse pelo link: https://app.exemplo.com/primeiro-acesso"
}
}

UI de preview: modal com assunto + corpo renderizados; use dados fictícios nos query params.

Módulo 2 — password-reset (GET)

GET .../password-reset/render?link=https%3A%2F%2F...&codigo=482913

Placeholders: link, codigo (ambos na query).

Módulo 3 — first-access (GET)

GET .../first-access/render?nome=Maria%20Souza
QueryPlaceholderOrigem
nome{{nome}}Frontend
{{link_acesso}}Backend (https://{slug}.gevtech.com.br)

Se o tenant não tiver slug404.

Módulo 4 — proposal (POST)

Muitos campos → usa POST com body:

POST /api/companies/{companyUuid}/email-templates/proposal/render
Content-Type: application/json

{
"values": {
"nome_cliente": "Maria Silva",
"representante": "João Souza",
"cpf_cnpj": "12.345.678/0001-90",
"tipo_pessoa": "Pessoa Jurídica",
"telefone": "(11) 90000-0000",
"email": "maria@empresa.com",
"cnae": "4751-2/01",
"mcc": "5732",
"data_simulacao": "12/06/2026",
"taxa_antecipacao": "1,99%",
"taxa_pix": "0,99%",
"tabela_taxas_configuradas": "Visa | 1,20% | ...",
"tabela_cet": "1x | 2,5% | ...",
"telefone_representante": "(11) 91111-1111",
"email_representante": "joao@empresa.com",
"marca": "Acme Pagamentos",
"ano": "2026"
}
}

Chaves em values = placeholders sem {{ }}. Todas as chaves presentes no template devem ter valor.

Módulo 5 — document-delivery (GET)

GET .../document-delivery/render?nomeSolicitante=Maria&email=maria%40cliente.com&linkAprovacao=https%3A%2F%2F...

Placeholders: nome_solicitante, email, link_aprovacao.


4. Placeholders por módulo (referência para chips)

Formato no editor: {{chave}}. A lista exata vem sempre do GET (data.placeholders).

Módulo 1 — welcome

PlaceholderUso
{{nome_empresa}}Nome da empresa (resolvido no render pelo backend)
{{nome_usuario}}Nome do novo utilizador
{{link_primeiro_acesso}}URL de primeiro acesso / definição de senha

Módulo 2 — password-reset

PlaceholderQuery no render
{{link}}link
{{codigo}}codigo

Módulo 3 — first-access

PlaceholderOrigem
{{nome}}Query nome
{{link_acesso}}Backend (slug do tenant)

Módulo 4 — proposal

17 placeholders: nome_cliente, representante, cpf_cnpj, tipo_pessoa, telefone, email, cnae, mcc, data_simulacao, taxa_antecipacao, taxa_pix, tabela_taxas_configuradas, tabela_cet, telefone_representante, email_representante, marca, ano.

tabela_taxas_configuradas e tabela_cet podem ser multilinha ou HTML — o frontend monta o texto antes do render.

Módulo 5 — document-delivery

PlaceholderQuery no render
{{nome_solicitante}}nomeSolicitante
{{email}}email
{{link_aprovacao}}linkAprovacao

5. Comportamentos de UI a implementar

5.1 Inserção de placeholder (chips)

Ao clicar num chip {{nome_usuario}}:

  1. Inserir no corpo na posição do cursor (ou no fim se não houver seleção).
  2. Opcional: permitir inserção no assunto com segundo clique ou menu.
  3. Não validar duplicados — o backend aceita o mesmo placeholder várias vezes.

5.2 Contadores de caracteres

const limits = {
welcome: { assunto: 200, corpo: 5000 },
"password-reset": { assunto: 200, corpo: 5000 },
"first-access": { assunto: 200, corpo: 5000 },
proposal: { assunto: 200, corpo: 20000 },
"document-delivery": { assunto: 200, corpo: 5000 },
};

Desabilitar «Guardar» se assunto.length > max ou corpo.length > max, ou se campos estiverem vazios.

5.3 Badge «Padrão do sistema»

{isDefault && <Badge>Padrão do sistema</Badge>}

Desaparece após o primeiro PATCH bem-sucedido (isDefault: false).

5.4 Botão Guardar

  • Enviar PATCH apenas com campos alterados (opcional) ou sempre ambos.
  • Loading state durante o pedido.
  • Em sucesso: atualizar estado local com data da resposta; isDirty = false.
  • Em 400: mostrar message (ex.: placeholders não suportados).

5.5 Botão Pré-visualizar

  1. Validar localmente (limites + placeholders).
  2. Chamar endpoint /render com dados de exemplo (hardcoded ou formulário «dados de teste»).
  3. Mostrar modal com data.assunto e data.corpo renderizados.
  4. Não persiste alterações — usa o texto atual do formulário apenas se o backend renderizar a partir do template já gravado.
    Importante: o render usa o template persistido na empresa, não o rascunho local. Se o utilizador editou mas não guardou, o preview reflete a versão antiga. Opções:
    • Forçar guardar antes do preview, ou
    • Avisar «Guarde as alterações para pré-visualizar», ou
    • Implementar render client-side só para preview (substituir {{chave}} localmente com dados mock).

5.6 Tratamento de erros HTTP

StatusCausa típicaAção na UI
400Placeholder inválido, campo vazio, render incompletoToast / mensagem inline com message
401Token expiradoRedirecionar para login
403Sem permissão na empresaMensagem de acesso negado
404Empresa inexistente ou slug ausente (first-access)Mensagem específica

6. Exemplo completo — Módulo 1 (TypeScript)

const API_BASE = "https://api.exemplo.com/api";

type ApiResponse<T> =
| { status: "success"; message: string; data: T }
| { status: "error"; message: string };

interface TemplateState {
assunto: string;
corpo: string;
isDefault: boolean;
placeholders: string[];
updated_at: string | null;
updated_by: string | null;
}

function headers(token: string): HeadersInit {
return {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
};
}

export async function loadWelcomeTemplate(
companyUuid: string,
token: string,
): Promise<TemplateState> {
const res = await fetch(
`${API_BASE}/companies/${companyUuid}/email-templates/welcome`,
{ headers: headers(token) },
);
const json = (await res.json()) as ApiResponse<TemplateState>;
if (!res.ok || json.status === "error") throw new Error(json.message);
return json.data;
}

export async function saveWelcomeTemplate(
companyUuid: string,
token: string,
patch: { assunto?: string; corpo?: string },
): Promise<TemplateState> {
const res = await fetch(
`${API_BASE}/companies/${companyUuid}/email-templates/welcome`,
{
method: "PATCH",
headers: headers(token),
body: JSON.stringify(patch),
},
);
const json = (await res.json()) as ApiResponse<TemplateState>;
if (!res.ok || json.status === "error") throw new Error(json.message);
return json.data;
}

export async function previewWelcomeTemplate(
companyUuid: string,
token: string,
sample: { nomeUsuario: string; linkPrimeiroAcesso: string },
): Promise<{ assunto: string; corpo: string }> {
const qs = new URLSearchParams({
nomeUsuario: sample.nomeUsuario,
linkPrimeiroAcesso: sample.linkPrimeiroAcesso,
});
const res = await fetch(
`${API_BASE}/companies/${companyUuid}/email-templates/welcome/render?${qs}`,
{ headers: headers(token) },
);
const json = (await res.json()) as ApiResponse<{ assunto: string; corpo: string }>;
if (!res.ok || json.status === "error") throw new Error(json.message);
return json.data;
}

7. Envio real de e-mail (fora desta tela)

Fluxo típico após criar utilizador:

1. POST /api/users → criar utilizador
2. Gerar linkPrimeiroAcesso → serviço de auth / frontend
3. GET .../welcome/render?... → obter assunto + corpo finais
4. Enviar via SMTP / SendGrid / etc. → serviço externo (não é este backend)

O editor de templates não dispara envio; apenas configura o texto.


8. Painel admin global (opcional)

Para editar o default do sistema (não por empresa), use:

GET /api/global-config/email-templates
GET /api/global-config/email-templates/{templateKey}
PATCH /api/global-config/email-templates/{templateKey}

A UI por empresa continua a usar /api/companies/{uuid}/email-templates/.... Alterar o global não sobrescreve empresas que já customizaram (isDefault: false).


9. Persistência no backend (contexto)

Por empresa, os templates ficam em JSONB:

{
"emailTemplates": {
"welcome": {
"assunto": "Bem-vindo à {{nome_empresa}}!",
"corpo": "Olá {{nome_usuario}}, ...",
"is_customized": false,
"updated_at": null,
"updated_by": null
}
}
}

Defaults globais: tabelas global_email_template_* (ver db/add_global_email_templates.sql).


10. Checklist de implementação noutro sistema

  • Obter companyUuid e JWT após login
  • Mapear 5 módulos UI → templateKey (§2)
  • GET ao abrir cada módulo → preencher formulário
  • Chips de placeholders a partir de data.placeholders
  • Contadores e validação local (limites + placeholders)
  • PATCH ao guardar; atualizar badge isDefault
  • Preview via /render (dados de teste) ou render local do rascunho
  • Tratar erros 400/401/403/404
  • Documentar que envio SMTP é responsabilidade do cliente
  • (Opcional) Tela admin com /api/global-config/email-templates

11. Recursos

RecursoLocal
Referência API completaFRONTEND_EMAIL_TEMPLATES.md
SwaggerGET /api/docs — tag E-mail templates
Postmanpostman/CRM_Backend_API.postman_collection.json
Endpoints geraisapi-endpoints.md
SQL / seedsdb/add_global_email_templates.sql, db/global_email_templates_seed.sql