import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { District, Building, User, DomaApplication, Employee } from '../types'; import { storageService } from '../services/storageService'; import { domaService } from '../services/domaService'; import { backendApi } from '../services/apiClient'; import { Plus, Search, ArrowLeft } from 'lucide-react'; import { PerformanceCard } from './objects/PerformanceCard'; // FIX: Removed incorrect self-imports of DistrictModal and BuildingModal which do not exist and were causing circular dependencies. import { DistrictsSummary } from './objects/DistrictsSummary'; import { DistrictStaffModal } from './objects/DistrictStaffModal'; import { DeleteConfirmModal } from './objects/DeleteConfirmModal'; import { MoveBuildingsModal } from './objects/MoveBuildingsModal'; import { REFRESH_EVENTS } from '../constants/refreshEvents'; import { readCache, saveCache } from '../hooks/useCachedFetch'; import { canAccessSub } from '../constants/permissions'; interface Props { currentUser: User; onSelectBuilding: (building: Building) => void; allowedPermissions?: string[] | null; } // Кеш из localStorage для мгновенного отображения (без ожидания API) const getCachedDistricts = () => storageService.getDistricts(); const getCachedBuildings = () => storageService.getAllBuildings(); const CACHE_EMPLOYEES = 'mkd_hr_employee_registry'; // общий кеш с EmployeeRegistry const getCachedEmployees = () => readCache(CACHE_EMPLOYEES, []); export const DashboardNavigation: React.FC = ({ currentUser, onSelectBuilding, allowedPermissions }) => { const [districts, setDistricts] = useState(getCachedDistricts); const [buildings, setBuildings] = useState(getCachedBuildings); const [applications, setApplications] = useState([]); const [employees, setEmployees] = useState(getCachedEmployees); const [loading, setLoading] = useState(() => { const d = getCachedDistricts(); const b = getCachedBuildings(); return d.length === 0 && b.length === 0; // есть кеш — не показываем спиннер }); const [selectedDistrict, setSelectedDistrict] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [newBuildingAddress, setNewBuildingAddress] = useState(''); const [isCreateBuildingOpen, setIsCreateBuildingOpen] = useState(false); const [staffModalDistrict, setStaffModalDistrict] = useState(null); const [districtToDelete, setDistrictToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [showMoveBuildingsModal, setShowMoveBuildingsModal] = useState(false); const [buildingToDelete, setBuildingToDelete] = useState(null); const [isDeletingBuilding, setIsDeletingBuilding] = useState(false); const canManage = ['DIRECTOR', 'ENGINEER'].includes(currentUser.role); // Участки и дома — сначала (разблокируют интерфейс), сотрудники — следом в фоне const fetchData = useCallback(async (showLoading = true) => { if (showLoading) setLoading(true); // Фаза 1: участки + дома (критично для отображения) const [districtsResult, buildingsResult] = await Promise.allSettled([ backendApi.getDistricts().catch(() => storageService.getDistricts()), backendApi.getBuildings().catch(() => storageService.getAllBuildings()), ]); const allDistricts = districtsResult.status === 'fulfilled' ? districtsResult.value : storageService.getDistricts(); const allBuildings = buildingsResult.status === 'fulfilled' ? buildingsResult.value : storageService.getAllBuildings(); if (districtsResult.status === 'rejected') { console.warn('[DashboardNavigation] Backend districts unavailable, using local storage/mocks:', districtsResult.reason); } if (buildingsResult.status === 'rejected') { console.warn('[DashboardNavigation] Backend buildings unavailable, using local storage/mocks:', buildingsResult.reason); } // Фильтрация участков по правам доступа и scope let filteredDistricts = allDistricts; // Проверка прав доступа к разделу objects const hasObjectsAccess = !allowedPermissions || allowedPermissions.length === 0 || allowedPermissions.includes('all') || canAccessSub(allowedPermissions, 'objects'); if (!hasObjectsAccess) { filteredDistricts = []; } else { // #region agent log console.log('[DashboardNavigation] Filter districts:', { role: currentUser.role, scope: currentUser.scope, assignedDistrictId: currentUser.assignedDistrictId, allDistrictsCount: allDistricts.length }); fetch('http://localhost:7243/ingest/a8a7528a-e6ae-43df-b788-a3a4c916ecb1',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'DashboardNavigation.tsx:filterDistricts',message:'before filter',data:{role:currentUser.role,scope:currentUser.scope,assignedDistrictId:currentUser.assignedDistrictId,allDistrictsCount:allDistricts.length},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A'})}).catch(()=>{}); // #endregion // Фильтрация по scope (own_district) — показываем все участки, на которые назначен сотрудник (может быть несколько) const userDistrictIds = (currentUser.assignedDistrictIds && currentUser.assignedDistrictIds.length > 0) ? currentUser.assignedDistrictIds : (currentUser.assignedDistrictId ? [currentUser.assignedDistrictId] : []); if (currentUser.scope === 'own_district' && userDistrictIds.length > 0) { filteredDistricts = filteredDistricts.filter((d: District) => userDistrictIds.includes(d.id)); // #region agent log console.log('[DashboardNavigation] Filtered by scope:', { filteredCount: filteredDistricts.length, assignedDistrictIds: userDistrictIds }); fetch('http://localhost:7243/ingest/a8a7528a-e6ae-43df-b788-a3a4c916ecb1',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'DashboardNavigation.tsx:filterDistricts',message:'filtered by scope',data:{filteredCount:filteredDistricts.length,assignedDistrictIds:userDistrictIds},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'B'})}).catch(()=>{}); // #endregion } // Фильтрация по роли (MASTER видит только свои участки) — fallback: все участки из назначений или по старой логике один else if (currentUser.role === 'MASTER' && userDistrictIds.length > 0) { filteredDistricts = filteredDistricts.filter((d: District) => userDistrictIds.includes(d.id)); // #region agent log console.log('[DashboardNavigation] Filtered by MASTER role:', { filteredCount: filteredDistricts.length }); fetch('http://localhost:7243/ingest/a8a7528a-e6ae-43df-b788-a3a4c916ecb1',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'DashboardNavigation.tsx:filterDistricts',message:'filtered by MASTER role',data:{filteredCount:filteredDistricts.length},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{}); // #endregion } // #region agent log console.log('[DashboardNavigation] Final districts:', { finalCount: filteredDistricts.length, districtIds: filteredDistricts.map(d => d.id) }); fetch('http://localhost:7243/ingest/a8a7528a-e6ae-43df-b788-a3a4c916ecb1',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'DashboardNavigation.tsx:filterDistricts',message:'after filter',data:{finalCount:filteredDistricts.length,districtIds:filteredDistricts.map(d=>d.id)},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'D'})}).catch(()=>{}); // #endregion } setDistricts(filteredDistricts); setBuildings(allBuildings); setLoading(false); // Кешируем для мгновенного отображения при следующем открытии try { localStorage.setItem('mkd_districts_v2', JSON.stringify(allDistricts)); localStorage.setItem('mkd_buildings_v2', JSON.stringify(allBuildings)); } catch (_) {} // Фаза 2: сотрудники в фоне (для staffCount и модалки штата) backendApi.getEmployees().catch(() => getCachedEmployees()).then(allEmployees => { setEmployees(allEmployees); saveCache(CACHE_EMPLOYEES, allEmployees); }); }, [currentUser]); // Заявки из Doma AI — только по таймеру (не блокируют загрузку участков/домов) const fetchApplications = useCallback(async () => { try { const apps = await domaService.getApplications(); setApplications(apps); } catch (e) { console.warn('[DashboardNavigation] Ошибка загрузки заявок Doma AI:', e); setApplications([]); } }, []); // Первичная загрузка: при наличии кеша — без спиннера, иначе — показываем загрузку useEffect(() => { const hasCache = getCachedDistricts().length > 0 || getCachedBuildings().length > 0; fetchData(!hasCache); }, [fetchData]); // Фоновое обновление участков/домов каждые 10 секунд useEffect(() => { const interval = setInterval(() => fetchData(false), 10 * 1000); return () => clearInterval(interval); }, [fetchData]); // Заявки Doma AI: первый запуск через 2 сек, далее каждые 10 секунд по таймеру useEffect(() => { const t = setTimeout(fetchApplications, 2000); const interval = setInterval(fetchApplications, 10 * 1000); return () => { clearTimeout(t); clearInterval(interval); }; }, [fetchApplications]); useEffect(() => { const onRefresh = () => fetchData(false); window.addEventListener(REFRESH_EVENTS.dashboard, onRefresh); return () => window.removeEventListener(REFRESH_EVENTS.dashboard, onRefresh); }, [fetchData]); // Штат: мгновенное обновление при изменении сотрудников (create/update в HR) useEffect(() => { const onRefresh = () => { backendApi.getEmployees().catch(() => getCachedEmployees()).then(allEmployees => { setEmployees(allEmployees); saveCache(CACHE_EMPLOYEES, allEmployees); }); }; window.addEventListener(REFRESH_EVENTS.employees, onRefresh); return () => window.removeEventListener(REFRESH_EVENTS.employees, onRefresh); }, []); const aggregatedData = useMemo(() => { if (loading) return {}; const districtStats: any = {}; districts.forEach(d => { districtStats[d.id] = { district: d, buildings: [], applications: [] }; }); buildings.forEach(b => { if (districtStats[b.districtId]) districtStats[b.districtId].buildings.push(b); }); applications.forEach(app => { const b = buildings.find(build => build.passport.address === app.address); if (b && districtStats[b.districtId]) districtStats[b.districtId].applications.push(app); }); return districtStats; }, [districts, buildings, applications, loading]); // Проверка прав доступа к разделу objects const hasObjectsAccess = !allowedPermissions || allowedPermissions.length === 0 || allowedPermissions.includes('all') || canAccessSub(allowedPermissions, 'objects'); if (loading) return
Загрузка активов...
; if (!hasObjectsAccess) { return (

Нет доступа к разделу «Участки»

Обратитесь к администратору для настройки прав доступа.

); } // VIEW: IF SINGLE DISTRICT SELECTED (Building list within district) if (selectedDistrict) { const districtBuildings = buildings.filter(b => b.districtId === selectedDistrict.id && b.passport.address.toLowerCase().includes(searchQuery.toLowerCase())); // Debug: log buildingToDelete state if (buildingToDelete) { console.log('buildingToDelete is set in render:', buildingToDelete.id, buildingToDelete.passport.address); } return ( <>

{selectedDistrict.name}

Список домов участка

{canManage && ( )}
{isCreateBuildingOpen && selectedDistrict && (
{ e.preventDefault(); if (!newBuildingAddress.trim()) return; try { // создаём полный объект Building локально const createdLocal = storageService.createBuilding({ address: newBuildingAddress.trim(), districtId: selectedDistrict.id, }); setBuildings((prev) => [...prev, createdLocal]); // пытаемся записать в БД try { await backendApi.createBuilding(createdLocal); window.dispatchEvent(new CustomEvent(REFRESH_EVENTS.dashboard)); await fetchData(); } catch (err) { console.error('Failed to persist building to backend, local only:', err); } } finally { setIsCreateBuildingOpen(false); setNewBuildingAddress(''); } }} className="bg-white border border-slate-200 rounded-2xl p-4 flex flex-col md:flex-row gap-3 items-stretch md:items-end shadow-sm" >
setNewBuildingAddress(e.target.value)} placeholder="Например: ул. Новая, д.10" className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm focus:ring-2 focus:ring-primary-500 outline-none" />
)}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm" />
{districtBuildings.map(b => ( a.address === b.passport.address)} onClick={() => onSelectBuilding(b)} type="building" onDelete={canManage ? () => { console.log('Delete button clicked for building:', b.id, b.passport.address); console.log('Setting buildingToDelete state...'); setBuildingToDelete(b); console.log('buildingToDelete state should be set now'); } : undefined} /> ))}
{buildingToDelete && ( { console.log('Delete modal onClose called (district view), isDeletingBuilding:', isDeletingBuilding); if (!isDeletingBuilding) { setBuildingToDelete(null); } }} onConfirm={async () => { if (!buildingToDelete) { console.error('buildingToDelete is null'); return; } console.log('Confirming deletion of building:', buildingToDelete.id, buildingToDelete.passport.address); setIsDeletingBuilding(true); try { // Пытаемся удалить на сервере try { console.log('Attempting to delete building on server:', buildingToDelete.id); await backendApi.deleteBuilding(buildingToDelete.id); window.dispatchEvent(new CustomEvent(REFRESH_EVENTS.dashboard)); console.log('Building deleted on server successfully'); } catch (serverError: any) { // Если сервер недоступен, продолжаем с локальным удалением console.warn('Server deletion failed, deleting locally:', serverError); } // Удаляем из localStorage console.log('Deleting building from localStorage:', buildingToDelete.id); storageService.deleteBuilding(buildingToDelete.id); // Удаляем из состояния console.log('Removing building from state:', buildingToDelete.id); setBuildings(prev => { const filtered = prev.filter(b => b.id !== buildingToDelete.id); console.log('Buildings after filter:', filtered.length, 'of', prev.length); return filtered; }); // Закрываем модальное окно console.log('Closing delete modal'); setBuildingToDelete(null); // Обновляем список домов из БД (если сервер доступен) try { console.log('Refreshing data from server'); await fetchData(); } catch (fetchError) { console.warn('Failed to refresh data after deletion:', fetchError); } } catch (error: any) { console.error('Failed to delete building:', error); // Показываем ошибку пользователю let errorMessage = 'Ошибка при удалении дома'; if (error?.message) { errorMessage = error.message; } else if (error?.error) { errorMessage = error.error; } else if (typeof error === 'string') { errorMessage = error; } alert(errorMessage); } finally { setIsDeletingBuilding(false); } }} title="Удаление дома" message="Вы уверены, что хотите удалить этот дом?" itemName={buildingToDelete.passport.address} isLoading={isDeletingBuilding} /> )} ); } return ( <>
{ try { const created = await backendApi.createDistrict({ name, managerName }); window.dispatchEvent(new CustomEvent(REFRESH_EVENTS.dashboard)); await fetchData(); } catch (error) { console.error('Failed to create district via backend, falling back to local storage:', error); const createdLocal = storageService.createDistrict({ name, managerName }); setDistricts((prev) => [...prev, createdLocal]); } }} onDeleteDistrict={async (d) => { // Проверяем, есть ли дома, привязанные к этому участку const districtBuildings = buildings.filter(b => b.districtId === d.id); if (districtBuildings.length > 0) { // Показываем модальное окно перемещения домов setDistrictToDelete(d); setShowMoveBuildingsModal(true); } else { // Если домов нет, сразу показываем окно подтверждения удаления setDistrictToDelete(d); } }} onViewStaff={(d) => setStaffModalDistrict(d)} onDistrictUpdated={() => fetchData()} role={currentUser.role} employees={employees} />
{staffModalDistrict && ( setStaffModalDistrict(null)} /> )} {districtToDelete && showMoveBuildingsModal && ( { setShowMoveBuildingsModal(false); setDistrictToDelete(null); }} onComplete={async () => { // После перемещения домов обновляем данные await fetchData(); setShowMoveBuildingsModal(false); // Автоматически показываем окно подтверждения удаления // (districtToDelete уже установлен) }} sourceDistrict={districtToDelete} buildings={buildings.filter(b => b.districtId === districtToDelete.id)} allDistricts={districts} /> )} {districtToDelete && !showMoveBuildingsModal && ( { if (!isDeleting) { setDistrictToDelete(null); } }} onConfirm={async () => { if (!districtToDelete) return; setIsDeleting(true); try { await backendApi.deleteDistrict(districtToDelete.id); window.dispatchEvent(new CustomEvent(REFRESH_EVENTS.dashboard)); storageService.deleteDistrict(districtToDelete.id); // Обновляем список участков из БД await fetchData(); setDistrictToDelete(null); } catch (error: any) { console.error('Failed to delete district:', error); // Показываем ошибку пользователю let errorMessage = 'Ошибка при удалении участка'; if (error?.message) { errorMessage = error.message; } else if (error?.error) { errorMessage = error.error; } else if (typeof error === 'string') { errorMessage = error; } alert(errorMessage); } finally { setIsDeleting(false); } }} title="Удаление участка" message="Вы уверены, что хотите удалить этот участок?" itemName={districtToDelete.name} isLoading={isDeleting} /> )} {buildingToDelete && ( { console.log('Delete modal onClose called, isDeletingBuilding:', isDeletingBuilding); if (!isDeletingBuilding) { setBuildingToDelete(null); } }} onConfirm={async () => { if (!buildingToDelete) { console.error('buildingToDelete is null'); return; } console.log('Confirming deletion of building:', buildingToDelete.id, buildingToDelete.passport.address); setIsDeletingBuilding(true); try { // Пытаемся удалить на сервере try { console.log('Attempting to delete building on server:', buildingToDelete.id); await backendApi.deleteBuilding(buildingToDelete.id); console.log('Building deleted on server successfully'); } catch (serverError: any) { // Если сервер недоступен, продолжаем с локальным удалением console.warn('Server deletion failed, deleting locally:', serverError); } // Удаляем из localStorage console.log('Deleting building from localStorage:', buildingToDelete.id); storageService.deleteBuilding(buildingToDelete.id); // Удаляем из состояния console.log('Removing building from state:', buildingToDelete.id); setBuildings(prev => { const filtered = prev.filter(b => b.id !== buildingToDelete.id); console.log('Buildings after filter:', filtered.length, 'of', prev.length); return filtered; }); // Закрываем модальное окно console.log('Closing delete modal'); setBuildingToDelete(null); // Обновляем список домов из БД (если сервер доступен) try { console.log('Refreshing data from server'); await fetchData(); } catch (fetchError) { console.warn('Failed to refresh data after deletion:', fetchError); } } catch (error: any) { console.error('Failed to delete building:', error); // Показываем ошибку пользователю let errorMessage = 'Ошибка при удалении дома'; if (error?.message) { errorMessage = error.message; } else if (error?.error) { errorMessage = error.error; } else if (typeof error === 'string') { errorMessage = error; } alert(errorMessage); } finally { setIsDeletingBuilding(false); } }} title="Удаление дома" message="Вы уверены, что хотите удалить этот дом?" itemName={buildingToDelete.passport.address} isLoading={isDeletingBuilding} /> )} ); };