import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Building, District } from '../types'; import { ArrowLeft, Pencil, Save, X, LayoutDashboard, Gauge, CalendarDays, Package, Camera, Banknote, Users, FileText, MapPin } from 'lucide-react'; import { storageService } from '../services/storageService'; import { backendApi } from '../services/apiClient'; import { readCache, saveCache } from '../hooks/useCachedFetch'; import { REFRESH_EVENTS } from '../constants/refreshEvents'; // Modular Sheet Imports import { Overview } from './building/Overview'; import { MeterCheck } from './building/MeterCheck'; import { WorkPlan } from './building/WorkPlan'; import { Supply } from './building/Supply'; import { Inspections } from './building/Inspections'; import { Finance } from './building/Finance'; import { Accounts } from './building/Accounts'; import { Passport } from './building/Passport'; interface Props { building: Building; onBack: () => void; onBuildingUpdate?: (building: Building) => void; } type TabType = 'overview' | 'meterCheck' | 'workPlan' | 'supply' | 'inspections' | 'finance' | 'accounts' | 'passport'; const TABS: { id: TabType; label: string; icon: any }[] = [ { id: 'overview', label: 'Сводка', icon: LayoutDashboard }, { id: 'meterCheck', label: 'Сверка ПУ', icon: Gauge }, { id: 'workPlan', label: 'План работ', icon: CalendarDays }, { id: 'supply', label: 'Снабжение', icon: Package }, { id: 'inspections', label: 'Осмотры', icon: Camera }, { id: 'finance', label: 'Финансы', icon: Banknote }, { id: 'accounts', label: 'Счета', icon: Users }, { id: 'passport', label: 'Паспорт', icon: FileText }, ]; const BUILDING_TABS: TabType[] = ['overview', 'meterCheck', 'workPlan', 'supply', 'inspections', 'finance', 'accounts', 'passport']; const SUBTAB_KEY = 'mkd_subTab_building'; export const BuildingCharacteristics: React.FC = ({ building: initialBuilding, onBack, onBuildingUpdate }) => { const [building, setBuildingState] = useState(initialBuilding); const setBuilding = useCallback((next: Building | ((prev: Building) => Building)) => { setBuildingState(prev => { const nextBuilding = typeof next === 'function' ? next(prev) : next; onBuildingUpdate?.(nextBuilding); return nextBuilding; }); }, [onBuildingUpdate]); const [originalBuilding, setOriginalBuilding] = useState(initialBuilding); const [activeTab, setActiveTab] = useState(() => { const s = localStorage.getItem(SUBTAB_KEY); return (s && BUILDING_TABS.includes(s as TabType)) ? s as TabType : 'overview'; }); useEffect(() => { localStorage.setItem(SUBTAB_KEY, activeTab); }, [activeTab]); const [isEditing, setIsEditing] = useState(false); const [districts, setDistricts] = useState([]); // Синхронизируем состояние только при смене дома (по id), чтобы избежать бесконечного цикла // при перерисовке родителя с новым объектом initialBuilding. // Кеш лицевых счетов: при открытии дома показываем из кеша для мгновенного отображения. useEffect(() => { const buildingId = initialBuilding.id; const globalFields = storageService.getGlobalPassportFields(); const fromStorage = storageService.getBuildingById(buildingId); const cachedBuilding = readCache(`mkd_building_${buildingId}`, null); let updatedBuilding: Building = { ...initialBuilding, tasks: Array.isArray(fromStorage?.tasks) ? fromStorage.tasks : (initialBuilding.tasks || []), annualPlan: Array.isArray(fromStorage?.annualPlan) ? fromStorage.annualPlan : (initialBuilding.annualPlan || []), }; if (cachedBuilding?.accounts?.length) { updatedBuilding.accounts = cachedBuilding.accounts; } if (!updatedBuilding.passport.odpu) { updatedBuilding.passport.odpu = { customFields: {} }; } (['general', 'construction', 'engineering', 'odpu', 'land', 'management'] as const).forEach(section => { if (globalFields[section]) { const sectionData = updatedBuilding.passport[section] as any; if (!sectionData) { updatedBuilding.passport[section] = { customFields: {} } as any; return; } const customFields = { ...(sectionData.customFields || {}) }; Object.keys(globalFields[section]).forEach(fieldName => { if (!customFields[fieldName]) { customFields[fieldName] = { ...globalFields[section][fieldName] }; } else { customFields[fieldName] = { ...customFields[fieldName], type: globalFields[section][fieldName].type, files: customFields[fieldName].files || [] }; } }); updatedBuilding.passport[section] = { ...sectionData, customFields } as any; } }); setBuildingState(updatedBuilding); setOriginalBuilding(updatedBuilding); // Зависимость только по id — эффект не дергается при каждой перерисовке родителя }, [initialBuilding.id]); // Загружаем список участков для выбора useEffect(() => { const loadDistricts = async () => { try { const allDistricts = await backendApi.getDistricts(); setDistricts(allDistricts); } catch (error) { console.warn('Failed to load districts, using local storage:', error); setDistricts(storageService.getDistricts()); } }; loadDistricts(); }, []); // Реестр лицевых счетов: кеш, polling 30 сек, dispatch при изменении const buildingId = building.id; const fetchBuilding = useCallback(async () => { try { const fresh = await backendApi.getBuilding(buildingId); const fromStorage = storageService.getBuildingById(buildingId); const merged: Building = { ...fresh, tasks: Array.isArray(fromStorage?.tasks) ? fromStorage.tasks : (fresh.tasks || []), annualPlan: Array.isArray(fromStorage?.annualPlan) ? fromStorage.annualPlan : (fresh.annualPlan || []), }; setBuilding(merged); saveCache(`mkd_building_${buildingId}`, merged); } catch (e) { console.warn('[BuildingCharacteristics] Failed to refresh building:', e); } }, [buildingId, setBuilding]); useEffect(() => { const onRefresh = () => fetchBuilding(); window.addEventListener(REFRESH_EVENTS.buildingAccounts, onRefresh); return () => window.removeEventListener(REFRESH_EVENTS.buildingAccounts, onRefresh); }, [fetchBuilding]); useEffect(() => { const interval = setInterval(() => fetchBuilding(), 10 * 1000); return () => clearInterval(interval); }, [fetchBuilding]); // Автосохранение при изменениях (план работ, осмотры и т.д.) — в storage и в БД const skipInitialPersist = useRef(true); useEffect(() => { if (skipInitialPersist.current) { skipInitialPersist.current = false; return; } const t = setTimeout(() => { const toSave = { ...building, isDirty: true }; storageService.saveBuildingData(toSave); backendApi.updateBuilding(toSave).catch((err) => { console.warn('[BuildingCharacteristics] Auto-save to backend failed:', err); }); }, 1000); return () => clearTimeout(t); }, [building]); const handleSave = async () => { const newBuilding = { ...building, isDirty: true }; setBuilding(newBuilding); setOriginalBuilding(newBuilding); storageService.saveBuildingData(newBuilding); setIsEditing(false); // Дополнительно отправляем в БД (паспорт и прочие изменения) try { await backendApi.updateBuilding(newBuilding); } catch (error) { console.error('[BuildingCharacteristics] Failed to sync building to backend:', error); // UI продолжит работать, данные останутся хотя бы в localStorage } }; const handleCancel = () => { setBuilding(originalBuilding); setIsEditing(false); }; return (
{/* Header Panel */}
{isEditing ? (
setBuilding({...building, passport: {...building.passport, address: e.target.value}})} className="text-lg font-black text-slate-900 w-full bg-white border border-primary-300 rounded px-2 py-1 outline-none focus:ring-2 focus:ring-primary-500/20" />
) : ( <>

{building.passport.address}

{districts.find(d => d.id === building.districtId)?.name || 'Участок не указан'}
)}
{isEditing ? 'Правка данных' : 'Режим просмотра'} {TABS.find(t => t.id === activeTab)?.label}
{isEditing ? ( <> ) : ( )}
{/* Sheet Selector (Excel-style bottom tabs look) */}
{TABS.map((tab) => ( ))}
{/* Dynamic Content Area */}
{activeTab === 'overview' && } {activeTab === 'meterCheck' && } {activeTab === 'workPlan' && } {activeTab === 'supply' && } {activeTab === 'inspections' && } {activeTab === 'finance' && } {activeTab === 'accounts' && } {activeTab === 'passport' && }
); };