feat(scope): add company scope switcher — two views (by-task all companies / by-company all tasks)

- CompanyScopeContext + Provider with localStorage persistence
- Sidebar company selector (desktop + mobile)
- apiFetch auto-injects company_id on GET requests
- Dashboard summary API supports company_id filter
- Milestones/Financial/DigitalTwins pages use scope instead of manual input
- HealthHeatmap/HealthTrends components react to scope changes
This commit is contained in:
selfrelease
2026-07-20 07:47:53 +08:00
parent 8b8926bde3
commit e81aa6828e
10 changed files with 306 additions and 53 deletions
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { useCompanyScope } from "@/lib/company-scope";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
import { DIMENSIONS_14 } from "@/components/health/HealthRadar";
@@ -29,18 +30,20 @@ const LINE_COLORS = [
/** 健康度趋势对比图 — 按企业分组,每家企业一条折线。 */
export function HealthTrends({ companyId }: { companyId?: string }) {
const { companyId: scopeCompanyId } = useCompanyScope();
const effectiveCompanyId = companyId ?? scopeCompanyId;
const [data, setData] = useState<CompanyTrend[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const path = companyId
? `/dashboard/trends?company_id=${companyId}&months=6`
const path = effectiveCompanyId
? `/dashboard/trends?company_id=${effectiveCompanyId}&months=6`
: "/dashboard/trends?months=6";
apiFetch<CompanyTrend[]>(path)
.then((res) => setData((res.data as CompanyTrend[]) || []))
.catch(() => {})
.finally(() => setLoading(false));
}, [companyId]);
}, [effectiveCompanyId]);
if (loading) return <LoadingSpinner />;
if (!data.length) return <EmptyState title="暂无趋势数据" />;
@@ -156,6 +159,8 @@ export function HealthTrends({ companyId }: { companyId?: string }) {
);
}
export function HealthHeatmap({ companyId }: { companyId?: string }) {
const { companyId: scopeCompanyId } = useCompanyScope();
const effectiveCompanyId = companyId ?? scopeCompanyId;
const [data, setData] = useState<HeatmapRow[]>([]);
const [loading, setLoading] = useState(true);
@@ -163,11 +168,11 @@ export function HealthHeatmap({ companyId }: { companyId?: string }) {
apiFetch<HeatmapRow[]>("/dashboard/heatmap")
.then((res) => {
const rows = (res.data as HeatmapRow[]) || [];
setData(companyId ? rows.filter((r) => r.company_id === companyId) : rows);
setData(effectiveCompanyId ? rows.filter((r) => r.company_id === effectiveCompanyId) : rows);
})
.catch(() => {})
.finally(() => setLoading(false));
}, [companyId]);
}, [effectiveCompanyId]);
if (loading) return <LoadingSpinner />;
if (!data.length) return <EmptyState title="暂无热力图数据" />;