omnichannel-frontend/src/modules/management/components/TemplateManagementPanel.jsx

713 lines
30 KiB
React
Raw Normal View History

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: 10,
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: 8, 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 (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.65rem' }}>
<span style={{ ...labelStyle, marginBottom: 0 }}>Pré-visualização</span>
<div style={{
width: 272, background: '#ece5dd', borderRadius: 20,
border: '7px solid #1a1a1a', padding: '1rem 0.65rem',
boxShadow: '0 6px 24px rgba(0,0,0,0.18)', minHeight: 160,
}}>
<div style={{ height: 6, background: '#1a1a1a', borderRadius: 3, marginBottom: '0.65rem', opacity: 0.12 }} />
<div style={{
background: '#fff', borderRadius: '4px 14px 14px 14px', padding: '0.5rem 0.6rem',
maxWidth: '88%', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', position: 'relative',
}}>
<div style={{
position: 'absolute', top: 0, left: -7, width: 0, height: 0,
borderTop: '7px solid #fff', borderLeft: '7px solid transparent',
}} />
{headerType === 'TEXT' && headerText && (
<p style={{ margin: '0 0 0.3rem', fontWeight: 800, fontSize: '0.82rem', color: '#111' }}>
{headerText}
</p>
)}
{headerType === 'IMAGE' && (
<div style={{
height: 80, borderRadius: 6, background: '#d0e8ff',
display: 'grid', placeItems: 'center', marginBottom: '0.35rem',
color: '#5588bb', fontWeight: 700, fontSize: '0.75rem',
}}>
📷 Imagem
</div>
)}
<p style={{
margin: 0, fontSize: '0.8rem',
color: bodyText ? '#111' : '#aaa',
fontStyle: bodyText ? 'normal' : 'italic',
lineHeight: 1.45, whiteSpace: 'pre-wrap', wordBreak: 'break-word',
}}>
{bodyText || 'Corpo do template…'}
</p>
{footerText && (
<p style={{ margin: '0.3rem 0 0', fontSize: '0.71rem', color: '#888', fontWeight: 500 }}>
{footerText}
</p>
)}
<p style={{ margin: '0.25rem 0 0', fontSize: '0.68rem', color: '#aaa', textAlign: 'right' }}>
12:00
</p>
</div>
{buttons.length > 0 && (
<div style={{ marginTop: '0.3rem', display: 'flex', flexDirection: 'column', gap: '0.2rem' }}>
{buttons.map((b, i) => (
<div key={i} style={{
background: '#fff', borderRadius: 6, padding: '0.35rem 0.55rem',
textAlign: 'center', fontSize: '0.77rem', fontWeight: 700,
color: '#00a5f4', boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
}}>
{b.type === 'URL' ? '🔗 ' : b.type === 'PHONE_NUMBER' ? '📞 ' : '↩ '}
{b.text || '(sem texto)'}
</div>
))}
</div>
)}
</div>
</div>
);
}
// ── 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 (
<div style={{
border: '1px solid var(--color-border)', borderRadius: 16,
padding: '1rem', background: '#fff',
display: 'flex', flexDirection: 'column', gap: '0.5rem',
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem', flexWrap: 'wrap' }}>
<strong style={{ flex: 1, fontSize: '0.95rem', color: 'var(--color-text)', lineHeight: 1.3 }}>
{t.name}
</strong>
<span style={{
fontSize: '0.72rem', fontWeight: 800, padding: '0.2rem 0.55rem', borderRadius: 20,
background: ms.bg, color: ms.color, border: `1px solid ${ms.border}`, whiteSpace: 'nowrap',
}}>
{ms.label}
</span>
</div>
<div style={{ display: 'flex', gap: '0.35rem', flexWrap: 'wrap', alignItems: 'center' }}>
<span style={pill('#f0f2f5', '#444')}>{t.category || 'UTILITY'}</span>
<span style={pill('#f0f2f5', '#444')}>{t.language || 'pt_BR'}</span>
{t.area_nome && <span style={pill('#e8f0ff', '#2244aa')}>{t.area_nome}</span>}
</div>
<p style={{
margin: 0, fontSize: '0.83rem', color: 'var(--color-text-soft)', lineHeight: 1.4,
overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
}}>
{t.content || '(sem corpo)'}
</p>
{(t.header_type || t.footer_text || btns.length > 0) && (
<div style={{ display: 'flex', gap: '0.3rem', flexWrap: 'wrap' }}>
{t.header_type && <span style={pill('#f0f2f5', '#555')}>Header: {t.header_type}</span>}
{t.footer_text && <span style={pill('#f0f2f5', '#555')}>Footer</span>}
{btns.length > 0 && (
<span style={pill('#e8f4ff', '#0055bb')}>
{btns.length} botão{btns.length > 1 ? 'ões' : ''}
</span>
)}
</div>
)}
{t.meta_template_name && (
<p style={{ margin: 0, fontSize: '0.7rem', color: '#999', fontFamily: 'monospace' }}>
{t.meta_template_name}
</p>
)}
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap', marginTop: '0.2rem' }}>
<button type="button" onClick={() => onEdit(t)} style={actionBtn('#f0f2f5', '#333')}>
Editar
</button>
<button type="button" onClick={() => 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'}
</button>
<button type="button" onClick={() => onDelete(t.id)} disabled={isDeleting}
style={{ ...actionBtn('#fff2f2', '#b00020', isDeleting), marginLeft: 'auto' }}>
{isDeleting ? '…' : 'Excluir'}
</button>
</div>
</div>
);
}
// ── 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 (
<div style={{ display: 'grid', gap: '1.1rem' }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<button type="button" onClick={onBack} style={{
border: 'none', background: 'none', cursor: 'pointer',
fontWeight: 800, color: 'var(--color-text-soft)', fontSize: '0.88rem', padding: 0,
}}>
Voltar
</button>
<strong style={{ fontSize: '1rem' }}>
{editingId ? 'Editar Template HSM' : 'Novo Template HSM'}
</strong>
</div>
{/* Two-column */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 290px', gap: '1.5rem', alignItems: 'start' }}>
{/* ── Form column ── */}
<div style={{ display: 'grid', gap: '0.9rem' }}>
{/* Nome */}
<div>
<label style={labelStyle} htmlFor="tpl-name">Nome do template *</label>
<input id="tpl-name" type="text" value={form.name}
onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
placeholder="ex: Boas Vindas Suporte" style={fieldStyle} />
{metaNamePreview && (
<p style={{ margin: '0.2rem 0 0', fontSize: '0.74rem', color: '#888' }}>
Nome Meta:{' '}
<code style={{ fontFamily: 'monospace', background: '#f0f2f5', padding: '0.1rem 0.3rem', borderRadius: 4 }}>
{metaNamePreview}
</code>
</p>
)}
</div>
{/* Categoria / Idioma / Área */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.6rem' }}>
<div>
<label style={labelStyle}>Categoria</label>
<select value={form.category} onChange={e => setForm(f => ({ ...f, category: e.target.value }))} style={fieldStyle}>
{CATEGORIES.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
</select>
</div>
<div>
<label style={labelStyle}>Idioma</label>
<select value={form.language} onChange={e => setForm(f => ({ ...f, language: e.target.value }))} style={fieldStyle}>
{LANGUAGES.map(l => <option key={l.value} value={l.value}>{l.label}</option>)}
</select>
</div>
<div>
<label style={labelStyle}>Área</label>
<select value={form.areaId} onChange={e => setForm(f => ({ ...f, areaId: e.target.value }))} style={fieldStyle}>
<option value="">Todas</option>
{areas.map(a => <option key={a.id} value={String(a.id)}>{a.nome}</option>)}
</select>
</div>
</div>
{/* Header */}
<div style={{ border: '1px solid var(--color-border)', borderRadius: 12, padding: '0.75rem', display: 'grid', gap: '0.5rem' }}>
<label style={{ ...labelStyle, marginBottom: 0 }}>Header (opcional)</label>
<div style={{ display: 'grid', gridTemplateColumns: '140px 1fr', gap: '0.5rem' }}>
<select value={form.headerType}
onChange={e => setForm(f => ({ ...f, headerType: e.target.value, headerText: '' }))}
style={fieldStyle}>
<option value="">Sem header</option>
<option value="TEXT">Texto</option>
<option value="IMAGE">Imagem (URL)</option>
</select>
{form.headerType && (
<input type="text" value={form.headerText}
onChange={e => setForm(f => ({ ...f, headerText: e.target.value }))}
placeholder={form.headerType === 'TEXT' ? 'Texto do cabeçalho' : 'URL da imagem'}
style={fieldStyle} />
)}
</div>
</div>
{/* Corpo */}
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.3rem' }}>
<label style={{ ...labelStyle, marginBottom: 0 }} htmlFor="tpl-body">Corpo *</label>
<button type="button" onClick={insertVar} style={{
border: '1px solid var(--color-border)', borderRadius: 8, padding: '0.2rem 0.6rem',
background: '#fff', color: 'var(--color-text-soft)', fontWeight: 800, fontSize: '0.75rem', cursor: 'pointer',
}}>
+ Variável
</button>
</div>
<textarea id="tpl-body" value={form.content} onChange={handleContent} rows={5}
placeholder={'Olá {{1}}, tudo bem?\n\nEntramos em contato referente ao seu pedido {{2}}.'}
style={{ ...fieldStyle, resize: 'vertical', fontFamily: 'inherit', lineHeight: 1.5 }} />
<p style={{ margin: '0.2rem 0 0', fontSize: '0.74rem', color: '#888' }}>
Use {'{{1}}'}, {'{{2}}'}, etc. As variáveis são substituídas no envio.
</p>
</div>
{/* Variáveis detectadas */}
{form.bodyVariables.length > 0 && (
<div style={{ border: '1px solid #e0e4ec', borderRadius: 12, padding: '0.75rem', background: '#fafbfc' }}>
<p style={{ ...labelStyle, marginBottom: '0.5rem' }}>Variáveis do corpo</p>
<div style={{ display: 'grid', gap: '0.4rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: '64px 1fr 1fr', gap: '0.4rem' }}>
<span style={{ fontSize: '0.73rem', fontWeight: 800, color: '#888' }}>Var</span>
<span style={{ fontSize: '0.73rem', fontWeight: 800, color: '#888' }}>Rótulo</span>
<span style={{ fontSize: '0.73rem', fontWeight: 800, color: '#888' }}>Exemplo (obrigatório)</span>
</div>
{form.bodyVariables.map(v => (
<div key={v.index} style={{ display: 'grid', gridTemplateColumns: '64px 1fr 1fr', gap: '0.4rem', alignItems: 'center' }}>
<span style={{
...fieldStyle, padding: '0.4rem 0.5rem', textAlign: 'center',
background: '#e8eaed', fontFamily: 'monospace', fontSize: '0.82rem',
}}>
{`{{${v.index}}}`}
</span>
<input type="text" placeholder="ex: nome" value={v.label}
onChange={e => setVar(v.index, 'label', e.target.value)}
style={{ ...fieldStyle, fontSize: '0.83rem', padding: '0.4rem 0.6rem' }} />
<input type="text" placeholder="ex: Rafael" value={v.example}
onChange={e => setVar(v.index, 'example', e.target.value)}
style={{ ...fieldStyle, fontSize: '0.83rem', padding: '0.4rem 0.6rem' }} />
</div>
))}
</div>
</div>
)}
{/* Footer */}
<div>
<label style={labelStyle} htmlFor="tpl-footer">Footer (opcional)</label>
<input id="tpl-footer" type="text" value={form.footerText}
onChange={e => setForm(f => ({ ...f, footerText: e.target.value }))}
placeholder="ex: Não responda a esta mensagem" style={fieldStyle} />
</div>
{/* Botões */}
<div style={{ border: '1px solid var(--color-border)', borderRadius: 12, padding: '0.75rem', display: 'grid', gap: '0.6rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label style={{ ...labelStyle, marginBottom: 0 }}>Botões (opcional)</label>
<div style={{ display: 'flex', gap: '0.3rem', flexWrap: 'wrap' }}>
{BUTTON_TYPES.map(bt => (
<button key={bt.value} type="button" onClick={() => addButton(bt.value)}
disabled={form.buttons.length >= 10}
style={{
border: '1px solid #dde1e7', borderRadius: 8, padding: '0.2rem 0.55rem',
background: '#fff', color: '#333', fontWeight: 700, fontSize: '0.73rem', cursor: 'pointer',
}}>
+ {bt.label}
</button>
))}
</div>
</div>
{form.buttons.length > 0 && (
<div style={{ display: 'grid', gap: '0.4rem' }}>
{form.buttons.map((b, i) => (
<div key={i} style={{
display: 'grid',
gridTemplateColumns: b.type === 'QUICK_REPLY' ? '1fr auto' : '1fr 1fr auto',
gap: '0.4rem', alignItems: 'center',
}}>
<input type="text" placeholder="Texto do botão" value={b.text}
onChange={e => updateBtn(i, 'text', e.target.value)}
style={{ ...fieldStyle, fontSize: '0.83rem', padding: '0.4rem 0.6rem' }} />
{b.type === 'URL' && (
<input type="text" placeholder="URL (https://…)" value={b.url}
onChange={e => updateBtn(i, 'url', e.target.value)}
style={{ ...fieldStyle, fontSize: '0.83rem', padding: '0.4rem 0.6rem' }} />
)}
{b.type === 'PHONE_NUMBER' && (
<input type="text" placeholder="Número (55119…)" value={b.phone}
onChange={e => updateBtn(i, 'phone', e.target.value)}
style={{ ...fieldStyle, fontSize: '0.83rem', padding: '0.4rem 0.6rem' }} />
)}
<button type="button" onClick={() => removeBtn(i)} style={{
border: 'none', background: '#fff2f2', color: '#b00020',
borderRadius: 8, padding: '0.4rem 0.55rem', fontWeight: 800, cursor: 'pointer',
}}></button>
</div>
))}
</div>
)}
</div>
{/* Feedback */}
{error && <p style={{ margin: 0, color: '#b00020', fontWeight: 700, fontSize: '0.88rem' }}>{error}</p>}
{success && <p style={{ margin: 0, color: '#1a7a44', fontWeight: 700, fontSize: '0.88rem' }}>{success}</p>}
{/* Actions */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.65rem' }}>
<button type="button" onClick={onBack} style={{
border: '1px solid var(--color-border)', borderRadius: 12, padding: '0.6rem 1rem',
background: '#fff', color: 'var(--color-text-soft)', fontWeight: 800, cursor: 'pointer',
}}>
Cancelar
</button>
<button type="button" onClick={onSave} disabled={saving} style={{
border: 'none', borderRadius: 12, padding: '0.6rem 1.25rem',
background: saving ? 'var(--color-border)' : 'var(--color-primary)',
color: '#fff', fontWeight: 800, cursor: saving ? 'not-allowed' : 'pointer',
}}>
{saving ? 'Salvando…' : editingId ? 'Salvar alterações' : 'Salvar rascunho'}
</button>
</div>
</div>
{/* ── Preview column ── */}
<WhatsAppPreview form={form} />
</div>
</div>
);
}
// ── Main Component ─────────────────────────────────────────────────────────
export function TemplateManagementPanel({ areas = [] }) {
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(true);
const [view, setView] = useState('list');
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(''); setView('form');
}
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(''); setView('form');
}
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(() => { setView('list'); 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() { setView('list'); setError(''); setSuccess(''); }
// ── Form view ──
if (view === 'form') {
return (
<TemplateFormView
form={form} setForm={setForm} editingId={editingId}
onSave={handleSave} saving={saving} error={error} success={success}
onBack={goBack} areas={areas}
/>
);
}
// ── List view ──
return (
<div style={{ display: 'grid', gap: '1rem' }}>
{/* Toolbar */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', flexWrap: 'wrap' }}>
<strong style={{ flex: 1, fontSize: '0.95rem' }}>Templates HSM</strong>
<button type="button" onClick={handleSync} disabled={syncing} style={{
border: '1px solid var(--color-border)', borderRadius: 10, padding: '0.45rem 0.85rem',
background: '#fff', color: 'var(--color-text-soft)', fontWeight: 800, fontSize: '0.82rem',
cursor: syncing ? 'not-allowed' : 'pointer',
}}>
{syncing ? 'Sincronizando…' : '↻ Sincronizar'}
</button>
<button type="button" onClick={openNew} style={{
border: 'none', borderRadius: 10, padding: '0.45rem 0.85rem',
background: 'var(--color-primary)', color: '#fff', fontWeight: 800,
fontSize: '0.82rem', cursor: 'pointer',
}}>
+ Novo template
</button>
</div>
{/* Info banner */}
<div style={{ background: '#f0f4ff', borderRadius: 12, padding: '0.7rem 0.9rem', border: '1px solid #dde4f5' }}>
<p style={{ margin: 0, fontSize: '0.8rem', color: '#334', fontWeight: 600, lineHeight: 1.55 }}>
<strong>Templates HSM</strong> são aprovados pela Meta para iniciar conversas fora da janela de 24h.
Crie Salve <strong>Envie para Meta</strong> aguarde aprovação {' '}
<strong>Sincronize</strong> para atualizar o status. Templates <strong>Aprovados</strong> ficam
disponíveis em Atendimento Ativo.
</p>
</div>
{/* Feedback */}
{error && <p style={{ margin: 0, color: '#b00020', fontWeight: 700, fontSize: '0.88rem' }}>{error}</p>}
{success && <p style={{ margin: 0, color: '#1a7a44', fontWeight: 700, fontSize: '0.88rem' }}>{success}</p>}
{/* List */}
{loading && (
<p style={{ margin: 0, color: 'var(--color-text-soft)', fontWeight: 700 }}>Carregando</p>
)}
{!loading && templates.length === 0 && (
<div style={{ textAlign: 'center', padding: '2.5rem 1rem', color: 'var(--color-text-soft)' }}>
<p style={{ fontSize: '2rem', margin: '0 0 0.5rem' }}>📝</p>
<p style={{ margin: 0, fontWeight: 700, fontSize: '0.95rem' }}>Nenhum template cadastrado.</p>
<p style={{ margin: '0.3rem 0 0', fontSize: '0.85rem' }}>
Clique em <strong>"+ Novo template"</strong> para criar o primeiro.
</p>
</div>
)}
{!loading && templates.length > 0 && (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '0.75rem',
}}>
{templates.map(t => (
<TemplateCard key={t.id} t={t}
onEdit={openEdit} onSubmit={handleSubmit} onDelete={handleDelete}
submitting={submitting} deleting={deleting} />
))}
</div>
)}
</div>
);
}