Utilities section becomes create/edit/archive-able, with its child data. API: - Property gains archivedAt (soft-delete); list/browser default to archivedAt=null with ?includeArchived opt-in. - PropertiesService: header create/update/archive/restore (customer FK validated); PropertyService add/update/remove scoped to the property; TrustAccount upsert (1:1) + remove; ServiceDocument pointer delete. - Controller write routes: create needs STAFF+ (property:create), archive MANAGER+ (property:delete), every service/trust/document route property:update. Mutations audited. DTOs added. - Document *upload* deliberately deferred: it needs the object-storage client wired into the API (today only the migration writes to MinIO); removing an existing pointer row is supported and the UI says so. Web: - PropertyForm (header) with CustomerPicker; /servicios/nuevo (accepts ?customerId prefill) and /servicios/[id]/editar. - Property detail: gated action bar (Editar/Archivar) + "Administrar propiedad" — services via the shared ChildCollection editor, an inline 1:1 TrustEditor (create/update/clear), and document-row delete. - "Nueva propiedad" buttons on the list and customer detail (prefilled). api.ts + types for all of it. Verified against dev: property create (archivedAt null), service add/update, VIEWER service-add 403, trust upsert (create then update the same row), trust/service remove, cross-property child guard 404, archive drops from the default list and includeArchived surfaces it. Both apps compile clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
207 lines
5.6 KiB
TypeScript
207 lines
5.6 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
Put,
|
|
Query,
|
|
Req,
|
|
UseGuards,
|
|
} from "@nestjs/common";
|
|
import { ServiceKind } from "@jorgecuadros/database";
|
|
import { Request } from "express";
|
|
import { AuthenticatedGuard } from "../auth/authenticated.guard";
|
|
import { AbilityGuard } from "../auth/ability.guard";
|
|
import { RequireAbility } from "../auth/require-ability.decorator";
|
|
import { AuditService } from "../common/audit.service";
|
|
import {
|
|
PropertiesService,
|
|
type PropertySort,
|
|
type TrustFilter,
|
|
} from "./properties.service";
|
|
import {
|
|
CreatePropertyDto,
|
|
ServiceDto,
|
|
TrustDto,
|
|
UpdatePropertyDto,
|
|
UpdateServiceDto,
|
|
} from "./property.dto";
|
|
|
|
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",
|
|
];
|
|
|
|
function parseDays(days?: string): number {
|
|
return Math.min(365, Math.max(1, Number(days) || 30));
|
|
}
|
|
|
|
@UseGuards(AuthenticatedGuard, AbilityGuard)
|
|
@Controller("properties")
|
|
export class PropertiesController {
|
|
constructor(
|
|
private readonly properties: PropertiesService,
|
|
private readonly audit: AuditService,
|
|
) {}
|
|
|
|
private actingId(req: Request): string {
|
|
return (req.user as { id: string }).id;
|
|
}
|
|
|
|
@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("includeArchived") includeArchived?: 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),
|
|
includeArchived: includeArchived === "true",
|
|
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));
|
|
}
|
|
|
|
// --- header writes --------------------------------------------------------
|
|
|
|
@Post()
|
|
@RequireAbility("property:create")
|
|
async create(@Body() dto: CreatePropertyDto, @Req() req: Request) {
|
|
const p = await this.properties.create(dto);
|
|
void this.audit.log(this.actingId(req), "property.create", { propertyId: p.id });
|
|
return p;
|
|
}
|
|
|
|
@Patch(":id")
|
|
@RequireAbility("property:update")
|
|
async update(@Param("id") id: string, @Body() dto: UpdatePropertyDto, @Req() req: Request) {
|
|
const p = await this.properties.update(id, dto);
|
|
void this.audit.log(this.actingId(req), "property.update", { propertyId: id });
|
|
return p;
|
|
}
|
|
|
|
@Delete(":id")
|
|
@RequireAbility("property:delete")
|
|
async archive(@Param("id") id: string, @Req() req: Request) {
|
|
const p = await this.properties.archive(id);
|
|
void this.audit.log(this.actingId(req), "property.archive", { propertyId: id });
|
|
return p;
|
|
}
|
|
|
|
@Post(":id/restore")
|
|
@RequireAbility("property:delete")
|
|
async restore(@Param("id") id: string, @Req() req: Request) {
|
|
const p = await this.properties.restore(id);
|
|
void this.audit.log(this.actingId(req), "property.restore", { propertyId: id });
|
|
return p;
|
|
}
|
|
|
|
// --- services (property:update) -------------------------------------------
|
|
|
|
@Post(":id/services")
|
|
@RequireAbility("property:update")
|
|
addService(@Param("id") id: string, @Body() dto: ServiceDto) {
|
|
return this.properties.addService(id, dto);
|
|
}
|
|
@Patch(":id/services/:childId")
|
|
@RequireAbility("property:update")
|
|
updateService(
|
|
@Param("id") id: string,
|
|
@Param("childId") childId: string,
|
|
@Body() dto: UpdateServiceDto,
|
|
) {
|
|
return this.properties.updateService(id, childId, dto);
|
|
}
|
|
@Delete(":id/services/:childId")
|
|
@RequireAbility("property:update")
|
|
removeService(@Param("id") id: string, @Param("childId") childId: string) {
|
|
return this.properties.removeService(id, childId);
|
|
}
|
|
|
|
// --- trust account (1:1) --------------------------------------------------
|
|
|
|
@Put(":id/trust")
|
|
@RequireAbility("property:update")
|
|
upsertTrust(@Param("id") id: string, @Body() dto: TrustDto) {
|
|
return this.properties.upsertTrust(id, dto);
|
|
}
|
|
@Delete(":id/trust")
|
|
@RequireAbility("property:update")
|
|
removeTrust(@Param("id") id: string) {
|
|
return this.properties.removeTrust(id);
|
|
}
|
|
|
|
// --- documents (remove pointer only) --------------------------------------
|
|
|
|
@Delete(":id/documents/:childId")
|
|
@RequireAbility("property:update")
|
|
removeDocument(@Param("id") id: string, @Param("childId") childId: string) {
|
|
return this.properties.removeDocument(id, childId);
|
|
}
|
|
}
|