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

246 lines
10 KiB
TypeScript
Executable File
Raw 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, { 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>
);
};