The "Flags del envío" panel lived inside the Servicios tab and only
governed the four bulk jobs. The pólizas half had no debug at all, so
there was no way to test a renewal notice without mailing a real
customer. The panel now lives in the /notificaciones shell above the
tabs and both halves read it.
`debug` on the renewal path diverts to the same override inbox as the
servicios jobs and deliberately does NOT write the `RenewalNotice` row
or advance the sweep's `lastSuccessfulAt` — the customer was not
notified, so nothing may gate the letter they are still owed.
`ignoreDayRestriction` and `useEmailLimit` stay estado-de-cuenta-only
and are labelled as such.
Both automatic sweeps are now operator-editable. The renewal cadence
was a `@Cron("0 6 * * *")` literal and servicios had no automatic run
at all; both now resolve through `NotificationScheduleService`, which
stores the cadence in `app_settings` and reinstalls the cron job on
save — no redeploy, no restart. Defaults preserve current behaviour:
pólizas 06:00 daily, servicios off. A scheduled run never inherits the
UI flags; it always sends for real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useCan } from "@/lib/abilities";
|
|
import { NotificacionesServicios } from "@/components/NotificacionesServicios";
|
|
import { NotificacionesPolizas } from "@/components/NotificacionesPolizas";
|
|
import { NotificationFlagsCard } from "@/components/NotificationFlagsCard";
|
|
import { NotificationScheduleCard } from "@/components/NotificationScheduleCard";
|
|
import type { NotificationFlags } from "@/lib/api";
|
|
|
|
/**
|
|
* Notificaciones — one screen, two subsections:
|
|
*
|
|
* - **servicios** — the four mass-email jobs against customer ledgers
|
|
* (pagos pendientes, confirmación de pago, estado de cuenta, fideicomiso).
|
|
* - **polizas** — renewal notices, 30/15 days before and 7 days after a
|
|
* policy expires.
|
|
*
|
|
* Both are "tell a customer something by email", so they are modes of one
|
|
* screen rather than two menu entries. `/renovaciones` still resolves here on
|
|
* the pólizas tab so old bookmarks keep working (same pattern as Captura).
|
|
*
|
|
* Two things are owned by this shell rather than by a tab, because they are
|
|
* true of every notification: the send flags (`debug` in particular, which the
|
|
* pólizas half honours exactly like the servicios half) and the automatic
|
|
* cadence of both sweeps. Keeping the flags here also means switching tabs
|
|
* cannot silently drop a `debug` the operator just ticked.
|
|
*/
|
|
|
|
export type NotificacionesTab = "servicios" | "polizas";
|
|
|
|
const TAB_HINT: Record<NotificacionesTab, string> = {
|
|
servicios:
|
|
"Envíos masivos de cobranza y estado de cuenta a los clientes de servicios.",
|
|
polizas: "Avisos de renovación de pólizas: 30 y 15 días antes, 7 días después.",
|
|
};
|
|
|
|
export function Notificaciones({
|
|
initialTab = "servicios",
|
|
}: {
|
|
initialTab?: NotificacionesTab;
|
|
}) {
|
|
const canNotify = useCan("notification:send");
|
|
const canRenew = useCan("renewal:send");
|
|
|
|
// Gating is cosmetic (the API enforces every send), but a user who only has
|
|
// one of the two abilities should land on the tab they can actually use.
|
|
// Servicios stays visible read-only for STAFF, who can browse the log.
|
|
const tabs: { key: NotificacionesTab; label: string }[] = [
|
|
{ key: "servicios", label: "Servicios" },
|
|
...(canRenew ? [{ key: "polizas" as const, label: "Pólizas" }] : []),
|
|
];
|
|
|
|
const [tab, setTab] = useState<NotificacionesTab>(
|
|
tabs.some((t) => t.key === initialTab) ? initialTab : "servicios",
|
|
);
|
|
// Defaults to debug ON: the safe end of the switch is the one you land on.
|
|
const [flags, setFlags] = useState<NotificationFlags>({ debug: true });
|
|
|
|
if (!canNotify && !canRenew) {
|
|
return (
|
|
<div className="state-box state-error">
|
|
No tienes permiso para enviar notificaciones.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="page-head">
|
|
<p className="eyebrow">Notificaciones</p>
|
|
<h1 className="page-title">Notificaciones</h1>
|
|
<p className="muted" style={{ marginTop: 6, maxWidth: 720 }}>
|
|
{TAB_HINT[tab]}
|
|
</p>
|
|
</div>
|
|
|
|
<div style={{ display: "grid", gap: 16, marginBottom: 20 }}>
|
|
<NotificationFlagsCard
|
|
flags={flags}
|
|
onChange={setFlags}
|
|
disabled={!canNotify && !canRenew}
|
|
/>
|
|
<NotificationScheduleCard />
|
|
</div>
|
|
|
|
{tabs.length > 1 && (
|
|
<div className="seg" role="tablist" style={{ marginBottom: 20 }}>
|
|
{tabs.map((t) => (
|
|
<button
|
|
key={t.key}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={tab === t.key}
|
|
className={`seg-btn ${tab === t.key ? "active" : ""}`}
|
|
onClick={() => setTab(t.key)}
|
|
>
|
|
{t.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{tab === "servicios" ? (
|
|
<NotificacionesServicios flags={flags} />
|
|
) : (
|
|
<NotificacionesPolizas flags={flags} />
|
|
)}
|
|
</>
|
|
);
|
|
}
|