Insurance module: policy browser (list/search/detail) + renewals view

Plan step 4. Adds the policies API and the Spanish-first /polizas pages on
top of the customer records the customer module already exposes.

API (apps/api/src/policies):
- GET /policies — search over policy number, customer name, agent, vehicle
  license plate, insured-driver name and legacy id; filters for vigencia
  bucket, ramo, aseguradora and liquidation state; five sort orders.
- GET /policies/stats — bucket counts plus premium in force split by
  currency (MXN and USD can't be summed).
- GET /policies/facets — ramos/aseguradoras with counts for the dropdowns.
- GET /policies/:id — full policy plus the owning customer.

Vigencia is derived from policyTo as active/expiring/expired/undated.
"undated" is a real bucket rather than an error case: 528 of the 2378
migrated policies carry no end date at all.

Web:
- /polizas — renewals-first browser; the stat cells double as vigencia
  filters, with a secondary row for ramo, aseguradora and sort order.
- /polizas/[id] — vigencia hero, condiciones y primas, pagos, vehículos,
  asegurados/beneficiarios, siniestros, the verbatim legacy coverage
  columns, and documents.
- Nav gains Clientes | Pólizas with a real active state, and the two
  modules cross-link in both directions.

Also fixes a display bug on the customer detail page: it headlined
policies.total, which is dead data — only 2 of 2378 rows are non-zero
(1585 are literally 0, 791 null), and one of those two is lower than its
own net premium. That rendered "$0.00 Total" on 1585 policies. Premium
headlines and the premium sort now use netPremium (2377/2378 populated);
total is shown only where it is non-zero, as raw source data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 21:27:38 -07:00
co-authored by Claude Opus 4.8
parent fa9b696752
commit e2aba8bd17
13 changed files with 1957 additions and 16 deletions
+240
View File
@@ -1444,3 +1444,243 @@ button {
color: var(--muted);
margin-top: 10px;
}
/* ============================================================================
Policies — vigencia badges
========================================================================== */
.badge.status-active {
background: var(--positive-tint);
color: var(--positive);
border-color: rgba(47, 109, 60, 0.25);
}
.badge.status-expiring {
background: var(--accent-tint);
color: var(--accent-600);
border-color: rgba(191, 90, 52, 0.28);
}
.badge.status-expired {
background: var(--negative-tint);
color: var(--negative);
border-color: rgba(162, 58, 44, 0.25);
}
.badge.status-undated {
background: var(--paper-2);
color: var(--muted);
border-color: var(--line-strong);
}
/* ============================================================================
Policies — header strips
========================================================================== */
/* Stat cells double as vigencia filters on /polizas. */
.stat-cell-btn {
display: block;
width: 100%;
text-align: left;
border: none;
cursor: pointer;
font: inherit;
transition: background 0.15s;
}
.stat-cell-btn:hover {
background: var(--surface-2);
}
.stat-cell-btn.accent:hover {
background: #d7e6e1;
}
.stat-cell-btn.selected {
box-shadow: inset 0 -3px 0 var(--brand-600);
}
.premium-strip {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin-top: 14px;
padding: 0 2px;
}
.premium-caption {
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: var(--muted);
}
.premium-chip {
display: inline-flex;
align-items: baseline;
gap: 8px;
}
.premium-chip strong {
font-family: var(--font-mono);
font-feature-settings: "tnum" 1;
font-size: 15px;
color: var(--brand-700);
}
.premium-chip-sub {
font-size: 12px;
color: var(--muted);
}
/* ============================================================================
Policies — secondary filter row
========================================================================== */
.filter-row {
display: flex;
gap: 14px;
align-items: flex-end;
flex-wrap: wrap;
margin-bottom: 18px;
}
.filter-field {
display: flex;
flex-direction: column;
gap: 5px;
flex: 1 1 210px;
min-width: 0;
}
.filter-label {
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: var(--muted);
}
.select {
appearance: none;
cursor: pointer;
padding-right: 34px;
background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
linear-gradient(135deg, var(--muted) 50%, transparent 50%);
background-position: calc(100% - 18px) 55%, calc(100% - 13px) 55%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
.filter-clear {
flex: 0 0 auto;
height: 40px;
}
/* ============================================================================
Policies — list rows
========================================================================== */
.pol-row .cust-name {
flex-wrap: wrap;
gap: 9px;
}
.pol-number {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.01em;
}
.pol-side {
text-align: right;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 3px;
flex: 0 0 auto;
}
.pol-premium {
font-family: var(--font-mono);
font-feature-settings: "tnum" 1;
font-size: 15px;
font-weight: 600;
color: var(--ink);
}
.pol-dates {
font-size: 12px;
color: var(--muted);
white-space: nowrap;
}
.pol-phrase {
font-size: 11.5px;
font-weight: 600;
}
.pol-phrase.expiring {
color: var(--accent-600);
}
.pol-phrase.active {
color: var(--positive);
}
@media (max-width: 620px) {
.pol-side {
align-items: flex-start;
text-align: left;
margin-top: 8px;
}
}
/* ============================================================================
Policy detail — owner card, linked properties, vehicles
========================================================================== */
.owner-link {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 22px;
color: inherit;
text-decoration: none;
border-radius: var(--radius-lg);
transition: background 0.15s;
}
.owner-link:hover {
background: var(--surface-2);
}
.owner-name {
font-family: var(--font-display);
font-size: 19px;
font-weight: 560;
margin-bottom: 3px;
}
.owner-cta {
font-size: 13px;
font-weight: 600;
color: var(--brand-600);
white-space: nowrap;
}
.linked-props {
border-top: 1px solid var(--line);
padding: 16px 22px 18px;
}
.linked-prop {
font-size: 14px;
margin-top: 4px;
}
.veh-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 12px;
padding: 16px;
}
.veh-card {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 13px 15px;
}
.veh-title {
font-weight: 600;
font-size: 14.5px;
margin-bottom: 6px;
}
.veh-facts {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12.5px;
color: var(--muted);
}
/* Policy number on the customer detail card links through to /polizas/[id]. */
.policy-num-link {
color: inherit;
text-decoration: none;
transition: color 0.15s;
}
.policy-num-link:hover {
color: var(--brand-600);
}