'use client'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { useQuery } from '@tanstack/react-query'; import { PageHeader } from '@/components/shared/page-header'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { apiFetch } from '@/lib/api/client'; import { PIPELINE_STAGES, STAGE_LABELS, type PipelineStage } from '@/lib/constants'; interface DashboardStats { totals: { totalClients: number; totalInterests: number; totalBerths: number }; recent: { newInquiries7d: number; completed30d: number }; pipeline: Record; berthStatus: { available: number; under_offer: number; sold: number }; conversion: { closedTotal: number; openTotal: number; conversionPct: number }; } const BERTH_STATUS_COLORS: Record = { available: 'bg-green-500', under_offer: 'bg-amber-500', sold: 'bg-slate-500', }; const BERTH_STATUS_LABELS: Record = { available: 'Available', under_offer: 'Under offer', sold: 'Sold', }; export function ReportsDashboard() { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const { data, isLoading, error } = useQuery({ queryKey: ['admin-dashboard-stats'], queryFn: () => apiFetch<{ data: DashboardStats }>('/api/v1/admin/dashboard-stats'), refetchInterval: 60_000, }); if (isLoading) { return (

Loading…

); } if (error || !data) { return (

Failed to load stats: {error instanceof Error ? error.message : 'unknown error'}

); } const stats = data.data; const maxStageCount = Math.max(1, ...Object.values(stats.pipeline)); const totalBerths = stats.berthStatus.available + stats.berthStatus.under_offer + stats.berthStatus.sold; return (
{/* KPI tiles */}
0 ? 'success' : undefined} />
{/* Pipeline funnel */} Pipeline funnel Open interests by stage (excludes archived).
    {PIPELINE_STAGES.map((stage) => { const n = stats.pipeline[stage] ?? 0; const pct = (n / maxStageCount) * 100; return (
  • {STAGE_LABELS[stage as PipelineStage]} {n}
  • ); })}
Conversion (completed / total):{' '} {stats.conversion.conversionPct}%
{/* Berth occupancy */} Berth occupancy Current public-status mix for {totalBerths} berths in this port.
{(['available', 'under_offer', 'sold'] as const).map((status) => { const n = stats.berthStatus[status]; const pct = totalBerths === 0 ? 0 : Math.round((n / totalBerths) * 100); return (
{BERTH_STATUS_LABELS[status]}
{n} · {pct}%
); })}

Need scheduled or downloadable reports?{' '} Open the report generator {' '} to produce PDF exports of these views.

); } function KpiTile({ label, value, href, accent, }: { label: string; value: number; href?: string; accent?: 'success' | 'danger'; }) { const accentClass = accent === 'success' ? 'text-green-700' : accent === 'danger' ? 'text-red-700' : ''; const inner = (
{value}
{label}
); return href ? ( {inner} ) : ( inner ); }