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;