feat(client): interests tab + pipeline summary panel + list-row counts

Promotes interests from a stub tab to a first-class surface on the client
detail page, and surfaces pipeline activity in two more places:

UI:
  - New ClientInterestsTab (475 lines) — table of every active interest
    for the client with stage-stepper visualization, lead category, source,
    last-activity timestamp, and a drawer-on-tap row preview.
  - New OverviewTab pipeline-summary panel above the existing 2-column
    layout, rendering ClientPipelineSummary (already on this branch) in
    its panel variant. Reps see the live pipeline at a glance without
    leaving Overview.
  - Removes "Preferred Language" inline field from the Overview tab and
    the create form — unused, and the field added noise without driving
    any downstream behavior.
  - Tab order: Overview / Interests / Yachts / Companies / ... (Interests
    moves up from the back of the list, where it was a stub anyway).

Data:
  - listClients now returns interestCount + latestInterest{stage, mooring}
    per row, joined from interests + berths in two parallel queries.
    ClientRow type updated to surface them; Client list views can now
    render "3 interests · last on D-02 (EOI Signed)" without a per-row
    fetch.
  - Contact rows in client detail now expose valueE164 + valueCountry to
    the UI (already returned by the API; just wasn't typed through the
    detail-page contract).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-03 16:14:37 +02:00
parent 4767caec01
commit e9359fc431
6 changed files with 601 additions and 92 deletions

View File

@@ -9,6 +9,8 @@ import { InlineTimezoneField } from '@/components/shared/inline-timezone-field';
import { InlineTagEditor } from '@/components/shared/inline-tag-editor';
import { NotesList } from '@/components/shared/notes-list';
import type { CountryCode } from '@/lib/i18n/countries';
import { ClientInterestsTab } from '@/components/clients/client-interests-tab';
import { ClientPipelineSummary } from '@/components/clients/client-pipeline-summary';
import { ClientYachtsTab } from '@/components/clients/client-yachts-tab';
import { ClientCompaniesTab } from '@/components/clients/client-companies-tab';
import { ClientReservationsTab } from '@/components/clients/client-reservations-tab';
@@ -131,82 +133,82 @@ function OverviewTab({
};
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Personal Info */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Personal Information</h3>
<dl>
<EditableRow label="Full Name">
<InlineEditableField value={client.fullName} onSave={save('fullName')} />
</EditableRow>
<EditableRow label="Nationality">
<InlineCountryField
value={client.nationalityIso ?? null}
onSave={async (iso) => {
await mutation.mutateAsync({ nationalityIso: iso });
}}
data-testid="client-nationality-inline"
/>
</EditableRow>
<EditableRow label="Preferred Language">
<InlineEditableField
value={client.preferredLanguage}
onSave={save('preferredLanguage')}
/>
</EditableRow>
<EditableRow label="Timezone">
<InlineTimezoneField
value={client.timezone}
countryHint={(client.nationalityIso as CountryCode | null) ?? null}
onSave={async (tz) => {
await mutation.mutateAsync({ timezone: tz });
}}
data-testid="client-timezone-inline"
/>
</EditableRow>
<EditableRow label="Preferred Contact">
<InlineEditableField
variant="select"
options={CONTACT_METHOD_OPTIONS}
value={client.preferredContactMethod}
onSave={save('preferredContactMethod')}
/>
</EditableRow>
</dl>
<div className="space-y-6">
<div className="rounded-xl border border-border bg-card p-4 shadow-sm">
<ClientPipelineSummary clientId={clientId} variant="panel" />
</div>
{/* Contacts */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Contact Details</h3>
<ContactsEditor clientId={clientId} contacts={client.contacts ?? []} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Personal Info */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Personal Information</h3>
<dl>
<EditableRow label="Full Name">
<InlineEditableField value={client.fullName} onSave={save('fullName')} />
</EditableRow>
<EditableRow label="Nationality">
<InlineCountryField
value={client.nationalityIso ?? null}
onSave={async (iso) => {
await mutation.mutateAsync({ nationalityIso: iso });
}}
data-testid="client-nationality-inline"
/>
</EditableRow>
<EditableRow label="Timezone">
<InlineTimezoneField
value={client.timezone}
countryHint={(client.nationalityIso as CountryCode | null) ?? null}
onSave={async (tz) => {
await mutation.mutateAsync({ timezone: tz });
}}
data-testid="client-timezone-inline"
/>
</EditableRow>
<EditableRow label="Preferred Contact">
<InlineEditableField
variant="select"
options={CONTACT_METHOD_OPTIONS}
value={client.preferredContactMethod}
onSave={save('preferredContactMethod')}
/>
</EditableRow>
</dl>
</div>
{/* Source */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Source</h3>
<dl>
<EditableRow label="Source">
<InlineEditableField
variant="select"
options={SOURCE_OPTIONS}
value={client.source}
onSave={save('source')}
/>
</EditableRow>
<EditableRow label="Source Details">
<InlineEditableField value={client.sourceDetails} onSave={save('sourceDetails')} />
</EditableRow>
</dl>
</div>
{/* Contacts */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Contact Details</h3>
<ContactsEditor clientId={clientId} contacts={client.contacts ?? []} />
</div>
{/* Tags */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Tags</h3>
<InlineTagEditor
endpoint={`/api/v1/clients/${clientId}/tags`}
currentTags={client.tags ?? []}
invalidateKey={['clients', clientId]}
/>
{/* Source */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Source</h3>
<dl>
<EditableRow label="Source">
<InlineEditableField
variant="select"
options={SOURCE_OPTIONS}
value={client.source}
onSave={save('source')}
/>
</EditableRow>
<EditableRow label="Source Details">
<InlineEditableField value={client.sourceDetails} onSave={save('sourceDetails')} />
</EditableRow>
</dl>
</div>
{/* Tags */}
<div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Tags</h3>
<InlineTagEditor
endpoint={`/api/v1/clients/${clientId}/tags`}
currentTags={client.tags ?? []}
invalidateKey={['clients', clientId]}
/>
</div>
</div>
</div>
);
@@ -219,6 +221,11 @@ export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOpt
label: 'Overview',
content: <OverviewTab clientId={clientId} client={client} />,
},
{
id: 'interests',
label: 'Interests',
content: <ClientInterestsTab clientId={clientId} />,
},
{
id: 'yachts',
label: 'Yachts',
@@ -251,15 +258,6 @@ export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOpt
/>
),
},
{
id: 'interests',
label: 'Interests',
content: (
<div className="text-center py-12 text-muted-foreground">
<p>Interests will appear here once created.</p>
</div>
),
},
{
id: 'notes',
label: 'Notes',