Add SMTP test email endpoint and mail logs in admin settings
- POST /settings/test-email: send test email to diagnose SMTP config - Admin settings: test email form + last 20 email logs with status/error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
b0ca6804fa
commit
1f3efb50da
@@ -68,6 +68,9 @@ export default function SettingsPage() {
|
||||
const [smtp, setSmtp] = useState<SmtpConfig>({ host: '', port: '587', user: '', pass: '', from: '' });
|
||||
const [showSmtpPass, setShowSmtpPass] = useState(false);
|
||||
const [savingSmtp, setSavingSmtp] = useState(false);
|
||||
const [testEmailTo, setTestEmailTo] = useState('');
|
||||
const [sendingTest, setSendingTest] = useState(false);
|
||||
const [mailLogs, setMailLogs] = useState<{ id: string; recipient: string; body: string; status: string; error?: string; created_at: string }[]>([]);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
@@ -77,13 +80,15 @@ export default function SettingsPage() {
|
||||
api.get<{ privacy: string | null; terms: string | null }>('/settings/policies'),
|
||||
api.get<SmtpConfig | null>('/settings/smtp').catch(() => null),
|
||||
api.get<{ connected: boolean }>('/verifik/status').catch(() => ({ connected: false })),
|
||||
api.get<{ data: any[] }>('/settings/message-logs?limit=20&channel=email').catch(() => ({ data: [] })),
|
||||
])
|
||||
.then(([globalData, mapsData, policiesData, smtpData, verifikStatus]) => {
|
||||
.then(([globalData, mapsData, policiesData, smtpData, verifikStatus, logsData]) => {
|
||||
setGlobal(globalData || {});
|
||||
setMapsConfigured(mapsData.configured);
|
||||
setPolicies({ privacy: policiesData.privacy || '', terms: policiesData.terms || '' });
|
||||
if (smtpData) setSmtp({ host: smtpData.host || '', port: smtpData.port || '587', user: smtpData.user || '', pass: smtpData.pass || '', from: smtpData.from || '' });
|
||||
setVerifikConnected(verifikStatus.connected);
|
||||
setMailLogs(logsData.data || []);
|
||||
})
|
||||
.catch(() => toast.error('Error al cargar configuración'))
|
||||
.finally(() => setLoading(false));
|
||||
@@ -143,6 +148,20 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const sendTestEmail = async () => {
|
||||
if (!testEmailTo.trim()) return toast.error('Ingresa un correo destino');
|
||||
setSendingTest(true);
|
||||
try {
|
||||
await api.post('/settings/test-email', { to: testEmailTo.trim() });
|
||||
toast.success(`Correo de prueba enviado a ${testEmailTo}`);
|
||||
load(); // refresh logs
|
||||
} catch (e: any) {
|
||||
toast.error(e?.message || 'Error al enviar correo de prueba');
|
||||
} finally {
|
||||
setSendingTest(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sendVerifikOtp = async () => {
|
||||
if (!verifikEmail.trim()) return toast.error('Ingresa tu email de Verifik');
|
||||
setVerifikLoading(true);
|
||||
@@ -469,6 +488,70 @@ export default function SettingsPage() {
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* ── Test de correo + Logs ── */}
|
||||
<section className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Mail size={18} className="text-muted-foreground" />
|
||||
Probar configuración SMTP
|
||||
</CardTitle>
|
||||
<CardDescription>Envía un correo de prueba para verificar que el servidor SMTP funciona.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="destino@ejemplo.com"
|
||||
value={testEmailTo}
|
||||
onChange={(e) => setTestEmailTo(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={sendTestEmail} disabled={sendingTest}>
|
||||
{sendingTest ? 'Enviando...' : 'Enviar prueba'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{mailLogs.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">Últimos 20 correos enviados</p>
|
||||
<div className="rounded-lg border overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2 font-medium">Destinatario</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Asunto</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Estado</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Fecha</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{mailLogs.map((log) => (
|
||||
<tr key={log.id} className="border-t hover:bg-muted/30">
|
||||
<td className="px-3 py-2 text-muted-foreground">{log.recipient}</td>
|
||||
<td className="px-3 py-2 max-w-[200px] truncate">{log.body}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
log.status === 'sent' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{log.status === 'sent' ? '✓ enviado' : '✗ error'}
|
||||
</span>
|
||||
{log.error && <p className="text-xs text-destructive mt-0.5 truncate max-w-[180px]" title={log.error}>{log.error}</p>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-muted-foreground whitespace-nowrap">
|
||||
{new Date(log.created_at).toLocaleString('es-CO', { dateStyle: 'short', timeStyle: 'short' })}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* ── Verifik ── */}
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Verifik — Validación de profesionales</h2>
|
||||
|
||||
Reference in New Issue
Block a user