feat(properties): CRUD + service/trust/document editors (plan phase 4)
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>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { useCan } from "@/lib/abilities";
|
||||
import {
|
||||
EXPIRY_WINDOW_DAYS,
|
||||
getPropertyFacets,
|
||||
@@ -81,6 +82,7 @@ export default function ServiciosPage() {
|
||||
}
|
||||
|
||||
function ServiciosBrowser() {
|
||||
const canCreate = useCan("property:create");
|
||||
const [stats, setStats] = useState<PropertyStats | null>(null);
|
||||
const [facets, setFacets] = useState<PropertyFacets | null>(null);
|
||||
|
||||
@@ -164,7 +166,13 @@ function ServiciosBrowser() {
|
||||
<>
|
||||
<div className="page-head rise">
|
||||
<p className="eyebrow">Administración de servicios</p>
|
||||
<h1 className="page-title">Propiedades</h1>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<h1 className="page-title" style={{ margin: 0 }}>Propiedades</h1>
|
||||
<span style={{ flex: 1 }} />
|
||||
{canCreate && (
|
||||
<Link href="/servicios/nuevo" className="btn btn-primary">+ Nueva propiedad</Link>
|
||||
)}
|
||||
</div>
|
||||
<StatStrip stats={stats} focus={focus} onPickFocus={pickFocus} />
|
||||
<ServiceMixStrip
|
||||
stats={stats}
|
||||
|
||||
Reference in New Issue
Block a user