Files
mkd/components/finance/AggregatedReportView.tsx
2026-02-04 00:17:04 +05:00

234 lines
10 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};