omnichannel-frontend/src/modules/management/components/WhatsappConfigModal.jsx
Rafael Lopes faa03f80cf
Some checks are pending
Deploy Dev / deploy (push) Waiting to run
REFACTOR: Ajustes de borda e modal de novo template
- Ajustado border Radius de todos componentes de 28 para 20px
- Criado modal para quando for criado um novo template
2026-06-24 14:46:00 -03:00

404 lines
13 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 { useEffect, useState } from 'react';
import {
getWhatsappConfig,
saveWhatsappConfig,
} from '../services/whatsappConfigService';
const fieldStyle = {
width: '100%',
border: '1px solid var(--color-border)',
borderRadius: 9,
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: '',
waba_id: '',
});
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 || '',
waba_id: data.waba_id || '',
}));
})
.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);
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`;
try {
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,
});
}
} catch {
setValidation({ valid: false, error: 'Sem acesso à internet para validar' });
} finally {
setValidating(false);
}
}
async function handleSave() {
setSaving(true);
setSaveSuccess(false);
setError('');
try {
await saveWhatsappConfig(form);
setSaveSuccess(true);
setTimeout(onClose, 1200);
} catch (err) {
const msg = err?.message || '';
setError(`Erro ao salvar${msg ? `: ${msg}` : '. Verifique se a migration 025 foi executada no banco.'}`);
} 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: 16,
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: 10,
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: 9,
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>
<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>
<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: 9,
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: 10,
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: 10,
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>
);
}