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

@@ -77,6 +77,11 @@ export const clientContacts = pgTable(
index('idx_cc_phone')
.on(table.channel, table.value)
.where(sql`${table.channel} = 'phone'`),
// At most one is_primary=true per (client_id, channel). Prevents
// ambiguity when the /clients list pulls "the" primary phone/email.
uniqueIndex('idx_cc_one_primary_per_channel')
.on(table.clientId, table.channel)
.where(sql`${table.isPrimary} = true`),
],
);