/** * Per-berth interest-count rankings — investor-facing analytics surface. * For each berth in the port, returns the count of active interests * (archived_at IS NULL AND outcome IS NULL) currently linked via the * primary `interest_berths` row. * * Drives the BerthHeatWidget on the dashboard (ranked table view). * A future heatmap-style visualization can read the same shape. */ import { and, count, desc, eq, isNull } from 'drizzle-orm'; import { db } from '@/lib/db'; import { berths } from '@/lib/db/schema/berths'; import { interests, interestBerths } from '@/lib/db/schema/interests'; export interface BerthHeatRow { berthId: string; mooringNumber: string; area: string | null; status: string; /** Count of active (non-terminal, non-archived) interests linked to * this berth via interest_berths. Treats every interest-berth link * equally (no is_primary requirement) so a multi-berth deal warms * every berth in its bundle. */ activeInterestCount: number; } export async function getBerthHeatRanking(portId: string, limit = 20): Promise { const rows = await db .select({ berthId: berths.id, mooringNumber: berths.mooringNumber, area: berths.area, status: berths.status, activeInterestCount: count(interests.id), }) .from(berths) .leftJoin(interestBerths, eq(interestBerths.berthId, berths.id)) .leftJoin( interests, and( eq(interests.id, interestBerths.interestId), eq(interests.portId, portId), isNull(interests.archivedAt), isNull(interests.outcome), ), ) .where(and(eq(berths.portId, portId), isNull(berths.archivedAt))) .groupBy(berths.id, berths.mooringNumber, berths.area, berths.status) .orderBy(desc(count(interests.id)), berths.mooringNumber) .limit(limit); return rows.map((r) => ({ berthId: r.berthId, mooringNumber: r.mooringNumber, area: r.area, status: r.status, activeInterestCount: Number(r.activeInterestCount ?? 0), })); }