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:
@@ -0,0 +1,98 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from "@nestjs/common";
|
||||
import { ServiceKind } from "@jorgecuadros/database";
|
||||
import { AuthenticatedGuard } from "../auth/authenticated.guard";
|
||||
import {
|
||||
PropertiesService,
|
||||
type PropertySort,
|
||||
type TrustFilter,
|
||||
} from "./properties.service";
|
||||
|
||||
const KINDS: ServiceKind[] = [
|
||||
"WATER",
|
||||
"ELECTRIC",
|
||||
"GAS",
|
||||
"CABLE",
|
||||
"PROPERTY_TAX",
|
||||
"FEDERAL_ZONE",
|
||||
"ALARM",
|
||||
"OTHER",
|
||||
];
|
||||
|
||||
const TRUST_FILTERS: TrustFilter[] = [
|
||||
"with",
|
||||
"without",
|
||||
"active",
|
||||
"expiring",
|
||||
"expired",
|
||||
"undated",
|
||||
];
|
||||
|
||||
const SORTS: PropertySort[] = [
|
||||
"customer",
|
||||
"address",
|
||||
"services_desc",
|
||||
"trust_due_asc",
|
||||
"trust_due_desc",
|
||||
];
|
||||
|
||||
/** Clamped trust-renewal window; 30 days matches the policies module. */
|
||||
function parseDays(days?: string): number {
|
||||
return Math.min(365, Math.max(1, Number(days) || 30));
|
||||
}
|
||||
|
||||
@UseGuards(AuthenticatedGuard)
|
||||
@Controller("properties")
|
||||
export class PropertiesController {
|
||||
constructor(private readonly properties: PropertiesService) {}
|
||||
|
||||
@Get("stats")
|
||||
stats(@Query("days") days?: string) {
|
||||
return this.properties.stats(parseDays(days));
|
||||
}
|
||||
|
||||
@Get("facets")
|
||||
facets() {
|
||||
return this.properties.facets();
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@Query("query") query?: string,
|
||||
@Query("page") page?: string,
|
||||
@Query("pageSize") pageSize?: string,
|
||||
@Query("serviceKind") serviceKind?: string,
|
||||
@Query("municipality") municipality?: string,
|
||||
@Query("bank") bank?: string,
|
||||
@Query("trust") trust?: string,
|
||||
@Query("hasServices") hasServices?: string,
|
||||
@Query("customerId") customerId?: string,
|
||||
@Query("days") days?: string,
|
||||
@Query("sort") sort?: string,
|
||||
) {
|
||||
return this.properties.list({
|
||||
query,
|
||||
page: Math.max(1, Number(page) || 1),
|
||||
pageSize: Math.min(100, Math.max(1, Number(pageSize) || 25)),
|
||||
serviceKind: KINDS.includes(serviceKind as ServiceKind)
|
||||
? (serviceKind as ServiceKind)
|
||||
: undefined,
|
||||
municipality: municipality || undefined,
|
||||
bank: bank || undefined,
|
||||
trust: TRUST_FILTERS.includes(trust as TrustFilter)
|
||||
? (trust as TrustFilter)
|
||||
: undefined,
|
||||
hasServices:
|
||||
hasServices === "true" ? true : hasServices === "false" ? false : undefined,
|
||||
customerId: customerId || undefined,
|
||||
days: parseDays(days),
|
||||
sort: SORTS.includes(sort as PropertySort)
|
||||
? (sort as PropertySort)
|
||||
: "customer",
|
||||
});
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
detail(@Param("id") id: string, @Query("days") days?: string) {
|
||||
return this.properties.detail(id, parseDays(days));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user