This commit is contained in:
2024-07-20 09:32:01 +03:00
parent 5c6e7cf5f5
commit 54c9ca8908
48 changed files with 1057 additions and 87 deletions

View File

@@ -0,0 +1,5 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
}

View File

@@ -0,0 +1,53 @@
import styles from './AdminPage.module.css';
import {Tabs} from "@mantine/core";
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
import {IconBriefcase, IconUser, IconUsersGroup} from "@tabler/icons-react";
import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx";
import UsersTab from "./tabs/Users/UsersTab.tsx";
import {motion} from "framer-motion";
const AdminPage = () => {
return (
<div className={styles['container']}>
<PageBlock fullHeight>
<Tabs variant={"outline"} keepMounted={false} defaultValue={"users"}>
<Tabs.List>
<Tabs.Tab value={"users"} leftSection={<IconUser/>}>
Пользователи
</Tabs.Tab>
<Tabs.Tab value={"rolesAndPositions"} leftSection={<IconBriefcase/>}>
Роли и должности
</Tabs.Tab>
<Tabs.Tab value={"employees"} leftSection={<IconUsersGroup/>}>
Сотрудники
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value={"users"}>
<motion.div
initial={{scaleY: 0}}
animate={{scaleY: 1}}
transition={{duration: 0.1}}>
<UsersTab/>
</motion.div>
</Tabs.Panel>
<Tabs.Panel value={"rolesAndPositions"}>
<motion.div
initial={{scaleY: 0}}
animate={{scaleY: 1}}
transition={{duration: 0.1}}
>
<RolesAndPositionsTab/>
</motion.div>
</Tabs.Panel>
</Tabs>
</PageBlock>
</div>
)
}
export default AdminPage;

View File

@@ -0,0 +1,19 @@
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
import {PositionSchema} from "../../../../client";
import {FC} from "react";
import usePositionsList from "../../hooks/usePositionsList.tsx";
type Props = Omit<ObjectSelectProps<PositionSchema>, 'data' | 'getLabelFn' | 'getValueFn'>;
const PositionSelect: FC<Props> = (props) => {
const {objects: positions} = usePositionsList();
return (
<ObjectSelect
getLabelFn={(position) => position.name}
getValueFn={(position) => position.key}
data={positions}
{...props}
/>
)
}
export default PositionSelect;

View File

@@ -0,0 +1,41 @@
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {PositionSchema} from "../../../../client";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {usePositionsTableColumns} from "./columns.tsx";
import {FC} from "react";
import {Button, Flex, rem} from "@mantine/core";
type Props = CRUDTableProps<PositionSchema>;
const PositionsTable: FC<Props> = ({items}) => {
const columns = usePositionsTableColumns();
return (
<BaseTable
restProps={{
enableTopToolbar: true,
enableSorting: false,
enableColumnActions: false,
renderTopToolbar: () => (
<Flex p={rem(10)}>
<Button
variant={"default"}
onClick={() => {
}}
>
Создать должность
</Button>
</Flex>
)
}}
data={items}
columns={columns}
/>
)
}
export default PositionsTable;

View File

@@ -0,0 +1,16 @@
import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table";
import {PositionSchema} from "../../../../client";
export const usePositionsTableColumns = () => {
return useMemo<MRT_ColumnDef<PositionSchema>[]>(() => [
{
accessorKey: "key",
header: "Ключ"
},
{
accessorKey: "name",
header: "Название должности"
}
], []);
}

View File

@@ -0,0 +1,19 @@
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
import {RoleSchema} from "../../../../client";
import {FC} from "react";
import useRolesList from "../../hooks/useRolesList.tsx";
type Props = Omit<ObjectSelectProps<RoleSchema>, 'data' | 'getLabelFn' | 'getValueFn'>;
const RolesSelect: FC<Props> = (props) => {
const {objects: roles} = useRolesList();
return (
<ObjectSelect
getLabelFn={(position) => position.name}
getValueFn={(position) => position.key}
data={roles}
{...props}
/>
)
}
export default RolesSelect;

View File

@@ -0,0 +1,81 @@
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
import {UserSchema} from "../../../../client";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {FC} from "react";
import {ActionIcon, Flex, Text, Tooltip} from "@mantine/core";
import {useUsersTableColumns} from "./columns.tsx";
import {IconEdit, IconTrash} from "@tabler/icons-react";
import {modals} from "@mantine/modals";
import {MRT_TableOptions} from "mantine-react-table";
type Props = CRUDTableProps<UserSchema>;
const UsersTable: FC<Props> = ({items, onChange, onDelete}) => {
const columns = useUsersTableColumns();
const onEditClick = (user: UserSchema) => {
if (!onChange) return;
modals.openContextModal({
modal: "userFormModal",
title: 'Редактирование пользователя',
withCloseButton: false,
innerProps: {
onChange: onChange,
element: user,
},
size: "md"
})
}
const onDeleteClick = (user: UserSchema) => {
if (!onDelete) return;
modals.openConfirmModal({
title: 'Удаление пользователя',
centered: true,
children: (
<Text size="sm">
Вы уверены что хотите удалить пользователя {user.firstName} {user.secondName}
</Text>
),
labels: {confirm: 'Да', cancel: "Нет"},
confirmProps: {color: 'red'},
onConfirm: () => onDelete(user)
});
}
return (
<BaseTable
data={items}
columns={columns}
restProps={{
enableTopToolbar: false,
enableSorting: false,
enableColumnActions: false,
enableRowActions: true,
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip onClick={() => {
onDeleteClick(row.original);
}} label="Удалить">
<ActionIcon variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Tooltip
onClick={() => {
onEditClick(row.original)
}}
label="Редактировать">
<ActionIcon
variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<UserSchema>}
/>
)
}
export default UsersTable;

View File

@@ -0,0 +1,40 @@
import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table";
import {UserSchema} from "../../../../client";
import {IconCheck, IconX} from "@tabler/icons-react";
export const useUsersTableColumns = () => {
return useMemo<MRT_ColumnDef<UserSchema>[]>(() => [
{
accessorKey: "id",
header: "ID"
},
{
accessorKey: "telegramId",
header: "ID Телеграм"
},
{
accessorKey: "phoneNumber",
header: "Номер телефона"
},
{
accessorKey: "role.name",
header: "Роль"
},
{
accessorKey: "comment",
header: "Дополнительная информация"
},
{
accessorKey: "isAdmin",
header: "Администратор",
Cell: ({row}) => row.original.isAdmin ? <IconCheck/> : <IconX/>
},
{
accessorKey: "isBlocked",
header: "Заблокирован",
Cell: ({row}) => row.original.isBlocked ? <IconCheck/> : <IconX/>
},
], []);
}

View File

@@ -0,0 +1,9 @@
import {PositionService} from "../../../client";
import ObjectList from "../../../hooks/objectList.tsx";
const usePositionsList = () => ObjectList({
queryFn: PositionService.getAllPositions,
getObjectsFn: response => response.positions,
queryKey: "getAllPositions"
})
export default usePositionsList;

View File

@@ -0,0 +1,9 @@
import ObjectList from "../../../hooks/objectList.tsx";
import {RoleService} from "../../../client";
const useRolesList = () => ObjectList({
queryFn: RoleService.getAllRoles,
getObjectsFn: response => response.roles,
queryKey: "getAllRoles"
})
export default useRolesList;

View File

@@ -0,0 +1,11 @@
import ObjectList from "../../../hooks/objectList.tsx";
import {UserService} from "../../../client";
const useUsersList = () => ObjectList({
queryFn: UserService.getAllUsers,
getObjectsFn: (response) => response.users,
queryKey: "getAllUsers"
});
export default useUsersList;

View File

@@ -0,0 +1,108 @@
import {ContextModalProps} from "@mantine/modals";
import BaseFormModal, {EditProps} 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";
type Props = EditProps<UserSchema>;
const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
const initialValues = innerProps.element;
const form = useForm<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 && 'Неверно указан номер телефона',
}
});
console.log(form.getInputProps('isAdmin'))
return (<BaseFormModal
form={form}
closeOnSubmit
onClose={() => context.closeContextModal(id)}
{...innerProps}
>
<BaseFormModal.Body>
<>
<Fieldset legend={"Общая информация"}>
<Stack>
<TextInput
label={"Имя"}
placeholder={"Введите имя пользователя"}
{...form.getInputProps("firstName")}
onChange={event => form.getInputProps('firstName').onChange(capitalize(event.target.value).trim())}
/>
<TextInput
{...form.getInputProps("secondName")}
label={"Фамилия"}
placeholder={"Введите фамилию пользователя"}
onChange={event => form.getInputProps('secondName').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>
<RoleSelect
label={"Роль пользователя"}
placeholder={"Выберите роль пользователя"}
{...form.getInputProps('role')}
/>
{form.values.role.key === UserRoleEnum.EMPLOYEE &&
<PositionSelect
label={"Должность сотрудника"}
placeholder={"Выберите должность сотрудника"}
{...form.getInputProps('position')}
/>
}
</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 UserFormModal;

View File

@@ -0,0 +1,39 @@
import {Tabs} from "@mantine/core";
import PositionsTable from "../../components/PositionsTable/PositionsTable.tsx";
import usePositionsList from "../../hooks/usePositionsList.tsx";
import {motion} from "framer-motion";
const RolesAndPositionsTab = () => {
const {objects: positions} = usePositionsList();
return (
<Tabs w={"100%"}
variant={"default"}
keepMounted={false}
defaultValue={"roles"}
>
<Tabs.List grow>
<Tabs.Tab value={"roles"}>Роли</Tabs.Tab>
<Tabs.Tab value={"positions"}>Должности</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value={"roles"}>
<motion.div
initial={{scaleY: 0}}
animate={{scaleY: 1}}
transition={{duration: 0.1}}
>
<PositionsTable items={positions}/>
</motion.div>
</Tabs.Panel>
<Tabs.Panel value={"positions"}>
<motion.div
initial={{scaleY: 0}}
animate={{scaleY: 1}}
transition={{duration: 0.1}}
>
<PositionsTable items={positions}/>
</motion.div>
</Tabs.Panel>
</Tabs>
)
}
export default RolesAndPositionsTab;

View File

@@ -0,0 +1,35 @@
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";
const UsersTab = () => {
const {objects: users, refetch} = useUsersList();
const onChange = (user: UserSchema) => {
UserService.updateUser({
requestBody: {
data: {
...user,
positionKey: user.position?.key,
}
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
if (!ok) return;
await refetch();
})
}
const onDelete = async (user: UserSchema) => {
onChange({...user, isDeleted: true});
}
return (
<UsersTable
items={users}
onChange={onChange}
onDelete={onDelete}
/>
)
}
export default UsersTab;

View File

@@ -2,23 +2,22 @@ import {UseFormReturnType} from "@mantine/form";
import {Button, Flex, rem} from "@mantine/core";
import {FC} from "react";
type CreateProps<T> = {
export type CreateProps<T> = {
onCreate(values: T): void;
}
type EditProps<T> = {
export type EditProps<T> = {
onChange(values: T): void;
element: T;
}
export type CreateEditFormProps<T> = CreateProps<T> | EditProps<T>;
type BaseProps<T> = {
export type BaseFormProps<T> = {
form: UseFormReturnType<T>
onClose: () => void;
closeOnSubmit?: boolean;
children: React.JSX.Element;
}
type Props<T> = BaseProps<T> & (CreateProps<T> | EditProps<T>);
type Props<T> = BaseFormProps<T> & (CreateProps<T> | EditProps<T>);
const BaseFormModal = <T, >(props: Props<T>) => {
const {closeOnSubmit = false} = props;

View File

@@ -2,15 +2,18 @@ import {FC} from "react";
import {useDealPageContext} from "../../../contexts/DealPageContext.tsx";
import {Button, Checkbox, Divider, Fieldset, Flex, Group, rem, Textarea, TextInput} from "@mantine/core";
import {useForm} from "@mantine/form";
import {ClientService, DealSchema, DealService} from "../../../../../client";
import {ClientService, DealSchema, DealService, ShippingWarehouseSchema} from "../../../../../client";
import {DealStatus, DealStatusDictionary} from "../../../../../shared/enums/DealStatus.ts";
import {isEqual} from "lodash";
import {notifications} from "../../../../../shared/lib/notifications.ts";
import {useQueryClient} from "@tanstack/react-query";
import ShippingWarehouseAutocomplete
from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
type Props = {
deal: DealSchema
}
type FormType = Omit<DealSchema, 'statusHistory' | 'services' | 'products'>
const Content: FC<Props> = ({deal}) => {
@@ -30,7 +33,7 @@ const Content: FC<Props> = ({deal}) => {
return DealService.updateDealGeneralInfo({
requestBody: {
dealId: deal.id,
data: values
data: {...values, shippingWarehouse: values.shippingWarehouse?.toString()}
}
}).then(({ok, message}) => {
notifications.guess(ok, {message});
@@ -61,6 +64,10 @@ const Content: FC<Props> = ({deal}) => {
// updating deal info
await updateDealInfo(values);
}
const isShippingWarehouse = (value: (ShippingWarehouseSchema | string | null | undefined)): value is ShippingWarehouseSchema => {
return !["string", "null", "undefined"].includes((typeof value));
}
return (
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
<Flex direction={'column'}>
@@ -87,11 +94,17 @@ const Content: FC<Props> = ({deal}) => {
placeholder={'Введите коментарий к сделке'}
{...form.getInputProps('comment')}
/>
<TextInput
disabled
<ShippingWarehouseAutocomplete
placeholder={"Введите склад отгрузки"}
label={"Склад отгрузки"}
value={form.values.shippingWarehouse?.name}
value={isShippingWarehouse(form.values.shippingWarehouse) ? form.values.shippingWarehouse : undefined}
onChange={event => {
if (isShippingWarehouse(event)) {
form.getInputProps('shippingWarehouse').onChange(event.name)
return
}
form.getInputProps('shippingWarehouse').onChange(event)
}}
/>
</Flex>
</Fieldset>

View File

@@ -5,17 +5,27 @@ import {AuthService} from "../../client";
import TelegramLoginButton, {TelegramUser} from "../../components/TelegramAuthButton/TelegramAuthButton.tsx";
import {notifications} from "../../shared/lib/notifications.ts";
import {login} from "../../features/authSlice.ts";
import {useNavigate} from "@tanstack/react-router";
import {Navigate, useNavigate} from "@tanstack/react-router";
import {useSelector} from "react-redux";
import {useEffect} from "react";
const LoginPage = () => {
const dispatch = useAppDispatch();
const authState = useSelector((state: RootState) => state.auth);
const navigate = useNavigate();
useEffect(() => {
if (authState.isAuthorized)
// ???????????
navigate({to: "/leads"}).then(() => {
navigate({to: "/leads"}).then(() => {
notifications.success({message: "Вы успешно вошли!"})
});
});
}, [authState.isAuthorized])
if (authState.isAuthorized) {
navigate({to: "/leads"})
return (<></>)
return (<Navigate to={"/leads"}/>)
}
return (
<Container size={420} my={40}>
@@ -41,9 +51,6 @@ const LoginPage = () => {
AuthService.loginAuthLoginPost({requestBody: data})
.then(({accessToken}) => {
dispatch(login({accessToken: accessToken}));
navigate({to: "/"}).then(() => {
notifications.success({message: "Вы успешно вошли!"})
})
}).catch(() => {
notifications.error({message: "Неудалось войти!"})
})

View File

@@ -1,12 +1,19 @@
import {Outlet} from "@tanstack/react-router";
import {useMatch, useMatches} from "@tanstack/react-router";
import {useEffect} from "react";
import {useSelector} from "react-redux";
import {RootState} from "../../redux/store.ts";
import {OpenAPI} from "../../client";
import PageWrapper from "../PageWrapper/PageWrapper.tsx";
import {LoadingOverlay} from "@mantine/core";
import {AnimatePresence} from "framer-motion";
import AnimatedOutlet from "../../components/AnimatedOutlet/au.tsx";
const RootPage = () => {
const matches = useMatches();
const match = useMatch({strict: false});
const nextMatchIndex = matches.findIndex((d) => d.id === match.id) + 1;
const nextMatch = matches[nextMatchIndex];
const authState = useSelector((state: RootState) => state.auth);
const uiState = useSelector((state: RootState) => state.ui);
const rewriteLocalStorage = () => {
@@ -20,12 +27,13 @@ const RootPage = () => {
rewriteLocalStorage();
setOpenApiToken();
}, [authState]);
return (
<>
<LoadingOverlay visible={uiState.isLoading}/>
<PageWrapper>
<Outlet/>
<AnimatePresence mode="popLayout">
<AnimatedOutlet key={nextMatch.id}/>
</AnimatePresence>
</PageWrapper>
</>

View File

@@ -86,6 +86,9 @@ export const ServicesPage: FC = () => {
await refetch();
})
}
return (
<div className={styles['container']}>
<PageBlock>
@@ -100,11 +103,11 @@ export const ServicesPage: FC = () => {
</div>
</PageBlock>
<PageBlock>
<ServicesTable
onDelete={onServiceDelete}
onChange={onServiceUpdate}
items={services.filter(service => service.serviceType == serviceType)}
/>
<ServicesTable
onDelete={onServiceDelete}
onChange={onServiceUpdate}
items={services.filter(service => service.serviceType == serviceType)}
/>
</PageBlock>
</div>
)