import { Box, Typography } from '@mui/material';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { findItemPage, positiveInteger, readListVisit, rememberListVisit } from '../../utils/listNavigation';
import { toast } from 'react-toastify';
import { ProjectsApi } from '../../api/projects';
import { SspApi } from '../../api/ssp';
import { useAuth } from '../../app/context/AuthProvider';
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { DangerOutlinedButton, PrimaryButton } from '../../components/common/Buttons/Buttons';
import Modal from '../../components/common/Modal/Modal';
import { WhitePlus } from '../../components/common/icons/icons';
import { ROLES_NAME_ID } from '../../constants/constants';
import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig';
import { ModalCreateProject } from '../ProjectsPage/ModalCreateProject';
import EditModal from './components/EditModal/EditModal';
import ProjectSummaryTable from './components/ProjectSummaryTable/ProjectSummaryTable';
import ProjectsTable from './components/ProjectsTable/ProjectsTable';
import TableFilters from './components/TableFilters/TableFilters'; // Импортируем компонент фильтров
import { handleNavigateClick } from './utils/tableHandlers';
const NavigationProjectPage = () => {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const [lastVisit] = useState(() => readListVisit('projects'));
const restoreVisit = useRef(lastVisit?.url === `${location.pathname}${location.search}` ? lastVisit : null);
const branchIdsParam = searchParams.get('branch_ids') || '';
const branchIds = useMemo(
() =>
branchIdsParam
.split(',')
.filter(Boolean)
.map(Number)
.filter((value) => Number.isInteger(value) && value > 0),
[branchIdsParam],
);
const [selectedRow, setSelectedRow] = useState(null);
const [editModalOpen, setEditModalOpen] = useState(false);
const [selectedRowData, setSelectedRowData] = useState(null);
const [tableData, setTableData] = useState([]);
const [summaryData, setSummaryData] = useState([]);
const [isProjectsLoading, setIsProjectsLoading] = useState(true);
const [isSummaryLoading, setIsSummaryLoading] = useState(false);
const [orgUnitNames, setOrgUnitNames] = useState({});
const [orgUnits, setOrgUnits] = useState([]);
const [isSaving, setIsSaving] = useState(false);
const [projectsReloadKey, setProjectsReloadKey] = useState(0);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [projectToDelete, setProjectToDelete] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const { user } = useAuth();
// Состояния для фильтров
const [selectedBranches, setSelectedBranches] = useState([]);
const searchQuery = searchParams.get('search') || '';
const selectedStatus = searchParams.get('status') || '';
const [archivedData, setArchivedData] = useState([]);
const [archivedTotalCount, setArchivedTotalCount] = useState(0);
const archivedPagination = {
pageIndex: positiveInteger(searchParams.get('page'), 1) - 1,
pageSize: positiveInteger(searchParams.get('limit'), 10),
};
const setArchivedPagination = (updater) => {
restoreVisit.current = null;
const next = typeof updater === 'function' ? updater(archivedPagination) : updater;
setSearchParams((params) => {
const result = new URLSearchParams(params);
result.set('page', String(next.pageIndex + 1));
result.set('limit', String(next.pageSize));
return result;
});
};
const updateListFilter = (name, value) => {
restoreVisit.current = null;
setSearchParams((params) => {
const next = new URLSearchParams(params);
if (value) next.set(name, value); else next.delete(name);
next.set('page', '1');
return next;
});
};
const isArchiveMode = selectedStatus === 'archived';
const loadData = useCallback(async () => {
if (!user) return;
setIsLoading(true);
try {
const isAdmin = user.role_id === ROLES_NAME_ID.admin;
if (isAdmin) {
const sspResponse = await SspApi.getAll({ is_active: true });
if (!sspResponse.success) {
toast.error('Ошибка загрузки списка ССП');
return;
}
setOrgUnits(sspResponse.result);
setOrgUnitNames(Object.fromEntries((sspResponse.result || []).map((orgUnit) => [orgUnit.id, orgUnit.title])));
} else {
setOrgUnits(user.org_units ?? []);
setOrgUnitNames(Object.fromEntries((user.org_units || []).map((orgUnit) => [orgUnit.id, orgUnit.title])));
}
} catch (error) {
console.error('Error loading data:', error);
toast.error(error.response?.data?.message || 'Ошибка загрузки данных');
} finally {
setIsLoading(false);
}
}, [user]);
useEffect(() => {
loadData();
}, [loadData]);
useEffect(() => {
setSelectedBranches(orgUnits.filter(({ id }) => branchIds.includes(id)));
}, [branchIds, orgUnits]);
const createProjectConfig = useMemo(
() => ({
...DEFAULT_PROJECT_CONFIG,
'ССП/РФ': {
...DEFAULT_PROJECT_CONFIG['ССП/РФ'],
options: orgUnits,
},
}),
[orgUnits],
);
useEffect(() => {
let active = true;
if (!user || isLoading || isArchiveMode) return undefined;
const loadProjects = async () => {
setIsProjectsLoading(true);
try {
const response = await ProjectsApi.listWithReports();
if (active) setTableData(response.result || []);
} catch (requestError) {
console.error('Error loading projects with reports:', requestError);
if (active) toast.error('Не удалось загрузить проекты');
} finally {
if (active) setIsProjectsLoading(false);
}
};
loadProjects();
return () => {
active = false;
};
}, [user, isLoading, isArchiveMode, projectsReloadKey]);
useEffect(() => {
let active = true;
if (!user || isLoading || !isArchiveMode) return undefined;
const loadArchivedProjects = async () => {
setIsProjectsLoading(true);
try {
const offset = archivedPagination.pageIndex * archivedPagination.pageSize;
const params = {
status_in: 'archived',
offset,
limit: archivedPagination.pageSize,
...(branchIdsParam ? { branch_ids: branchIdsParam } : {}),
...(searchQuery.trim() ? { search: searchQuery.trim() } : {}),
};
const response = await ProjectsApi.listWithReports(params);
if (!active) return;
const visit = restoreVisit.current;
restoreVisit.current = null;
if (visit && !(response.result || []).some((item) => String(item.id) === visit.id)) {
const foundPage = await findItemPage({
id: visit.id, currentPage: archivedPagination.pageIndex + 1,
pageSize: archivedPagination.pageSize, count: response.count,
fetchPage: async (page) => (await ProjectsApi.listWithReports({ ...params, offset: (page - 1) * archivedPagination.pageSize })).result || [],
isCancelled: () => !active,
});
if (!active) return;
if (foundPage) {
const savedUrl = new URL(visit.url, window.location.origin);
savedUrl.searchParams.set('page', String(foundPage));
rememberListVisit('projects', visit.id, `${savedUrl.pathname}${savedUrl.search}`);
setSearchParams(savedUrl.searchParams, { replace: true });
return;
}
}
if (active) {
setArchivedData(response.result || []);
setArchivedTotalCount(response.count || 0);
}
} catch (requestError) {
console.error('Error loading archived projects:', requestError);
if (active) {
setArchivedData([]);
setArchivedTotalCount(0);
toast.error('Не удалось загрузить архивные проекты');
}
} finally {
if (active) setIsProjectsLoading(false);
}
};
loadArchivedProjects();
return () => {
active = false;
};
}, [
user,
isLoading,
isArchiveMode,
branchIdsParam,
searchQuery,
archivedPagination.pageIndex,
archivedPagination.pageSize,
projectsReloadKey,
]);
useEffect(() => {
let active = true;
if (!selectedRow?.original?.id) {
setSummaryData([]);
return () => {
active = false;
};
}
const loadSummary = async () => {
setIsSummaryLoading(true);
try {
const response = await ProjectsApi.getSummary(selectedRow.original.id);
if (active) setSummaryData(response.result.filter((v) => v.row_type !== 'PROJECT') || []);
} catch (requestError) {
console.error('Error loading project summary:', requestError);
if (active) {
setSummaryData([]);
toast.error('Не удалось загрузить сводку проекта');
}
} finally {
if (active) setIsSummaryLoading(false);
}
};
loadSummary();
return () => {
active = false;
};
}, [selectedRow?.original?.id]);
const filteredData = useMemo(() => {
if (isArchiveMode) {
return archivedData;
}
if (selectedBranches.length === 0 && !searchQuery && !selectedStatus) {
return tableData;
}
const query = searchQuery.toLowerCase().trim();
return tableData
.filter((item) => {
if (selectedStatus && item.status !== selectedStatus) {
return false;
}
// Фильтр по филиалу (только для корневых)
if (selectedBranches.length > 0) {
return selectedBranches.some(({ id }) => item.org_unit_id === id);
}
return true;
})
.map((item) => {
if (!query) {
return item;
}
// Фильтруем дочерние элементы - оставляем только те, что совпадают с поиском
const filteredSubRows = item.sub_rows?.filter((subItem) => (subItem.project || '').toLowerCase().includes(query)) || [];
// Возвращаем элемент с отфильтрованными дочерними элементами
return {
...item,
sub_rows: filteredSubRows,
};
})
.filter((item) => {
if (!query) {
return true;
}
const parentMatches = (item.name || '').toLowerCase().includes(query);
const hasMatchingChildren = item.sub_rows && item.sub_rows.length > 0;
return parentMatches || hasMatchingChildren;
});
}, [archivedData, isArchiveMode, searchQuery, selectedBranches, selectedStatus, tableData]);
const onEdit = useCallback((row) => {
const isSmeta = row.depth > 0;
const parentProject = isSmeta ? row.getParentRow()?.original : row.original;
const projectId = parentProject?.id;
setSelectedRowData({
...row.original,
_rowType: isSmeta ? 'smeta' : 'project',
_projectId: projectId,
_projectFundingByKoDecision: parentProject?.funding_by_ko_decision,
});
setEditModalOpen(true);
}, []);
const onNavigate = useCallback(
(row) => {
rememberListVisit('projects', row.original.id || row.original.project, `${location.pathname}${location.search}`);
handleNavigateClick(row, navigate);
},
[navigate, location.pathname, location.search],
);
const handleDeleteProject = async () => {
if (!projectToDelete?.id) return;
setIsDeleting(true);
try {
await ProjectsApi.delete(projectToDelete.id);
if (selectedRow?.original?.id === projectToDelete.id) {
setSelectedRow(null);
setSummaryData([]);
}
setProjectToDelete(null);
setProjectsReloadKey((key) => key + 1);
toast.success('Проект успешно удалён');
} catch (requestError) {
console.error('Error deleting project:', requestError);
toast.error(requestError.response?.data?.detail || requestError.response?.data?.message || 'Не удалось удалить проект');
} finally {
setIsDeleting(false);
}
};
const onSaveEdit = async (formData) => {
setIsSaving(true);
try {
if (formData._rowType === 'smeta') {
const { is_in_plan, is_in_plan_q2, is_in_plan_q3, is_in_plan_q4, development_block, reserve_to_prrs_ahr, reserve_to_prrs_kv } =
formData;
await ProjectsApi.updateSmeta(formData._projectId, formData.year, {
is_in_plan,
is_in_plan_q2,
is_in_plan_q3,
is_in_plan_q4,
development_block: development_block || null,
reserve_to_prrs_ahr: reserve_to_prrs_ahr === '' ? null : reserve_to_prrs_ahr,
reserve_to_prrs_kv: reserve_to_prrs_kv === '' ? null : reserve_to_prrs_kv,
});
} else {
const {
name,
status,
technical_number,
project_type,
vsp_format,
placement_type,
object_address,
staff_count,
total_area,
org_unit_id,
krf_decision_date,
fk_decision_date,
board_decision_date,
open_relocate_close_date,
funding_by_ko_decision,
} = formData;
await ProjectsApi.update(formData._projectId, {
name,
status: status || null,
technical_number: technical_number || null,
project_type: project_type || null,
vsp_format: vsp_format || null,
placement_type: placement_type || null,
object_address: object_address || null,
staff_count: staff_count === '' ? null : staff_count,
total_area: total_area === '' ? null : total_area,
org_unit_id: org_unit_id === '' ? null : org_unit_id,
krf_decision_date: krf_decision_date || null,
fk_decision_date: fk_decision_date || null,
board_decision_date: board_decision_date || null,
open_relocate_close_date: open_relocate_close_date || null,
funding_by_ko_decision: funding_by_ko_decision || null,
});
}
setEditModalOpen(false);
setProjectsReloadKey((key) => key + 1);
} catch (requestError) {
console.error('Error updating project summary row:', requestError);
toast.error(requestError.response?.data?.detail || requestError.response?.data?.message || 'Не удалось сохранить изменения');
} finally {
setIsSaving(false);
}
};
// Обработчики фильтров
const handleBranchChange = (value) => {
setSelectedRow(null);
setSummaryData([]);
setSelectedBranches(value);
updateListFilter('branch_ids', value.map(({ id }) => id).join(','));
};
const handleSearchChange = (value) => {
updateListFilter('search', value);
};
const handleStatusChange = (value) => {
setSelectedRow(null);
setSummaryData([]);
updateListFilter('status', value);
};
const handleCreateProject = async (formData) => {
try {
await ProjectsApi.create({
name: formData['Название проекта'] || '',
project_type: formData['Тип проекта развития'] || null,
vsp_format: formData['Формат ВСП'] || null,
object_address: formData['Адрес объекта'] || '',
staff_count: formData['Целевая численность, чел.'] || 0,
placement_type: formData['Тип размещения'] || null,
total_area: formData['Площадь размещения, м2'] || 0,
year: formData.Год || null,
branch_id: formData['ССП/РФ'] || null,
});
setProjectsReloadKey((key) => key + 1);
toast.success('Проект успешно создан');
} catch (requestError) {
console.error('Error creating project:', requestError);
toast.error(requestError.response?.data?.detail || requestError.response?.data?.message || 'Ошибка при создании проекта');
}
};
return (
<>
{/* Компонент фильтров */}
navigate(branchIdsParam ? `/projects/summaries?branch_ids=${branchIdsParam}` : '/projects/summaries')}
sx={{ height: '2.5rem', whiteSpace: 'nowrap' }}>
Перейти к сводам
setCreateModalOpen(true)} startIcon={} sx={{ height: '2.5rem', whiteSpace: 'nowrap' }}>
Создать проект
{/* Модалка редактирования */}
setEditModalOpen(false)}
rowData={selectedRowData}
onSave={onSaveEdit}
isSaving={isSaving}
orgUnitNames={orgUnitNames}
/>
setCreateModalOpen(false)}
onCreate={handleCreateProject}
config={createProjectConfig}
/>
!isDeleting && setProjectToDelete(null)}
title='Удалить проект?'
size='small'
actions={
<>
setProjectToDelete(null)} disabled={isDeleting}>
Отмена
{isDeleting ? 'Удаление...' : 'Удалить'}
>
}>
Проект «{projectToDelete?.name}» будет удалён без возможности восстановления.
>
);
};
export default NavigationProjectPage;