audit-logs-projects #109
@ -191,6 +191,7 @@ export const AddStagesModal = ({
|
|||||||
onChange={handleSheetChange}
|
onChange={handleSheetChange}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
size='small'
|
size='small'
|
||||||
|
disabled={stage !== null}
|
||||||
MenuProps={stageSelectMenuProps}>
|
MenuProps={stageSelectMenuProps}>
|
||||||
<MenuItem value='' disabled>
|
<MenuItem value='' disabled>
|
||||||
Выберите лист
|
Выберите лист
|
||||||
@ -214,6 +215,7 @@ export const AddStagesModal = ({
|
|||||||
onChange={(e) => setSelectedDirection(e.target.value)}
|
onChange={(e) => setSelectedDirection(e.target.value)}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
size='small'
|
size='small'
|
||||||
|
disabled={stage !== null}
|
||||||
MenuProps={stageSelectMenuProps}
|
MenuProps={stageSelectMenuProps}
|
||||||
>
|
>
|
||||||
<MenuItem value='' disabled>
|
<MenuItem value='' disabled>
|
||||||
|
|||||||
@ -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',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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: 'Детали',
|
||||||
|
|||||||
@ -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 ?? '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если значение - объект или массив, рекурсивно обходим его
|
// Если значение - объект или массив, рекурсивно обходим его
|
||||||
|
|||||||
@ -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 = '';
|
||||||
|
|
||||||
|
|||||||
@ -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: 'Дата закрытия (новая)',
|
||||||
|
|||||||
@ -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] || 'Роль не найдена';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user