diff --git a/frontend/src/app/(investor)/aars/page.tsx b/frontend/src/app/(investor)/aars/page.tsx index 47326ca..05aec20 100644 --- a/frontend/src/app/(investor)/aars/page.tsx +++ b/frontend/src/app/(investor)/aars/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import {Sparkles } from "lucide-react"; import { listAARs, generateAAR } from "@/lib/api-v2"; import { PageContainer, Card} from "@/components/shared/PageContainer"; @@ -22,12 +23,12 @@ export default function AARsPage() { const [originalPlan, setOriginalPlan] = useState(""); const [actualResult, setActualResult] = useState(""); - useEffect(() => { + useScopeEffect(() => { listAARs() .then((resp) => setItems((resp.data as AARItem[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); const handleGenerate = async () => { if (!triggerEvent.trim()) { diff --git a/frontend/src/app/(investor)/agents/page.tsx b/frontend/src/app/(investor)/agents/page.tsx index 17e3ad4..6564c84 100644 --- a/frontend/src/app/(investor)/agents/page.tsx +++ b/frontend/src/app/(investor)/agents/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { listAgentExecutions, reviewAgentExecution } from "@/lib/api-v2"; import { PageContainer, Badge} from "@/components/shared/PageContainer"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; @@ -28,7 +29,7 @@ export default function AgentsPage() { .finally(() => setIsLoading(false)); }; - useEffect(() => { load(); }, []); + useScopeEffect(() => { load(); }); const handleReview = async (id: string, status: string) => { try { diff --git a/frontend/src/app/(investor)/agreements/page.tsx b/frontend/src/app/(investor)/agreements/page.tsx index dc547cc..99a8f61 100644 --- a/frontend/src/app/(investor)/agreements/page.tsx +++ b/frontend/src/app/(investor)/agreements/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import {Plus } from "lucide-react"; import { listAgreements } from "@/lib/api-v2"; import { PageContainer, Badge, TableEmpty } from "@/components/shared/PageContainer"; @@ -19,12 +20,12 @@ export default function AgreementsPage() { const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { + useScopeEffect(() => { listAgreements() .then((resp) => setItems((resp.data as Agreement[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); return ( ([]); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { + useScopeEffect(() => { listInterventions() .then((resp) => setItems((resp.data as Intervention[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); return ( ([]); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { + useScopeEffect(() => { listBoardMeetings() .then((resp) => setItems((resp.data as BoardMeeting[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); return ( diff --git a/frontend/src/app/(investor)/customer-growth/page.tsx b/frontend/src/app/(investor)/customer-growth/page.tsx index 3120f23..04b761d 100644 --- a/frontend/src/app/(investor)/customer-growth/page.tsx +++ b/frontend/src/app/(investor)/customer-growth/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { apiFetch } from "@/lib/api"; import { PlanCard } from "@/components/customer/PlanCard"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; @@ -34,10 +35,10 @@ export default function CustomerGrowthPage() { } } - useEffect(() => { + useScopeEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect loadPlans(); - }, []); + }); async function handleGenerate() { if (!companyContext.trim()) return; diff --git a/frontend/src/app/(investor)/customer-success/page.tsx b/frontend/src/app/(investor)/customer-success/page.tsx index d879ad9..15f763b 100644 --- a/frontend/src/app/(investor)/customer-success/page.tsx +++ b/frontend/src/app/(investor)/customer-success/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { apiFetch } from "@/lib/api"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; @@ -50,10 +51,10 @@ export default function CustomerSuccessPage() { } } - useEffect(() => { + useScopeEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect loadChurnRisks(); - }, []); + }); async function handleQBR() { if (!qbrData.trim()) return; diff --git a/frontend/src/app/(investor)/dashboard/page.tsx b/frontend/src/app/(investor)/dashboard/page.tsx index 6153b2a..6b99a69 100644 --- a/frontend/src/app/(investor)/dashboard/page.tsx +++ b/frontend/src/app/(investor)/dashboard/page.tsx @@ -1,10 +1,10 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Building2, Heart, AlertTriangle, FileText, TrendingUp, TrendingDown, Minus } from "lucide-react"; import Link from "next/link"; import { getDashboardSummary, type DashboardSummary } from "@/lib/dashboard"; -import { useCompanyScope } from "@/lib/company-scope"; +import { useCompanyScope, useScopeEffect } from "@/lib/company-scope"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; import { HealthScoreBadge } from "@/components/shared/HealthScoreBadge"; @@ -18,7 +18,7 @@ export default function InvestorDashboardPage() { const [summary, setSummary] = useState(null); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { + useScopeEffect(() => { async function load() { try { const resp = await getDashboardSummary(); @@ -30,7 +30,7 @@ export default function InvestorDashboardPage() { } } load(); - }, []); + }); if (isLoading) { return ( diff --git a/frontend/src/app/(investor)/events/page.tsx b/frontend/src/app/(investor)/events/page.tsx index 9a231c9..2a24671 100644 --- a/frontend/src/app/(investor)/events/page.tsx +++ b/frontend/src/app/(investor)/events/page.tsx @@ -1,7 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { listMajorEvents, listInquiries } from "@/lib/api-v2"; +import { useScopeEffect } from "@/lib/company-scope"; import { PageContainer, Badge } from "@/components/shared/PageContainer"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; @@ -26,7 +27,7 @@ export default function EventsPage() { const [isLoading, setIsLoading] = useState(true); const [tab, setTab] = useState<"events" | "inquiries">("events"); - useEffect(() => { + useScopeEffect(() => { Promise.all([listMajorEvents(), listInquiries()]) .then(([e, i]) => { setEvents((e.data as MajorEvent[]) ?? []); @@ -37,7 +38,7 @@ export default function EventsPage() { setInquiries([]); }) .finally(() => setIsLoading(false)); - }, []); + }); return ( diff --git a/frontend/src/app/(investor)/exit-signals/page.tsx b/frontend/src/app/(investor)/exit-signals/page.tsx index 5aedcd4..41f672a 100644 --- a/frontend/src/app/(investor)/exit-signals/page.tsx +++ b/frontend/src/app/(investor)/exit-signals/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import {Sparkles } from "lucide-react"; import { listExitPredictions, predictExit } from "@/lib/api-v2"; import { PageContainer, Badge, Card } from "@/components/shared/PageContainer"; @@ -22,12 +23,12 @@ export default function ExitSignalsPage() { const [companyData, setCompanyData] = useState(""); const [predicting, setPredicting] = useState(false); - useEffect(() => { + useScopeEffect(() => { listExitPredictions() .then((resp) => setItems((resp.data as ExitPrediction[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); const handlePredict = async () => { if (!companyData.trim()) { diff --git a/frontend/src/app/(investor)/nudges/page.tsx b/frontend/src/app/(investor)/nudges/page.tsx index 99d85f7..57724fa 100644 --- a/frontend/src/app/(investor)/nudges/page.tsx +++ b/frontend/src/app/(investor)/nudges/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { apiFetch } from "@/lib/api"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; @@ -41,10 +42,10 @@ export default function NudgesPage() { } } - useEffect(() => { + useScopeEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect loadNudges(); - }, []); + }); async function handleSelect() { if (!context.trim()) return; diff --git a/frontend/src/app/(investor)/okrs/page.tsx b/frontend/src/app/(investor)/okrs/page.tsx index bf17b4f..0c6d144 100644 --- a/frontend/src/app/(investor)/okrs/page.tsx +++ b/frontend/src/app/(investor)/okrs/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import {Plus } from "lucide-react"; import { listOKRs } from "@/lib/api-v2"; import { PageContainer, Badge, Card } from "@/components/shared/PageContainer"; @@ -27,12 +28,12 @@ export default function OKRsPage() { const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { + useScopeEffect(() => { listOKRs() .then((resp) => setItems((resp.data as OKRItem[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); return ( { + useScopeEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect loadCircles(); - }, []); + }); async function handleMatch() { if (!foundersContext.trim()) return; diff --git a/frontend/src/app/(investor)/pre-mortems/page.tsx b/frontend/src/app/(investor)/pre-mortems/page.tsx index dc7a09c..c1f87ca 100644 --- a/frontend/src/app/(investor)/pre-mortems/page.tsx +++ b/frontend/src/app/(investor)/pre-mortems/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import {Sparkles } from "lucide-react"; import { listPreMortems, runPreMortem, listRedTeams, runRedTeam } from "@/lib/api-v2"; import { PageContainer, Card, Badge } from "@/components/shared/PageContainer"; @@ -33,7 +34,7 @@ export default function PreMortemsPage() { const [input, setInput] = useState(""); const [perspective, setPerspective] = useState("competitor"); - useEffect(() => { + useScopeEffect(() => { Promise.all([listPreMortems(), listRedTeams()]) .then(([pm, rt]) => { setPreMortems((pm.data as PreMortemItem[]) ?? []); @@ -44,7 +45,7 @@ export default function PreMortemsPage() { setRedTeams([]); }) .finally(() => setIsLoading(false)); - }, []); + }); const handleRun = async () => { if (!input.trim()) { diff --git a/frontend/src/app/(investor)/product-diagnostics/page.tsx b/frontend/src/app/(investor)/product-diagnostics/page.tsx index fc95aab..87899a2 100644 --- a/frontend/src/app/(investor)/product-diagnostics/page.tsx +++ b/frontend/src/app/(investor)/product-diagnostics/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { apiFetch } from "@/lib/api"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; @@ -46,10 +47,10 @@ export default function ProductDiagnosticsPage() { } } - useEffect(() => { + useScopeEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect loadDiagnostics(); - }, []); + }); async function handleDiagnose() { if (!productInfo.trim()) return; diff --git a/frontend/src/app/(investor)/reports/page.tsx b/frontend/src/app/(investor)/reports/page.tsx index 73a60d4..6599e6c 100644 --- a/frontend/src/app/(investor)/reports/page.tsx +++ b/frontend/src/app/(investor)/reports/page.tsx @@ -1,8 +1,9 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useState, useCallback } from "react"; import {Plus, Trash2, Send } from "lucide-react"; import { listReports, submitReport, deleteReport, STATUS_LABELS, STATUS_COLORS, type MonthlyReport } from "@/lib/reports"; +import { useScopeEffect } from "@/lib/company-scope"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; @@ -31,7 +32,7 @@ export default function ReportsPage() { } }, [page]); - useEffect(() => { + useScopeEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect loadReports(); }, [loadReports]); diff --git a/frontend/src/app/(investor)/risks/page.tsx b/frontend/src/app/(investor)/risks/page.tsx index 4dc0895..2190694 100644 --- a/frontend/src/app/(investor)/risks/page.tsx +++ b/frontend/src/app/(investor)/risks/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useState, useCallback } from "react"; import { Filter } from "lucide-react"; import { toast } from "sonner"; import { @@ -10,6 +10,7 @@ import { RISK_STATUS_LABELS, type RiskEvent, } from "@/lib/risks"; +import { useScopeEffect } from "@/lib/company-scope"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; import { RiskCard } from "@/components/risk/RiskCard"; @@ -44,7 +45,7 @@ export default function RisksPage() { } }, [page, statusFilter]); - useEffect(() => { + useScopeEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect loadRisks(); }, [loadRisks]); diff --git a/frontend/src/app/(investor)/sentinels/page.tsx b/frontend/src/app/(investor)/sentinels/page.tsx index 4029305..6c8a618 100644 --- a/frontend/src/app/(investor)/sentinels/page.tsx +++ b/frontend/src/app/(investor)/sentinels/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { listDecisionSentinels } from "@/lib/api-v2"; import { PageContainer, Badge} from "@/components/shared/PageContainer"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; @@ -18,12 +19,12 @@ export default function SentinelsPage() { const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { + useScopeEffect(() => { listDecisionSentinels() .then((resp) => setItems((resp.data as DecisionSentinel[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); return ( diff --git a/frontend/src/app/(investor)/synergies/page.tsx b/frontend/src/app/(investor)/synergies/page.tsx index ebda471..f325f32 100644 --- a/frontend/src/app/(investor)/synergies/page.tsx +++ b/frontend/src/app/(investor)/synergies/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { listSynergies, authorizeSynergy } from "@/lib/api-v2"; import { PageContainer, Badge } from "@/components/shared/PageContainer"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; @@ -37,7 +38,7 @@ export default function SynergiesPage() { .finally(() => setIsLoading(false)); }; - useEffect(() => { load(); }, []); + useScopeEffect(() => { load(); }); const handleAuthorize = async (id: string) => { try { diff --git a/frontend/src/app/(investor)/talents/page.tsx b/frontend/src/app/(investor)/talents/page.tsx index fba8286..ada6cb0 100644 --- a/frontend/src/app/(investor)/talents/page.tsx +++ b/frontend/src/app/(investor)/talents/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { listTalents } from "@/lib/api-v2"; import { PageContainer, Badge } from "@/components/shared/PageContainer"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; @@ -20,12 +21,12 @@ export default function TalentsPage() { const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { + useScopeEffect(() => { listTalents() .then((resp) => setItems((resp.data as Talent[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); return ( diff --git a/frontend/src/app/(investor)/tasks/page.tsx b/frontend/src/app/(investor)/tasks/page.tsx index 04041cd..1a03b73 100644 --- a/frontend/src/app/(investor)/tasks/page.tsx +++ b/frontend/src/app/(investor)/tasks/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import {Plus } from "lucide-react"; import { listTasks, updateTask } from "@/lib/api-v2"; import { PageContainer, Badge } from "@/components/shared/PageContainer"; @@ -42,7 +43,7 @@ export default function TasksPage() { .finally(() => setIsLoading(false)); }; - useEffect(() => { load(); }, []); + useScopeEffect(() => { load(); }); const handleStatusChange = async (id: string, status: string) => { try { diff --git a/frontend/src/app/(investor)/weak-signals/page.tsx b/frontend/src/app/(investor)/weak-signals/page.tsx index f44d2a5..ee81ea8 100644 --- a/frontend/src/app/(investor)/weak-signals/page.tsx +++ b/frontend/src/app/(investor)/weak-signals/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useScopeEffect } from "@/lib/company-scope"; import { listWeakSignals } from "@/lib/api-v2"; import { PageContainer, Badge} from "@/components/shared/PageContainer"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; @@ -26,12 +27,12 @@ export default function WeakSignalsPage() { const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { + useScopeEffect(() => { listWeakSignals() .then((resp) => setItems((resp.data as WeakSignal[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }); return ( diff --git a/frontend/src/lib/company-scope.tsx b/frontend/src/lib/company-scope.tsx index 4a813c2..0b2fb11 100644 --- a/frontend/src/lib/company-scope.tsx +++ b/frontend/src/lib/company-scope.tsx @@ -51,7 +51,7 @@ export function CompanyScopeProvider({ children }: { children: ReactNode }) { } }, []); - // 持久化选择 + // 持久化选择 + 派发事件通知各页面重新加载 function setCompanyId(id: string | null) { setCompanyIdState(id); if (id) { @@ -59,6 +59,8 @@ export function CompanyScopeProvider({ children }: { children: ReactNode }) { } else { localStorage.setItem("company_scope_id", "all"); } + // 派发全局事件,让监听了 useScopeEffect 的页面重新加载数据 + window.dispatchEvent(new CustomEvent("company-scope-change", { detail: { companyId: id } })); } const companyName = companyId @@ -86,3 +88,22 @@ export function useCompanyScope() { } return ctx; } + +/** + * 监听企业视角变化,自动触发回调重新加载数据。 + * + * 各页面在 useEffect 中调用此 hook,当全局企业选择变化时, + * 会自动触发回调重新加载(apiFetch 会自动注入新的 company_id)。 + * + * @param callback 数据加载回调 + * @param deps 额外依赖(如分页、筛选状态) + */ +export function useScopeEffect(callback: () => void, deps: unknown[] = []) { + const { companyId } = useCompanyScope(); + + useEffect(() => { + callback(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [companyId, ...deps]); +} +