diff --git a/web/src/components/Stages/constants.js b/web/src/components/Stages/constants.js index 451ce13..c62a3d6 100644 --- a/web/src/components/Stages/constants.js +++ b/web/src/components/Stages/constants.js @@ -8,6 +8,7 @@ export const stageColumnsHiddenFromPicker = [ 'data.header.name', 'data.header.internal_order', 'data.header.vsp_id', + 'data.header.vsp_address', 'data.header.section_code', // 'data.header.justification', ]; diff --git a/web/src/pages/AdminPanel/AuditLogsPage/components/AuditLogsTable.jsx b/web/src/pages/AdminPanel/AuditLogsPage/components/AuditLogsTable.jsx index 52591ac..017521b 100644 --- a/web/src/pages/AdminPanel/AuditLogsPage/components/AuditLogsTable.jsx +++ b/web/src/pages/AdminPanel/AuditLogsPage/components/AuditLogsTable.jsx @@ -2,7 +2,7 @@ import { Box, Chip, Link, Typography } from '@mui/material'; import { useMemo, useState } from 'react'; import { ROLES_ID_RUSSIAN_NAME } from '../../../../constants/constants'; import StyledTable from '../../components/StyledTable/StyledTable'; -import { ENTITY_RUSSIAN_NAMES, getRussianNamesForType } from '../constants/auditLogs'; +import { getRussianNamesForType } from '../constants/auditLogs'; import JsonViewModal from './JsonViewModal'; const formatDateTime = (dateString) => { @@ -102,20 +102,6 @@ const AuditLogsTable = ({ logs, loading, totalCount, page, rowsPerPage, onPageCh enableColumnActions: false, enableSorting: false, }, - { - accessorKey: 'context', - header: 'Контекст', - size: 160, - Cell: ({ row }) => ( - - {ENTITY_RUSSIAN_NAMES[row.original.entity?.toUpperCase()]?.[1] != null - ? `${ENTITY_RUSSIAN_NAMES[row.original.entity?.toUpperCase()]?.[1]} #${row.original.entity_id}` - : '-'} - - ), - enableColumnActions: false, - enableSorting: false, - }, { accessorKey: 'payload_json', header: 'Детали', diff --git a/web/src/pages/AdminPanel/AuditLogsPage/components/JsonViewModal.jsx b/web/src/pages/AdminPanel/AuditLogsPage/components/JsonViewModal.jsx index c770bf6..8b600a7 100644 --- a/web/src/pages/AdminPanel/AuditLogsPage/components/JsonViewModal.jsx +++ b/web/src/pages/AdminPanel/AuditLogsPage/components/JsonViewModal.jsx @@ -44,7 +44,7 @@ const JsonViewModal = ({ open, onClose, jsonData, eventType }) => { const configKey = config[currentPath] || config[key]; // Применяем преобразование, если есть функция для этого ключа - if (VALUE_TRANSFORMERS[configKey] && value) { + if (VALUE_TRANSFORMERS[configKey] && value !== null && value !== undefined) { try { // Собираем зависимости для трансформера // По умолчанию передаём только текущее значение @@ -70,7 +70,7 @@ const JsonViewModal = ({ open, onClose, jsonData, eventType }) => { } } - result[configKey] = value || '—'; + result[configKey] = value ?? '—'; } // Если значение - объект или массив, рекурсивно обходим его diff --git a/web/src/pages/AdminPanel/AuditLogsPage/constants/auditLogs.jsx b/web/src/pages/AdminPanel/AuditLogsPage/constants/auditLogs.jsx index 7ef77cb..c8428b1 100644 --- a/web/src/pages/AdminPanel/AuditLogsPage/constants/auditLogs.jsx +++ b/web/src/pages/AdminPanel/AuditLogsPage/constants/auditLogs.jsx @@ -14,7 +14,14 @@ export const DEFAULT_ACTIONS = { VSP_UPDATE: 'Обновление ВСП', DELETE_VSP: 'Удаление ВСП', PROJECT_CREATE: 'Создание проекта', + PROJECT_UPDATE: 'Обновление проекта', PROJECT_DELETE: 'Удаление проекта', + PROJECT_YEAR_ADD: 'Добавление года для проекта', + SMETA_UPDATE: 'Обновление сметы проекта', +}; + +const ACTION_NAME_ALIASES = { + UPDATE_SMETA: 'Обновление сметы проекта', }; export const ENTITY_RUSSIAN_NAMES = { @@ -46,6 +53,7 @@ export const ACTION_RUSSIAN_NAMES = { export const getRussianNamesForType = (name) => { if (!name) return name; + if (DEFAULT_ACTIONS[name] || ACTION_NAME_ALIASES[name]) return DEFAULT_ACTIONS[name] || ACTION_NAME_ALIASES[name]; let entityTranslation = ''; diff --git a/web/src/pages/AdminPanel/AuditLogsPage/constants/configEvent.jsx b/web/src/pages/AdminPanel/AuditLogsPage/constants/configEvent.jsx index ff41343..49f4069 100644 --- a/web/src/pages/AdminPanel/AuditLogsPage/constants/configEvent.jsx +++ b/web/src/pages/AdminPanel/AuditLogsPage/constants/configEvent.jsx @@ -71,15 +71,60 @@ export const EVENT_CONFIGS = { direction: 'Направление', }, PROJECT_CREATE: { + project_id: 'Проект', name: 'Название', year: 'Год', + level: 'Уровень проекта', + parent_id: 'ID родительского проекта', org_unit_id: 'СПП/РФ', + project_type: 'Тип проекта развития', + vsp_format: 'Формат ВСП', + placement_type: 'Тип размещения', + object_address: 'Адрес объекта', + system_code: 'Системный код', + technical_number: 'Технический номер проекта', + staff_count: 'Целевая численность, чел.', + total_area: 'Площадь размещения, м²', + limit_report_id: 'ID отчёта «Лимит»', + current_expenses_report_id: 'ID отчёта «Текущие расходы»', + }, + PROJECT_UPDATE: { + project_id: 'Проект', + 'changes.name': 'Название', + 'changes.status': 'Статус проекта', + 'changes.project_type': 'Тип проекта развития', + 'changes.vsp_format': 'Формат ВСП', + 'changes.placement_type': 'Тип размещения', + 'changes.object_address': 'Адрес объекта', + 'changes.staff_count': 'Целевая численность, чел.', + 'changes.total_area': 'Площадь размещения, м²', + 'changes.org_unit_id': 'ССП/РФ проекта', + 'changes.krf_decision_date': 'Дата решения КРФ', + 'changes.fk_decision_date': 'Дата решения ФК', + 'changes.board_decision_date': 'Дата решения Правления', + 'changes.open_relocate_close_date': 'Дата открытия / переезда / закрытия', + 'changes.funding_by_ko_decision': 'Финансирование по решению КО', }, PROJECT_DELETE: { name: 'Название', year: 'Год', org_unit_id: 'СПП/РФ', }, + PROJECT_YEAR_ADD: { + project_id: 'Проект', + year: 'Год', + }, + + SMETA_UPDATE: { + project_id: 'Проект', + 'changes.is_in_plan': 'В базовой смете', + 'changes.is_in_plan_q2': 'В скорректированной смете 2 кв.', + 'changes.is_in_plan_q3': 'В скорректированной смете 3 кв.', + 'changes.is_in_plan_q4': 'В скорректированной смете 4 кв.', + 'changes.development_block': 'Блок развития', + 'changes.reserve_to_prrs_ahr': 'Из резерва Банка в смету ПРРС АХР', + 'changes.reserve_to_prrs_kv': 'Из резерва Банка в смету ПРРС КВ', + }, ACCESS_EXTEND: { form_id: 'Информация о задаче', closes_at_after: 'Дата закрытия (новая)', diff --git a/web/src/pages/AdminPanel/AuditLogsPage/utils/transformers.jsx b/web/src/pages/AdminPanel/AuditLogsPage/utils/transformers.jsx index f0e43ad..bd1fd52 100644 --- a/web/src/pages/AdminPanel/AuditLogsPage/utils/transformers.jsx +++ b/web/src/pages/AdminPanel/AuditLogsPage/utils/transformers.jsx @@ -1,8 +1,10 @@ import { DictVspApi } from '../../../../api/dict-vsp'; import { FormsSheetApi } from '../../../../api/form_sheet'; import { FormsApi } from '../../../../api/forms'; +import { ProjectsApi } from '../../../../api/projects'; import { SspApi } from '../../../../api/ssp'; import { UsersApi } from '../../../../api/users'; +import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../../ProjectPages/constants'; import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, @@ -28,8 +30,115 @@ export const VALUE_TRANSFORMERS = { 'Колонки в этапе': transformColumns, Форма: transformFormType, ВСП: transformVSP, + Проект: transformProject, + 'В базовой смете': transformBooleanChange, + 'В скорректированной смете 2 кв.': transformBooleanChange, + 'В скорректированной смете 3 кв.': transformBooleanChange, + 'В скорректированной смете 4 кв.': transformBooleanChange, + 'Блок развития': transformDevelopmentBlockChange, + 'Из резерва Банка в смету ПРРС АХР': transformNumberChange, + 'Из резерва Банка в смету ПРРС КВ': transformNumberChange, + 'Целевая численность, чел.': transformNumberChange, + 'Площадь размещения, м²': transformNumberChange, + 'Статус проекта': transformProjectStatusChange, + 'Тип размещения': transformPlacementTypeChange, + 'Финансирование по решению КО': transformFundingChange, + 'ССП/РФ проекта': transformSspChange, + 'Дата решения КРФ': transformDateChange, + 'Дата решения ФК': transformDateChange, + 'Дата решения Правления': transformDateChange, + 'Дата открытия / переезда / закрытия': transformDateChange, }; +const PROJECT_STATUSES = { + created: 'Создан', + agreed: 'Согласован', + approved: 'Согласован', + archived: 'В архиве', + deleted: 'Удалён', +}; + +const PLACEMENT_TYPES = { + own: 'Собственность', + rent: 'Аренда', + other: 'Субаренда', +}; + +function transformChange(value, transformer) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, transformer(item)])); + } + return transformer(value); +} + +export function transformBooleanChange(value) { + return transformChange(value, (item) => { + if (item === null || item === undefined) return '—'; + if (item === true || item === 'true') return 'Да'; + if (item === false || item === 'false') return 'Нет'; + return item; + }); +} + +export function transformDevelopmentBlockChange(value) { + return transformChange(value, (item) => { + if (item === null || item === undefined) return '—'; + return DEVELOMENT_BLOCK.find(([key]) => key === item)?.[1] || item; + }); +} + +export function transformNumberChange(value) { + return transformChange(value, (item) => { + if (item === null || item === undefined || item === '') return '—'; + const number = Number(item); + return Number.isNaN(number) ? item : number.toLocaleString('ru-RU'); + }); +} + +function transformDictionaryChange(value, dictionary) { + return transformChange(value, (item) => { + if (item === null || item === undefined) return '—'; + return dictionary[item] || item; + }); +} + +export function transformProjectStatusChange(value) { + return transformDictionaryChange(value, PROJECT_STATUSES); +} + +export function transformPlacementTypeChange(value) { + return transformDictionaryChange(value, PLACEMENT_TYPES); +} + +export function transformFundingChange(value) { + return transformDictionaryChange(value, Object.fromEntries(FUNDING_BY_DECISION)); +} + +export async function transformSspChange(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return transformSsp(value); + const entries = await Promise.all( + Object.entries(value).map(async ([key, item]) => [key, item === null || item === undefined ? '—' : await transformSsp(item)]), + ); + return Object.fromEntries(entries); +} + +export function transformDateChange(value) { + return transformChange(value, (item) => { + if (item === null || item === undefined || item === '') return '—'; + const [year, month, day] = String(item).split('-'); + return year && month && day ? `${day}.${month}.${year}` : item; + }); +} + +export async function transformProject(projectId) { + try { + const project = await ProjectsApi.get(projectId); + return project?.result?.name || `Проект #${projectId}`; + } catch (error) { + return `Проект #${projectId}`; + } +} + export function transformRole(value) { return ROLES_ID_RUSSIAN_NAME[value] || 'Роль не найдена'; } diff --git a/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx b/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx index d679569..ec2948c 100644 --- a/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx +++ b/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx @@ -19,7 +19,15 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa const handleChange = (field) => (event) => { let value = event.target.value; if (field.valueType === 'boolean') value = value === '' ? null : value === 'true'; - if (field.valueType === 'number') value = value === '' ? '' : Number(value); + if (field.valueType === 'number') { + if (value === '') { + value = ''; + } else { + const numberValue = Number(value); + if (Number.isNaN(numberValue) || (field.min !== undefined && numberValue < field.min)) return; + value = numberValue; + } + } const nextFormData = { ...formData, @@ -51,8 +59,8 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa { key: 'project_type', label: 'Тип проекта', options: ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'] }, { key: 'vsp_format', label: 'Формат ВСП', options: ['Флагманский', 'Типовой', 'Розничный', 'МСБ', 'Лёгкий', 'Мини', 'Розничный-киоск', 'МБО', 'Офис самообслуживания', 'Другое'] }, { key: 'placement_type', label: 'Размещение', options: ['Собственность', 'Аренда', 'Субаренда'] }, - { key: 'staff_count', label: 'Количество сотрудников', type: 'number', valueType: 'number' }, - { key: 'total_area', label: 'Общая площадь', type: 'number', valueType: 'number' }, + { key: 'staff_count', label: 'Количество сотрудников', type: 'number', valueType: 'number', min: 0 }, + { key: 'total_area', label: 'Общая площадь', type: 'number', valueType: 'number', min: 0 }, { key: 'funding_by_ko_decision', label: 'Финансирование по решению КО', options: FUNDING_BY_DECISION }, { key: 'krf_decision_date', label: 'Дата решения КРФ', type: 'date' }, { key: 'fk_decision_date', label: 'Дата решения ФК', type: 'date' }, @@ -115,6 +123,7 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa select={Boolean(field.options)} disabled={isFieldDisabled || isSaving} InputLabelProps={field.type === 'date' ? { shrink: true } : undefined} + slotProps={field.type === 'number' ? { htmlInput: { min: field.min } } : undefined} sx={{ '& .MuiOutlinedInput-root': { bgcolor: 'transparent', diff --git a/web/src/pages/ProjectsPage/ModalCreateProject.jsx b/web/src/pages/ProjectsPage/ModalCreateProject.jsx index f84bfba..47c1ce6 100644 --- a/web/src/pages/ProjectsPage/ModalCreateProject.jsx +++ b/web/src/pages/ProjectsPage/ModalCreateProject.jsx @@ -54,9 +54,9 @@ export const ModalCreateProject = ({ }; const validateField = (fieldName, config) => { - if (config.required_field) { - const value = projectData[fieldName]; + const value = projectData[fieldName]; + if (config.required_field) { if (config.type === 'multiselect') { if (!value || (Array.isArray(value) && value.length === 0) || value === '') { return `Поле "${fieldName}" обязательно для заполнения`; @@ -69,12 +69,15 @@ export const ModalCreateProject = ({ 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}`; - } + } + } + + if (config.type === 'number' && value !== null && value !== undefined && value !== '') { + if (config.min !== undefined && Number(value) < config.min) { + return `Значение должно быть не меньше ${config.min}`; + } + if (config.max !== undefined && Number(value) > config.max) { + return `Значение должно быть не больше ${config.max}`; } } return null;