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