196 lines
13 KiB
TypeScript
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>;
|
|
}
|