"use client"; import { useCallback, useEffect, useState } from "react"; import { useCan } from "@/lib/abilities"; import { formatDateTime, NOTIFICATION_STATUS_LABELS, NOTIFICATION_TYPE_LABELS, } from "@/lib/labels"; import { NotificationLogPanel } from "@/components/NotificationLogPanel"; import { AdminEmailsSetting } from "@/components/AdminEmailsSetting"; import { getNotificationStats, runAccountStatus, runAllNotifications, runOutstandingPayments, runPaymentConfirmation, runTrustConfirmation, SERVICIOS_LOG_SCOPE, } from "@/lib/api"; import type { NotificationFlags, NotificationJobResponse, NotificationRunAllResponse, NotificationStats, } from "@/lib/api"; /** * Mass email notifications — the "Servicios" half of /notificaciones. Manual * triggers for the four jobs plus a paged log browser. Gated on * `notification:send`; a STAFF viewer sees the read-only log table but not the * trigger buttons. * * The send flags come from the shell above the tabs — they are shared with the * pólizas half — so this component only consumes them. */ type JobKind = "outstanding" | "payment" | "account" | "trust"; interface JobDef { kind: JobKind; title: string; endpoint: string; description: string; servicio: "Clientes" | "Fideicomiso"; flagsHint?: string; } const JOBS: JobDef[] = [ { kind: "outstanding", title: "Pagos pendientes", endpoint: "sendOutstandingPaymentAlerts", servicio: "Clientes", description: "Clientes con al menos un movimiento marcado como pendiente (outstanding). Equivale a la columna NOPAGO=1 del antiguo datosfreak.", }, { kind: "payment", title: "Confirmación de pago", endpoint: "sendPaymentConfirmation", servicio: "Clientes", description: "Clientes con un crédito (abono) en las últimas 24 horas. Un correo por cliente con el pago más reciente.", }, { kind: "account", title: "Estado de cuenta", endpoint: "sendAccountStatus", servicio: "Clientes", description: "Alerta amarilla (DEBAJO DEL TIPO) los miércoles y roja (EN ROJO) lunes/miércoles/viernes. El flag ignoreDayRestriction salta los gates.", flagsHint: "Solo este job respeta ignoreDayRestriction y useEmailLimit.", }, { kind: "trust", title: "Confirmación fideicomiso", endpoint: "sendConfirmTrustPayment", servicio: "Fideicomiso", description: "Clientes con TrustAccount que recibieron un crédito en el dominio TRUST en las últimas 24 horas.", }, ]; /** Job title by kind — used by the run-all summary, which only carries kinds. */ const JOB_TITLES: Record = JOBS.reduce( (acc, j) => ({ ...acc, [j.kind]: j.title }), {} as Record, ); export function NotificacionesServicios({ flags }: { flags: NotificationFlags }) { const allowed = useCan("notification:send"); const [stats, setStats] = useState(null); /** Raised after every run so the shared log panel reloads. */ const [logToken, setLogToken] = useState(0); const [busy, setBusy] = useState(null); const [lastResult, setLastResult] = useState< NotificationJobResponse | NotificationRunAllResponse | null >(null); const [error, setError] = useState(null); const refresh = useCallback(async () => { try { setStats(await getNotificationStats(SERVICIOS_LOG_SCOPE)); setLogToken((t) => t + 1); setError(null); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } }, []); useEffect(() => { void refresh(); }, [refresh]); const run = useCallback( async (job: JobDef) => { if (!allowed) return; setBusy(job.kind); setError(null); try { let res: NotificationJobResponse; if (job.kind === "outstanding") res = await runOutstandingPayments(flags); else if (job.kind === "payment") res = await runPaymentConfirmation(flags); else if (job.kind === "account") res = await runAccountStatus(flags); else res = await runTrustConfirmation(flags); setLastResult(res); await refresh(); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(null); } }, [allowed, flags, refresh], ); // "Ejecutar todos" — one POST, the API runs the four jobs sequentially with // the same flags. Confirmation only matters when debug is off, since that // is the case where real customers receive mail. const runAll = useCallback(async () => { if (!allowed) return; if (!flags.debug) { const ok = window.confirm( "debug está desactivado: los cuatro envíos irán a los correos reales de los clientes. ¿Ejecutar todos?", ); if (!ok) return; } setBusy("all"); setError(null); try { const res = await runAllNotifications(flags); setLastResult(res); await refresh(); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(null); } }, [allowed, flags, refresh]); return (

Disparo manual de los cuatro envíos equivalentes a los scripts PHP de{" "} email.notifications/. Cada ejecución registra todas las filas (enviado, fallido, omitido) en email_notification_log.

{!allowed && (
Tu rol no incluye notification:send. Solo puedes ver el registro. Para disparar envíos pide a un MANAGER/ADMIN.
)} {error &&
{error}
}

Ejecutar ahora

Dispara los cuatro envíos en orden (pagos pendientes, confirmación de pago, estado de cuenta, fideicomiso) con los flags de arriba. Si uno falla, los demás continúan. Es lo mismo que ejecuta la corrida programada de Servicios.
{JOBS.map((j) => (
{j.title} {j.servicio}

{j.description}

{j.flagsHint && (

{j.flagsHint}

)}
))}
{stats && (

Estado del transporte

  • SES configurado:{" "} {stats.transport.available ? "sí" : "no"} {stats.transport.devFallback && " (fallback dev: stdout)"}
  • Último envío registrado:{" "} {stats.lastRun ? `${NOTIFICATION_TYPE_LABELS[stats.lastRun.notificationType]} — ${formatDateTime(stats.lastRun.sendDate)}` : "—"}
  • Totales:{" "} {stats.byStatus.map((s) => ( {NOTIFICATION_STATUS_LABELS[s.status]}: {s._count._all} ))}
)} {lastResult && (

Última respuesta

{lastResult.type === "RUN_ALL" && (
  • Totales: enviados {lastResult.sent} · omitidos{" "} {lastResult.skipped} · fallidos {lastResult.failed} {lastResult.errors > 0 && ` · jobs con error ${lastResult.errors}`}
  • {lastResult.jobs.map((j) => (
  • {JOB_TITLES[j.kind]}:{" "} {j.ok && j.result ? ( <> enviados {j.result.sent} · omitidos {j.result.skipped} · fallidos {j.result.failed} ) : ( error — {j.error} )}
  • ))}
)}
            {JSON.stringify(lastResult, null, 2)}
          
)}
); }