Skip to content

Commit ab07770

Browse files
brucezyclaude
andcommitted
feat: add ActionableInsights page, route guard, and Risk Signals nav
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5813c76 commit ab07770

File tree

3 files changed

+136
-15
lines changed

3 files changed

+136
-15
lines changed

frontend/src/components/app/AppLayout.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
PenLine,
1313
Settings,
1414
Shield,
15+
ShieldAlert,
1516
Sparkles,
1617
Swords,
1718
TrendingUp,
@@ -113,16 +114,13 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
113114
path: "/app/insight/market-dynamic",
114115
requiredFeature: "insightAll",
115116
},
116-
// TODO: Risk Intelligence hidden — functionality needs revisiting before re-enabling
117-
// {
118-
// name: "Risk Intelligence",
119-
// icon: ShieldAlert,
120-
// path: "/app/insight/risk-intelligence",
121-
// beta: true,
122-
// requiredFeature: "insightAll",
123-
// },
124117
{ name: "Settings", icon: Settings, path: "/app/settings" },
125-
...(isSuperUser ? [{ name: "Blog Admin", icon: PenLine, path: "/app/admin/blog" }] : []),
118+
...(isSuperUser
119+
? [
120+
{ name: "Blog Admin", icon: PenLine, path: "/app/admin/blog" },
121+
{ name: "Risk Signals", icon: ShieldAlert, path: "/app/insight/risk-intelligence" },
122+
]
123+
: []),
126124
]
127125

128126
const toggleMenu = (name: string) => {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useQuery } from "@tanstack/react-query"
2+
import { useState } from "react"
3+
import { getAuthToken } from "@/clients/auth-helper"
4+
import { dashboardAPI } from "@/clients/dashboard"
5+
import SignalCommandCenter from "./SignalCommandCenter"
6+
import SignalDetail from "./SignalDetail"
7+
8+
// ── Types ────────────────────────────────────────────────────────────────────
9+
10+
interface SignalSummaryItem {
11+
signal_type: string
12+
severity: string
13+
business_meaning: string
14+
score: number
15+
top_competitor: string | null
16+
created_date: string
17+
}
18+
19+
interface OverviewResponse {
20+
date: string
21+
summary: { high: number; medium: number; low: number }
22+
categories: {
23+
competitive_position: SignalSummaryItem[]
24+
momentum_acceleration: SignalSummaryItem[]
25+
structural_strength: SignalSummaryItem[]
26+
risk_instability: SignalSummaryItem[]
27+
}
28+
}
29+
30+
interface SelectedSignal {
31+
signal_type: string
32+
segment: string
33+
date: string
34+
}
35+
36+
// ── API helpers ──────────────────────────────────────────────────────────────
37+
38+
const API_BASE_URL = import.meta.env.VITE_API_URL ?? ""
39+
const API_PREFIX = "/api/v1"
40+
41+
async function fetchOverview(
42+
brand_id: number,
43+
segment: string,
44+
): Promise<OverviewResponse> {
45+
const token = getAuthToken()
46+
const params = new URLSearchParams({ brand_id: String(brand_id), segment })
47+
const url = `${API_BASE_URL}${API_PREFIX}/insights/overview?${params.toString()}`
48+
const res = await fetch(url, {
49+
headers: {
50+
...(token ? { Authorization: `Bearer ${token}` } : {}),
51+
"Content-Type": "application/json",
52+
},
53+
})
54+
if (!res.ok) {
55+
throw new Error(`Failed to fetch insights overview: ${res.status}`)
56+
}
57+
return res.json() as Promise<OverviewResponse>
58+
}
59+
60+
// ── Component ────────────────────────────────────────────────────────────────
61+
62+
export default function ActionableInsights() {
63+
const [selectedSignal, setSelectedSignal] = useState<SelectedSignal | null>(
64+
null,
65+
)
66+
67+
// Fetch brands to get brand_id
68+
const { data: brandsData } = useQuery({
69+
queryKey: ["user-brands-insights"],
70+
queryFn: () => dashboardAPI.getUserBrands(),
71+
})
72+
73+
const brand = brandsData?.brands?.[0]
74+
const brandId = brand ? Number(brand.brand_id) : null
75+
const segment = ""
76+
77+
// Fetch overview once brand is available
78+
const { data: overview, isLoading: overviewLoading } =
79+
useQuery({
80+
queryKey: ["insights-overview", brandId, segment],
81+
queryFn: () => fetchOverview(brandId!, segment),
82+
enabled: brandId !== null,
83+
})
84+
85+
return (
86+
<div className="flex h-full min-h-0 overflow-hidden">
87+
{/* Left: Command Center */}
88+
<div className="w-60 shrink-0 overflow-y-auto border-r border-slate-700">
89+
<SignalCommandCenter
90+
overview={overviewLoading ? undefined : overview}
91+
selectedSignal={selectedSignal}
92+
onSelectSignal={setSelectedSignal}
93+
/>
94+
</div>
95+
96+
{/* Right: Signal Detail */}
97+
<div className="flex-1 overflow-y-auto">
98+
{brandId !== null ? (
99+
<SignalDetail selectedSignal={selectedSignal} brandId={brandId} />
100+
) : (
101+
<div className="flex h-full items-center justify-center text-slate-500">
102+
Loading...
103+
</div>
104+
)}
105+
</div>
106+
</div>
107+
)
108+
}
Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1-
import { createFileRoute, redirect } from "@tanstack/react-router"
1+
import { createFileRoute, useNavigate } from "@tanstack/react-router"
2+
import { useEffect } from "react"
3+
import { useSubscription } from "@/contexts/SubscriptionContext"
4+
import ActionableInsights from "@/components/app/insight/ActionableInsights"
25

3-
// Risk Intelligence is temporarily hidden — redirect direct URL access to market dynamic
4-
// The component and backend endpoints are preserved for future re-enabling
56
export const Route = createFileRoute("/app/insight/risk-intelligence")({
6-
beforeLoad: () => {
7-
throw redirect({ to: "/app/insight/market-dynamic" })
8-
},
7+
component: RiskSignalsPage,
98
})
9+
10+
function RiskSignalsPage() {
11+
const navigate = useNavigate()
12+
const { subscription } = useSubscription()
13+
const isSuperUser = subscription?.is_super_user === true
14+
15+
useEffect(() => {
16+
if (subscription !== undefined && !isSuperUser) {
17+
navigate({ to: "/app/dashboard/overview" })
18+
}
19+
}, [isSuperUser, subscription, navigate])
20+
21+
if (!isSuperUser) return null
22+
23+
return <ActionableInsights />
24+
}

0 commit comments

Comments
 (0)