fix(interests): list yacht join + EOI status column + col redesign

Wire interests.yachtId -> yachts.name into the listInterests post-fetch
enrichment so the redesigned columns (Client · Yacht · Berth · Stage ·
EOI status · Source · Last activity) render the linked yacht.

- Add yachtId/yachtName to InterestRow.
- listInterests: fourth parallel join for yachts.name, Map merged
  alongside the existing client/berth/tag/notes joins.
- interest-columns: add Yacht column (with link to /yachts/[id] when
  the yacht has an id); replace Category with EOI status (badge
  driven by interests.eoi_status); drop default-view Tags.

The "Berth size desired" column called out in §5.2 is deferred to
Phase 2 since the underlying desired_*_ft columns don't exist yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-05 02:18:13 +02:00
parent 3017ce4b3a
commit 05257723f6
2 changed files with 61 additions and 37 deletions

View File

@@ -13,7 +13,6 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Badge } from '@/components/ui/badge';
import { TagBadge } from '@/components/shared/tag-badge';
import { stageBadgeClass, stageLabel } from '@/lib/constants';
import { computeUrgencyBadges, type InterestUrgencyInput } from '@/components/interests/urgency';
@@ -21,6 +20,8 @@ export interface InterestRow {
id: string;
clientId: string;
clientName: string | null;
yachtId?: string | null;
yachtName?: string | null;
berthId: string | null;
berthMooringNumber: string | null;
pipelineStage: string;
@@ -40,12 +41,6 @@ export interface InterestRow {
tags?: Array<{ id: string; name: string; color: string }>;
}
const CATEGORY_LABELS: Record<string, string> = {
general_interest: 'General Interest',
specific_qualified: 'Specific Qualified',
hot_lead: 'Hot Lead',
};
const SOURCE_LABELS: Record<string, string> = {
website: 'Website',
manual: 'Manual',
@@ -53,6 +48,12 @@ const SOURCE_LABELS: Record<string, string> = {
broker: 'Broker',
};
const EOI_STATUS_LABELS: Record<string, { label: string; tone: string }> = {
waiting_for_signatures: { label: 'Waiting', tone: 'bg-amber-100 text-amber-900' },
signed: { label: 'Signed', tone: 'bg-emerald-100 text-emerald-900' },
expired: { label: 'Expired', tone: 'bg-rose-100 text-rose-900' },
};
interface GetColumnsOptions {
portSlug: string;
onEdit: (interest: InterestRow) => void;
@@ -93,6 +94,27 @@ export function getInterestColumns({
);
},
},
{
id: 'yachtName',
accessorKey: 'yachtName',
header: 'Yacht',
enableSorting: false,
cell: ({ row }) => {
const name = row.original.yachtName;
if (!name) return <span className="text-muted-foreground">-</span>;
const yachtId = row.original.yachtId;
if (!yachtId) return <span className="truncate text-sm">{name}</span>;
return (
<Link
href={`/${portSlug}/yachts/${yachtId}`}
className="truncate text-primary hover:underline text-sm"
onClick={(e) => e.stopPropagation()}
>
{name}
</Link>
);
},
},
{
id: 'berthMooringNumber',
accessorKey: 'berthMooringNumber',
@@ -145,16 +167,22 @@ export function getInterestColumns({
},
},
{
id: 'leadCategory',
accessorKey: 'leadCategory',
header: 'Category',
id: 'eoiStatus',
accessorKey: 'eoiStatus',
header: 'EOI status',
enableSorting: false,
cell: ({ getValue }) => {
const cat = getValue() as string | null;
if (!cat) return <span className="text-muted-foreground">-</span>;
const status = getValue() as string | null;
if (!status) return <span className="text-muted-foreground">-</span>;
const meta = EOI_STATUS_LABELS[status];
return (
<Badge variant="outline" className="text-xs capitalize">
{CATEGORY_LABELS[cat] ?? cat}
</Badge>
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
meta?.tone ?? 'bg-muted text-muted-foreground'
}`}
>
{meta?.label ?? status}
</span>
);
},
},
@@ -172,27 +200,6 @@ export function getInterestColumns({
);
},
},
{
id: 'tags',
header: 'Tags',
enableSorting: false,
cell: ({ row }) => {
const rowTags = row.original.tags ?? [];
if (rowTags.length === 0) return <span className="text-muted-foreground">-</span>;
return (
<div className="flex flex-wrap gap-1">
{rowTags.slice(0, 3).map((tag) => (
<TagBadge key={tag.id} name={tag.name} color={tag.color} />
))}
{rowTags.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{rowTags.length - 3}
</Badge>
)}
</div>
);
},
},
{
// Sales-triage default: prefer the explicit dateLastContact, fall back
// to updatedAt. Sortable on dateLastContact server-side; the column

View File

@@ -209,7 +209,7 @@ export async function listInterests(portId: string, query: ListInterestsInput) {
archivedAtColumn: interests.archivedAt,
});
// Join client names and berth mooring numbers
// Join client names, berth mooring numbers, and yacht names.
const interestIds = (
result.data as Array<{ id: string; clientId: string; berthId: string | null }>
).map((i) => i.id);
@@ -223,9 +223,17 @@ export async function listInterests(portId: string, query: ListInterestsInput) {
.filter(Boolean) as string[],
),
];
const yachtIds = [
...new Set(
(result.data as Array<{ yachtId: string | null }>)
.map((i) => i.yachtId)
.filter(Boolean) as string[],
),
];
let clientsMap: Record<string, string> = {};
let berthsMap: Record<string, string> = {};
let yachtsMap: Record<string, string> = {};
const tagsByInterestId: Record<string, Array<{ id: string; name: string; color: string }>> = {};
const notesCountByInterestId: Record<string, number> = {};
@@ -245,6 +253,14 @@ export async function listInterests(portId: string, query: ListInterestsInput) {
berthsMap = Object.fromEntries(berthRows.map((b) => [b.id, b.mooringNumber]));
}
if (yachtIds.length > 0) {
const yachtRows = await db
.select({ id: yachts.id, name: yachts.name })
.from(yachts)
.where(inArray(yachts.id, yachtIds));
yachtsMap = Object.fromEntries(yachtRows.map((y) => [y.id, y.name]));
}
if (interestIds.length > 0) {
const tagRows = await db
.select({
@@ -280,6 +296,7 @@ export async function listInterests(portId: string, query: ListInterestsInput) {
...i,
clientName: clientsMap[i.clientId as string] ?? null,
berthMooringNumber: i.berthId ? (berthsMap[i.berthId as string] ?? null) : null,
yachtName: i.yachtId ? (yachtsMap[i.yachtId as string] ?? null) : null,
tags: tagsByInterestId[i.id as string] ?? [],
notesCount: notesCountByInterestId[i.id as string] ?? 0,
}));