2026-06-22 15:22:54 -03:00
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
|
import {
|
|
|
|
|
|
getWhatsappConfig,
|
|
|
|
|
|
saveWhatsappConfig,
|
|
|
|
|
|
} from '../services/whatsappConfigService';
|
|
|
|
|
|
|
|
|
|
|
|
const fieldStyle = {
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
padding: '0.7rem 0.85rem',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
color: 'var(--color-text)',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '0.92rem',
|
|
|
|
|
|
boxSizing: 'border-box',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const labelStyle = {
|
|
|
|
|
|
display: 'block',
|
|
|
|
|
|
fontSize: '0.8rem',
|
|
|
|
|
|
fontWeight: 800,
|
|
|
|
|
|
color: 'var(--color-text-soft)',
|
|
|
|
|
|
marginBottom: '0.35rem',
|
|
|
|
|
|
letterSpacing: '0.03em',
|
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export function WhatsappConfigModal({ onClose }) {
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [currentConfig, setCurrentConfig] = useState(null);
|
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
const [validating, setValidating] = useState(false);
|
|
|
|
|
|
const [validation, setValidation] = useState(null);
|
|
|
|
|
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
|
|
|
|
|
|
|
const [form, setForm] = useState({
|
|
|
|
|
|
access_token: '',
|
|
|
|
|
|
phone_number_id: '',
|
|
|
|
|
|
api_version: 'v25.0',
|
|
|
|
|
|
webhook_token: '',
|
2026-06-23 09:57:09 -03:00
|
|
|
|
waba_id: '',
|
2026-06-22 15:22:54 -03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
getWhatsappConfig()
|
|
|
|
|
|
.then((data) => {
|
|
|
|
|
|
setCurrentConfig(data);
|
|
|
|
|
|
setForm((f) => ({
|
|
|
|
|
|
...f,
|
|
|
|
|
|
phone_number_id: data.phone_number_id || '',
|
|
|
|
|
|
api_version: data.api_version || 'v25.0',
|
|
|
|
|
|
webhook_token: data.webhook_token || '',
|
2026-06-23 09:57:09 -03:00
|
|
|
|
waba_id: data.waba_id || '',
|
2026-06-22 15:22:54 -03:00
|
|
|
|
}));
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => setError('Não foi possível carregar a configuração atual.'))
|
|
|
|
|
|
.finally(() => setLoading(false));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
function handleChange(field, value) {
|
|
|
|
|
|
setValidation(null);
|
|
|
|
|
|
setSaveSuccess(false);
|
|
|
|
|
|
setError('');
|
|
|
|
|
|
setForm((f) => ({ ...f, [field]: value }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleValidate() {
|
|
|
|
|
|
if (!form.access_token || !form.phone_number_id) return;
|
|
|
|
|
|
setValidating(true);
|
|
|
|
|
|
setValidation(null);
|
2026-06-22 15:36:34 -03:00
|
|
|
|
const apiVersion = form.api_version || 'v25.0';
|
|
|
|
|
|
const url =
|
|
|
|
|
|
`https://graph.facebook.com/${apiVersion}/${form.phone_number_id}` +
|
|
|
|
|
|
`?access_token=${encodeURIComponent(form.access_token)}&fields=display_phone_number,verified_name,quality_rating`;
|
2026-06-22 15:22:54 -03:00
|
|
|
|
try {
|
2026-06-22 15:36:34 -03:00
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
setValidation({
|
|
|
|
|
|
valid: false,
|
|
|
|
|
|
error: data?.error?.message || 'Credenciais inválidas',
|
|
|
|
|
|
code: data?.error?.code,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setValidation({
|
|
|
|
|
|
valid: true,
|
|
|
|
|
|
phone_number: data.display_phone_number,
|
|
|
|
|
|
verified_name: data.verified_name,
|
|
|
|
|
|
quality_rating: data.quality_rating,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-06-22 15:22:54 -03:00
|
|
|
|
} catch {
|
2026-06-22 15:36:34 -03:00
|
|
|
|
setValidation({ valid: false, error: 'Sem acesso à internet para validar' });
|
2026-06-22 15:22:54 -03:00
|
|
|
|
} finally {
|
|
|
|
|
|
setValidating(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSave() {
|
|
|
|
|
|
setSaving(true);
|
|
|
|
|
|
setSaveSuccess(false);
|
|
|
|
|
|
setError('');
|
|
|
|
|
|
try {
|
|
|
|
|
|
await saveWhatsappConfig(form);
|
|
|
|
|
|
setSaveSuccess(true);
|
|
|
|
|
|
setTimeout(onClose, 1200);
|
2026-06-22 15:36:34 -03:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const msg = err?.message || '';
|
|
|
|
|
|
setError(`Erro ao salvar${msg ? `: ${msg}` : '. Verifique se a migration 025 foi executada no banco.'}`);
|
2026-06-22 15:22:54 -03:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const canValidate = !!(form.access_token && form.phone_number_id);
|
|
|
|
|
|
const canSave = !!(form.access_token || form.phone_number_id) && !saving;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
role="dialog"
|
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
|
aria-labelledby="wa-config-title"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
background: 'rgba(0, 20, 32, 0.42)',
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
placeItems: 'center',
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
zIndex: 50,
|
|
|
|
|
|
}}
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 'min(540px, 100%)',
|
|
|
|
|
|
borderRadius: 22,
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: '1.4rem',
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gap: '1rem',
|
|
|
|
|
|
boxShadow: 'var(--shadow-lg)',
|
|
|
|
|
|
maxHeight: '90vh',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.85rem' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 46,
|
|
|
|
|
|
height: 46,
|
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
placeItems: 'center',
|
|
|
|
|
|
background: '#20a45b',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontWeight: 900,
|
|
|
|
|
|
fontSize: '1rem',
|
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
WA
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong id="wa-config-title" style={{ display: 'block', fontSize: '1.05rem' }}>
|
|
|
|
|
|
Configurar WhatsApp
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700, fontSize: '0.88rem' }}>
|
|
|
|
|
|
Meta Cloud API
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="Fechar"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginLeft: 'auto',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
background: 'none',
|
|
|
|
|
|
fontSize: '1.3rem',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
color: 'var(--color-text-soft)',
|
|
|
|
|
|
lineHeight: 1,
|
|
|
|
|
|
padding: '0.2rem 0.4rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Status atual */}
|
|
|
|
|
|
{!loading && currentConfig && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
padding: '0.7rem 0.9rem',
|
|
|
|
|
|
background: currentConfig.is_configured ? '#f0faf4' : '#fff8f0',
|
|
|
|
|
|
border: `1px solid ${currentConfig.is_configured ? '#b6e8c8' : '#f5d9a8'}`,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '0.6rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span style={{ fontSize: '1.1rem' }}>{currentConfig.is_configured ? '✅' : '⚠️'}</span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong style={{ fontSize: '0.88rem', color: currentConfig.is_configured ? '#1a7a44' : '#a05a00' }}>
|
|
|
|
|
|
{currentConfig.is_configured ? 'Integração configurada' : 'Configuração incompleta'}
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
{currentConfig.is_configured && (
|
|
|
|
|
|
<span style={{ display: 'block', fontSize: '0.8rem', color: 'var(--color-text-soft)', fontWeight: 600 }}>
|
|
|
|
|
|
Token via {currentConfig.token_source === 'database' ? 'banco de dados' : 'variável de ambiente'}
|
|
|
|
|
|
{currentConfig.phone_number_id ? ` · ID: ${currentConfig.phone_number_id}` : ''}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!currentConfig.is_configured && (
|
|
|
|
|
|
<span style={{ display: 'block', fontSize: '0.8rem', color: 'var(--color-text-soft)', fontWeight: 600 }}>
|
|
|
|
|
|
Preencha o Token e o Phone Number ID abaixo
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{loading && (
|
|
|
|
|
|
<p style={{ margin: 0, color: 'var(--color-text-soft)', fontWeight: 700, fontSize: '0.9rem' }}>
|
|
|
|
|
|
Carregando configuração atual…
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Formulário */}
|
|
|
|
|
|
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label htmlFor="wa-token" style={labelStyle}>
|
|
|
|
|
|
Access Token{currentConfig?.has_token ? ' (deixe vazio para manter o atual)' : ''}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="wa-token"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
placeholder={currentConfig?.has_token ? '•••••••• (configurado)' : 'EAAxxxxxxx...'}
|
|
|
|
|
|
value={form.access_token}
|
|
|
|
|
|
onChange={(e) => handleChange('access_token', e.target.value)}
|
|
|
|
|
|
style={fieldStyle}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label htmlFor="wa-phone-id" style={labelStyle}>
|
|
|
|
|
|
Phone Number ID
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="wa-phone-id"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="ex: 123456789012345"
|
|
|
|
|
|
value={form.phone_number_id}
|
|
|
|
|
|
onChange={(e) => handleChange('phone_number_id', e.target.value)}
|
|
|
|
|
|
style={fieldStyle}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-23 09:57:09 -03:00
|
|
|
|
<div>
|
|
|
|
|
|
<label htmlFor="wa-waba-id" style={labelStyle}>
|
|
|
|
|
|
WABA ID (WhatsApp Business Account ID)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="wa-waba-id"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="ex: 123456789012345"
|
|
|
|
|
|
value={form.waba_id}
|
|
|
|
|
|
onChange={(e) => handleChange('waba_id', e.target.value)}
|
|
|
|
|
|
style={fieldStyle}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p style={{ margin: '0.2rem 0 0', fontSize: '0.75rem', color: 'var(--color-text-soft)', fontWeight: 600 }}>
|
|
|
|
|
|
Necessário para criar e sincronizar templates HSM.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-22 15:22:54 -03:00
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.65rem' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label htmlFor="wa-api-version" style={labelStyle}>
|
|
|
|
|
|
Versão da API
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="wa-api-version"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="v25.0"
|
|
|
|
|
|
value={form.api_version}
|
|
|
|
|
|
onChange={(e) => handleChange('api_version', e.target.value)}
|
|
|
|
|
|
style={fieldStyle}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label htmlFor="wa-webhook-token" style={labelStyle}>
|
|
|
|
|
|
Webhook Verify Token
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="wa-webhook-token"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="token de verificação"
|
|
|
|
|
|
value={form.webhook_token}
|
|
|
|
|
|
onChange={(e) => handleChange('webhook_token', e.target.value)}
|
|
|
|
|
|
style={fieldStyle}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Resultado da validação */}
|
|
|
|
|
|
{validation && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
padding: '0.7rem 0.9rem',
|
|
|
|
|
|
background: validation.valid ? '#f0faf4' : '#fff2f2',
|
|
|
|
|
|
border: `1px solid ${validation.valid ? '#b6e8c8' : '#f5c6c6'}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{validation.valid ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<strong style={{ color: '#1a7a44', fontSize: '0.88rem' }}>
|
|
|
|
|
|
✅ Credenciais válidas
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<span style={{ display: 'block', color: 'var(--color-text-soft)', fontSize: '0.82rem', fontWeight: 600, marginTop: '0.2rem' }}>
|
|
|
|
|
|
{validation.verified_name && `Conta: ${validation.verified_name}`}
|
|
|
|
|
|
{validation.phone_number && ` · ${validation.phone_number}`}
|
|
|
|
|
|
{validation.quality_rating && ` · Qualidade: ${validation.quality_rating}`}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<strong style={{ color: '#b00020', fontSize: '0.88rem' }}>
|
|
|
|
|
|
❌ Credenciais inválidas
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<span style={{ display: 'block', color: 'var(--color-text-soft)', fontSize: '0.82rem', fontWeight: 600, marginTop: '0.2rem' }}>
|
|
|
|
|
|
{validation.error || 'Verifique o Token e o Phone Number ID'}
|
|
|
|
|
|
{validation.code ? ` (código ${validation.code})` : ''}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Feedback de erro geral */}
|
|
|
|
|
|
{error && (
|
|
|
|
|
|
<p style={{ margin: 0, color: '#b00020', fontWeight: 700, fontSize: '0.88rem' }}>
|
|
|
|
|
|
{error}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Feedback de sucesso */}
|
|
|
|
|
|
{saveSuccess && (
|
|
|
|
|
|
<p style={{ margin: 0, color: '#1a7a44', fontWeight: 700, fontSize: '0.88rem' }}>
|
|
|
|
|
|
✅ Configuração salva com sucesso!
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Ações */}
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.65rem', justifyContent: 'flex-end', flexWrap: 'wrap' }}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleValidate}
|
|
|
|
|
|
disabled={!canValidate || validating}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid #20a45b44',
|
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
|
padding: '0.65rem 0.9rem',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
color: canValidate ? '#20a45b' : 'var(--color-text-soft)',
|
|
|
|
|
|
fontWeight: 800,
|
|
|
|
|
|
cursor: canValidate && !validating ? 'pointer' : 'not-allowed',
|
|
|
|
|
|
opacity: canValidate && !validating ? 1 : 0.6,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{validating ? 'Validando…' : 'Validar credenciais'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
|
disabled={!canSave}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
|
padding: '0.65rem 1rem',
|
|
|
|
|
|
background: canSave ? 'var(--color-primary)' : 'var(--color-border)',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontWeight: 800,
|
|
|
|
|
|
cursor: canSave ? 'pointer' : 'not-allowed',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{saving ? 'Salvando…' : 'Salvar'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|