Initial commit MKD fixes
This commit is contained in:
674
components/admin/DataCleanupSection.tsx
Executable file
674
components/admin/DataCleanupSection.tsx
Executable file
@@ -0,0 +1,674 @@
|
||||
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<EntityType, string> = {
|
||||
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<EntityType>('buildings');
|
||||
const [items, setItems] = useState<any[]>([]);
|
||||
const [buildingsForAccounts, setBuildingsForAccounts] = useState<Building[]>([]);
|
||||
const [selectedBuildingId, setSelectedBuildingId] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deletingId, setDeletingId] = useState<string | number | null>(null);
|
||||
const [bulkDeleting, setBulkDeleting] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [error, setError] = useState<string | null>(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<void>) => {
|
||||
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<void>
|
||||
) => {
|
||||
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<void>, label: string) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(id, label, fn)}
|
||||
disabled={deletingId === id}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-red-600 hover:bg-red-50 text-xs font-medium rounded-lg disabled:opacity-50"
|
||||
>
|
||||
{deletingId === id ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />}
|
||||
Удалить
|
||||
</button>
|
||||
);
|
||||
|
||||
switch (entityType) {
|
||||
case 'buildings':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.passport?.address || item.id}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteBuilding(item.id), item.passport?.address || item.id)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'employees':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.name}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.position}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteEmployee(item.id), item.name)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'districts':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.name}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.managerName}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteDistrict(item.id), item.name)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'portal-users':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4">
|
||||
<span className="font-medium text-slate-800">{item.login}</span>
|
||||
<span className="block text-xs text-slate-500">{item.employeeName} · {item.employeePosition}</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deletePortalUser(item.id), item.login)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'permission-templates':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.name}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.description || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deletePermissionTemplate(item.id), item.name)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'vacancies':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.title || item.position || item.name || item.id}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{[item.department, item.status].filter(Boolean).join(' / ') || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteVacancy(item.id), item.title || item.position || String(item.id))}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'candidates':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.full_name || item.name || item.id}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{[item.vacancy_id, item.stage].filter(Boolean).join(' / ') || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteCandidate(item.id), item.full_name || item.name || String(item.id))}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'training-programs':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.title}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{[item.type, item.category].filter(Boolean).join(' / ') || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteTrainingProgram(item.id), item.title)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'expense-categories':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.name}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.code || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteExpenseCategory(item.id), item.name)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'expense-items':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.name}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.category_name || item.categoryId || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteExpenseItem(item.id), item.name)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'supply-requests':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.item_name || item.itemName} / {item.requester_name || item.requesterName}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.status || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteSupplyRequest(item.id), item.item_name || item.itemName || String(item.id))}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'office-inventory':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.name}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.category || '—'} / {item.quantity ?? '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteOfficeInventory(item.id), item.name)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'office-documents':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.reg_number || item.regNumber} — {item.title}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.correspondent || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteOfficeDocument(item.id), item.title || item.reg_number || String(item.id))}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'hr-template-documents':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.name}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{(item.originalFilename || item.filePath || '—') + (item.createdAt ? ` / ${item.createdAt.slice(0, 10)}` : '')}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteHrTemplateDocument(item.id), item.name)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'doma-address-mappings':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.domaAddress}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.buildingAddress || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteDomaAddressMapping(item.id), item.domaAddress)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'doma-employee-mappings':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.domaName}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.employeeName || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteDomaEmployeeMapping(item.id), item.domaName)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'pr-work-photos':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.workName || item.work_name} / {item.workDate || item.work_date}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{item.address || '—'}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteWorkPhoto(item.id), item.workName || item.work_name || String(item.id))}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'pr-events':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.title}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{(item.date || item.eventDate) + (item.type ? ` / ${item.type}` : '')}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deletePREvent(item.id), item.title)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
case 'accounts':
|
||||
return (
|
||||
<>
|
||||
<td className="py-3 px-4 font-medium text-slate-800">{item.accountNumber || item.account_number || item.id}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{(item.apartment || item.apartmentNumber) + (item.ownerName ? ` / ${item.ownerName}` : '')}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
{del(() => backendApi.deleteAccount(selectedBuildingId, item.id), item.accountNumber || item.account_number || String(item.id))}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const headers = getTableHeaders();
|
||||
const colCount = headers.sub ? 3 : 2;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-slate-800">Очистка данных</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Удаление записей из базы по всем типам сущностей. Действие необратимо — при необходимости создайте резервную копию.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="text-sm font-bold text-slate-700">Тип данных:</label>
|
||||
<select
|
||||
value={entityType}
|
||||
onChange={(e) => {
|
||||
setEntityType(e.target.value as EntityType);
|
||||
if (e.target.value !== 'accounts') setSelectedBuildingId('');
|
||||
}}
|
||||
className="px-3 py-2 border border-slate-200 rounded-lg text-sm min-w-[220px]"
|
||||
>
|
||||
{(Object.keys(ENTITY_LABELS) as EntityType[]).map((t) => (
|
||||
<option key={t} value={t}>{ENTITY_LABELS[t]}</option>
|
||||
))}
|
||||
</select>
|
||||
{entityType === 'accounts' && (
|
||||
<>
|
||||
<label className="text-sm text-slate-600">Дом:</label>
|
||||
<select
|
||||
value={selectedBuildingId}
|
||||
onChange={(e) => setSelectedBuildingId(e.target.value)}
|
||||
className="px-3 py-2 border border-slate-200 rounded-lg text-sm min-w-[240px]"
|
||||
>
|
||||
<option value="">— Выберите дом —</option>
|
||||
{buildingsForAccounts.map((b) => (
|
||||
<option key={b.id} value={b.id}>{b.passport?.address || b.id}</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-xl border border-red-200 bg-red-50 p-4 text-red-700 text-sm flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 flex-shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary-500" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{items.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-3 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedIds.size === 0 || bulkDeleting}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-red-700 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 disabled:opacity-50 disabled:pointer-events-none"
|
||||
>
|
||||
{bulkDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||||
Удалить выбранные ({selectedIds.size})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteAll}
|
||||
disabled={bulkDeleting}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-red-600 border border-red-700 rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:pointer-events-none"
|
||||
>
|
||||
{bulkDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||||
Удалить все ({items.length})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
{items.length > 0 && (
|
||||
<th className="w-10 py-3 px-4 text-left">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={items.length > 0 && selectedIds.size === items.length}
|
||||
onChange={toggleSelectAll}
|
||||
disabled={bulkDeleting}
|
||||
className="rounded border-slate-300 text-red-600 focus:ring-red-500"
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
<th className="text-left py-3 px-4 font-bold text-slate-700">{headers.main}</th>
|
||||
{headers.sub && <th className="text-left py-3 px-4 font-bold text-slate-700">{headers.sub}</th>}
|
||||
<th className="text-right py-3 px-4 font-bold text-slate-700">Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={colCount} className="py-8 px-4 text-center text-slate-500">
|
||||
{entityType === 'accounts' && !selectedBuildingId
|
||||
? 'Выберите дом'
|
||||
: `Нет записей: ${ENTITY_LABELS[entityType]}`}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{items.map((item) => {
|
||||
const id = getItemId(item, entityType);
|
||||
return (
|
||||
<tr key={`${entityType}-${id}`} className="border-b border-slate-100 hover:bg-slate-50/50">
|
||||
<td className="w-10 py-3 px-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(id)}
|
||||
onChange={() => toggleSelect(id)}
|
||||
disabled={bulkDeleting}
|
||||
className="rounded border-slate-300 text-red-600 focus:ring-red-500"
|
||||
/>
|
||||
</td>
|
||||
{renderRowContent(item)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user