diff --git a/web-v2/src/pages/DocumentDeliveryPage.tsx b/web-v2/src/pages/DocumentDeliveryPage.tsx index baeaa3c..9c26fe9 100644 --- a/web-v2/src/pages/DocumentDeliveryPage.tsx +++ b/web-v2/src/pages/DocumentDeliveryPage.tsx @@ -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 = { +const statusLabel: Record = { PENDING: 'Pendiente', WAITING_RECIPIENT: 'Falta destinatario', WAITING_TRANSPORT: 'SMTP sin configurar', @@ -20,125 +28,165 @@ const statusLabel: Record = { 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(null); - const [items, setItems] = useState([]); + const [settings, setSettings] = useState(null); + const [smtp, setSmtp] = useState(null); + const [items, setItems] = useState([]); const [officeEmail, setOfficeEmail] = useState(''); - const [directorEmail, setDirectorEmail] = useState(''); + const [host, setHost] = useState(''); + const [port, setPort] = useState('587'); + const [securityMode, setSecurityMode] = useState('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[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 ; return
-
-
- DOCUMENTOS -

Entrega documental

-

Configurá los destinatarios institucionales y controlá el envío automático de Actas e Informes.

-
- {canManage && } -
- {error && {error}} - {success && {success}} +
SUPERADMIN · CORREO

Entrega documental

Configuración institucional de SMTP, destinatarios y trazabilidad de los envíos automáticos.

{canManage && }
+ {error && {error}}{success && {success}} -
+
-
-

Destinatarios institucionales

-

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.

-
-
- - -
-

{settings?.smtpConfigured ? 'Servidor de correo configurado' : 'Servidor de correo pendiente'}{settings?.mailFrom ? ` · Remitente ${settings.mailFrom}` : ' · Falta configurar SMTP_HOST y MAIL_FROM en el servidor.'}

- {canManage &&
} +

Destinatarios institucionales

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.

+ +

{settings?.smtpConfigured ? 'Salida SMTP operativa' : 'Salida SMTP pendiente'}{settings?.mailFrom ? ` · Remitente ${settings.mailFrom}` : ''}

+ {canManage &&
}
-
-
TRAZABILIDAD

Últimas entregas

{items.length}
- {items.length === 0 - ?
Todavía no existen entregas documentales.
- :
{items.map((baseItem) => { - const item = baseItem as DeliveryWithInspector; - return - - - - - - ; - })}
DocumentoDestinoEstadoIntentos
{item.documentKind === 'ACT_PDF' ? item.actCode : item.reportCode}{item.documentKind === 'ACT_PDF' ? 'Acta PDF' : 'Informe Word'}{recipientLabel(item)}{item.recipientEmail ?? 'Sin email configurado'}{statusLabel[item.status]}{item.lastError && {item.lastError}}{item.attempts}{canManage && item.status !== 'SENT' && }
} -
+ {canManage &&
+
+
SALIDA DE EMAIL

Servidor SMTP

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.

+ {smtp?.source === 'ENVIRONMENT' && 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.} +
+ + + + + + + + +
+ +
+
+
} + + {canManage &&

Probar configuración

Envía un correo real con un archivo de prueba usando exactamente la configuración activa.

} + +
+
TRAZABILIDAD

Últimas entregas

Actas PDF a Empresa/Oficina/Inspector e INF Word editable al Inspector responsable.

{items.length}
+ {items.length === 0 ?
Todavía no existen entregas documentales.
:
{items.map((item) => )}
DocumentoDestinoEstadoIntentos
{item.documentKind === 'ACT_PDF' ? item.actCode : item.reportCode}{item.documentKind === 'ACT_PDF' ? 'Acta PDF inmutable' : 'INF Word editable'}{recipientLabel(item)}{item.recipientEmail ?? 'Sin email configurado'}{statusLabel[item.status]}{item.lastError && {item.lastError}}{item.attempts}{canManage && item.status !== 'SENT' && }
} +
; }