Files
dh-inspeccion-v2/web-v2/src/pages/DocumentDeliveryPage.tsx
T

196 lines
13 KiB
TypeScript

import { useEffect, useState } from 'react';
import type { FormEvent } from 'react';
import { useAuth } from '../auth/AuthContext';
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { SearchableSelect } from '../components/SearchableSelect';
import {
getDocumentDeliverySettingsF4,
listDocumentDeliveriesF4,
retryDocumentDeliveryF4,
retryPendingDocumentDeliveriesF4,
updateDocumentDeliverySettingsF4,
} from '../lib/documentDeliveryF4Api';
import type { DocumentDeliveryItemF4, DocumentDeliverySettingsF4 } from '../lib/documentDeliveryF4Api';
import {
getSmtpSettings,
saveSmtpSettings,
testSmtpSettings,
} from '../lib/smtpSettingsApi';
import type { PublicSmtpSettings, SmtpSecurityMode } from '../lib/smtpSettingsApi';
const statusLabel: Record<DocumentDeliveryItemF4['status'], string> = {
PENDING: 'Pendiente',
WAITING_RECIPIENT: 'Falta destinatario',
WAITING_TRANSPORT: 'SMTP sin configurar',
WAITING_ARTIFACT: 'Documento pendiente',
SENT: 'Enviado',
FAILED: 'Error',
};
function recipientLabel(item: DocumentDeliveryItemF4): string {
if (item.recipientAssetName) return item.recipientAssetName;
if (item.recipientUserName) return item.recipientUserName;
if (item.recipientKind === 'COMPANY') return 'Empresa sin identificar';
if (item.recipientKind === 'OFFICE') return 'Oficina';
return 'Inspector responsable';
}
function defaultPort(mode: SmtpSecurityMode): number {
return mode === 'TLS' ? 465 : mode === 'STARTTLS' ? 587 : 25;
}
export function DocumentDeliveryPage() {
const { hasPermission, user } = useAuth();
const canManage = hasPermission('document_delivery.manage');
const canSmtpAdmin = Boolean(user?.roles.includes('admin'));
const [settings, setSettings] = useState<DocumentDeliverySettingsF4 | null>(null);
const [smtp, setSmtp] = useState<PublicSmtpSettings | null>(null);
const [items, setItems] = useState<DocumentDeliveryItemF4[]>([]);
const [officeEmail, setOfficeEmail] = useState('');
const [host, setHost] = useState('');
const [port, setPort] = useState('587');
const [securityMode, setSecurityMode] = useState<SmtpSecurityMode>('STARTTLS');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [fromName, setFromName] = useState('DH Inspección');
const [fromEmail, setFromEmail] = useState('');
const [replyTo, setReplyTo] = useState('');
const [smtpEnabled, setSmtpEnabled] = useState(true);
const [testEmail, setTestEmail] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const applySmtp = (value: PublicSmtpSettings) => {
setSmtp(value);
setHost(value.host ?? '');
setPort(String(value.port ?? defaultPort(value.securityMode ?? 'STARTTLS')));
setSecurityMode(value.securityMode ?? 'STARTTLS');
setUsername(value.username ?? '');
setPassword('');
setFromName(value.fromName ?? 'DH Inspección');
setFromEmail(value.fromEmail ?? '');
setReplyTo(value.replyTo ?? '');
setSmtpEnabled(value.enabled);
};
const load = async () => {
const [currentSettings, outbox, smtpSettings] = await Promise.all([
getDocumentDeliverySettingsF4(),
listDocumentDeliveriesF4(),
canSmtpAdmin ? getSmtpSettings() : Promise.resolve(null),
]);
setSettings(currentSettings);
setOfficeEmail(currentSettings.officeEmail ?? '');
setItems(outbox.data);
if (smtpSettings) applySmtp(smtpSettings);
};
useEffect(() => {
load().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false));
}, [canSmtpAdmin]);
const saveOffice = async (event: FormEvent) => {
event.preventDefault();
setSaving(true); setError(''); setSuccess('');
try {
const updated = await updateDocumentDeliverySettingsF4(officeEmail.trim() || null);
setSettings(updated);
setSuccess('Destinatario institucional de oficina actualizado.');
} catch (requestError) { setError(errorMessage(requestError)); }
finally { setSaving(false); }
};
const saveSmtp = async (event: FormEvent) => {
event.preventDefault();
if (!canSmtpAdmin || !host.trim() || !fromName.trim() || !fromEmail.trim()) return;
setSaving(true); setError(''); setSuccess('');
try {
const input: Parameters<typeof saveSmtpSettings>[0] = {
host: host.trim(),
port: Number(port),
securityMode,
username: username.trim() || null,
fromName: fromName.trim(),
fromEmail: fromEmail.trim(),
replyTo: replyTo.trim() || null,
enabled: smtpEnabled,
};
if (password) input.password = password;
const updated = await saveSmtpSettings(input);
applySmtp(updated);
setSuccess('Configuración SMTP guardada. La contraseña no se devuelve ni se muestra en pantalla.');
const refreshed = await getDocumentDeliverySettingsF4();
setSettings(refreshed);
} catch (requestError) { setError(errorMessage(requestError)); }
finally { setSaving(false); }
};
const sendTest = async (event: FormEvent) => {
event.preventDefault();
if (!canSmtpAdmin || !testEmail.trim()) return;
setSaving(true); setError(''); setSuccess('');
try {
const result = await testSmtpSettings(testEmail.trim());
setSuccess(`Prueba SMTP enviada a ${result.recipient}.`);
} catch (requestError) { setError(errorMessage(requestError)); }
finally { setSaving(false); }
};
const retryAll = async () => {
setSaving(true); setError(''); setSuccess('');
try {
const result = await retryPendingDocumentDeliveriesF4();
await load();
setSuccess(`Se procesaron ${result.processed} entrega/s pendientes.`);
} catch (requestError) { setError(errorMessage(requestError)); }
finally { setSaving(false); }
};
if (loading) return <LoadingBlock label="Cargando entrega documental…" />;
return <section className="narrow-section">
<div className="page-heading"><div><span className="eyebrow">ENTREGA DOCUMENTAL</span><h1>Correo y envíos</h1><p>Destinatarios institucionales, trazabilidad de envíos y configuración técnica de correo reservada al Superadmin.</p></div>{canManage && <button className="button secondary" onClick={retryAll} disabled={saving}><Icon name="history" />Reintentar pendientes</button>}</div>
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
<form className="panel form-panel" onSubmit={saveOffice}>
<div className="form-section">
<div><h2>Destinatarios institucionales</h2><p className="section-copy">La empresa usa el email de su ficha de Inventario. El Inspector usa el email de su Usuario. Acá se configura únicamente la copia institucional de oficina.</p></div>
<label className="field"><span>Email de oficina</span><input type="email" value={officeEmail} onChange={(event) => setOfficeEmail(event.target.value)} disabled={!canManage} /></label>
<div className="temporal-notice"><Icon name={settings?.smtpConfigured ? 'check' : 'alert'} /><p><strong>{settings?.smtpConfigured ? 'Salida SMTP operativa' : 'Salida SMTP pendiente'}</strong>{settings?.mailFrom ? ` · Remitente ${settings.mailFrom}` : ''}</p></div>
{canManage && <div className="form-actions"><button className="button primary" disabled={saving}>Guardar destinatario</button></div>}
</div>
</form>
{canSmtpAdmin && <form className="panel form-panel" onSubmit={saveSmtp}>
<div className="form-section">
<div><span className="eyebrow">SUPERADMIN · SALIDA DE EMAIL</span><h2>Servidor SMTP</h2><p className="section-copy">La contraseña se cifra en el servidor con AES-256-GCM. Al volver a esta pantalla sólo se informa si existe una clave guardada; nunca se devuelve su contenido.</p></div>
{smtp?.source === 'ENVIRONMENT' && <Alert type="info">Actualmente se está usando la configuración del servidor. Al guardar este formulario, la configuración de base de datos administrable pasará a tener prioridad.</Alert>}
<div className="form-grid">
<label className="field"><span>Host SMTP</span><input value={host} onChange={(event) => setHost(event.target.value)} required maxLength={255} placeholder="smtp.ejemplo.com" /></label>
<label className="field"><span>Puerto</span><input type="number" min={1} max={65535} value={port} onChange={(event) => setPort(event.target.value)} required /></label>
<label className="field"><span>Seguridad</span><SearchableSelect value={securityMode} onChange={(event) => { const mode = event.target.value as SmtpSecurityMode; setSecurityMode(mode); setPort(String(defaultPort(mode))); }}><option value="STARTTLS">STARTTLS · habitual 587</option><option value="TLS">TLS implícito · habitual 465</option><option value="NONE">Sin cifrado · sólo redes controladas</option></SearchableSelect></label>
<label className="field"><span>Usuario <em>opcional</em></span><input value={username} onChange={(event) => setUsername(event.target.value)} maxLength={255} autoComplete="username" /></label>
<label className="field"><span>Contraseña {smtp?.hasPassword && <em>· ya existe una guardada</em>}</span><input type="password" value={password} onChange={(event) => setPassword(event.target.value)} maxLength={1000} autoComplete="new-password" placeholder={smtp?.hasPassword ? 'Dejar vacío para conservar la actual' : 'Contraseña SMTP'} /></label>
<label className="field"><span>Nombre remitente</span><input value={fromName} onChange={(event) => setFromName(event.target.value)} required maxLength={200} /></label>
<label className="field"><span>Email remitente</span><input type="email" value={fromEmail} onChange={(event) => setFromEmail(event.target.value)} required maxLength={320} /></label>
<label className="field"><span>Reply-To <em>opcional</em></span><input type="email" value={replyTo} onChange={(event) => setReplyTo(event.target.value)} maxLength={320} /></label>
</div>
<label className="inspection-member selected"><input type="checkbox" checked={smtpEnabled} onChange={(event) => setSmtpEnabled(event.target.checked)} /><span><strong>Habilitar envío de correo</strong><small>Si se desactiva, las entregas quedan en espera sin perderse.</small></span></label>
<div className="form-actions"><button className="button primary" disabled={saving || !host.trim() || !fromName.trim() || !fromEmail.trim()}>{saving ? 'Guardando…' : 'Guardar SMTP'}</button></div>
</div>
</form>}
{canSmtpAdmin && <form className="panel form-panel" onSubmit={sendTest}><div className="form-section"><div><h2>Probar configuración</h2><p className="section-copy">Envía un correo real con un archivo de prueba usando exactamente la configuración activa.</p></div><div className="form-grid"><label className="field"><span>Email de prueba</span><input type="email" value={testEmail} onChange={(event) => setTestEmail(event.target.value)} required placeholder="tu@email.com" /></label></div><div className="form-actions"><button className="button secondary" disabled={saving || !testEmail.trim()}>Enviar prueba</button></div></div></form>}
{!canSmtpAdmin && <div className="temporal-notice"><Icon name="lock" /><p><strong>SMTP reservado al Superadmin.</strong> Los usuarios operativos pueden consultar la trazabilidad y, según permisos, gestionar entregas, pero no ver ni modificar credenciales de correo.</p></div>}
<article className="panel">
<div className="panel-heading"><div><span className="eyebrow">TRAZABILIDAD</span><h2>Últimas entregas</h2><p className="section-copy">Actas PDF a Empresa/Oficina/Inspector e INF Word editable al Inspector responsable.</p></div><span className="count-pill">{items.length}</span></div>
{items.length === 0 ? <div className="inline-empty">Todavía no existen entregas documentales.</div> : <div className="table-wrap"><table><thead><tr><th>Documento</th><th>Destino</th><th>Estado</th><th>Intentos</th><th /></tr></thead><tbody>{items.map((item) => <tr key={item.id}><td><strong>{item.documentKind === 'ACT_PDF' ? item.actCode : item.reportCode}</strong><small className="block-muted">{item.documentKind === 'ACT_PDF' ? 'Acta PDF inmutable' : 'INF Word editable'}</small></td><td>{recipientLabel(item)}<small className="block-muted">{item.recipientEmail ?? 'Sin email configurado'}</small></td><td><span className={`status-badge ${item.status === 'SENT' ? 'active' : item.status === 'FAILED' ? 'inactive' : 'pending'}`}>{statusLabel[item.status]}</span>{item.lastError && <small className="block-muted">{item.lastError}</small>}</td><td>{item.attempts}</td><td>{canManage && item.status !== 'SENT' && <button className="button text" type="button" disabled={saving} onClick={async () => { setSaving(true); try { await retryDocumentDeliveryF4(item.id); await load(); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }}>Reintentar</button>}</td></tr>)}</tbody></table></div>}
</article>
</section>;
}