财务分类体系+统一账本+Excel导入+报表改造

This commit is contained in:
a273825743
2026-05-16 03:04:48 +08:00
parent a4048776ba
commit ac7b616d02
14 changed files with 1552 additions and 397 deletions
+85 -22
View File
@@ -868,31 +868,94 @@ const ProjectDetail: React.FC = () => {
</Card>
)
// 财务信息 Tab
const FinanceTab = () => {
const contractAmount = parseFloat(project?.contract_amount || '0')
const totalIncome = finances.filter(f => f.payment_type === 'income').reduce((sum, f) => sum + (f.amount || 0), 0)
const totalExpense = finances.filter(f => f.payment_type === 'expense').reduce((sum, f) => sum + (f.amount || 0), 0)
const grossProfit = totalIncome - totalExpense
const [profitData, setProfitData] = React.useState<any>(null);
const [profitLoading, setProfitLoading] = React.useState(false);
React.useEffect(() => {
if (!project?.id) return;
setProfitLoading(true);
apiClient.get(`/financial-records/project-profit/${project.id}`)
.then(res => { if (res.data.success) setProfitData(res.data.data); })
.catch(() => {})
.finally(() => setProfitLoading(false));
}, [project?.id]);
const contractAmount = parseFloat(project?.contract_amount || '0');
const totalIncome = profitData?.total_income || 0;
const totalExpense = profitData?.total_expense || 0;
const grossProfit = profitData?.gross_profit || 0;
const grossMargin = profitData?.gross_margin || 0;
const LEVEL2_LABELS: Record<string, string> = {
material: '材料采购', equipment: '设备采购', subcontract: '施工分包', labor: '人工工资',
travel: '差旅交通', accommodation: '食宿费用', freight: '运输物流', design: '勘测设计',
tools: '小型工具', client_relations: '客户/EDL关系', other_project: '其他项目支出',
contract_payment: '合同收款', deposit_refund: '质保金退回'
};
return (
<Card title="财务信息">
<Descriptions column={2} bordered>
<Descriptions.Item label="合同金额">
¥{contractAmount.toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="已收款">¥{totalIncome.toLocaleString()}</Descriptions.Item>
<Descriptions.Item label="预支总额">¥0</Descriptions.Item>
<Descriptions.Item label="报销总额">¥0</Descriptions.Item>
<Descriptions.Item label="分包付款">¥0</Descriptions.Item>
<Descriptions.Item label="支出合计">¥{totalExpense.toLocaleString()}</Descriptions.Item>
<Descriptions.Item label="毛利润" span={2}>
<span style={{ color: grossProfit >= 0 ? '#52c41a' : '#ff4d4f', fontWeight: 'bold' }}>
¥{grossProfit.toLocaleString()}
</span>
</Descriptions.Item>
</Descriptions>
</Card>
<Spin spinning={profitLoading}>
<Card title="财务信息">
<Descriptions column={2} bordered>
<Descriptions.Item label="合同金额">¥{contractAmount.toLocaleString()}</Descriptions.Item>
<Descriptions.Item label="已收款">
<span style={{ color: '#3f8600', fontWeight: 'bold' }}>¥{totalIncome.toLocaleString()}</span>
</Descriptions.Item>
<Descriptions.Item label="支出合计">
<span style={{ color: '#cf1322' }}>¥{totalExpense.toLocaleString()}</span>
</Descriptions.Item>
<Descriptions.Item label="毛利润">
<span style={{ color: grossProfit >= 0 ? '#52c41a' : '#ff4d4f', fontWeight: 'bold', fontSize: 16 }}>
¥{grossProfit.toLocaleString()}
</span>
</Descriptions.Item>
<Descriptions.Item label="毛利率" span={2}>
<span style={{ color: grossProfit >= 0 ? '#52c41a' : '#ff4d4f', fontWeight: 'bold' }}>
{grossMargin}%
</span>
</Descriptions.Item>
</Descriptions>
</Card>
{profitData?.expense_by_category?.length > 0 && (
<Card title="支出分类明细" size="small" style={{ marginTop: 16 }}>
<Table
dataSource={profitData.expense_by_category}
rowKey="category_level2"
pagination={false}
size="small"
columns={[
{ title: '分类', dataIndex: 'category_level2', render: (v: string) => LEVEL2_LABELS[v] || v },
{ title: '金额(¥)', dataIndex: 'total_amount', render: (v: number) => parseFloat(v).toLocaleString(), align: 'right' as const },
{ title: '笔数', dataIndex: 'count', align: 'center' as const },
{
title: '占比', render: (_: unknown, r: any) => {
const pct = totalExpense > 0 ? (parseFloat(r.total_amount) / totalExpense * 100).toFixed(1) : '0';
return `${pct}%`;
}
},
]}
/>
</Card>
)}
{profitData?.expense_by_user?.length > 0 && (
<Card title="人员支出明细" size="small" style={{ marginTop: 16 }}>
<Table
dataSource={profitData.expense_by_user}
rowKey="user_name"
pagination={false}
size="small"
columns={[
{ title: '人员', dataIndex: 'user_name' },
{ title: '金额(¥)', dataIndex: 'total_amount', render: (v: number) => parseFloat(v).toLocaleString(), align: 'right' as const },
{ title: '笔数', dataIndex: 'count', align: 'center' as const },
]}
/>
</Card>
)}
</Spin>
)
}