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

735 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<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: 15,
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: 12,
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: 15,
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', padding: '1.75rem 2rem' }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<strong style={{ fontSize: '1.15rem', color: 'var(--color-text)' }}>
{editingId ? 'Editar template' : 'Novo template'}
</strong>
<button type="button" onClick={onBack} aria-label="Fechar" style={{
border: 'none', background: 'rgba(0,0,0,0.06)', cursor: 'pointer',
width: 34, height: 34, borderRadius: '50%', display: 'grid', placeItems: 'center',
fontSize: '1.1rem', color: 'var(--color-text-soft)', lineHeight: 1,
}}>
×
</button>
</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: 9, 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: 6, 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: 9, 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: 9, 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: 6, 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: 6, 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: 9, 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: 9, 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 [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();
const parts = [];
if (result.imported) parts.push(`${result.imported} importado(s)`);
if (result.synced) parts.push(`${result.synced} atualizado(s)`);
setSuccess(`Sincronizado com a Meta! ${parts.length ? parts.join(', ') + '.' : 'Nenhuma alteração.'}`);
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 && (
<div
onClick={goBack}
style={{
position: 'fixed', inset: 0, zIndex: 50,
background: 'rgba(0, 49, 80, 0.28)',
display: 'grid', placeItems: 'center', padding: '1rem',
}}
>
<div
onClick={e => 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)',
}}
>
<TemplateFormView
form={form} setForm={setForm} editingId={editingId}
onSave={handleSave} saving={saving} error={error} success={success}
onBack={goBack} areas={areas}
/>
</div>
</div>
)}
<div style={{ display: 'grid', gap: '1rem' }}>
{/* Toolbar */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
<button type="button" onClick={handleSync} disabled={syncing} style={{
border: '1px solid var(--color-border)', borderRadius: 8, 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: 8, 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: 9, 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>
</>
);
}