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:
@@ -1684,3 +1684,139 @@ button {
|
||||
.policy-num-link:hover {
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Utilities — service mix strip (doubles as the service-kind filter)
|
||||
========================================================================== */
|
||||
.mix-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 14px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.mix-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 11px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.mix-chip:hover {
|
||||
border-color: var(--brand-600);
|
||||
}
|
||||
.mix-chip.selected {
|
||||
border-color: var(--brand-600);
|
||||
box-shadow: inset 0 0 0 1px var(--brand-600);
|
||||
}
|
||||
.mix-glyph {
|
||||
font-size: 13px;
|
||||
color: var(--brand-600);
|
||||
}
|
||||
.mix-body {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
.mix-body strong {
|
||||
font-family: var(--font-mono);
|
||||
font-feature-settings: "tnum" 1;
|
||||
font-size: 14px;
|
||||
color: var(--brand-700);
|
||||
}
|
||||
.mix-label {
|
||||
font-size: 12.5px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.mix-inactive {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
border-left: 1px solid var(--line);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Utilities — property list rows
|
||||
========================================================================== */
|
||||
.prop-row .cust-name {
|
||||
flex-wrap: wrap;
|
||||
gap: 9px;
|
||||
}
|
||||
.prop-side {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 5px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.svc-chips {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.svc-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 7px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--line);
|
||||
font-size: 12px;
|
||||
color: var(--brand-600);
|
||||
}
|
||||
.svc-chip.inactive {
|
||||
opacity: 0.45;
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
.prop-side {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.svc-chips {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sibling properties on the property detail are navigable. */
|
||||
.linked-prop.link {
|
||||
display: block;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.linked-prop.link:hover {
|
||||
color: var(--brand-600);
|
||||
}
|
||||
.inline-link {
|
||||
color: var(--brand-600);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.inline-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Visually hidden, still read by screen readers (service kind chips). */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user