feat(dashboard): berth-heat widget + investor-default surfacing

Step 6 minimal-but-functional per PRE-DEPLOY-PLAN § 1.6.

Berth Heat — new widget showing top 15 berths by active interest
count via the interest_berths junction (non-primary links included so
multi-berth deals warm every berth in their bundle). Investor-friendly
demand-pressure view; the ranked-table shape exports cleanly to PDF/
CSV. Future heatmap viz reads the same shape via /api/v1/dashboard/
berth-heat.

Defaults flipped for investor-friendliness:
- kpi_pipeline_value → defaultVisible (currency-aware headline number).
- source_conversion → defaultVisible (conversion funnel by source;
  reads the inquiry → client linkage from Step 3).
- berth_heat → defaultVisible.

Pipeline-velocity-over-time + true heatmap viz deferred. pipeline_funnel
covers snapshot stage breakdowns; over-time velocity warrants its own
design pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 15:47:49 +02:00
parent 709ef350ff
commit 66869c9a90
4 changed files with 199 additions and 3 deletions

View File

@@ -0,0 +1,25 @@
import { NextResponse } from 'next/server';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { errorResponse } from '@/lib/errors';
import { getBerthHeatRanking } from '@/lib/services/berth-heat.service';
/**
* GET /api/v1/dashboard/berth-heat
*
* Returns the top-N berths by active interest count, sorted hottest-
* first. Drives the BerthHeatWidget on the dashboard and the future
* heatmap visualization.
*/
export const GET = withAuth(
withPermission('reports', 'view_dashboard', async (req, ctx) => {
try {
const url = new URL(req.url);
const limit = Math.max(1, Math.min(50, Number(url.searchParams.get('limit') ?? '20')));
const rows = await getBerthHeatRanking(ctx.portId, limit);
return NextResponse.json({ data: { rows } });
} catch (error) {
return errorResponse(error);
}
}),
);