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 (
);
}
function SectionTitle({ title, text, action }) {
return (
);
}
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 })} />
}
/>
{filteredTransactions.map((tx) => (
{tx.type === "income" ? "收入" : "支出"}
{!tx.matched && 未匹配}
{tx.flagged && 已标记}
{money(tx.amount)} · {tx.category}
{tx.date} · {tx.account} · {tx.customer || "无客户资料"} · {tx.invoiceNo || "无 Invoice"}
{tx.note &&
备注:{tx.note}
}
))}
)}
{tab === "alerts" && (
{db.alerts.map((alert) => (
{alert.title}
负责人:{alert.owner} · 到期:{alert.dueDate}
{alert.level}
{alert.status === "closed" ? "已关闭" : "处理中"}
{alert.description}
))}
)}
{tab === "tasks" && (
{db.tasks.map((task) => (
{task.title}
负责人:{task.owner} · 到期:{task.dueDate}
{task.priority}优先级
{task.done ? "已完成" : "待处理"}
))}
)}
{tab === "team" && (
| 岗位 |
职责 |
KPI |
{db.team.map((member) => (
| {member.name} |
{member.duty} |
{member.kpi} |
))}
{[
"第一阶段:先用这版录入数据、追踪异常、安排任务。",
"第二阶段:把 Google Sheets / Airtable 接进来做多人同步。",
"第三阶段:接 WhatsApp API 与 Email 自动日报。",
"第四阶段:接银行流水导入与异常规则引擎。",
].map((item) => (
{item}
))}
)}
{tab === "settings" && (
1. 建一个 React 或 Next.js 项目,把当前文件作为首页组件。
2. 直接部署到 Vercel / Netlify,先让你和团队内部使用。
3. 之后把 localStorage 改成 Supabase / Firebase,就能多人同步。
4. 再接 Make / n8n / WhatsApp API / Email API 做自动提醒和日报。
5. payment list 已支持 CSV / JSON 批量导入。你把资料发给我后,我可以继续帮你整理字段并放进系统初始数据。
)}
);
}
“OTG acc的服务超出了我们的预期,团队专业、效率高!”
“我们与OTG acc的合作让我们的财务管理变得轻松,他们的透明度让我们非常安心。”
“感谢OTG acc的团队,他们提供的财务报表帮助我们做出了正确的决策。”
我们定期发布行业动态分析报告,帮助企业把握市场趋势。请持续关注我们的博客,获取最新的财务和市场信息。
我们将于下个月举行年度客户反馈活动,期待与您分享我们的进展和未来规划。您的意见对我们至关重要,欢迎参与!
随着行业的发展,我们的财务报表服务也在不断创新。近期,我们引入了新的技术,提升了报表的准确性和易读性,为客户提供更优质的服务体验。
Cookie的使用
我们使用cookies来确保流畅的浏览体验。若继续,我们认为你接受使用cookies。
了解更多