FEAT: Modal de Config do Whatsapp adicionado
Some checks are pending
Deploy Dev / deploy (push) Waiting to run

This commit is contained in:
Rafael Alves Lopes 2026-06-22 15:22:54 -03:00
parent 02f7955d67
commit 1d0e5f27d7
3 changed files with 406 additions and 1 deletions

View File

@ -0,0 +1,370 @@
import { useEffect, useState } from 'react';
import {
getWhatsappConfig,
saveWhatsappConfig,
validateWhatsappConfig,
} 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: '',
});
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 || '',
}));
})
.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);
try {
const result = await validateWhatsappConfig({
access_token: form.access_token,
phone_number_id: form.phone_number_id,
api_version: form.api_version || 'v25.0',
});
setValidation(result);
} catch {
setValidation({ valid: false, error: 'Erro de conexão com a Meta API' });
} finally {
setValidating(false);
}
}
async function handleSave() {
setSaving(true);
setSaveSuccess(false);
setError('');
try {
await saveWhatsappConfig(form);
setSaveSuccess(true);
setTimeout(onClose, 1200);
} catch {
setError('Erro ao salvar. Verifique os campos e tente novamente.');
} 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>
<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>
);
}

View File

@ -31,6 +31,7 @@ import {
} from '../services/adminAccessService';
import { useViewport } from '../../../shared/hooks/useViewport';
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
import { WhatsappConfigModal } from '../components/WhatsappConfigModal';
const selectStyle = {
width: '100%',
@ -256,6 +257,7 @@ export function AdminPage() {
const [authConfigModal, setAuthConfigModal] = useState(null);
const [integrationNotice, setIntegrationNotice] = useState('');
const [configurationModal, setConfigurationModal] = useState(null);
const [whatsappConfigModal, setWhatsappConfigModal] = useState(false);
useEffect(() => {
let isMounted = true;
@ -1709,7 +1711,7 @@ export function AdminPage() {
type="button"
onClick={() => {
if (item.id === 'whatsapp') {
navigate('/admin/whatsapp');
setWhatsappConfigModal(true);
return;
}
@ -1732,6 +1734,10 @@ export function AdminPage() {
})}
</div>
{whatsappConfigModal && (
<WhatsappConfigModal onClose={() => setWhatsappConfigModal(false)} />
)}
{configurationModal ? (
<div
role="dialog"

View File

@ -0,0 +1,29 @@
import { apiRequest } from '../../../shared/services/apiClient';
export async function getWhatsappConfig() {
return apiRequest('/admin/whatsapp-config', {
fallbackMessage: 'Falha ao carregar configuração do WhatsApp',
});
}
export async function saveWhatsappConfig(fields) {
const body = {};
if (fields.access_token) body.access_token = fields.access_token;
if (fields.phone_number_id) body.phone_number_id = fields.phone_number_id;
if (fields.api_version) body.api_version = fields.api_version;
if (fields.webhook_token) body.webhook_token = fields.webhook_token;
return apiRequest('/admin/whatsapp-config', {
method: 'PUT',
body: JSON.stringify(body),
fallbackMessage: 'Falha ao salvar configuração do WhatsApp',
});
}
export async function validateWhatsappConfig({ access_token, phone_number_id, api_version }) {
return apiRequest('/admin/whatsapp-config/validate', {
method: 'POST',
body: JSON.stringify({ access_token, phone_number_id, api_version }),
fallbackMessage: 'Falha ao validar credenciais',
});
}