F4: make SMTP configurable from Superadmin and remove Director
This commit is contained in:
@@ -1,17 +1,25 @@
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
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 {
|
||||
getDocumentDeliverySettings,
|
||||
listDocumentDeliveries,
|
||||
retryDocumentDelivery,
|
||||
retryPendingDocumentDeliveries,
|
||||
updateDocumentDeliverySettings,
|
||||
} from '../lib/api';
|
||||
import type { DocumentDeliveryItem, DocumentDeliverySettings } from '../lib/api';
|
||||
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<DocumentDeliveryItem['status'], string> = {
|
||||
const statusLabel: Record<DocumentDeliveryItemF4['status'], string> = {
|
||||
PENDING: 'Pendiente',
|
||||
WAITING_RECIPIENT: 'Falta destinatario',
|
||||
WAITING_TRANSPORT: 'SMTP sin configurar',
|
||||
@@ -20,125 +28,165 @@ const statusLabel: Record<DocumentDeliveryItem['status'], string> = {
|
||||
FAILED: 'Error',
|
||||
};
|
||||
|
||||
type DeliveryWithInspector = DocumentDeliveryItem & {
|
||||
recipientUserId?: string | null;
|
||||
recipientUserName?: string | null;
|
||||
};
|
||||
|
||||
function recipientLabel(item: DeliveryWithInspector): string {
|
||||
function recipientLabel(item: DocumentDeliveryItemF4): string {
|
||||
if (item.recipientAssetName) return item.recipientAssetName;
|
||||
if (item.recipientUserName) return item.recipientUserName;
|
||||
const kind = String(item.recipientKind);
|
||||
if (kind === 'COMPANY') return 'Empresa sin identificar';
|
||||
if (kind === 'OFFICE') return 'Oficina';
|
||||
if (kind === 'INSPECTOR') return 'Inspector responsable';
|
||||
return 'Director';
|
||||
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 } = useAuth();
|
||||
const canManage = hasPermission('document_delivery.manage');
|
||||
const [settings, setSettings] = useState<DocumentDeliverySettings | null>(null);
|
||||
const [items, setItems] = useState<DocumentDeliveryItem[]>([]);
|
||||
const [settings, setSettings] = useState<DocumentDeliverySettingsF4 | null>(null);
|
||||
const [smtp, setSmtp] = useState<PublicSmtpSettings | null>(null);
|
||||
const [items, setItems] = useState<DocumentDeliveryItemF4[]>([]);
|
||||
const [officeEmail, setOfficeEmail] = useState('');
|
||||
const [directorEmail, setDirectorEmail] = 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] = await Promise.all([
|
||||
getDocumentDeliverySettings(),
|
||||
listDocumentDeliveries(),
|
||||
const [currentSettings, outbox, smtpSettings] = await Promise.all([
|
||||
getDocumentDeliverySettingsF4(),
|
||||
listDocumentDeliveriesF4(),
|
||||
canManage ? getSmtpSettings() : Promise.resolve(null),
|
||||
]);
|
||||
setSettings(currentSettings);
|
||||
setOfficeEmail(currentSettings.officeEmail ?? '');
|
||||
setDirectorEmail(currentSettings.directorEmail ?? '');
|
||||
setItems(outbox.data);
|
||||
if (smtpSettings) applySmtp(smtpSettings);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
load().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false));
|
||||
}, []);
|
||||
}, [canManage]);
|
||||
|
||||
const save = async (event: FormEvent) => {
|
||||
const saveOffice = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
setSaving(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setSaving(true); setError(''); setSuccess('');
|
||||
try {
|
||||
const currentSettings = await updateDocumentDeliverySettings({
|
||||
officeEmail: officeEmail || null,
|
||||
directorEmail: directorEmail || null,
|
||||
});
|
||||
setSettings(currentSettings);
|
||||
setSuccess('Destinatarios institucionales actualizados');
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
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 (!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 (!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('');
|
||||
setSaving(true); setError(''); setSuccess('');
|
||||
try {
|
||||
const result = await retryPendingDocumentDeliveries();
|
||||
const result = await retryPendingDocumentDeliveriesF4();
|
||||
await load();
|
||||
setSuccess(`Se procesaron ${result.processed} entrega/s pendientes.`);
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
} 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">DOCUMENTOS</span>
|
||||
<h1>Entrega documental</h1>
|
||||
<p>Configurá los destinatarios institucionales y controlá el envío automático de Actas e Informes.</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>}
|
||||
<div className="page-heading"><div><span className="eyebrow">SUPERADMIN · CORREO</span><h1>Entrega documental</h1><p>Configuración institucional de SMTP, destinatarios y trazabilidad de los envíos automáticos.</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={save}>
|
||||
<form className="panel form-panel" onSubmit={saveOffice}>
|
||||
<div className="form-section">
|
||||
<div>
|
||||
<h2>Destinatarios institucionales</h2>
|
||||
<p className="section-copy">El email de cada empresa se configura dentro de su ficha de Inventario. El Inspector responsable usa automáticamente el email declarado en su Usuario. Las credenciales SMTP permanecen fuera de la base de datos.</p>
|
||||
</div>
|
||||
<div className="form-grid">
|
||||
<label className="field"><span>Email de oficina</span><input type="email" value={officeEmail} onChange={(event) => setOfficeEmail(event.target.value)} disabled={!canManage} /></label>
|
||||
<label className="field"><span>Email del Director de Hidrocarburos</span><input type="email" value={directorEmail} onChange={(event) => setDirectorEmail(event.target.value)} disabled={!canManage} /></label>
|
||||
</div>
|
||||
<div className="temporal-notice"><Icon name={settings?.smtpConfigured ? 'check' : 'alert'} /><p><strong>{settings?.smtpConfigured ? 'Servidor de correo configurado' : 'Servidor de correo pendiente'}</strong>{settings?.mailFrom ? ` · Remitente ${settings.mailFrom}` : ' · Falta configurar SMTP_HOST y MAIL_FROM en el servidor.'}</p></div>
|
||||
{canManage && <div className="form-actions"><button className="button primary" disabled={saving}><Icon name="check" />{saving ? 'Guardando…' : 'Guardar destinatarios'}</button></div>}
|
||||
<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>
|
||||
|
||||
<div className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">TRAZABILIDAD</span><h2>Últimas entregas</h2></div><span>{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></th></tr></thead><tbody>{items.map((baseItem) => {
|
||||
const item = baseItem as DeliveryWithInspector;
|
||||
return <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' : 'Informe Word'}</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 retryDocumentDelivery(item.id); await load(); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }}>Reintentar</button>}</td>
|
||||
</tr>;
|
||||
})}</tbody></table></div>}
|
||||
</div>
|
||||
{canManage && <form className="panel form-panel" onSubmit={saveSmtp}>
|
||||
<div className="form-section">
|
||||
<div><span className="eyebrow">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>}
|
||||
|
||||
{canManage && <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>}
|
||||
|
||||
<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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user