import { Box, CircularProgress, Paper, Typography } from '@mui/material'; import { MaterialReactTable, useMaterialReactTable } from 'material-react-table'; import { useEffect, useMemo, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; import { ProjectsApi } from '../../api/projects'; import { SspApi } from '../../api/ssp'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; import { DangerOutlinedButton, PrimaryButton } from '../../components/common/Buttons/Buttons'; import { WhitePlus } from '../../components/common/icons/icons'; import Modal from '../../components/common/Modal/Modal'; import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig'; import { ModalCreateProject } from '../ProjectsPage/ModalCreateProject'; import { createFirstColumns, secondColumns } from './columns'; import EditModal from './components/EditModal/EditModal'; import TableFilters from './components/TableFilters/TableFilters'; // Импортируем компонент фильтров import { handleNavigateClick } from './utils/tableHandlers'; import { useAuth } from '../../app/context/AuthProvider'; import { ROLES_NAME_ID } from '../../constants/constants'; const SummaryPage = () => { const navigate = useNavigate(); 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 [selectedBranch, setSelectedBranch] = useState(''); const [searchQuery, setSearchQuery] = useState(''); 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]); const createProjectConfig = useMemo(() => ({ ...DEFAULT_PROJECT_CONFIG, 'ССП/РФ': { ...DEFAULT_PROJECT_CONFIG['ССП/РФ'], options: orgUnits, }, }), [orgUnits]); useEffect(() => { let active = true; const loadProjects = async () => { setIsProjectsLoading(true); try { const response = await ProjectsApi.listWithReports({ limit: 1000, ...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}), }); 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; }; }, [selectedBranch?.id, 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 getFilteredData = () => { if (!selectedBranch && !searchQuery) { return tableData; } return tableData .filter((item) => { // Фильтр по филиалу (только для корневых) if (selectedBranch) { return item.org_unit_id === selectedBranch.id; } return true; }) .map((item) => { if (!searchQuery) { return item; } const query = searchQuery.toLowerCase().trim(); // Проверяем, совпадает ли родитель const parentMatches = (item.name || '').toLowerCase().includes(query); // Фильтруем дочерние элементы - оставляем только те, что совпадают с поиском const filteredSubRows = item.sub_rows?.filter((subItem) => (subItem.project || '').toLowerCase().includes(query)) || []; // Возвращаем элемент с отфильтрованными дочерними элементами return { ...item, sub_rows: filteredSubRows, }; }) .filter((item) => { if (!searchQuery) { return true; } const query = searchQuery.toLowerCase().trim(); const parentMatches = (item.name || '').toLowerCase().includes(query); const hasMatchingChildren = item.sub_rows && item.sub_rows.length > 0; return parentMatches || hasMatchingChildren; }); }; const filteredData = getFilteredData(); const handleRowClick = (row) => { if (row.depth === 0 && row) { setSelectedRow(row); } }; const isChildOfSelectedRow = (row) => { if (!selectedRow) return false; const parentRowId = row.parentId; return parentRowId === selectedRow.id; }; const onEdit = (row) => { const isSmeta = row.depth > 0; const projectId = isSmeta ? row.getParentRow()?.original?.id : row.original.id; setSelectedRowData({ ...row.original, _rowType: isSmeta ? 'smeta' : 'project', _projectId: projectId, }); setEditModalOpen(true); }; const onNavigate = (row) => { handleNavigateClick(row, navigate); }; 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 firstColumns = useMemo( () => createFirstColumns({ onDelete: setProjectToDelete, onEdit, onNavigate, orgUnitNames }), [orgUnitNames], ); const tableHeadCellStyles = { fontWeight: 700, fontSize: '12px', lineHeight: 1.15, textAlign: 'left', backgroundColor: 'rgb(248, 249, 250)', borderRight: '1px solid #e0e0e0', '&:last-child': { borderRight: 'none', }, whiteSpace: 'nowrap', position: 'sticky', top: 0, boxShadow: 'none', padding: '0.5rem 1rem', '& .MuiTableSortLabel-icon': { fontSize: '0.875rem', }, '& .MuiTableSortLabel-iconDirectionDesc': { fontSize: '0.875rem', }, '& .MuiTableSortLabel-iconDirectionAsc': { fontSize: '0.875rem', }, }; const projectsTableOptions = { enableColumnActions: false, enableColumnFilters: false, enablePagination: false, enableSorting: true, enableBottomToolbar: false, enableTopToolbar: false, enableExpanding: true, getSubRows: (row) => row.sub_rows, muiTableHeadCellProps: { sx: tableHeadCellStyles, }, muiExpandButtonProps: { sx: { width: '1.5rem', height: '1.5rem', minWidth: '1.5rem', '& .MuiSvgIcon-root': { fontSize: '0.875rem', }, }, }, muiTableBodyRowProps: ({ row }) => { const isSelected = selectedRow && row.id === selectedRow.id; const isChild = isChildOfSelectedRow(row); return { sx: { backgroundColor: '#ffffff', '&:hover': { backgroundColor: 'rgb(248, 250, 252)', }, '&:hover td::after': { backgroundColor: 'transparent !important', }, ...(row.depth > 0 && { backgroundColor: 'rgb(252, 252, 253)', }), ...((isSelected || isChild) && { backgroundColor: 'rgb(234, 247, 236)', '&:hover': { backgroundColor: 'rgb(225, 242, 229) !important', }, }), }, onClick: () => handleRowClick(row), }; }, muiTableBodyCellProps: ({ row, column }) => ({ sx: { borderRight: '1px solid #e0e0e0', '&:last-child': { borderRight: 'none', }, '&:hover td::after': { backgroundColor: 'transparent !important', }, textAlign: 'left', fontSize: '0.875rem', padding: '0.5rem 1rem', ...(column.id === 'mrt-row-expand' && { width: '1.5rem', maxWidth: '1.5rem', minWidth: '1.5rem', padding: '0.5rem 0.25rem', }), ...(column.id === 'project' && row.depth > 0 && { pl: `${2 + row.depth * 0.5}rem`, }), }, }), muiTablePaperProps: { elevation: 0, sx: { border: '1px solid #e0e0e0', borderRadius: '1rem', overflow: 'hidden', height: '45vh', display: 'flex', flexDirection: 'column', }, }, muiTableContainerProps: { sx: { flex: 1, overflow: 'auto', '& thead': { position: 'sticky', top: 0, zIndex: 10, }, }, }, }; const summaryProjectTableOptions = { enableColumnActions: false, enableColumnFilters: false, enablePagination: false, enableSorting: true, enableBottomToolbar: false, enableTopToolbar: false, enableExpanding: false, muiTableBodyRowProps: ({ row }) => ({ sx: { backgroundColor: '#ffffff', '&:hover': { backgroundColor: 'rgb(248, 250, 252)', }, '&:hover td::after': { backgroundColor: 'transparent !important', }, boxShadow: 'none', }, }), muiTableHeadCellProps: { sx: tableHeadCellStyles, }, muiTableBodyCellProps: ({ column }) => ({ sx: { borderRight: '1px solid #e0e0e0', '&:last-child': { borderRight: 'none', }, textAlign: 'left', fontSize: '0.875rem', padding: '0.5rem 1rem', }, }), muiTablePaperProps: { elevation: 0, sx: { border: '1px solid #e0e0e0', borderRadius: '0 0 1rem 1rem', overflow: 'hidden', height: '35vh', display: 'flex', flexDirection: 'column', }, }, muiTableContainerProps: { sx: { flex: 1, overflow: 'auto', '& thead': { position: 'sticky', top: 0, zIndex: 10, }, }, }, initialState: { columnPinning: { left: ['project'] }, }, }; const projectsTable = useMaterialReactTable({ columns: firstColumns, data: filteredData, ...projectsTableOptions, initialState: { columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] }, }, state: { isLoading: isProjectsLoading }, }); const summaryProjectTable = useMaterialReactTable({ columns: secondColumns, data: summaryData, ...summaryProjectTableOptions, initialState: { columnPinning: { left: ['data.header.name'] }, }, state: { isLoading: isSummaryLoading }, }); const getSelectedProjectTitle = () => { if (selectedRow?.original.name) { return selectedRow.original.name; } return 'Проект не выбран'; }; // Обработчики фильтров const handleBranchChange = (value) => { setSelectedRow(null); setSummaryData([]); setSelectedBranch(value); }; const handleSearchChange = (value) => { setSearchQuery(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 ( <> {/* Компонент фильтров */} setCreateModalOpen(true)} startIcon={} sx={{ height: '2.5rem' }}> Создать проект {/* Первая таблица */} {/* Вторая таблица */} {getSelectedProjectTitle()} {selectedRow ? ( {isSummaryLoading && !summaryData.length ? ( ) : } ) : ( Разверните или выберите проект в верхней таблице )} {/* Модалка редактирования */} 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 SummaryPage;