Files
mkd/components/finance/ReportDetailView.tsx

860 lines
39 KiB
TypeScript
Raw Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState, useEffect } from 'react';
import { ArrowLeft, Building2, Grid, List, Home, MapPin, DollarSign, TrendingUp, TrendingDown, FileText, Loader2, CheckCircle2, Eye } from 'lucide-react';
import { FinancialReport } from '../../types';
import { apiClient, authFetch } from '../../services/apiClient';
import { AggregatedReportView } from './AggregatedReportView';
import { ResidentReportView } from '../pr/ResidentReportView';
interface Building {
id: string;
address: string;
district_id?: string;
district_name?: string;
total_income: number;
total_expenses: number;
balance: number;
reports_count: number;
}
interface ReportDetailViewProps {
report: FinancialReport;
onBack: () => void;
}
type ViewMode = 'all' | 'sections' | 'specific';
type DisplayMode = 'grid' | 'list';
export const ReportDetailView: React.FC<ReportDetailViewProps> = ({ report, onBack }) => {
const [buildings, setBuildings] = useState<Building[]>([]);
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState<ViewMode>('all');
const [displayMode, setDisplayMode] = useState<DisplayMode>('grid');
const [selectedBuilding, setSelectedBuilding] = useState<string | null>(null);
const [selectedDistrict, setSelectedDistrict] = useState<string | null>(null);
const [sections, setSections] = useState<Record<string, Building[]>>({});
const [residentReportIds, setResidentReportIds] = useState<Record<string, number>>({});
const [aggregatedData, setAggregatedData] = useState<any>(null);
const [districtAggregatedData, setDistrictAggregatedData] = useState<any>(null);
const [selectedBuildingReport, setSelectedBuildingReport] = useState<any>(null);
const [buildingAggregatedData, setBuildingAggregatedData] = useState<any>(null);
const [loadingAggregated, setLoadingAggregated] = useState(false);
const [selectedPeriodStart, setSelectedPeriodStart] = useState<string>('');
const [selectedPeriodEnd, setSelectedPeriodEnd] = useState<string>('');
const [buildingReportViewMode, setBuildingReportViewMode] = useState<'aggregated' | 'detailed'>('detailed');
const [report76Rows, setReport76Rows] = useState<any[]>([]);
const [loading76Rows, setLoading76Rows] = useState(false);
useEffect(() => {
if (report.reportType === 'balance_sheet_76') {
setLoading(true);
fetchReport76Rows();
} else {
fetchBuildings();
if (report.reportType === 'balance_sheet') {
fetchResidentReportIds();
}
}
}, [report.id]);
useEffect(() => {
if (viewMode === 'all' && report.reportType === 'balance_sheet') {
fetchAggregatedData();
} else {
setAggregatedData(null);
}
}, [viewMode, report.id, selectedPeriodStart, selectedPeriodEnd]);
useEffect(() => {
if (selectedDistrict && report.reportType === 'balance_sheet') {
fetchDistrictAggregatedData(selectedDistrict);
} else {
setDistrictAggregatedData(null);
}
}, [selectedDistrict, report.id, selectedPeriodStart, selectedPeriodEnd]);
useEffect(() => {
if (selectedBuilding && report.reportType === 'balance_sheet') {
if (buildingReportViewMode === 'aggregated') {
fetchBuildingAggregatedReport(selectedBuilding);
} else {
fetchBuildingReport(selectedBuilding);
}
} else {
setBuildingAggregatedData(null);
setSelectedBuildingReport(null);
}
}, [selectedBuilding, selectedPeriodStart, selectedPeriodEnd, buildingReportViewMode]);
const fetchResidentReportIds = async () => {
try {
// Получаем период из ведомости
const buildingsData = await authFetch(`/api/finance/buildings?reportId=${report.id}`).then(r => r.json());
if (!buildingsData || buildingsData.length === 0) return;
// Берем период из первого дома
const firstBuilding = buildingsData[0];
const periodStart = firstBuilding.period_start;
const periodEnd = firstBuilding.period_end;
const reportIds: Record<string, number> = {};
// Получаем все отчеты собственников
const allReports = await apiClient.get<any[]>(`/api/pr/reports`);
for (const building of buildingsData) {
// Ищем отчет для этого дома и периода
const matchingReport = allReports.find((r: any) =>
r.buildingId === building.id &&
r.periodStart === periodStart &&
r.periodEnd === periodEnd
);
if (matchingReport) {
reportIds[building.id] = matchingReport.id;
}
}
setResidentReportIds(reportIds);
} catch (err) {
console.warn('Не удалось загрузить ID отчетов собственников:', err);
}
};
useEffect(() => {
if (buildings.length > 0) {
// Группируем дома по участкам
const grouped: Record<string, Building[]> = {};
buildings.forEach(building => {
const section = building.district_name || building.district_id || 'Без участка';
if (!grouped[section]) {
grouped[section] = [];
}
grouped[section].push(building);
});
setSections(grouped);
}
}, [buildings]);
const fetchReport76Rows = async () => {
try {
setLoading76Rows(true);
const response = await authFetch(`/api/finance/reports/${report.id}/balance-sheet-76-rows`);
if (!response.ok) throw new Error('Не удалось загрузить строки');
const data = await response.json();
setReport76Rows(Array.isArray(data) ? data : []);
} catch (err: any) {
console.error('Ошибка загрузки строк ОСВ 76:', err);
setReport76Rows([]);
} finally {
setLoading76Rows(false);
setLoading(false);
}
};
const fetchBuildings = async () => {
try {
setLoading(true);
const response = await authFetch(`/api/finance/buildings?reportId=${report.id}`);
if (!response.ok) {
throw new Error('Не удалось загрузить дома');
}
const data = await response.json();
// Преобразуем snake_case из API в camelCase
const formattedBuildings: Building[] = data.map((b: any) => {
// Используем district_name, если он есть и не равен district_id, иначе используем district_id
const districtName = b.district_name && b.district_name !== b.district_id
? b.district_name
: (b.district_id || undefined);
// Логируем для отладки
if (b.district_id && !b.district_name) {
console.warn(`[ReportDetailView] Участок ${b.district_id} не найден в таблице districts`);
}
return {
id: b.id,
address: b.address,
district_id: b.district_id,
district_name: districtName,
total_income: parseFloat(b.total_income) || 0,
total_expenses: parseFloat(b.total_expenses) || 0,
balance: parseFloat(b.balance) || 0,
reports_count: parseInt(b.reports_count) || 0
};
});
setBuildings(formattedBuildings);
} catch (err: any) {
console.error('Ошибка загрузки домов:', err);
} finally {
setLoading(false);
}
};
const fetchAggregatedData = async () => {
try {
setLoadingAggregated(true);
const params = new URLSearchParams();
// Фильтруем по периоду только если период выбран
if (selectedPeriodStart) params.append('periodStart', selectedPeriodStart);
if (selectedPeriodEnd) params.append('periodEnd', selectedPeriodEnd);
const url = `/api/finance/reports/${report.id}/aggregated${params.toString() ? '?' + params.toString() : ''}`;
const response = await fetch(url);
if (!response.ok) {
// Если ошибка 404, пробуем загрузить без фильтра по периоду
if (response.status === 404 && (selectedPeriodStart || selectedPeriodEnd)) {
const fallbackUrl = `/api/finance/reports/${report.id}/aggregated`;
const fallbackResponse = await fetch(fallbackUrl);
if (fallbackResponse.ok) {
const data = await fallbackResponse.json();
// Инициализируем период из данных
if (data.periodStart && data.periodEnd) {
setSelectedPeriodStart(data.periodStart);
setSelectedPeriodEnd(data.periodEnd);
}
setAggregatedData({
...data,
title: 'Отчет по всем домам',
subtitle: `Всего домов: ${data.buildingsCount}`
});
return;
}
}
throw new Error('Не удалось загрузить агрегированные данные');
}
const data = await response.json();
// Инициализируем период из данных, если период еще не установлен
if (!selectedPeriodStart && !selectedPeriodEnd && data.periodStart && data.periodEnd) {
setSelectedPeriodStart(data.periodStart);
setSelectedPeriodEnd(data.periodEnd);
}
setAggregatedData({
...data,
title: 'Отчет по всем домам',
subtitle: `Всего домов: ${data.buildingsCount}`
});
} catch (err) {
console.error('Ошибка загрузки агрегированных данных:', err);
setAggregatedData(null);
} finally {
setLoadingAggregated(false);
}
};
const fetchDistrictAggregatedData = async (districtId: string) => {
try {
setLoadingAggregated(true);
const params = new URLSearchParams();
if (selectedPeriodStart) params.append('periodStart', selectedPeriodStart);
if (selectedPeriodEnd) params.append('periodEnd', selectedPeriodEnd);
const url = `/api/finance/reports/${report.id}/districts/${districtId}/aggregated${params.toString() ? '?' + params.toString() : ''}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Не удалось загрузить данные по участку');
const data = await response.json();
setDistrictAggregatedData({
...data,
title: `Отчет по участку: ${data.districtName}`,
subtitle: `Домов в участке: ${data.buildingsCount}`
});
} catch (err) {
console.error('Ошибка загрузки данных по участку:', err);
} finally {
setLoadingAggregated(false);
}
};
const fetchBuildingReport = async (buildingId: string) => {
try {
setLoadingAggregated(true);
// Сначала пробуем загрузить существующий отчет из resident_reports
const reportId = residentReportIds[buildingId];
if (reportId) {
try {
const response = await fetch(`/api/pr/reports/${reportId}`);
if (response.ok) {
const reportData = await response.json();
setSelectedBuildingReport(reportData);
return;
}
} catch (err) {
console.warn('Не удалось загрузить существующий отчет, генерируем на лету:', err);
}
}
// Если отчета нет или не удалось загрузить, генерируем на лету из данных ведомости
const params = new URLSearchParams();
if (selectedPeriodStart) params.append('periodStart', selectedPeriodStart);
if (selectedPeriodEnd) params.append('periodEnd', selectedPeriodEnd);
const url = `/api/finance/reports/${report.id}/buildings/${buildingId}/report${params.toString() ? '?' + params.toString() : ''}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('Не удалось загрузить или сгенерировать отчет');
}
const reportData = await response.json();
setSelectedBuildingReport(reportData);
} catch (err) {
console.error('Ошибка загрузки отчета по дому:', err);
setSelectedBuildingReport(null);
} finally {
setLoadingAggregated(false);
}
};
const fetchBuildingAggregatedReport = async (buildingId: string) => {
try {
setLoadingAggregated(true);
// Загружаем агрегированный отчет по дому
const params = new URLSearchParams();
if (selectedPeriodStart) params.append('periodStart', selectedPeriodStart);
if (selectedPeriodEnd) params.append('periodEnd', selectedPeriodEnd);
const url = `/api/finance/reports/${report.id}/buildings/${buildingId}/aggregated${params.toString() ? '?' + params.toString() : ''}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('Не удалось загрузить отчет по дому');
}
const reportData = await response.json();
setBuildingAggregatedData(reportData);
} catch (err) {
console.error('Ошибка загрузки агрегированного отчета по дому:', err);
setBuildingAggregatedData(null);
} finally {
setLoadingAggregated(false);
}
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(amount);
};
const renderBuildingCard = (building: Building, onClick?: () => void) => {
const CardContent = (
<div className={`bg-white rounded-xl border-2 ${
selectedBuilding === building.id
? 'border-primary-600 bg-primary-50'
: 'border-slate-200 hover:border-primary-400'
} p-5 transition-all ${onClick ? 'cursor-pointer hover:shadow-lg' : ''}`}>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2 flex-1 min-w-0">
<Building2 className="w-5 h-5 text-primary-600 flex-shrink-0" />
<h4 className="font-bold text-slate-800 line-clamp-2">{building.address}</h4>
</div>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 flex items-center gap-1">
<TrendingUp className="w-4 h-4 text-emerald-600" />
Доходы:
</span>
<span className="font-bold text-emerald-600">
{formatCurrency(building.total_income)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 flex items-center gap-1">
<TrendingDown className="w-4 h-4 text-red-600" />
Расходы:
</span>
<span className="font-bold text-red-600">
{formatCurrency(building.total_expenses)}
</span>
</div>
<div className="flex items-center justify-between text-sm pt-2 border-t border-slate-200">
<span className="text-slate-700 font-medium flex items-center gap-1">
<DollarSign className="w-4 h-4 text-primary-600" />
Баланс:
</span>
<span className={`font-bold text-lg ${
building.balance >= 0 ? 'text-emerald-600' : 'text-red-600'
}`}>
{formatCurrency(building.balance)}
</span>
</div>
{/* Дополнительная статистика */}
<div className="pt-2 border-t border-slate-100 space-y-1">
{building.reports_count > 0 && (
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Отчетов создано:</span>
<span className="font-medium text-slate-700">{building.reports_count}</span>
</div>
)}
{building.total_expenses > 0 && building.total_income > 0 && (
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Рентабельность:</span>
<span className={`font-medium ${
(building.total_income / building.total_expenses) >= 1 ? 'text-emerald-600' : 'text-red-600'
}`}>
{Math.round((building.total_income / building.total_expenses) * 100)}%
</span>
</div>
)}
</div>
</div>
{building.district_name && (
<div className="flex items-center gap-1 text-xs text-slate-500 mb-3">
<MapPin className="w-3 h-3" />
<span>Участок: {building.district_name}</span>
</div>
)}
{/* Кнопка просмотра отчета собственника (если есть) */}
{report.reportType === 'balance_sheet' && residentReportIds[building.id] && (
<button
onClick={(e) => {
e.stopPropagation();
// Открываем отчет в новой вкладке
const url = `/pr/reports/${residentReportIds[building.id]}`;
window.open(url, '_blank');
}}
className="mt-3 w-full bg-primary-600 text-white px-4 py-2 rounded-lg flex items-center justify-center gap-2 hover:bg-primary-700 transition-colors text-sm font-medium"
title="Открыть отчет собственникам МКД для этого дома"
>
<Eye className="w-4 h-4" />
Отчет собственникам
</button>
)}
</div>
);
if (onClick) {
return (
<div key={building.id} onClick={onClick}>
{CardContent}
</div>
);
}
return <div key={building.id}>{CardContent}</div>;
};
if (loading && report.reportType !== 'balance_sheet_76') {
return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto mb-4"></div>
<p className="text-slate-600">Загрузка данных...</p>
</div>
);
}
if (report.reportType === 'balance_sheet_76') {
if (loading76Rows) {
return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto mb-4"></div>
<p className="text-slate-600">Загрузка лицевых счетов...</p>
</div>
);
}
const formatNum = (n: number) => (n == null || Number.isNaN(n) ? '—' : Number(n).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
return (
<div className="space-y-6 animate-fade-in">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-6">
<button onClick={onBack} className="p-2 hover:bg-slate-100 rounded-lg transition-colors flex-shrink-0">
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div className="flex-1 min-w-0">
<h2 className="text-xl font-bold text-slate-800 mb-1 break-words">{report.filename}</h2>
<p className="text-sm text-slate-500">Лицевые счета: {report76Rows.length}</p>
</div>
</div>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-200">
<th className="text-left py-3 px-4 font-semibold text-slate-700">Лицевой счёт</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Сальдо на начало</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Обороты дебет</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Обороты кредит</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Сальдо на конец (дебет)</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Сальдо на конец (кредит)</th>
</tr>
</thead>
<tbody>
{report76Rows.map((row: any) => (
<tr key={row.id ?? row.rowIndex} className="border-b border-slate-100 hover:bg-slate-50">
<td className="py-2 px-4 text-slate-800">{row.accountLabel ?? '—'}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.saldoStartDebet)}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.turnoverDebet)}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.turnoverCredit)}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.saldoEndDebet)}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.saldoEndCredit)}</td>
</tr>
))}
</tbody>
</table>
</div>
{report76Rows.length === 0 && (
<div className="p-12 text-center text-slate-500">Нет данных по лицевым счетам</div>
)}
</div>
</div>
);
}
return (
<div className="space-y-6 animate-fade-in">
{/* Заголовок и навигация */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-6">
<button
onClick={onBack}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors flex-shrink-0"
>
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div className="flex-1 min-w-0">
<h2 className="text-xl font-bold text-slate-800 mb-1 break-words">{report.filename}</h2>
<p className="text-sm text-slate-500">
{buildings.length} {buildings.length === 1 ? 'дом' : buildings.length < 5 ? 'дома' : 'домов'}
</p>
</div>
</div>
{/* Выбор периода */}
{report.reportType === 'balance_sheet' && (
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<label className="text-sm font-medium text-slate-700 whitespace-nowrap">Период отчета:</label>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 flex-1">
<div className="flex items-center gap-2">
<label className="text-xs text-slate-600 whitespace-nowrap">С:</label>
<input
type="date"
value={selectedPeriodStart}
onChange={(e) => setSelectedPeriodStart(e.target.value)}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-slate-600 whitespace-nowrap">По:</label>
<input
type="date"
value={selectedPeriodEnd}
onChange={(e) => setSelectedPeriodEnd(e.target.value)}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
</div>
</div>
</div>
)}
{/* Кнопки навигации */}
<div className="grid grid-cols-3 gap-3 mb-4">
<button
onClick={() => {
setViewMode('all');
setSelectedBuilding(null);
}}
className={`p-4 rounded-xl border-2 transition-all ${
viewMode === 'all'
? 'border-primary-600 bg-primary-50 text-primary-700 font-bold'
: 'border-slate-200 hover:border-primary-300 text-slate-700'
}`}
>
<div className="flex flex-col items-center gap-2">
<Grid className="w-6 h-6" />
<span className="text-sm">Все дома</span>
</div>
</button>
<button
onClick={() => {
setViewMode('sections');
setSelectedBuilding(null);
}}
className={`p-4 rounded-xl border-2 transition-all ${
viewMode === 'sections'
? 'border-primary-600 bg-primary-50 text-primary-700 font-bold'
: 'border-slate-200 hover:border-primary-300 text-slate-700'
}`}
>
<div className="flex flex-col items-center gap-2">
<List className="w-6 h-6" />
<span className="text-sm">По участкам</span>
</div>
</button>
<button
onClick={() => {
setViewMode('specific');
setSelectedBuilding(null);
}}
className={`p-4 rounded-xl border-2 transition-all ${
viewMode === 'specific'
? 'border-primary-600 bg-primary-50 text-primary-700 font-bold'
: 'border-slate-200 hover:border-primary-300 text-slate-700'
}`}
>
<div className="flex flex-col items-center gap-2">
<Home className="w-6 h-6" />
<span className="text-sm">Конкретный дом</span>
</div>
</button>
</div>
{/* Переключатель вида отображения */}
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setDisplayMode('grid')}
className={`p-2 rounded-lg transition-all ${
displayMode === 'grid'
? 'bg-primary-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setDisplayMode('list')}
className={`p-2 rounded-lg transition-all ${
displayMode === 'list'
? 'bg-primary-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<List className="w-4 h-4" />
</button>
</div>
</div>
{/* Контент в зависимости от режима */}
{viewMode === 'all' && (
<>
{loadingAggregated ? (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<Loader2 className="w-8 h-8 animate-spin text-primary-600 mx-auto mb-4" />
<p className="text-slate-600">Загрузка агрегированного отчета...</p>
</div>
) : aggregatedData ? (
<AggregatedReportView data={aggregatedData} />
) : (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-12 text-center">
<p className="text-slate-600">Нет данных для агрегированного отчета</p>
</div>
)}
</>
)}
{viewMode === 'sections' && (
<>
{selectedDistrict ? (
<>
<button
onClick={() => {
setSelectedDistrict(null);
setDistrictAggregatedData(null);
}}
className="mb-4 flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium"
>
<ArrowLeft className="w-4 h-4" />
Назад к списку участков
</button>
{loadingAggregated ? (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<Loader2 className="w-8 h-8 animate-spin text-primary-600 mx-auto mb-4" />
<p className="text-slate-600">Загрузка отчета по участку...</p>
</div>
) : districtAggregatedData ? (
<AggregatedReportView data={districtAggregatedData} />
) : (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-12 text-center">
<p className="text-slate-600">Нет данных для отчета по участку</p>
</div>
)}
</>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(sections).map(([sectionName, sectionBuildings]) => {
const districtId = sectionBuildings[0]?.district_id;
const totalIncome = sectionBuildings.reduce((sum, b) => sum + b.total_income, 0);
const totalExpenses = sectionBuildings.reduce((sum, b) => sum + b.total_expenses, 0);
const totalBalance = sectionBuildings.reduce((sum, b) => sum + b.balance, 0);
return (
<div
key={sectionName}
onClick={() => districtId && setSelectedDistrict(districtId)}
className="bg-white rounded-2xl border-2 border-slate-200 hover:border-primary-400 p-6 cursor-pointer transition-all hover:shadow-lg"
>
<div className="flex items-center gap-2 mb-4">
<MapPin className="w-5 h-5 text-primary-600" />
<h3 className="text-lg font-bold text-slate-800">{sectionName}</h3>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600">Домов:</span>
<span className="font-bold text-slate-800">{sectionBuildings.length}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 flex items-center gap-1">
<TrendingUp className="w-4 h-4 text-emerald-600" />
Доходы:
</span>
<span className="font-bold text-emerald-600">{formatCurrency(totalIncome)}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 flex items-center gap-1">
<TrendingDown className="w-4 h-4 text-red-600" />
Расходы:
</span>
<span className="font-bold text-red-600">{formatCurrency(totalExpenses)}</span>
</div>
<div className="flex items-center justify-between text-sm pt-2 border-t border-slate-200">
<span className="text-slate-700 font-medium flex items-center gap-1">
<DollarSign className="w-4 h-4 text-primary-600" />
Баланс:
</span>
<span className={`font-bold text-lg ${
totalBalance >= 0 ? 'text-emerald-600' : 'text-red-600'
}`}>
{formatCurrency(totalBalance)}
</span>
</div>
</div>
<div className="mt-4 pt-4 border-t border-slate-200">
<p className="text-xs text-primary-600 font-medium text-center">
Нажмите для просмотра отчета
</p>
</div>
</div>
);
})}
</div>
)}
</>
)}
{viewMode === 'specific' && (
<>
{selectedBuilding && (buildingAggregatedData || selectedBuildingReport) ? (
<>
<div className="flex items-center justify-between mb-4">
<button
onClick={() => {
setSelectedBuilding(null);
setBuildingAggregatedData(null);
setSelectedBuildingReport(null);
}}
className="flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium"
>
<ArrowLeft className="w-4 h-4" />
Назад к списку домов
</button>
<div className="flex gap-2">
<button
onClick={() => setBuildingReportViewMode('detailed')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
buildingReportViewMode === 'detailed'
? 'bg-primary-600 text-white'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Детальный отчет
</button>
<button
onClick={() => setBuildingReportViewMode('aggregated')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
buildingReportViewMode === 'aggregated'
? 'bg-primary-600 text-white'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Агрегированный отчет
</button>
</div>
</div>
{loadingAggregated ? (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<Loader2 className="w-8 h-8 animate-spin text-primary-600 mx-auto mb-4" />
<p className="text-slate-600">Загрузка отчета по дому...</p>
</div>
) : buildingReportViewMode === 'aggregated' && buildingAggregatedData ? (
<AggregatedReportView data={buildingAggregatedData} />
) : buildingReportViewMode === 'detailed' && selectedBuildingReport?.content ? (
<ResidentReportView
content={selectedBuildingReport.content}
buildingAddress={selectedBuildingReport.address}
period={selectedBuildingReport.periodStart && selectedBuildingReport.periodEnd
? `${new Date(selectedBuildingReport.periodStart).toLocaleDateString('ru-RU')} - ${new Date(selectedBuildingReport.periodEnd).toLocaleDateString('ru-RU')}`
: selectedBuildingReport.month}
/>
) : (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-12 text-center">
<p className="text-slate-600">Отчет по дому не найден</p>
</div>
)}
</>
) : (
<div className="space-y-4">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<h3 className="text-lg font-bold text-slate-800 mb-4">Выберите дом</h3>
<div className={displayMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
: 'space-y-3'
}>
{buildings.map(building =>
renderBuildingCard(building, () => setSelectedBuilding(building.id))
)}
</div>
</div>
</div>
)}
</>
)}
{buildings.length === 0 && (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-12">
<div className="text-center mb-6">
<Building2 className="w-12 h-12 text-amber-400 mx-auto mb-4" />
<p className="text-slate-600 font-medium mb-2">Нет данных по домам для этого отчета</p>
<p className="text-sm text-slate-500">
Возможные причины:
</p>
</div>
<div className="bg-amber-50 rounded-lg p-4 space-y-2 text-sm text-slate-700">
<p> Адреса домов в ведомости не совпадают с адресами в базе данных</p>
<p> Дома еще не добавлены в систему</p>
<p> Ошибка при парсинге ведомости</p>
</div>
{report.errorLog && report.errorLog.notFoundAddresses && (
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-sm font-medium text-amber-800 mb-2">
Не найдено домов: {report.errorLog.notFoundAddresses.length}
</p>
<p className="text-xs text-amber-700 mb-3">
{report.errorLog.message || 'Данные по этим адресам не были загружены. Создайте дома вручную в системе, затем загрузите отчет повторно.'}
</p>
<div className="text-xs text-amber-700 space-y-1 max-h-40 overflow-y-auto">
{report.errorLog.notFoundAddresses.slice(0, 10).map((addr: string, idx: number) => (
<div key={idx}> {addr}</div>
))}
{report.errorLog.notFoundAddresses.length > 10 && (
<div className="text-amber-600 font-medium">
... и еще {report.errorLog.notFoundAddresses.length - 10} адресов
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
);
};