|
|
|
@@ -1,26 +0,0 @@
|
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { ArrowLeft, Download, RefreshCw, Save, Send, Trash2 } from "lucide-react";
|
|
|
|
|
import { FormEvent, useEffect, useState } from "react";
|
|
|
|
|
import { apiFetch } from "@/lib/api";
|
|
|
|
|
|
|
|
|
|
interface Customer { customer_id?: number; id?: number; real_name?: string; name?: string; }
|
|
|
|
|
interface Draft { draft_id?: string; id?: string; title?: string; content?: string; status?: string; customer_id?: number; }
|
|
|
|
|
interface Visit { id: number; customer_id: number; visit_type?: string; visit_time?: string; summary?: string; content?: string; }
|
|
|
|
|
|
|
|
|
|
export default function AdvisorToolsPage() {
|
|
|
|
|
const [customers, setCustomers] = useState<Customer[]>([]); const [customerId, setCustomerId] = useState(""); const [section, setSection] = useState("detail"); const [data, setData] = useState<unknown>(null); const [message, setMessage] = useState("");
|
|
|
|
|
const [drafts, setDrafts] = useState<Draft[]>([]); const [draftId, setDraftId] = useState(""); const [draftTitle, setDraftTitle] = useState(""); const [draftContent, setDraftContent] = useState("");
|
|
|
|
|
const [visits, setVisits] = useState<Visit[]>([]); const [visitId, setVisitId] = useState<number | null>(null); const [visitType, setVisitType] = useState("电话回访"); const [visitTime, setVisitTime] = useState(""); const [visitSummary, setVisitSummary] = useState("");
|
|
|
|
|
useEffect(() => { void apiFetch<{ items?: Customer[] }>("/advisor/customers?page=1&page_size=100").then((result) => { setCustomers(result.items ?? []); const first = result.items?.[0]; if (first) setCustomerId(String(first.customer_id ?? first.id)); }).catch(() => setMessage("客户列表加载失败")); }, []);
|
|
|
|
|
async function load() { setMessage(""); try { const id = Number(customerId); const paths: Record<string, string> = { detail: `/advisor/customers/${id}`, holdings: `/advisor/customers/${id}/holdings`, reports: `/advisor/customers/${id}/reports`, diagnosis: `/advisor/diagnosis/${id}`, strategies: "/advisor/strategies", funds: "/advisor/funds", audit: "/advisor/audit/ledger" }; if (section === "drafts") { const result = await apiFetch<{ items?: Draft[] }>(`/advisor/drafts?page=1&page_size=50${customerId ? `&customer_id=${id}` : ""}`); setDrafts(result.items ?? []); setData(result); return; } if (section === "visits") { const result = await apiFetch<{ items?: Visit[] }>(`/advisor/visits?page=1&page_size=50${customerId ? `&customer_id=${id}` : ""}`); setVisits(result.items ?? []); setData(result); return; } setData(await apiFetch(paths[section])); } catch (error) { setMessage(error instanceof Error ? error.message : "加载失败"); } }
|
|
|
|
|
function chooseDraft(draft: Draft) { const id = String(draft.draft_id ?? draft.id ?? ""); setDraftId(id); setDraftTitle(draft.title ?? ""); setDraftContent(draft.content ?? ""); }
|
|
|
|
|
async function draftAction(action: "save" | "discard" | "send") { if (!draftId) return; try { if (action === "save") await apiFetch(`/advisor/drafts/${draftId}/save`, { method: "PUT", body: JSON.stringify({ title: draftTitle, content: draftContent }) }); else await apiFetch(`/advisor/drafts/${draftId}/${action}`, { method: "POST" }); setMessage(action === "save" ? "草稿已保存" : action === "discard" ? "草稿已废弃" : "草稿已发送"); await load(); } catch (error) { setMessage(error instanceof Error ? error.message : "草稿操作失败"); } }
|
|
|
|
|
async function saveVisit(event: FormEvent) { event.preventDefault(); try { const body = { customer_id: Number(customerId), visit_type: visitType, visit_time: new Date(visitTime).toISOString(), summary: visitSummary }; if (visitId) await apiFetch(`/advisor/visits/${visitId}`, { method: "PUT", body: JSON.stringify(body) }); else await apiFetch("/advisor/visits", { method: "POST", body: JSON.stringify(body) }); setMessage(visitId ? "回访已更新" : "回访已新增"); setVisitId(null); setVisitSummary(""); await load(); } catch (error) { setMessage(error instanceof Error ? error.message : "回访保存失败"); } }
|
|
|
|
|
return <main className="min-h-screen bg-slate-50 px-10 py-10"><div className="mx-auto max-w-7xl"><Link href="/admin/advisor" className="inline-flex items-center gap-2 text-sm text-slate-500"><ArrowLeft className="h-4 w-4" />返回投顾工作区</Link><h1 className="mt-8 text-3xl font-semibold text-[var(--brand-navy)]">投顾业务工具</h1><div className="mt-6 flex flex-wrap gap-3 rounded-xl border border-slate-200 bg-white p-5"><select value={customerId} onChange={(event) => setCustomerId(event.target.value)} className="rounded-lg border border-slate-200 px-3 py-2 text-sm"><option value="">选择客户</option>{customers.map((item) => { const id = item.customer_id ?? item.id; return <option key={id} value={id}>{item.real_name ?? item.name ?? `客户 #${id}`}</option>; })}</select><select value={section} onChange={(event) => setSection(event.target.value)} className="rounded-lg border border-slate-200 px-3 py-2 text-sm">{[["detail", "客户详情"], ["holdings", "客户持仓"], ["reports", "历史报告"], ["diagnosis", "持仓诊断"], ["strategies", "标准策略"], ["funds", "白名单基金"], ["drafts", "投顾草稿"], ["visits", "回访记录"], ["audit", "审计台账"]].map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select><button onClick={() => void load()} disabled={!customerId && ["detail", "holdings", "reports", "diagnosis", "visits"].includes(section)} className="flex items-center gap-2 rounded-lg bg-[var(--brand-primary)] px-4 py-2 text-sm text-white"><RefreshCw className="h-4 w-4" />加载</button>{section === "audit" && <button onClick={() => window.open("/backend/advisor/audit/export", "_blank")} className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm"><Download className="h-4 w-4" />导出</button>}</div>{message && <p className="mt-4 rounded-lg bg-rose-50 p-4 text-sm text-rose-600">{message}</p>}
|
|
|
|
|
{section === "drafts" && <div className="mt-6 grid gap-6 lg:grid-cols-[.8fr_1.2fr]"><div className="rounded-xl border border-slate-200 bg-white p-5"><h2 className="font-semibold">草稿列表</h2><div className="mt-4 divide-y divide-slate-100">{drafts.map((draft) => <button key={draft.draft_id ?? draft.id} onClick={() => chooseDraft(draft)} className={`block w-full px-2 py-3 text-left text-sm ${String(draft.draft_id ?? draft.id) === draftId ? "bg-indigo-50 text-[var(--brand-primary)]" : "text-slate-600"}`}>{draft.title ?? "未命名草稿"}<small className="ml-2 text-slate-400">{draft.status ?? "草稿"}</small></button>)}{!drafts.length && <p className="py-6 text-sm text-slate-400">暂无草稿</p>}</div></div><div className="rounded-xl border border-slate-200 bg-white p-5"><input value={draftTitle} onChange={(event) => setDraftTitle(event.target.value)} placeholder="草稿标题" className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm" /><textarea value={draftContent} onChange={(event) => setDraftContent(event.target.value)} placeholder="草稿内容" className="mt-4 min-h-64 w-full rounded-lg border border-slate-200 px-3 py-2 text-sm" /><div className="mt-4 flex gap-3"><button onClick={() => void draftAction("save")} disabled={!draftId} className="flex items-center gap-2 rounded-lg bg-[var(--brand-primary)] px-4 py-2 text-sm text-white disabled:opacity-50"><Save className="h-4 w-4" />保存</button><button onClick={() => void draftAction("send")} disabled={!draftId} className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm disabled:opacity-50"><Send className="h-4 w-4" />发送</button><button onClick={() => void draftAction("discard")} disabled={!draftId} className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm text-rose-600 disabled:opacity-50"><Trash2 className="h-4 w-4" />废弃</button></div></div></div>}
|
|
|
|
|
{section === "visits" && <div className="mt-6 grid gap-6 lg:grid-cols-[1fr_1fr]"><form onSubmit={saveVisit} className="rounded-xl border border-slate-200 bg-white p-5"><h2 className="font-semibold">{visitId ? "编辑回访" : "新增回访"}</h2><label className="mt-4 block text-sm">回访类型<select value={visitType} onChange={(event) => setVisitType(event.target.value)} className="mt-2 w-full rounded-lg border border-slate-200 px-3 py-2"><option>电话回访</option><option>线上沟通</option><option>线下拜访</option></select></label><label className="mt-4 block text-sm">回访时间<input required type="datetime-local" value={visitTime} onChange={(event) => setVisitTime(event.target.value)} className="mt-2 w-full rounded-lg border border-slate-200 px-3 py-2" /></label><label className="mt-4 block text-sm">回访摘要<textarea value={visitSummary} onChange={(event) => setVisitSummary(event.target.value)} className="mt-2 min-h-32 w-full rounded-lg border border-slate-200 px-3 py-2" /></label><button className="mt-4 rounded-lg bg-[var(--brand-primary)] px-4 py-2 text-sm text-white">保存回访</button></form><div className="rounded-xl border border-slate-200 bg-white p-5"><h2 className="font-semibold">历史回访</h2>{visits.map((visit) => <button key={visit.id} onClick={() => { setVisitId(visit.id); setVisitType(visit.visit_type ?? "电话回访"); setVisitSummary(visit.summary ?? visit.content ?? ""); setVisitTime(visit.visit_time?.slice(0, 16) ?? ""); }} className="mt-3 block w-full rounded-lg bg-slate-50 p-3 text-left text-sm">客户 #{visit.customer_id} · {visit.visit_type ?? "回访"}<p className="mt-1 text-xs text-slate-500">{visit.summary ?? visit.content ?? "暂无摘要"}</p></button>)}{!visits.length && <p className="mt-5 text-sm text-slate-400">暂无回访记录</p>}</div></div>}
|
|
|
|
|
{!['drafts', 'visits'].includes(section) && <pre className="mt-6 min-h-96 overflow-auto rounded-xl border border-slate-200 bg-white p-6 text-xs text-slate-700">{data ? JSON.stringify(data, null, 2) : "选择模块后点击加载"}</pre>}
|
|
|
|
|
</div></main>;
|
|
|
|
|
}
|