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

@@ -11,54 +11,58 @@
"generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
},
"dependencies": {
"@hello-pangea/dnd": "^16.5.0",
"@mantine/core": "^7.6.1",
"@mantine/dates": "^7.6.1",
"@mantine/dropzone": "^7.9.2",
"@mantine/form": "^7.6.1",
"@mantine/hooks": "^7.6.1",
"@mantine/modals": "^7.6.1",
"@mantine/notifications": "^7.6.1",
"@reduxjs/toolkit": "^2.2.1",
"@tabler/icons-react": "^2.47.0",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-router": "^1.16.6",
"@tanstack/router-devtools": "^1.16.6",
"@tanstack/router-vite-plugin": "^1.16.5",
"axios": "^1.6.7",
"@hello-pangea/dnd": "^16.6.0",
"@mantine/core": "^7.11.2",
"@mantine/dates": "^7.11.2",
"@mantine/dropzone": "^7.11.2",
"@mantine/form": "^7.11.2",
"@mantine/hooks": "^7.11.2",
"@mantine/modals": "^7.11.2",
"@mantine/notifications": "^7.11.2",
"@reduxjs/toolkit": "^2.2.6",
"@tabler/icons-react": "^3.11.0",
"@tanstack/react-query": "^5.51.9",
"@tanstack/react-router": "^1.45.6",
"@tanstack/router-devtools": "^1.45.6",
"@tanstack/router-vite-plugin": "^1.45.3",
"axios": "^1.7.2",
"classnames": "^2.5.1",
"clsx": "^2.1.0",
"dayjs": "^1.11.10",
"dot-object": "^2.1.4",
"clsx": "^2.1.1",
"dayjs": "^1.11.12",
"dot-object": "^2.1.5",
"framer-motion": "^11.3.8",
"lodash": "^4.17.21",
"mantine-form-zod-resolver": "^1.1.0",
"mantine-react-table": "^2.0.0-beta.0",
"react": "^18.2.0",
"react-barcode": "^1.5.1",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"mantine-react-table": "^2.0.0-beta.5",
"phone": "^3.1.49",
"react": "^18.3.1",
"react-barcode": "^1.5.3",
"react-dom": "^18.3.1",
"react-imask": "^7.6.1",
"react-redux": "^9.1.2",
"react-to-print": "^2.15.1",
"reactflow": "^11.10.4",
"zod": "^3.22.4"
"reactflow": "^11.11.4",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/dot-object": "^2",
"@types/lodash": "^4",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"openapi-typescript-codegen": "^0.27.0",
"postcss": "^8.4.35",
"postcss-preset-mantine": "^1.13.0",
"@types/dot-object": "^2.1.6",
"@types/lodash": "^4.17.7",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@vitejs/plugin-react-swc": "^3.7.0",
"eslint": "^9.7.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.8",
"openapi-typescript-codegen": "^0.29.0",
"postcss": "^8.4.39",
"postcss-preset-mantine": "^1.16.0",
"postcss-simple-vars": "^7.0.1",
"sass": "^1.71.1",
"typescript": "^5.2.2",
"vite": "^5.1.4"
"sass": "^1.77.8",
"typescript": "^5.5.3",
"vite": "^5.3.4",
"yarn-upgrade-all": "^0.7.2"
},
"packageManager": "yarn@4.1.0"
}

View File

@@ -36,6 +36,8 @@ export type { ClientUpdateRequest } from './models/ClientUpdateRequest';
export type { ClientUpdateResponse } from './models/ClientUpdateResponse';
export type { CreateBarcodeTemplateAttributeRequest } from './models/CreateBarcodeTemplateAttributeRequest';
export type { CreateBarcodeTemplateAttributeResponse } from './models/CreateBarcodeTemplateAttributeResponse';
export type { CreatePositionRequest } from './models/CreatePositionRequest';
export type { CreatePositionResponse } from './models/CreatePositionResponse';
export type { DealAddProductRequest } from './models/DealAddProductRequest';
export type { DealAddProductResponse } from './models/DealAddProductResponse';
export type { DealAddServiceRequest } from './models/DealAddServiceRequest';
@@ -80,7 +82,10 @@ export type { DealUpdateServiceResponse } from './models/DealUpdateServiceRespon
export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBarcodeTemplateAttributesResponse';
export type { GetAllBarcodeTemplateSizesResponse } from './models/GetAllBarcodeTemplateSizesResponse';
export type { GetAllBarcodeTemplatesResponse } from './models/GetAllBarcodeTemplatesResponse';
export type { GetAllPositionsResponse } from './models/GetAllPositionsResponse';
export type { GetAllRolesResponse } from './models/GetAllRolesResponse';
export type { GetAllShippingWarehousesResponse } from './models/GetAllShippingWarehousesResponse';
export type { GetAllUsersResponse } from './models/GetAllUsersResponse';
export type { GetBarcodeTemplateByIdRequest } from './models/GetBarcodeTemplateByIdRequest';
export type { GetBarcodeTemplateByIdResponse } from './models/GetBarcodeTemplateByIdResponse';
export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest';
@@ -89,6 +94,8 @@ export type { GetProductBarcodeRequest } from './models/GetProductBarcodeRequest
export type { GetProductBarcodeResponse } from './models/GetProductBarcodeResponse';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { PaginationInfoSchema } from './models/PaginationInfoSchema';
export type { PermissionSchema } from './models/PermissionSchema';
export type { PositionSchema } from './models/PositionSchema';
export type { ProductAddBarcodeRequest } from './models/ProductAddBarcodeRequest';
export type { ProductAddBarcodeResponse } from './models/ProductAddBarcodeResponse';
export type { ProductCreateRequest } from './models/ProductCreateRequest';
@@ -104,6 +111,7 @@ export type { ProductSchema } from './models/ProductSchema';
export type { ProductUpdateRequest } from './models/ProductUpdateRequest';
export type { ProductUpdateResponse } from './models/ProductUpdateResponse';
export type { ProductUploadImageResponse } from './models/ProductUploadImageResponse';
export type { RoleSchema } from './models/RoleSchema';
export type { ServiceCategorySchema } from './models/ServiceCategorySchema';
export type { ServiceCreateCategoryRequest } from './models/ServiceCreateCategoryRequest';
export type { ServiceCreateCategoryResponse } from './models/ServiceCreateCategoryResponse';
@@ -118,13 +126,19 @@ export type { ServiceSchema } from './models/ServiceSchema';
export type { ServiceUpdateRequest } from './models/ServiceUpdateRequest';
export type { ServiceUpdateResponse } from './models/ServiceUpdateResponse';
export type { ShippingWarehouseSchema } from './models/ShippingWarehouseSchema';
export type { UpdateUserRequest } from './models/UpdateUserRequest';
export type { UpdateUserResponse } from './models/UpdateUserResponse';
export type { UserSchema } from './models/UserSchema';
export type { UserUpdate } from './models/UserUpdate';
export type { ValidationError } from './models/ValidationError';
export { AuthService } from './services/AuthService';
export { BarcodeService } from './services/BarcodeService';
export { ClientService } from './services/ClientService';
export { DealService } from './services/DealService';
export { PositionService } from './services/PositionService';
export { ProductService } from './services/ProductService';
export { RoleService } from './services/RoleService';
export { ServiceService } from './services/ServiceService';
export { ShippingWarehouseService } from './services/ShippingWarehouseService';
export { UserService } from './services/UserService';

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PositionSchema } from './PositionSchema';
export type CreatePositionRequest = {
data: PositionSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreatePositionResponse = {
ok: boolean;
message: string;
};

View File

@@ -7,5 +7,6 @@ export type DealGeneralInfoSchema = {
isDeleted: boolean;
isCompleted: boolean;
comment: string;
shippingWarehouse?: (string | null);
};

View File

@@ -20,6 +20,6 @@ export type DealSchema = {
isCompleted: boolean;
client: ClientSchema;
comment: string;
shippingWarehouse?: (ShippingWarehouseSchema | null);
shippingWarehouse?: (ShippingWarehouseSchema | string | null);
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PositionSchema } from './PositionSchema';
export type GetAllPositionsResponse = {
positions: Array<PositionSchema>;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RoleSchema } from './RoleSchema';
export type GetAllRolesResponse = {
roles: Array<RoleSchema>;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { UserSchema } from './UserSchema';
export type GetAllUsersResponse = {
users: Array<UserSchema>;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type PermissionSchema = {
key: string;
name: string;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type PositionSchema = {
name: string;
key: string;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PermissionSchema } from './PermissionSchema';
export type RoleSchema = {
key: string;
name: string;
permissions: Array<PermissionSchema>;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { UserUpdate } from './UserUpdate';
export type UpdateUserRequest = {
data: UserUpdate;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateUserResponse = {
ok: boolean;
message: string;
};

View File

@@ -2,10 +2,20 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PositionSchema } from './PositionSchema';
import type { RoleSchema } from './RoleSchema';
export type UserSchema = {
id: number;
telegramId: number;
phoneNumber?: (string | null);
firstName: string;
secondName: string;
comment: string;
isAdmin: boolean;
isBlocked: boolean;
isDeleted: boolean;
roleKey: string;
role: RoleSchema;
position?: (PositionSchema | null);
};

View File

@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UserUpdate = {
id: number;
telegramId: number;
phoneNumber?: (string | null);
firstName: string;
secondName: string;
comment: string;
isAdmin: boolean;
isBlocked: boolean;
isDeleted: boolean;
positionKey?: (string | null);
};

View File

@@ -0,0 +1,43 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CreatePositionRequest } from '../models/CreatePositionRequest';
import type { CreatePositionResponse } from '../models/CreatePositionResponse';
import type { GetAllPositionsResponse } from '../models/GetAllPositionsResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class PositionService {
/**
* Get All
* @returns GetAllPositionsResponse Successful Response
* @throws ApiError
*/
public static getAllPositions(): CancelablePromise<GetAllPositionsResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/position/get-all',
});
}
/**
* Create
* @returns CreatePositionResponse Successful Response
* @throws ApiError
*/
public static createPosition({
requestBody,
}: {
requestBody: CreatePositionRequest,
}): CancelablePromise<CreatePositionResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/position/create',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -0,0 +1,21 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GetAllRolesResponse } from '../models/GetAllRolesResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class RoleService {
/**
* Get All
* @returns GetAllRolesResponse Successful Response
* @throws ApiError
*/
public static getAllRoles(): CancelablePromise<GetAllRolesResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/role/get-all',
});
}
}

View File

@@ -0,0 +1,43 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GetAllUsersResponse } from '../models/GetAllUsersResponse';
import type { UpdateUserRequest } from '../models/UpdateUserRequest';
import type { UpdateUserResponse } from '../models/UpdateUserResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class UserService {
/**
* Get All
* @returns GetAllUsersResponse Successful Response
* @throws ApiError
*/
public static getAllUsers(): CancelablePromise<GetAllUsersResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/user/get-all',
});
}
/**
* Update
* @returns UpdateUserResponse Successful Response
* @throws ApiError
*/
public static updateUser({
requestBody,
}: {
requestBody: UpdateUserRequest,
}): CancelablePromise<UpdateUserResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/user/update',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -0,0 +1,34 @@
import {forwardRef, RefObject, useContext, useRef} from "react";
import {getRouterContext, Outlet} from "@tanstack/react-router";
import {motion, useIsPresent} from "framer-motion";
import {cloneDeep} from "lodash";
const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
const RouterContext = getRouterContext();
const routerContext = useContext(RouterContext);
const renderedContext = useRef(routerContext);
const isPresent = useIsPresent();
if (isPresent) {
renderedContext.current = cloneDeep(routerContext);
}
return (
<motion.div ref={ref}
initial={{x: "-100%"}}
animate={{x: 0}}
transition={{duration: 0.3}}
onAnimationComplete={()=>{
(ref as RefObject<HTMLDivElement>).current?.style.removeProperty("transform")
}}
>
<RouterContext.Provider value={renderedContext.current}>
<Outlet/>
</RouterContext.Provider>
</motion.div>
);
});
export default AnimatedOutlet

View File

@@ -26,7 +26,7 @@ export type BaseTableRef<T extends MRT_RowData> = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>((props, ref) => {
const {data, columns, restProps, striped, onSelectionChange} = props;
const {data, columns, restProps, striped = true, onSelectionChange} = props;
const table = useMantineReactTable({
localization: MRT_Localization_RU,

View File

@@ -2,7 +2,7 @@ import {Center, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColorSchem
import {
IconBarcode,
IconBox,
IconCash,
IconCash, IconDashboard,
IconFileBarcode,
IconHome2,
IconLogout,
@@ -76,7 +76,7 @@ export function Navbar() {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const router = useRouterState();
const {colorScheme, toggleColorScheme} = useMantineColorScheme({keepTransitions: false});
const {colorScheme, toggleColorScheme} = useMantineColorScheme({keepTransitions: true});
const onLogoutClick = () => {
dispatch(logout());
navigate({to: '/login'});
@@ -110,6 +110,12 @@ export function Navbar() {
</div>
<Stack w={"100%"} justify="center" gap={0}>
<NavbarLink icon={IconDashboard}
href={"/admin"}
index={-1}
label={"Панель администратора"}
onClick={() => onNavlinkClick({href: "/admin", index: -1, icon: IconDashboard})}
/>
<NavbarLink label={"Сменить тему"} onClick={toggleColorScheme}
icon={colorScheme == "dark" ? IconSun : IconMoon} href={"#"} index={-1}/>
<NavbarLink index={-1} href={"#"} onClick={onLogoutClick} icon={IconLogout} label="Выйти"/>

View File

@@ -1,35 +1,55 @@
import {Select, SelectProps} from "@mantine/core";
import {useEffect, useMemo, useState} from "react";
import {ObjectWithNameAndId} from "../../types/utils.ts";
import {groupBy, omit} from "lodash";
interface ObjectWithIdAndName {
id: number,
name: string
}
export type SelectObjectType<T extends ObjectWithNameAndId> = T;
export type SelectObjectType<T> = T;
type ControlledValueProps<T extends ObjectWithNameAndId> = {
type ControlledValueProps<T> = {
value: SelectObjectType<T>,
onChange: (value: SelectObjectType<T>) => void;
}
type CustomLabelAndKeyProps<T> = {
getLabelFn: (item: SelectObjectType<T>) => string;
getValueFn: (item: SelectObjectType<T>) => string;
}
type RestProps<T extends ObjectWithNameAndId> = {
type RestProps<T> = {
defaultValue?: SelectObjectType<T>
onChange: (value: SelectObjectType<T>) => void;
data: SelectObjectType<T>[];
groupBy?: (item: SelectObjectType<T>) => string;
filterBy?: (item: SelectObjectType<T>) => boolean;
};
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
return item.name;
}
export type ObjectSelectProps<T extends ObjectWithNameAndId> =
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
return item.id.toString();
}
export type ObjectSelectProps<T> =
(RestProps<T> & Partial<ControlledValueProps<T>>)
& Omit<SelectProps, 'value' | 'onChange' | 'data'>;
& Omit<SelectProps, 'value' | 'onChange' | 'data'>
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>)
const ObjectSelect = <T extends ObjectWithNameAndId, >(props: ObjectSelectProps<T>) => {
const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
const isControlled = 'value' in props;
const haveGetValueFn = 'getValueFn' in props;
const haveGetLabelFn = 'getLabelFn' in props;
const [internalValue, setInternalValue] = useState<SelectObjectType<T> | undefined>(props.defaultValue);
const value = isControlled ? props.value : internalValue;
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
const data = useMemo(() => {
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
if (props.groupBy) {
@@ -38,21 +58,21 @@ const ObjectSelect = <T extends ObjectWithNameAndId, >(props: ObjectSelectProps<
return Object.entries(groupedData).map(([group, items]) => ({
group,
items: items.map(item => ({
label: item.name,
value: item.id.toString()
label: getLabelFn(item),
value: getValueFn(item)
}))
}));
} else {
return propsData.map(item => ({
label: item.name,
value: item.id.toString()
label: getLabelFn(item),
value: getValueFn(item)
}));
}
}, [props.data, props.groupBy]);
const handleOnChange = (event: string | null) => {
if (!event) return;
const object = props.data.find(item => parseInt(event) == item.id);
const object = props.data.find(item => event == getValueFn(item));
if (!object) return;
if (isControlled) {
props.onChange(object);
@@ -65,11 +85,11 @@ const ObjectSelect = <T extends ObjectWithNameAndId, >(props: ObjectSelectProps<
if (isControlled || !internalValue) return;
props.onChange(internalValue);
}, [internalValue]);
const restProps = omit(props, ['filterBy', 'groupBy']);
const restProps = omit(props, ['filterBy', 'groupBy', 'getValueFn', 'getLabelFn']);
return (
<Select
{...restProps}
value={value?.id.toString()}
value={value && getValueFn(value)}
onChange={handleOnChange}
data={data}
/>

21
src/hooks/objectList.tsx Normal file
View File

@@ -0,0 +1,21 @@
import {QueryObserverResult, RefetchOptions, useQuery} from "@tanstack/react-query";
import {CancelablePromise} from "../client";
type Props<T, K> = {
queryFn: () => CancelablePromise<T>,
getObjectsFn: (response: T) => K[],
queryKey: string
}
type Response<T, K> = {
objects: K[],
refetch: (options?: RefetchOptions) => Promise<QueryObserverResult<T, Error>>
}
const ObjectList = <T, K, >(props: Props<T, K>): Response<T, K> => {
const {isPending, error, data, refetch} = useQuery({
queryKey: [props.queryKey],
queryFn: props.queryFn
});
const objects = isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
return {objects, refetch}
}
export default ObjectList

View File

@@ -10,6 +10,7 @@ import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
import BarcodeTemplateFormModal
from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx";
import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx";
export const modals = {
enterDeadline: EnterDeadlineModal,
@@ -22,5 +23,6 @@ export const modals = {
productServiceForm: ProductServiceFormModal,
printBarcode: PrintBarcodeModal,
addBarcode: AddBarcodeModal,
barcodeTemplateFormModal: BarcodeTemplateFormModal
barcodeTemplateFormModal: BarcodeTemplateFormModal,
userFormModal: UserFormModal
}

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>
)

View File

@@ -23,6 +23,7 @@ const LoginLazyImport = createFileRoute('/login')()
const LeadsLazyImport = createFileRoute('/leads')()
const ClientsLazyImport = createFileRoute('/clients')()
const BarcodeLazyImport = createFileRoute('/barcode')()
const AdminLazyImport = createFileRoute('/admin')()
const IndexLazyImport = createFileRoute('/')()
// Create/Update Routes
@@ -62,6 +63,11 @@ const BarcodeLazyRoute = BarcodeLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/barcode.lazy').then((d) => d.Route))
const AdminLazyRoute = AdminLazyImport.update({
path: '/admin',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/admin.lazy').then((d) => d.Route))
const IndexLazyRoute = IndexLazyImport.update({
path: '/',
getParentRoute: () => rootRoute,
@@ -72,34 +78,65 @@ const IndexLazyRoute = IndexLazyImport.update({
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexLazyImport
parentRoute: typeof rootRoute
}
'/admin': {
id: '/admin'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AdminLazyImport
parentRoute: typeof rootRoute
}
'/barcode': {
id: '/barcode'
path: '/barcode'
fullPath: '/barcode'
preLoaderRoute: typeof BarcodeLazyImport
parentRoute: typeof rootRoute
}
'/clients': {
id: '/clients'
path: '/clients'
fullPath: '/clients'
preLoaderRoute: typeof ClientsLazyImport
parentRoute: typeof rootRoute
}
'/leads': {
id: '/leads'
path: '/leads'
fullPath: '/leads'
preLoaderRoute: typeof LeadsLazyImport
parentRoute: typeof rootRoute
}
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginLazyImport
parentRoute: typeof rootRoute
}
'/products': {
id: '/products'
path: '/products'
fullPath: '/products'
preLoaderRoute: typeof ProductsLazyImport
parentRoute: typeof rootRoute
}
'/services': {
id: '/services'
path: '/services'
fullPath: '/services'
preLoaderRoute: typeof ServicesLazyImport
parentRoute: typeof rootRoute
}
'/test': {
id: '/test'
path: '/test'
fullPath: '/test'
preLoaderRoute: typeof TestLazyImport
parentRoute: typeof rootRoute
}
@@ -108,8 +145,9 @@ declare module '@tanstack/react-router' {
// Create and export the route tree
export const routeTree = rootRoute.addChildren([
export const routeTree = rootRoute.addChildren({
IndexLazyRoute,
AdminLazyRoute,
BarcodeLazyRoute,
ClientsLazyRoute,
LeadsLazyRoute,
@@ -117,6 +155,54 @@ export const routeTree = rootRoute.addChildren([
ProductsLazyRoute,
ServicesLazyRoute,
TestLazyRoute,
])
})
/* prettier-ignore-end */
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/admin",
"/barcode",
"/clients",
"/leads",
"/login",
"/products",
"/services",
"/test"
]
},
"/": {
"filePath": "index.lazy.tsx"
},
"/admin": {
"filePath": "admin.lazy.tsx"
},
"/barcode": {
"filePath": "barcode.lazy.tsx"
},
"/clients": {
"filePath": "clients.lazy.tsx"
},
"/leads": {
"filePath": "leads.lazy.tsx"
},
"/login": {
"filePath": "login.lazy.tsx"
},
"/products": {
"filePath": "products.lazy.tsx"
},
"/services": {
"filePath": "services.lazy.tsx"
},
"/test": {
"filePath": "test.lazy.tsx"
}
}
}
ROUTE_MANIFEST_END */

View File

@@ -0,0 +1,6 @@
import {createLazyFileRoute} from "@tanstack/react-router";
import AdminPage from "../pages/AdminPage/AdminPage.tsx";
export const Route = createLazyFileRoute('/admin')({
component: AdminPage
})

View File

@@ -1,14 +1,43 @@
import {createLazyFileRoute} from "@tanstack/react-router";
import {Flex, Tabs} from '@mantine/core';
import {motion} from 'framer-motion';
export const Route = createLazyFileRoute('/test')({
component: TestPage
})
const tabs = [
['tab111', 'content111'],
['tab222', 'content222'],
['tab333', 'content333'],
];
function Demo() {
return (
<Tabs>
{tabs.map((el, i) => (
<Tabs.Tab key={i} value={el[0]}>
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{duration: 0.5}}
>
<div>{el[1]}</div>
</motion.div>
</Tabs.Tab>
))}
</Tabs>
);
}
function TestPage() {
return (
<>
{/*<ShippingWarehouseAutocomplete/>*/}
<Flex w={"80vw"}>
<Demo/>
</Flex>
</>
);
}

View File

@@ -0,0 +1,6 @@
export enum UserRoleEnum {
USER = 'user',
EMPLOYEE = 'employee',
MANAGER = 'manager',
ADMIN = 'admin'
}