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