@@ -1,46 +0,0 @@
"use client" ;
import Link from "next/link" ;
import { Activity , AlertTriangle , BarChart3 , Check , FileText , LogOut , ShieldAlert , Users } from "lucide-react" ;
import { useEffect , useState } from "react" ;
import { apiFetch } from "@/lib/api" ;
import { clearToken , getRoleLabel } from "@/lib/auth" ;
import type { UserPayload } from "@/types/api" ;
interface DashboardData { overview ? : { total_customers? : number ; signed_customers? : number ; total_aum? : string } ; todos ? : { pending? : number } }
interface RiskAlert { id : number ; customer_id : number ; alert_type : string ; alert_level : string ; status : string }
interface WorkOrder { id : number ; work_order_no : string ; order_type : string ; status : string ; priority : string }
interface Customer { customer_id? : number ; id? : number ; real_name? : string ; name? : string ; risk_level? : string ; total_aum? : string | number }
interface Todo { id : number ; todo_type : string ; status : string ; priority : string }
interface Visit { id : number ; customer_id : number ; visit_type? : string ; content? : string }
function Frame ( { title , subtitle , role , children } : { title : string ; subtitle : string ; role : string ; children : React.ReactNode } ) {
return < main className = "min-h-screen bg-slate-50 text-slate-900" > < header className = "flex h-20 items-center justify-between border-b border-slate-200 bg-white px-10" > < div > < p className = "text-xs uppercase tracking-[0.2em] text-slate-400" > Huaxia Fund Operations < / p > < h1 className = "mt-1 text-xl font-semibold text-[var(--brand-navy)]" > { title } < / h1 > < / div > < div className = "flex items-center gap-5" > < span className = "text-sm text-slate-500" > { role } < / span > < button onClick = { ( ) = > { clearToken ( ) ; window . location . href = "/login" ; } } className = "flex items-center gap-2 text-sm text-slate-500 hover:text-[var(--brand-primary)]" > < LogOut className = "h-4 w-4" / > 退 出 登 录 < / button > < / div > < / header > < section className = "mx-auto max-w-7xl px-10 py-10" > < p className = "text-sm text-slate-500" > { subtitle } < / p > { children } < / section > < / main > ;
}
async function getMe() { return ( await apiFetch < { user : UserPayload } > ( "/auth/me" ) ) . user ; }
function Notice ( { children } : { children : React.ReactNode } ) { return < div className = "mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800" > { children } < / div > ; }
export function AdvisorWorkspace() {
const [ user , setUser ] = useState < UserPayload | null > ( null ) ; const [ dashboard , setDashboard ] = useState < DashboardData | null > ( null ) ; const [ customers , setCustomers ] = useState < Customer [ ] > ( [ ] ) ; const [ todos , setTodos ] = useState < Todo [ ] > ( [ ] ) ; const [ visits , setVisits ] = useState < Visit [ ] > ( [ ] ) ; const [ report , setReport ] = useState < Record < string , unknown > | null > ( null ) ; const [ error , setError ] = useState ( "" ) ;
async function load() { try { const [ current , overview , customerData , todoData , visitData , personalReport ] = await Promise . all ( [ getMe ( ) , apiFetch < DashboardData > ( "/advisor/dashboard" ) , apiFetch < { items? : Customer [ ] } > ( "/advisor/customers?page=1&page_size=50" ) , apiFetch < { items? : Todo [ ] } > ( "/advisor/todos?page=1&page_size=20" ) , apiFetch < { items? : Visit [ ] } > ( "/advisor/visits?page=1&page_size=20" ) , apiFetch < Record < string , unknown > > ( "/advisor/report/personal" ) ] ) ; setUser ( current ) ; setDashboard ( overview ) ; setCustomers ( customerData . items ? ? [ ] ) ; setTodos ( todoData . items ? ? [ ] ) ; setVisits ( visitData . items ? ? [ ] ) ; setReport ( personalReport ) ; } catch { setError ( "部分投顾数据加载失败,请确认投顾权限或后端服务状态。" ) ; } }
useEffect ( ( ) = > { void load ( ) ; } , [ ] ) ;
async function finishTodo ( id : number ) { await apiFetch ( ` /advisor/todos/ ${ id } /handle ` , { method : "POST" , body : JSON.stringify ( { action : "done" } ) } ) ; await load ( ) ; }
const overview = dashboard ? . overview ;
return < Frame title = "投顾工作区" subtitle = "客户关系、投顾服务与业务待办" role = { getRoleLabel ( user ? . employee_role ) } > { error && < Notice > { error } < / Notice > } < div className = "mt-8 grid gap-5 md:grid-cols-4" > < div className = "rounded-xl border border-slate-200 bg-white p-6" > < Users className = "h-5 w-5 text-[var(--brand-primary)]" / > < p className = "mt-5 text-sm text-slate-500" > 名 下 客 户 < / p > < p className = "mt-2 text-3xl font-semibold" > { overview ? . total_customers ? ? "--" } < / p > < / div > < div className = "rounded-xl border border-slate-200 bg-white p-6" > < Check className = "h-5 w-5 text-[var(--brand-primary)]" / > < p className = "mt-5 text-sm text-slate-500" > 签 约 客 户 < / p > < p className = "mt-2 text-3xl font-semibold" > { overview ? . signed_customers ? ? "--" } < / p > < / div > < div className = "rounded-xl border border-slate-200 bg-white p-6" > < BarChart3 className = "h-5 w-5 text-[var(--brand-primary)]" / > < p className = "mt-5 text-sm text-slate-500" > 管 理 资 产 < / p > < p className = "mt-2 text-3xl font-semibold" > { overview ? . total_aum ? ` ¥ ${ overview . total_aum } ` : "--" } < / p > < / div > < div className = "rounded-xl border border-slate-200 bg-white p-6" > < Activity className = "h-5 w-5 text-[var(--brand-primary)]" / > < p className = "mt-5 text-sm text-slate-500" > 待 办 事 项 < / p > < p className = "mt-2 text-3xl font-semibold" > { dashboard ? . todos ? . pending ? ? "--" } < / p > < / div > < / div > < div className = "mt-8 grid gap-6 lg:grid-cols-2" > < section className = "rounded-xl border border-slate-200 bg-white p-6" > < h2 className = "font-semibold" > 客 户 管 理 < / h2 > < div className = "mt-4 divide-y divide-slate-100" > { customers . slice ( 0 , 8 ) . map ( ( item ) = > < div key = { item . customer_id ? ? item . id } className = "flex justify-between py-3 text-sm" > < span > { item . real_name ? ? item . name ? ? ` 客户 # ${ item . customer_id ? ? item . id } ` } < / span > < span className = "text-slate-500" > { item . risk_level ? ? "未评估" } · { item . total_aum ? ? "--" } < / span > < / div > ) } { ! customers . length && < p className = "py-6 text-sm text-slate-400" > 暂 无 客 户 数 据 < / p > } < / div > < / section > < section className = "rounded-xl border border-slate-200 bg-white p-6" > < h2 className = "font-semibold" > 待 办 处 理 < / h2 > < div className = "mt-4 space-y-3" > { todos . slice ( 0 , 6 ) . map ( ( todo ) = > < div key = { todo . id } className = "flex items-center justify-between rounded-lg bg-slate-50 p-3 text-sm" > < span > { todo . todo_type } · { todo . priority } < / span > < button onClick = { ( ) = > void finishTodo ( todo . id ) } className = "text-[var(--brand-primary)]" > 完 成 < / button > < / div > ) } { ! todos . length && < p className = "py-6 text-sm text-slate-400" > 暂 无 待 办 < / p > } < / div > < / section > < section className = "rounded-xl border border-slate-200 bg-white p-6" > < h2 className = "font-semibold" > 回 访 记 录 < / h2 > < div className = "mt-4 space-y-3" > { visits . slice ( 0 , 5 ) . map ( ( visit ) = > < div key = { visit . id } className = "rounded-lg bg-slate-50 p-3 text-sm" > 客 户 # { visit . customer_id } · { visit . visit_type ? ? "回访" } < p className = "mt-1 text-xs text-slate-500" > { visit . content ? ? "暂无内容" } < / p > < / div > ) } { ! visits . length && < p className = "py-6 text-sm text-slate-400" > 暂 无 回 访 记 录 < / p > } < / div > < / section > < section className = "rounded-xl border border-slate-200 bg-white p-6" > < h2 className = "font-semibold" > 个 人 绩 效 < / h2 > < pre className = "mt-4 max-h-48 overflow-auto rounded-lg bg-slate-50 p-3 text-xs text-slate-600" > { report ? JSON . stringify ( report , null , 2 ) : "暂无绩效数据" } < / pre > < / section > < / div > < / Frame > ;
}
export function RiskWorkspace() {
const [ user , setUser ] = useState < UserPayload | null > ( null ) ; const [ alerts , setAlerts ] = useState < RiskAlert [ ] > ( [ ] ) ; const [ orders , setOrders ] = useState < WorkOrder [ ] > ( [ ] ) ; const [ error , setError ] = useState ( "" ) ;
async function load() { try { const [ current , alertData , orderData ] = await Promise . all ( [ getMe ( ) , apiFetch < { items? : RiskAlert [ ] } > ( "/risk/alert/list?page=1&page_size=10" ) , apiFetch < { items? : WorkOrder [ ] } > ( "/work-order/list?page=1&page_size=10" ) ] ) ; setUser ( current ) ; setAlerts ( alertData . items ? ? [ ] ) ; setOrders ( orderData . items ? ? [ ] ) ; } catch { setError ( "风控数据加载失败,请确认风控权限或后端服务状态。" ) ; } }
useEffect ( ( ) = > { void load ( ) ; } , [ ] ) ;
async function alertAction ( id : number , action : "release" | "block" | "freeze" ) { await apiFetch ( ` /risk/alert/ ${ id } / ${ action } ` , { method : "POST" } ) ; await load ( ) ; }
async function orderAction ( id : number , action : "claim" | "submit-review" | "review" ) { const body = action === "review" ? { work_order_id : id , approve : window.confirm ( "确认通过该工单?" ) , comment : "前端工作区复核" } : { work_order_id : id } ; await apiFetch ( ` /work-order/ ${ action } ` , { method : "POST" , body : JSON.stringify ( body ) } ) ; await load ( ) ; }
return < Frame title = "风控工作区" subtitle = "风险预警、处置与工单审核" role = { getRoleLabel ( user ? . employee_role ) } > { error && < Notice > { error } < / Notice > } < div className = "mt-8 grid gap-5 md:grid-cols-3" > < div className = "rounded-xl border border-slate-200 bg-white p-6" > < ShieldAlert className = "h-5 w-5 text-rose-500" / > < p className = "mt-5 text-sm text-slate-500" > 当 前 预 警 < / p > < p className = "mt-2 text-3xl font-semibold" > { alerts . length } < / p > < / div > < div className = "rounded-xl border border-slate-200 bg-white p-6" > < AlertTriangle className = "h-5 w-5 text-amber-500" / > < p className = "mt-5 text-sm text-slate-500" > 高 风 险 事 项 < / p > < p className = "mt-2 text-3xl font-semibold" > { alerts . filter ( ( item ) = > item . alert_level === "高" ) . length } < / p > < / div > < div className = "rounded-xl border border-slate-200 bg-white p-6" > < FileText className = "h-5 w-5 text-[var(--brand-primary)]" / > < p className = "mt-5 text-sm text-slate-500" > 工 单 数 量 < / p > < p className = "mt-2 text-3xl font-semibold" > { orders . length } < / p > < / div > < / div > < section className = "mt-8 rounded-xl border border-slate-200 bg-white p-6" > < h2 className = "font-semibold" > 风 险 预 警 处 置 < / h2 > < div className = "mt-4 divide-y divide-slate-100" > { alerts . map ( ( alert ) = > < div key = { alert . id } className = "flex items-center justify-between gap-4 py-4 text-sm" > < span > { alert . alert_type } · 客 户 # { alert . customer_id } < small className = "ml-3 text-slate-400" > { alert . status } < / small > < / span > < div className = "flex gap-2" > < button onClick = { ( ) = > void alertAction ( alert . id , "release" ) } className = "rounded border px-2 py-1 text-xs hover:text-emerald-600" > 放 行 < / button > < button onClick = { ( ) = > void alertAction ( alert . id , "block" ) } className = "rounded border px-2 py-1 text-xs hover:text-rose-600" > 拦 截 < / button > < button onClick = { ( ) = > void alertAction ( alert . id , "freeze" ) } className = "rounded border px-2 py-1 text-xs hover:text-amber-600" > 冻 结 < / button > < / div > < / div > ) } { ! alerts . length && < p className = "py-6 text-sm text-slate-400" > 暂 无 风 险 预 警 < / p > } < / div > < / section > < section className = "mt-8 rounded-xl border border-slate-200 bg-white p-6" > < h2 className = "font-semibold" > 业 务 工 单 < / h2 > < div className = "mt-4 divide-y divide-slate-100" > { orders . map ( ( item ) = > < div key = { item . id } className = "flex items-center justify-between gap-4 py-4 text-sm" > < span > { item . work_order_no } · { item . order_type } · { item . status } < / span > < div className = "flex gap-2" > < button onClick = { ( ) = > void orderAction ( item . id , "claim" ) } className = "rounded border px-2 py-1 text-xs" > 认 领 < / button > < button onClick = { ( ) = > void orderAction ( item . id , "submit-review" ) } className = "rounded border px-2 py-1 text-xs" > 提 交 审 核 < / button > < button onClick = { ( ) = > void orderAction ( item . id , "review" ) } className = "rounded border px-2 py-1 text-xs" > 复 核 < / button > < / div > < / div > ) } { ! orders . length && < p className = "py-6 text-sm text-slate-400" > 暂 无 工 单 < / p > } < / div > < / section > < / Frame > ;
}
export function CommonWorkspace() {
const [ user , setUser ] = useState < UserPayload | null > ( null ) ; useEffect ( ( ) = > { void getMe ( ) . then ( setUser ) . catch ( ( ) = > undefined ) ; } , [ ] ) ;
const isAdmin = user ? . user_type === "ADMIN" ;
return < Frame title = "员工通用工作区" subtitle = "基金信息、系统工具与基础运营入口" role = { getRoleLabel ( user ? . employee_role ) } > < div className = "mt-8 grid gap-5 md:grid-cols-3" > < Link href = "/products" className = "rounded-xl border border-slate-200 bg-white p-6 hover:border-[var(--brand-primary)]" > < FileText className = "h-5 w-5 text-[var(--brand-primary)]" / > < h2 className = "mt-5 font-semibold" > 基 金 数 据 < / h2 > < p className = "mt-2 text-sm text-slate-500" > 查 看 真 实 基 金 产 品 和 历 史 净 值 < / p > < / Link > { isAdmin ? < Link href = "/tools/nl2sql" className = "rounded-xl border border-slate-200 bg-white p-6 hover:border-[var(--brand-primary)]" > < Activity className = "h-5 w-5 text-[var(--brand-primary)]" / > < h2 className = "mt-5 font-semibold" > NL2SQL 配 置 < / h2 > < p className = "mt-2 text-sm text-slate-500" > 管 理 员 专 用 : 运 行 配 置 和 查 询 权 限 管 理 < / p > < / Link > : < Link href = "/tools/nl2sql" className = "rounded-xl border border-slate-200 bg-white p-6 hover:border-[var(--brand-primary)]" > < Activity className = "h-5 w-5 text-[var(--brand-primary)]" / > < h2 className = "mt-5 font-semibold" > 数 据 查 询 < / h2 > < p className = "mt-2 text-sm text-slate-500" > 员 工 自 然 语 言 查 询 与 历 史 记 录 < / p > < / Link > } { isAdmin && < Link href = "/tools/knowledge" className = "rounded-xl border border-slate-200 bg-white p-6 hover:border-[var(--brand-primary)]" > < FileText className = "h-5 w-5 text-[var(--brand-primary)]" / > < h2 className = "mt-5 font-semibold" > 知 识 库 管 理 < / h2 > < p className = "mt-2 text-sm text-slate-500" > 管 理 员 专 用 : 文 档 上 传 、 预 览 和 删 除 < / p > < / Link > } < / div > < / Frame > ;
}