fix(clients): list contacts join + nationality backfill + col redesign

Wire primary email + primary phone into the /clients list service so
the redesigned columns (Name · Email · Phone · Country · Source ·
Latest stage · Created) actually have data. Picks the row marked
is_primary=true; falls back to most-recent created_at when the flag
is unset.

- 0026 schema migration: unique partial index
  idx_cc_one_primary_per_channel on (client_id, channel) WHERE
  is_primary=true. Prevents the §14.2 "multiple primaries" ambiguity.
- 0027 data migration: backfill clients.nationality_iso from the
  primary phone's value_country. 218 -> 36 missing on dev. Idempotent.
- listClients: add a fifth parallel query for client_contacts; build
  primaryEmailMap / primaryPhoneMap in-memory from the pre-sorted
  result.
- client-columns: drop Yachts/Companies/Tags from the default view
  per §5.1; add Email/Phone/Country/Latest-stage columns; rename
  "Nationality" -> "Country" since phone country is a proxy (§14.2).
- client-card: prefer email, fall back to phone, for the line under
  the name; replaces the old `contacts.find(isPrimary)` lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-05 02:15:03 +02:00
parent a2588f2c4a
commit 3017ce4b3a
9 changed files with 21563 additions and 106 deletions

View File

@@ -35,7 +35,8 @@ interface ClientCardProps {
}
export function ClientCard({ client, portSlug, onEdit, onArchive }: ClientCardProps) {
const primary = client.contacts?.find((c) => c.isPrimary);
// Card display: prefer email, fall back to phone.
const primaryContactValue = client.primaryEmail ?? client.primaryPhone ?? null;
const nationality = client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null;
const sourceLabel = client.source ? (SOURCE_LABELS[client.source] ?? client.source) : null;
const tags = client.tags ?? [];
@@ -93,8 +94,8 @@ export function ClientCard({ client, portSlug, onEdit, onArchive }: ClientCardPr
<span aria-hidden className="block h-9 w-9 shrink-0" />
</div>
{primary ? (
<p className="truncate text-sm text-muted-foreground">{primary.value}</p>
{primaryContactValue ? (
<p className="truncate text-sm text-muted-foreground">{primaryContactValue}</p>
) : null}
{meta.length > 0 ? (