feat: nested department sections, attaching department sections in the user editor
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mantine-contextmenu": "^7.12.2",
|
||||
"mantine-datatable": "^7.15.1",
|
||||
"mantine-form-zod-resolver": "^1.1.0",
|
||||
"mantine-react-table": "^2.0.0-beta.5",
|
||||
"phone": "^3.1.49",
|
||||
|
||||
@@ -186,6 +186,7 @@ export type { DeleteUserResponse } from './models/DeleteUserResponse';
|
||||
export type { DepartmentBaseSchema } from './models/DepartmentBaseSchema';
|
||||
export type { DepartmentSchema } from './models/DepartmentSchema';
|
||||
export type { DepartmentSectionBaseSchema } from './models/DepartmentSectionBaseSchema';
|
||||
export type { DepartmentSectionBriefSchema } from './models/DepartmentSectionBriefSchema';
|
||||
export type { DepartmentSectionSchema } from './models/DepartmentSectionSchema';
|
||||
export type { FinishPauseByShiftIdResponse } from './models/FinishPauseByShiftIdResponse';
|
||||
export type { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse';
|
||||
@@ -215,6 +216,7 @@ export type { GetClientMarketplacesResponse } from './models/GetClientMarketplac
|
||||
export type { GetDealBillById } from './models/GetDealBillById';
|
||||
export type { GetDealProductsBarcodesPdfRequest } from './models/GetDealProductsBarcodesPdfRequest';
|
||||
export type { GetDealProductsBarcodesPdfResponse } from './models/GetDealProductsBarcodesPdfResponse';
|
||||
export type { GetDepartmentSectionsResponse } from './models/GetDepartmentSectionsResponse';
|
||||
export type { GetDepartmentsResponse } from './models/GetDepartmentsResponse';
|
||||
export type { GetManagersResponse } from './models/GetManagersResponse';
|
||||
export type { GetPaymentRecordsResponse } from './models/GetPaymentRecordsResponse';
|
||||
@@ -350,10 +352,14 @@ export type { UpdateTransactionResponse } from './models/UpdateTransactionRespon
|
||||
export type { UpdateTransactionSchema } from './models/UpdateTransactionSchema';
|
||||
export type { UpdateTransactionTagRequest } from './models/UpdateTransactionTagRequest';
|
||||
export type { UpdateTransactionTagResponse } from './models/UpdateTransactionTagResponse';
|
||||
export type { UpdateUserDepartmentSectionsRequest } from './models/UpdateUserDepartmentSectionsRequest';
|
||||
export type { UpdateUserDepartmentSectionsResponse } from './models/UpdateUserDepartmentSectionsResponse';
|
||||
export type { UpdateUserRequest } from './models/UpdateUserRequest';
|
||||
export type { UpdateUserResponse } from './models/UpdateUserResponse';
|
||||
export type { UploadPassportImageResponse } from './models/UploadPassportImageResponse';
|
||||
export type { UserCreate } from './models/UserCreate';
|
||||
export type { UserDepartmentSectionSchema } from './models/UserDepartmentSectionSchema';
|
||||
export type { UserDepartmentSectionsSchema } from './models/UserDepartmentSectionsSchema';
|
||||
export type { UserSchema } from './models/UserSchema';
|
||||
export type { UserUpdate } from './models/UserUpdate';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
export type AddUserRequest = {
|
||||
userId: number;
|
||||
sectionId: number;
|
||||
isChief: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
/* eslint-disable */
|
||||
export type DepartmentSectionBaseSchema = {
|
||||
name: string;
|
||||
departmentId: number;
|
||||
departmentId: (number | null);
|
||||
parentDepartmentSectionId: (number | null);
|
||||
};
|
||||
|
||||
|
||||
11
src/client/models/DepartmentSectionBriefSchema.ts
Normal file
11
src/client/models/DepartmentSectionBriefSchema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DepartmentSectionBriefSchema = {
|
||||
name: string;
|
||||
departmentId: (number | null);
|
||||
parentDepartmentSectionId: (number | null);
|
||||
id: number;
|
||||
};
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { UserSchema } from './UserSchema';
|
||||
import type { UserDepartmentSectionSchema } from './UserDepartmentSectionSchema';
|
||||
export type DepartmentSectionSchema = {
|
||||
name: string;
|
||||
departmentId: number;
|
||||
departmentId: (number | null);
|
||||
parentDepartmentSectionId: (number | null);
|
||||
id: number;
|
||||
users?: Array<UserSchema>;
|
||||
users?: Array<UserDepartmentSectionSchema>;
|
||||
sections?: Array<DepartmentSectionSchema>;
|
||||
};
|
||||
|
||||
|
||||
9
src/client/models/GetDepartmentSectionsResponse.ts
Normal file
9
src/client/models/GetDepartmentSectionsResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { DepartmentSectionBriefSchema } from './DepartmentSectionBriefSchema';
|
||||
export type GetDepartmentSectionsResponse = {
|
||||
departmentSections: Array<DepartmentSectionBriefSchema>;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateUserDepartmentSectionsRequest.ts
Normal file
9
src/client/models/UpdateUserDepartmentSectionsRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { UserDepartmentSectionsSchema } from './UserDepartmentSectionsSchema';
|
||||
export type UpdateUserDepartmentSectionsRequest = {
|
||||
departmentSections: Array<UserDepartmentSectionsSchema>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateUserDepartmentSectionsResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
10
src/client/models/UserDepartmentSectionSchema.ts
Normal file
10
src/client/models/UserDepartmentSectionSchema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { UserSchema } from './UserSchema';
|
||||
export type UserDepartmentSectionSchema = {
|
||||
user: UserSchema;
|
||||
isChief: boolean;
|
||||
};
|
||||
|
||||
9
src/client/models/UserDepartmentSectionsSchema.ts
Normal file
9
src/client/models/UserDepartmentSectionsSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UserDepartmentSectionsSchema = {
|
||||
sectionId: number;
|
||||
isChief: boolean;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { PassportImageSchema } from './PassportImageSchema';
|
||||
import type { PayRateSchema } from './PayRateSchema';
|
||||
import type { PositionSchema } from './PositionSchema';
|
||||
import type { RoleSchema } from './RoleSchema';
|
||||
import type { UserDepartmentSectionsSchema } from './UserDepartmentSectionsSchema';
|
||||
export type UserSchema = {
|
||||
telegramId: number;
|
||||
phoneNumber?: (string | null);
|
||||
@@ -24,5 +25,6 @@ export type UserSchema = {
|
||||
id: number;
|
||||
role: RoleSchema;
|
||||
position?: (PositionSchema | null);
|
||||
departmentSections?: (Array<UserDepartmentSectionsSchema> | null);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { DeleteDepartmentSectionResponse } from '../models/DeleteDepartment
|
||||
import type { DeleteUserRequest } from '../models/DeleteUserRequest';
|
||||
import type { DeleteUserResponse } from '../models/DeleteUserResponse';
|
||||
import type { GetAvailableUsersForDepartmentSectionResponse } from '../models/GetAvailableUsersForDepartmentSectionResponse';
|
||||
import type { GetDepartmentSectionsResponse } from '../models/GetDepartmentSectionsResponse';
|
||||
import type { GetDepartmentsResponse } from '../models/GetDepartmentsResponse';
|
||||
import type { UpdateDepartmentRequest } from '../models/UpdateDepartmentRequest';
|
||||
import type { UpdateDepartmentResponse } from '../models/UpdateDepartmentResponse';
|
||||
@@ -94,6 +95,17 @@ export class DepartmentService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get Sections
|
||||
* @returns GetDepartmentSectionsResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getSections(): CancelablePromise<GetDepartmentSectionsResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/department/section',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create Section
|
||||
* @returns CreateDepartmentSectionResponse Successful Response
|
||||
|
||||
@@ -7,6 +7,8 @@ import type { CreateUserRequest } from '../models/CreateUserRequest';
|
||||
import type { CreateUserResponse } from '../models/CreateUserResponse';
|
||||
import type { GetAllUsersResponse } from '../models/GetAllUsersResponse';
|
||||
import type { GetManagersResponse } from '../models/GetManagersResponse';
|
||||
import type { UpdateUserDepartmentSectionsRequest } from '../models/UpdateUserDepartmentSectionsRequest';
|
||||
import type { UpdateUserDepartmentSectionsResponse } from '../models/UpdateUserDepartmentSectionsResponse';
|
||||
import type { UpdateUserRequest } from '../models/UpdateUserRequest';
|
||||
import type { UpdateUserResponse } from '../models/UpdateUserResponse';
|
||||
import type { UploadPassportImageResponse } from '../models/UploadPassportImageResponse';
|
||||
@@ -45,6 +47,31 @@ export class UserService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update User Department Sections
|
||||
* @returns UpdateUserDepartmentSectionsResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static updateUserDepartmentSections({
|
||||
userId,
|
||||
requestBody,
|
||||
}: {
|
||||
userId: number,
|
||||
requestBody: UpdateUserDepartmentSectionsRequest,
|
||||
}): CancelablePromise<UpdateUserDepartmentSectionsResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/user/update/department-sections/{user_id}',
|
||||
path: {
|
||||
'user_id': userId,
|
||||
},
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create
|
||||
* @returns CreateUserResponse Successful Response
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { DepartmentSectionBriefSchema, DepartmentService } from "../../../../client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Anchor, Checkbox, Group, Stack, Text } from "@mantine/core";
|
||||
|
||||
|
||||
type Props = {
|
||||
userSectionIsChiefMap: Map<number, boolean>;
|
||||
}
|
||||
|
||||
const UserDepartmentInput = ({ userSectionIsChiefMap }: Props) => {
|
||||
const [allDepartmentSections, setAllDepartmentSections] = useState<DepartmentSectionBriefSchema[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
DepartmentService.getSections()
|
||||
.then(res => {
|
||||
setAllDepartmentSections(res.departmentSections);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
}, []);
|
||||
|
||||
const getUserParticipationRole = (sectionId: number) => {
|
||||
if (!userSectionIsChiefMap.has(sectionId)) return;
|
||||
if (userSectionIsChiefMap.get(sectionId)) return "Руководитель";
|
||||
return "Сотрудник";
|
||||
};
|
||||
|
||||
const toggleUserParticipationRole = (sectionId: number) => {
|
||||
if (!userSectionIsChiefMap.has(sectionId)) return;
|
||||
userSectionIsChiefMap.set(sectionId, !userSectionIsChiefMap.get(sectionId));
|
||||
};
|
||||
|
||||
const onSectionChoice = (sectionId: number) => {
|
||||
if (!userSectionIsChiefMap.has(sectionId)) {
|
||||
userSectionIsChiefMap.set(sectionId, false);
|
||||
} else {
|
||||
userSectionIsChiefMap.delete(sectionId);
|
||||
}
|
||||
};
|
||||
|
||||
const userParticipationRoleButton = (sectionId: number) => {
|
||||
const role = getUserParticipationRole(sectionId);
|
||||
if (!role) return;
|
||||
return (
|
||||
<Anchor onClick={() => toggleUserParticipationRole(sectionId)}>
|
||||
<Text size={"sm"}>{role}</Text>
|
||||
</Anchor>
|
||||
);
|
||||
};
|
||||
|
||||
const departmentSectionChoice = (section: DepartmentSectionBriefSchema) => {
|
||||
return (
|
||||
<Group
|
||||
key={section.id}
|
||||
wrap={"nowrap"}
|
||||
justify={"space-between"}
|
||||
gap={0}
|
||||
>
|
||||
<Group wrap={"nowrap"} w={"70%"}>
|
||||
<Checkbox
|
||||
variant={"default"}
|
||||
checked={userSectionIsChiefMap.has(section.id)}
|
||||
onChange={() => onSectionChoice(section.id)}
|
||||
/>
|
||||
<Text size={"sm"}>{section.name}</Text>
|
||||
</Group>
|
||||
{userParticipationRoleButton(section.id)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{allDepartmentSections.map((section) => departmentSectionChoice(section))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDepartmentInput;
|
||||
@@ -1,26 +1,37 @@
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { UserSchema } from "../../../../client";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { FC } from "react";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { useUsersTableColumns } from "./columns.tsx";
|
||||
import { IconEdit, IconQrcode, IconTrash } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { useUsersTabContext } from "../../tabs/Users/contexts/UsersTabContext.tsx";
|
||||
|
||||
type Props = CRUDTableProps<UserSchema>;
|
||||
|
||||
const UsersTable: FC<Props> = ({ items, onChange, onDelete, onCreate }) => {
|
||||
const UsersTable = () => {
|
||||
const columns = useUsersTableColumns();
|
||||
|
||||
const {
|
||||
usersCrud: {
|
||||
items,
|
||||
onChange,
|
||||
onCreate,
|
||||
onDelete,
|
||||
},
|
||||
refetch,
|
||||
} = useUsersTabContext();
|
||||
|
||||
const onEditClick = (user: UserSchema) => {
|
||||
if (!onChange) return;
|
||||
console.log(user);
|
||||
modals.openContextModal({
|
||||
modal: "userFormModal",
|
||||
title: "Редактирование пользователя",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: onChange,
|
||||
onChange,
|
||||
element: user,
|
||||
refetch,
|
||||
},
|
||||
size: "md",
|
||||
});
|
||||
@@ -29,7 +40,6 @@ const UsersTable: FC<Props> = ({ items, onChange, onDelete, onCreate }) => {
|
||||
if (!onDelete) return;
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление пользователя",
|
||||
// centered: true,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить пользователя {user.firstName}{" "}
|
||||
@@ -48,7 +58,8 @@ const UsersTable: FC<Props> = ({ items, onChange, onDelete, onCreate }) => {
|
||||
title: "Редактирование пользователя",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCreate,
|
||||
onCreate,
|
||||
refetch,
|
||||
},
|
||||
size: "md",
|
||||
});
|
||||
|
||||
@@ -1,193 +1,54 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, { CreateEditFormProps } from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { CreateEditFormProps } from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { UserSchema } from "../../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { Checkbox, Fieldset, Input, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import RoleSelect from "../../components/RoleSelect/RoleSelect.tsx";
|
||||
import PositionSelect from "../../components/PositionSelect/PositionSelect.tsx";
|
||||
import { UserRoleEnum } from "../../../../shared/enums/UserRole.ts";
|
||||
import { capitalize } from "lodash";
|
||||
import { IMaskInput } from "react-imask";
|
||||
import phone from "phone";
|
||||
import PayRateSelect from "../../../../components/Selects/PayRateSelect/PayRateSelect.tsx";
|
||||
import { BaseFormInputProps } from "../../../../types/utils.ts";
|
||||
import PassportImageDropzone from "../../components/PassportImageDropzone/PassportImageDropzone.tsx";
|
||||
import { Stack } from "@mantine/core";
|
||||
import CommonTab from "./tabs/CommonTab/CommonTab.tsx";
|
||||
import DepartmentSectionsTab from "./tabs/DepartmentSectionsTab/DepartmentSectionsTab.tsx";
|
||||
import { ModalTab, UsersModalTabSegmentedControl } from "./components/UsersModalTabSegmentedControl.tsx";
|
||||
import { useState } from "react";
|
||||
|
||||
type PropsRefetch = {
|
||||
refetch: () => void;
|
||||
}
|
||||
type Props = CreateEditFormProps<UserSchema> & PropsRefetch;
|
||||
|
||||
type Props = CreateEditFormProps<UserSchema>;
|
||||
const UserFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValues = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
isAdmin: false,
|
||||
isBlocked: false,
|
||||
isDeleted: false,
|
||||
comment: "",
|
||||
roleKey: UserRoleEnum.USER,
|
||||
};
|
||||
const [modalTab, setModalTab] = useState<ModalTab>(ModalTab.COMMON);
|
||||
|
||||
const closeModal = () => {
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<CommonTab closeModal={closeModal} formProps={innerProps} />
|
||||
);
|
||||
}
|
||||
|
||||
const form = useForm<Partial<UserSchema>>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
firstName: value => !value?.trim() && "Укажите имя пользователя",
|
||||
secondName: value => !value?.trim() && "Укажите фамилию",
|
||||
position: (value, values) =>
|
||||
values.role?.key === UserRoleEnum.EMPLOYEE &&
|
||||
!value &&
|
||||
"Необходимо указать должность сотрудника",
|
||||
phoneNumber: value =>
|
||||
!phone(value || "", {
|
||||
country: "",
|
||||
strictDetection: false,
|
||||
validateMobilePrefix: false,
|
||||
}).isValid && "Неверно указан номер телефона",
|
||||
},
|
||||
});
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Общая информация"}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
{...form.getInputProps("secondName")}
|
||||
label={"Фамилия"}
|
||||
placeholder={"Введите фамилию пользователя"}
|
||||
onChange={event =>
|
||||
form
|
||||
.getInputProps("secondName")
|
||||
.onChange(
|
||||
capitalize(
|
||||
event.target.value,
|
||||
).trim(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label={"Имя"}
|
||||
placeholder={"Введите имя пользователя"}
|
||||
{...form.getInputProps("firstName")}
|
||||
onChange={event =>
|
||||
form
|
||||
.getInputProps("firstName")
|
||||
.onChange(
|
||||
capitalize(
|
||||
event.target.value,
|
||||
).trim(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
{...form.getInputProps("patronymic")}
|
||||
label={"Отчество"}
|
||||
placeholder={"Введите отчество пользователя"}
|
||||
onChange={event =>
|
||||
form
|
||||
.getInputProps("patronymic")
|
||||
.onChange(
|
||||
capitalize(
|
||||
event.target.value,
|
||||
).trim(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input.Wrapper
|
||||
label={"Номер телефона"}
|
||||
error={form.getInputProps("phoneNumber").error}>
|
||||
<Input
|
||||
component={IMaskInput}
|
||||
mask="+7 000 000-00-00"
|
||||
placeholder={"Введите номер телефона"}
|
||||
{...form.getInputProps("phoneNumber")}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Паспортные данные"}>
|
||||
<Stack>
|
||||
<Input.Wrapper
|
||||
label={"Серия и номер паспорта"}
|
||||
error={form.getInputProps("passportData").error}>
|
||||
<Input
|
||||
component={IMaskInput}
|
||||
mask="00 00 000000"
|
||||
placeholder={"Введите серию и номер паспорта"}
|
||||
{...form.getInputProps("passportData")}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
{
|
||||
isEditing && (
|
||||
<PassportImageDropzone
|
||||
imageUrlInputProps={
|
||||
form.getInputProps(
|
||||
"passportImageUrl",
|
||||
) as BaseFormInputProps<string>
|
||||
}
|
||||
userId={innerProps?.element.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Роль и должность"}>
|
||||
<Stack>
|
||||
<RoleSelect
|
||||
label={"Роль пользователя"}
|
||||
placeholder={"Выберите роль пользователя"}
|
||||
{...form.getInputProps("role")}
|
||||
/>
|
||||
{form.values.role?.key ===
|
||||
UserRoleEnum.EMPLOYEE && (
|
||||
<>
|
||||
<PositionSelect
|
||||
label={"Должность сотрудника"}
|
||||
placeholder={
|
||||
"Выберите должность сотрудника"
|
||||
}
|
||||
{...form.getInputProps("position")}
|
||||
/>
|
||||
<PayRateSelect
|
||||
label={"Тариф"}
|
||||
placeholder={
|
||||
"Выберите тариф сотрудника"
|
||||
}
|
||||
{...form.getInputProps("payRate")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Дополнительные параметры"}>
|
||||
<Stack>
|
||||
<Checkbox
|
||||
label={"Права администратора"}
|
||||
{...form.getInputProps("isAdmin", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Заблокирован"}
|
||||
{...form.getInputProps("isBlocked", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Textarea
|
||||
label={"Дополнительная информация"}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
<Stack>
|
||||
<UsersModalTabSegmentedControl
|
||||
value={modalTab.toString()}
|
||||
onChange={event => setModalTab(parseInt(event))}
|
||||
/>
|
||||
{modalTab === ModalTab.COMMON ? (
|
||||
<CommonTab
|
||||
closeModal={closeModal}
|
||||
formProps={innerProps}
|
||||
/>
|
||||
) : (
|
||||
<DepartmentSectionsTab
|
||||
closeModal={closeModal}
|
||||
user={innerProps.element}
|
||||
refetch={innerProps.refetch}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
|
||||
import { FC } from "react";
|
||||
|
||||
export enum ModalTab {
|
||||
COMMON,
|
||||
DEPARTMENT_SECTIONS,
|
||||
}
|
||||
|
||||
type Props = Omit<SegmentedControlProps, "data">;
|
||||
const data = [
|
||||
{
|
||||
label: "Общее",
|
||||
value: ModalTab.COMMON.toString(),
|
||||
},
|
||||
{
|
||||
label: "Отделы",
|
||||
value: ModalTab.DEPARTMENT_SECTIONS.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
export const UsersModalTabSegmentedControl: FC<Props> = props => {
|
||||
return (
|
||||
<SegmentedControl
|
||||
data={data}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import { UserRoleEnum } from "../../../../../../shared/enums/UserRole.ts";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { UserSchema } from "../../../../../../client";
|
||||
import phone from "phone";
|
||||
import BaseFormModal, { CreateEditFormProps } from "../../../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { Checkbox, Fieldset, Input, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import { capitalize } from "lodash";
|
||||
import { IMaskInput } from "react-imask";
|
||||
import PassportImageDropzone from "../../../../components/PassportImageDropzone/PassportImageDropzone.tsx";
|
||||
import { BaseFormInputProps } from "../../../../../../types/utils.ts";
|
||||
import RoleSelect from "../../../../components/RoleSelect/RoleSelect.tsx";
|
||||
import PositionSelect from "../../../../components/PositionSelect/PositionSelect.tsx";
|
||||
import PayRateSelect from "../../../../../../components/Selects/PayRateSelect/PayRateSelect.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
closeModal: () => void;
|
||||
formProps: CreateEditFormProps<UserSchema>;
|
||||
};
|
||||
|
||||
const CommonTab = ({
|
||||
closeModal,
|
||||
formProps,
|
||||
}: Props) => {
|
||||
const isEditing = "element" in formProps;
|
||||
const initialValues = isEditing
|
||||
? formProps.element
|
||||
: {
|
||||
isAdmin: false,
|
||||
isBlocked: false,
|
||||
isDeleted: false,
|
||||
comment: "",
|
||||
roleKey: UserRoleEnum.USER,
|
||||
};
|
||||
|
||||
const form = useForm<Partial<UserSchema>>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
firstName: value => !value?.trim() && "Укажите имя пользователя",
|
||||
secondName: value => !value?.trim() && "Укажите фамилию",
|
||||
position: (value, values) =>
|
||||
values.role?.key === UserRoleEnum.EMPLOYEE &&
|
||||
!value &&
|
||||
"Необходимо указать должность сотрудника",
|
||||
phoneNumber: value =>
|
||||
!phone(value || "", {
|
||||
country: "",
|
||||
strictDetection: false,
|
||||
validateMobilePrefix: false,
|
||||
}).isValid && "Неверно указан номер телефона",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={closeModal}
|
||||
{...formProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Общая информация"}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
{...form.getInputProps("secondName")}
|
||||
label={"Фамилия"}
|
||||
placeholder={"Введите фамилию пользователя"}
|
||||
onChange={event =>
|
||||
form
|
||||
.getInputProps("secondName")
|
||||
.onChange(
|
||||
capitalize(
|
||||
event.target.value,
|
||||
).trim(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label={"Имя"}
|
||||
placeholder={"Введите имя пользователя"}
|
||||
{...form.getInputProps("firstName")}
|
||||
onChange={event =>
|
||||
form
|
||||
.getInputProps("firstName")
|
||||
.onChange(
|
||||
capitalize(
|
||||
event.target.value,
|
||||
).trim(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
{...form.getInputProps("patronymic")}
|
||||
label={"Отчество"}
|
||||
placeholder={"Введите отчество пользователя"}
|
||||
onChange={event =>
|
||||
form
|
||||
.getInputProps("patronymic")
|
||||
.onChange(
|
||||
capitalize(
|
||||
event.target.value,
|
||||
).trim(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input.Wrapper
|
||||
label={"Номер телефона"}
|
||||
error={form.getInputProps("phoneNumber").error}>
|
||||
<Input
|
||||
component={IMaskInput}
|
||||
mask="+7 000 000-00-00"
|
||||
placeholder={"Введите номер телефона"}
|
||||
{...form.getInputProps("phoneNumber")}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Паспортные данные"}>
|
||||
<Stack>
|
||||
<Input.Wrapper
|
||||
label={"Серия и номер паспорта"}
|
||||
error={form.getInputProps("passportData").error}>
|
||||
<Input
|
||||
component={IMaskInput}
|
||||
mask="00 00 000000"
|
||||
placeholder={"Введите серию и номер паспорта"}
|
||||
{...form.getInputProps("passportData")}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
{
|
||||
isEditing && (
|
||||
<PassportImageDropzone
|
||||
imageUrlInputProps={
|
||||
form.getInputProps(
|
||||
"passportImageUrl",
|
||||
) as BaseFormInputProps<string>
|
||||
}
|
||||
userId={formProps?.element.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Роль и должность"}>
|
||||
<Stack>
|
||||
<RoleSelect
|
||||
label={"Роль пользователя"}
|
||||
placeholder={"Выберите роль пользователя"}
|
||||
{...form.getInputProps("role")}
|
||||
/>
|
||||
{form.values.role?.key ===
|
||||
UserRoleEnum.EMPLOYEE && (
|
||||
<>
|
||||
<PositionSelect
|
||||
label={"Должность сотрудника"}
|
||||
placeholder={
|
||||
"Выберите должность сотрудника"
|
||||
}
|
||||
{...form.getInputProps("position")}
|
||||
/>
|
||||
<PayRateSelect
|
||||
label={"Тариф"}
|
||||
placeholder={
|
||||
"Выберите тариф сотрудника"
|
||||
}
|
||||
{...form.getInputProps("payRate")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Дополнительные параметры"}>
|
||||
<Stack>
|
||||
<Checkbox
|
||||
label={"Права администратора"}
|
||||
{...form.getInputProps("isAdmin", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Заблокирован"}
|
||||
{...form.getInputProps("isBlocked", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Textarea
|
||||
label={"Дополнительная информация"}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonTab;
|
||||
@@ -0,0 +1,58 @@
|
||||
import UserDepartmentInput from "../../../../components/UserDepartmentInput/UserDepartmentInput.tsx";
|
||||
import { Button, Fieldset, Stack } from "@mantine/core";
|
||||
import { useMap } from "@mantine/hooks";
|
||||
import { UserSchema, UserService } from "../../../../../../client";
|
||||
import { notifications } from "../../../../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
user: UserSchema;
|
||||
closeModal: () => void;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
const DepartmentSectionsTab = ({ user, closeModal, refetch }: Props) => {
|
||||
const userSectionIsChiefMap = useMap<number, boolean>(
|
||||
user.departmentSections?.map(s => [s.sectionId, s.isChief]),
|
||||
);
|
||||
|
||||
const onSubmit = () => {
|
||||
const departmentSections = userSectionIsChiefMap
|
||||
.entries()
|
||||
.toArray()
|
||||
.map(([sectionId, isChief]) => ({
|
||||
sectionId,
|
||||
isChief,
|
||||
}));
|
||||
|
||||
UserService.updateUserDepartmentSections({
|
||||
userId: user.id,
|
||||
requestBody: {
|
||||
departmentSections,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
refetch();
|
||||
notifications.guess(ok, { message });
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Fieldset legend={"Отделы"}>
|
||||
<UserDepartmentInput
|
||||
userSectionIsChiefMap={userSectionIsChiefMap}
|
||||
/>
|
||||
</Fieldset>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Сохранить отделы
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentSectionsTab;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { DepartmentSchema, DepartmentSectionSchema } from "../../../../../client";
|
||||
import { IconEdit, IconPlaylistAdd, IconTrash, IconUserPlus } from "@tabler/icons-react";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useDepartmentContext } from "../contexts/DepartmentContext.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
element: DepartmentSectionSchema | DepartmentSchema,
|
||||
isSection: boolean,
|
||||
level?: number,
|
||||
}
|
||||
|
||||
const DepartmentActions = ({ element, isSection, level = 1 }: Props) => {
|
||||
const {
|
||||
onCreateDepartmentClick,
|
||||
onUpdateDepartmentClick,
|
||||
onDeleteDepartmentClick,
|
||||
onAddUserClick,
|
||||
} = useDepartmentContext();
|
||||
|
||||
const getAction = (label: string, func: () => void, icon: ReactNode) => {
|
||||
return (
|
||||
<Tooltip label={label} key={label}>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
func();
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const actions = [
|
||||
isSection && (
|
||||
getAction(
|
||||
"Добавить пользователя в отдел",
|
||||
() => onAddUserClick(element as DepartmentSectionSchema),
|
||||
<IconUserPlus />,
|
||||
)
|
||||
),
|
||||
level < 9 && getAction(
|
||||
"Добавить отдел",
|
||||
() => onCreateDepartmentClick(true, {
|
||||
departmentId: isSection ? null : element.id,
|
||||
parentDepartmentSectionId: isSection ? element.id : null,
|
||||
name: "",
|
||||
}),
|
||||
<IconPlaylistAdd />,
|
||||
),
|
||||
getAction(
|
||||
"Редактировать",
|
||||
() => onUpdateDepartmentClick(element, isSection),
|
||||
<IconEdit />,
|
||||
),
|
||||
getAction(
|
||||
"Удалить",
|
||||
() => onDeleteDepartmentClick(element, isSection),
|
||||
<IconTrash />,
|
||||
),
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex gap={"md"} mx={"md"} direction={"row"}>
|
||||
{...actions}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentActions;
|
||||
@@ -1,24 +1,15 @@
|
||||
import { Button, Group, rem } from "@mantine/core";
|
||||
import { IconChevronsDown, IconChevronsUp, IconPlus } from "@tabler/icons-react";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { useDepartmentContext } from "../contexts/DepartmentContext.tsx";
|
||||
|
||||
|
||||
const DepartmentButtons = () => {
|
||||
const {
|
||||
departmentIds,
|
||||
toggleAllDepartmentIds,
|
||||
onCreateDepartmentClick,
|
||||
} = useDepartmentContext();
|
||||
|
||||
|
||||
return (
|
||||
<Group gap={"md"}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={toggleAllDepartmentIds}
|
||||
>
|
||||
{departmentIds.length > 0 ? <IconChevronsUp /> : <IconChevronsDown />}
|
||||
</Button>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateDepartmentClick(false)}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { DataTable } from "mantine-datatable";
|
||||
import { Box, Stack } from "@mantine/core";
|
||||
import { IconBuilding, IconChevronRight } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import classes from "./DepartmentsTree/DepartmentsTree.module.css";
|
||||
import { DepartmentSchema, DepartmentSectionSchema } from "../../../../../client";
|
||||
import "mantine-datatable/styles.css";
|
||||
import { useState } from "react";
|
||||
import DepartmentActions from "./DepartmentActions.tsx";
|
||||
import UsersTable from "./UsersTable.tsx";
|
||||
|
||||
type Props = {
|
||||
parentItem: DepartmentSchema | DepartmentSectionSchema;
|
||||
level: number;
|
||||
}
|
||||
|
||||
const DepartmentSectionsTable = ({ parentItem, level }: Props) => {
|
||||
const [departmentSectionIds, setDepartmentSectionIds] = useState<number[]>(
|
||||
parentItem.sections?.map(section => section.id) ?? [],
|
||||
);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
noHeader
|
||||
columns={[
|
||||
{
|
||||
accessor: "name",
|
||||
noWrap: true,
|
||||
render: ({ id, name }) => (
|
||||
<Box component="span" ml={20 * level}>
|
||||
<IconChevronRight
|
||||
className={clsx(classes.icon, classes.expandIcon, {
|
||||
[classes.expandIconRotated]: departmentSectionIds.includes(id),
|
||||
})}
|
||||
/>
|
||||
<IconBuilding className={classes.icon} />
|
||||
<span>{name}</span>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "actions",
|
||||
title: "",
|
||||
width: "0%",
|
||||
render: (departmentSection) => (
|
||||
<DepartmentActions element={departmentSection} isSection={true} level={level} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
records={parentItem.sections?.sort((a, b) => a.id - b.id)}
|
||||
rowExpansion={{
|
||||
allowMultiple: true,
|
||||
expanded: { recordIds: departmentSectionIds, onRecordIdsChange: setDepartmentSectionIds },
|
||||
content: ({ record }) => (
|
||||
<Stack gap={0}>
|
||||
{record.users && record.users.length > 0 && (
|
||||
<UsersTable
|
||||
key={`1 ${record.id}`}
|
||||
sectionId={record.id}
|
||||
users={record.users}
|
||||
level={level + 1}
|
||||
/>
|
||||
)}
|
||||
{record.sections && record.sections.length > 0 && (
|
||||
<DepartmentSectionsTable
|
||||
key={`2 ${record.id}`}
|
||||
parentItem={record}
|
||||
level={level + 1}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentSectionsTable;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Stack } from "@mantine/core";
|
||||
import DepartmentsTree from "./DepartmentsTree.tsx";
|
||||
import DepartmentsTree from "./DepartmentsTree/DepartmentsTree.tsx";
|
||||
import DepartmentButtons from "./DepartmentButtons.tsx";
|
||||
import { DepartmentContextProvider } from "../contexts/DepartmentContext.tsx";
|
||||
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { Accordion, ActionIcon, Center, Flex, Text, Title, Tooltip } from "@mantine/core";
|
||||
import { IconEdit, IconPlaylistAdd, IconTrash, IconUserPlus } from "@tabler/icons-react";
|
||||
import { DepartmentSchema, DepartmentSectionSchema } from "../../../../../client";
|
||||
import { useDepartmentContext } from "../contexts/DepartmentContext.tsx";
|
||||
import UsersTable from "./UsersTable.tsx";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
|
||||
const DepartmentsTree = () => {
|
||||
const {
|
||||
departments,
|
||||
departmentIds,
|
||||
setDepartmentIds,
|
||||
onCreateDepartmentClick,
|
||||
onUpdateDepartmentClick,
|
||||
onDeleteDepartmentClick,
|
||||
onAddUserClick,
|
||||
} = useDepartmentContext();
|
||||
|
||||
const getAction = (label: string, func: () => void, icon: ReactNode) => {
|
||||
return (
|
||||
<Tooltip label={label} key={label}>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={func}
|
||||
>
|
||||
{icon}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const getActions = (
|
||||
element: DepartmentSectionSchema | DepartmentSchema,
|
||||
isSection: boolean,
|
||||
) => {
|
||||
const actions = [
|
||||
isSection ? (
|
||||
getAction(
|
||||
"Добавить пользователя в отдел",
|
||||
() => onAddUserClick(element as DepartmentSectionSchema),
|
||||
<IconUserPlus />,
|
||||
)
|
||||
) : (
|
||||
getAction(
|
||||
"Добавить отдел",
|
||||
() => onCreateDepartmentClick(true, { departmentId: element.id, name: "" }),
|
||||
<IconPlaylistAdd />,
|
||||
)
|
||||
),
|
||||
getAction(
|
||||
"Редактировать",
|
||||
() => onUpdateDepartmentClick(element, isSection),
|
||||
<IconEdit />,
|
||||
),
|
||||
getAction(
|
||||
"Удалить",
|
||||
() => onDeleteDepartmentClick(element, isSection),
|
||||
<IconTrash />,
|
||||
),
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex gap={"md"} mx={"md"} direction={"row"}>
|
||||
{...actions}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const getUsersTable = (section: DepartmentSectionSchema) => {
|
||||
if (!section.users || section.users.length === 0) {
|
||||
return <Text>Сотрудники не добавлены</Text>;
|
||||
}
|
||||
return <UsersTable users={section.users} sectionId={section.id}/>;
|
||||
};
|
||||
|
||||
const getDepartmentSections = (department: DepartmentSchema) => {
|
||||
if (!department.sections || department.sections.length === 0) return;
|
||||
|
||||
const accordionIds: string[] = [];
|
||||
const sortedSections: DepartmentSectionSchema[] = department.sections?.sort((a, b) => a.id - b.id) ?? [];
|
||||
const items = sortedSections.map(section => {
|
||||
const value = `section ${section.id}`;
|
||||
accordionIds.push(value);
|
||||
return (
|
||||
<Accordion.Item key={section.id} value={value}>
|
||||
<Center>
|
||||
<Accordion.Control>
|
||||
<Title order={4}>Отдел - {section.name}</Title>
|
||||
</Accordion.Control>
|
||||
{getActions(section, true)}
|
||||
</Center>
|
||||
<Accordion.Panel>
|
||||
{getUsersTable(section)}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
multiple={true}
|
||||
defaultValue={accordionIds}
|
||||
bd={"solid 1px gray"}
|
||||
>
|
||||
{items}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
const getDepartments = departments.map((department) => {
|
||||
return (
|
||||
<Accordion.Item key={department.id} value={department.id.toString()}>
|
||||
<Center>
|
||||
<Accordion.Control>
|
||||
<Title order={4}>Департамент - {department.name}</Title>
|
||||
</Accordion.Control>
|
||||
{getActions(department, false)}
|
||||
</Center>
|
||||
<Accordion.Panel>
|
||||
{getDepartmentSections(department)}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
multiple={true}
|
||||
value={departmentIds}
|
||||
onChange={value => setDepartmentIds(value)}
|
||||
bd={"solid 1px gray"}
|
||||
>
|
||||
{getDepartments}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentsTree;
|
||||
@@ -0,0 +1,18 @@
|
||||
.icon {
|
||||
width: rem(13px);
|
||||
height: auto;
|
||||
vertical-align: rem(-1px);
|
||||
margin-right: rem(8px);
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.expandIconRotated {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.usersTableBorder {
|
||||
border-bottom: solid 1px var(--mantine-color-dark-5);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useDepartmentContext } from "../../contexts/DepartmentContext.tsx";
|
||||
import { DataTable } from "mantine-datatable";
|
||||
import { IconBuildings, IconChevronRight } from "@tabler/icons-react";
|
||||
import classes from "./DepartmentsTree.module.css";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import "mantine-datatable/styles.css";
|
||||
import DepartmentActions from "../DepartmentActions.tsx";
|
||||
import DepartmentSectionsTable from "../DepartmentSectionsTable.tsx";
|
||||
|
||||
const DepartmentsTree = () => {
|
||||
const { departments } = useDepartmentContext();
|
||||
|
||||
const [departmentIds, setDepartmentIds] = useState<number[]>([]);
|
||||
let isInitialSetting = true;
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialSetting && departments) {
|
||||
isInitialSetting = false;
|
||||
setDepartmentIds(departments.map(department => department.id));
|
||||
}
|
||||
}, [departments]);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
noHeader
|
||||
withTableBorder
|
||||
columns={[
|
||||
{
|
||||
accessor: "name",
|
||||
title: "Департамент",
|
||||
noWrap: true,
|
||||
render: ({ id, name }) => (
|
||||
<>
|
||||
<IconChevronRight
|
||||
className={clsx(classes.icon, classes.expandIcon, {
|
||||
[classes.expandIconRotated]: departmentIds?.includes(id),
|
||||
})}
|
||||
/>
|
||||
<IconBuildings className={classes.icon} />
|
||||
<span>{name}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "actions",
|
||||
title: "",
|
||||
width: "0%",
|
||||
render: (department) => (
|
||||
<DepartmentActions element={department} isSection={false} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
records={departments.sort((a, b) => a.id - b.id)}
|
||||
rowExpansion={{
|
||||
allowMultiple: true,
|
||||
expanded: { recordIds: departmentIds, onRecordIdsChange: setDepartmentIds },
|
||||
content: ({ record }) => (
|
||||
<DepartmentSectionsTable parentItem={record} level={1} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentsTree;
|
||||
@@ -1,63 +1,69 @@
|
||||
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useUsersTableColumns from "../hooks/useUsersTableColumns.tsx";
|
||||
import { DepartmentService, UserSchema } from "../../../../../client";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { useDepartmentContext } from "../contexts/DepartmentContext.tsx";
|
||||
|
||||
import { UserDepartmentSectionSchema } from "../../../../../client";
|
||||
import { DataTable } from "mantine-datatable";
|
||||
import "mantine-datatable/styles.css";
|
||||
import { Box } from "@mantine/core";
|
||||
import { IconChevronRight, IconUsers } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import classes from "./DepartmentsTree/DepartmentsTree.module.css";
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
users: UserSchema[];
|
||||
users: UserDepartmentSectionSchema[];
|
||||
sectionId: number;
|
||||
level: number;
|
||||
}
|
||||
|
||||
const UsersTable = ({ users, sectionId }: Props) => {
|
||||
const columns = useUsersTableColumns();
|
||||
const { fetchDepartments } = useDepartmentContext();
|
||||
const UsersTable = ({ users, sectionId, level }: Props) => {
|
||||
const columns = useUsersTableColumns({ sectionId });
|
||||
const [userIds, setUserIds] = useState<number[]>([1]);
|
||||
|
||||
const onDeleteUserClick = (userId: number) => {
|
||||
DepartmentService.deleteUser({
|
||||
requestBody: {
|
||||
sectionId,
|
||||
userId,
|
||||
}
|
||||
})
|
||||
.then(({ok, message}) => {
|
||||
fetchDepartments();
|
||||
if (ok) return;
|
||||
notifications.error({ message });
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
}
|
||||
const header = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Пользователи",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseTable
|
||||
data={users}
|
||||
columns={columns}
|
||||
|
||||
restProps={
|
||||
<DataTable
|
||||
noHeader
|
||||
withColumnBorders
|
||||
className={clsx(classes.usersTableBorder)}
|
||||
columns={[
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
positionActionsColumn: "last",
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteUserClick(row.original.id)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
accessor: "title",
|
||||
noWrap: true,
|
||||
render: ({ id, title }) => (
|
||||
<Box component="span" ml={20 * level}>
|
||||
<IconChevronRight
|
||||
className={clsx(classes.icon, classes.expandIcon, {
|
||||
[classes.expandIconRotated]: userIds.includes(id),
|
||||
})}
|
||||
/>
|
||||
<IconUsers className={classes.icon} />
|
||||
<span>{title}</span>
|
||||
</Box>
|
||||
),
|
||||
} as MRT_TableOptions<UserSchema>
|
||||
}
|
||||
},
|
||||
]}
|
||||
records={header}
|
||||
rowExpansion={{
|
||||
allowMultiple: true,
|
||||
expanded: { recordIds: userIds, onRecordIdsChange: setUserIds },
|
||||
content: () => (
|
||||
<Box pl={20 * (level + 2.5)}>
|
||||
<DataTable
|
||||
withTableBorder
|
||||
withColumnBorders
|
||||
columns={columns}
|
||||
records={users}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersTable;
|
||||
|
||||
@@ -12,14 +12,11 @@ import useDepartmentSectionCrud from "../hooks/useDepartmentSectionCrud.tsx";
|
||||
|
||||
type DepartmentContextState = {
|
||||
departments: DepartmentSchema[],
|
||||
departmentIds: string[],
|
||||
setDepartmentIds: React.Dispatch<React.SetStateAction<string[]>>,
|
||||
fetchDepartments: () => void,
|
||||
onCreateDepartmentClick: (isSection: boolean, element?: DepartmentSectionBaseSchema) => void,
|
||||
onUpdateDepartmentClick: (element: DepartmentSchema | DepartmentSectionSchema, isSection: boolean) => void,
|
||||
onDeleteDepartmentClick: (element: DepartmentSchema, isSection: boolean) => void,
|
||||
onAddUserClick: (departmentSection: DepartmentSectionSchema) => void,
|
||||
toggleAllDepartmentIds: () => void,
|
||||
};
|
||||
|
||||
const DepartmentContext = createContext<DepartmentContextState | undefined>(
|
||||
@@ -28,15 +25,11 @@ const DepartmentContext = createContext<DepartmentContextState | undefined>(
|
||||
|
||||
const useDepartmentContextState = () => {
|
||||
const [departments, setDepartments] = useState<DepartmentSchema[]>([]);
|
||||
const [departmentIds, setDepartmentIds] = useState<string[]>([]);
|
||||
|
||||
const fetchDepartments = () => {
|
||||
DepartmentService.getDepartments()
|
||||
.then(res => {
|
||||
setDepartments(res.departments);
|
||||
if (departmentIds.length === 0) {
|
||||
setDepartmentIds(res.departments.map(dep => dep.id.toString()));
|
||||
}
|
||||
})
|
||||
.catch(e => console.log(e));
|
||||
};
|
||||
@@ -66,17 +59,26 @@ const useDepartmentContextState = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateDepartmentClick = (isSection: boolean, element?: DepartmentSectionBaseSchema) => {
|
||||
const onCreateDepartmentClick = (
|
||||
isSection: boolean,
|
||||
element?: DepartmentSectionBaseSchema,
|
||||
) => {
|
||||
const title: string = isSection ? "Создание отдела" : "Создание департамента";
|
||||
openDepartmentModal(title, isSection, element);
|
||||
};
|
||||
|
||||
const onUpdateDepartmentClick = (element: DepartmentSchema | DepartmentSectionSchema, isSection: boolean) => {
|
||||
const onUpdateDepartmentClick = (
|
||||
element: DepartmentSchema | DepartmentSectionSchema,
|
||||
isSection: boolean,
|
||||
) => {
|
||||
const title: string = isSection ? "Редактирование отдела" : "Редактирование департамента";
|
||||
openDepartmentModal(title, isSection, element);
|
||||
};
|
||||
|
||||
const onDeleteDepartmentClick = (element: DepartmentSchema | DepartmentSectionSchema, isSection: boolean) => {
|
||||
const onDeleteDepartmentClick = (
|
||||
element: DepartmentSchema | DepartmentSectionSchema,
|
||||
isSection: boolean,
|
||||
) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление " + (isSection ? "отдела" : "департамента"),
|
||||
children: (
|
||||
@@ -90,7 +92,7 @@ const useDepartmentContextState = () => {
|
||||
if (isSection) {
|
||||
departmentSectionsCrud.onDelete(element as DepartmentSectionSchema);
|
||||
} else {
|
||||
departmentsCrud.onDelete(element);
|
||||
departmentsCrud.onDelete(element as DepartmentSchema);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -106,26 +108,15 @@ const useDepartmentContextState = () => {
|
||||
fetchDepartments,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const toggleAllDepartmentIds = () => {
|
||||
if (departmentIds.length > 0) {
|
||||
setDepartmentIds([]);
|
||||
return;
|
||||
}
|
||||
setDepartmentIds(departments.map(department => department.id.toString()));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
departments,
|
||||
departmentIds,
|
||||
setDepartmentIds,
|
||||
fetchDepartments,
|
||||
onCreateDepartmentClick,
|
||||
onUpdateDepartmentClick,
|
||||
onDeleteDepartmentClick,
|
||||
onAddUserClick,
|
||||
toggleAllDepartmentIds,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,31 +1,81 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { UserSchema } from "../../../../../client";
|
||||
import { IconCheck, IconTrash, IconX } from "@tabler/icons-react";
|
||||
import { DepartmentService, UserDepartmentSectionSchema } from "../../../../../client";
|
||||
import { DataTableColumn } from "mantine-datatable";
|
||||
import { ActionIcon, Box, Flex, Tooltip } from "@mantine/core";
|
||||
import { useDepartmentContext } from "../contexts/DepartmentContext.tsx";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
sectionId: number;
|
||||
}
|
||||
|
||||
const useUsersTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<UserSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "ФИО",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.secondName} ${row.original.firstName} ${row.original.patronymic}`,
|
||||
const useUsersTableColumns = ({ sectionId }: Props) => {
|
||||
const { fetchDepartments } = useDepartmentContext();
|
||||
|
||||
const onDeleteUserClick = (userId: number) => {
|
||||
DepartmentService.deleteUser({
|
||||
requestBody: {
|
||||
sectionId,
|
||||
userId,
|
||||
},
|
||||
{
|
||||
accessorKey: "role.name",
|
||||
header: "Роль",
|
||||
},
|
||||
{
|
||||
accessorKey: "position.name",
|
||||
header: "Должность",
|
||||
},
|
||||
{
|
||||
accessorKey: "comment",
|
||||
header: "Доп. информация",
|
||||
}
|
||||
],
|
||||
[],
|
||||
);
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
fetchDepartments();
|
||||
if (ok) return;
|
||||
notifications.error({ message });
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
title: "ФИО",
|
||||
accessor: "user.secondName",
|
||||
render: (userData) => (
|
||||
<Box component="span">
|
||||
<span>{userData.user.secondName} {userData.user.firstName} {userData.user.patronymic}</span>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "user.role.name",
|
||||
title: "Роль",
|
||||
},
|
||||
{
|
||||
accessor: "user.position.name",
|
||||
title: "Должность",
|
||||
},
|
||||
{
|
||||
accessor: "user.comment",
|
||||
title: "Доп. информация",
|
||||
},
|
||||
{
|
||||
accessor: "isChief",
|
||||
title: "Является руководителем",
|
||||
render: (userData) =>
|
||||
userData.isChief ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconX />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "actions",
|
||||
title: "",
|
||||
width: "0%",
|
||||
render: (userData) => (
|
||||
<Flex gap="md" mx={"md"}>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteUserClick(userData.user.id)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
] as DataTableColumn<UserDepartmentSectionSchema>[];
|
||||
};
|
||||
|
||||
export default useUsersTableColumns;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Flex, rem } from "@mantine/core";
|
||||
import { Button, Checkbox, Flex, rem } from "@mantine/core";
|
||||
import { DepartmentSectionSchema, DepartmentService } from "../../../../../client";
|
||||
import AddUserToDepartmentModalForm from "../types/AddUserToDepartmentModalForm.tsx";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
@@ -12,11 +12,15 @@ type Props = {
|
||||
}
|
||||
|
||||
const AddUserToDepartmentModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const form = useForm<Partial<AddUserToDepartmentModalForm>>({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const initialValues = {
|
||||
isChief: false,
|
||||
};
|
||||
const form = useForm<AddUserToDepartmentModalForm>({
|
||||
initialValues,
|
||||
validate: {
|
||||
user: user => !user && "Необходимо выбрать пользователя",
|
||||
},
|
||||
@@ -28,7 +32,8 @@ const AddUserToDepartmentModal = ({
|
||||
requestBody: {
|
||||
userId: form.values.user.id,
|
||||
sectionId: innerProps.departmentSection.id,
|
||||
}
|
||||
isChief: form.values.isChief,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
@@ -50,6 +55,10 @@ const AddUserToDepartmentModal = ({
|
||||
{...form.getInputProps("user")}
|
||||
sectionId={innerProps.departmentSection.id}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Является руководителем"}
|
||||
{...form.getInputProps("isChief", { type: "checkbox" })}
|
||||
/>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { UserSchema } from "../../../../../client";
|
||||
|
||||
type AddUserToDepartmentModalForm = {
|
||||
user: UserSchema;
|
||||
user?: UserSchema;
|
||||
isChief: boolean;
|
||||
}
|
||||
|
||||
export default AddUserToDepartmentModalForm;
|
||||
|
||||
@@ -1,50 +1,11 @@
|
||||
import useUsersList from "../../hooks/useUsersList.tsx";
|
||||
import UsersTable from "../../components/UsersTable/UsersTable.tsx";
|
||||
import { UserSchema, UserService } from "../../../../client";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { UsersTabContextProvider } from "./contexts/UsersTabContext.tsx";
|
||||
|
||||
const UsersTab = () => {
|
||||
const { objects: users, refetch } = useUsersList();
|
||||
|
||||
const onChange = (user: UserSchema) => {
|
||||
UserService.updateUser({
|
||||
requestBody: {
|
||||
data: {
|
||||
...user,
|
||||
positionKey: user.position?.key,
|
||||
roleKey: user.role.key,
|
||||
},
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
};
|
||||
const onDelete = async (user: UserSchema) => {
|
||||
onChange({ ...user, isDeleted: true });
|
||||
};
|
||||
const onCreate = (user: UserSchema) => {
|
||||
UserService.createUser({
|
||||
requestBody: {
|
||||
data: {
|
||||
...user,
|
||||
telegramId: -1,
|
||||
},
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
};
|
||||
return (
|
||||
<UsersTable
|
||||
items={users}
|
||||
onChange={onChange}
|
||||
onDelete={onDelete}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
<UsersTabContextProvider>
|
||||
<UsersTable />
|
||||
</UsersTabContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
86
src/pages/AdminPage/tabs/Users/contexts/UsersTabContext.tsx
Normal file
86
src/pages/AdminPage/tabs/Users/contexts/UsersTabContext.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { createContext, FC, useContext } from "react";
|
||||
import { UserSchema, UserService } from "../../../../../client";
|
||||
import useUsersList from "../../../hooks/useUsersList.tsx";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { CRUDTableProps } from "../../../../../types/CRUDTable.tsx";
|
||||
|
||||
type UsersTabContextState = {
|
||||
refetch: () => void;
|
||||
usersCrud: CRUDTableProps<UserSchema>;
|
||||
};
|
||||
|
||||
const UsersTabContext = createContext<UsersTabContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const useUsersTabContextState = () => {
|
||||
const { objects: users, refetch } = useUsersList();
|
||||
|
||||
const onChange = (user: UserSchema) => {
|
||||
UserService.updateUser({
|
||||
requestBody: {
|
||||
data: {
|
||||
...user,
|
||||
positionKey: user.position?.key,
|
||||
roleKey: user.role.key,
|
||||
},
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
};
|
||||
const onDelete = async (user: UserSchema) => {
|
||||
onChange({ ...user, isDeleted: true });
|
||||
};
|
||||
const onCreate = (user: UserSchema) => {
|
||||
UserService.createUser({
|
||||
requestBody: {
|
||||
data: {
|
||||
...user,
|
||||
telegramId: -1,
|
||||
},
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
};
|
||||
|
||||
const usersCrud: CRUDTableProps<UserSchema> = {
|
||||
items: users,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onChange,
|
||||
}
|
||||
|
||||
return {
|
||||
usersCrud,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
type UsersTabContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const UsersTabContextProvider: FC<UsersTabContextProviderProps> = ({ children }) => {
|
||||
const state = useUsersTabContextState();
|
||||
return (
|
||||
<UsersTabContext.Provider value={state}>
|
||||
{children}
|
||||
</UsersTabContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUsersTabContext = () => {
|
||||
const context = useContext(UsersTabContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useUsersTabContext must be used within a UsersTabContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user