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