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,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { TagBadge } from '@/components/shared/tag-badge';
|
|
||||||
import { stageBadgeClass, stageLabel } from '@/lib/constants';
|
import { stageBadgeClass, stageLabel } from '@/lib/constants';
|
||||||
import { computeUrgencyBadges, type InterestUrgencyInput } from '@/components/interests/urgency';
|
import { computeUrgencyBadges, type InterestUrgencyInput } from '@/components/interests/urgency';
|
||||||
|
|
||||||
@@ -21,6 +20,8 @@ export interface InterestRow {
|
|||||||
id: string;
|
id: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientName: string | null;
|
clientName: string | null;
|
||||||
|
yachtId?: string | null;
|
||||||
|
yachtName?: string | null;
|
||||||
berthId: string | null;
|
berthId: string | null;
|
||||||
berthMooringNumber: string | null;
|
berthMooringNumber: string | null;
|
||||||
pipelineStage: string;
|
pipelineStage: string;
|
||||||
@@ -40,12 +41,6 @@ export interface InterestRow {
|
|||||||
tags?: Array<{ id: string; name: string; color: string }>;
|
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> = {
|
const SOURCE_LABELS: Record<string, string> = {
|
||||||
website: 'Website',
|
website: 'Website',
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
@@ -53,6 +48,12 @@ const SOURCE_LABELS: Record<string, string> = {
|
|||||||
broker: 'Broker',
|
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 {
|
interface GetColumnsOptions {
|
||||||
portSlug: string;
|
portSlug: string;
|
||||||
onEdit: (interest: InterestRow) => void;
|
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',
|
id: 'berthMooringNumber',
|
||||||
accessorKey: 'berthMooringNumber',
|
accessorKey: 'berthMooringNumber',
|
||||||
@@ -145,16 +167,22 @@ export function getInterestColumns({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'leadCategory',
|
id: 'eoiStatus',
|
||||||
accessorKey: 'leadCategory',
|
accessorKey: 'eoiStatus',
|
||||||
header: 'Category',
|
header: 'EOI status',
|
||||||
|
enableSorting: false,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const cat = getValue() as string | null;
|
const status = getValue() as string | null;
|
||||||
if (!cat) return <span className="text-muted-foreground">-</span>;
|
if (!status) return <span className="text-muted-foreground">-</span>;
|
||||||
|
const meta = EOI_STATUS_LABELS[status];
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className="text-xs capitalize">
|
<span
|
||||||
{CATEGORY_LABELS[cat] ?? cat}
|
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||||
</Badge>
|
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
|
// Sales-triage default: prefer the explicit dateLastContact, fall back
|
||||||
// to updatedAt. Sortable on dateLastContact server-side; the column
|
// to updatedAt. Sortable on dateLastContact server-side; the column
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export async function listInterests(portId: string, query: ListInterestsInput) {
|
|||||||
archivedAtColumn: interests.archivedAt,
|
archivedAtColumn: interests.archivedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Join client names and berth mooring numbers
|
// Join client names, berth mooring numbers, and yacht names.
|
||||||
const interestIds = (
|
const interestIds = (
|
||||||
result.data as Array<{ id: string; clientId: string; berthId: string | null }>
|
result.data as Array<{ id: string; clientId: string; berthId: string | null }>
|
||||||
).map((i) => i.id);
|
).map((i) => i.id);
|
||||||
@@ -223,9 +223,17 @@ export async function listInterests(portId: string, query: ListInterestsInput) {
|
|||||||
.filter(Boolean) as string[],
|
.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 clientsMap: Record<string, string> = {};
|
||||||
let berthsMap: 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 tagsByInterestId: Record<string, Array<{ id: string; name: string; color: string }>> = {};
|
||||||
const notesCountByInterestId: Record<string, number> = {};
|
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]));
|
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) {
|
if (interestIds.length > 0) {
|
||||||
const tagRows = await db
|
const tagRows = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -280,6 +296,7 @@ export async function listInterests(portId: string, query: ListInterestsInput) {
|
|||||||
...i,
|
...i,
|
||||||
clientName: clientsMap[i.clientId as string] ?? null,
|
clientName: clientsMap[i.clientId as string] ?? null,
|
||||||
berthMooringNumber: i.berthId ? (berthsMap[i.berthId as string] ?? null) : 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] ?? [],
|
tags: tagsByInterestId[i.id as string] ?? [],
|
||||||
notesCount: notesCountByInterestId[i.id as string] ?? 0,
|
notesCount: notesCountByInterestId[i.id as string] ?? 0,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user