From c8cc1afec103d8cf34de7dc3e0b32199ead9bc9d Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Tue, 30 Jun 2026 15:07:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20co?= =?UTF-8?q?nstants.js=20=D0=B2=20=D0=BF=D0=B0=D0=BF=D0=BA=D1=83=20constant?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/projects.js | 2 +- web/src/app/Routes.jsx | 1 + .../components/HeaderMenu/HeaderSwitch.jsx | 2 +- web/src/components/Stages/AddStagesModal.jsx | 2 +- .../components/Stages/StageInfoForTooltip.jsx | 2 +- .../Stages/StagesTable/StagesTable.jsx | 2 +- web/src/components/common/PaginatedList.jsx | 2 +- web/src/components/common/Selects/Selects.jsx | 2 +- .../common/SwitchFormTask/SwitchFormTask.jsx | 2 +- .../components/common/TableCard/TableCard.jsx | 2 +- .../components/common/TableList/TableList.jsx | 7 +- .../components/common/TaskCardComponent.jsx | 2 +- web/src/{ => constants}/constants.jsx | 0 web/src/constants/projectConfig.jsx | 90 ++++++++ .../AuditLogsPage/AuditLogsPage.jsx | 2 +- .../components/AuditLogsTable.jsx | 2 +- .../AuditLogsPage/utils/transformers.jsx | 2 +- .../pages/AdminPanel/SppsPage/SppsPage.jsx | 2 +- .../AdminPanel/UsersPage/ModalEditUserSsp.jsx | 2 +- .../UsersPage/UserTable/EditUserModal.jsx | 2 +- .../UsersPage/UserTable/UserTable.jsx | 4 +- .../AdminPanel/UsersPage/UserTableForAdd.jsx | 2 +- .../AdminPanel/UsersPage/UsersActionBar.jsx | 2 +- .../pages/AdminPanel/UsersPage/UsersPage.jsx | 4 +- web/src/pages/DictVspPage/DictVspPage.jsx | 2 +- .../components/Modals/ModalEditAddVsp.jsx | 2 +- .../DictVspPage/components/TableDictVsp.jsx | 2 +- web/src/pages/NewFormTablePage.jsx | 2 +- .../pages/ProjectsPage/ModalCreateProject.jsx | 146 ++++++++++++- web/src/pages/ProjectsPage/ProjectCard.jsx | 119 +++++----- web/src/pages/ProjectsPage/ProjectsPage.jsx | 203 +++++++++++------- web/src/pages/TablePage.jsx | 2 +- web/src/pages/TaskPage/TaskPage.jsx | 104 +++++---- web/src/pages/TasksPage/TasksPage.jsx | 3 +- 34 files changed, 513 insertions(+), 214 deletions(-) rename web/src/{ => constants}/constants.jsx (100%) create mode 100644 web/src/constants/projectConfig.jsx diff --git a/web/src/api/projects.js b/web/src/api/projects.js index 218ec55..ae05081 100644 --- a/web/src/api/projects.js +++ b/web/src/api/projects.js @@ -14,7 +14,7 @@ export const ProjectsApi = { }, update: (id, data) => { - return api.put(`/projects/${id}`, data).then((r) => r.data); + return api.patch(`/projects/${id}`, data).then((r) => r.data); }, delete: (id) => { diff --git a/web/src/app/Routes.jsx b/web/src/app/Routes.jsx index 7b62aa3..4387604 100644 --- a/web/src/app/Routes.jsx +++ b/web/src/app/Routes.jsx @@ -41,6 +41,7 @@ export const AppRoutes = () => { }/> } /> } /> + } /> } /> } /> } /> diff --git a/web/src/components/HeaderMenu/HeaderSwitch.jsx b/web/src/components/HeaderMenu/HeaderSwitch.jsx index dcd49f1..561da87 100644 --- a/web/src/components/HeaderMenu/HeaderSwitch.jsx +++ b/web/src/components/HeaderMenu/HeaderSwitch.jsx @@ -4,7 +4,7 @@ import { NavLink } from 'react-router-dom'; import styled from '@emotion/styled'; import { TasksSvg, AdminPanelSvg, DictSvg } from '../common/icons/icons'; import { useAuth } from '../../app/context/AuthProvider'; -import { ROLES_NAME_ID } from '../../constants'; +import { ROLES_NAME_ID } from '../../constants/constants'; const Switch = styled.div` margin-left: 1rem; diff --git a/web/src/components/Stages/AddStagesModal.jsx b/web/src/components/Stages/AddStagesModal.jsx index 4b61bee..cdcec58 100644 --- a/web/src/components/Stages/AddStagesModal.jsx +++ b/web/src/components/Stages/AddStagesModal.jsx @@ -25,7 +25,7 @@ import { StageStatusChip } from './StageStatusChip'; import { formatDateForApi } from './utils'; import CollapsibleTree from '../common/CollapsibleTree'; import { collectLeafColumns } from '../RealtimeTable/utils/columnUtils'; -import { STAGE_ROLE_RUSSIAN_NAME } from '../../constants'; +import { STAGE_ROLE_RUSSIAN_NAME } from '../../constants/constants'; export const AddStagesModal = ({ isOpen, diff --git a/web/src/components/Stages/StageInfoForTooltip.jsx b/web/src/components/Stages/StageInfoForTooltip.jsx index b0e13b3..ec2ef96 100644 --- a/web/src/components/Stages/StageInfoForTooltip.jsx +++ b/web/src/components/Stages/StageInfoForTooltip.jsx @@ -1,6 +1,6 @@ import { Box, Divider, Stack } from '@mui/material'; import { UnLockedSvg } from '../common/icons/icons'; -import { ROLES_ID_RUSSIAN_NAME, EDIT_ACCESS_RUSSIAN } from '../../constants'; +import { ROLES_ID_RUSSIAN_NAME, EDIT_ACCESS_RUSSIAN } from '../../constants/constants'; export const StageInfoForTooltip = ({ stageData = [] }) => { const accessData = stageData[0] || []; diff --git a/web/src/components/Stages/StagesTable/StagesTable.jsx b/web/src/components/Stages/StagesTable/StagesTable.jsx index 8a52ce6..4cef924 100644 --- a/web/src/components/Stages/StagesTable/StagesTable.jsx +++ b/web/src/components/Stages/StagesTable/StagesTable.jsx @@ -20,7 +20,7 @@ import { import { getStageStatus, getStatusColors } from './utils'; import { formatDate } from '../../../utils/formatDate'; import { StageStatusChip } from '../StageStatusChip'; -import { ROLES_NAME_ID, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../constants'; +import { ROLES_NAME_ID, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../constants/constants'; import { useAuth } from '../../../app/context/AuthProvider'; export const StagesTable = ({ stages, onEdit, onDelete }) => { diff --git a/web/src/components/common/PaginatedList.jsx b/web/src/components/common/PaginatedList.jsx index ce4ee52..430ce72 100644 --- a/web/src/components/common/PaginatedList.jsx +++ b/web/src/components/common/PaginatedList.jsx @@ -56,7 +56,7 @@ export function PaginatedList({ extraActions = null, enableExport = true, limitOptions = [10, 20, 50, 100, 200], - maxHeight = 'calc(110vh - 300px)', + maxHeight = 'calc(100vh - 300px)', enableScroll = true, }) { const [exportMode, setExportMode] = useState(false); diff --git a/web/src/components/common/Selects/Selects.jsx b/web/src/components/common/Selects/Selects.jsx index 4787aa1..60c1e8b 100644 --- a/web/src/components/common/Selects/Selects.jsx +++ b/web/src/components/common/Selects/Selects.jsx @@ -1,5 +1,5 @@ import { useEffect, useState, useRef } from 'react'; -import { EDIT_ACCESS_RUSSIAN, ROLES_ID_RUSSIAN_NAME } from '../../../constants'; +import { EDIT_ACCESS_RUSSIAN, ROLES_ID_RUSSIAN_NAME } from '../../../constants/constants'; import { SelectWrapper, SelectContainer, diff --git a/web/src/components/common/SwitchFormTask/SwitchFormTask.jsx b/web/src/components/common/SwitchFormTask/SwitchFormTask.jsx index b48d6d9..4e2eab0 100644 --- a/web/src/components/common/SwitchFormTask/SwitchFormTask.jsx +++ b/web/src/components/common/SwitchFormTask/SwitchFormTask.jsx @@ -1,5 +1,5 @@ import { useAuth } from '../../../app/context/AuthProvider'; -import { ROLES_NAME_ID } from '../../../constants'; +import { ROLES_NAME_ID } from '../../../constants/constants'; import { Switch, Buttons, StyledNavLink } from './SwitchFormTask.style'; export function SwitchFormTask() { diff --git a/web/src/components/common/TableCard/TableCard.jsx b/web/src/components/common/TableCard/TableCard.jsx index 367d163..1ddc649 100644 --- a/web/src/components/common/TableCard/TableCard.jsx +++ b/web/src/components/common/TableCard/TableCard.jsx @@ -1,4 +1,4 @@ -import { DIRECTION_TRANSLATE, FORM_TYPE_OPTIONS } from '../../../constants'; +import { DIRECTION_TRANSLATE, FORM_TYPE_OPTIONS } from '../../../constants/constants'; import { Section, SectionStartPart, diff --git a/web/src/components/common/TableList/TableList.jsx b/web/src/components/common/TableList/TableList.jsx index e0afa57..4dcb645 100644 --- a/web/src/components/common/TableList/TableList.jsx +++ b/web/src/components/common/TableList/TableList.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Box, Stack, Typography } from '@mui/material'; import { useNavigate } from 'react-router-dom'; import TableCard from '../TableCard/TableCard'; @@ -12,6 +12,9 @@ const TablesList = ({ formInfo }) => { const navigate = useNavigate(); + const isProject = useMemo(() => { + return !!projects; + }, [projects]) const handleNavigate = (path) => { navigate(path); @@ -22,7 +25,7 @@ const TablesList = ({ if (!formInfo){ return; } - const path = `${basePath}/form/${formInfo.id}/form-type/${formInfo.form_type_code}/${table.sheet}/${table?.direction}`; + const path = `${basePath}/form/${formInfo.id}/form-type/${isProject ? 'project' : formInfo.form_type_code}/${table.sheet}/${table?.direction}`; return ( { diff --git a/web/src/pages/DictVspPage/components/Modals/ModalEditAddVsp.jsx b/web/src/pages/DictVspPage/components/Modals/ModalEditAddVsp.jsx index 60d32ed..f08e7eb 100644 --- a/web/src/pages/DictVspPage/components/Modals/ModalEditAddVsp.jsx +++ b/web/src/pages/DictVspPage/components/Modals/ModalEditAddVsp.jsx @@ -14,7 +14,7 @@ import { PrimaryButton, } from '../../../../components/common/Buttons/Buttons'; import { useAuth } from '../../../../app/context/AuthProvider'; -import { ROLES_NAME_ID } from '../../../../constants'; +import { ROLES_NAME_ID } from '../../../../constants/constants'; import { ModalContainer, FieldContainer, diff --git a/web/src/pages/DictVspPage/components/TableDictVsp.jsx b/web/src/pages/DictVspPage/components/TableDictVsp.jsx index d1c4397..580b3b2 100644 --- a/web/src/pages/DictVspPage/components/TableDictVsp.jsx +++ b/web/src/pages/DictVspPage/components/TableDictVsp.jsx @@ -11,7 +11,7 @@ import { PrimaryOutlinedButton, } from '../../../components/common/Buttons/Buttons'; import { useAuth } from '../../../app/context/AuthProvider'; -import { ROLES_NAME_ID } from '../../../constants'; +import { ROLES_NAME_ID } from '../../../constants/constants'; import ModalExport from './Modals/ModalExport'; const TableDictVsp = ({ diff --git a/web/src/pages/NewFormTablePage.jsx b/web/src/pages/NewFormTablePage.jsx index e144ff0..ac51181 100644 --- a/web/src/pages/NewFormTablePage.jsx +++ b/web/src/pages/NewFormTablePage.jsx @@ -8,7 +8,7 @@ import { useParams } from 'react-router-dom'; import { RealtimeProvider } from '../components/RealtimeTable/contexts/RealtimeContext'; import { useAuth } from '../app/context/AuthProvider'; import { BackButton } from '../components/common/Buttons/BackButton'; -import { SHEET_NAME } from '../constants'; +import { SHEET_NAME } from '../constants/constants'; const TableInfo = ({ formId, sheetName }) => { const portalContent = ( diff --git a/web/src/pages/ProjectsPage/ModalCreateProject.jsx b/web/src/pages/ProjectsPage/ModalCreateProject.jsx index 5f7034a..20c3f94 100644 --- a/web/src/pages/ProjectsPage/ModalCreateProject.jsx +++ b/web/src/pages/ProjectsPage/ModalCreateProject.jsx @@ -1,4 +1,5 @@ -import { useState } from 'react'; +// src/components/common/ProjectCardComponent/ModalCreateProject.jsx +import { useState, useEffect } from 'react'; import { Box, Button, Divider, TextField, Typography } from '@mui/material'; import SelectWithOptions from '../AdminPanel/components/SelectWithOptions'; import Modal from '../../components/common/Modal/Modal'; @@ -9,25 +10,103 @@ export const ModalCreateProject = ({ onClose, onCreate, config = {}, + initialData = null, + isEdit = false, + projectId = null, }) => { const [projectData, setProjectData] = useState(() => { - const initialData = {}; + // Если есть initialData, используем её, иначе создаем из конфига + if (initialData) { + return { ...initialData }; + } + const initial = {}; Object.keys(config).forEach((key) => { - initialData[key] = config[key].defaultValue; + initial[key] = config[key].defaultValue; }); - return initialData; + return initial; }); + const [errors, setErrors] = useState({}); + + // Обновляем данные при изменении initialData + useEffect(() => { + if (initialData) { + setProjectData({ ...initialData }); + } else { + const initial = {}; + Object.keys(config).forEach((key) => { + initial[key] = config[key].defaultValue; + }); + setProjectData(initial); + } + setErrors({}); + }, [initialData, config]); + const handleChange = (field, value) => { setProjectData((prev) => ({ ...prev, [field]: value, })); + // Очищаем ошибку для поля при изменении + if (errors[field]) { + setErrors((prev) => ({ + ...prev, + [field]: undefined, + })); + } + }; + + const validateField = (fieldName, config) => { + if (config.required_field) { + const value = projectData[fieldName]; + + // Проверка для разных типов полей + if (config.type === 'multiselect') { + if (!value || (Array.isArray(value) && value.length === 0) || value === '') { + return `Поле "${fieldName}" обязательно для заполнения`; + } + } else if (config.type === 'text') { + if (!value || value.trim() === '') { + return `Поле "${fieldName}" обязательно для заполнения`; + } + } else if (config.type === 'number') { + if (value === null || value === undefined || value === '') { + return `Поле "${fieldName}" обязательно для заполнения`; + } + // Проверка на минимальное значение + if (config.min !== undefined && Number(value) < config.min) { + return `Значение должно быть не меньше ${config.min}`; + } + // Проверка на максимальное значение + if (config.max !== undefined && Number(value) > config.max) { + return `Значение должно быть не больше ${config.max}`; + } + } + } + return null; + }; + + const validateAllFields = () => { + const newErrors = {}; + let hasErrors = false; + + Object.entries(config).forEach(([fieldName, fieldConfig]) => { + const error = validateField(fieldName, fieldConfig); + if (error) { + newErrors[fieldName] = error; + hasErrors = true; + } + }); + + setErrors(newErrors); + return !hasErrors; }; const handleClickCreate = () => { - onCreate(projectData); - onClose(); + if (validateAllFields()) { + onCreate(projectData); + // Не закрываем модалку здесь, пусть это делает родительский компонент + } }; const handleClose = () => { @@ -36,10 +115,14 @@ export const ModalCreateProject = ({ resetData[key] = config[key].defaultValue; }); setProjectData(resetData); + setErrors({}); onClose(); }; const renderField = (fieldName, config) => { + const hasError = !!errors[fieldName]; + const errorText = errors[fieldName]; + switch (config.type) { case 'multiselect': return ( @@ -50,9 +133,13 @@ export const ModalCreateProject = ({ fontWeight: '400', fontSize: '0.75rem', paddingBottom: '.25rem', + color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)', }} > {fieldName} + {config.required_field && ( + * + )} handleChange(fieldName, value)} width="28.25rem" + style={{ + borderColor: hasError ? 'var(--error-color, #d32f2f)' : undefined, + borderWidth: hasError ? '2px' : undefined, + }} /> + {hasError && ( + + {errorText} + + )} ); @@ -73,11 +175,14 @@ export const ModalCreateProject = ({ sx={{ fontWeight: '400', fontSize: '0.75rem', - color: 'var(--grey-dark)', + color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)', paddingBottom: '.25rem', }} > {fieldName} + {config.required_field && ( + * + )} handleChange(fieldName, e.target.value)} placeholder={config.placeholder} size="small" + error={hasError} + helperText={hasError ? errorText : ''} + FormHelperTextProps={{ + sx: { + marginLeft: 0, + fontSize: '0.75rem', + }, + }} /> ); @@ -97,11 +210,14 @@ export const ModalCreateProject = ({ sx={{ fontWeight: '400', fontSize: '0.75rem', - color: 'var(--grey-dark)', + color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)', paddingBottom: '.25rem', }} > {fieldName} + {config.required_field && ( + * + )} ); @@ -136,7 +260,7 @@ export const ModalCreateProject = ({ return ( - Создать + {isEdit ? 'Сохранить' : 'Создать'} } @@ -180,4 +304,4 @@ export const ModalCreateProject = ({ ); -}; +}; \ No newline at end of file diff --git a/web/src/pages/ProjectsPage/ProjectCard.jsx b/web/src/pages/ProjectsPage/ProjectCard.jsx index 29f281c..15ed15c 100644 --- a/web/src/pages/ProjectsPage/ProjectCard.jsx +++ b/web/src/pages/ProjectsPage/ProjectCard.jsx @@ -25,6 +25,7 @@ import { } from '../../components/common/icons/icons'; import { IconWithContent } from '../../components/common/IconWithContent'; import { ExportButton } from '../../components/common/Buttons/ButtonsActions'; +import { ModalCreateProject } from './ModalCreateProject'; const ProjectCard = styled.div` width: 26.8rem; @@ -137,17 +138,9 @@ const getStatusLabel = (status) => { return statusMap[status] || status || 'Не указан'; }; -const getLevelLabel = (level) => { - const levelMap = { - project: 'Проект', - subproject: 'Подпроект', - task: 'Задача', - }; - return levelMap[level] || level || 'Не указан'; -}; - export const ProjectCardComponent = ({ project, + projectDataInit, exportMode = false, isLoadingFile = false, onAddForExport, @@ -172,22 +165,41 @@ export const ProjectCardComponent = ({ } = project; const [isEditModalOpen, setEditModalOpen] = useState(false); - const [editedName, setEditedName] = useState(''); const [currentName, setCurrentName] = useState(name || ''); const [isChecked, setIsChecked] = useState(false); + const [editFormData, setEditFormData] = useState({}); + const [projectData, setProjectData] = useState(projectDataInit); useEffect(() => { setIsChecked(false); }, [exportMode]); + + + // Функция для подготовки данных проекта к редактированию + const prepareProjectDataForEdit = () => { + return { + 'ССП/РФ': project.org_unit_id || null, + 'Год': year || null, + 'Тип проекта развития': project_type || null, + 'Название проекта': currentName, + 'Формат ВСП': vsp_format || null, + 'Адрес объекта': object_address || '', + 'Целевая численность, чел.': staff_count || 0, + 'Тип размещения': placement_type || null, + 'Площадь размещения, м2': total_area || 0, + }; + }; + const handleOpenEdit = (event) => { event.stopPropagation(); - setEditedName(currentName); + setEditFormData(prepareProjectDataForEdit()); setEditModalOpen(true); }; const handleCloseEdit = () => { setEditModalOpen(false); + setEditFormData({}); }; const handleCheckboxClick = (event) => { @@ -211,30 +223,39 @@ export const ProjectCardComponent = ({ } }; - const updateProject = async (nextName) => { + // Обновленная функция обновления проекта + const updateProject = async (formData) => { try { - await ProjectsApi.update(id, { name: nextName }); - setCurrentName(nextName); + const projectData = { + name: formData['Название проекта'] || currentName, + project_type: formData['Тип проекта развития'] || null, + vsp_format: formData['Формат ВСП'] || null, + object_address: formData['Адрес объекта'] || '', + staff_count: Number(formData['Целевая численность, чел.']) || 0, + placement_type: formData['Тип размещения'] || null, + total_area: Number(formData['Площадь размещения, м2']) || 0, + year: formData['Год'] ? Number(formData['Год']) : null, + branch_id: formData['ССП/РФ'] || null, + }; + + await ProjectsApi.update(id, projectData); + + // Обновляем локальные данные + setCurrentName(projectData.name); setEditModalOpen(false); - toast.success('Название проекта обновлено'); + toast.success('Данные проекта успешно обновлены'); + + // Можно добавить рефреш данных или перезагрузку страницы + window.location.reload(); // или используйте callback для обновления списка } catch (error) { toast.error( - error?.response?.data?.detail || 'Не удалось обновить название проекта', + error?.response?.data?.detail || 'Не удалось обновить данные проекта', ); } }; - const handleSaveName = async () => { - const nextName = editedName.trim(); - if (!nextName) { - toast.error('Название не может быть пустым'); - return; - } - if (nextName === currentName) { - setEditModalOpen(false); - return; - } - await updateProject(nextName); + const handleSaveProject = async (formData) => { + await updateProject(formData); }; const getInitials = (name) => { @@ -252,18 +273,21 @@ export const ProjectCardComponent = ({ ID: {id} - + {!exportMode && ( <> {status && ( - )} - - + {/* {Пока нет редактирование + экспорт} */} + {/* + */} )} @@ -272,18 +296,13 @@ export const ProjectCardComponent = ({ {currentName} - {level && ( - - )} {project_type && ( )} {report_count !== undefined && report_count !== null && ( )} - {parent_id && ( - - )} + @@ -302,18 +321,6 @@ export const ProjectCardComponent = ({ }> {org_unit_name || 'Организация не указана'} - {object_address && ( - - {object_address} - - )} - {(staff_count || total_area) && ( - - {staff_count && `👥 ${staff_count}`} - {staff_count && total_area && ' | '} - {total_area && `📐 ${total_area} м²`} - - )} {exportMode && ( @@ -327,6 +334,16 @@ export const ProjectCardComponent = ({ )} + + ); }; \ No newline at end of file diff --git a/web/src/pages/ProjectsPage/ProjectsPage.jsx b/web/src/pages/ProjectsPage/ProjectsPage.jsx index d4f59a0..29bd04a 100644 --- a/web/src/pages/ProjectsPage/ProjectsPage.jsx +++ b/web/src/pages/ProjectsPage/ProjectsPage.jsx @@ -1,4 +1,3 @@ -// src/pages/ProjectsPage/ProjectsPage.jsx import { useEffect, useMemo, useState } from 'react'; import { ProjectsApi } from '../../api/projects'; import { CardsSkeleton } from '../../components/common/Skeleton'; @@ -18,12 +17,14 @@ import { Typography, IconButton, Tooltip, + TextField, + Autocomplete, } from '@mui/material'; import { toast } from 'react-toastify'; import { ModalCreateProject } from './ModalCreateProject'; import { ROLES_NAME_ID, -} from '../../constants'; +} from '../../constants/constants'; import { useAuth } from '../../app/context/AuthProvider'; import { PrimaryButton } from '../../components/common/Buttons/Buttons'; import { @@ -34,79 +35,20 @@ import { exportBulkForms, exportSingleForm } from '../../utils/exportForm'; import { WhitePlus } from '../../components/common/icons/icons'; import { useNavigate } from 'react-router-dom'; import { PaginatedList } from '../../components/common/PaginatedList'; +import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig'; +import { SspApi } from '../../api/ssp'; - -// Конфигурация для создания проекта -const defaultProjectConfig = { - 'Тип проекта развития': { - type: 'multiselect', - options: [ - 'Открытие ВСП', - 'Закрытие ВСП', - 'Переезд ВСП', - 'Реновация РФ', - 'Открытие УРМ', - ], - placeholder: 'Выберите тип проекта', - defaultValue: null, - }, - 'Название проекта': { - type: 'text', - placeholder: 'Введите название проекта', - defaultValue: '', - }, - 'Формат ВСП': { - type: 'multiselect', - options: [ - 'Фланговый', - 'Типовой', - 'Розничный', - 'МСБ', - 'Лёгкий', - 'Мини', - 'Розничный-киоск', - 'МБО', - 'Офис самообслуживания', - 'Другое', - ], - placeholder: 'Выберите формат ВСП', - defaultValue: null, - }, - 'Адрес объекта': { - type: 'text', - placeholder: 'Введите адрес объекта', - defaultValue: '', - }, - 'Целевая численность, чел.': { - type: 'number', - placeholder: 'Введите число', - defaultValue: 0, - min: 0, - step: 1, - }, - 'Тип размещения': { - type: 'multiselect', - options: ['Собственность', 'Аренда', 'Субаренда'], - placeholder: 'Выберите тип размещения', - defaultValue: null, - }, - 'Площадь размещения, м2': { - type: 'number', - placeholder: 'Введите число', - defaultValue: 0, - min: 0, - step: 1, - }, -}; +export { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig'; export default function ProjectsPage() { const navigate = useNavigate(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); - const [query, setQuery] = useState(''); - const [statusFilter, setStatusFilter] = useState(''); - const [levelFilter, setLevelFilter] = useState(''); + + // Новые фильтры + const [yearFilter, setYearFilter] = useState(''); + const [branchFilter, setBranchFilter] = useState(''); // Pagination state const [page, setPage] = useState(1); @@ -118,11 +60,47 @@ export default function ProjectsPage() { const [exportMode, setExportMode] = useState(false); const [exportList, setExportList] = useState([]); + const [projectData, setProjectData] = useState(DEFAULT_PROJECT_CONFIG); + + // Получаем список годов для фильтра (можно динамически или задать диапазон) + const yearOptions = useMemo(() => { + const currentYear = new Date().getFullYear(); + const years = []; + for (let year = currentYear - 5; year <= currentYear + 2; year++) { + years.push(year); + } + return years; + }, []); + + useEffect(() => { + if (!user) return; + if (user.role_id == ROLES_NAME_ID.admin) { + SspApi.getAll().then(data => { + if (data.success) { + setProjectData(prev => ({ + ...prev, 'ССП/РФ': { + ...prev['ССП/РФ'], + options: data.result + } + })) + return; + } + toast.error('Ошибка при получении ССП/РФ'); + }); + return; + } + setProjectData(prev => ({ + ...prev, 'ССП/РФ': { + ...prev['ССП/РФ'], + options: user.org_units, + } + })) + }, [user]) + const openModal = () => { setModalOpen(true); }; - // Маппинг полей из модалки в API const mapFormDataToApi = (formData) => { const apiData = { name: formData['Название проекта'] || '', @@ -132,12 +110,9 @@ export default function ProjectsPage() { staff_count: formData['Целевая численность, чел.'] || 0, placement_type: formData['Тип размещения'] || null, total_area: formData['Площадь размещения, м2'] || 0, - // Добавляем значения по умолчанию для обязательных полей - year: new Date().getFullYear(), - org_unit_id: user?.org_unit_id || null, - form_type_code: 'project', // или другое значение по умолчанию - status: 'active', - level: 'project', + year: formData['Год'] || null, + branch_id: formData['ССП/РФ'] || null, + }; return apiData; }; @@ -169,6 +144,9 @@ export default function ProjectsPage() { const params = { offset: skip, limit: currentLimit, + // Добавляем фильтры в параметры запроса + ...(yearFilter && { year: parseInt(yearFilter) }), + ...(branchFilter && { branch_id: parseInt(branchFilter.id) }), }; const res = await ProjectsApi.list(params); @@ -184,10 +162,11 @@ export default function ProjectsPage() { } }; + // Сбрасываем страницу при изменении любых фильтров useEffect(() => { setPage(1); loadProjects(1, limit); - }, [query, statusFilter, levelFilter]); + }, [yearFilter, branchFilter]); const handlePageChange = (event, value) => { setPage(value); @@ -201,6 +180,13 @@ export default function ProjectsPage() { loadProjects(1, newLimit); }; + // Сброс всех фильтров + const handleClearFilters = () => { + setYearFilter(''); + setBranchFilter(''); + setPage(1); + }; + useEffect(() => { loadProjects(page, limit); }, []); @@ -253,6 +239,68 @@ export default function ProjectsPage() { + {/* Фильтры */} + + + {/* Фильтр по году */} + + + + + {/* Фильтр по ССП/РФ */} + + option.title || option.name || '-'} + value={ + branchFilter + } + onChange={(event, newValue) => { + setBranchFilter(newValue); + }} + sx={{ height: '2.5rem', minWidth: '21.25rem' }} + renderInput={(params) => ( + + )} + /> + + + + )} @@ -286,7 +335,7 @@ export default function ProjectsPage() { open={modalOpen} onClose={() => setModalOpen(false)} onCreate={handleCreateProject} - config={defaultProjectConfig} + config={projectData} /> ); diff --git a/web/src/pages/TablePage.jsx b/web/src/pages/TablePage.jsx index d678eed..4540c9e 100644 --- a/web/src/pages/TablePage.jsx +++ b/web/src/pages/TablePage.jsx @@ -57,7 +57,7 @@ export default function TablePage() { const connection = connectToTable(tableId, user?.id, { onError: () => { - toast.error('Ошибка подключения к серверу реального времени'); + toast.error('Ошибка подключения к серверу'); }, onClose: (event) => { if (isUnmountingRef.current) { diff --git a/web/src/pages/TaskPage/TaskPage.jsx b/web/src/pages/TaskPage/TaskPage.jsx index 62b22e0..9dfdfd9 100644 --- a/web/src/pages/TaskPage/TaskPage.jsx +++ b/web/src/pages/TaskPage/TaskPage.jsx @@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'; import { useParams, useNavigate } from 'react-router-dom'; import { TasksApi } from '../../api/tasks'; import { TablesApi } from '../../api/tables'; +import { ProjectsApi } from '../../api/projects'; // Добавьте импорт API для проектов import { TaskInfoContainer, NameTask, @@ -21,13 +22,13 @@ import { FormsApi } from '../../api/forms'; import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; import { exportSingleForm } from '../../utils/exportForm'; import TablesList from '../../components/common/TableList/TableList'; -import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants'; +import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants/constants'; -const TaskInfo = ({ task }) => { +const TaskInfo = ({ task, isProject }) => { const portalContent = ( - - {FORM_TYPE_TRANSLATE[task.form_type_code] || ''} + + {isProject ? 'Проект' : FORM_TYPE_TRANSLATE[task.form_type_code] || ''} ); @@ -39,9 +40,13 @@ const TaskInfo = ({ task }) => { }; export default function TaskPage() { - const { taskId } = useParams(); + const { taskId, projectId } = useParams(); const navigate = useNavigate(); - const id = Number(taskId); + + // Определяем, что у нас за ID и какой тип + const isProject = !!projectId; + const id = isProject ? Number(projectId) : Number(taskId); + const [task, setTask] = useState(null); const [tables, setTables] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -50,23 +55,42 @@ export default function TaskPage() { const [modalProjectOpen, setModalProjectOpen] = useState(false); const [tempProjects, setTempProjects] = useState([]); const [projects, setProjects] = useState([]); + const [isProjectData, setIsProjectData] = useState(false); useEffect(() => { - const getSheets = async () => { + const getData = async () => { setIsLoading(true); try { - const data = await TasksApi.getSheets(id); - // Преобразуем данные с учетом direction - const mappedTables = data.result.all_sheets.map(sheet => ({ - name: SHEET_NAME[sheet.sheet_name] || sheet.sheet_name, - sheet: sheet.sheet_name, - direction: sheet.direction, - })); - //пока скрыт - setTables(mappedTables.filter(t => t.sheet !== "OTCH9F",)); + if (isProject) { + // Получаем информацию о проекте + const projectData = await ProjectsApi.get(id); + setTask(projectData.result); + setIsProjectData(true); + console.log(projectData.result.reports); + + const mappedTables = projectData.result.reports.map(sheet => ({ + name: SHEET_NAME[sheet.report_type] || sheet.report_type, + sheet: sheet.report_type, + })); + setTables(mappedTables); + } else { + // Получаем информацию о задаче + const taskData = await TasksApi.getById(id); + setTask(taskData.result); + setIsProjectData(false); + + // Получаем таблицы задачи + const tablesData = await TasksApi.getSheets(id); + const mappedTables = tablesData.result.all_sheets.map(sheet => ({ + name: SHEET_NAME[sheet.sheet_name] || sheet.sheet_name, + sheet: sheet.sheet_name, + direction: sheet.direction, + })); + setTables(mappedTables.filter(t => t.sheet !== "OTCH9F")); + } } catch (error) { toast.error( - error?.response?.data?.detail || 'Ошибка получения данных задачи', + error?.response?.data?.detail || `Ошибка получения данных ${isProject ? 'проекта' : 'задачи'}`, ); } finally { setIsLoading(false); @@ -74,27 +98,11 @@ export default function TaskPage() { }; if (Number.isFinite(id)) { - getSheets(); + getData(); } - }, [id]); + }, [id, isProject]); - useEffect(() => { - const getFormInfo = async () => { - setIsLoading(true); - try { - const data = await TasksApi.getById(id); - setTask(data.result); - } catch (error) { - // обработка ошибки - } finally { - setIsLoading(false); - } - }; - - if (Number.isFinite(id)) { - getFormInfo(); - } - }, [id]); + // Объединили два useEffect в один const handleCreateProject = (projectData) => { // логика создания проекта @@ -106,6 +114,14 @@ export default function TaskPage() { return searchString.includes(query.toLowerCase()); }); + // Функция для экспорта + const handleExport = () => { + const fileName = isProject + ? `project_${task?.title || id}` + : `form_${task?.title || taskId}`; + exportSingleForm(id, fileName); + }; + if (isLoading) { return (
@@ -116,7 +132,7 @@ export default function TaskPage() { return ( <> - {task && } + {task && } - exportSingleForm(taskId, task?.title ?? 'form')} - /> + setModalStageOpen(true)} @@ -155,9 +169,10 @@ export default function TaskPage() { setModalStageOpen(false)} - taskId={taskId} - mode="task" - formTypeCode={task?.form_type_code || ''} + taskId={isProject ? undefined : taskId} + projectId={isProject ? projectId : undefined} + mode={isProject ? "project" : "task"} + formTypeCode={ task?.form_type_code || ''} sheets={tables} /> @@ -168,8 +183,9 @@ export default function TaskPage() { query={query} projects={projects} filteredTables={filteredTables} - basePath="/table" + basePath={"/table"} formInfo={task} + isProject={isProject} /> diff --git a/web/src/pages/TasksPage/TasksPage.jsx b/web/src/pages/TasksPage/TasksPage.jsx index aee48c2..f9bb40a 100644 --- a/web/src/pages/TasksPage/TasksPage.jsx +++ b/web/src/pages/TasksPage/TasksPage.jsx @@ -27,7 +27,7 @@ import { ORG_UNIT_TYPE_OPTIONS, ROLES_NAME_ID, FORM_TYPE_TRANSLATE -} from '../../constants'; +} from '../../constants/constants'; import { useAuth } from '../../app/context/AuthProvider'; import { SspApi } from '../../api/ssp'; import { PrimaryButton } from '../../components/common/Buttons/Buttons'; @@ -256,7 +256,6 @@ export default function TasksPage() { <> -