feat(reports): parameterized renewal-notice report + legacy report reference
Build and Push Images / Build jorgecuadros-web (push) Failing after 59s
Build and Push Images / Build jorgecuadros-api (push) Successful in 1m59s

Replaces ~40 legacy Access renewal-notice report clones (one per carrier
per coverage tier, e.g. AMPL/RC/LIC RENEW X MES/VENCE ATLAS 13/2013) with
one parameterized aviso-renovacion report driven by real Policy/Vehicle/
coveragesJson data instead of hand-typed label text per clone.

- schema.prisma: add RenewalNotice, replacing the legacy CONTROL <ramo>
  RENEW[2/3] X MES paper log of which notice generation was sent
- reports: new "letter" ReportFormat + aviso-renovacion registry entry +
  LetterLayout renderer in ReportRunner.tsx
- docs/RENEWAL_NOTICES.md + migration/legacy_report_defs/: extracted (via
  Application.SaveAsText, since the VBA project wouldn't load) and
  documented the legacy report/query chain this replaces

Coveragesjson key names and a mark-as-sent mutation are still unverified/
unbuilt — see caveats in docs/RENEWAL_NOTICES.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 23:05:21 -07:00
co-authored by Claude Sonnet 5
parent 1b79b43a54
commit f7ae0d5342
12 changed files with 8358 additions and 3 deletions
+159
View File
@@ -267,6 +267,8 @@ function ResultBlock({
{def.format === "statement" ? (
<StatementLayout result={result} />
) : def.format === "letter" ? (
<LetterLayout result={result} />
) : (
<TabularLayout result={result} />
)}
@@ -472,5 +474,162 @@ function StatementLayout({ result }: { result: ReportRunResult }) {
);
}
/* ----------------------------------------------------------- letter layout */
const GENERATION_LABEL: Record<string, string> = {
"1": "1er aviso",
"2": "2o aviso",
"3": "3er aviso",
};
interface LetterVehicle {
make?: string | null;
model?: string | null;
modelYear?: string | number | null;
bodyType?: string | null;
engineNumber?: string | null;
licensePlate?: string | null;
}
/**
* One card per policy due for renewal — the parameterized replacement for
* the ~40 cloned "AVISO DE RENOVACION" Access reports (see
* docs/RENEWAL_NOTICES.md). Coverage figures (CSL limit, medical coverage,
* etc.) come from data (`aviso-renovacion`'s ReportDef reads
* `Policy.coveragesJson`) instead of the legacy's hand-typed label text, so
* one layout renders every carrier/coverage combination.
*/
function LetterLayout({ result }: { result: ReportRunResult }) {
const letters = result.rows.filter((r) => r.__kind === "letter");
if (letters.length === 0) {
return (
<div className="empty-inline">
No hay pólizas por vencer con los filtros actuales.
</div>
);
}
return (
<div className="renewal-letters">
{letters.map((r, i) => {
const vehicle = r.vehicle as LetterVehicle | null;
const generation = String(r.generation ?? "1");
return (
<article className="renewal-letter" key={String(r.policyId ?? i)}>
<header className="renewal-letter-head">
<div>
<h2 className="renewal-letter-title">Aviso de renovación</h2>
<p className="muted small">
{GENERATION_LABEL[generation] ?? `Aviso ${generation}`}
</p>
</div>
<div className="renewal-letter-status">
{r.sentAt ? (
<span className="badge badge-positive">
<span className="dot" />
Enviado {String(r.sentAt)}
</span>
) : (
<span className="badge badge-neutral">
<span className="dot" />
Pendiente
</span>
)}
</div>
</header>
<div className="renewal-letter-grid">
<div>
<p className="muted small">Cliente</p>
<p className="renewal-letter-value">
{String(r.customerName ?? "—")}
</p>
</div>
<div>
<p className="muted small">Póliza</p>
<p className="renewal-letter-value">
{String(r.policyNumber ?? "—")}
</p>
</div>
<div>
<p className="muted small">Aseguradora</p>
<p className="renewal-letter-value">
{String(r.provider ?? "—")}
</p>
</div>
<div>
<p className="muted small">Vence</p>
<p className="renewal-letter-value">
{String(r.policyTo ?? "—")}
</p>
</div>
</div>
{vehicle && (
<div className="renewal-letter-vehicle">
<p className="muted small">Vehículo asegurado</p>
<p className="renewal-letter-value">
{[vehicle.modelYear, vehicle.make, vehicle.model]
.filter(Boolean)
.join(" ")}
{vehicle.bodyType ? ` · ${vehicle.bodyType}` : ""}
</p>
<p className="muted small">
{[
vehicle.engineNumber && `Motor: ${vehicle.engineNumber}`,
vehicle.licensePlate && `Placa: ${vehicle.licensePlate}`,
]
.filter(Boolean)
.join(" · ")}
</p>
</div>
)}
<div className="renewal-letter-coverage">
<CoverageItem label="Cobertura (días)" value={r.coverageDays} />
<CoverageItem label="CSL" value={r.cslLimit} money />
<CoverageItem label="Gastos médicos" value={r.medicalCoverage} money />
<CoverageItem label="Daños a propiedad" value={r.propertyDamage} money />
<CoverageItem label="Responsabilidad por persona" value={r.perPersonLiability} money />
</div>
<div className="renewal-letter-premium">
{r.netPremium && (
<span>Prima neta: {formatCell(r.netPremium, "money")}</span>
)}
{r.total && (
<span className="renewal-letter-total">
Total: {formatCell(r.total, "money")} {String(r.currency ?? "")}
</span>
)}
</div>
</article>
);
})}
</div>
);
}
function CoverageItem({
label,
value,
money,
}: {
label: string;
value: unknown;
money?: boolean;
}) {
if (value === null || value === undefined || value === "") return null;
return (
<div className="renewal-letter-coverage-item">
<p className="muted small">{label}</p>
<p className="renewal-letter-value">
{money ? formatCell(value, "money") : String(value)}
</p>
</div>
);
}
// Hint to the bundler that API_ORIGIN is part of the API surface used here.
void API_ORIGIN;