import { useState, useEffect } from 'react';
import {
listTemplates,
saveTemplate,
updateTemplate,
deleteTemplate,
submitTemplateToMeta,
syncTemplatesFromMeta,
} from '../services/templateService';
// ── Constants ──────────────────────────────────────────────────────────────
const META_STATUS_MAP = {
APPROVED: { label: 'Aprovado', color: '#1a7a44', bg: '#edfaf3', border: '#b6e8c8' },
PENDING: { label: 'Aguardando Meta', color: '#a05a00', bg: '#fff8f0', border: '#f5d9a8' },
REJECTED: { label: 'Rejeitado', color: '#b00020', bg: '#fff2f2', border: '#f5c6c6' },
PAUSED: { label: 'Pausado', color: '#6b4700', bg: '#fffbf0', border: '#f5e0a8' },
DISABLED: { label: 'Desabilitado', color: '#555', bg: '#f5f5f5', border: '#ddd' },
};
const DRAFT_STATUS = { label: 'Rascunho', color: '#555', bg: '#f0f2f5', border: '#dde1e7' };
function getMetaStatus(t) {
return t.meta_status ? (META_STATUS_MAP[t.meta_status] || DRAFT_STATUS) : DRAFT_STATUS;
}
const CATEGORIES = [
{ value: 'UTILITY', label: 'Utilidade' },
{ value: 'MARKETING', label: 'Marketing' },
{ value: 'AUTHENTICATION', label: 'Autenticação' },
];
const LANGUAGES = [
{ value: 'pt_BR', label: 'Português (Brasil)' },
{ value: 'en_US', label: 'English (US)' },
{ value: 'es', label: 'Español' },
];
const BUTTON_TYPES = [
{ value: 'QUICK_REPLY', label: 'Resposta rápida' },
{ value: 'URL', label: 'Abrir URL' },
{ value: 'PHONE_NUMBER', label: 'Ligar' },
];
const EMPTY_FORM = {
name: '', language: 'pt_BR', category: 'UTILITY', areaId: '',
headerType: '', headerText: '', content: '',
bodyVariables: [], footerText: '', buttons: [],
};
// ── Helpers ─────────────────────────────────────────────────────────────────
function toMetaName(name) {
return String(name || '')
.normalize('NFD').replace(/[̀-ͯ]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.substring(0, 60) || '';
}
function syncBodyVars(content, prevVars) {
const matches = [...content.matchAll(/\{\{(\d+)\}\}/g)];
const indices = [...new Set(matches.map(m => Number(m[1])))].sort((a, b) => a - b);
return indices.map(i => prevVars.find(v => v.index === i) || { index: i, label: '', example: '' });
}
function renderPreviewBody(content, bodyVariables) {
return content.replace(/\{\{(\d+)\}\}/g, (_, n) => {
const v = bodyVariables.find(v => v.index === Number(n));
return v?.example || (v?.label ? `[${v.label}]` : `{{${n}}}`);
});
}
// ── Styles ────────────────────────────────────────────────────────────────
const fieldStyle = {
width: '100%', border: '1px solid var(--color-border)', borderRadius: 8,
padding: '0.6rem 0.75rem', fontSize: '0.9rem', fontWeight: 600,
color: 'var(--color-text)', background: '#fff', boxSizing: 'border-box',
};
const labelStyle = {
display: 'block', fontSize: '0.78rem', fontWeight: 800,
color: 'var(--color-text-soft)', textTransform: 'uppercase',
letterSpacing: '0.04em', marginBottom: '0.3rem',
};
function pill(bg, color) {
return { fontSize: '0.7rem', fontWeight: 700, background: bg, color, borderRadius: 6, padding: '0.15rem 0.45rem' };
}
function actionBtn(bg, color, disabled = false) {
return {
border: 'none', borderRadius: 6, padding: '0.4rem 0.75rem',
background: disabled ? '#f0f2f5' : bg, color: disabled ? '#aaa' : color,
fontWeight: 800, fontSize: '0.8rem', cursor: disabled ? 'not-allowed' : 'pointer',
};
}
// ── WhatsApp Preview ──────────────────────────────────────────────────────
function WhatsAppPreview({ form }) {
const { headerType, headerText, content, bodyVariables, footerText, buttons } = form;
const bodyText = renderPreviewBody(content || '', bodyVariables);
return (
Pré-visualização
{headerType === 'TEXT' && headerText && (
{headerText}
)}
{headerType === 'IMAGE' && (
📷 Imagem
)}
{bodyText || 'Corpo do template…'}
{footerText && (
{footerText}
)}
12:00
{buttons.length > 0 && (
{buttons.map((b, i) => (
{b.type === 'URL' ? '🔗 ' : b.type === 'PHONE_NUMBER' ? '📞 ' : '↩ '}
{b.text || '(sem texto)'}
))}
)}
);
}
// ── Template Card ─────────────────────────────────────────────────────────
function TemplateCard({ t, onEdit, onSubmit, onDelete, submitting, deleting }) {
const ms = getMetaStatus(t);
const btns = Array.isArray(t.buttons) ? t.buttons : [];
const isSubmitting = submitting === t.id;
const isDeleting = deleting === t.id;
const canSubmit = t.meta_status !== 'PENDING';
return (
{t.name}
{ms.label}
{t.category || 'UTILITY'}
{t.language || 'pt_BR'}
{t.area_nome && {t.area_nome} }
{t.content || '(sem corpo)'}
{(t.header_type || t.footer_text || btns.length > 0) && (
{t.header_type && Header: {t.header_type} }
{t.footer_text && Footer }
{btns.length > 0 && (
{btns.length} botão{btns.length > 1 ? 'ões' : ''}
)}
)}
{t.meta_template_name && (
{t.meta_template_name}
)}
onEdit(t)} style={actionBtn('#f0f2f5', '#333')}>
Editar
onSubmit(t.id)} disabled={!canSubmit || isSubmitting}
style={actionBtn(canSubmit ? '#e8f4ff' : '#f5f5f5', canSubmit ? '#0066cc' : '#aaa', !canSubmit || isSubmitting)}>
{isSubmitting ? 'Enviando…' : t.meta_status === 'APPROVED' ? 'Reenviar' : 'Enviar para Meta'}
onDelete(t.id)} disabled={isDeleting}
style={{ ...actionBtn('#fff2f2', '#b00020', isDeleting), marginLeft: 'auto' }}>
{isDeleting ? '…' : 'Excluir'}
);
}
// ── Template Form ─────────────────────────────────────────────────────────
function TemplateFormView({ form, setForm, editingId, onSave, saving, error, success, onBack, areas }) {
function handleContent(e) {
const content = e.target.value;
setForm(f => ({ ...f, content, bodyVariables: syncBodyVars(content, f.bodyVariables) }));
}
function insertVar() {
const nextIdx = form.bodyVariables.length > 0
? Math.max(...form.bodyVariables.map(v => v.index)) + 1
: 1;
const ta = document.getElementById('tpl-body');
const pos = ta ? (ta.selectionStart ?? form.content.length) : form.content.length;
const tag = `{{${nextIdx}}}`;
const newContent = form.content.slice(0, pos) + tag + form.content.slice(pos);
setForm(f => ({ ...f, content: newContent, bodyVariables: syncBodyVars(newContent, f.bodyVariables) }));
setTimeout(() => {
if (ta) { ta.focus(); ta.setSelectionRange(pos + tag.length, pos + tag.length); }
}, 0);
}
function setVar(index, field, value) {
setForm(f => ({
...f,
bodyVariables: f.bodyVariables.map(v => v.index === index ? { ...v, [field]: value } : v),
}));
}
function addButton(type) {
setForm(f => ({ ...f, buttons: [...f.buttons, { type, text: '', url: '', phone: '' }] }));
}
function updateBtn(i, field, value) {
setForm(f => ({ ...f, buttons: f.buttons.map((b, j) => j === i ? { ...b, [field]: value } : b) }));
}
function removeBtn(i) {
setForm(f => ({ ...f, buttons: f.buttons.filter((_, j) => j !== i) }));
}
const metaNamePreview = toMetaName(form.name);
return (
{/* Header */}
{editingId ? 'Editar template' : 'Novo template'}
×
{/* Two-column */}
{/* ── Form column ── */}
{/* Nome */}
Nome do template *
setForm(f => ({ ...f, name: e.target.value }))}
placeholder="ex: Boas Vindas Suporte" style={fieldStyle} />
{metaNamePreview && (
Nome Meta:{' '}
{metaNamePreview}
)}
{/* Categoria / Idioma / Área */}
Categoria
setForm(f => ({ ...f, category: e.target.value }))} style={fieldStyle}>
{CATEGORIES.map(c => {c.label} )}
Idioma
setForm(f => ({ ...f, language: e.target.value }))} style={fieldStyle}>
{LANGUAGES.map(l => {l.label} )}
Área
setForm(f => ({ ...f, areaId: e.target.value }))} style={fieldStyle}>
Todas
{areas.map(a => {a.nome} )}
{/* Header */}
{/* Corpo */}
Corpo *
+ Variável
Use {'{{1}}'}, {'{{2}}'}, etc. As variáveis são substituídas no envio.
{/* Variáveis detectadas */}
{form.bodyVariables.length > 0 && (
)}
{/* Footer */}
Footer (opcional)
setForm(f => ({ ...f, footerText: e.target.value }))}
placeholder="ex: Não responda a esta mensagem" style={fieldStyle} />
{/* Botões */}
Botões (opcional)
{BUTTON_TYPES.map(bt => (
addButton(bt.value)}
disabled={form.buttons.length >= 10}
style={{
border: '1px solid #dde1e7', borderRadius: 6, padding: '0.2rem 0.55rem',
background: '#fff', color: '#333', fontWeight: 700, fontSize: '0.73rem', cursor: 'pointer',
}}>
+ {bt.label}
))}
{form.buttons.length > 0 && (
{form.buttons.map((b, i) => (
updateBtn(i, 'text', e.target.value)}
style={{ ...fieldStyle, fontSize: '0.83rem', padding: '0.4rem 0.6rem' }} />
{b.type === 'URL' && (
updateBtn(i, 'url', e.target.value)}
style={{ ...fieldStyle, fontSize: '0.83rem', padding: '0.4rem 0.6rem' }} />
)}
{b.type === 'PHONE_NUMBER' && (
updateBtn(i, 'phone', e.target.value)}
style={{ ...fieldStyle, fontSize: '0.83rem', padding: '0.4rem 0.6rem' }} />
)}
removeBtn(i)} style={{
border: 'none', background: '#fff2f2', color: '#b00020',
borderRadius: 6, padding: '0.4rem 0.55rem', fontWeight: 800, cursor: 'pointer',
}}>✕
))}
)}
{/* Feedback */}
{error &&
{error}
}
{success &&
{success}
}
{/* Actions */}
Cancelar
{saving ? 'Salvando…' : editingId ? 'Salvar alterações' : 'Salvar rascunho'}
{/* ── Preview column ── */}
);
}
// ── Main Component ─────────────────────────────────────────────────────────
export function TemplateManagementPanel({ areas = [] }) {
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [editingId, setEditingId] = useState(null);
const [form, setForm] = useState(EMPTY_FORM);
const [saving, setSaving] = useState(false);
const [submitting, setSubmitting] = useState(null);
const [syncing, setSyncing] = useState(false);
const [deleting, setDeleting] = useState(null);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
useEffect(() => { load(); }, []);
async function load() {
setLoading(true);
try {
const data = await listTemplates();
setTemplates(Array.isArray(data) ? data : []);
} catch {
setError('Erro ao carregar templates.');
} finally {
setLoading(false);
}
}
function openNew() {
setForm(EMPTY_FORM); setEditingId(null);
setError(''); setSuccess(''); setModalOpen(true);
}
function openEdit(t) {
setForm({
name: t.name || '',
language: t.language || 'pt_BR',
category: t.category || 'UTILITY',
areaId: t.area_id ? String(t.area_id) : '',
headerType: t.header_type || '',
headerText: t.header_text || '',
content: t.content || '',
bodyVariables: Array.isArray(t.body_variables) ? t.body_variables : [],
footerText: t.footer_text || '',
buttons: Array.isArray(t.buttons) ? t.buttons : [],
});
setEditingId(t.id); setError(''); setSuccess(''); setModalOpen(true);
}
async function handleSave() {
if (!form.name.trim() || !form.content.trim()) {
setError('Nome e Corpo são obrigatórios.');
return;
}
setSaving(true); setError(''); setSuccess('');
try {
const payload = {
name: form.name.trim(),
content: form.content,
language: form.language,
category: form.category,
areaId: form.areaId ? Number(form.areaId) : null,
headerType: form.headerType || null,
headerText: form.headerText.trim() || null,
footerText: form.footerText.trim() || null,
buttons: form.buttons.length ? form.buttons : null,
bodyVariables: form.bodyVariables.length ? form.bodyVariables : null,
};
if (editingId) await updateTemplate(editingId, payload);
else await saveTemplate(payload);
setSuccess('Template salvo! Clique em "Enviar para Meta" para submeter à aprovação.');
await load();
setTimeout(() => { setModalOpen(false); setSuccess(''); }, 2500);
} catch (err) {
setError(err?.message || 'Erro ao salvar.');
} finally {
setSaving(false);
}
}
async function handleSubmit(id) {
setSubmitting(id); setError(''); setSuccess('');
try {
const result = await submitTemplateToMeta(id);
setSuccess(`Enviado para Meta! Status: ${result.status || 'PENDING'}. Clique "Sincronizar" para atualizar.`);
await load();
} catch (err) {
setError(err?.message || 'Erro ao enviar para Meta. Verifique o WABA ID em Admin → WhatsApp → Configurar.');
} finally {
setSubmitting(null);
}
}
async function handleSync() {
setSyncing(true); setError('');
try {
const result = await syncTemplatesFromMeta();
setSuccess(`Sincronizado! ${result.synced ?? 0} template(s) atualizado(s).`);
await load();
} catch (err) {
setError(err?.message || 'Erro ao sincronizar com a Meta.');
} finally {
setSyncing(false);
}
}
async function handleDelete(id) {
if (!window.confirm('Excluir este template? A remoção na Meta também será tentada.')) return;
setDeleting(id); setError('');
try {
await deleteTemplate(id);
await load();
} catch (err) {
setError(err?.message || 'Erro ao excluir.');
} finally {
setDeleting(null);
}
}
function goBack() { setModalOpen(false); setError(''); setSuccess(''); }
// ── List view (sempre renderizado) ──
return (
<>
{/* Modal do formulário */}
{modalOpen && (
e.stopPropagation()}
style={{
background: '#fff', borderRadius: '20px',
width: '100%', maxWidth: '1040px',
maxHeight: '92vh', overflowY: 'auto',
boxShadow: '0 8px 40px rgba(0,0,0,0.18)',
}}
>
)}
{/* Toolbar */}
{syncing ? 'Sincronizando…' : '↻ Sincronizar'}
+ Novo template
{/* Info banner */}
Templates HSM são aprovados pela Meta para iniciar conversas fora da janela de 24h.
Crie → Salve → Envie para Meta → aguarde aprovação →{' '}
Sincronize para atualizar o status. Templates Aprovados ficam
disponíveis em Atendimento Ativo.
{/* Feedback */}
{error &&
{error}
}
{success &&
{success}
}
{/* List */}
{loading && (
Carregando…
)}
{!loading && templates.length === 0 && (
📝
Nenhum template cadastrado.
Clique em "+ Novo template" para criar o primeiro.
)}
{!loading && templates.length > 0 && (
{templates.map(t => (
))}
)}
>
);
}