Files
mkd/components/finance/AggregatedReportView.tsx

234 lines
10 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
import React from 'react';
import { Download, Printer } from 'lucide-react';
interface AggregatedReportViewProps {
data: {
periodStart: string;
periodEnd: string;
totalIncome: number;
totalExpenses: number;
totalBalance: number;
totalArea: number;
totalLivingArea: number;
totalNonLivingArea: number;
buildingsCount: number;
aggregatedExpenses: Record<string, number>;
title?: string;
subtitle?: string;
};
}
export const AggregatedReportView: React.FC<AggregatedReportViewProps> = ({ data }) => {
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount);
};
const formatNumber = (num: number) => {
return new Intl.NumberFormat('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(num);
};
const handleDownloadCSV = () => {
// TODO: Реализовать экспорт в CSV
alert('Экспорт в CSV будет реализован');
};
const handlePrint = () => {
window.print();
};
// Рассчитываем период в месяцах
const startDate = new Date(data.periodStart);
const endDate = new Date(data.periodEnd);
const monthsDiff = (endDate.getFullYear() - startDate.getFullYear()) * 12 +
(endDate.getMonth() - startDate.getMonth()) + 1;
// Группируем расходы по категориям
const expenseCategories: Record<string, { items: Array<{ name: string; amount: number }>; total: number }> = {};
for (const [key, amount] of Object.entries(data.aggregatedExpenses)) {
const parts = key.split(' > ');
const category = parts[0] || 'Прочие';
const item = parts[1] || key;
if (!expenseCategories[category]) {
expenseCategories[category] = { items: [], total: 0 };
}
expenseCategories[category].items.push({ name: item, amount });
expenseCategories[category].total += amount;
}
// Сортируем категории и элементы
const sortedCategories = Object.entries(expenseCategories)
.sort((a, b) => b[1].total - a[1].total)
.map(([category, data]) => ({
category,
items: data.items.sort((a, b) => b.amount - a.amount),
total: data.total
}));
const totalExpensesPerMonth = data.totalExpenses / monthsDiff;
const totalExpensesPerSqM = data.totalArea > 0 ? data.totalExpenses / data.totalArea : 0;
const totalExpensesPerSqMPerMonth = data.totalArea > 0 ? totalExpensesPerMonth / data.totalArea : 0;
return (
<div className="space-y-6 animate-fade-in print:space-y-4">
{/* Кнопки действий */}
<div className="flex justify-end gap-3 print:hidden">
<button
onClick={handleDownloadCSV}
className="bg-primary-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-primary-700 transition-colors"
>
<Download className="w-4 h-4" />
Скачать CSV
</button>
<button
onClick={handlePrint}
className="bg-slate-100 text-slate-700 px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-slate-200 transition-colors"
>
<Printer className="w-4 h-4" />
Печать
</button>
</div>
{/* Параметры */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 print:border-0 print:shadow-none">
<h3 className="text-lg font-bold text-slate-800 mb-4">Параметры:</h3>
<div className="space-y-3">
<div>
<p className="text-sm text-slate-600 mb-1">
Период: {new Date(data.periodStart).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(data.periodEnd).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' })}
</p>
{data.title && (
<p className="text-sm text-slate-600 mb-1">{data.title}</p>
)}
{data.subtitle && (
<p className="text-sm text-slate-600 mb-1">{data.subtitle}</p>
)}
<p className="text-sm text-slate-600 mb-1">
Количество домов: {data.buildingsCount}
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 pt-2 border-t border-slate-200">
{data.totalLivingArea > 0 && (
<div>
<p className="text-sm text-slate-600 mb-1">Ж (кв.м)</p>
<p className="font-bold text-slate-800">{formatNumber(data.totalLivingArea)}</p>
</div>
)}
{data.totalNonLivingArea > 0 && (
<div>
<p className="text-sm text-slate-600 mb-1">НЖ (кв.м) в т.ч.:</p>
<p className="font-bold text-slate-800">{formatNumber(data.totalNonLivingArea)}</p>
</div>
)}
{data.totalArea > 0 && (
<div>
<p className="text-sm text-slate-600 mb-1">Итого (кв.м)</p>
<p className="font-bold text-slate-800">{formatNumber(data.totalArea)}</p>
</div>
)}
</div>
</div>
</div>
{/* Сводка по доходам и расходам */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 print:border-0 print:shadow-none">
<h3 className="text-lg font-bold text-slate-800 mb-4">Сводка:</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-emerald-50 p-4 rounded-lg">
<p className="text-sm text-slate-600 mb-1">Доходы</p>
<p className="text-2xl font-bold text-emerald-600">{formatCurrency(data.totalIncome)}</p>
</div>
<div className="bg-red-50 p-4 rounded-lg">
<p className="text-sm text-slate-600 mb-1">Расходы</p>
<p className="text-2xl font-bold text-red-600">{formatCurrency(data.totalExpenses)}</p>
</div>
<div className={`p-4 rounded-lg ${data.totalBalance >= 0 ? 'bg-emerald-50' : 'bg-red-50'}`}>
<p className="text-sm text-slate-600 mb-1">Баланс</p>
<p className={`text-2xl font-bold ${data.totalBalance >= 0 ? 'text-emerald-600' : 'text-red-600'}`}>
{formatCurrency(data.totalBalance)}
</p>
</div>
</div>
</div>
{/* Детальная таблица расходов */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 print:border-0 print:shadow-none overflow-x-auto">
<h3 className="text-lg font-bold text-slate-800 mb-4">Детальная разбивка расходов:</h3>
<table className="w-full border-collapse">
<thead>
<tr className="border-b-2 border-slate-300">
<th className="text-left p-3 text-sm font-bold text-slate-700"> п/п</th>
<th className="text-left p-3 text-sm font-bold text-slate-700">Статья затрат</th>
<th className="text-right p-3 text-sm font-bold text-slate-700">В месяц</th>
<th className="text-right p-3 text-sm font-bold text-slate-700">Сумма</th>
<th className="text-right p-3 text-sm font-bold text-slate-700">руб/кв.м в месяц</th>
</tr>
</thead>
<tbody>
{sortedCategories.map((category, categoryIndex) => (
<React.Fragment key={categoryIndex}>
<tr className="bg-slate-50 border-b border-slate-200">
<td colSpan={5} className="p-3 font-bold text-slate-800">
{categoryIndex + 1}. {category.category}
</td>
</tr>
{category.items.map((item, itemIndex) => {
const perMonth = item.amount / monthsDiff;
const perSqMPerMonth = data.totalArea > 0 ? perMonth / data.totalArea : 0;
return (
<tr key={itemIndex} className="border-b border-slate-100 hover:bg-slate-50">
<td className="p-3 text-sm text-slate-600">{categoryIndex + 1}.{itemIndex + 1}</td>
<td className="p-3 text-sm text-slate-700">{item.name}</td>
<td className="p-3 text-sm text-right text-slate-700">{formatCurrency(perMonth)}</td>
<td className="p-3 text-sm text-right font-medium text-slate-800">{formatCurrency(item.amount)}</td>
<td className="p-3 text-sm text-right text-slate-600">{formatNumber(perSqMPerMonth)}</td>
</tr>
);
})}
<tr className="bg-slate-100 border-b-2 border-slate-300">
<td colSpan={2} className="p-3 font-bold text-slate-800">
Итого по {category.category}:
</td>
<td className="p-3 text-sm text-right font-bold text-slate-800">
{formatCurrency(category.total / monthsDiff)}
</td>
<td className="p-3 text-sm text-right font-bold text-slate-800">
{formatCurrency(category.total)}
</td>
<td className="p-3 text-sm text-right font-bold text-slate-800">
{formatNumber((category.total / monthsDiff) / (data.totalArea || 1))}
</td>
</tr>
</React.Fragment>
))}
<tr className="bg-primary-50 border-t-4 border-primary-600">
<td colSpan={2} className="p-4 font-bold text-lg text-slate-800">
ИТОГО:
</td>
<td className="p-4 text-right font-bold text-lg text-slate-800">
{formatCurrency(totalExpensesPerMonth)}
</td>
<td className="p-4 text-right font-bold text-lg text-slate-800">
{formatCurrency(data.totalExpenses)}
</td>
<td className="p-4 text-right font-bold text-lg text-slate-800">
{formatNumber(totalExpensesPerSqMPerMonth)}
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
};