AURORA-1134

This commit is contained in:
PotapovaA 2026-06-25 18:20:18 +03:00
parent 3f9595928e
commit 761a435b98
15 changed files with 185 additions and 93 deletions

View File

@ -5,13 +5,10 @@ export const StagesApi = {
return api.post(`/stages/form/${formId}`, payload).then((r) => r.data);
},
updateStage(payload) {
const { budget_form_id, sheet, phase_code, opens_at, closes_at } = payload;
const { budget_form_id, sheet, phase_code } = payload;
return api
.patch(`/stages/form/${budget_form_id}/${sheet}/${phase_code}`, {
opens_at,
closes_at,
})
.patch(`/stages/form/${budget_form_id}/${sheet}/${phase_code}`, payload)
.then((r) => r.data);
},
deleteStage(formId, sheet, phaseCode) {

View File

@ -55,17 +55,9 @@ export const AddStagesModal = ({
try {
const { config } = await import(`../RealtimeTable/constants/${formType}/${sheetObj.sheet}.js`);
setColumnsConfig(config.config);
// Если есть сохраненные выбранные колонки для этого листа
if (stage?.column_keys) {
setSelectedColumns(stage.column_keys);
} else {
setSelectedColumns([]);
}
} catch (error) {
console.error(`Failed to load config for ${formType}/${selectedSheet}:`, error);
setColumnsConfig({ columns: [], colors: {} });
setSelectedColumns([]);
}
};
@ -79,10 +71,7 @@ export const AddStagesModal = ({
setStageName(stage.phase_code ?? '');
setSelectedSheet(stage.sheet ?? null);
setSelectedRole(stage.role ?? null);
if (stage.column_keys) {
setSelectedColumns(stage.column_keys.map(c => 'data.' + c));
}
} else {
setStartDate(null);
setEndDate(null);
@ -201,6 +190,7 @@ export const AddStagesModal = ({
placeholder="Введите название этапа"
fullWidth
size="small"
disabled={stage !== null}
/>
</FormControl>
</Stack>

View File

@ -26,7 +26,7 @@ import { formatDate } from '../../utils/formatDate';
import { IconWithContent } from './IconWithContent';
import { Chip } from '../styles/StyledChip';
import { ExportButton } from '../common/Buttons/ButtonsActions';
import { FROM_TYPE_TRANSLATE } from '../../constants';
import { FORM_TYPE_TRANSLATE } from '../../constants';
const TaskCard = styled.div`
width: 26.8rem;
@ -100,7 +100,7 @@ export const TaskCardComponent = ({
const [isEditModalOpen, setEditModalOpen] = useState(false);
const [editedTitle, setEditedTitle] = useState('');
const [currentTitle, setCurrentTitle] = useState(FROM_TYPE_TRANSLATE[form_type_code] ||'');
const [currentTitle, setCurrentTitle] = useState(FORM_TYPE_TRANSLATE[form_type_code] ||'');
const [isChecked, setIsChecked] = useState(false);
useEffect(() => {

View File

@ -36,7 +36,7 @@ export const EDIT_ACCESS_RUSSIAN = {
};
export const FORM_TYPE_OPTIONS = ['FORM_1', 'FORM_2', 'FORM_3', 'FORM_4'];
export const FROM_TYPE_TRANSLATE = { 'FORM_1': 'Смета ССП', 'FORM_2': 'Смета РФ', 'FORM_3': 'Программа развитие ПРРС', 'FORM_4': 'Проектная деятельность' };
export const FORM_TYPE_TRANSLATE = { 'FORM_1': 'Смета ССП', 'FORM_2': 'Смета РФ', 'FORM_3': 'Программа развитие ПРРС', 'FORM_4': 'Проектная деятельность' };
export const DIRECTION_TRANSLATE = { 'Support': 'Поддержка', 'Development': 'Развитие' };
export const ORG_UNIT_TYPE_OPTIONS = [

View File

@ -70,7 +70,7 @@ const AuditLogsPage = () => {
limit: rowsPerPage,
user_id: selectedUser?.id,
org_unit_id: selectedSsp?.id,
event_type: selectedEventType?.id,
event: selectedEventType?.id,
date_from: dateFrom ? dateFrom.toISOString() : undefined,
date_to: dateTo ? dateTo.toISOString() : undefined,
task_id: taskId ? taskId : undefined,

View File

@ -177,32 +177,6 @@ const AuditLogsFilters = ({
spacing={'1rem'}
sx={{ width: '100%', alignItems: 'end' }}
>
<FilterFieldWrapper>
<FormLabelStyled>Поиск по контексту (ID задачи)</FormLabelStyled>
<SearchComponent
style={{
width: '100%',
}}
height={'2.56rem'}
placeholder="Например: #1234"
debounceMs={400}
onChange={onTaskIdChange}
value={taskId}
/>
</FilterFieldWrapper>
<FilterFieldWrapper>
<FormLabelStyled>Поиск по контексту (ID формы)</FormLabelStyled>
<SearchComponent
style={{
width: '100%',
}}
height={'2.56rem'}
placeholder="Например: #1234"
debounceMs={400}
onChange={onFormIdChange}
value={formId}
/>
</FilterFieldWrapper>
<DefaultOutlinedButton
onClick={onResetFilters}

View File

@ -7,7 +7,7 @@ import {
CircularProgress,
} from '@mui/material';
import { useEffect, useState } from 'react';
import { EVENT_CONFIGS } from '../constants/configEvent';
import { EVENT_CONFIGS, EVENT_CONFIGS_MULT_DEPENDENTS } from '../constants/configEvent';
import { VALUE_TRANSFORMERS } from '../utils/transformers';
const JsonViewModal = ({ open, onClose, jsonData, eventType }) => {
@ -36,6 +36,7 @@ const JsonViewModal = ({ open, onClose, jsonData, eventType }) => {
async function extractInfo(data, type) {
data = JSON.parse(data);
const config = EVENT_CONFIGS[type] || {};
const configForMultDependents = EVENT_CONFIGS_MULT_DEPENDENTS[type] || {};
const result = {};
// Функция для рекурсивного обхода всех узлов
@ -46,16 +47,33 @@ const JsonViewModal = ({ open, onClose, jsonData, eventType }) => {
const currentPath = path ? `${path}.${key}` : key;
if (config[currentPath] || config[key]) {
const configKey = config[currentPath] || config[key];
let value = obj[key];
const configKey = config[currentPath] || config[key];
// Применяем преобразование, если есть функция для этого ключа
if (VALUE_TRANSFORMERS[configKey] && value) {
try {
// Собираем зависимости для трансформера
// По умолчанию передаём только текущее значение
const dependents = [value];
// Если для этого ключа есть дополнительные зависимости (другие поля)
if (configForMultDependents[configKey] || configForMultDependents[configKey]) {
configForMultDependents[configKey].forEach(d => {
// Добавляем значения из зависимых полей (в корне data)
dependents.push(data[d]);
});
}
const transformer = VALUE_TRANSFORMERS[configKey];
if (transformer.constructor.name === 'AsyncFunction') {
value = await transformer(value);
value = await transformer(...dependents);
} else {
value = transformer(value);
value = transformer(...dependents);
}
} catch (error) {
console.error(`Ошибка в трансформере для ключа "${configKey}":`, error);
value = '—';
}
}
@ -69,8 +87,11 @@ const JsonViewModal = ({ open, onClose, jsonData, eventType }) => {
}
}
// Начинаем обход с основного объекта
try {
await traverseAndExtract(data);
} catch (error) {
return JSON.stringify({ error: 'Ошибка при извлечении данных' }, null, 2);
}
return JSON.stringify(result, null, 2);
}

View File

@ -1,16 +1,17 @@
export const DEFAULT_ACTIONS = {
ROW_CREATE: 'Создание строки',
ROW_DELETE: 'Удаление строки',
ACCESS_EXTEND: 'Расширение доступа',
ACCESS_WINDOW_CHANGE: 'Изменение окна доступа',
ACCESS_EXTEND: 'Продление этапа',
ACCESS_WINDOW_CHANGE: 'Изменение этапа',
USER_ROLE_CHANGE: 'Изменение роли пользователя',
CREATE_TASK: 'Создание задачи',
UPDATE_TASK: 'Обновление задачи',
DELETE_TASK: 'Удаление задачи',
// UPDATE_TASK: 'Обновление задачи', //сейчас нет события
// DELETE_TASK: 'Удаление задачи',
USER_CREATE: 'Создание пользователя',
USER_ACCESS_GRANTED: 'Предоставление доступа к ССП/РФ',
USER_ACCESS_REVOKED: 'Отмена доступа к ССП/РФ',
VSP_CREATE: 'Создание ВСП',
VSP_UPDATE: 'Обновление ВСП',
};
export const ENTITY_RUSSIAN_NAMES = {
@ -23,8 +24,8 @@ export const ENTITY_RUSSIAN_NAMES = {
TASK: 'задачи',
USER_ACCESS: 'доступа к ССП/РФ',
USER: 'пользователя',
ACCESS_WINDOW: 'окна доступа',
ACCESS: 'окна доступа',
ACCESS_WINDOW: 'этапа',
ACCESS: 'этапа',
USER_ROLE: 'роли пользователя',
VSP: 'ВСП',
};
@ -37,7 +38,7 @@ export const ACTION_RUSSIAN_NAMES = {
GET: 'Получение',
GRANTED: 'Предоставление',
REVOKED: 'Отмена',
EXTEND: 'Расширение',
EXTEND: 'Продление',
};
export const getRussianNamesForType = (name) => {

View File

@ -6,12 +6,11 @@ export const EVENT_CONFIGS = {
entity_id: 'Пользователь',
},
VSP_CREATE: {
ssp_id: 'Региональный филиал',
reg_number: 'Регистрационный номер ВСП',
vsp_type: 'Вид ВСП',
address: 'Адрес ВСП',
approved_format: 'Утвержденный формат',
open_date: 'Дата открытия ВСП',
format: 'Утвержденный формат',
opened_at: 'Дата открытия ВСП',
notes: 'Примечания',
location_form: 'Форма расположения ВСП',
numbers: 'Штатная численность ВСП',
@ -22,14 +21,16 @@ export const EVENT_CONFIGS = {
branch_name: 'Региональный филиал ',
closed_at: 'Дата закрытия ВСП',
user_email: 'Почта пользователя',
staff_count: 'Кол-во работников',
placement_type: 'Тип размещение',
is_active: 'Активный',
},
VSP_UPDATE: {
ssp_id: 'Региональный филиал',
reg_number: 'Регистрационный номер ВСП',
vsp_type: 'Вид ВСП',
address: 'Адрес ВСП',
approved_format: 'Утвержденный формат',
open_date: 'Дата открытия ВСП',
format: 'Утвержденный формат',
opened_at: 'Дата открытия ВСП',
notes: 'Примечания',
location_form: 'Форма расположения ВСП',
numbers: 'Штатная численность ВСП',
@ -37,9 +38,12 @@ export const EVENT_CONFIGS = {
rent_contract_num: 'Номер договора аренды',
rent_end_date: 'Срок окончания договора аренды',
system_code: 'Код РФ',
branch_name: 'Региональный филиал ',
'changes.branch_name': 'Региональный филиал ',
closed_at: 'Дата закрытия ВСП',
user_email: 'Почта пользователя',
staff_count: 'Кол-во работников',
placement_type: 'Тип размещение',
is_active: 'Активный',
},
USER_ACCESS_GRANTED: {
entity_id: 'Пользователь',
@ -76,10 +80,26 @@ export const EVENT_CONFIGS = {
"year": "Год",
"org_unit_id": "СПП/РФ",
},
ACCESS_EXTEND:{
ACCESS_EXTEND: {
form_id: 'Информация о задаче',
closes_at_after: 'Дата закрытия',
closes_at_before: 'Дата открытия',
phase_name: 'Названия этапа'
phase_name: 'Названия этапа',
sheet: "Лист",
},
ACCESS_WINDOW_CHANGE: {
form_id: 'Информация о задаче',
closes_at: 'Дата закрытия',
open_at: 'Дата открытия',
phase_name: 'Названия этапа',
role: 'Для роли',
column_keys: 'Колонки в этапе',
action: 'Действие',
sheet: "Лист",
},
};
export const EVENT_CONFIGS_MULT_DEPENDENTS = {
ACCESS_WINDOW_CHANGE: {
'Колонки в этапе': ['form_id', 'sheet']
},
}

View File

@ -4,7 +4,7 @@ import { FormsApi } from '../../../../api/forms';
import { SspApi } from '../../../../api/ssp';
import { TasksApi } from '../../../../api/tasks';
import { UsersApi } from '../../../../api/users';
import { DIRECTION_TRANSLATE, FROM_TYPE_TRANSLATE, ROLES_ID_RUSSIAN_NAME, SHEET_NAME } from '../../../../constants';
import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, ROLES_ID_RUSSIAN_NAME, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../../constants';
export const VALUE_TRANSFORMERS = {
Роль: transformRole,
@ -18,12 +18,28 @@ export const VALUE_TRANSFORMERS = {
'СПП/РФ': transformSsp,
'Статья расходов': transformExpense,
'Информация о задаче': transformForm,
'Направление': transformDirection
'Направление': transformDirection,
'Для роли': transformRoleChanges,
'Колонки в этапе': transformColumns,
'Форма': transformFormType,
};
export function transformRole(value) {
return ROLES_ID_RUSSIAN_NAME[value] || 'Роль не найдена';
}
export function transformFormType(value) {
return FORM_TYPE_TRANSLATE[value] || 'Форма не найдена';
}
export function transformRoleChanges(value) {
if (typeof value === 'string') {
return STAGE_ROLE_RUSSIAN_NAME[value]
}
return {
before: STAGE_ROLE_RUSSIAN_NAME[value.before],
after: STAGE_ROLE_RUSSIAN_NAME[value.after],
}
}
export async function transformUser(userId) {
try {
@ -105,16 +121,90 @@ export async function transformForm(formId) {
const res = await FormsApi.getForm(formId);
const form = res.result;
const formInfo = {
'название': FROM_TYPE_TRANSLATE[form.form_type_code],
'название': FORM_TYPE_TRANSLATE[form.form_type_code],
'ССП/РФ': form.org_unit.title,
'Год': form.year,
}
return formInfo
}
export function transformDirection(direction){
if (!direction){
export function transformDirection(direction) {
if (!direction) {
return '-';
}
return DIRECTION_TRANSLATE[direction];
}
export async function transformColumns(cols, formId, sheet) {
if (!formId || !sheet) return cols;
const res = await FormsApi.getForm(formId);
const form = res.result;
const formType = form.form_type_code;
const { config } = await import(`../../../../components/RealtimeTable/constants/${formType}/${sheet}.js`);
const columns = config.config.columns;
function findColumnAndHeaders(columns, targetAccessorKey) {
// Рекурсивная функция для поиска колонки и сбора родителей
function searchRecursively(columnsArray, path = []) {
for (let column of columnsArray) {
// Проверяем текущую колонку
if (column.accessorKey === targetAccessorKey) {
// Нашли целевую колонку, возвращаем путь с заголовками
return {
found: true,
headers: [...path, column.header],
column: column
};
}
// Если у колонки есть вложенные колонки, ищем в них
if (column.columns && Array.isArray(column.columns)) {
const result = searchRecursively(column.columns, [...path, column.header]);
if (result.found) {
return result;
}
}
}
return { found: false };
}
const result = searchRecursively(columns);
if (result.found) {
return {
headers: result.headers,
column: result.column,
path: result.headers.join(' → ')
};
} else {
return {
headers: [],
column: null,
path: null,
error: `Колонка с accessorKey "${targetAccessorKey}" не найдена`
};
}
}
if (Array.isArray(cols)) {
const res = [];
cols.forEach(c => {
res.push(findColumnAndHeaders(columns, "data." + c).path || '');
})
return res;
}
const translateColsAfter = [];
const translateColsBefore = [];
cols.after.forEach(c => {
translateColsAfter.push(findColumnAndHeaders(columns, "data." + c).path || '');
})
cols.before.forEach(c => {
translateColsBefore.push(findColumnAndHeaders(columns, "data." + c).path || '');
})
// Пример использования:
return { after: translateColsAfter, before: translateColsBefore };
}

View File

@ -20,6 +20,10 @@ export const ModalEditUserSsp = ({
const [selectedType, setSelectedType] = useState('rf'); // 'rf' или 'ssp'
useEffect(() => {
if (!open) {
setCurUser(null);
return;
}
setCurUser(user);
try {
if (user && user.role_id == ROLES_NAME_ID.executor_rf) {
@ -29,7 +33,7 @@ export const ModalEditUserSsp = ({
} catch {
setSelectedType('rf');
}
}, [user]);
}, [user, open]);
useEffect(() => {
if (!deps) return;
@ -75,14 +79,9 @@ export const ModalEditUserSsp = ({
}, [depsData]);
const sspForDfip = useMemo(() => {
if (!curUser) return [];
if (newDepsData.update.length > 0) {
return newDepsData.update.map(
(sId) => deps.filter((dep) => dep.id == sId)[0],
);
}
if (!curUser|| !open) return [];
return curUser.org_units ?? [];
}, [curUser, deps, newDepsData]);
}, [curUser, deps, newDepsData, open]);
const handleTypeChange = (value) => {
setSelectedType(value);

View File

@ -359,8 +359,8 @@ export const EditUserModal = ({
customSize={58.13}
>
<FormStack>
<Box display={'flex'} gap={'1.5rem'} flexDirection={'column'}>
<Box display={'flex'} gap={'.5rem'} flexDirection={'column'}>
<Box sx={{ display: 'flex', gap: '1.5rem', flexDirection: 'column' }} >
<Box sx={{ display: 'flex', gap: '.5rem', flexDirection: 'column' }} >
{renderField('ФИО', 'full_name', editUser.full_name)}
{renderField('Почта', 'email', editUser.email, 'email')}
</Box>

View File

@ -105,7 +105,7 @@ export const AutocompleteForChangeOptions = ({
if (option.email) {
label += ` (${option.email})`;
}
return label;
return label ?? '-';
}}
onChange={handleSelect}
inputValue={inputValue}

View File

@ -22,13 +22,13 @@ import { ModalCreateProject } from './ModalCreateProject';
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
import { exportSingleForm } from '../../utils/exportForm';
import TablesList from '../../components/common/TableList/TableList';
import { DIRECTION_TRANSLATE, FROM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants';
import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants';
const TaskInfo = ({ task }) => {
const portalContent = (
<TaskInfoContainer>
<BackButton to="/tasks" />
<NameTask>{FROM_TYPE_TRANSLATE[task.form_type_code] || ''}</NameTask>
<NameTask>{FORM_TYPE_TRANSLATE[task.form_type_code] || ''}</NameTask>
</TaskInfoContainer>
);

View File

@ -26,7 +26,7 @@ import {
FORM_TYPE_OPTIONS,
ORG_UNIT_TYPE_OPTIONS,
ROLES_NAME_ID,
FROM_TYPE_TRANSLATE
FORM_TYPE_TRANSLATE
} from '../../constants';
import { useAuth } from '../../app/context/AuthProvider';
import { SspApi } from '../../api/ssp';
@ -414,7 +414,7 @@ export default function TasksPage() {
onChange={(e) => handleFormChange('formTypeCode', e.target.value)}
renderValue={(selected) =>
selected ? (
FROM_TYPE_TRANSLATE[selected]
FORM_TYPE_TRANSLATE[selected]
) : (
<span style={{ color: 'rgba(0,0,0,0.6)' }}>Не выбрана</span>
)
@ -424,7 +424,7 @@ export default function TasksPage() {
>
{FORM_TYPE_OPTIONS.map((formType) => (
<MenuItem key={formType} value={formType}>
{FROM_TYPE_TRANSLATE[formType]}
{FORM_TYPE_TRANSLATE[formType]}
</MenuItem>
))}
</Select>