Files
mkd/components/finance/BuildingFinancialSummary.tsx

246 lines
10 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState, useEffect } from 'react';
import { Building, BuildingFinancialData } from '../../types';
import { TrendingUp, TrendingDown, DollarSign, Calendar, Filter, Download } from 'lucide-react';
interface BuildingFinancialSummaryProps {
buildingId: string;
building?: Building;
}
export const BuildingFinancialSummary: React.FC<BuildingFinancialSummaryProps> = ({
buildingId,
building
}) => {
const [financialData, setFinancialData] = useState<BuildingFinancialData[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
periodStart: '',
periodEnd: '',
periodType: 'month' as 'month' | 'quarter' | 'year'
});
useEffect(() => {
fetchFinancialData();
}, [buildingId, filters]);
const fetchFinancialData = async () => {
setLoading(true);
try {
const params = new URLSearchParams();
if (filters.periodStart) params.append('periodStart', filters.periodStart);
if (filters.periodEnd) params.append('periodEnd', filters.periodEnd);
if (filters.periodType) params.append('periodType', filters.periodType);
const response = await fetch(`/api/finance/building/${buildingId}?${params}`);
if (response.ok) {
const data = await response.json();
setFinancialData(data);
}
} catch (error) {
console.error('Ошибка загрузки финансовых данных:', error);
} finally {
setLoading(false);
}
};
const calculateTotals = () => {
return financialData.reduce((acc, item) => ({
totalIncome: acc.totalIncome + (item.totalIncome || 0),
totalExpenses: acc.totalExpenses + (item.totalExpenses || 0),
balance: acc.balance + (item.balance || 0)
}), { totalIncome: 0, totalExpenses: 0, balance: 0 });
};
const totals = calculateTotals();
if (loading) {
return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
</div>
);
}
return (
<div className="space-y-6 animate-fade-in">
{/* Заголовок */}
<div className="flex justify-between items-center">
<div>
<h3 className="text-xl font-bold text-slate-800">
Финансовая сводка
</h3>
{building && (
<p className="text-sm text-slate-500 mt-1">{building.passport.address}</p>
)}
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 transition-colors">
<Download className="w-4 h-4" />
Экспорт
</button>
</div>
{/* Фильтры */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-4">
<div className="flex items-center gap-2 mb-4">
<Filter className="w-4 h-4 text-slate-400" />
<span className="text-sm font-bold text-slate-700">Фильтры</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">
Период с
</label>
<input
type="date"
value={filters.periodStart}
onChange={(e) => setFilters({ ...filters, periodStart: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">
Период по
</label>
<input
type="date"
value={filters.periodEnd}
onChange={(e) => setFilters({ ...filters, periodEnd: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">
Тип периода
</label>
<select
value={filters.periodType}
onChange={(e) => setFilters({ ...filters, periodType: e.target.value as any })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="month">Месяц</option>
<option value="quarter">Квартал</option>
<option value="year">Год</option>
</select>
</div>
<div className="flex items-end">
<button
onClick={() => setFilters({ periodStart: '', periodEnd: '', periodType: 'month' })}
className="w-full px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-200 transition-colors"
>
Сбросить
</button>
</div>
</div>
</div>
{/* Итоговые показатели */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-xl">
<TrendingUp className="w-6 h-6" />
</div>
<span className="text-2xl font-black text-slate-800">
{totals.totalIncome.toLocaleString('ru-RU')}
</span>
</div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider">
Общий доход
</p>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-red-50 text-red-600 rounded-xl">
<TrendingDown className="w-6 h-6" />
</div>
<span className="text-2xl font-black text-slate-800">
{totals.totalExpenses.toLocaleString('ru-RU')}
</span>
</div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider">
Общие расходы
</p>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex items-center justify-between mb-4">
<div className={`p-3 rounded-xl ${
totals.balance >= 0 ? 'bg-primary-50 text-primary-600' : 'bg-red-50 text-red-600'
}`}>
<DollarSign className="w-6 h-6" />
</div>
<span className={`text-2xl font-black ${
totals.balance >= 0 ? 'text-primary-600' : 'text-red-600'
}`}>
{totals.balance.toLocaleString('ru-RU')}
</span>
</div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider">
Баланс
</p>
</div>
</div>
{/* Детальная таблица */}
{financialData.length > 0 ? (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-4 bg-slate-50 border-b border-slate-200">
<h4 className="font-bold text-slate-800 text-sm">Детальная разбивка по периодам</h4>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-bold text-slate-600 uppercase">Период</th>
<th className="px-4 py-3 text-right text-xs font-bold text-slate-600 uppercase">Доходы</th>
<th className="px-4 py-3 text-right text-xs font-bold text-slate-600 uppercase">Расходы</th>
<th className="px-4 py-3 text-right text-xs font-bold text-slate-600 uppercase">Баланс</th>
<th className="px-4 py-3 text-center text-xs font-bold text-slate-600 uppercase">Действия</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{financialData.map((item) => (
<tr key={item.id} className="hover:bg-slate-50">
<td className="px-4 py-3 text-sm text-slate-700">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-slate-400" />
{new Date(item.periodStart).toLocaleDateString('ru-RU')} - {new Date(item.periodEnd).toLocaleDateString('ru-RU')}
</div>
</td>
<td className="px-4 py-3 text-sm text-right font-medium text-emerald-600">
{item.totalIncome.toLocaleString('ru-RU')}
</td>
<td className="px-4 py-3 text-sm text-right font-medium text-red-600">
{item.totalExpenses.toLocaleString('ru-RU')}
</td>
<td className={`px-4 py-3 text-sm text-right font-bold ${
item.balance >= 0 ? 'text-primary-600' : 'text-red-600'
}`}>
{item.balance.toLocaleString('ru-RU')}
</td>
<td className="px-4 py-3 text-center">
<button className="text-primary-600 hover:text-primary-700 text-xs font-medium">
Детали
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<p className="text-slate-500">Нет данных за выбранный период</p>
<p className="text-sm text-slate-400 mt-2">
Загрузите отчеты из 1С для отображения финансовых данных
</p>
</div>
)}
</div>
);
};