Files
Mutual_Fund/frontend/components/admin/unified-workspace.tsx
T

218 lines
8.4 KiB
TypeScript
Raw Normal View History

2026-09-14 20:31:06 +08:00
"use client";
import { ShieldAlert } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { AdminShell, type ShellNotification } from "@/components/admin/shell";
import { AdminSection } from "@/components/admin/sections/admin";
import { CustomersSection } from "@/components/admin/sections/customers";
import { DraftsSection } from "@/components/admin/sections/drafts";
import { FundsSection } from "@/components/admin/sections/funds";
import { Nl2SqlSection } from "@/components/admin/sections/nl2sql";
import { OverviewSection } from "@/components/admin/sections/overview";
import { RiskSection } from "@/components/admin/sections/risk";
import { TodosSection } from "@/components/admin/sections/todos";
import { VisitsSection } from "@/components/admin/sections/visits";
import { WorkOrdersSection } from "@/components/admin/sections/workorders";
import { LoadingBlock, NoPermission, ToastProvider, formatTime } from "@/components/admin/ui";
import { adminApi } from "@/lib/admin-api";
import { canUseSection, isAdvisor, isRiskOfficer, isStaff, isWorkspaceSection, type WorkspaceSection } from "@/lib/roles";
import type { UserPayload } from "@/types/api";
const SECTION_META: Record<WorkspaceSection, { title: string; subtitle: string }> = {
overview: { title: "工作台概览", subtitle: "按当前角色展示核心经营指标" },
todos: { title: "我的待办", subtitle: "投顾待办事项的处理与跟踪" },
customers: { title: "客户管理", subtitle: "名下客户、持仓、诊断与签约状态" },
drafts: { title: "投顾草稿", subtitle: "建议草稿的编辑、发送与废弃" },
visits: { title: "客户回访", subtitle: "回访留痕与合规话术参考" },
risk: { title: "风险预警", subtitle: "待处置预警与人工处置通道" },
workorders: { title: "工单处理", subtitle: "认领、提交审核与复核闭环" },
funds: { title: "基金数据", subtitle: "白名单产品与净值数据" },
nl2sql: { title: "智能问数", subtitle: "自然语言查数,自动生成 SQL 并返回结果" },
admin: { title: "系统管理", subtitle: "NL2SQL 配置、知识库与数据管理" },
};
const REQUIRED_ROLE: Record<WorkspaceSection, string> = {
overview: "员工账号",
todos: "投顾权限",
customers: "投顾权限",
drafts: "投顾权限",
visits: "投顾权限",
risk: "风控权限",
workorders: "风控权限",
funds: "员工账号",
nl2sql: "员工账号",
admin: "管理员权限",
};
export function UnifiedWorkspace({ defaultSection = "overview" }: { defaultSection?: WorkspaceSection }) {
const [user, setUser] = useState<UserPayload | null>(null);
const [access, setAccess] = useState<"checking" | "allowed" | "denied">("checking");
const [section, setSection] = useState<WorkspaceSection>(defaultSection);
const [notifications, setNotifications] = useState<ShellNotification[]>([]);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const initial = params.get("section");
if (isWorkspaceSection(initial)) setSection(initial);
}, []);
const changeSection = useCallback((next: WorkspaceSection) => {
setSection(next);
const url = new URL(window.location.href);
url.searchParams.set("section", next);
window.history.replaceState(null, "", url.toString());
}, []);
useEffect(() => {
void (async () => {
try {
const current = await adminApi.me();
setUser(current);
setAccess(isStaff(current) ? "allowed" : "denied");
} catch {
setAccess("denied");
}
})();
}, []);
useEffect(() => {
if (access !== "allowed" || !user) return;
void (async () => {
const result: ShellNotification[] = [];
if (isAdvisor(user)) {
try {
const dashboard = await adminApi.dashboard();
for (const todo of (dashboard.todos?.items ?? []).slice(0, 5)) {
result.push({
id: `todo-${todo.id}`,
title: todo.todo_type,
body: `${todo.customer_id ? `客户 #${todo.customer_id} · ` : ""}截止 ${formatTime(todo.due_at)}`,
time: todo.priority ?? "普通",
tone: todo.priority === "高" ? "alert" : "info",
section: "todos",
});
}
} catch {
/* 无投顾权限时静默跳过 */
}
}
if (isRiskOfficer(user)) {
try {
const alerts = await adminApi.alerts({ status: "未处理", page: 1, page_size: 5 });
for (const alert of alerts.items ?? []) {
result.push({
id: `alert-${alert.id}`,
title: `${alert.alert_type}(${alert.alert_level})`,
body: `客户 #${alert.customer_id} · 待处置`,
time: formatTime(alert.create_time),
tone: alert.alert_level === "高" ? "alert" : "info",
section: "risk",
});
}
} catch {
/* 无风控权限时静默跳过 */
}
try {
const orders = await adminApi.workOrders({ status: "待审核", page: 1, page_size: 5 });
for (const order of orders.items ?? []) {
result.push({
id: `order-${order.id}`,
title: `${order.order_type} 待复核`,
body: `${order.work_order_no} · 优先级 ${order.priority}`,
time: formatTime(order.create_time),
tone: "check",
section: "workorders",
});
}
} catch {
/* 无风控权限时静默跳过 */
}
}
setNotifications(result);
})();
}, [access, user]);
if (access === "checking") {
return (
<main className="flex min-h-screen items-center justify-center bg-slate-50">
<LoadingBlock text="正在验证员工身份..." />
</main>
);
}
if (access === "denied" || !user) {
return (
<main className="flex min-h-screen items-center justify-center bg-slate-50 px-6">
<section className="w-full max-w-md rounded-xl border border-slate-200 bg-white p-8 text-center">
<ShieldAlert className="mx-auto h-10 w-10 text-rose-500" />
<h1 className="mt-5 text-xl font-semibold text-slate-900">无权限访问员工工作台</h1>
<p className="mt-2 text-sm leading-6 text-slate-500">
当前账号不是员工账号,无法进入统一员工工作台。请使用员工账号登录,或返回官网浏览基金产品。
</p>
<div className="mt-6 flex justify-center gap-3">
<Link
href="/"
className="rounded-lg border border-slate-200 px-4 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50"
>
返回官网
</Link>
<Link
href="/login"
className="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
>
重新登录
</Link>
</div>
</section>
</main>
);
}
const meta = SECTION_META[section];
const allowed = canUseSection(user, section);
return (
<ToastProvider>
<AdminShell
user={user}
section={section}
onSectionChange={changeSection}
title={meta.title}
subtitle={meta.subtitle}
notifications={notifications}
>
{!allowed ? (
<NoPermission module={meta.title} hint={`该模块需要${REQUIRED_ROLE[section]},当前账号不具备操作权限。`} />
) : section === "overview" ? (
<OverviewSection
user={user}
onNavigate={(target) => changeSection(target)}
/>
) : section === "todos" ? (
<TodosSection onOpenCustomer={() => changeSection("customers")} />
) : section === "customers" ? (
<CustomersSection />
) : section === "drafts" ? (
<DraftsSection />
) : section === "visits" ? (
<VisitsSection />
) : section === "risk" ? (
<RiskSection />
) : section === "workorders" ? (
<WorkOrdersSection />
) : section === "funds" ? (
<FundsSection user={user} />
) : section === "nl2sql" ? (
<Nl2SqlSection />
) : (
<AdminSection username={user.real_name ?? user.username} />
)}
</AdminShell>
</ToastProvider>
);
}