feat(customers): create/edit/archive CRUD with soft-delete (plan phase 2)

First master-data CRUD module on the phase-1 RBAC foundation.

API:
- Customer gains archivedAt (soft-delete marker, distinct from the legacy
  `status` business flag); pushed to dev (nullable, non-destructive).
- CustomersService: create/update/archive/restore. list() and the browser
  default to archivedAt=null; ?includeArchived=true opts in. App-created
  rows set nameMissing=false and leave legacy provenance null.
- CustomersController write routes guarded per the matrix: create/update
  need STAFF+ (customer:create/update), archive/restore need ADMIN
  (customer:delete). Every mutation audit-logged.
- create/update DTOs (class-validator); date strings coerced to Date.

Web:
- Shared CustomerForm (create + edit) with identity/address/account
  sections; new routes /clientes/nuevo and /clientes/[id]/editar, each
  self-gated on the ability.
- List page: ability-gated "Nuevo cliente" button. Detail page: gated
  Editar / Archivar (Restaurar) action bar; archived badge.
- api.ts create/update/archive/restore; CustomerInput type; archived flag
  on list items.

Verified against dev: create (dates coerced, archivedAt null), edit 200,
VIEWER create 403, STAFF create 201 but archive 403, ADMIN archive drops
the row from the default list and includeArchived surfaces it, restore
returns it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 12:08:44 -07:00
co-authored by Claude Opus 4.8
parent 74e2ad8bcd
commit 12692a0af8
13 changed files with 715 additions and 8 deletions
+26
View File
@@ -17,6 +17,7 @@ import type {
BillingStats,
BusinessLine,
CustomerDetail,
CustomerInput,
CustomerListResponse,
CustomerStats,
LedgerCurrency,
@@ -132,6 +133,31 @@ export function getCustomer(id: string): Promise<CustomerDetail> {
return apiFetch<CustomerDetail>(`/customers/${id}`);
}
export function createCustomer(input: CustomerInput): Promise<CustomerDetail> {
return apiFetch<CustomerDetail>("/customers", {
method: "POST",
body: JSON.stringify(input),
});
}
export function updateCustomer(
id: string,
input: Partial<CustomerInput>,
): Promise<CustomerDetail> {
return apiFetch<CustomerDetail>(`/customers/${id}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export function archiveCustomer(id: string): Promise<CustomerDetail> {
return apiFetch<CustomerDetail>(`/customers/${id}`, { method: "DELETE" });
}
export function restoreCustomer(id: string): Promise<CustomerDetail> {
return apiFetch<CustomerDetail>(`/customers/${id}/restore`, { method: "POST" });
}
/* ------------------------------------------------------ Policies module */
/** Renewal horizon in days, shared by the list, stats and detail calls so the
+29
View File
@@ -1,6 +1,8 @@
// TypeScript types for the Jorge Cuadros & Asociados API responses.
// Decimals arrive as strings, dates as ISO strings.
export type Currency = "USD" | "MXN";
export type Role = "ADMIN" | "MANAGER" | "STAFF" | "VIEWER";
export type Ability =
@@ -65,6 +67,7 @@ export interface CustomerListItem {
phone: string | null;
mobile: string | null;
status: boolean;
archived: boolean;
propertyCount: number;
policyCount: number;
transactionCount: number;
@@ -695,8 +698,10 @@ export interface CustomerDetail {
identificationExpiration: string | null;
customerSince: string | null;
status: boolean;
minimumBalance: string | number | null;
feeAmount: string | number | null;
preferredCurrency: string | null;
archivedAt: string | null;
legacyRefs: LegacyRef[];
properties: Property[];
policies: Policy[];
@@ -704,6 +709,30 @@ export interface CustomerDetail {
transactionSummary: TransactionSummaryRow[];
}
/** Editable customer fields — shared by the create/edit form and the API. */
export interface CustomerInput {
name: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
zipCode?: string;
country?: string;
phone?: string;
mobile?: string;
fax?: string;
email?: string;
notes?: string;
identificationType?: string;
identificationNumber?: string;
identificationExpiration?: string;
customerSince?: string;
status?: boolean;
minimumBalance?: number;
feeAmount?: number;
preferredCurrency?: Currency;
}
/* ------------------------------------------------- Bank register (chequera) */
/**