PotapovaA 63c3cb26c7 fix
2026-08-17 13:21:08 +03:00

643 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<>
<HeaderSwitch />
<Box
sx={{
p: '1.5rem',
maxWidth: '1600px',
mx: 'auto',
height: '100vh',
display: 'flex',
flexDirection: 'column',
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1, }}>
{/* Компонент фильтров */}
<TableFilters
selectedBranch={selectedBranch}
onBranchChange={handleBranchChange}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
/>
<PrimaryButton onClick={() => setCreateModalOpen(true)} startIcon={<WhitePlus />} sx={{ height: '2.5rem' }}>
Создать проект
</PrimaryButton>
</Box>
{/* Первая таблица */}
<Paper
sx={{
mb: '2rem',
overflow: 'hidden',
flex: '0 0 45vh',
borderRadius: '1rem',
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
}}>
<MaterialReactTable table={projectsTable} />
</Paper>
{/* Вторая таблица */}
<Paper
sx={{
overflow: 'hidden',
flex: '0 0 35vh',
borderRadius: '1rem',
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
}}>
<Typography
variant='subtitle1'
sx={{
p: '0.75rem 1.5rem',
backgroundColor: 'white',
borderBottom: '1px solid #e0e0e0',
fontWeight: 'bold',
flexShrink: 0,
fontSize: '1.25rem',
}}>
{getSelectedProjectTitle()}
</Typography>
{selectedRow ? (
<Box sx={{ height: 'calc(35vh - 3rem)' }}>
{isSummaryLoading && !summaryData.length ? (
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CircularProgress size={32} />
</Box>
) : <MaterialReactTable table={summaryProjectTable} />}
</Box>
) : (
<Box
sx={{
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'rgb(152, 162, 179)',
fontSize: '1rem',
fontWeight: 600,
}}>
Разверните или выберите проект в верхней таблице
</Box>
)}
</Paper>
{/* Модалка редактирования */}
<EditModal open={editModalOpen} onClose={() => setEditModalOpen(false)} rowData={selectedRowData} onSave={onSaveEdit} isSaving={isSaving} orgUnitNames={orgUnitNames} />
<ModalCreateProject
open={createModalOpen}
onClose={() => setCreateModalOpen(false)}
onCreate={handleCreateProject}
config={createProjectConfig}
/>
<Modal
open={Boolean(projectToDelete)}
onClose={() => !isDeleting && setProjectToDelete(null)}
title='Удалить проект?'
size='small'
actions={
<>
<PrimaryButton onClick={() => setProjectToDelete(null)} disabled={isDeleting}>
Отмена
</PrimaryButton>
<DangerOutlinedButton onClick={handleDeleteProject} disabled={isDeleting}>
{isDeleting ? 'Удаление...' : 'Удалить'}
</DangerOutlinedButton>
</>
}>
<Typography>
Проект «{projectToDelete?.name}» будет удалён без возможности восстановления.
</Typography>
</Modal>
</Box>
</>
);
};
export default SummaryPage;