feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import type { DetailTab } from '@/components/shared/detail-layout';
|
||||
import { InlineEditableField } from '@/components/shared/inline-editable-field';
|
||||
import { InlineCountryField } from '@/components/shared/inline-country-field';
|
||||
import { InlineTimezoneField } from '@/components/shared/inline-timezone-field';
|
||||
import { primaryTimezoneFor } from '@/lib/i18n/timezones';
|
||||
import { InlineTagEditor } from '@/components/shared/inline-tag-editor';
|
||||
import { NotesList } from '@/components/shared/notes-list';
|
||||
import type { CountryCode } from '@/lib/i18n/countries';
|
||||
@@ -35,6 +36,7 @@ const SOURCE_OPTIONS = [
|
||||
{ value: 'manual', label: 'Manual' },
|
||||
{ value: 'referral', label: 'Referral' },
|
||||
{ value: 'broker', label: 'Broker' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
];
|
||||
|
||||
const CONTACT_METHOD_OPTIONS = [
|
||||
@@ -150,18 +152,36 @@ function OverviewTab({
|
||||
<EditableRow label="Full Name">
|
||||
<InlineEditableField value={client.fullName} onSave={save('fullName')} />
|
||||
</EditableRow>
|
||||
<EditableRow label="Nationality">
|
||||
<EditableRow label="Country">
|
||||
<InlineCountryField
|
||||
value={client.nationalityIso ?? null}
|
||||
onSave={async (iso) => {
|
||||
await mutation.mutateAsync({ nationalityIso: iso });
|
||||
// Auto-default the timezone to the country's primary
|
||||
// zone when none is set yet — saves the rep a click
|
||||
// and matches what a marina actually wants for first
|
||||
// contact (London for GB, NYC for US, etc.). Only
|
||||
// fires when timezone is empty so we never clobber a
|
||||
// value the rep deliberately picked.
|
||||
const patch: { nationalityIso: string | null; timezone?: string | null } = {
|
||||
nationalityIso: iso,
|
||||
};
|
||||
if (iso && !client.timezone) {
|
||||
const defaultTz = primaryTimezoneFor(iso as CountryCode);
|
||||
if (defaultTz) patch.timezone = defaultTz;
|
||||
}
|
||||
await mutation.mutateAsync(patch);
|
||||
}}
|
||||
data-testid="client-nationality-inline"
|
||||
data-testid="client-country-inline"
|
||||
/>
|
||||
</EditableRow>
|
||||
<EditableRow label="Timezone">
|
||||
<InlineTimezoneField
|
||||
value={client.timezone}
|
||||
value={
|
||||
client.timezone ??
|
||||
(client.nationalityIso
|
||||
? primaryTimezoneFor(client.nationalityIso as CountryCode)
|
||||
: null)
|
||||
}
|
||||
countryHint={(client.nationalityIso as CountryCode | null) ?? null}
|
||||
onSave={async (tz) => {
|
||||
await mutation.mutateAsync({ timezone: tz });
|
||||
@@ -267,7 +287,14 @@ export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOpt
|
||||
id: 'notes',
|
||||
label: 'Notes',
|
||||
badge: client.noteCount,
|
||||
content: <NotesList entityType="clients" entityId={clientId} currentUserId={currentUserId} />,
|
||||
content: (
|
||||
<NotesList
|
||||
entityType="clients"
|
||||
entityId={clientId}
|
||||
currentUserId={currentUserId}
|
||||
aggregate
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
|
||||
Reference in New Issue
Block a user