Utilities module: property browser (list/search/detail) + trust renewals

Plan step 5. Properties, services and trust accounts become a first-class
browser the way /polizas is for insurance.

API (apps/api/src/properties):
  GET /properties         search over address, customer, service account
                          number, meter, trust number and phones; filters for
                          service kind, municipality, trust bank, trust bucket
                          (with|without|active|expiring|expired|undated) and
                          hasServices; 5 sorts
  GET /properties/stats   properties/owners/services/trusts, renewal counts,
                          service mix per kind
  GET /properties/facets  kinds, municipalities, banks — all with counts
  GET /properties/:id     services, fideicomiso, linked policy, owner and
                          sibling properties, owner-level utility ledger

Web: /servicios (renewals-first browser, clickable stat cells and service-mix
strip) and /servicios/[id]. Property cards on /clientes/[id] and linked
properties on /polizas/[id] now navigate into it.

Data findings baked into the design:
  - The trust deadline staff chase is trust_accounts.dueDate2 (DATMEX vence2),
    one year after vence1 on 531 of 541 dated trusts: 18 due within 30 days,
    119 already overdue. Every renewal bucket keys off dueDate2 alone.
  - properties.zone is dead (1444 of 1519 null, the rest near-unique), so the
    geographic filter is the municipality carried in the predial service's
    notes (ROSARITO 566 / TIJUANA 221 / ENSENADA 152, 939/939 populated).
  - PropertyService.notes means a different thing per kind (municipality, CFE
    PAR/IMPAR cycle, gas supply type, cable provider) and is labelled as such.
  - 240 of 1519 properties have no service rows at all — its own bucket.

Sorting by trust due date scopes to properties that have a trust, since MySQL
would otherwise float the ~966 trust-less NULLs above every real due date;
the sort label and the result meta both say so.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 22:04:07 -07:00
co-authored by Claude Opus 4.8
parent c291bc8d4c
commit 61193586a5
14 changed files with 2086 additions and 7 deletions
+37 -1
View File
@@ -1,6 +1,11 @@
// Spanish label maps + formatting helpers. Single source of truth for i18n.
import type { PolicyStatus, ServiceKind, TransactionDomain } from "./types";
import type {
PolicyStatus,
ServiceKind,
TransactionDomain,
TrustStatus,
} from "./types";
/**
* Placeholder the migration writes when a legacy record had no name and none
@@ -49,6 +54,37 @@ export function serviceKindGlyph(kind: ServiceKind): string {
return SERVICE_KIND_GLYPH[kind] ?? "•";
}
/**
* Free-text detail the migration parked in `PropertyService.notes`, which
* means something different per service kind: the municipality that bills the
* predial / zona federal, the CFE billing cycle (PAR/IMPAR), and the gas
* supply type. Used to label the note instead of dumping a bare string.
*/
export const SERVICE_NOTE_LABELS: Record<string, string> = {
PROPERTY_TAX: "Municipio",
FEDERAL_ZONE: "Municipio",
ELECTRIC: "Ciclo",
GAS: "Suministro",
CABLE: "Proveedor",
};
export function serviceNoteLabel(kind: ServiceKind): string | null {
return SERVICE_NOTE_LABELS[kind] ?? null;
}
// ----- fideicomisos (trusts) -----
export const TRUST_STATUS_LABELS: Record<TrustStatus, string> = {
active: "Vigente",
expiring: "Por vencer",
expired: "Vencido",
undated: "Sin fecha",
};
export function trustStatusLabel(status: TrustStatus): string {
return TRUST_STATUS_LABELS[status] ?? status;
}
// ----- policies -----
export const POLICY_STATUS_LABELS: Record<PolicyStatus, string> = {