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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user