588 lines
30 KiB
TypeScript
Executable File
588 lines
30 KiB
TypeScript
Executable File
|
||
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<Employee[]>(CACHE_EMPLOYEES, []);
|
||
|
||
export const DashboardNavigation: React.FC<Props> = ({ currentUser, onSelectBuilding, allowedPermissions }) => {
|
||
const [districts, setDistricts] = useState<District[]>(getCachedDistricts);
|
||
const [buildings, setBuildings] = useState<Building[]>(getCachedBuildings);
|
||
const [applications, setApplications] = useState<DomaApplication[]>([]);
|
||
const [employees, setEmployees] = useState<Employee[]>(getCachedEmployees);
|
||
const [loading, setLoading] = useState(() => {
|
||
const d = getCachedDistricts();
|
||
const b = getCachedBuildings();
|
||
return d.length === 0 && b.length === 0; // есть кеш — не показываем спиннер
|
||
});
|
||
|
||
const [selectedDistrict, setSelectedDistrict] = useState<District | null>(null);
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [newBuildingAddress, setNewBuildingAddress] = useState('');
|
||
const [isCreateBuildingOpen, setIsCreateBuildingOpen] = useState(false);
|
||
const [staffModalDistrict, setStaffModalDistrict] = useState<District | null>(null);
|
||
const [districtToDelete, setDistrictToDelete] = useState<District | null>(null);
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
const [showMoveBuildingsModal, setShowMoveBuildingsModal] = useState(false);
|
||
const [buildingToDelete, setBuildingToDelete] = useState<Building | null>(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 <div className="p-10 text-center animate-pulse font-bold text-slate-400 uppercase tracking-widest">Загрузка активов...</div>;
|
||
|
||
if (!hasObjectsAccess) {
|
||
return (
|
||
<div className="p-10 text-center">
|
||
<p className="text-slate-500 font-medium mb-2">Нет доступа к разделу «Участки»</p>
|
||
<p className="text-sm text-slate-400">Обратитесь к администратору для настройки прав доступа.</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 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 (
|
||
<>
|
||
<div className="animate-fade-in space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<button onClick={() => setSelectedDistrict(null)} className="p-2 -ml-2 rounded-full hover:bg-slate-100"><ArrowLeft className="w-5 h-5" /></button>
|
||
<div>
|
||
<h2 className="text-lg font-bold text-slate-800 leading-none">{selectedDistrict.name}</h2>
|
||
<p className="text-[10px] text-slate-400 font-bold uppercase mt-1">Список домов участка</p>
|
||
</div>
|
||
</div>
|
||
{canManage && (
|
||
<button
|
||
onClick={() => setIsCreateBuildingOpen(true)}
|
||
className="bg-primary-600 text-white px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-2 shadow-md active:scale-95 transition-transform"
|
||
>
|
||
<Plus className="w-4 h-4" /> Дом
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{isCreateBuildingOpen && selectedDistrict && (
|
||
<form
|
||
onSubmit={async (e) => {
|
||
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"
|
||
>
|
||
<div className="flex-1">
|
||
<label className="block text-[10px] font-black uppercase text-slate-400 mb-1">
|
||
Адрес дома
|
||
</label>
|
||
<input
|
||
value={newBuildingAddress}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setIsCreateBuildingOpen(false);
|
||
setNewBuildingAddress('');
|
||
}}
|
||
className="px-4 py-2 rounded-xl border border-slate-200 text-xs font-bold text-slate-500 bg-white hover:bg-slate-50"
|
||
>
|
||
Отмена
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="px-4 py-2 rounded-xl bg-primary-600 text-white text-xs font-bold hover:bg-primary-700 active:scale-95 transition-transform"
|
||
>
|
||
Сохранить
|
||
</button>
|
||
</div>
|
||
</form>
|
||
)}
|
||
<div className="relative">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||
<input type="text" placeholder="Поиск по адресу..." value={searchQuery} onChange={e => 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" />
|
||
</div>
|
||
<div className="grid grid-cols-1 gap-4">
|
||
{districtBuildings.map(b => (
|
||
<PerformanceCard
|
||
key={b.id} title={b.passport.address}
|
||
subtitle={`${b.passport.general.floors} эт. • ${b.passport.general.totalArea} м²`}
|
||
applications={applications.filter(a => 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}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{buildingToDelete && (
|
||
<DeleteConfirmModal
|
||
isOpen={!!buildingToDelete}
|
||
onClose={() => {
|
||
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 (
|
||
<>
|
||
<div className="animate-fade-in pb-20 space-y-6">
|
||
<div className="min-h-[280px] sm:min-h-[360px] md:min-h-[500px]">
|
||
<DistrictsSummary
|
||
aggregatedData={aggregatedData}
|
||
onSelectDistrict={setSelectedDistrict}
|
||
canManage={canManage}
|
||
onAddDistrict={async ({ name, managerName }) => {
|
||
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}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{staffModalDistrict && (
|
||
<DistrictStaffModal
|
||
district={staffModalDistrict}
|
||
employees={employees}
|
||
onClose={() => setStaffModalDistrict(null)}
|
||
/>
|
||
)}
|
||
|
||
{districtToDelete && showMoveBuildingsModal && (
|
||
<MoveBuildingsModal
|
||
isOpen={showMoveBuildingsModal}
|
||
onClose={() => {
|
||
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 && (
|
||
<DeleteConfirmModal
|
||
isOpen={!!districtToDelete}
|
||
onClose={() => {
|
||
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 && (
|
||
<DeleteConfirmModal
|
||
isOpen={!!buildingToDelete}
|
||
onClose={() => {
|
||
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}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
};
|