财务分类体系+统一账本+Excel导入+报表改造
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user