import React, { useEffect, useMemo, useRef, useState } from "react"; import { motion } from "framer-motion"; import { Bell, Wallet, AlertTriangle, CheckCircle2, Users, Settings, FileText, Plus, Search, Filter, Download, ArrowUpRight, ArrowDownRight, Clock3, Building2, BadgeCheck, CircleDollarSign, ClipboardList, Upload, Trash2, ShieldCheck, Database, } from "lucide-react"; const money = (n) => new Intl.NumberFormat("en-MY", { style: "currency", currency: "MYR", maximumFractionDigits: 0, }).format(Number(n || 0)); const todayString = () => new Date().toISOString().slice(0, 10); const uid = () => Math.random().toString(36).slice(2, 10); const parseCsv = (text) => { const lines = text .split(/ ? /) .map((line) => line.trim()) .filter(Boolean); if (!lines.length) return []; const splitRow = (row) => { const cells = []; let current = ""; let inQuotes = false; for (let i = 0; i < row.length; i += 1) { const char = row[i]; const next = row[i + 1]; if (char === '"') { if (inQuotes && next === '"') { current += '"'; i += 1; } else { inQuotes = !inQuotes; } } else if (char === "," && !inQuotes) { cells.push(current.trim()); current = ""; } else { current += char; } } cells.push(current.trim()); return cells; }; const headers = splitRow(lines[0]).map((h) => h.toLowerCase()); return lines.slice(1).map((line) => { const values = splitRow(line); return headers.reduce((obj, header, index) => { obj[header] = values[index] ?? ""; return obj; }, {}); }); }; const normalizeImportedTransaction = (row) => ({ id: uid(), date: row.date || todayString(), type: String(row.type || "income").toLowerCase() === "expense" ? "expense" : "income", amount: Number(row.amount || 0), category: row.category || "Uncategorized", account: row.account || "Unknown Account", customer: row.customer || row.vendor || "", invoiceNo: row.invoiceno || row.invoice || row.reference || "", note: row.note || row.remark || "", matched: String(row.matched || "false").toLowerCase() === "true", flagged: String(row.flagged || "false").toLowerCase() === "true", }); const seedData = { company: { name: "AI CFO 财务监督系统", owner: "LOON SIA", reminderChannels: ["WhatsApp", "Email"], reportTime: "18:00", }, transactions: [ { id: uid(), date: todayString(), type: "income", amount: 18500, category: "Sales", account: "Maybank", customer: "ABC Trading", invoiceNo: "INV-240301", note: "已收款", matched: true, flagged: false, }, { id: uid(), date: todayString(), type: "expense", amount: 4200, category: "Marketing", account: "Public Bank", customer: "", invoiceNo: "", note: "广告投放", matched: true, flagged: true, }, { id: uid(), date: todayString(), type: "income", amount: 7800, category: "Rental", account: "CIMB", customer: "Unit A 租户", invoiceNo: "RENT-MAR-A", note: "3月租金", matched: false, flagged: true, }, { id: uid(), date: todayString(), type: "expense", amount: 1300, category: "Utilities", account: "Maybank", customer: "", invoiceNo: "", note: "水电费", matched: true, flagged: false, }, ], alerts: [ { id: uid(), level: "高风险", title: "未匹配收款", description: "发现 1 笔 Rental 收入未对应 Invoice / Customer。", owner: "Invoice 负责人", dueDate: todayString(), status: "open", }, { id: uid(), level: "中风险", title: "支出分类异常", description: "Marketing 项下出现可疑行政支出,建议复核。", owner: "Checker", dueDate: todayString(), status: "open", }, { id: uid(), level: "提醒", title: "出租账未更新", description: "今日租金账本尚未确认完成。", owner: "出租账负责人", dueDate: todayString(), status: "open", }, ], tasks: [ { id: uid(), title: "核对昨日银行流水", owner: "Account Head", priority: "高", dueDate: todayString(), done: false, }, { id: uid(), title: "补 1 笔未匹配 Invoice", owner: "Invoice 负责人", priority: "高", dueDate: todayString(), done: false, }, { id: uid(), title: "确认出租房账本更新", owner: "出租账负责人", priority: "中", dueDate: todayString(), done: false, }, ], team: [ { id: uid(), name: "Account Head", duty: "看总表、批异常、控现金流", kpi: "异常关闭率 / 月结准时率", }, { id: uid(), name: "Invoice 负责人", duty: "开单、跟收款、补单据", kpi: "漏单率 / 回款跟进率", }, { id: uid(), name: "出租账负责人", duty: "租金、押金、水电与维修入账", kpi: "账目完整率 / 逾期追踪率", }, { id: uid(), name: "Checker", duty: "只检查高风险分类与异常", kpi: "误分类发现率 / 复核效率", }, ], checklist: [ { id: uid(), text: "核对昨日全部银行收支是否导入", done: false }, { id: uid(), text: "检查未匹配收款是否已补 Customer / Invoice", done: false }, { id: uid(), text: "检查大额支出分类是否正确", done: false }, { id: uid(), text: "确认出租房账目是否更新", done: false }, { id: uid(), text: "确认今日需催收客户名单", done: false }, { id: uid(), text: "确认异常事项是否已发提醒", done: false }, ], }; function useLocalState(key, initialValue) { const [state, setState] = useState(() => { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : initialValue; } catch { return initialValue; } }); useEffect(() => { localStorage.setItem(key, JSON.stringify(state)); }, [key, state]); return [state, setState]; } function Card({ title, icon: Icon, value, sub, accent }) { return (
{title}
{value}
{sub}
); } function SectionTitle({ title, text, action }) { return (

{title}

{text &&

{text}

}
{action}
); } function StatusPill({ children, tone = "slate" }) { const tones = { red: "bg-rose-50 text-rose-700 border-rose-200", amber: "bg-amber-50 text-amber-700 border-amber-200", green: "bg-emerald-50 text-emerald-700 border-emerald-200", slate: "bg-slate-100 text-slate-700 border-slate-200", blue: "bg-blue-50 text-blue-700 border-blue-200", }; return ( {children} ); } function exportJson(data) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `ai-cfo-backup-${todayString()}.json`; a.click(); URL.revokeObjectURL(url); } export default function AICFOWebAppDeployable() { const fileInputRef = useRef(null); const [db, setDb] = useLocalState("ai-cfo-v5-deployable", seedData); const [tab, setTab] = useState("dashboard"); const [query, setQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [newTx, setNewTx] = useState({ date: todayString(), type: "income", amount: "", category: "", account: "Maybank", customer: "", invoiceNo: "", note: "", }); const [newTask, setNewTask] = useState({ title: "", owner: "Account Head", priority: "中", dueDate: todayString(), }); const [newAlert, setNewAlert] = useState({ level: "提醒", title: "", description: "", owner: "Account Head", dueDate: todayString(), }); const [importMessage, setImportMessage] = useState(""); const income = useMemo( () => db.transactions.filter((t) => t.type === "income").reduce((s, t) => s + Number(t.amount || 0), 0), [db.transactions] ); const expense = useMemo( () => db.transactions.filter((t) => t.type === "expense").reduce((s, t) => s + Number(t.amount || 0), 0), [db.transactions] ); const unmatched = useMemo(() => db.transactions.filter((t) => !t.matched).length, [db.transactions]); const flagged = useMemo(() => db.transactions.filter((t) => t.flagged).length, [db.transactions]); const openAlerts = useMemo(() => db.alerts.filter((a) => a.status === "open").length, [db.alerts]); const doneTasks = useMemo(() => db.tasks.filter((t) => t.done).length, [db.tasks]); const todayChecklistDone = useMemo(() => db.checklist.filter((c) => c.done).length, [db.checklist]); const filteredTransactions = useMemo(() => { return db.transactions.filter((t) => { const matchesQuery = [t.category, t.account, t.customer, t.invoiceNo, t.note] .join(" ") .toLowerCase() .includes(query.toLowerCase()); const matchesType = typeFilter === "all" ? true : t.type === typeFilter; return matchesQuery && matchesType; }); }, [db.transactions, query, typeFilter]); const downloadImportTemplate = () => { const sample = [ ["date", "type", "amount", "category", "account", "customer", "invoiceNo", "note", "matched", "flagged"], [todayString(), "income", "18500", "Sales", "Maybank", "ABC Trading", "INV-240301", "已收款", "true", "false"], [todayString(), "expense", "4200", "Marketing", "Public Bank", "", "", "广告投放", "true", "true"], ] .map((row) => row.join(",")) .join(" "); const blob = new Blob([sample], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "payment-list-template.csv"; a.click(); URL.revokeObjectURL(url); }; const importTransactionsFromFile = async (file) => { try { const text = await file.text(); let rows = []; if (file.name.toLowerCase().endsWith(".json")) { const parsed = JSON.parse(text); rows = Array.isArray(parsed) ? parsed : parsed.transactions || []; } else { rows = parseCsv(text); } const normalized = rows.map(normalizeImportedTransaction).filter((row) => row.amount > 0); if (!normalized.length) { setImportMessage("导入失败:文件里没有可用的 payment list 数据。"); return; } setDb((prev) => ({ ...prev, transactions: [...normalized, ...prev.transactions] })); setImportMessage(`导入成功:已加入 ${normalized.length} 笔 payment list。`); } catch { setImportMessage("导入失败:请上传 CSV 或 JSON 格式,并按模板字段排列。"); } }; const addTransaction = () => { if (!newTx.amount || !newTx.category) return; const tx = { ...newTx, id: uid(), amount: Number(newTx.amount), matched: newTx.type === "expense", flagged: false, }; setDb((prev) => ({ ...prev, transactions: [tx, ...prev.transactions] })); setNewTx({ date: todayString(), type: "income", amount: "", category: "", account: "Maybank", customer: "", invoiceNo: "", note: "", }); }; const addTask = () => { if (!newTask.title) return; setDb((prev) => ({ ...prev, tasks: [{ id: uid(), done: false, ...newTask }, ...prev.tasks], })); setNewTask({ title: "", owner: "Account Head", priority: "中", dueDate: todayString() }); }; const addAlert = () => { if (!newAlert.title || !newAlert.description) return; setDb((prev) => ({ ...prev, alerts: [{ id: uid(), status: "open", ...newAlert }, ...prev.alerts], })); setNewAlert({ level: "提醒", title: "", description: "", owner: "Account Head", dueDate: todayString() }); }; const nav = [ { id: "dashboard", label: "总览", icon: Wallet }, { id: "transactions", label: "收支记录", icon: CircleDollarSign }, { id: "alerts", label: "异常中心", icon: AlertTriangle }, { id: "tasks", label: "任务追踪", icon: ClipboardList }, { id: "team", label: "岗位分工", icon: Users }, { id: "settings", label: "系统设置", icon: Settings }, ]; return (
可直接运用版后台

AI CFO 财务监督系统

这版已经不是展示页,而是能直接开始录入、检查、追踪异常、分派任务、保存数据的单页后台。

{ const file = e.target.files?.[0]; if (file) importTransactionsFromFile(file); e.target.value = ""; }} />
{importMessage && (
{importMessage}
)}
{tab === "dashboard" && (
已完成 {todayChecklistDone} / {db.checklist.length}} />
{db.checklist.map((item) => ( ))}
净现金流
{money(income - expense)}
已完成任务
{doneTasks} / {db.tasks.length}
提醒渠道
{db.company.reminderChannels.map((c) => ( {c} ))}
{db.alerts.map((alert) => (
{alert.level}
{alert.title}
{alert.status === "closed" ? "已关闭" : "处理中"}

{alert.description}

负责人:{alert.owner} 到期:{alert.dueDate}
))}
)} {tab === "transactions" && (
这版已支持直接导入 payment list。建议字段:date, type, amount, category, account, customer, invoiceNo, note, matched, flagged。
setNewTx({ ...newTx, date: e.target.value })} /> setNewTx({ ...newTx, amount: e.target.value })} /> setNewTx({ ...newTx, category: e.target.value })} /> setNewTx({ ...newTx, account: e.target.value })} /> setNewTx({ ...newTx, customer: e.target.value })} /> setNewTx({ ...newTx, invoiceNo: e.target.value })} />