Files
pn-new-crm/src/components/interests/interest-detail.tsx
Matt 3e4d9d6310 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>
2026-05-07 20:59:28 +02:00

140 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { DetailLayout } from '@/components/shared/detail-layout';
import { InterestDetailHeader } from '@/components/interests/interest-detail-header';
import { getInterestTabs } from '@/components/interests/interest-tabs';
import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider';
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
import { useBreadcrumbHint } from '@/hooks/use-breadcrumb-hint';
import { apiFetch } from '@/lib/api/client';
interface InterestData {
id: string;
portId: string;
clientId: string;
clientName: string | null;
/** Linked client's primary email (display value). Powers the header
* "Email" button and the EOI prereq checklist. */
clientPrimaryEmail: string | null;
/** Linked client's primary phone (display value). Powers the header
* "Call" button. */
clientPrimaryPhone: string | null;
/** Linked client's primary phone in E.164 form ("+1XXXXXXXXXX"). Used
* by wa.me to assemble the WhatsApp deep-link. */
clientPrimaryPhoneE164: string | null;
/** True when the linked client has any primary address row. Used by
* the EOI prereq checklist on the Documents tab. */
clientHasAddress: boolean;
/** Surfaced for the bell badge on the detail header (pending/snoozed
* reminders linked to this interest). */
activeReminderCount?: number;
/** Surfaced for the most-recent-note teaser on the Overview tab. */
notesCount?: number;
recentNote?: {
id: string;
content: string;
authorId: string;
createdAt: string;
} | null;
berthId: string | null;
berthMooringNumber: string | null;
/** Yacht-fit dimensions (numeric strings from postgres). Drive the
* recommender panel guard ("Set desired dimensions to see recommendations"). */
desiredLengthFt: string | null;
desiredWidthFt: string | null;
desiredDraftFt: string | null;
pipelineStage: string;
leadCategory: string | null;
source: string | null;
eoiStatus: string | null;
contractStatus: string | null;
depositStatus: string | null;
reservationStatus: string | null;
dateFirstContact: string | null;
dateLastContact: string | null;
dateEoiSent: string | null;
dateEoiSigned: string | null;
dateContractSent: string | null;
dateContractSigned: string | null;
dateDepositReceived: string | null;
reminderEnabled: boolean;
reminderDays: number | null;
reminderLastFired: string | null;
notes: string | null;
archivedAt: string | null;
createdAt: string;
updatedAt: string;
outcome?: string | null;
outcomeReason?: string | null;
tags: Array<{ id: string; name: string; color: string }>;
}
interface InterestDetailProps {
interestId: string;
currentUserId?: string;
}
export function InterestDetail({ interestId, currentUserId }: InterestDetailProps) {
const params = useParams<{ portSlug: string }>();
const portSlug = params?.portSlug ?? '';
const { data, isLoading } = useQuery<InterestData>({
queryKey: ['interests', interestId],
queryFn: () =>
apiFetch<{ data: InterestData }>(`/api/v1/interests/${interestId}`).then((r) => r.data),
});
useRealtimeInvalidation({
'interest:updated': [['interests', interestId]],
'interest:stageChanged': [['interests', interestId]],
'interest:archived': [['interests', interestId]],
'interest:berthLinked': [['interests', interestId]],
'interest:berthUnlinked': [['interests', interestId]],
});
const { setChrome } = useMobileChrome();
const titleForChrome: string | null = data?.clientName ?? null;
useEffect(() => {
setChrome({ title: titleForChrome, showBackButton: true });
return () => setChrome({ title: null, showBackButton: false });
}, [titleForChrome, setChrome]);
// Topbar breadcrumb: Clients Mary Smith Interest B17.
// Parent client links straight back to the client detail; the
// current crumb is the primary berth's mooring (or "Interest" if
// no berth linked yet — same trick the page H1 uses).
useBreadcrumbHint(
data
? {
parents:
data.clientId && data.clientName
? [{ label: data.clientName, href: `/${portSlug}/clients/${data.clientId}` }]
: [],
current: data.berthMooringNumber ?? 'Interest',
}
: null,
);
const tabs = data
? getInterestTabs({
interestId,
currentUserId,
clientId: data.clientId ?? null,
interest: data,
})
: [];
return (
<DetailLayout
header={data ? <InterestDetailHeader portSlug={portSlug} interest={data} /> : null}
tabs={tabs}
defaultTab="overview"
isLoading={isLoading}
/>
);
}