feat(dashboard): add company filter — switch between portfolio overview and single company view
This commit is contained in:
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { Building2, Heart, AlertTriangle, FileText, TrendingUp, TrendingDown, Minus } from "lucide-react";
|
import { Building2, Heart, AlertTriangle, FileText, TrendingUp, TrendingDown, Minus } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getDashboardSummary, type DashboardSummary } from "@/lib/dashboard";
|
import { getDashboardSummary, type DashboardSummary } from "@/lib/dashboard";
|
||||||
|
import { listCompanies, type Company } from "@/lib/companies";
|
||||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||||
import { EmptyState } from "@/components/shared/EmptyState";
|
import { EmptyState } from "@/components/shared/EmptyState";
|
||||||
import { HealthScoreBadge } from "@/components/shared/HealthScoreBadge";
|
import { HealthScoreBadge } from "@/components/shared/HealthScoreBadge";
|
||||||
@@ -14,13 +15,19 @@ import { HealthHeatmap, HealthTrends } from "@/components/dashboard/HealthHeatma
|
|||||||
*/
|
*/
|
||||||
export default function InvestorDashboardPage() {
|
export default function InvestorDashboardPage() {
|
||||||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||||
|
const [companies, setCompanies] = useState<Company[]>([]);
|
||||||
|
const [selectedCompanyId, setSelectedCompanyId] = useState<string>("");
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const resp = await getDashboardSummary();
|
const [summaryResp, companiesResp] = await Promise.all([
|
||||||
if (resp.data) setSummary(resp.data);
|
getDashboardSummary(),
|
||||||
|
listCompanies({ page_size: 100 }),
|
||||||
|
]);
|
||||||
|
if (summaryResp.data) setSummary(summaryResp.data);
|
||||||
|
if (companiesResp.data?.items) setCompanies(companiesResp.data.items);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
@@ -76,11 +83,35 @@ export default function InvestorDashboardPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 筛选后的评分列表
|
||||||
|
const filteredScores = selectedCompanyId
|
||||||
|
? summary?.recent_scores.filter((s) => s.company_id === selectedCompanyId) ?? []
|
||||||
|
: summary?.recent_scores ?? [];
|
||||||
|
|
||||||
|
// 选中的企业名称
|
||||||
|
const selectedCompany = companies.find((c) => c.id === selectedCompanyId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl font-bold text-foreground">投资机构驾驶舱</h1>
|
<div>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">Portfolio 全局健康度与风险概览</p>
|
<h1 className="text-2xl font-bold text-foreground">投资机构驾驶舱</h1>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
{selectedCompany ? `${selectedCompany.name} — 企业健康度概览` : "Portfolio 全局健康度与风险概览"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 企业筛选器 */}
|
||||||
|
<select
|
||||||
|
value={selectedCompanyId}
|
||||||
|
onChange={(e) => setSelectedCompanyId(e.target.value)}
|
||||||
|
className="rounded-md border bg-white px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--investor-primary)]"
|
||||||
|
>
|
||||||
|
<option value="">全部企业</option>
|
||||||
|
{companies.map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>{c.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* KPI 卡片 */}
|
{/* KPI 卡片 */}
|
||||||
@@ -113,9 +144,9 @@ export default function InvestorDashboardPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary && summary.recent_scores.length > 0 ? (
|
{filteredScores.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{summary.recent_scores.map((score) => (
|
{filteredScores.map((score) => (
|
||||||
<div key={score.id} className="flex items-center justify-between border-b pb-3 last:border-0 last:pb-0">
|
<div key={score.id} className="flex items-center justify-between border-b pb-3 last:border-0 last:pb-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<HealthScoreBadge score={Math.round(score.total_score)} />
|
<HealthScoreBadge score={Math.round(score.total_score)} />
|
||||||
@@ -156,10 +187,10 @@ export default function InvestorDashboardPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-lg border bg-white p-5 shadow-sm">
|
<div className="rounded-lg border bg-white p-5 shadow-sm">
|
||||||
<h2 className="mb-4 text-lg font-semibold">健康度热力图</h2>
|
<h2 className="mb-4 text-lg font-semibold">健康度热力图</h2>
|
||||||
<HealthHeatmap />
|
<HealthHeatmap companyId={selectedCompanyId || undefined} />
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border bg-white p-5 shadow-sm">
|
<div className="rounded-lg border bg-white p-5 shadow-sm">
|
||||||
<HealthTrends />
|
<HealthTrends companyId={selectedCompanyId || undefined} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -155,16 +155,19 @@ export function HealthTrends({ companyId }: { companyId?: string }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function HealthHeatmap() {
|
export function HealthHeatmap({ companyId }: { companyId?: string }) {
|
||||||
const [data, setData] = useState<HeatmapRow[]>([]);
|
const [data, setData] = useState<HeatmapRow[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiFetch<HeatmapRow[]>("/dashboard/heatmap")
|
apiFetch<HeatmapRow[]>("/dashboard/heatmap")
|
||||||
.then((res) => setData((res.data as HeatmapRow[]) || []))
|
.then((res) => {
|
||||||
|
const rows = (res.data as HeatmapRow[]) || [];
|
||||||
|
setData(companyId ? rows.filter((r) => r.company_id === companyId) : rows);
|
||||||
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, [companyId]);
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
if (!data.length) return <EmptyState title="暂无热力图数据" />;
|
if (!data.length) return <EmptyState title="暂无热力图数据" />;
|
||||||
|
|||||||
Reference in New Issue
Block a user