234 lines
10 KiB
TypeScript
Executable File
234 lines
10 KiB
TypeScript
Executable File
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>
|
||
);
|
||
};
|