feat(addresses): full CRUD UI for client + company multi-address

Client and company detail pages each gain an Addresses tab with click-to-edit
fields wired to the existing CountryCombobox/SubdivisionCombobox primitives.
Adds a primary toggle that demotes the previous primary inside one transaction
so the partial unique index never trips.

- New service helpers: list/add/update/remove ClientAddress + CompanyAddress
- New routes: /api/v1/clients/[id]/addresses[/addressId], same under companies/
- New shared component: <AddressesEditor> reused by both detail surfaces
- Integration tests cover happy path, primary demotion, and tenant scoping

Tests: 747/747 vitest (was 741, +6 address tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-28 19:38:43 +02:00
parent 27cdbcc695
commit 46937bbcb9
12 changed files with 1117 additions and 5 deletions

View File

@@ -7,6 +7,7 @@ import { ClientDetailHeader } from '@/components/clients/client-detail-header';
import { getClientTabs } from '@/components/clients/client-tabs';
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
import { apiFetch } from '@/lib/api/client';
import type { Address } from '@/components/shared/addresses-editor';
interface ClientData {
id: string;
@@ -64,6 +65,7 @@ interface ClientData {
tenureType: string;
status: string;
}>;
addresses: Address[];
}
interface ClientDetailProps {

View File

@@ -13,6 +13,7 @@ import { ClientYachtsTab } from '@/components/clients/client-yachts-tab';
import { ClientCompaniesTab } from '@/components/clients/client-companies-tab';
import { ClientReservationsTab } from '@/components/clients/client-reservations-tab';
import { ContactsEditor } from '@/components/clients/contacts-editor';
import { AddressesEditor, type Address } from '@/components/shared/addresses-editor';
import { apiFetch } from '@/lib/api/client';
type ClientPatchField =
@@ -83,6 +84,7 @@ interface ClientTabsOptions {
label?: string | null;
isPrimary: boolean;
}>;
addresses?: Address[];
yachts: Array<{
id: string;
name: string;
@@ -237,6 +239,18 @@ export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOpt
<ClientReservationsTab clientId={clientId} activeReservations={client.activeReservations} />
),
},
{
id: 'addresses',
label: 'Addresses',
badge: client.addresses?.length ?? 0,
content: (
<AddressesEditor
endpoint={`/api/v1/clients/${clientId}/addresses`}
invalidateKey={['clients', clientId]}
addresses={client.addresses ?? []}
/>
),
},
{
id: 'interests',
label: 'Interests',