import React, { useCallback, useEffect, useState } from 'react'; import { backendApi, PortalUserRow } from '../../services/apiClient'; import { Building, District, Employee } from '../../types'; import { Trash2, Loader2, AlertTriangle } from 'lucide-react'; type EntityType = | 'buildings' | 'employees' | 'districts' | 'portal-users' | 'permission-templates' | 'vacancies' | 'candidates' | 'training-programs' | 'expense-categories' | 'expense-items' | 'supply-requests' | 'office-inventory' | 'office-documents' | 'hr-template-documents' | 'doma-address-mappings' | 'doma-employee-mappings' | 'pr-work-photos' | 'pr-events' | 'accounts'; const ENTITY_LABELS: Record = { buildings: 'Дома', employees: 'Сотрудники', districts: 'Участки', 'portal-users': 'Пользователи портала', 'permission-templates': 'Шаблоны прав', vacancies: 'Вакансии', candidates: 'Кандидаты', 'training-programs': 'Программы обучения', 'expense-categories': 'Категории расходов', 'expense-items': 'Статьи расходов', 'supply-requests': 'Заявки на ТМЦ', 'office-inventory': 'Склад (офис)', 'office-documents': 'Документы (офис)', 'hr-template-documents': 'Типовые документы (HR)', 'doma-address-mappings': 'Сопоставления адресов (Doma)', 'doma-employee-mappings': 'Сопоставления сотрудников (Doma)', 'pr-work-photos': 'Фото работ (ПР)', 'pr-events': 'Мероприятия (ПР)', accounts: 'Лицевые счета', }; function getItemId(item: any, entityType: EntityType): string { const id = item.id ?? item.candidateId ?? item.vacancy_id; return String(id); } export const DataCleanupSection: React.FC = () => { const [entityType, setEntityType] = useState('buildings'); const [items, setItems] = useState([]); const [buildingsForAccounts, setBuildingsForAccounts] = useState([]); const [selectedBuildingId, setSelectedBuildingId] = useState(''); const [loading, setLoading] = useState(true); const [deletingId, setDeletingId] = useState(null); const [bulkDeleting, setBulkDeleting] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [error, setError] = useState(null); const loadItems = useCallback(async () => { setLoading(true); setError(null); try { switch (entityType) { case 'buildings': { const b = await backendApi.getBuildings(); setItems(b); break; } case 'employees': { const e = await backendApi.getEmployees(); setItems(e); break; } case 'districts': { const d = await backendApi.getDistricts(); setItems(d); break; } case 'portal-users': { const pu = await backendApi.getPortalUsers(); setItems(pu); break; } case 'permission-templates': { const pt = await backendApi.getPermissionTemplates(); setItems(pt); break; } case 'vacancies': { const v = await backendApi.getVacancies(); setItems(v); break; } case 'candidates': { const c = await backendApi.getCandidates(); setItems(c); break; } case 'training-programs': { const t = await backendApi.getTrainingPrograms(); setItems(t); break; } case 'expense-categories': { const ec = await backendApi.getExpenseCategories(); setItems(ec); break; } case 'expense-items': { const ei = await backendApi.getExpenseItems(); setItems(ei); break; } case 'supply-requests': { const sr = await backendApi.getSupplyRequests(); setItems(sr); break; } case 'office-inventory': { const inv = await backendApi.getOfficeInventory(); setItems(inv); break; } case 'office-documents': { const doc = await backendApi.getOfficeDocuments(); setItems(doc); break; } case 'hr-template-documents': { const hr = await backendApi.getHrTemplateDocuments(); setItems(hr); break; } case 'doma-address-mappings': case 'doma-employee-mappings': { const res = await backendApi.getDomaMappings(); const data = res?.data || { addresses: [], employees: [] }; setItems(entityType === 'doma-address-mappings' ? data.addresses : data.employees); break; } case 'pr-work-photos': { const wp = await backendApi.getWorkPhotos(); setItems(wp); break; } case 'pr-events': { const ev = await backendApi.getPREvents({ limit: 500 }); setItems(ev); break; } case 'accounts': { if (selectedBuildingId) { const building = await backendApi.getBuilding(selectedBuildingId); setItems(building?.accounts || []); } else { const b = await backendApi.getBuildings(); setBuildingsForAccounts(b); setItems([]); } break; } default: setItems([]); } } catch (err: any) { setError(err?.message || 'Ошибка загрузки'); setItems([]); } finally { setLoading(false); } }, [entityType, selectedBuildingId]); useEffect(() => { loadItems(); }, [loadItems]); useEffect(() => { setSelectedIds(new Set()); }, [entityType, selectedBuildingId]); const getDeleteFnForItem = useCallback((item: any): (() => Promise) => { switch (entityType) { case 'buildings': return () => backendApi.deleteBuilding(item.id); case 'employees': return () => backendApi.deleteEmployee(item.id); case 'districts': return () => backendApi.deleteDistrict(item.id); case 'portal-users': return () => backendApi.deletePortalUser(item.id); case 'permission-templates': return () => backendApi.deletePermissionTemplate(item.id); case 'vacancies': return () => backendApi.deleteVacancy(item.id); case 'candidates': return () => backendApi.deleteCandidate(item.id); case 'training-programs': return () => backendApi.deleteTrainingProgram(item.id); case 'expense-categories': return () => backendApi.deleteExpenseCategory(item.id); case 'expense-items': return () => backendApi.deleteExpenseItem(item.id); case 'supply-requests': return () => backendApi.deleteSupplyRequest(item.id); case 'office-inventory': return () => backendApi.deleteOfficeInventory(item.id); case 'office-documents': return () => backendApi.deleteOfficeDocument(item.id); case 'hr-template-documents': return () => backendApi.deleteHrTemplateDocument(item.id); case 'doma-address-mappings': return () => backendApi.deleteDomaAddressMapping(item.id); case 'doma-employee-mappings': return () => backendApi.deleteDomaEmployeeMapping(item.id); case 'pr-work-photos': return () => backendApi.deleteWorkPhoto(item.id); case 'pr-events': return () => backendApi.deletePREvent(item.id); case 'accounts': return () => backendApi.deleteAccount(selectedBuildingId, item.id); default: return async () => {}; } }, [entityType, selectedBuildingId]); const handleDeleteSelected = async () => { if (selectedIds.size === 0) return; if (!confirm(`Удалить выбранные записи (${selectedIds.size})? Действие необратимо.`)) return; setBulkDeleting(true); setError(null); try { const toDelete = items.filter((it) => selectedIds.has(getItemId(it, entityType))); for (const item of toDelete) { await getDeleteFnForItem(item)(); } setSelectedIds(new Set()); await loadItems(); } catch (err: any) { setError(err?.message || 'Ошибка удаления'); } finally { setBulkDeleting(false); } }; const handleDeleteAll = async () => { if (items.length === 0) return; if (!confirm(`Удалить все записи (${items.length}) в разделе «${ENTITY_LABELS[entityType]}»? Это необратимо. Рекомендуется создать резервную копию.`)) return; setBulkDeleting(true); setError(null); try { for (const item of items) { await getDeleteFnForItem(item)(); } setSelectedIds(new Set()); await loadItems(); } catch (err: any) { setError(err?.message || 'Ошибка удаления'); } finally { setBulkDeleting(false); } }; const toggleSelectAll = () => { if (selectedIds.size === items.length) { setSelectedIds(new Set()); } else { setSelectedIds(new Set(items.map((it) => getItemId(it, entityType)))); } }; const toggleSelect = (id: string) => { setSelectedIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const handleDelete = async ( id: string | number, label: string, deleteFn: () => Promise ) => { if (!confirm(`Удалить «${label}»? Действие необратимо.`)) return; setDeletingId(id); setError(null); try { await deleteFn(); await loadItems(); } catch (err: any) { setError(err?.message || 'Ошибка удаления'); } finally { setDeletingId(null); } }; const getTableHeaders = (): { main: string; sub?: string } => { switch (entityType) { case 'buildings': return { main: 'Адрес' }; case 'employees': return { main: 'ФИО', sub: 'Должность' }; case 'districts': return { main: 'Название', sub: 'Руководитель' }; case 'portal-users': return { main: 'Логин / Сотрудник' }; case 'permission-templates': return { main: 'Название', sub: 'Описание' }; case 'vacancies': return { main: 'Должность', sub: 'Отдел / Статус' }; case 'candidates': return { main: 'ФИО', sub: 'Вакансия / Этап' }; case 'training-programs': return { main: 'Название', sub: 'Тип / Категория' }; case 'expense-categories': return { main: 'Название', sub: 'Код' }; case 'expense-items': return { main: 'Название', sub: 'Категория' }; case 'supply-requests': return { main: 'Товар / Заявитель', sub: 'Статус' }; case 'office-inventory': return { main: 'Наименование', sub: 'Категория / Кол-во' }; case 'office-documents': return { main: 'Рег. № / Тема', sub: 'Корреспондент' }; case 'hr-template-documents': return { main: 'Название', sub: 'Файл / Дата' }; case 'doma-address-mappings': return { main: 'Адрес Doma', sub: 'Сопоставленный дом' }; case 'doma-employee-mappings': return { main: 'Имя Doma', sub: 'Сотрудник' }; case 'pr-work-photos': return { main: 'Работа / Дата', sub: 'Адрес' }; case 'pr-events': return { main: 'Название', sub: 'Дата / Тип' }; case 'accounts': return { main: 'Лицевой счёт', sub: 'Квартира / Владелец' }; default: return { main: 'Название' }; } }; const renderRowContent = (item: any) => { const id = item.id ?? item.candidateId ?? item.vacancy_id; const del = (fn: () => Promise, label: string) => ( ); switch (entityType) { case 'buildings': return ( <> {item.passport?.address || item.id} {del(() => backendApi.deleteBuilding(item.id), item.passport?.address || item.id)} ); case 'employees': return ( <> {item.name} {item.position} {del(() => backendApi.deleteEmployee(item.id), item.name)} ); case 'districts': return ( <> {item.name} {item.managerName} {del(() => backendApi.deleteDistrict(item.id), item.name)} ); case 'portal-users': return ( <> {item.login} {item.employeeName} · {item.employeePosition} {del(() => backendApi.deletePortalUser(item.id), item.login)} ); case 'permission-templates': return ( <> {item.name} {item.description || '—'} {del(() => backendApi.deletePermissionTemplate(item.id), item.name)} ); case 'vacancies': return ( <> {item.title || item.position || item.name || item.id} {[item.department, item.status].filter(Boolean).join(' / ') || '—'} {del(() => backendApi.deleteVacancy(item.id), item.title || item.position || String(item.id))} ); case 'candidates': return ( <> {item.full_name || item.name || item.id} {[item.vacancy_id, item.stage].filter(Boolean).join(' / ') || '—'} {del(() => backendApi.deleteCandidate(item.id), item.full_name || item.name || String(item.id))} ); case 'training-programs': return ( <> {item.title} {[item.type, item.category].filter(Boolean).join(' / ') || '—'} {del(() => backendApi.deleteTrainingProgram(item.id), item.title)} ); case 'expense-categories': return ( <> {item.name} {item.code || '—'} {del(() => backendApi.deleteExpenseCategory(item.id), item.name)} ); case 'expense-items': return ( <> {item.name} {item.category_name || item.categoryId || '—'} {del(() => backendApi.deleteExpenseItem(item.id), item.name)} ); case 'supply-requests': return ( <> {item.item_name || item.itemName} / {item.requester_name || item.requesterName} {item.status || '—'} {del(() => backendApi.deleteSupplyRequest(item.id), item.item_name || item.itemName || String(item.id))} ); case 'office-inventory': return ( <> {item.name} {item.category || '—'} / {item.quantity ?? '—'} {del(() => backendApi.deleteOfficeInventory(item.id), item.name)} ); case 'office-documents': return ( <> {item.reg_number || item.regNumber} — {item.title} {item.correspondent || '—'} {del(() => backendApi.deleteOfficeDocument(item.id), item.title || item.reg_number || String(item.id))} ); case 'hr-template-documents': return ( <> {item.name} {(item.originalFilename || item.filePath || '—') + (item.createdAt ? ` / ${item.createdAt.slice(0, 10)}` : '')} {del(() => backendApi.deleteHrTemplateDocument(item.id), item.name)} ); case 'doma-address-mappings': return ( <> {item.domaAddress} {item.buildingAddress || '—'} {del(() => backendApi.deleteDomaAddressMapping(item.id), item.domaAddress)} ); case 'doma-employee-mappings': return ( <> {item.domaName} {item.employeeName || '—'} {del(() => backendApi.deleteDomaEmployeeMapping(item.id), item.domaName)} ); case 'pr-work-photos': return ( <> {item.workName || item.work_name} / {item.workDate || item.work_date} {item.address || '—'} {del(() => backendApi.deleteWorkPhoto(item.id), item.workName || item.work_name || String(item.id))} ); case 'pr-events': return ( <> {item.title} {(item.date || item.eventDate) + (item.type ? ` / ${item.type}` : '')} {del(() => backendApi.deletePREvent(item.id), item.title)} ); case 'accounts': return ( <> {item.accountNumber || item.account_number || item.id} {(item.apartment || item.apartmentNumber) + (item.ownerName ? ` / ${item.ownerName}` : '')} {del(() => backendApi.deleteAccount(selectedBuildingId, item.id), item.accountNumber || item.account_number || String(item.id))} ); default: return null; } }; const headers = getTableHeaders(); const colCount = headers.sub ? 3 : 2; return (

Очистка данных

Удаление записей из базы по всем типам сущностей. Действие необратимо — при необходимости создайте резервную копию.

{entityType === 'accounts' && ( <> )}
{error && (
{error}
)} {loading ? (
) : ( <> {items.length > 0 && (
)}
{items.length > 0 && ( )} {headers.sub && } {items.length === 0 && ( )} {items.map((item) => { const id = getItemId(item, entityType); return ( {renderRowContent(item)} ); })}
0 && selectedIds.size === items.length} onChange={toggleSelectAll} disabled={bulkDeleting} className="rounded border-slate-300 text-red-600 focus:ring-red-500" /> {headers.main}{headers.sub}Действие
{entityType === 'accounts' && !selectedBuildingId ? 'Выберите дом' : `Нет записей: ${ENTITY_LABELS[entityType]}`}
toggleSelect(id)} disabled={bulkDeleting} className="rounded border-slate-300 text-red-600 focus:ring-red-500" />
)}
); };