"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. */ 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() { const allowed = useCan("notification:send"); const [flags, setFlags] = useState({ debug: true }); 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}
}

Flags del envío

Dispara los cuatro envíos en orden (pagos pendientes, confirmación de pago, estado de cuenta, fideicomiso) con estos mismos flags. Si uno falla, los demás continúan.
{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)}
          
)}
); }