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 tela | Comportamento | Origem dos dados |
|---|---|---|
| Título — ex.: «Módulo 1 — e-mail de boas-vindas» | Label fixo no frontend; identifica o tipo de template | Configuração local do frontend (ver tabela §2) |
| Badge «Padrão do sistema» | Visível quando isDefault === true | Campo isDefault do GET |
| Descrição — ex.: «Modelo de e-mail enviado ao novo utilizador…» | Texto explicativo fixo | Frontend (i18n / config) |
| Campo Assunto | Input de linha única, max 200 caracteres | data.assunto do GET |
Contador assunto — ex.: 29 / 200 caracteres | Validação client-side antes do PATCH | Limite por tipo (§2) |
| Campo Corpo | Textarea multilinha, max 5000 (ou 20000 para proposta) | data.corpo do GET |
Contador corpo — ex.: 86 / 5000 caracteres | Validação client-side | Limite 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 template | Envia PATCH com assunto e/ou corpo | API §3.2 |
| Pré-visualizar | Chama render com dados de exemplo e mostra modal/painel | API §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 UI | Título sugerido | templateKey (URL) | Chave em company.metadata | Assunto max | Corpo max |
|---|---|---|---|---|---|
| 1 | E-mail de boas-vindas | welcome | emailTemplates.welcome | 200 | 5 000 |
| 2 | Reset de senha | password-reset | emailTemplates.passwordReset | 200 | 5 000 |
| 3 | Primeiro acesso | first-access | emailTemplates.firstAccess | 200 | 5 000 |
| 4 | Proposta comercial | proposal | emailTemplates.proposal | 200 | 20 000 |
| 5 | Envio de documento | document-delivery | emailTemplates.documentDelivery | 200 | 5 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
| Item | Valor |
|---|---|
| Base URL | http://localhost:3000 (ou ambiente) |
| Prefixo | /api |
| Header | Authorization: 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:
- Ao montar a página, chamar GET com
companyUuid+templateKey. - Preencher
assuntoecorpo. - Renderizar chips a partir de
placeholders→ mostrar{{nome_empresa}}, etc. - 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,corpoou ambos (pelo menos um). - Strings não vazias após
trim. - Respeitar limites de caracteres (§2).
- Só placeholders listados em
placeholderssã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 param | Placeholder | Quem 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
| Query | Placeholder | Origem |
|---|---|---|
nome | {{nome}} | Frontend |
| — | {{link_acesso}} | Backend (https://{slug}.gevtech.com.br) |
Se o tenant não tiver slug → 404.
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
| Placeholder | Uso |
|---|---|
{{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
| Placeholder | Query no render |
|---|---|
{{link}} | link |
{{codigo}} | codigo |
Módulo 3 — first-access
| Placeholder | Origem |
|---|---|
{{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
| Placeholder | Query 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}}:
- Inserir no corpo na posição do cursor (ou no fim se não houver seleção).
- Opcional: permitir inserção no assunto com segundo clique ou menu.
- 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
datada resposta;isDirty = false. - Em 400: mostrar
message(ex.: placeholders não suportados).
5.5 Botão Pré-visualizar
- Validar localmente (limites + placeholders).
- Chamar endpoint
/rendercom dados de exemplo (hardcoded ou formulário «dados de teste»). - Mostrar modal com
data.assuntoedata.corporenderizados. - 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
| Status | Causa típica | Ação na UI |
|---|---|---|
| 400 | Placeholder inválido, campo vazio, render incompleto | Toast / mensagem inline com message |
| 401 | Token expirado | Redirecionar para login |
| 403 | Sem permissão na empresa | Mensagem de acesso negado |
| 404 | Empresa 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
companyUuide 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
| Recurso | Local |
|---|---|
| Referência API completa | FRONTEND_EMAIL_TEMPLATES.md |
| Swagger | GET /api/docs — tag E-mail templates |
| Postman | postman/CRM_Backend_API.postman_collection.json |
| Endpoints gerais | api-endpoints.md |
| SQL / seeds | db/add_global_email_templates.sql, db/global_email_templates_seed.sql |