k
This commit is contained in:
86
package.json
86
package.json
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
9
src/client/models/CreatePositionRequest.ts
Normal file
9
src/client/models/CreatePositionRequest.ts
Normal 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;
|
||||
};
|
||||
|
||||
9
src/client/models/CreatePositionResponse.ts
Normal file
9
src/client/models/CreatePositionResponse.ts
Normal 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;
|
||||
};
|
||||
|
||||
@@ -7,5 +7,6 @@ export type DealGeneralInfoSchema = {
|
||||
isDeleted: boolean;
|
||||
isCompleted: boolean;
|
||||
comment: string;
|
||||
shippingWarehouse?: (string | null);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,6 @@ export type DealSchema = {
|
||||
isCompleted: boolean;
|
||||
client: ClientSchema;
|
||||
comment: string;
|
||||
shippingWarehouse?: (ShippingWarehouseSchema | null);
|
||||
shippingWarehouse?: (ShippingWarehouseSchema | string | null);
|
||||
};
|
||||
|
||||
|
||||
9
src/client/models/GetAllPositionsResponse.ts
Normal file
9
src/client/models/GetAllPositionsResponse.ts
Normal 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>;
|
||||
};
|
||||
|
||||
9
src/client/models/GetAllRolesResponse.ts
Normal file
9
src/client/models/GetAllRolesResponse.ts
Normal 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>;
|
||||
};
|
||||
|
||||
9
src/client/models/GetAllUsersResponse.ts
Normal file
9
src/client/models/GetAllUsersResponse.ts
Normal 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>;
|
||||
};
|
||||
|
||||
9
src/client/models/PermissionSchema.ts
Normal file
9
src/client/models/PermissionSchema.ts
Normal 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;
|
||||
};
|
||||
|
||||
9
src/client/models/PositionSchema.ts
Normal file
9
src/client/models/PositionSchema.ts
Normal 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;
|
||||
};
|
||||
|
||||
11
src/client/models/RoleSchema.ts
Normal file
11
src/client/models/RoleSchema.ts
Normal 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>;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateUserRequest.ts
Normal file
9
src/client/models/UpdateUserRequest.ts
Normal 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;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateUserResponse.ts
Normal file
9
src/client/models/UpdateUserResponse.ts
Normal 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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
17
src/client/models/UserUpdate.ts
Normal file
17
src/client/models/UserUpdate.ts
Normal 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);
|
||||
};
|
||||
|
||||
43
src/client/services/PositionService.ts
Normal file
43
src/client/services/PositionService.ts
Normal 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`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
21
src/client/services/RoleService.ts
Normal file
21
src/client/services/RoleService.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
}
|
||||
43
src/client/services/UserService.ts
Normal file
43
src/client/services/UserService.ts
Normal 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`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
34
src/components/AnimatedOutlet/au.tsx
Normal file
34
src/components/AnimatedOutlet/au.tsx
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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="Выйти"/>
|
||||
|
||||
@@ -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
21
src/hooks/objectList.tsx
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
5
src/pages/AdminPage/AdminPage.module.css
Normal file
5
src/pages/AdminPage/AdminPage.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
53
src/pages/AdminPage/AdminPage.tsx
Normal file
53
src/pages/AdminPage/AdminPage.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
16
src/pages/AdminPage/components/PositionsTable/columns.tsx
Normal file
16
src/pages/AdminPage/components/PositionsTable/columns.tsx
Normal 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: "Название должности"
|
||||
}
|
||||
], []);
|
||||
}
|
||||
19
src/pages/AdminPage/components/RoleSelect/RoleSelect.tsx
Normal file
19
src/pages/AdminPage/components/RoleSelect/RoleSelect.tsx
Normal 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;
|
||||
81
src/pages/AdminPage/components/UsersTable/UsersTable.tsx
Normal file
81
src/pages/AdminPage/components/UsersTable/UsersTable.tsx
Normal 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;
|
||||
40
src/pages/AdminPage/components/UsersTable/columns.tsx
Normal file
40
src/pages/AdminPage/components/UsersTable/columns.tsx
Normal 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/>
|
||||
},
|
||||
], []);
|
||||
}
|
||||
|
||||
9
src/pages/AdminPage/hooks/usePositionsList.tsx
Normal file
9
src/pages/AdminPage/hooks/usePositionsList.tsx
Normal 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;
|
||||
9
src/pages/AdminPage/hooks/useRolesList.tsx
Normal file
9
src/pages/AdminPage/hooks/useRolesList.tsx
Normal 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;
|
||||
11
src/pages/AdminPage/hooks/useUsersList.tsx
Normal file
11
src/pages/AdminPage/hooks/useUsersList.tsx
Normal 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;
|
||||
108
src/pages/AdminPage/modals/UserFormModal/UserFormModal.tsx
Normal file
108
src/pages/AdminPage/modals/UserFormModal/UserFormModal.tsx
Normal 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;
|
||||
@@ -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;
|
||||
35
src/pages/AdminPage/tabs/Users/UsersTab.tsx
Normal file
35
src/pages/AdminPage/tabs/Users/UsersTab.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: "Неудалось войти!"})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
6
src/routes/admin.lazy.tsx
Normal file
6
src/routes/admin.lazy.tsx
Normal 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
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
6
src/shared/enums/UserRole.ts
Normal file
6
src/shared/enums/UserRole.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum UserRoleEnum {
|
||||
USER = 'user',
|
||||
EMPLOYEE = 'employee',
|
||||
MANAGER = 'manager',
|
||||
ADMIN = 'admin'
|
||||
}
|
||||
Reference in New Issue
Block a user