Compare commits

..

No commits in common. "e2a16989ae2783b7deefd8f84e9fc0efa594c8e6" and "ecb2708b4efbd3a49bbcded402f03b68bcba784c" have entirely different histories.

21 changed files with 105 additions and 250 deletions

View File

@ -1,4 +1,4 @@
import api from "./client";
import api from './client';
export const FormsSheetApi = {
get(formId, sheetName, params) {
@ -9,10 +9,7 @@ export const FormsSheetApi = {
export(formId, sheetName, params = {}) {
return api.get(`export/form/${formId}/sheet/${sheetName}`, {
params,
responseType: "blob",
responseType: 'blob',
});
},
getExpenseItem(itemId) {
return api.get(`/expense-item/${itemId}`).then((r) => r.data);
},
};

View File

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

View File

@ -160,7 +160,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
if (isPinned) {
const colId = table.getState().columnPinning.left[index];
const column = table.getColumn(colId);
return column?.getSize() ?? 150;
return column.columnDef.size;
}
const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()]
return allCols[index]?.getSize() ?? 150;
@ -252,6 +252,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
toast.error('Ошибка редактирования ячейки');
}
}, [dataEditingCells, editingCell]);
console.log(editingCell)
useEffect(() => {
if (table && data && columns.length !== 0) {

View File

@ -55,9 +55,17 @@ 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([]);
}
};
@ -71,7 +79,10 @@ export const AddStagesModal = ({
setStageName(stage.phase_code ?? '');
setSelectedSheet(stage.sheet ?? null);
setSelectedRole(stage.role ?? null);
setSelectedColumns(stage.column_keys.map(c => 'data.' + c));
if (stage.column_keys) {
setSelectedColumns(stage.column_keys.map(c => 'data.' + c));
}
} else {
setStartDate(null);
setEndDate(null);
@ -190,7 +201,6 @@ 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 { FORM_TYPE_TRANSLATE } from '../../constants';
import { FROM_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(FORM_TYPE_TRANSLATE[form_type_code] ||'');
const [currentTitle, setCurrentTitle] = useState(FROM_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 FORM_TYPE_TRANSLATE = { 'FORM_1': 'Смета ССП', 'FORM_2': 'Смета РФ', 'FORM_3': 'Программа развитие ПРРС', 'FORM_4': 'Проектная деятельность' };
export const FROM_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: selectedEventType?.id,
event_type: selectedEventType?.id,
date_from: dateFrom ? dateFrom.toISOString() : undefined,
date_to: dateTo ? dateTo.toISOString() : undefined,
task_id: taskId ? taskId : undefined,
@ -137,9 +137,7 @@ const AuditLogsPage = () => {
return (
<PageContainer>
<HeaderSwitch />
<Box sx={{
display: "flex", justifyContent: "space-between"
}} >
<Box display="flex" justifyContent="space-between">
<SwitchUserSsp />
</Box>

View File

@ -177,6 +177,32 @@ 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

@ -77,9 +77,9 @@ const AuditLogsTable = ({
<Typography component="div" fontWeight="medium">
{row.original.user
? row.original.user.full_name +
' (' +
row.original.user.email +
')'
' (' +
row.original.user.email +
')'
: '-'}
</Typography>
</Box>
@ -118,7 +118,8 @@ const AuditLogsTable = ({
size: 250,
Cell: ({ row }) => (
<Typography>
{getRussianNamesForType(row.original.action) }
{getRussianNamesForType(row.original.action) ||
row.original.event_type}
</Typography>
),
enableColumnActions: false,

View File

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

View File

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

View File

@ -6,11 +6,12 @@ export const EVENT_CONFIGS = {
entity_id: 'Пользователь',
},
VSP_CREATE: {
ssp_id: 'Региональный филиал',
reg_number: 'Регистрационный номер ВСП',
vsp_type: 'Вид ВСП',
address: 'Адрес ВСП',
format: 'Утвержденный формат',
opened_at: 'Дата открытия ВСП',
approved_format: 'Утвержденный формат',
open_date: 'Дата открытия ВСП',
notes: 'Примечания',
location_form: 'Форма расположения ВСП',
numbers: 'Штатная численность ВСП',
@ -21,16 +22,14 @@ 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: 'Адрес ВСП',
format: 'Утвержденный формат',
opened_at: 'Дата открытия ВСП',
approved_format: 'Утвержденный формат',
open_date: 'Дата открытия ВСП',
notes: 'Примечания',
location_form: 'Форма расположения ВСП',
numbers: 'Штатная численность ВСП',
@ -38,12 +37,9 @@ export const EVENT_CONFIGS = {
rent_contract_num: 'Номер договора аренды',
rent_end_date: 'Срок окончания договора аренды',
system_code: 'Код РФ',
'changes.branch_name': 'Региональный филиал ',
branch_name: 'Региональный филиал ',
closed_at: 'Дата закрытия ВСП',
user_email: 'Почта пользователя',
staff_count: 'Кол-во работников',
placement_type: 'Тип размещение',
is_active: 'Активный',
},
USER_ACCESS_GRANTED: {
entity_id: 'Пользователь',
@ -60,15 +56,11 @@ export const EVENT_CONFIGS = {
},
ROW_CREATE: {
sheet: "Лист",
expense_item_id: 'Статья расходов',
form_id: 'Информация о задаче',
direction: 'Направление',
expense_item_id: 'Статья расходов'
},
ROW_DELETE: {
sheet: "Лист",
expense_item_id: 'Статья расходов',
form_id: 'Информация о задаче',
direction: 'Направление',
expense_item_id: 'Статья расходов'
},
PROJECT_CREATE: {
"name": "Название",
@ -79,27 +71,5 @@ export const EVENT_CONFIGS = {
"name": "Название",
"year": "Год",
"org_unit_id": "СПП/РФ",
},
ACCESS_EXTEND: {
form_id: 'Информация о задаче',
closes_at_after: 'Дата закрытия',
closes_at_before: 'Дата открытия',
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

@ -1,10 +1,8 @@
import { DictVspApi } from '../../../../api/dict-vsp';
import { FormsSheetApi } from '../../../../api/form_sheet';
import { FormsApi } from '../../../../api/forms';
import { SspApi } from '../../../../api/ssp';
import { TasksApi } from '../../../../api/tasks';
import { UsersApi } from '../../../../api/users';
import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, ROLES_ID_RUSSIAN_NAME, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../../constants';
import { ROLES_ID_RUSSIAN_NAME, SHEET_NAME } from '../../../../constants';
export const VALUE_TRANSFORMERS = {
Роль: transformRole,
@ -16,30 +14,11 @@ export const VALUE_TRANSFORMERS = {
'Создана для СПП/РФ': transformSspsDict,
'Лист': transformSheet,
'СПП/РФ': transformSsp,
'Статья расходов': transformExpense,
'Информация о задаче': transformForm,
'Направление': 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 {
@ -104,107 +83,3 @@ export async function transformSspsDict(ssps) {
export function transformSheet(value) {
return SHEET_NAME[value] || 'Лист не найден';
}
export async function transformExpense(itemId) {
if (!itemId) {
return 'нет информации'
}
const res = await FormsSheetApi.getExpenseItem(itemId);
const expense = res.result;
return expense.name;
}
export async function transformForm(formId) {
if (!formId) {
return 'нет информации'
}
const res = await FormsApi.getForm(formId);
const form = res.result;
const formInfo = {
'название': FORM_TYPE_TRANSLATE[form.form_type_code],
'ССП/РФ': form.org_unit.title,
'Год': form.year,
}
return formInfo
}
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,10 +20,6 @@ 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) {
@ -33,7 +29,7 @@ export const ModalEditUserSsp = ({
} catch {
setSelectedType('rf');
}
}, [user, open]);
}, [user]);
useEffect(() => {
if (!deps) return;
@ -79,9 +75,14 @@ export const ModalEditUserSsp = ({
}, [depsData]);
const sspForDfip = useMemo(() => {
if (!curUser|| !open) return [];
if (!curUser) return [];
if (newDepsData.update.length > 0) {
return newDepsData.update.map(
(sId) => deps.filter((dep) => dep.id == sId)[0],
);
}
return curUser.org_units ?? [];
}, [curUser, deps, newDepsData, open]);
}, [curUser, deps, newDepsData]);
const handleTypeChange = (value) => {
setSelectedType(value);

View File

@ -359,8 +359,8 @@ export const EditUserModal = ({
customSize={58.13}
>
<FormStack>
<Box sx={{ display: 'flex', gap: '1.5rem', flexDirection: 'column' }} >
<Box sx={{ display: 'flex', gap: '.5rem', flexDirection: 'column' }} >
<Box display={'flex'} gap={'1.5rem'} flexDirection={'column'}>
<Box 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

@ -95,9 +95,7 @@ const TableFooter = ({
</FormControl>
</Box>
<Stack sx={{
direction: "row", spacing: 2, alignItems: "center"
}}>
<Stack direction="row" spacing={2} alignItems="center">
<Pagination
count={Math.max(1, totalPages)}
page={page + 1}

View File

@ -142,7 +142,7 @@ function DictVspPage() {
<DictVspInfo />
{/* <HeaderSwitch /> */}
<PageContainer>
<Stack sx={{ alignItems: 'stretch' }} width="100%">
<Stack alignItems={'stretch'} width="100%">
<Box width="100%">
{user.role_id !== ROLES_NAME_ID.executor_rf && (
<DictVspFilters

View File

@ -106,12 +106,9 @@ function DictsNavigatorPage() {
</FeatureList>
<PrimaryButton height={'2.75rem'} onClick={handleNavigateToVSP}>
<Stack
sx={{
spacing: '.25rem',
direction: 'row',
alignItems: 'center',
}}
spacing={'.25rem'}
direction={'row'}
alignItems={'center'}
>
<TransitionSvg />
<p>Открыть справочник объектов</p>

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, FORM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants';
import { DIRECTION_TRANSLATE, FROM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants';
const TaskInfo = ({ task }) => {
const portalContent = (
<TaskInfoContainer>
<BackButton to="/tasks" />
<NameTask>{FORM_TYPE_TRANSLATE[task.form_type_code] || ''}</NameTask>
<NameTask>{FROM_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,
FORM_TYPE_TRANSLATE
FROM_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 ? (
FORM_TYPE_TRANSLATE[selected]
FROM_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}>
{FORM_TYPE_TRANSLATE[formType]}
{FROM_TYPE_TRANSLATE[formType]}
</MenuItem>
))}
</Select>