add formatted project logs

This commit is contained in:
PotapovaA 2026-08-20 17:25:18 +03:00
parent 36a534f06f
commit 3b08e4bd59
8 changed files with 189 additions and 28 deletions

View File

@ -8,6 +8,7 @@ export const stageColumnsHiddenFromPicker = [
'data.header.name', 'data.header.name',
'data.header.internal_order', 'data.header.internal_order',
'data.header.vsp_id', 'data.header.vsp_id',
'data.header.vsp_address',
'data.header.section_code', 'data.header.section_code',
// 'data.header.justification', // 'data.header.justification',
]; ];

View File

@ -2,7 +2,7 @@ import { Box, Chip, Link, Typography } from '@mui/material';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { ROLES_ID_RUSSIAN_NAME } from '../../../../constants/constants'; import { ROLES_ID_RUSSIAN_NAME } from '../../../../constants/constants';
import StyledTable from '../../components/StyledTable/StyledTable'; import StyledTable from '../../components/StyledTable/StyledTable';
import { ENTITY_RUSSIAN_NAMES, getRussianNamesForType } from '../constants/auditLogs'; import { getRussianNamesForType } from '../constants/auditLogs';
import JsonViewModal from './JsonViewModal'; import JsonViewModal from './JsonViewModal';
const formatDateTime = (dateString) => { const formatDateTime = (dateString) => {
@ -102,20 +102,6 @@ const AuditLogsTable = ({ logs, loading, totalCount, page, rowsPerPage, onPageCh
enableColumnActions: false, enableColumnActions: false,
enableSorting: false, enableSorting: false,
}, },
{
accessorKey: 'context',
header: 'Контекст',
size: 160,
Cell: ({ row }) => (
<Typography>
{ENTITY_RUSSIAN_NAMES[row.original.entity?.toUpperCase()]?.[1] != null
? `${ENTITY_RUSSIAN_NAMES[row.original.entity?.toUpperCase()]?.[1]} #${row.original.entity_id}`
: '-'}
</Typography>
),
enableColumnActions: false,
enableSorting: false,
},
{ {
accessorKey: 'payload_json', accessorKey: 'payload_json',
header: 'Детали', header: 'Детали',

View File

@ -44,7 +44,7 @@ const JsonViewModal = ({ open, onClose, jsonData, eventType }) => {
const configKey = config[currentPath] || config[key]; const configKey = config[currentPath] || config[key];
// Применяем преобразование, если есть функция для этого ключа // Применяем преобразование, если есть функция для этого ключа
if (VALUE_TRANSFORMERS[configKey] && value) { if (VALUE_TRANSFORMERS[configKey] && value !== null && value !== undefined) {
try { try {
// Собираем зависимости для трансформера // Собираем зависимости для трансформера
// По умолчанию передаём только текущее значение // По умолчанию передаём только текущее значение
@ -70,7 +70,7 @@ const JsonViewModal = ({ open, onClose, jsonData, eventType }) => {
} }
} }
result[configKey] = value || '—'; result[configKey] = value ?? '—';
} }
// Если значение - объект или массив, рекурсивно обходим его // Если значение - объект или массив, рекурсивно обходим его

View File

@ -14,7 +14,14 @@ export const DEFAULT_ACTIONS = {
VSP_UPDATE: 'Обновление ВСП', VSP_UPDATE: 'Обновление ВСП',
DELETE_VSP: 'Удаление ВСП', DELETE_VSP: 'Удаление ВСП',
PROJECT_CREATE: 'Создание проекта', PROJECT_CREATE: 'Создание проекта',
PROJECT_UPDATE: 'Обновление проекта',
PROJECT_DELETE: 'Удаление проекта', PROJECT_DELETE: 'Удаление проекта',
PROJECT_YEAR_ADD: 'Добавление года для проекта',
SMETA_UPDATE: 'Обновление сметы проекта',
};
const ACTION_NAME_ALIASES = {
UPDATE_SMETA: 'Обновление сметы проекта',
}; };
export const ENTITY_RUSSIAN_NAMES = { export const ENTITY_RUSSIAN_NAMES = {
@ -46,6 +53,7 @@ export const ACTION_RUSSIAN_NAMES = {
export const getRussianNamesForType = (name) => { export const getRussianNamesForType = (name) => {
if (!name) return name; if (!name) return name;
if (DEFAULT_ACTIONS[name] || ACTION_NAME_ALIASES[name]) return DEFAULT_ACTIONS[name] || ACTION_NAME_ALIASES[name];
let entityTranslation = ''; let entityTranslation = '';

View File

@ -71,15 +71,60 @@ export const EVENT_CONFIGS = {
direction: 'Направление', direction: 'Направление',
}, },
PROJECT_CREATE: { PROJECT_CREATE: {
project_id: 'Проект',
name: 'Название', name: 'Название',
year: 'Год', year: 'Год',
level: 'Уровень проекта',
parent_id: 'ID родительского проекта',
org_unit_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: { PROJECT_DELETE: {
name: 'Название', name: 'Название',
year: 'Год', year: 'Год',
org_unit_id: 'СПП/РФ', 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: { ACCESS_EXTEND: {
form_id: 'Информация о задаче', form_id: 'Информация о задаче',
closes_at_after: 'Дата закрытия (новая)', closes_at_after: 'Дата закрытия (новая)',

View File

@ -1,8 +1,10 @@
import { DictVspApi } from '../../../../api/dict-vsp'; import { DictVspApi } from '../../../../api/dict-vsp';
import { FormsSheetApi } from '../../../../api/form_sheet'; import { FormsSheetApi } from '../../../../api/form_sheet';
import { FormsApi } from '../../../../api/forms'; import { FormsApi } from '../../../../api/forms';
import { ProjectsApi } from '../../../../api/projects';
import { SspApi } from '../../../../api/ssp'; import { SspApi } from '../../../../api/ssp';
import { UsersApi } from '../../../../api/users'; import { UsersApi } from '../../../../api/users';
import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../../ProjectPages/constants';
import { import {
DIRECTION_TRANSLATE, DIRECTION_TRANSLATE,
FORM_TYPE_TRANSLATE, FORM_TYPE_TRANSLATE,
@ -28,8 +30,115 @@ export const VALUE_TRANSFORMERS = {
'Колонки в этапе': transformColumns, 'Колонки в этапе': transformColumns,
Форма: transformFormType, Форма: transformFormType,
ВСП: transformVSP, ВСП: 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) { export function transformRole(value) {
return ROLES_ID_RUSSIAN_NAME[value] || 'Роль не найдена'; return ROLES_ID_RUSSIAN_NAME[value] || 'Роль не найдена';
} }

View File

@ -19,7 +19,15 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa
const handleChange = (field) => (event) => { const handleChange = (field) => (event) => {
let value = event.target.value; let value = event.target.value;
if (field.valueType === 'boolean') value = value === '' ? null : value === 'true'; 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 = { const nextFormData = {
...formData, ...formData,
@ -51,8 +59,8 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa
{ key: 'project_type', label: 'Тип проекта', options: ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'] }, { key: 'project_type', label: 'Тип проекта', options: ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'] },
{ key: 'vsp_format', label: 'Формат ВСП', options: ['Флагманский', 'Типовой', 'Розничный', 'МСБ', 'Лёгкий', 'Мини', 'Розничный-киоск', 'МБО', 'Офис самообслуживания', 'Другое'] }, { key: 'vsp_format', label: 'Формат ВСП', options: ['Флагманский', 'Типовой', 'Розничный', 'МСБ', 'Лёгкий', 'Мини', 'Розничный-киоск', 'МБО', 'Офис самообслуживания', 'Другое'] },
{ key: 'placement_type', label: 'Размещение', options: ['Собственность', 'Аренда', 'Субаренда'] }, { key: 'placement_type', label: 'Размещение', options: ['Собственность', 'Аренда', 'Субаренда'] },
{ key: 'staff_count', label: 'Количество сотрудников', type: 'number', valueType: 'number' }, { key: 'staff_count', label: 'Количество сотрудников', type: 'number', valueType: 'number', min: 0 },
{ key: 'total_area', label: 'Общая площадь', type: 'number', valueType: 'number' }, { key: 'total_area', label: 'Общая площадь', type: 'number', valueType: 'number', min: 0 },
{ key: 'funding_by_ko_decision', label: 'Финансирование по решению КО', options: FUNDING_BY_DECISION }, { key: 'funding_by_ko_decision', label: 'Финансирование по решению КО', options: FUNDING_BY_DECISION },
{ key: 'krf_decision_date', label: 'Дата решения КРФ', type: 'date' }, { key: 'krf_decision_date', label: 'Дата решения КРФ', type: 'date' },
{ key: 'fk_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)} select={Boolean(field.options)}
disabled={isFieldDisabled || isSaving} disabled={isFieldDisabled || isSaving}
InputLabelProps={field.type === 'date' ? { shrink: true } : undefined} InputLabelProps={field.type === 'date' ? { shrink: true } : undefined}
slotProps={field.type === 'number' ? { htmlInput: { min: field.min } } : undefined}
sx={{ sx={{
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
bgcolor: 'transparent', bgcolor: 'transparent',

View File

@ -54,9 +54,9 @@ export const ModalCreateProject = ({
}; };
const validateField = (fieldName, config) => { 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 (config.type === 'multiselect') {
if (!value || (Array.isArray(value) && value.length === 0) || value === '') { if (!value || (Array.isArray(value) && value.length === 0) || value === '') {
return `Поле "${fieldName}" обязательно для заполнения`; return `Поле "${fieldName}" обязательно для заполнения`;
@ -69,6 +69,10 @@ export const ModalCreateProject = ({
if (value === null || value === undefined || value === '') { if (value === null || value === undefined || value === '') {
return `Поле "${fieldName}" обязательно для заполнения`; return `Поле "${fieldName}" обязательно для заполнения`;
} }
}
}
if (config.type === 'number' && value !== null && value !== undefined && value !== '') {
if (config.min !== undefined && Number(value) < config.min) { if (config.min !== undefined && Number(value) < config.min) {
return `Значение должно быть не меньше ${config.min}`; return `Значение должно быть не меньше ${config.min}`;
} }
@ -76,7 +80,6 @@ export const ModalCreateProject = ({
return `Значение должно быть не больше ${config.max}`; return `Значение должно быть не больше ${config.max}`;
} }
} }
}
return null; return null;
}; };