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:
@@ -97,7 +97,11 @@ function Detail({ id }: { id: string }) {
|
||||
<Hero data={data} hasUtilities={hasUtilities} hasInsurance={hasInsurance} />
|
||||
|
||||
<DatosSection data={data} />
|
||||
<PropiedadesSection properties={data.properties} />
|
||||
<PropiedadesSection
|
||||
properties={data.properties}
|
||||
customerId={data.id}
|
||||
customerName={data.name}
|
||||
/>
|
||||
<PolizasSection
|
||||
policies={data.policies}
|
||||
customerId={data.id}
|
||||
@@ -337,14 +341,33 @@ function KV({
|
||||
}
|
||||
|
||||
/* ----------------------------------------------- Propiedades y servicios */
|
||||
function PropiedadesSection({ properties }: { properties: Property[] }) {
|
||||
function PropiedadesSection({
|
||||
properties,
|
||||
customerId,
|
||||
customerName,
|
||||
}: {
|
||||
properties: Property[];
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
}) {
|
||||
const canCreate = useCan("property:create");
|
||||
return (
|
||||
<section className="section">
|
||||
<SectionHead
|
||||
rule="servicios"
|
||||
title="Propiedades y servicios"
|
||||
count={properties.length}
|
||||
/>
|
||||
<div className="detail-actionbar">
|
||||
<SectionHead
|
||||
rule="servicios"
|
||||
title="Propiedades y servicios"
|
||||
count={properties.length}
|
||||
/>
|
||||
{canCreate && (
|
||||
<Link
|
||||
href={`/servicios/nuevo?customerId=${customerId}&customerName=${encodeURIComponent(customerName)}`}
|
||||
className="btn btn-outline"
|
||||
>
|
||||
+ Nueva propiedad
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="card">
|
||||
{properties.length === 0 ? (
|
||||
<div className="empty-inline">
|
||||
|
||||
Reference in New Issue
Block a user