feat: prettier
This commit is contained in:
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
public
|
||||||
|
node_modules
|
||||||
|
src/client
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"singleAttributePerLine": true,
|
"singleAttributePerLine": true,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"quoteProps": "consistent",
|
"quoteProps": "consistent",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"bracketSameLine": true,
|
"bracketSameLine": true,
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -4,27 +4,27 @@ This template provides a minimal setup to get React working in Vite with HMR and
|
|||||||
|
|
||||||
Currently, two official plugins are available:
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
- Configure the top-level `parserOptions` property like this:
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
export default {
|
export default {
|
||||||
// other rules...
|
// other rules...
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: "latest",
|
||||||
sourceType: 'module',
|
sourceType: "module",
|
||||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
project: ["./tsconfig.json", "./tsconfig.node.json"],
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ export default [
|
|||||||
{ languageOptions: { globals: globals.browser } },
|
{ languageOptions: { globals: globals.browser } },
|
||||||
pluginJs.configs.recommended,
|
pluginJs.configs.recommended,
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
];
|
];
|
||||||
|
|||||||
27
index.html
27
index.html
@@ -1,13 +1,20 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/icons/favicon.png" />
|
<link
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
rel="icon"
|
||||||
<title>DENCO: Fulfillment</title>
|
type="image/svg+xml"
|
||||||
</head>
|
href="/icons/favicon.png" />
|
||||||
<body>
|
<meta
|
||||||
<div id="root"></div>
|
name="viewport"
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
content="width=device-width, initial-scale=1.0" />
|
||||||
</body>
|
<title>DENCO: Fulfillment</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
7894
package-lock.json
generated
7894
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
156
package.json
156
package.json
@@ -1,80 +1,80 @@
|
|||||||
{
|
{
|
||||||
"name": "fulfillment-frontend",
|
"name": "fulfillment-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
|
"generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@hello-pangea/dnd": "^16.6.0",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@mantine/core": "^7.11.2",
|
"@mantine/core": "^7.11.2",
|
||||||
"@mantine/dates": "^7.11.2",
|
"@mantine/dates": "^7.11.2",
|
||||||
"@mantine/dropzone": "^7.11.2",
|
"@mantine/dropzone": "^7.11.2",
|
||||||
"@mantine/form": "^7.11.2",
|
"@mantine/form": "^7.11.2",
|
||||||
"@mantine/hooks": "^7.11.2",
|
"@mantine/hooks": "^7.11.2",
|
||||||
"@mantine/modals": "^7.11.2",
|
"@mantine/modals": "^7.11.2",
|
||||||
"@mantine/notifications": "^7.11.2",
|
"@mantine/notifications": "^7.11.2",
|
||||||
"@reduxjs/toolkit": "^2.2.6",
|
"@reduxjs/toolkit": "^2.2.6",
|
||||||
"@tabler/icons-react": "^3.11.0",
|
"@tabler/icons-react": "^3.11.0",
|
||||||
"@tanstack/react-query": "^5.51.9",
|
"@tanstack/react-query": "^5.51.9",
|
||||||
"@tanstack/react-router": "^1.45.6",
|
"@tanstack/react-router": "^1.45.6",
|
||||||
"@tanstack/router-devtools": "^1.45.6",
|
"@tanstack/router-devtools": "^1.45.6",
|
||||||
"@tanstack/router-vite-plugin": "^1.45.3",
|
"@tanstack/router-vite-plugin": "^1.45.3",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cyrillic-to-translit-js": "^3.2.1",
|
"cyrillic-to-translit-js": "^3.2.1",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"dot-object": "^2.1.5",
|
"dot-object": "^2.1.5",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"framer-motion": "^11.3.8",
|
"framer-motion": "^11.3.8",
|
||||||
"globals": "^15.8.0",
|
"globals": "^15.8.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mantine-form-zod-resolver": "^1.1.0",
|
"mantine-form-zod-resolver": "^1.1.0",
|
||||||
"mantine-react-table": "^2.0.0-beta.5",
|
"mantine-react-table": "^2.0.0-beta.5",
|
||||||
"phone": "^3.1.49",
|
"phone": "^3.1.49",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-barcode": "^1.5.3",
|
"react-barcode": "^1.5.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-to-print": "^2.15.1",
|
"react-to-print": "^2.15.1",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.7.0",
|
"@eslint/js": "^9.7.0",
|
||||||
"@types/dot-object": "^2.1.6",
|
"@types/dot-object": "^2.1.6",
|
||||||
"@types/eslint__js": "^8.42.3",
|
"@types/eslint__js": "^8.42.3",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/lodash": "^4.17.7",
|
"@types/lodash": "^4.17.7",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||||
"@typescript-eslint/parser": "^7.16.1",
|
"@typescript-eslint/parser": "^7.16.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||||
"eslint": "^9.7.0",
|
"eslint": "^9.7.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-react-refresh": "^0.4.8",
|
"eslint-plugin-react-refresh": "^0.4.8",
|
||||||
"openapi-typescript-codegen": "^0.29.0",
|
"openapi-typescript-codegen": "^0.29.0",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"postcss-preset-mantine": "^1.16.0",
|
"postcss-preset-mantine": "^1.16.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^7.16.1",
|
"typescript-eslint": "^7.16.1",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.3.4",
|
||||||
"yarn-upgrade-all": "^0.7.2"
|
"yarn-upgrade-all": "^0.7.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.1.0"
|
"packageManager": "yarn@4.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'postcss-preset-mantine': {},
|
"postcss-preset-mantine": {},
|
||||||
'postcss-simple-vars': {
|
"postcss-simple-vars": {
|
||||||
variables: {
|
variables: {
|
||||||
'mantine-breakpoint-xs': '36em',
|
"mantine-breakpoint-xs": "36em",
|
||||||
'mantine-breakpoint-sm': '48em',
|
"mantine-breakpoint-sm": "48em",
|
||||||
'mantine-breakpoint-md': '62em',
|
"mantine-breakpoint-md": "62em",
|
||||||
'mantine-breakpoint-lg': '75em',
|
"mantine-breakpoint-lg": "75em",
|
||||||
'mantine-breakpoint-xl': '88em',
|
"mantine-breakpoint-xl": "88em",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {BarcodeService} from "../../client";
|
import { BarcodeService } from "../../client";
|
||||||
|
|
||||||
const useGetAllBarcodeTemplates = () => {
|
const useGetAllBarcodeTemplates = () => {
|
||||||
const {data, error, isLoading, refetch} = useQuery({
|
const { data, error, isLoading, refetch } = useQuery({
|
||||||
queryKey: ['getAllBarcodeTemplates'],
|
queryKey: ["getAllBarcodeTemplates"],
|
||||||
queryFn: () => BarcodeService.getAllBarcodeTemplates(),
|
queryFn: () => BarcodeService.getAllBarcodeTemplates(),
|
||||||
select: (data) => data.templates
|
select: data => data.templates,
|
||||||
});
|
});
|
||||||
const barcodeTemplates = isLoading || error || !data ? [] : data;
|
const barcodeTemplates = isLoading || error || !data ? [] : data;
|
||||||
return {barcodeTemplates, refetch}
|
return { barcodeTemplates, refetch };
|
||||||
}
|
};
|
||||||
|
|
||||||
export default useGetAllBarcodeTemplates;
|
export default useGetAllBarcodeTemplates;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {ProductService} from "../../client";
|
import { ProductService } from "../../client";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const useGetProductById = (id: number) => {
|
export const useGetProductById = (id: number) => {
|
||||||
const {data, error, isLoading, refetch} = useQuery({
|
const { data, error, isLoading, refetch } = useQuery({
|
||||||
queryKey: ['getProductById', id],
|
queryKey: ["getProductById", id],
|
||||||
queryFn: () => ProductService.getProductById({productId: id}),
|
queryFn: () => ProductService.getProductById({ productId: id }),
|
||||||
select: (data) => data
|
select: data => data,
|
||||||
});
|
});
|
||||||
return {product: data, error, isLoading, refetch}
|
return { product: data, error, isLoading, refetch };
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {forwardRef, useContext, useRef} from "react";
|
import { forwardRef, useContext, useRef } from "react";
|
||||||
import {getRouterContext, Outlet} from "@tanstack/react-router";
|
import { getRouterContext, Outlet } from "@tanstack/react-router";
|
||||||
import {motion, useIsPresent} from "framer-motion";
|
import { motion, useIsPresent } from "framer-motion";
|
||||||
import {cloneDeep} from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
|
const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
|
||||||
const RouterContext = getRouterContext();
|
const RouterContext = getRouterContext();
|
||||||
@@ -17,26 +17,24 @@ const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div ref={ref}
|
<motion.div
|
||||||
initial={{x: "-100%"}}
|
ref={ref}
|
||||||
animate={{
|
initial={{ x: "-100%" }}
|
||||||
x: 0,
|
animate={{
|
||||||
transform: "",
|
x: 0,
|
||||||
transitionEnd: {
|
transform: "",
|
||||||
transform: "none"
|
transitionEnd: {
|
||||||
}
|
transform: "none",
|
||||||
}}
|
},
|
||||||
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.4,
|
duration: 0.4,
|
||||||
ease: "circInOut",
|
ease: "circInOut",
|
||||||
}}
|
}}>
|
||||||
|
|
||||||
>
|
|
||||||
<RouterContext.Provider value={renderedContext.current}>
|
<RouterContext.Provider value={renderedContext.current}>
|
||||||
<Outlet/>
|
<Outlet />
|
||||||
</RouterContext.Provider>
|
</RouterContext.Provider>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
export default AnimatedOutlet
|
export default AnimatedOutlet;
|
||||||
|
|||||||
@@ -4,19 +4,18 @@ import {
|
|||||||
MRT_RowData,
|
MRT_RowData,
|
||||||
MRT_TableInstance,
|
MRT_TableInstance,
|
||||||
MRT_TableOptions,
|
MRT_TableOptions,
|
||||||
useMantineReactTable
|
useMantineReactTable,
|
||||||
} from "mantine-react-table";
|
} from "mantine-react-table";
|
||||||
import {MRT_Localization_RU} from "mantine-react-table/locales/ru/index.cjs";
|
import { MRT_Localization_RU } from "mantine-react-table/locales/ru/index.cjs";
|
||||||
import {forwardRef, useEffect, useImperativeHandle} from 'react';
|
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
||||||
|
|
||||||
type Props<T extends Record<string, unknown>, K extends keyof T> = {
|
type Props<T extends Record<string, unknown>, K extends keyof T> = {
|
||||||
data: T[],
|
data: T[];
|
||||||
onSelectionChange?: (selectedRows: T[]) => void,
|
onSelectionChange?: (selectedRows: T[]) => void;
|
||||||
columns: MRT_ColumnDef<T, K>[],
|
columns: MRT_ColumnDef<T, K>[];
|
||||||
restProps?: MRT_TableOptions<T>,
|
restProps?: MRT_TableOptions<T>;
|
||||||
striped?: boolean
|
striped?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// Экспортируем тип рефа, чтобы он мог быть использован в других компонентах
|
// Экспортируем тип рефа, чтобы он мог быть использован в других компонентах
|
||||||
export type BaseTableRef<T extends MRT_RowData> = {
|
export type BaseTableRef<T extends MRT_RowData> = {
|
||||||
@@ -25,33 +24,42 @@ export type BaseTableRef<T extends MRT_RowData> = {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>((props, ref) => {
|
export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>(
|
||||||
const {data, columns, restProps, striped = true, onSelectionChange} = props;
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
restProps,
|
||||||
|
striped = true,
|
||||||
|
onSelectionChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const table = useMantineReactTable({
|
const table = useMantineReactTable({
|
||||||
localization: MRT_Localization_RU,
|
localization: MRT_Localization_RU,
|
||||||
enablePagination: false,
|
enablePagination: false,
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
mantineTableProps: {
|
mantineTableProps: {
|
||||||
striped: striped
|
striped: striped,
|
||||||
},
|
},
|
||||||
enableTopToolbar: false,
|
enableTopToolbar: false,
|
||||||
enableBottomToolbar: false,
|
enableBottomToolbar: false,
|
||||||
enableRowSelection: onSelectionChange !== undefined,
|
enableRowSelection: onSelectionChange !== undefined,
|
||||||
...restProps,
|
...restProps,
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!onSelectionChange) return;
|
if (!onSelectionChange) return;
|
||||||
onSelectionChange(table.getSelectedRowModel().rows.map(row => row.original))
|
onSelectionChange(
|
||||||
}, [table.getState().rowSelection]);
|
table.getSelectedRowModel().rows.map(row => row.original)
|
||||||
|
);
|
||||||
|
}, [table.getState().rowSelection]);
|
||||||
|
|
||||||
// Используем useImperativeHandle для определения, что будет доступно через ref
|
// Используем useImperativeHandle для определения, что будет доступно через ref
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
getTable: () => table
|
getTable: () => table,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return <MantineReactTable table={table} />;
|
||||||
return <MantineReactTable table={table}/>;
|
}
|
||||||
});
|
);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {Button, rem, Tooltip} from '@mantine/core';
|
import { Button, rem, Tooltip } from "@mantine/core";
|
||||||
import {IconCheck, IconCopy} from '@tabler/icons-react';
|
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import {useClipboard} from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: string;
|
children: string;
|
||||||
value: string
|
value: string;
|
||||||
onCopiedLabel: string;
|
onCopiedLabel: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
export const ButtonCopy: FC<Props> = ({ children, onCopiedLabel, value }) => {
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -18,20 +18,19 @@ export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
|||||||
offset={5}
|
offset={5}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
transitionProps={{duration: 100, transition: 'slide-down'}}
|
transitionProps={{ duration: 100, transition: "slide-down" }}
|
||||||
opened={clipboard.copied}
|
opened={clipboard.copied}>
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
rightSection={
|
rightSection={
|
||||||
clipboard.copied ? (
|
clipboard.copied ? (
|
||||||
<IconCheck
|
<IconCheck
|
||||||
style={{width: rem(20), height: rem(20)}}
|
style={{ width: rem(20), height: rem(20) }}
|
||||||
stroke={1.5}
|
stroke={1.5}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<IconCopy
|
<IconCopy
|
||||||
style={{width: rem(20), height: rem(20)}}
|
style={{ width: rem(20), height: rem(20) }}
|
||||||
stroke={1.5}
|
stroke={1.5}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -42,13 +41,12 @@ export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
|||||||
root: {
|
root: {
|
||||||
paddingRight: rem(14),
|
paddingRight: rem(14),
|
||||||
},
|
},
|
||||||
section: {marginLeft: rem(22)},
|
section: { marginLeft: rem(22) },
|
||||||
}}
|
}}
|
||||||
onClick={() => clipboard.copy(value)}
|
onClick={() => clipboard.copy(value)}>
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
export default ButtonCopy;
|
export default ButtonCopy;
|
||||||
|
|||||||
@@ -1,35 +1,39 @@
|
|||||||
import {Button, rem, Tooltip} from '@mantine/core';
|
import { Button, rem, Tooltip } from "@mantine/core";
|
||||||
import {IconCheck, IconCopy} from '@tabler/icons-react';
|
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: string;
|
children: string;
|
||||||
onCopyClick: () => void;
|
onCopyClick: () => void;
|
||||||
onCopiedLabel: string;
|
onCopiedLabel: string;
|
||||||
copied: boolean;
|
copied: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopyClick, copied}) => {
|
export const ButtonCopyControlled: FC<Props> = ({
|
||||||
|
children,
|
||||||
|
onCopiedLabel,
|
||||||
|
onCopyClick,
|
||||||
|
copied,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={onCopiedLabel}
|
label={onCopiedLabel}
|
||||||
offset={5}
|
offset={5}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
transitionProps={{duration: 100, transition: 'slide-down'}}
|
transitionProps={{ duration: 100, transition: "slide-down" }}
|
||||||
opened={copied}
|
opened={copied}>
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
rightSection={
|
rightSection={
|
||||||
copied ? (
|
copied ? (
|
||||||
<IconCheck
|
<IconCheck
|
||||||
style={{width: rem(20), height: rem(20)}}
|
style={{ width: rem(20), height: rem(20) }}
|
||||||
stroke={1.5}
|
stroke={1.5}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<IconCopy
|
<IconCopy
|
||||||
style={{width: rem(20), height: rem(20)}}
|
style={{ width: rem(20), height: rem(20) }}
|
||||||
stroke={1.5}
|
stroke={1.5}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -40,12 +44,11 @@ export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopy
|
|||||||
root: {
|
root: {
|
||||||
paddingRight: rem(14),
|
paddingRight: rem(14),
|
||||||
},
|
},
|
||||||
section: {marginLeft: rem(22)},
|
section: { marginLeft: rem(22) },
|
||||||
}}
|
}}
|
||||||
onClick={onCopyClick}
|
onClick={onCopyClick}>
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import {NumberInput, NumberInputProps} from "@mantine/core";
|
import { NumberInput, NumberInputProps } from "@mantine/core";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {useDebouncedValue} from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import {isNumber, omit} from "lodash";
|
import { isNumber, omit } from "lodash";
|
||||||
|
|
||||||
|
|
||||||
type Props = NumberInputProps;
|
type Props = NumberInputProps;
|
||||||
|
|
||||||
const DebouncedNumberInput = (props: Props) => {
|
const DebouncedNumberInput = (props: Props) => {
|
||||||
const [value, setValue] = useState<number | string>(props.defaultValue || props.value || '');
|
const [value, setValue] = useState<number | string>(
|
||||||
|
props.defaultValue || props.value || ""
|
||||||
|
);
|
||||||
const [debounced] = useDebouncedValue(value, 200);
|
const [debounced] = useDebouncedValue(value, 200);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.onChange) return;
|
if (!props.onChange) return;
|
||||||
props.onChange(debounced);
|
props.onChange(debounced);
|
||||||
}, [debounced])
|
}, [debounced]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isNumber(props.value)) return;
|
if (!isNumber(props.value)) return;
|
||||||
setValue(props.value);
|
setValue(props.value);
|
||||||
}, [props.value])
|
}, [props.value]);
|
||||||
|
|
||||||
const restProps = omit(props, ["onChange", "value"])
|
const restProps = omit(props, ["onChange", "value"]);
|
||||||
return (
|
return (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default DebouncedNumberInput;
|
||||||
export default DebouncedNumberInput;
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/*background-color: green;*/
|
/*background-color: green;*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -34,7 +33,6 @@
|
|||||||
}
|
}
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: var(--mantine-color-dark-5);
|
background-color: var(--mantine-color-dark-5);
|
||||||
|
|
||||||
}
|
}
|
||||||
border-radius: var(--item-border-radius);
|
border-radius: var(--item-border-radius);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import styles from './Board.module.css';
|
import styles from "./Board.module.css";
|
||||||
import {Divider, Text, Title} from '@mantine/core';
|
import { Divider, Text, Title } from "@mantine/core";
|
||||||
import {Draggable, Droppable} from "@hello-pangea/dnd";
|
import { Draggable, Droppable } from "@hello-pangea/dnd";
|
||||||
import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx";
|
import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx";
|
||||||
import {DealSummary} from "../../../client";
|
import { DealSummary } from "../../../client";
|
||||||
import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx";
|
import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {getPluralForm} from "../../../shared/lib/utils.ts";
|
import { getPluralForm } from "../../../shared/lib/utils.ts";
|
||||||
import {sum} from "lodash";
|
import { sum } from "lodash";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
droppableId: string;
|
droppableId: string;
|
||||||
@@ -15,66 +15,74 @@ type Props = {
|
|||||||
withCreateButton?: boolean;
|
withCreateButton?: boolean;
|
||||||
summaries: DealSummary[];
|
summaries: DealSummary[];
|
||||||
color: string;
|
color: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Board: FC<Props> = ({droppableId, title, summaries, color, withCreateButton = false}) => {
|
export const Board: FC<Props> = ({
|
||||||
|
droppableId,
|
||||||
|
title,
|
||||||
|
summaries,
|
||||||
|
color,
|
||||||
|
withCreateButton = false,
|
||||||
|
}) => {
|
||||||
const getDealsText = () => {
|
const getDealsText = () => {
|
||||||
const pluralForm = getPluralForm(summaries.length, 'сделка', 'сделки', 'сделок');
|
const pluralForm = getPluralForm(
|
||||||
|
summaries.length,
|
||||||
|
"сделка",
|
||||||
|
"сделки",
|
||||||
|
"сделок"
|
||||||
|
);
|
||||||
return `${summaries.length} ${pluralForm}: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}₽`;
|
return `${summaries.length} ${pluralForm}: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}₽`;
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["container"]}>
|
<div className={styles["container"]}>
|
||||||
<div className={styles["header"]}>
|
<div className={styles["header"]}>
|
||||||
<Title size={"h4"}>{title}</Title>
|
<Title size={"h4"}>{title}</Title>
|
||||||
<Text>{getDealsText()}</Text>
|
<Text>{getDealsText()}</Text>
|
||||||
<Divider size={"xl"} my={10} color={color}/>
|
<Divider
|
||||||
|
size={"xl"}
|
||||||
|
my={10}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Droppable
|
<Droppable droppableId={droppableId}>
|
||||||
droppableId={droppableId}
|
|
||||||
>
|
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div ref={provided.innerRef}
|
<div
|
||||||
className={classNames(
|
ref={provided.innerRef}
|
||||||
styles["items-list"],
|
className={classNames(
|
||||||
(snapshot.isDraggingOver && !snapshot.draggingFromThisWith)
|
styles["items-list"],
|
||||||
&& styles["items-list-drag-over"]
|
snapshot.isDraggingOver &&
|
||||||
)}
|
!snapshot.draggingFromThisWith &&
|
||||||
{...provided.droppableProps}
|
styles["items-list-drag-over"]
|
||||||
>
|
)}
|
||||||
{withCreateButton &&
|
{...provided.droppableProps}>
|
||||||
<CreateDealButton
|
{withCreateButton && (
|
||||||
|
<CreateDealButton onClick={() => {}} />
|
||||||
onClick={() => {
|
)}
|
||||||
}}
|
{summaries.map(summary => (
|
||||||
/>}
|
<Draggable
|
||||||
{summaries.map(summary =>
|
draggableId={summary.id.toString()}
|
||||||
(
|
index={summary.rank}
|
||||||
<Draggable
|
key={summary.id}>
|
||||||
draggableId={summary.id.toString()}
|
{provided => (
|
||||||
index={summary.rank}
|
<div
|
||||||
key={summary.id}
|
{...provided.draggableProps}
|
||||||
>
|
{...provided.dragHandleProps}
|
||||||
{(provided) => (
|
ref={provided.innerRef}>
|
||||||
<div {...provided.draggableProps}
|
<DealSummaryCard
|
||||||
{...provided.dragHandleProps}
|
dealSummary={summary}
|
||||||
ref={provided.innerRef}
|
/>
|
||||||
|
</div>
|
||||||
>
|
)}
|
||||||
<DealSummaryCard dealSummary={summary}/>
|
</Draggable>
|
||||||
</div>
|
))}
|
||||||
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Board;
|
export default Board;
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||||
border-radius: var(--item-border-radius);
|
border-radius: var(--item-border-radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container:hover {
|
.container:hover {
|
||||||
background-color: light-dark(var(--mantine-color-default-hover), var(--mantine-color-gray-filled-hover));
|
background-color: light-dark(
|
||||||
|
var(--mantine-color-default-hover),
|
||||||
}
|
var(--mantine-color-gray-filled-hover)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,60 +1,61 @@
|
|||||||
import {FC, useState} from "react";
|
import { FC, useState } from "react";
|
||||||
|
|
||||||
import styles from './CreateDealButton.module.css';
|
import styles from "./CreateDealButton.module.css";
|
||||||
import {Text, Transition} from '@mantine/core';
|
import { Text, Transition } from "@mantine/core";
|
||||||
import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
|
import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
|
||||||
import {DealService} from "../../../client";
|
import { DealService } from "../../../client";
|
||||||
import {useQueryClient} from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import {dateWithoutTimezone} from "../../../shared/lib/date.ts";
|
import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
};
|
||||||
const CreateDealButton: FC<Props> = () => {
|
const CreateDealButton: FC<Props> = () => {
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
|
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles['container']}
|
<div
|
||||||
onClick={() => {
|
className={styles["container"]}
|
||||||
if (isCreating) return;
|
onClick={() => {
|
||||||
setIsCreating(prevState => !prevState)
|
if (isCreating) return;
|
||||||
setIsTransitionEnded(false);
|
setIsCreating(prevState => !prevState);
|
||||||
}}
|
setIsTransitionEnded(false);
|
||||||
>
|
}}>
|
||||||
{(!isCreating && isTransitionEnded) &&
|
{!isCreating && isTransitionEnded && (
|
||||||
<Text>Быстрое добавление</Text>
|
<Text>Быстрое добавление</Text>
|
||||||
}
|
)}
|
||||||
<Transition
|
<Transition
|
||||||
mounted={isCreating}
|
mounted={isCreating}
|
||||||
transition={"scale-y"}
|
transition={"scale-y"}
|
||||||
onExited={() => setIsTransitionEnded(true)}
|
onExited={() => setIsTransitionEnded(true)}>
|
||||||
|
{styles => (
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<div style={styles}>
|
<div style={styles}>
|
||||||
<CreateDealFrom
|
<CreateDealFrom
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setIsCreating(false)
|
setIsCreating(false);
|
||||||
}}
|
}}
|
||||||
onSubmit={(quickDeal) => {
|
onSubmit={quickDeal => {
|
||||||
DealService.quickCreateDealQuickCreatePost({
|
DealService.quickCreateDealQuickCreatePost({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
...quickDeal,
|
...quickDeal,
|
||||||
acceptanceDate: dateWithoutTimezone(quickDeal.acceptanceDate)
|
acceptanceDate: dateWithoutTimezone(
|
||||||
}
|
quickDeal.acceptanceDate
|
||||||
|
),
|
||||||
|
},
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
await queryClient.invalidateQueries({queryKey: ['getDealSummaries']});
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["getDealSummaries"],
|
||||||
|
});
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
})
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default CreateDealButton;
|
export default CreateDealButton;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.inputs {
|
.inputs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputs * {
|
.inputs * {
|
||||||
@@ -12,4 +12,3 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: rem(10);
|
gap: rem(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ import { useForm } from "@mantine/form";
|
|||||||
import styles from "./CreateDealForm.module.css";
|
import styles from "./CreateDealForm.module.css";
|
||||||
import ClientAutocomplete from "../../Selects/ClientAutocomplete/ClientAutocomplete.tsx";
|
import ClientAutocomplete from "../../Selects/ClientAutocomplete/ClientAutocomplete.tsx";
|
||||||
import { DateTimePicker } from "@mantine/dates";
|
import { DateTimePicker } from "@mantine/dates";
|
||||||
import ShippingWarehouseAutocomplete
|
import ShippingWarehouseAutocomplete from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
||||||
from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
|
||||||
import BaseMarketplaceSelect from "../../Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
import BaseMarketplaceSelect from "../../Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||||
import ServicePriceCategorySelect from "../../Selects/ServicePriceCategorySelect/ServicePriceCategorySelect.tsx";
|
import ServicePriceCategorySelect from "../../Selects/ServicePriceCategorySelect/ServicePriceCategorySelect.tsx";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit: (quickDeal: QuickDeal) => void
|
onSubmit: (quickDeal: QuickDeal) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
};
|
||||||
const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||||
const form = useForm<QuickDeal>({
|
const form = useForm<QuickDeal>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -33,15 +32,15 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
onSubmit={form.onSubmit((values) => onSubmit(values))}
|
onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||||
>
|
<div
|
||||||
<div style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
gap: rem(10),
|
gap: rem(10),
|
||||||
padding: rem(10),
|
padding: rem(10),
|
||||||
}}>
|
}}>
|
||||||
<div className={styles["inputs"]}>
|
<div className={styles["inputs"]}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder={"Название сделки"}
|
placeholder={"Название сделки"}
|
||||||
@@ -63,7 +62,6 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
|||||||
{...form.getInputProps("shippingWarehouse")}
|
{...form.getInputProps("shippingWarehouse")}
|
||||||
placeholder={"Склад отгрузки"}
|
placeholder={"Склад отгрузки"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["inputs"]}>
|
<div className={styles["inputs"]}>
|
||||||
<ServicePriceCategorySelect
|
<ServicePriceCategorySelect
|
||||||
@@ -89,20 +87,17 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className={styles["buttons"]}>
|
<div className={styles["buttons"]}>
|
||||||
<Button
|
<Button type={"submit"}>Добавить</Button>
|
||||||
type={"submit"}
|
|
||||||
>Добавить</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
onClick={() => onCancel()}
|
onClick={() => onCancel()}>
|
||||||
>Отменить</Button>
|
Отменить
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateDealFrom;
|
export default CreateDealFrom;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: var(--mantine-color-dark-5);
|
background-color: var(--mantine-color-dark-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-row-left {
|
.flex-row-left {
|
||||||
@@ -27,13 +26,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.flex-row {
|
.flex-row {
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-row-right {
|
.flex-row-right {
|
||||||
|
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +1,147 @@
|
|||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import {DealService, DealSummary} from "../../../client";
|
import { DealService, DealSummary } from "../../../client";
|
||||||
import styles from './DealSummaryCard.module.css';
|
import styles from "./DealSummaryCard.module.css";
|
||||||
|
|
||||||
import {ActionIcon, Badge, CopyButton, Flex, Image, Popover, rem, Text} from '@mantine/core';
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Badge,
|
||||||
|
CopyButton,
|
||||||
|
Flex,
|
||||||
|
Image,
|
||||||
|
Popover,
|
||||||
|
rem,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {useDealPageContext} from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
|
import { useDealPageContext } from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {faCheck} from "@fortawesome/free-solid-svg-icons";
|
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dealSummary: DealSummary
|
dealSummary: DealSummary;
|
||||||
}
|
};
|
||||||
|
|
||||||
const DealSummaryCard: FC<Props> = ({dealSummary}) => {
|
const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
|
||||||
const {setSelectedDeal} = useDealPageContext();
|
const { setSelectedDeal } = useDealPageContext();
|
||||||
const onDealSummaryClick = () => {
|
const onDealSummaryClick = () => {
|
||||||
DealService.getDealById({dealId: dealSummary.id})
|
DealService.getDealById({ dealId: dealSummary.id }).then(deal => {
|
||||||
.then((deal) => {
|
setSelectedDeal(deal);
|
||||||
setSelectedDeal(deal);
|
});
|
||||||
})
|
};
|
||||||
}
|
|
||||||
const getDeadlineTextColor = (deadline: string) => {
|
const getDeadlineTextColor = (deadline: string) => {
|
||||||
// generate three colors, yellow for 1 day, red for 0 days, green for more than 1 day
|
// generate three colors, yellow for 1 day, red for 0 days, green for more than 1 day
|
||||||
const deadlineDate = new Date(deadline);
|
const deadlineDate = new Date(deadline);
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const diff = deadlineDate.getTime() - currentDate.getTime();
|
const diff = deadlineDate.getTime() - currentDate.getTime();
|
||||||
const diffDays = Math.ceil(diff / (1000 * 3600 * 24));
|
const diffDays = Math.ceil(diff / (1000 * 3600 * 24));
|
||||||
if (diffDays < 0)
|
if (diffDays < 0) return "red.8"; // for past deadlines
|
||||||
return 'red.8'; // for past deadlines
|
|
||||||
if (diffDays === 1) {
|
if (diffDays === 1) {
|
||||||
return 'yellow.8';
|
return "yellow.8";
|
||||||
}
|
}
|
||||||
if (diffDays === 0) {
|
if (diffDays === 0) {
|
||||||
return 'orange.8';
|
return "orange.8";
|
||||||
}
|
}
|
||||||
return 'green.8';
|
return "green.8";
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<div onClick={() => onDealSummaryClick()} className={styles['container']}>
|
<div
|
||||||
<div className={classNames(styles['flex-row'], styles["flex-row-left"])}>
|
onClick={() => onDealSummaryClick()}
|
||||||
|
className={styles["container"]}>
|
||||||
<div className={styles['flex-item']}>
|
<div
|
||||||
<Text size={"sm"} c={"gray.6"}>
|
className={classNames(
|
||||||
|
styles["flex-row"],
|
||||||
|
styles["flex-row-left"]
|
||||||
|
)}>
|
||||||
|
<div className={styles["flex-item"]}>
|
||||||
|
<Text
|
||||||
|
size={"sm"}
|
||||||
|
c={"gray.6"}>
|
||||||
Клиент: {dealSummary.clientName}
|
Клиент: {dealSummary.clientName}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['flex-item']}>
|
<div className={styles["flex-item"]}>
|
||||||
<Text size={"md"} c={"blue.5"}>{dealSummary.name}</Text>
|
<Text
|
||||||
{dealSummary.shipmentWarehouseName &&
|
size={"md"}
|
||||||
<Text size={"sm"} c={"gray.6"}>{dealSummary.shipmentWarehouseName}</Text>
|
c={"blue.5"}>
|
||||||
}
|
{dealSummary.name}
|
||||||
|
</Text>
|
||||||
|
{dealSummary.shipmentWarehouseName && (
|
||||||
|
<Text
|
||||||
|
size={"sm"}
|
||||||
|
c={"gray.6"}>
|
||||||
|
{dealSummary.shipmentWarehouseName}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['flex-item']}>
|
<div className={styles["flex-item"]}>
|
||||||
<Text size={"sm"} c={"gray.6"}>
|
<Text
|
||||||
{dealSummary.totalPrice.toLocaleString('ru-RU')} руб, {dealSummary.totalProducts.toLocaleString("ru-RU")} тов.
|
size={"sm"}
|
||||||
|
c={"gray.6"}>
|
||||||
|
{dealSummary.totalPrice.toLocaleString("ru-RU")} руб,{" "}
|
||||||
|
{dealSummary.totalProducts.toLocaleString("ru-RU")} тов.
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
className={classNames(
|
||||||
className={classNames(styles['flex-row'], styles['flex-row-right'])}>
|
styles["flex-row"],
|
||||||
<div className={styles['flex-item']}>
|
styles["flex-row-right"]
|
||||||
|
)}>
|
||||||
|
<div className={styles["flex-item"]}>
|
||||||
<ActionIcon variant={"transparent"}>
|
<ActionIcon variant={"transparent"}>
|
||||||
<Image src={dealSummary.baseMarketplace?.iconUrl || ""}/>
|
<Image
|
||||||
|
src={dealSummary.baseMarketplace?.iconUrl || ""}
|
||||||
|
/>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['flex-item']}>
|
<div className={styles["flex-item"]}>
|
||||||
<Text size={"sm"} c={getDeadlineTextColor(dealSummary.deadline)}>
|
<Text
|
||||||
{new Date(dealSummary.deadline).toLocaleString('ru-RU').slice(0, -3)}
|
size={"sm"}
|
||||||
|
c={getDeadlineTextColor(dealSummary.deadline)}>
|
||||||
|
{new Date(dealSummary.deadline)
|
||||||
|
.toLocaleString("ru-RU")
|
||||||
|
.slice(0, -3)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<CopyButton value={"https://google.com"}>
|
<CopyButton value={"https://google.com"}>
|
||||||
{({copy, copied}) => (
|
{({ copy, copied }) => (
|
||||||
<Popover opened={copied} withArrow>
|
<Popover
|
||||||
|
opened={copied}
|
||||||
|
withArrow>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<div
|
<div
|
||||||
|
onClick={e => {
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
copy();
|
copy();
|
||||||
}}
|
}}
|
||||||
className={styles['flex-item']}>
|
className={styles["flex-item"]}>
|
||||||
<Badge variant={"light"} radius={"sm"}>
|
<Badge
|
||||||
|
variant={"light"}
|
||||||
|
radius={"sm"}>
|
||||||
ID: {dealSummary.id}
|
ID: {dealSummary.id}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<Flex justify={"center"} align={"center"} gap={rem(5)}>
|
<Flex
|
||||||
|
justify={"center"}
|
||||||
|
align={"center"}
|
||||||
|
gap={rem(5)}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
bounce
|
bounce
|
||||||
style={{animationIterationCount: 1}}
|
style={{ animationIterationCount: 1 }}
|
||||||
icon={faCheck}
|
icon={faCheck}
|
||||||
/>
|
/>
|
||||||
<Text size={"xs"}>ID сделки скопирован</Text>
|
<Text size={"xs"}>
|
||||||
|
ID сделки скопирован
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</CopyButton>
|
</CopyButton>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default DealSummaryCard;
|
export default DealSummaryCard;
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: var(--mantine-radius-md);
|
border-radius: var(--mantine-radius-md);
|
||||||
border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
border: rem(1px) solid
|
||||||
|
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-xl);
|
padding: var(--mantine-spacing-sm) var(--mantine-spacing-xl);
|
||||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-5));
|
background-color: light-dark(
|
||||||
|
var(--mantine-color-white),
|
||||||
|
var(--mantine-color-dark-5)
|
||||||
|
);
|
||||||
margin-bottom: var(--mantine-spacing-sm);
|
margin-bottom: var(--mantine-spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,4 +20,4 @@
|
|||||||
font-size: rem(30px);
|
font-size: rem(30px);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
width: rem(60px);
|
width: rem(60px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import styles from './Header.module.css';
|
import styles from "./Header.module.css";
|
||||||
import {Button, TextInput} from "@mantine/core";
|
import { Button, TextInput } from "@mantine/core";
|
||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
const Header: FC = ()=>{
|
|
||||||
|
|
||||||
|
const Header: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles['header']}>
|
<div className={styles["header"]}>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
radius={0}
|
radius={0}
|
||||||
placeholder={"Поиск и фильтры"}
|
placeholder={"Поиск и фильтры"}
|
||||||
size={"xl"}
|
size={"xl"}
|
||||||
className={styles['header-input']}
|
className={styles["header-input"]}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
radius={0}
|
radius={0}
|
||||||
color={"gray"}
|
color={"gray"}
|
||||||
variant={'default'}
|
variant={"default"}
|
||||||
className={styles['header-button']}
|
className={styles["header-button"]}>
|
||||||
>Поиск</Button>
|
Поиск
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|||||||
@@ -1,31 +1,45 @@
|
|||||||
import {Dropzone, DropzoneProps, FileWithPath} from "@mantine/dropzone";
|
import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
|
||||||
import {FC, useState} from "react";
|
import { FC, useState } from "react";
|
||||||
import {Button, Fieldset, Flex, Group, Image, Loader, rem, Text} from "@mantine/core";
|
import {
|
||||||
import {IconPhoto, IconUpload, IconX} from "@tabler/icons-react";
|
Button,
|
||||||
import {omit} from "lodash";
|
Fieldset,
|
||||||
import {BaseFormInputProps} from "../../types/utils.ts";
|
Flex,
|
||||||
import {notifications} from "../../shared/lib/notifications.ts";
|
Group,
|
||||||
import {ProductService} from "../../client";
|
Image,
|
||||||
|
Loader,
|
||||||
|
rem,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||||
|
import { omit } from "lodash";
|
||||||
|
import { BaseFormInputProps } from "../../types/utils.ts";
|
||||||
|
import { notifications } from "../../shared/lib/notifications.ts";
|
||||||
|
import { ProductService } from "../../client";
|
||||||
|
|
||||||
interface RestProps {
|
interface RestProps {
|
||||||
imageUrlInputProps?: BaseFormInputProps<string>;
|
imageUrlInputProps?: BaseFormInputProps<string>;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
|
||||||
type Props = Omit<DropzoneProps, 'onDrop'> & RestProps;
|
|
||||||
|
|
||||||
const ImageDropzone: FC<Props> = (props: Props) => {
|
const ImageDropzone: FC<Props> = (props: Props) => {
|
||||||
const [showDropzone, setShowDropzone] = useState(
|
const [showDropzone, setShowDropzone] = useState(
|
||||||
!(typeof props.imageUrlInputProps?.value === 'string' &&
|
!(
|
||||||
props.imageUrlInputProps.value.trim() !== '')
|
typeof props.imageUrlInputProps?.value === "string" &&
|
||||||
|
props.imageUrlInputProps.value.trim() !== ""
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const restProps = omit(props, ['imageUrl', 'productId', 'imageUrlInputProps']);
|
const restProps = omit(props, [
|
||||||
|
"imageUrl",
|
||||||
|
"productId",
|
||||||
|
"imageUrlInputProps",
|
||||||
|
]);
|
||||||
const onDrop = (files: FileWithPath[]) => {
|
const onDrop = (files: FileWithPath[]) => {
|
||||||
if (!props.productId || !props.imageUrlInputProps) return;
|
if (!props.productId || !props.imageUrlInputProps) return;
|
||||||
if (files.length > 1) {
|
if (files.length > 1) {
|
||||||
notifications.error({message: "Прикрепите одно изображение"});
|
notifications.error({ message: "Прикрепите одно изображение" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
@@ -34,33 +48,35 @@ const ImageDropzone: FC<Props> = (props: Props) => {
|
|||||||
ProductService.uploadProductImage({
|
ProductService.uploadProductImage({
|
||||||
productId: props.productId,
|
productId: props.productId,
|
||||||
formData: {
|
formData: {
|
||||||
upload_file: file
|
upload_file: file,
|
||||||
}
|
},
|
||||||
}).then(({ok, message, imageUrl}) => {
|
})
|
||||||
notifications.guess(ok, {message});
|
.then(({ ok, message, imageUrl }) => {
|
||||||
setIsLoading(false);
|
notifications.guess(ok, { message });
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
if (!ok || !imageUrl) {
|
if (!ok || !imageUrl) {
|
||||||
|
setShowDropzone(true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.imageUrlInputProps?.onChange(imageUrl);
|
||||||
|
setShowDropzone(false);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
notifications.error({ message: error.toString() });
|
||||||
setShowDropzone(true);
|
setShowDropzone(true);
|
||||||
|
setIsLoading(false);
|
||||||
return;
|
});
|
||||||
}
|
};
|
||||||
props.imageUrlInputProps?.onChange(imageUrl);
|
|
||||||
setShowDropzone(false);
|
|
||||||
}).catch(error => {
|
|
||||||
notifications.error({message: error.toString()});
|
|
||||||
setShowDropzone(true);
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const getBody = () => {
|
const getBody = () => {
|
||||||
return props.imageUrlInputProps?.value && !showDropzone ? (
|
return props.imageUrlInputProps?.value && !showDropzone ? (
|
||||||
<Image src={props.imageUrlInputProps.value}/>
|
<Image src={props.imageUrlInputProps.value} />
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
{...restProps}
|
{...restProps}
|
||||||
accept={["image/png",
|
accept={[
|
||||||
|
"image/png",
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
"image/gif",
|
"image/gif",
|
||||||
"image/bmp",
|
"image/bmp",
|
||||||
@@ -68,58 +84,74 @@ const ImageDropzone: FC<Props> = (props: Props) => {
|
|||||||
"image/x-icon",
|
"image/x-icon",
|
||||||
"image/webp",
|
"image/webp",
|
||||||
"image/svg+xml",
|
"image/svg+xml",
|
||||||
"image/heic"]}
|
"image/heic",
|
||||||
|
]}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}>
|
||||||
|
<Group
|
||||||
>
|
justify="center"
|
||||||
<Group justify="center" gap="xl" style={{pointerEvents: 'none'}}>
|
gap="xl"
|
||||||
|
style={{ pointerEvents: "none" }}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload
|
<IconUpload
|
||||||
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)'}}
|
style={{
|
||||||
|
width: rem(52),
|
||||||
|
height: rem(52),
|
||||||
|
color: "var(--mantine-color-blue-6)",
|
||||||
|
}}
|
||||||
stroke={1.5}
|
stroke={1.5}
|
||||||
/>
|
/>
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX
|
<IconX
|
||||||
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)'}}
|
style={{
|
||||||
|
width: rem(52),
|
||||||
|
height: rem(52),
|
||||||
|
color: "var(--mantine-color-red-6)",
|
||||||
|
}}
|
||||||
stroke={1.5}
|
stroke={1.5}
|
||||||
/>
|
/>
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto
|
<IconPhoto
|
||||||
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-dimmed)'}}
|
style={{
|
||||||
|
width: rem(52),
|
||||||
|
height: rem(52),
|
||||||
|
color: "var(--mantine-color-dimmed)",
|
||||||
|
}}
|
||||||
stroke={1.5}
|
stroke={1.5}
|
||||||
/>
|
/>
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
|
||||||
<div style={{textAlign: "center"}}>
|
<div style={{ textAlign: "center" }}>
|
||||||
<Text size="xl" inline>
|
<Text
|
||||||
|
size="xl"
|
||||||
|
inline>
|
||||||
Перенесите изображение или нажмите чтоб выбрать файл
|
Перенесите изображение или нажмите чтоб выбрать файл
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
gap={rem(10)}
|
gap={rem(10)}
|
||||||
direction={"column"}
|
direction={"column"}>
|
||||||
|
<Fieldset legend={"Изображение"}>
|
||||||
>
|
|
||||||
<Fieldset
|
|
||||||
legend={'Изображение'}>
|
|
||||||
<Flex justify={"center"}>
|
<Flex justify={"center"}>
|
||||||
{isLoading ? <Loader/> : getBody()}
|
{isLoading ? <Loader /> : getBody()}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
{!showDropzone &&
|
{!showDropzone && (
|
||||||
<Button onClick={() => setShowDropzone(true)} variant={"default"}>Заменить изображение {}</Button>
|
<Button
|
||||||
}
|
onClick={() => setShowDropzone(true)}
|
||||||
|
variant={"default"}>
|
||||||
|
Заменить изображение {}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ImageDropzone;
|
export default ImageDropzone;
|
||||||
|
|||||||
@@ -1,34 +1,47 @@
|
|||||||
.control {
|
.control {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||||
color: var(--mantine-color-text);
|
color: var(--mantine-color-text);
|
||||||
font-size: var(--mantine-font-size-sm);
|
font-size: var(--mantine-font-size-sm);
|
||||||
|
|
||||||
@mixin hover {
|
@mixin hover {
|
||||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
background-color: light-dark(
|
||||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
var(--mantine-color-gray-0),
|
||||||
}
|
var(--mantine-color-dark-7)
|
||||||
|
);
|
||||||
|
color: light-dark(
|
||||||
|
var(--mantine-color-black),
|
||||||
|
var(--mantine-color-dark-0)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||||
padding-left: var(--mantine-spacing-md);
|
padding-left: var(--mantine-spacing-md);
|
||||||
margin-left: var(--mantine-spacing-xl);
|
margin-left: var(--mantine-spacing-xl);
|
||||||
font-size: var(--mantine-font-size-sm);
|
font-size: var(--mantine-font-size-sm);
|
||||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||||
border-left: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
border-left: 1px solid
|
||||||
|
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||||
|
|
||||||
@mixin hover {
|
@mixin hover {
|
||||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
background-color: light-dark(
|
||||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
var(--mantine-color-gray-0),
|
||||||
}
|
var(--mantine-color-dark-7)
|
||||||
|
);
|
||||||
|
color: light-dark(
|
||||||
|
var(--mantine-color-black),
|
||||||
|
var(--mantine-color-dark-0)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
transition: transform 200ms ease;
|
transition: transform 200ms ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
import {Box, Collapse, Group, rem, ThemeIcon, UnstyledButton} from '@mantine/core';
|
import {
|
||||||
import {IconCalendarStats, IconChevronRight} from '@tabler/icons-react';
|
Box,
|
||||||
import classes from './LinksGroup.module.css';
|
Collapse,
|
||||||
import {Link} from "@tanstack/react-router";
|
Group,
|
||||||
|
rem,
|
||||||
|
ThemeIcon,
|
||||||
|
UnstyledButton,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconCalendarStats, IconChevronRight } from "@tabler/icons-react";
|
||||||
|
import classes from "./LinksGroup.module.css";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
interface LinksGroupProps {
|
interface LinksGroupProps {
|
||||||
icon: React.FC<any>;
|
icon: React.FC<any>;
|
||||||
@@ -11,28 +18,36 @@ interface LinksGroupProps {
|
|||||||
links?: { label: string; link: string }[];
|
links?: { label: string; link: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGroupProps) {
|
export function LinksGroup({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
initiallyOpened,
|
||||||
|
links,
|
||||||
|
}: LinksGroupProps) {
|
||||||
const hasLinks = Array.isArray(links);
|
const hasLinks = Array.isArray(links);
|
||||||
const [opened, setOpened] = useState(initiallyOpened || false);
|
const [opened, setOpened] = useState(initiallyOpened || false);
|
||||||
const items = (hasLinks ? links : []).map((link) => (
|
const items = (hasLinks ? links : []).map(link => (
|
||||||
<Link to={link.link}
|
<Link
|
||||||
className={classes.link}
|
to={link.link}
|
||||||
key={link.label}
|
className={classes.link}
|
||||||
|
key={link.label}>
|
||||||
>
|
|
||||||
|
|
||||||
{link.label}
|
{link.label}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UnstyledButton onClick={() => setOpened((o) => !o)} className={classes.control}>
|
<UnstyledButton
|
||||||
<Group justify="space-between" gap={0}>
|
onClick={() => setOpened(o => !o)}
|
||||||
<Box style={{display: 'flex', alignItems: 'center'}}>
|
className={classes.control}>
|
||||||
<ThemeIcon variant="light" size={30}>
|
<Group
|
||||||
<Icon style={{width: rem(18), height: rem(18)}}/>
|
justify="space-between"
|
||||||
|
gap={0}>
|
||||||
|
<Box style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<ThemeIcon
|
||||||
|
variant="light"
|
||||||
|
size={30}>
|
||||||
|
<Icon style={{ width: rem(18), height: rem(18) }} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Box ml="md">{label}</Box>
|
<Box ml="md">{label}</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -43,7 +58,7 @@ export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGro
|
|||||||
style={{
|
style={{
|
||||||
width: rem(16),
|
width: rem(16),
|
||||||
height: rem(16),
|
height: rem(16),
|
||||||
transform: opened ? 'rotate(-90deg)' : 'none',
|
transform: opened ? "rotate(-90deg)" : "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -55,19 +70,21 @@ export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mockdata = {
|
const mockdata = {
|
||||||
label: 'Releases',
|
label: "Releases",
|
||||||
icon: IconCalendarStats,
|
icon: IconCalendarStats,
|
||||||
links: [
|
links: [
|
||||||
{label: 'Upcoming releases', link: '/'},
|
{ label: "Upcoming releases", link: "/" },
|
||||||
{label: 'Previous releases', link: '/'},
|
{ label: "Previous releases", link: "/" },
|
||||||
{label: 'Releases schedule', link: '/'},
|
{ label: "Releases schedule", link: "/" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function NavbarLinksGroup() {
|
export function NavbarLinksGroup() {
|
||||||
return (
|
return (
|
||||||
<Box mih={220} p="md">
|
<Box
|
||||||
|
mih={220}
|
||||||
|
p="md">
|
||||||
<LinksGroup {...mockdata} />
|
<LinksGroup {...mockdata} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
|
export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} version="1.0" xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
viewBox="0 0 1024.000000 1024.000000"
|
{...props}
|
||||||
preserveAspectRatio="xMidYMid meet">
|
version="1.0"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
viewBox="0 0 1024.000000 1024.000000"
|
||||||
fill="#2a75ec" stroke="none">
|
preserveAspectRatio="xMidYMid meet">
|
||||||
<path d="M2600 7290 l0 -760 690 0 690 0 0 196 0 195 728 -4 c613 -3 743 -7
|
<g
|
||||||
|
transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#2a75ec"
|
||||||
|
stroke="none">
|
||||||
|
<path
|
||||||
|
d="M2600 7290 l0 -760 690 0 690 0 0 196 0 195 728 -4 c613 -3 743 -7
|
||||||
831 -20 57 -10 106 -15 108 -13 7 7 341 -80 358 -93 8 -7 79 -34 95 -36 28 -4
|
831 -20 57 -10 106 -15 108 -13 7 7 341 -80 358 -93 8 -7 79 -34 95 -36 28 -4
|
||||||
293 -151 298 -166 2 -5 8 -9 13 -9 9 0 75 -51 177 -138 44 -38 120 -123 218
|
293 -151 298 -166 2 -5 8 -9 13 -9 9 0 75 -51 177 -138 44 -38 120 -123 218
|
||||||
-245 73 -91 215 -390 230 -484 3 -21 8 -42 11 -46 20 -33 46 -190 58 -355 12
|
-245 73 -91 215 -390 230 -484 3 -21 8 -42 11 -46 20 -33 46 -190 58 -355 12
|
||||||
@@ -27,11 +32,13 @@ export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|||||||
-40 57 -103 155 -5 9 -15 20 -21 26 -6 5 -40 48 -76 95 -127 165 -291 325
|
-40 57 -103 155 -5 9 -15 20 -21 26 -6 5 -40 48 -76 95 -127 165 -291 325
|
||||||
-473 459 -38 29 -83 62 -98 74 -16 12 -40 29 -55 37 -15 8 -67 39 -115 69 -99
|
-473 459 -38 29 -83 62 -98 74 -16 12 -40 29 -55 37 -15 8 -67 39 -115 69 -99
|
||||||
59 -339 177 -443 216 -245 93 -524 167 -774 205 -335 51 -306 50 -1877 50
|
59 -339 177 -443 216 -245 93 -524 167 -774 205 -335 51 -306 50 -1877 50
|
||||||
l-1473 0 0 -760z"/>
|
l-1473 0 0 -760z"
|
||||||
<path d="M1730 5175 l0 -1095 1580 0 1580 0 0 1095 0 1095 -1580 0 -1580 0 0
|
/>
|
||||||
-1095z"/>
|
<path
|
||||||
|
d="M1730 5175 l0 -1095 1580 0 1580 0 0 1095 0 1095 -1580 0 -1580 0 0
|
||||||
|
-1095z"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
);
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,11 +6,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: var(--mantine-color-body);
|
background-color: var(--mantine-color-body);
|
||||||
box-shadow: 0 2px 4px var(--mantine-color-dark-7), 0 4px 24px var(--mantine-color-dark-7);
|
box-shadow:
|
||||||
|
0 2px 4px var(--mantine-color-dark-7),
|
||||||
|
0 4px 24px var(--mantine-color-dark-7);
|
||||||
}
|
}
|
||||||
@mixin light {
|
@mixin light {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .08), 0 4px 24px rgba(0, 0, 0, .08);
|
box-shadow:
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.08),
|
||||||
|
0 4px 24px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
border-radius: rem(20);
|
border-radius: rem(20);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -32,7 +36,10 @@
|
|||||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
|
background-color: light-dark(
|
||||||
|
var(--mantine-color-gray-0),
|
||||||
|
var(--mantine-color-dark-5)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-active] {
|
&[data-active] {
|
||||||
@@ -43,4 +50,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import {Center, Flex, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColorScheme} from '@mantine/core';
|
import {
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
Image,
|
||||||
|
rem,
|
||||||
|
Stack,
|
||||||
|
Tooltip,
|
||||||
|
UnstyledButton,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconBarcode,
|
IconBarcode,
|
||||||
IconBox, IconBuildingWarehouse,
|
IconBox,
|
||||||
|
IconBuildingWarehouse,
|
||||||
IconCash,
|
IconCash,
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
IconFileBarcode,
|
IconFileBarcode,
|
||||||
IconHome2,
|
IconHome2,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconMan,
|
IconMan,
|
||||||
IconMoon, IconShoppingCart,
|
IconMoon,
|
||||||
|
IconShoppingCart,
|
||||||
IconSun,
|
IconSun,
|
||||||
} from '@tabler/icons-react';
|
} from "@tabler/icons-react";
|
||||||
import classes from './Navbar.module.css';
|
import classes from "./Navbar.module.css";
|
||||||
import {useAppDispatch} from "../../redux/store.ts";
|
import { useAppDispatch } from "../../redux/store.ts";
|
||||||
import {logout} from "../../features/authSlice.ts";
|
import { logout } from "../../features/authSlice.ts";
|
||||||
import {useNavigate, useRouterState} from "@tanstack/react-router";
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||||
|
|
||||||
interface NavbarLinkProps {
|
interface NavbarLinkProps {
|
||||||
icon: typeof IconHome2;
|
icon: typeof IconHome2;
|
||||||
@@ -28,24 +39,31 @@ interface NavbarLinkProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function NavbarLink(props: NavbarLinkProps) {
|
function NavbarLink(props: NavbarLinkProps) {
|
||||||
const {icon: Icon, label, active, onClick} = props;
|
const { icon: Icon, label, active, onClick } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip display={!label ? "none" : "flex"} label={label} position="right" transitionProps={{duration: 0}}>
|
<Tooltip
|
||||||
<UnstyledButton onClick={() => onClick && onClick(props)}
|
display={!label ? "none" : "flex"}
|
||||||
className={classes.link}
|
label={label}
|
||||||
data-active={active || undefined}>
|
position="right"
|
||||||
<Icon style={{width: rem(20), height: rem(20)}} stroke={1.5}/>
|
transitionProps={{ duration: 0 }}>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => onClick && onClick(props)}
|
||||||
|
className={classes.link}
|
||||||
|
data-active={active || undefined}>
|
||||||
|
<Icon
|
||||||
|
style={{ width: rem(20), height: rem(20) }}
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockdata = [
|
const mockdata = [
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: IconCash,
|
icon: IconCash,
|
||||||
label: 'Сделки',
|
label: "Сделки",
|
||||||
href: '/leads'
|
href: "/leads",
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// icon: IconTable,
|
// icon: IconTable,
|
||||||
@@ -54,48 +72,50 @@ const mockdata = [
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
icon: IconMan,
|
icon: IconMan,
|
||||||
label: 'Клиенты',
|
label: "Клиенты",
|
||||||
href: '/clients'
|
href: "/clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconBox,
|
icon: IconBox,
|
||||||
label: 'Услуги',
|
label: "Услуги",
|
||||||
href: '/services'
|
href: "/services",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconBarcode,
|
icon: IconBarcode,
|
||||||
label: 'Товары',
|
label: "Товары",
|
||||||
href: '/products'
|
href: "/products",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconFileBarcode,
|
icon: IconFileBarcode,
|
||||||
label: 'Штрихкоды',
|
label: "Штрихкоды",
|
||||||
href: '/barcode'
|
href: "/barcode",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconBuildingWarehouse,
|
icon: IconBuildingWarehouse,
|
||||||
label: 'Склады отгрузки',
|
label: "Склады отгрузки",
|
||||||
href: '/shipping_warehouses'
|
href: "/shipping_warehouses",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon:IconShoppingCart,
|
icon: IconShoppingCart,
|
||||||
label: 'Маркетплейсы',
|
label: "Маркетплейсы",
|
||||||
href: '/marketplaces'
|
href: "/marketplaces",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const router = useRouterState();
|
const router = useRouterState();
|
||||||
const {colorScheme, toggleColorScheme} = useMantineColorScheme({keepTransitions: true});
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme({
|
||||||
|
keepTransitions: true,
|
||||||
|
});
|
||||||
const onLogoutClick = () => {
|
const onLogoutClick = () => {
|
||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
navigate({to: '/login'});
|
navigate({ to: "/login" });
|
||||||
}
|
};
|
||||||
const onNavlinkClick = (props: NavbarLinkProps) => {
|
const onNavlinkClick = (props: NavbarLinkProps) => {
|
||||||
navigate({to: props.href});
|
navigate({ to: props.href });
|
||||||
}
|
};
|
||||||
const links = mockdata.map((link, index) => (
|
const links = mockdata.map((link, index) => (
|
||||||
<NavbarLink
|
<NavbarLink
|
||||||
{...link}
|
{...link}
|
||||||
@@ -108,35 +128,62 @@ export function Navbar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={classes.navbar}>
|
<nav className={classes.navbar}>
|
||||||
<Flex direction={"column"} gap={rem(30)}>
|
<Flex
|
||||||
<Center
|
direction={"column"}
|
||||||
p={rem(5)}
|
gap={rem(30)}>
|
||||||
>
|
<Center p={rem(5)}>
|
||||||
<Image
|
<Image
|
||||||
flex={1}
|
flex={1}
|
||||||
// style={{filter: "drop-shadow(0 0 30px #fff)"}}
|
// style={{filter: "drop-shadow(0 0 30px #fff)"}}
|
||||||
src={colorScheme == "dark" ? "/icons/logo-light.png" : "/icons/logo.png"}
|
src={
|
||||||
|
colorScheme == "dark"
|
||||||
|
? "/icons/logo-light.png"
|
||||||
|
: "/icons/logo.png"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
<div className={classes.navbarMain}>
|
<div className={classes.navbarMain}>
|
||||||
<Stack justify="center" gap={rem(10)}>
|
<Stack
|
||||||
|
justify="center"
|
||||||
|
gap={rem(10)}>
|
||||||
{links}
|
{links}
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Stack w={"100%"} justify="center" gap={0}>
|
<Stack
|
||||||
<NavbarLink icon={IconDashboard}
|
w={"100%"}
|
||||||
href={"/admin"}
|
justify="center"
|
||||||
index={-1}
|
gap={0}>
|
||||||
label={"Панель администратора"}
|
<NavbarLink
|
||||||
onClick={() => onNavlinkClick({href: "/admin", index: -1, icon: IconDashboard})}
|
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="Выйти"
|
||||||
/>
|
/>
|
||||||
<NavbarLink label={"Сменить тему"} onClick={toggleColorScheme}
|
|
||||||
icon={colorScheme == "dark" ? IconSun : IconMoon} href={"#"} index={-1}/>
|
|
||||||
<NavbarLink index={-1} href={"#"} onClick={onLogoutClick} icon={IconLogout} label="Выйти"/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
.user {
|
.user {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--mantine-spacing-md);
|
padding: var(--mantine-spacing-md);
|
||||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||||
|
|
||||||
@mixin hover {
|
@mixin hover {
|
||||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
background-color: light-dark(
|
||||||
}
|
var(--mantine-color-gray-0),
|
||||||
}
|
var(--mantine-color-dark-8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core';
|
import { UnstyledButton, Group, Avatar, Text, rem } from "@mantine/core";
|
||||||
import { IconChevronRight } from '@tabler/icons-react';
|
import { IconChevronRight } from "@tabler/icons-react";
|
||||||
import classes from './UserButton.module.css';
|
import classes from "./UserButton.module.css";
|
||||||
|
|
||||||
export function UserButton() {
|
export function UserButton() {
|
||||||
return (
|
return (
|
||||||
@@ -12,17 +12,24 @@ export function UserButton() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Text size="sm" fw={500}>
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={500}>
|
||||||
Harriette Spoonlicker
|
Harriette Spoonlicker
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text c="dimmed" size="xs">
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
size="xs">
|
||||||
hspoonlicker@outlook.com
|
hspoonlicker@outlook.com
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} />
|
<IconChevronRight
|
||||||
|
style={{ width: rem(14), height: rem(14) }}
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import {IconAdjustments, IconGauge} from "@tabler/icons-react";
|
import { IconAdjustments, IconGauge } from "@tabler/icons-react";
|
||||||
|
|
||||||
export const NavbarLinks = [
|
export const NavbarLinks = [
|
||||||
{
|
{
|
||||||
label: 'Главная',
|
label: "Главная",
|
||||||
icon: IconGauge,
|
icon: IconGauge,
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
label: "123",
|
label: "123",
|
||||||
link: "/login"
|
link: "/login",
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Настройки',
|
label: "Настройки",
|
||||||
icon: IconAdjustments,
|
icon: IconAdjustments,
|
||||||
links:[
|
links: [
|
||||||
{
|
{
|
||||||
label: "Профиль"
|
label: "Профиль",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,40 +1,44 @@
|
|||||||
import {Autocomplete, AutocompleteProps} from "@mantine/core";
|
import { Autocomplete, AutocompleteProps } from "@mantine/core";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {ObjectWithNameAndId} from "../../types/utils.ts";
|
import { ObjectWithNameAndId } from "../../types/utils.ts";
|
||||||
import {omit} from "lodash";
|
import { omit } from "lodash";
|
||||||
|
|
||||||
|
|
||||||
export type AutocompleteObjectType<T extends ObjectWithNameAndId> = T;
|
export type AutocompleteObjectType<T extends ObjectWithNameAndId> = T;
|
||||||
|
|
||||||
type ControlledValueProps<T extends ObjectWithNameAndId> = {
|
type ControlledValueProps<T extends ObjectWithNameAndId> = {
|
||||||
value: AutocompleteObjectType<T>,
|
value: AutocompleteObjectType<T>;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
type RestProps<T extends ObjectWithNameAndId> = {
|
type RestProps<T extends ObjectWithNameAndId> = {
|
||||||
defaultValue?: AutocompleteObjectType<T>
|
defaultValue?: AutocompleteObjectType<T>;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
data: AutocompleteObjectType<T>[];
|
data: AutocompleteObjectType<T>[];
|
||||||
filterBy?: (item: AutocompleteObjectType<T>) => boolean;
|
filterBy?: (item: AutocompleteObjectType<T>) => boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ObjectAutocompleteProps<T extends ObjectWithNameAndId> =
|
export type ObjectAutocompleteProps<T extends ObjectWithNameAndId> =
|
||||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
(RestProps<T> & Partial<ControlledValueProps<T>>) &
|
||||||
& Omit<AutocompleteProps, 'value' | 'onChange' | 'data'>;
|
Omit<AutocompleteProps, "value" | "onChange" | "data">;
|
||||||
|
|
||||||
const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutocompleteProps<T>) => {
|
const ObjectAutocomplete = <T extends ObjectWithNameAndId>(
|
||||||
|
props: ObjectAutocompleteProps<T>
|
||||||
const isControlled = 'value' in props;
|
) => {
|
||||||
const [internalValue, setInternalValue] = useState<undefined | string>(props.defaultValue);
|
const isControlled = "value" in props;
|
||||||
|
const [internalValue, setInternalValue] = useState<undefined | string>(
|
||||||
|
props.defaultValue
|
||||||
|
);
|
||||||
|
|
||||||
const value = isControlled ? props.value?.name : internalValue;
|
const value = isControlled ? props.value?.name : internalValue;
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
|
const propsData = props.filterBy
|
||||||
|
? props.data.filter(props.filterBy)
|
||||||
|
: props.data;
|
||||||
|
|
||||||
return propsData.map(item => ({
|
return propsData.map(item => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
value: item.id.toString()
|
value: item.id.toString(),
|
||||||
}));
|
}));
|
||||||
}, [props.data]);
|
}, [props.data]);
|
||||||
|
|
||||||
@@ -45,13 +49,13 @@ const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutoco
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInternalValue(event);
|
setInternalValue(event);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isControlled || !internalValue) return;
|
if (isControlled || !internalValue) return;
|
||||||
props.onChange(internalValue);
|
props.onChange(internalValue);
|
||||||
}, [internalValue]);
|
}, [internalValue]);
|
||||||
const restProps = omit(props, ['filterBy', 'groupBy']);
|
const restProps = omit(props, ["filterBy", "groupBy"]);
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...restProps}
|
{...restProps}
|
||||||
@@ -59,7 +63,7 @@ const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutoco
|
|||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
data={data}
|
data={data}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ObjectAutocomplete;
|
export default ObjectAutocomplete;
|
||||||
|
|||||||
@@ -1,83 +1,91 @@
|
|||||||
import {MultiSelect, MultiSelectProps} from "@mantine/core";
|
import { MultiSelect, MultiSelectProps } from "@mantine/core";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {groupBy} from "lodash";
|
import { groupBy } from "lodash";
|
||||||
|
|
||||||
interface ObjectWithIdAndName {
|
interface ObjectWithIdAndName {
|
||||||
id: number,
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MultiselectObjectType<T> = T;
|
export type MultiselectObjectType<T> = T;
|
||||||
|
|
||||||
type ControlledValueProps<T> = {
|
type ControlledValueProps<T> = {
|
||||||
value: MultiselectObjectType<T>[],
|
value: MultiselectObjectType<T>[];
|
||||||
onChange: (value: MultiselectObjectType<T>[]) => void;
|
onChange: (value: MultiselectObjectType<T>[]) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
type CustomLabelAndKeyProps<T> = {
|
type CustomLabelAndKeyProps<T> = {
|
||||||
getLabelFn: (item: MultiselectObjectType<T>) => string;
|
getLabelFn: (item: MultiselectObjectType<T>) => string;
|
||||||
getValueFn: (item: MultiselectObjectType<T>) => string;
|
getValueFn: (item: MultiselectObjectType<T>) => string;
|
||||||
}
|
};
|
||||||
type RestProps<T> = {
|
type RestProps<T> = {
|
||||||
defaultValue?: MultiselectObjectType<T>[]
|
defaultValue?: MultiselectObjectType<T>[];
|
||||||
onChange: (value: MultiselectObjectType<T>[]) => void;
|
onChange: (value: MultiselectObjectType<T>[]) => void;
|
||||||
data: MultiselectObjectType<T>[];
|
data: MultiselectObjectType<T>[];
|
||||||
groupBy?: (item: MultiselectObjectType<T>) => string;
|
groupBy?: (item: MultiselectObjectType<T>) => string;
|
||||||
filterBy?: (item: MultiselectObjectType<T>) => boolean;
|
filterBy?: (item: MultiselectObjectType<T>) => boolean;
|
||||||
}
|
};
|
||||||
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||||
return item.name;
|
return item.name;
|
||||||
}
|
};
|
||||||
|
|
||||||
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||||
return item.id.toString();
|
return item.id.toString();
|
||||||
}
|
};
|
||||||
export type ObjectMultiSelectProps<T> =
|
export type ObjectMultiSelectProps<T> = (RestProps<T> &
|
||||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
Partial<ControlledValueProps<T>>) &
|
||||||
& Omit<MultiSelectProps, 'value' | 'onChange' | 'data'>
|
Omit<MultiSelectProps, "value" | "onChange" | "data"> &
|
||||||
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>);
|
(T extends ObjectWithIdAndName
|
||||||
|
? Partial<CustomLabelAndKeyProps<T>>
|
||||||
|
: CustomLabelAndKeyProps<T>);
|
||||||
|
|
||||||
const ObjectMultiSelect = <T, >(props: ObjectMultiSelectProps<T>) => {
|
const ObjectMultiSelect = <T,>(props: ObjectMultiSelectProps<T>) => {
|
||||||
|
const isControlled = "value" in props;
|
||||||
|
const haveGetValueFn = "getValueFn" in props;
|
||||||
|
const haveGetLabelFn = "getLabelFn" in props;
|
||||||
|
|
||||||
const isControlled = 'value' in props;
|
const [internalValue, setInternalValue] = useState<
|
||||||
const haveGetValueFn = 'getValueFn' in props;
|
MultiselectObjectType<T>[] | undefined
|
||||||
const haveGetLabelFn = 'getLabelFn' in props;
|
>(props.defaultValue);
|
||||||
|
|
||||||
const [internalValue, setInternalValue] = useState<MultiselectObjectType<T>[] | undefined>(props.defaultValue);
|
|
||||||
|
|
||||||
const value = (isControlled ? props.value : internalValue) || [];
|
const value = (isControlled ? props.value : internalValue) || [];
|
||||||
|
|
||||||
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
const getValueFn =
|
||||||
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||||
|
const getLabelFn =
|
||||||
|
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
|
const propsData = props.filterBy
|
||||||
|
? props.data.filter(props.filterBy)
|
||||||
|
: props.data;
|
||||||
if (props.groupBy) {
|
if (props.groupBy) {
|
||||||
|
|
||||||
const groupedData = groupBy(propsData, props.groupBy);
|
const groupedData = groupBy(propsData, props.groupBy);
|
||||||
return Object.entries(groupedData).map(([group, items]) => ({
|
return Object.entries(groupedData).map(([group, items]) => ({
|
||||||
group,
|
group,
|
||||||
items: items.map(item => ({
|
items: items.map(item => ({
|
||||||
label: getLabelFn(item),
|
label: getLabelFn(item),
|
||||||
value: getValueFn(item)
|
value: getValueFn(item),
|
||||||
}))
|
})),
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
return propsData.map(item => ({
|
return propsData.map(item => ({
|
||||||
label: getLabelFn(item),
|
label: getLabelFn(item),
|
||||||
value: getValueFn(item)
|
value: getValueFn(item),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [props.data, props.groupBy]);
|
}, [props.data, props.groupBy]);
|
||||||
|
|
||||||
const handleOnChange = (event: string[]) => {
|
const handleOnChange = (event: string[]) => {
|
||||||
const objects = props.data.filter(item => event.includes(getValueFn(item)));
|
const objects = props.data.filter(item =>
|
||||||
|
event.includes(getValueFn(item))
|
||||||
|
);
|
||||||
if (isControlled) {
|
if (isControlled) {
|
||||||
props.onChange(objects);
|
props.onChange(objects);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInternalValue(objects);
|
setInternalValue(objects);
|
||||||
}
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isControlled || !internalValue) return;
|
if (isControlled || !internalValue) return;
|
||||||
props.onChange(internalValue);
|
props.onChange(internalValue);
|
||||||
@@ -90,7 +98,7 @@ const ObjectMultiSelect = <T, >(props: ObjectMultiSelectProps<T>) => {
|
|||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
data={data}
|
data={data}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ObjectMultiSelect;
|
export default ObjectMultiSelect;
|
||||||
|
|||||||
@@ -1,73 +1,77 @@
|
|||||||
import {Select, SelectProps} from "@mantine/core";
|
import { Select, SelectProps } from "@mantine/core";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {groupBy, omit} from "lodash";
|
import { groupBy, omit } from "lodash";
|
||||||
|
|
||||||
interface ObjectWithIdAndName {
|
interface ObjectWithIdAndName {
|
||||||
id: number,
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectObjectType<T> = T;
|
export type SelectObjectType<T> = T;
|
||||||
|
|
||||||
type ControlledValueProps<T> = {
|
type ControlledValueProps<T> = {
|
||||||
value: SelectObjectType<T>,
|
value: SelectObjectType<T>;
|
||||||
onChange: (value: SelectObjectType<T>) => void;
|
onChange: (value: SelectObjectType<T>) => void;
|
||||||
}
|
};
|
||||||
type CustomLabelAndKeyProps<T> = {
|
type CustomLabelAndKeyProps<T> = {
|
||||||
getLabelFn: (item: SelectObjectType<T>) => string;
|
getLabelFn: (item: SelectObjectType<T>) => string;
|
||||||
getValueFn: (item: SelectObjectType<T>) => string;
|
getValueFn: (item: SelectObjectType<T>) => string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type RestProps<T> = {
|
type RestProps<T> = {
|
||||||
defaultValue?: SelectObjectType<T>
|
defaultValue?: SelectObjectType<T>;
|
||||||
onChange: (value: SelectObjectType<T>) => void;
|
onChange: (value: SelectObjectType<T>) => void;
|
||||||
data: SelectObjectType<T>[];
|
data: SelectObjectType<T>[];
|
||||||
groupBy?: (item: SelectObjectType<T>) => string;
|
groupBy?: (item: SelectObjectType<T>) => string;
|
||||||
filterBy?: (item: SelectObjectType<T>) => boolean;
|
filterBy?: (item: SelectObjectType<T>) => boolean;
|
||||||
};
|
};
|
||||||
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||||
|
|
||||||
return item.name;
|
return item.name;
|
||||||
}
|
};
|
||||||
|
|
||||||
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||||
if (!item) return item;
|
if (!item) return item;
|
||||||
return item.id.toString();
|
return item.id.toString();
|
||||||
}
|
};
|
||||||
export type ObjectSelectProps<T> =
|
export type ObjectSelectProps<T> = (RestProps<T> &
|
||||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
Partial<ControlledValueProps<T>>) &
|
||||||
& Omit<SelectProps, 'value' | 'onChange' | 'data'>
|
Omit<SelectProps, "value" | "onChange" | "data"> &
|
||||||
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>)
|
(T extends ObjectWithIdAndName
|
||||||
|
? Partial<CustomLabelAndKeyProps<T>>
|
||||||
|
: CustomLabelAndKeyProps<T>);
|
||||||
|
|
||||||
const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
const ObjectSelect = <T,>(props: ObjectSelectProps<T>) => {
|
||||||
|
const isControlled = "value" in props;
|
||||||
const isControlled = 'value' in props;
|
const haveGetValueFn = "getValueFn" in props;
|
||||||
const haveGetValueFn = 'getValueFn' in props;
|
const haveGetLabelFn = "getLabelFn" in props;
|
||||||
const haveGetLabelFn = 'getLabelFn' in props;
|
const [internalValue, setInternalValue] = useState<
|
||||||
const [internalValue, setInternalValue] = useState<SelectObjectType<T> | undefined>(props.defaultValue);
|
SelectObjectType<T> | undefined
|
||||||
|
>(props.defaultValue);
|
||||||
|
|
||||||
const value = isControlled ? props.value : internalValue;
|
const value = isControlled ? props.value : internalValue;
|
||||||
|
|
||||||
|
const getValueFn =
|
||||||
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||||
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
const getLabelFn =
|
||||||
|
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
|
const propsData = props.filterBy
|
||||||
|
? props.data.filter(props.filterBy)
|
||||||
|
: props.data;
|
||||||
if (props.groupBy) {
|
if (props.groupBy) {
|
||||||
|
|
||||||
const groupedData = groupBy(propsData, props.groupBy);
|
const groupedData = groupBy(propsData, props.groupBy);
|
||||||
return Object.entries(groupedData).map(([group, items]) => ({
|
return Object.entries(groupedData).map(([group, items]) => ({
|
||||||
group,
|
group,
|
||||||
items: items.map(item => ({
|
items: items.map(item => ({
|
||||||
label: getLabelFn(item),
|
label: getLabelFn(item),
|
||||||
value: getValueFn(item)
|
value: getValueFn(item),
|
||||||
}))
|
})),
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
return propsData.map(item => ({
|
return propsData.map(item => ({
|
||||||
label: getLabelFn(item),
|
label: getLabelFn(item),
|
||||||
value: getValueFn(item)
|
value: getValueFn(item),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [props.data, props.groupBy]);
|
}, [props.data, props.groupBy]);
|
||||||
@@ -81,14 +85,19 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInternalValue(object);
|
setInternalValue(object);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isControlled || !internalValue) return;
|
if (isControlled || !internalValue) return;
|
||||||
props.onChange(internalValue);
|
props.onChange(internalValue);
|
||||||
}, [internalValue]);
|
}, [internalValue]);
|
||||||
|
|
||||||
const restProps = omit(props, ['filterBy', 'groupBy', 'getValueFn', 'getLabelFn']);
|
const restProps = omit(props, [
|
||||||
|
"filterBy",
|
||||||
|
"groupBy",
|
||||||
|
"getValueFn",
|
||||||
|
"getLabelFn",
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
{...restProps}
|
{...restProps}
|
||||||
@@ -96,7 +105,7 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
|||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
data={data}
|
data={data}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ObjectSelect;
|
export default ObjectSelect;
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: var(--mantine-color-body);
|
background-color: var(--mantine-color-body);
|
||||||
box-shadow: 0 2px 4px var(--mantine-color-dark-7), 0 4px 24px var(--mantine-color-dark-7);
|
box-shadow:
|
||||||
|
0 2px 4px var(--mantine-color-dark-7),
|
||||||
|
0 4px 24px var(--mantine-color-dark-7);
|
||||||
}
|
}
|
||||||
@mixin light {
|
@mixin light {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .08), 0 4px 24px rgba(0, 0, 0, .08);
|
box-shadow:
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.08),
|
||||||
|
0 4px 24px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
padding: rem(15);
|
padding: rem(15);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
@@ -23,5 +26,4 @@
|
|||||||
|
|
||||||
.container-full-height-fixed {
|
.container-full-height-fixed {
|
||||||
height: calc(100vh - (rem(20) * 2));
|
height: calc(100vh - (rem(20) * 2));
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
import {CSSProperties, FC, ReactNode} from "react";
|
import { CSSProperties, FC, ReactNode } from "react";
|
||||||
import styles from './PageBlock.module.css';
|
import styles from "./PageBlock.module.css";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
fluid?: boolean;
|
fluid?: boolean;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
fullHeight?: boolean;
|
fullHeight?: boolean;
|
||||||
fullHeightFixed?: boolean;
|
fullHeightFixed?: boolean;
|
||||||
}
|
};
|
||||||
export const PageBlock: FC<Props> = ({children, style, fluid = true, fullHeight = false, fullHeightFixed = false}) => {
|
export const PageBlock: FC<Props> = ({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
fluid = true,
|
||||||
|
fullHeight = false,
|
||||||
|
fullHeightFixed = false,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div style={style}
|
<div
|
||||||
className={
|
style={style}
|
||||||
classNames(
|
className={classNames(
|
||||||
styles['container'],
|
styles["container"],
|
||||||
fluid && styles['container-fluid'],
|
fluid && styles["container-fluid"],
|
||||||
fullHeight && styles['container-full-height'],
|
fullHeight && styles["container-full-height"],
|
||||||
fullHeightFixed && styles['container-full-height-fixed']
|
fullHeightFixed && styles["container-full-height-fixed"]
|
||||||
)
|
)}>
|
||||||
}>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default PageBlock;
|
export default PageBlock;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.number-input {
|
.number-input {
|
||||||
width: rem(50);
|
width: rem(50);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import {ActionIcon, Flex, NumberInput, rem} from "@mantine/core";
|
import { ActionIcon, Flex, NumberInput, rem } from "@mantine/core";
|
||||||
import {IconMinus, IconPlus} from "@tabler/icons-react";
|
import { IconMinus, IconPlus } from "@tabler/icons-react";
|
||||||
import styles from './PlusMinusInput.module.css';
|
import styles from "./PlusMinusInput.module.css";
|
||||||
import {FC, useEffect, useState} from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
|
|
||||||
type ControlledValueProps = {
|
type ControlledValueProps = {
|
||||||
value: number;
|
value: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
type RestProps = {
|
type RestProps = {
|
||||||
defaultValue?: number;
|
defaultValue?: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Props = RestProps & Partial<ControlledValueProps>;
|
type Props = RestProps & Partial<ControlledValueProps>;
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
|||||||
const value = isControlled ? props.value : internalValue;
|
const value = isControlled ? props.value : internalValue;
|
||||||
|
|
||||||
const onMinusClick = () => {
|
const onMinusClick = () => {
|
||||||
|
|
||||||
const newValue = (value || 0) - 1;
|
const newValue = (value || 0) - 1;
|
||||||
if (newValue < 0) {
|
if (newValue < 0) {
|
||||||
return;
|
return;
|
||||||
@@ -32,7 +31,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
|||||||
} else {
|
} else {
|
||||||
setInternalValue(newValue);
|
setInternalValue(newValue);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const onPlusClick = () => {
|
const onPlusClick = () => {
|
||||||
const newValue = (value || 0) + 1;
|
const newValue = (value || 0) + 1;
|
||||||
@@ -41,7 +40,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
|||||||
} else {
|
} else {
|
||||||
setInternalValue(newValue);
|
setInternalValue(newValue);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleInputChange = (event: number | string) => {
|
const handleInputChange = (event: number | string) => {
|
||||||
let newValue = typeof event === "string" ? 0 : event;
|
let newValue = typeof event === "string" ? 0 : event;
|
||||||
@@ -53,7 +52,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
|||||||
} else {
|
} else {
|
||||||
setInternalValue(newValue);
|
setInternalValue(newValue);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isControlled) {
|
if (!isControlled) {
|
||||||
@@ -64,34 +63,33 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
align={"center"}
|
align={"center"}
|
||||||
gap={rem(10)}
|
gap={rem(10)}>
|
||||||
>
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
disabled={value === 0}
|
disabled={value === 0}
|
||||||
onClick={onMinusClick}
|
onClick={onMinusClick}
|
||||||
variant={"default"}>
|
variant={"default"}>
|
||||||
<IconMinus/>
|
<IconMinus />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
min={0}
|
min={0}
|
||||||
styles={{
|
styles={{
|
||||||
input: {
|
input: {
|
||||||
textAlign: "center"
|
textAlign: "center",
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
allowNegative={false}
|
allowNegative={false}
|
||||||
hideControls
|
hideControls
|
||||||
value={value}
|
value={value}
|
||||||
className={styles['number-input']}
|
className={styles["number-input"]}
|
||||||
onChange={(event) => handleInputChange(event)}
|
onChange={event => handleInputChange(event)}
|
||||||
/>
|
/>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={onPlusClick}
|
onClick={onPlusClick}
|
||||||
variant={"default"}>
|
variant={"default"}>
|
||||||
<IconPlus/>
|
<IconPlus />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PlusMinusInput;
|
export default PlusMinusInput;
|
||||||
|
|||||||
@@ -1,81 +1,113 @@
|
|||||||
import {ProductSchema} from "../../client";
|
import { ProductSchema } from "../../client";
|
||||||
import {FC, useState} from "react";
|
import { FC, useState } from "react";
|
||||||
import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx";
|
import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx";
|
||||||
import {omit} from "lodash";
|
import { omit } from "lodash";
|
||||||
import ObjectSelect, {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {ComboboxItem, Image, Loader, OptionsFilter, rem, SelectProps, Text, Tooltip} from "@mantine/core";
|
ObjectSelectProps,
|
||||||
import {getProductFields} from "../../types/utils.ts";
|
} from "../ObjectSelect/ObjectSelect.tsx";
|
||||||
import {useDebouncedValue} from "@mantine/hooks";
|
import {
|
||||||
|
ComboboxItem,
|
||||||
|
Image,
|
||||||
|
Loader,
|
||||||
|
OptionsFilter,
|
||||||
|
rem,
|
||||||
|
SelectProps,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { getProductFields } from "../../types/utils.ts";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
|
||||||
type RestProps = {
|
type RestProps = {
|
||||||
clientId: number;
|
clientId: number;
|
||||||
}
|
};
|
||||||
const MAX_PRODUCTS = 200;
|
const MAX_PRODUCTS = 200;
|
||||||
type Props = Omit<ObjectSelectProps<ProductSchema>, 'data'> & RestProps;
|
type Props = Omit<ObjectSelectProps<ProductSchema>, "data"> & RestProps;
|
||||||
const ProductSelect: FC<Props> = (props: Props) => {
|
const ProductSelect: FC<Props> = (props: Props) => {
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
const [debounced] = useDebouncedValue(searchValue, 500);
|
const [debounced] = useDebouncedValue(searchValue, 500);
|
||||||
const {products, isLoading} = useProductsList({
|
const { products, isLoading } = useProductsList({
|
||||||
clientId: props.clientId,
|
clientId: props.clientId,
|
||||||
searchInput: debounced,
|
searchInput: debounced,
|
||||||
page: 0,
|
page: 0,
|
||||||
itemsPerPage: MAX_PRODUCTS
|
itemsPerPage: MAX_PRODUCTS,
|
||||||
});
|
});
|
||||||
const restProps = omit(props, ['clientId']);
|
const restProps = omit(props, ["clientId"]);
|
||||||
const renderOption: SelectProps['renderOption'] = (item) => {
|
const renderOption: SelectProps["renderOption"] = item => {
|
||||||
const product = products.find(product => product.id == parseInt(item.option.value));
|
const product = products.find(
|
||||||
|
product => product.id == parseInt(item.option.value)
|
||||||
|
);
|
||||||
if (!product) return item.option.label;
|
if (!product) return item.option.label;
|
||||||
const productFields = getProductFields(product);
|
const productFields = getProductFields(product);
|
||||||
const imageUrl = product.images && product.images[0] ? product.images[0].imageUrl : undefined;
|
const imageUrl =
|
||||||
|
product.images && product.images[0]
|
||||||
|
? product.images[0].imageUrl
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
style={{whiteSpace: "pre-line"}}
|
style={{ whiteSpace: "pre-line" }}
|
||||||
multiline
|
multiline
|
||||||
disabled={productFields.length === 0}
|
disabled={productFields.length === 0}
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
{productFields.map(([key, value]) => {
|
{productFields
|
||||||
return `${key.toString()}: ${value.toString()}`;
|
.map(([key, value]) => {
|
||||||
}).join('\n')}
|
return `${key.toString()}: ${value.toString()}`;
|
||||||
{imageUrl && <Image
|
})
|
||||||
src={imageUrl}
|
.join("\n")}
|
||||||
alt={product.name}
|
{imageUrl && (
|
||||||
maw={rem(250)}
|
<Image
|
||||||
/>}
|
src={imageUrl}
|
||||||
|
alt={product.name}
|
||||||
|
maw={rem(250)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<div>
|
<div>
|
||||||
{product.name}<br/>
|
{product.name}
|
||||||
{product.barcodes && <Text size={"xs"}>{product.barcodes[0]}</Text>}
|
<br />
|
||||||
|
{product.barcodes && (
|
||||||
|
<Text size={"xs"}>{product.barcodes[0]}</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>)
|
</Tooltip>
|
||||||
}
|
|
||||||
const optionsFilter: OptionsFilter = ({options, search}) => {
|
|
||||||
return options;
|
|
||||||
const filtered = (options as ComboboxItem[]).filter((option) => {
|
|
||||||
const product = products.find(product => product.id == parseInt(option.value));
|
|
||||||
if (!product) return true;
|
|
||||||
return product.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
product.barcodes.some((value) => value.toLowerCase().includes(search.toLowerCase())) ||
|
|
||||||
product.article?.toLowerCase() === search.toLowerCase();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
const optionsFilter: OptionsFilter = ({ options, search }) => {
|
||||||
|
return options;
|
||||||
|
const filtered = (options as ComboboxItem[]).filter(option => {
|
||||||
|
const product = products.find(
|
||||||
|
product => product.id == parseInt(option.value)
|
||||||
|
);
|
||||||
|
if (!product) return true;
|
||||||
|
return (
|
||||||
|
product.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
product.barcodes.some(value =>
|
||||||
|
value.toLowerCase().includes(search.toLowerCase())
|
||||||
|
) ||
|
||||||
|
product.article?.toLowerCase() === search.toLowerCase()
|
||||||
|
);
|
||||||
|
});
|
||||||
filtered.sort((a, b) => a.label.localeCompare(b.label));
|
filtered.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
return filtered.length > MAX_PRODUCTS ? filtered.slice(0, MAX_PRODUCTS) : filtered;
|
return filtered.length > MAX_PRODUCTS
|
||||||
|
? filtered.slice(0, MAX_PRODUCTS)
|
||||||
|
: filtered;
|
||||||
};
|
};
|
||||||
const setSearchValueImpl = (value: string) => {
|
const setSearchValueImpl = (value: string) => {
|
||||||
const names = products.map(product => product.name);
|
const names = products.map(product => product.name);
|
||||||
if (names.includes(value)) return;
|
if (names.includes(value)) return;
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
// disabled={isLoading}
|
// disabled={isLoading}
|
||||||
rightSection={
|
rightSection={
|
||||||
(isLoading || searchValue !== debounced) ?
|
isLoading || searchValue !== debounced ? (
|
||||||
<Loader size={"sm"}/> : null
|
<Loader size={"sm"} />
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
onSearchChange={setSearchValueImpl}
|
onSearchChange={setSearchValueImpl}
|
||||||
renderOption={renderOption}
|
renderOption={renderOption}
|
||||||
@@ -84,8 +116,8 @@ const ProductSelect: FC<Props> = (props: Props) => {
|
|||||||
data={products}
|
data={products}
|
||||||
filter={optionsFilter}
|
filter={optionsFilter}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default ProductSelect;
|
export default ProductSelect;
|
||||||
// type ControlledValueProps = {
|
// type ControlledValueProps = {
|
||||||
// value: ProductSchema;
|
// value: ProductSchema;
|
||||||
@@ -140,4 +172,4 @@ export default ProductSelect;
|
|||||||
// />
|
// />
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
// export default ProductSelect;
|
// export default ProductSelect;
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {BarcodeTemplateSchema} from "../../../client";
|
ObjectSelectProps,
|
||||||
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { BarcodeTemplateSchema } from "../../../client";
|
||||||
import useGetAllBarcodeTemplates from "../../../api/barcode/useGetAllBarcodeTemplates.tsx";
|
import useGetAllBarcodeTemplates from "../../../api/barcode/useGetAllBarcodeTemplates.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<BarcodeTemplateSchema>, 'data'>;
|
type Props = Omit<ObjectSelectProps<BarcodeTemplateSchema>, "data">;
|
||||||
|
|
||||||
const BarcodeTemplateSelect = (props: Props) => {
|
const BarcodeTemplateSelect = (props: Props) => {
|
||||||
const {barcodeTemplates} = useGetAllBarcodeTemplates();
|
const { barcodeTemplates } = useGetAllBarcodeTemplates();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
data={barcodeTemplates}
|
data={barcodeTemplates}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default BarcodeTemplateSelect;
|
export default BarcodeTemplateSelect;
|
||||||
|
|||||||
@@ -1,45 +1,54 @@
|
|||||||
import {BaseEnumListSchema, type CancelablePromise} from "../../../client";
|
import { BaseEnumListSchema, type CancelablePromise } from "../../../client";
|
||||||
import {FC, useEffect, useMemo, useState} from "react";
|
import { FC, useEffect, useMemo, useState } from "react";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {Select, SelectProps} from "@mantine/core";
|
import { Select, SelectProps } from "@mantine/core";
|
||||||
import {omit} from "lodash";
|
import { omit } from "lodash";
|
||||||
|
|
||||||
type ControlledValueProps = {
|
type ControlledValueProps = {
|
||||||
value: number,
|
value: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
type RestProps = {
|
type RestProps = {
|
||||||
defaultValue?: number;
|
defaultValue?: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
fetchFn: () => CancelablePromise<BaseEnumListSchema>;
|
fetchFn: () => CancelablePromise<BaseEnumListSchema>;
|
||||||
queryKey: string;
|
queryKey: string;
|
||||||
}
|
};
|
||||||
export type BaseEnumSelectProps =
|
export type BaseEnumSelectProps = (RestProps & Partial<ControlledValueProps>) &
|
||||||
(RestProps & Partial<ControlledValueProps>)
|
Omit<SelectProps, "value" | "onChange" | "data" | "defaultValue">;
|
||||||
& Omit<SelectProps, 'value' | 'onChange' | 'data' | 'defaultValue'>;
|
|
||||||
|
|
||||||
export type EnumSelectProps = Omit<BaseEnumSelectProps, 'fetchFn' | 'queryKey'>;
|
export type EnumSelectProps = Omit<BaseEnumSelectProps, "fetchFn" | "queryKey">;
|
||||||
|
|
||||||
const BaseEnumSelect: FC<BaseEnumSelectProps> = (props: BaseEnumSelectProps) => {
|
const BaseEnumSelect: FC<BaseEnumSelectProps> = (
|
||||||
const {data: queryData = []} = useQuery({
|
props: BaseEnumSelectProps
|
||||||
|
) => {
|
||||||
|
const { data: queryData = [] } = useQuery({
|
||||||
queryKey: [props.queryKey],
|
queryKey: [props.queryKey],
|
||||||
queryFn: props.fetchFn,
|
queryFn: props.fetchFn,
|
||||||
select: data => data.items || []
|
select: data => data.items || [],
|
||||||
})
|
});
|
||||||
const isControlled = 'value' in props;
|
const isControlled = "value" in props;
|
||||||
const [internalValue, setInternalValue] = useState<number | undefined>(props.defaultValue);
|
const [internalValue, setInternalValue] = useState<number | undefined>(
|
||||||
|
props.defaultValue
|
||||||
|
);
|
||||||
const value = isControlled ? props.value : internalValue;
|
const value = isControlled ? props.value : internalValue;
|
||||||
const selectData = useMemo(() => queryData.reduce((acc, item) => {
|
const selectData = useMemo(
|
||||||
acc.push({
|
() =>
|
||||||
label: item.name,
|
queryData.reduce(
|
||||||
value: item.id.toString()
|
(acc, item) => {
|
||||||
});
|
acc.push({
|
||||||
return acc;
|
label: item.name,
|
||||||
}, [] as { label: string, value: string }[]), [queryData]);
|
value: item.id.toString(),
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as { label: string; value: string }[]
|
||||||
|
),
|
||||||
|
[queryData]
|
||||||
|
);
|
||||||
const handleOnChange = (event: string | null) => {
|
const handleOnChange = (event: string | null) => {
|
||||||
if (typeof event === 'undefined' || event === null) return;
|
if (typeof event === "undefined" || event === null) return;
|
||||||
const object = queryData.find(item => event == item.id.toString());
|
const object = queryData.find(item => event == item.id.toString());
|
||||||
if (!object) return;
|
if (!object) return;
|
||||||
if (isControlled) {
|
if (isControlled) {
|
||||||
@@ -47,22 +56,23 @@ const BaseEnumSelect: FC<BaseEnumSelectProps> = (props: BaseEnumSelectProps) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInternalValue(parseInt(event));
|
setInternalValue(parseInt(event));
|
||||||
}
|
};
|
||||||
const restProps = omit(props, ['fetchFn', 'queryKey'])
|
const restProps = omit(props, ["fetchFn", "queryKey"]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isControlled || typeof internalValue === 'undefined') return;
|
if (isControlled || typeof internalValue === "undefined") return;
|
||||||
props.onChange(internalValue);
|
props.onChange(internalValue);
|
||||||
}, [internalValue]);
|
}, [internalValue]);
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
|
||||||
{...restProps}
|
{...restProps}
|
||||||
defaultValue={props.defaultValue ? props.defaultValue.toString() : undefined}
|
defaultValue={
|
||||||
|
props.defaultValue ? props.defaultValue.toString() : undefined
|
||||||
|
}
|
||||||
value={value?.toString()}
|
value={value?.toString()}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
data={selectData}
|
data={selectData}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default BaseEnumSelect;
|
export default BaseEnumSelect;
|
||||||
|
|||||||
@@ -1,30 +1,42 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {BaseMarketplaceSchema} from "../../../client";
|
ObjectSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { BaseMarketplaceSchema } from "../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import useBaseMarketplacesList from "../../../hooks/useBaseMarketplacesList.tsx";
|
import useBaseMarketplacesList from "../../../hooks/useBaseMarketplacesList.tsx";
|
||||||
import {ActionIcon, Image} from "@mantine/core";
|
import { ActionIcon, Image } from "@mantine/core";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<BaseMarketplaceSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<BaseMarketplaceSchema>,
|
||||||
|
"data" | "getValueFn" | "getLabelFn"
|
||||||
|
>;
|
||||||
|
|
||||||
const BaseMarketplaceSelect: FC<Props> = (props) => {
|
const BaseMarketplaceSelect: FC<Props> = props => {
|
||||||
const {objects: baseMarketplaces} = useBaseMarketplacesList();
|
const { objects: baseMarketplaces } = useBaseMarketplacesList();
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
renderOption={(baseMarketplace) =>
|
renderOption={baseMarketplace => (
|
||||||
<>
|
<>
|
||||||
<ActionIcon radius={"md"} variant={"transparent"}>
|
<ActionIcon
|
||||||
|
radius={"md"}
|
||||||
|
variant={"transparent"}>
|
||||||
<Image
|
<Image
|
||||||
src={baseMarketplaces.find(el => baseMarketplace.option.value === el.key)?.iconUrl || ""}/>
|
src={
|
||||||
|
baseMarketplaces.find(
|
||||||
|
el =>
|
||||||
|
baseMarketplace.option.value === el.key
|
||||||
|
)?.iconUrl || ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
{baseMarketplace.option.label}
|
{baseMarketplace.option.label}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
}
|
getValueFn={baseMarketplace => baseMarketplace.key}
|
||||||
getValueFn={(baseMarketplace) => baseMarketplace.key}
|
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
|
||||||
data={baseMarketplaces}
|
data={baseMarketplaces}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default BaseMarketplaceSelect
|
export default BaseMarketplaceSelect;
|
||||||
|
|||||||
@@ -1,54 +1,63 @@
|
|||||||
import {useDebouncedValue} from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import {Autocomplete, AutocompleteProps, TextInputProps} from "@mantine/core";
|
import { Autocomplete, AutocompleteProps, TextInputProps } from "@mantine/core";
|
||||||
import {FC, useEffect, useState} from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import {Client} from "../../../types/Client.ts";
|
import { Client } from "../../../types/Client.ts";
|
||||||
import {ClientService} from "../../../client";
|
import { ClientService } from "../../../client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelect?: (client: Client) => void;
|
onSelect?: (client: Client) => void;
|
||||||
withAddress?: boolean;
|
withAddress?: boolean;
|
||||||
nameRestProps?: AutocompleteProps;
|
nameRestProps?: AutocompleteProps;
|
||||||
addressRestProps?: TextInputProps;
|
addressRestProps?: TextInputProps;
|
||||||
}
|
};
|
||||||
const ClientAutocomplete: FC<Props> = ({onSelect, addressRestProps, nameRestProps, withAddress = false}) => {
|
const ClientAutocomplete: FC<Props> = ({
|
||||||
const [value, setValue] = useState('');
|
onSelect,
|
||||||
|
addressRestProps,
|
||||||
|
nameRestProps,
|
||||||
|
withAddress = false,
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = useState("");
|
||||||
const [debouncedValue] = useDebouncedValue(value, 200);
|
const [debouncedValue] = useDebouncedValue(value, 200);
|
||||||
|
|
||||||
// const [isLoading, setIsLoading] = useState(false);
|
// const [isLoading, setIsLoading] = useState(false);
|
||||||
const [clients, setClients] = useState<Client[]>([])
|
const [clients, setClients] = useState<Client[]>([]);
|
||||||
const [selectedClient, selectClient] = useState<Client>();
|
const [selectedClient, selectClient] = useState<Client>();
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (value: string) => {
|
||||||
setClients([]);
|
setClients([]);
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}
|
};
|
||||||
const handleDebouncedChange = () => {
|
const handleDebouncedChange = () => {
|
||||||
if (!value.trim()) return;
|
if (!value.trim()) return;
|
||||||
// setIsLoading(true);
|
// setIsLoading(true);
|
||||||
ClientService.searchClients({name: value}).then(({clients}) => {
|
ClientService.searchClients({ name: value }).then(({ clients }) => {
|
||||||
setClients(clients);
|
setClients(clients);
|
||||||
// setIsLoading(false);
|
// setIsLoading(false);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleDebouncedChange();
|
handleDebouncedChange();
|
||||||
}, [debouncedValue]);
|
}, [debouncedValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectClient(clients.find(client =>
|
selectClient(
|
||||||
client.name.toLowerCase().trim() == value.toLowerCase().trim())
|
clients.find(
|
||||||
||
|
client =>
|
||||||
{
|
client.name.toLowerCase().trim() ==
|
||||||
|
value.toLowerCase().trim()
|
||||||
|
) || {
|
||||||
name: value,
|
name: value,
|
||||||
id: -1,
|
id: -1,
|
||||||
address: ""
|
address: "",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedClient) return;
|
if (!selectedClient) return;
|
||||||
if (onSelect) onSelect(selectedClient);
|
if (onSelect) onSelect(selectedClient);
|
||||||
if (nameRestProps?.onChange) nameRestProps.onChange(selectedClient.name);
|
if (nameRestProps?.onChange)
|
||||||
|
nameRestProps.onChange(selectedClient.name);
|
||||||
if (addressRestProps?.onChange) {
|
if (addressRestProps?.onChange) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@@ -59,25 +68,22 @@ const ClientAutocomplete: FC<Props> = ({onSelect, addressRestProps, nameRestProp
|
|||||||
<>
|
<>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...nameRestProps}
|
{...nameRestProps}
|
||||||
placeholder={'Клиент'}
|
placeholder={"Клиент"}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={value}
|
value={value}
|
||||||
data={clients.map(client => client.name)}
|
data={clients.map(client => client.name)}
|
||||||
styles={withAddress ? {
|
styles={
|
||||||
input: {
|
withAddress
|
||||||
borderBottomLeftRadius: 0,
|
? {
|
||||||
borderBottomRightRadius: 0
|
input: {
|
||||||
}
|
borderBottomLeftRadius: 0,
|
||||||
} : {}}
|
borderBottomRightRadius: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
export default ClientAutocomplete;
|
export default ClientAutocomplete;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import {Select} from "@mantine/core";
|
import { Select } from "@mantine/core";
|
||||||
import {ClientSchema} from "../../../client";
|
import { ClientSchema } from "../../../client";
|
||||||
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
|
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value?: ClientSchema;
|
value?: ClientSchema;
|
||||||
onChange: (client: ClientSchema) => void;
|
onChange: (client: ClientSchema) => void;
|
||||||
withLabel?: boolean;
|
withLabel?: boolean;
|
||||||
}
|
};
|
||||||
const ClientSelect: FC<Props> = ({value, onChange, withLabel = false}) => {
|
const ClientSelect: FC<Props> = ({ value, onChange, withLabel = false }) => {
|
||||||
const {clients} = useClientsList();
|
const { clients } = useClientsList();
|
||||||
const options = clients.map(client => ({label: client.name, value: client.id.toString()}))
|
const options = clients.map(client => ({
|
||||||
|
label: client.name,
|
||||||
|
value: client.id.toString(),
|
||||||
|
}));
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
searchable
|
searchable
|
||||||
placeholder={"Выберите клиента"}
|
placeholder={"Выберите клиента"}
|
||||||
value={value && options.find(client => client.value == value.id.toString())?.value}
|
value={
|
||||||
|
value &&
|
||||||
|
options.find(client => client.value == value.id.toString())
|
||||||
|
?.value
|
||||||
|
}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
const client = clients.find(client => client.id == parseInt(event));
|
const client = clients.find(
|
||||||
|
client => client.id == parseInt(event)
|
||||||
|
);
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
onChange(client);
|
onChange(client);
|
||||||
}}
|
}}
|
||||||
data={options}
|
data={options}
|
||||||
label={withLabel && "Клиент"}
|
label={withLabel && "Клиент"}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default ClientSelect;
|
export default ClientSelect;
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {ClientSchema} from "../../../client";
|
ObjectSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { ClientSchema } from "../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
|
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<ClientSchema>, 'data'>
|
type Props = Omit<ObjectSelectProps<ClientSchema>, "data">;
|
||||||
|
|
||||||
const ClientSelectNew: FC<Props> = (props) => {
|
const ClientSelectNew: FC<Props> = props => {
|
||||||
const {clients} = useClientsList();
|
const { clients } = useClientsList();
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
searchable
|
searchable
|
||||||
data={clients}
|
data={clients}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default ClientSelectNew;
|
export default ClientSelectNew;
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {PayRateSchema} from "../../../client";
|
ObjectSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { PayRateSchema } from "../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import usePayRatesList from "../../../pages/AdminPage/hooks/usePayRatesList.tsx";
|
import usePayRatesList from "../../../pages/AdminPage/hooks/usePayRatesList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<PayRateSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<PayRateSchema>,
|
||||||
|
"data" | "getValueFn" | "getLabelFn"
|
||||||
|
>;
|
||||||
|
|
||||||
const PayRateSelect: FC<Props> = (props) => {
|
const PayRateSelect: FC<Props> = props => {
|
||||||
const {objects: payRates} = usePayRatesList();
|
const { objects: payRates } = usePayRatesList();
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
getValueFn={(baseMarketplace) => baseMarketplace.id.toLocaleString()}
|
getValueFn={baseMarketplace => baseMarketplace.id.toLocaleString()}
|
||||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||||
data={payRates}
|
data={payRates}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default PayRateSelect;
|
export default PayRateSelect;
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {PayrollSchemeSchema} from "../../../client";
|
ObjectSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { PayrollSchemeSchema } from "../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import usePayrollSchemasList from "../../../hooks/usePayrollSchemasList.tsx";
|
import usePayrollSchemasList from "../../../hooks/usePayrollSchemasList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<PayrollSchemeSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<PayrollSchemeSchema>,
|
||||||
|
"data" | "getValueFn" | "getLabelFn"
|
||||||
|
>;
|
||||||
|
|
||||||
const PayrollSchemeSelect: FC<Props> = (props) => {
|
const PayrollSchemeSelect: FC<Props> = props => {
|
||||||
const {objects: payrollSchemeSchemas} = usePayrollSchemasList();
|
const { objects: payrollSchemeSchemas } = usePayrollSchemasList();
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
getValueFn={(baseMarketplace) => baseMarketplace.key}
|
getValueFn={baseMarketplace => baseMarketplace.key}
|
||||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||||
data={payrollSchemeSchemas}
|
data={payrollSchemeSchemas}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default PayrollSchemeSelect
|
export default PayrollSchemeSelect;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import ObjectSelect, { ObjectSelectProps } from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
|
ObjectSelectProps,
|
||||||
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
import { ServicePriceCategorySchema } from "../../../client";
|
import { ServicePriceCategorySchema } from "../../../client";
|
||||||
import useServicePriceCategoriesList from "../../../pages/ServicesPage/hooks/useServicePriceCategoriesList.tsx";
|
import useServicePriceCategoriesList from "../../../pages/ServicesPage/hooks/useServicePriceCategoriesList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">
|
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">;
|
||||||
|
|
||||||
const ServicePriceCategorySelect = (props: Props) => {
|
const ServicePriceCategorySelect = (props: Props) => {
|
||||||
const { objects } = useServicePriceCategoriesList();
|
const { objects } = useServicePriceCategoriesList();
|
||||||
@@ -14,4 +16,4 @@ const ServicePriceCategorySelect = (props: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ServicePriceCategorySelect;
|
export default ServicePriceCategorySelect;
|
||||||
|
|||||||
@@ -1,31 +1,36 @@
|
|||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {ServiceSchema} from "../../../client";
|
ObjectSelectProps,
|
||||||
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { ServiceSchema } from "../../../client";
|
||||||
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
|
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
|
||||||
import {omit} from "lodash";
|
import { omit } from "lodash";
|
||||||
import {ServiceType} from "../../../shared/enums/ServiceType.ts";
|
import { ServiceType } from "../../../shared/enums/ServiceType.ts";
|
||||||
import {ComboboxItem, OptionsFilter} from "@mantine/core";
|
import { ComboboxItem, OptionsFilter } from "@mantine/core";
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import {ComboboxParsedItemGroup} from "@mantine/core/lib/components/Combobox/Combobox.types";
|
import { ComboboxParsedItemGroup } from "@mantine/core/lib/components/Combobox/Combobox.types";
|
||||||
|
|
||||||
|
|
||||||
type RestProps = {
|
type RestProps = {
|
||||||
filterType?: ServiceType;
|
filterType?: ServiceType;
|
||||||
}
|
};
|
||||||
type Props = Omit<ObjectSelectProps<ServiceSchema>, 'data'> & RestProps;
|
type Props = Omit<ObjectSelectProps<ServiceSchema>, "data"> & RestProps;
|
||||||
const ServiceSelectNew: FC<Props> = (props: Props) => {
|
const ServiceSelectNew: FC<Props> = (props: Props) => {
|
||||||
const {services} = useServicesList();
|
const { services } = useServicesList();
|
||||||
const data = props.filterType ? services.filter(service => service.serviceType === props.filterType) : services;
|
const data = props.filterType
|
||||||
|
? services.filter(service => service.serviceType === props.filterType)
|
||||||
|
: services;
|
||||||
|
|
||||||
const restProps = omit(props, ['filterType']);
|
const restProps = omit(props, ["filterType"]);
|
||||||
const optionsFilter: OptionsFilter = ({options, search}) => {
|
const optionsFilter: OptionsFilter = ({ options, search }) => {
|
||||||
return (options as ComboboxParsedItemGroup<ComboboxItem>[]).map((option) => {
|
return (options as ComboboxParsedItemGroup<ComboboxItem>[]).map(
|
||||||
|
option => {
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
items:
|
items: option.items.filter((item: ComboboxItem) =>
|
||||||
option.items.filter((item: ComboboxItem) => item.label.toLowerCase().includes(search.toLowerCase()))
|
item.label.toLowerCase().includes(search.toLowerCase())
|
||||||
}
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -37,6 +42,6 @@ const ServiceSelectNew: FC<Props> = (props: Props) => {
|
|||||||
groupBy={item => item.category.name}
|
groupBy={item => item.category.name}
|
||||||
filter={optionsFilter}
|
filter={optionsFilter}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default ServiceSelectNew;
|
export default ServiceSelectNew;
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {GetServiceKitSchema} from "../../../client";
|
ObjectSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { GetServiceKitSchema } from "../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import useServicesKitsList from "../../../pages/ServicesPage/hooks/useServicesKitsList.tsx";
|
import useServicesKitsList from "../../../pages/ServicesPage/hooks/useServicesKitsList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<GetServiceKitSchema>, 'data'>
|
type Props = Omit<ObjectSelectProps<GetServiceKitSchema>, "data">;
|
||||||
const ServicesKitSelect: FC<Props> = (props) => {
|
const ServicesKitSelect: FC<Props> = props => {
|
||||||
const {objects} = useServicesKitsList();
|
const { objects } = useServicesKitsList();
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
data={objects}
|
data={objects}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ServicesKitSelect;
|
export default ServicesKitSelect;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import ObjectMultiSelect, {ObjectMultiSelectProps} from "../../ObjectMultiSelect/ObjectMultiSelect.tsx";
|
import ObjectMultiSelect, {
|
||||||
import {ServiceSchema} from "../../../client";
|
ObjectMultiSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../ObjectMultiSelect/ObjectMultiSelect.tsx";
|
||||||
|
import { ServiceSchema } from "../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
|
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectMultiSelectProps<ServiceSchema>, 'data'>
|
type Props = Omit<ObjectMultiSelectProps<ServiceSchema>, "data">;
|
||||||
const ServicesMultiselect: FC<Props> = (props: Props) => {
|
const ServicesMultiselect: FC<Props> = (props: Props) => {
|
||||||
const {services} = useServicesList();
|
const { services } = useServicesList();
|
||||||
return (
|
return (
|
||||||
<ObjectMultiSelect
|
<ObjectMultiSelect
|
||||||
data={services}
|
data={services}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default ServicesMultiselect;
|
export default ServicesMultiselect;
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import ObjectAutocomplete, {ObjectAutocompleteProps} from "../../ObjectAutocomplete/ObjectAutocomplete.tsx";
|
import ObjectAutocomplete, {
|
||||||
|
ObjectAutocompleteProps,
|
||||||
|
} from "../../ObjectAutocomplete/ObjectAutocomplete.tsx";
|
||||||
import useShippingWarehousesList from "./hooks/useShippingWarehousesList.tsx";
|
import useShippingWarehousesList from "./hooks/useShippingWarehousesList.tsx";
|
||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import {ShippingWarehouseSchema} from "../../../client";
|
import { ShippingWarehouseSchema } from "../../../client";
|
||||||
|
|
||||||
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, 'data'>;
|
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, "data">;
|
||||||
const ShippingWarehouseAutocomplete: FC<Props> = (props) => {
|
const ShippingWarehouseAutocomplete: FC<Props> = props => {
|
||||||
const {shippingWarehouses} = useShippingWarehousesList();
|
const { shippingWarehouses } = useShippingWarehousesList();
|
||||||
return (
|
return (
|
||||||
<ObjectAutocomplete
|
<ObjectAutocomplete
|
||||||
{...props}
|
{...props}
|
||||||
data={shippingWarehouses}
|
data={shippingWarehouses}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ShippingWarehouseAutocomplete;
|
export default ShippingWarehouseAutocomplete;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {ShippingWarehouseService} from "../../../../client";
|
import { ShippingWarehouseService } from "../../../../client";
|
||||||
|
|
||||||
const useShippingWarehousesList = () => {
|
const useShippingWarehousesList = () => {
|
||||||
const {isPending, error, data, refetch} = useQuery({
|
const { isPending, error, data, refetch } = useQuery({
|
||||||
queryKey: ['getAllShippingWarehouses'],
|
queryKey: ["getAllShippingWarehouses"],
|
||||||
queryFn: ShippingWarehouseService.getAllShippingWarehouses
|
queryFn: ShippingWarehouseService.getAllShippingWarehouses,
|
||||||
});
|
});
|
||||||
const shippingWarehouses = isPending || error || !data ? [] : data.shippingWarehouses;
|
const shippingWarehouses =
|
||||||
|
isPending || error || !data ? [] : data.shippingWarehouses;
|
||||||
|
|
||||||
return {shippingWarehouses, refetch}
|
return { shippingWarehouses, refetch };
|
||||||
}
|
};
|
||||||
export default useShippingWarehousesList;
|
export default useShippingWarehousesList;
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {UserSchema} from "../../../client";
|
ObjectSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { UserSchema } from "../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import useUsersList from "../../../pages/AdminPage/hooks/useUsersList.tsx";
|
import useUsersList from "../../../pages/AdminPage/hooks/useUsersList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<UserSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
type Props = Omit<
|
||||||
const UserSelect: FC<Props> = (props) => {
|
ObjectSelectProps<UserSchema>,
|
||||||
const {objects: users} = useUsersList();
|
"data" | "getValueFn" | "getLabelFn"
|
||||||
return (<ObjectSelect
|
>;
|
||||||
data={users}
|
const UserSelect: FC<Props> = props => {
|
||||||
getLabelFn={(user) => `${user.firstName} ${user.secondName}`}
|
const { objects: users } = useUsersList();
|
||||||
getValueFn={(user) => user.id.toString()}
|
return (
|
||||||
{...props}
|
<ObjectSelect
|
||||||
/>)
|
data={users}
|
||||||
}
|
getLabelFn={user => `${user.firstName} ${user.secondName}`}
|
||||||
export default UserSelect;
|
getValueFn={user => user.id.toString()}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default UserSelect;
|
||||||
|
|||||||
@@ -1,48 +1,66 @@
|
|||||||
import {ServiceSchema} from "../../client";
|
import { ServiceSchema } from "../../client";
|
||||||
import {FC, useEffect, useMemo, useState} from "react";
|
import { FC, useEffect, useMemo, useState } from "react";
|
||||||
import {Select, SelectProps} from "@mantine/core";
|
import { Select, SelectProps } from "@mantine/core";
|
||||||
import useServicesList from "../../pages/ServicesPage/hooks/useServicesList.tsx";
|
import useServicesList from "../../pages/ServicesPage/hooks/useServicesList.tsx";
|
||||||
|
|
||||||
type ControlledValueProps = {
|
type ControlledValueProps = {
|
||||||
value: ServiceSchema;
|
value: ServiceSchema;
|
||||||
onChange: (value: ServiceSchema) => void;
|
onChange: (value: ServiceSchema) => void;
|
||||||
}
|
};
|
||||||
type RestProps = {
|
type RestProps = {
|
||||||
defaultValue?: ServiceSchema;
|
defaultValue?: ServiceSchema;
|
||||||
onChange: (value: ServiceSchema) => void;
|
onChange: (value: ServiceSchema) => void;
|
||||||
}
|
};
|
||||||
type Props = (RestProps & Partial<ControlledValueProps>) & Omit<SelectProps, 'value' | 'onChange'>;
|
type Props = (RestProps & Partial<ControlledValueProps>) &
|
||||||
|
Omit<SelectProps, "value" | "onChange">;
|
||||||
|
|
||||||
|
const ServiceSelect: FC<Props> = props => {
|
||||||
const ServiceSelect: FC<Props> = (props) => {
|
|
||||||
const isControlled = props.value !== undefined;
|
const isControlled = props.value !== undefined;
|
||||||
const [internalValue, setInternalValue] = useState<ServiceSchema | undefined>(props.defaultValue);
|
const [internalValue, setInternalValue] = useState<
|
||||||
|
ServiceSchema | undefined
|
||||||
|
>(props.defaultValue);
|
||||||
const value = isControlled ? props.value : internalValue;
|
const value = isControlled ? props.value : internalValue;
|
||||||
const {services} = useServicesList();
|
const { services } = useServicesList();
|
||||||
const categories = useMemo(() => services.reduce((acc, service) => {
|
const categories = useMemo(
|
||||||
if (!acc.includes(service.category.name)) {
|
() =>
|
||||||
acc.push(service.category.name);
|
services.reduce((acc, service) => {
|
||||||
}
|
if (!acc.includes(service.category.name)) {
|
||||||
return acc;
|
acc.push(service.category.name);
|
||||||
}, [] as string[]), [services]);
|
}
|
||||||
|
return acc;
|
||||||
const data = useMemo(() => categories.map(category => ({
|
}, [] as string[]),
|
||||||
group: category,
|
[services]
|
||||||
items: services.filter(service => service.category.name === category)
|
);
|
||||||
.map(service => ({
|
|
||||||
value: service.id.toString(),
|
|
||||||
label: service.name
|
|
||||||
}))
|
|
||||||
})), [services, categories]);
|
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() =>
|
||||||
|
categories.map(category => ({
|
||||||
|
group: category,
|
||||||
|
items: services
|
||||||
|
.filter(service => service.category.name === category)
|
||||||
|
.map(service => ({
|
||||||
|
value: service.id.toString(),
|
||||||
|
label: service.name,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
[services, categories]
|
||||||
|
);
|
||||||
|
|
||||||
const handleOnChange = (value: string) => {
|
const handleOnChange = (value: string) => {
|
||||||
if (isControlled) {
|
if (isControlled) {
|
||||||
props.onChange(services.find(service => service.id.toString() === value) as ServiceSchema);
|
props.onChange(
|
||||||
|
services.find(
|
||||||
|
service => service.id.toString() === value
|
||||||
|
) as ServiceSchema
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInternalValue(services.find(service => service.id.toString() === value) as ServiceSchema);
|
setInternalValue(
|
||||||
}
|
services.find(
|
||||||
|
service => service.id.toString() === value
|
||||||
|
) as ServiceSchema
|
||||||
|
);
|
||||||
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isControlled) {
|
if (!isControlled) {
|
||||||
props.onChange(internalValue as ServiceSchema);
|
props.onChange(internalValue as ServiceSchema);
|
||||||
@@ -57,7 +75,7 @@ const ServiceSelect: FC<Props> = (props) => {
|
|||||||
onChange={event => event && handleOnChange(event)}
|
onChange={event => event && handleOnChange(event)}
|
||||||
data={data}
|
data={data}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ServiceSelect;
|
export default ServiceSelect;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx";
|
import { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx";
|
||||||
import { ServicePriceCategorySchema, ServiceSchema } from "../../client";
|
import { ServicePriceCategorySchema, ServiceSchema } from "../../client";
|
||||||
import { Flex, FlexProps, NumberInput, NumberInputProps, rem } from "@mantine/core";
|
import {
|
||||||
|
Flex,
|
||||||
|
FlexProps,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputProps,
|
||||||
|
rem,
|
||||||
|
} from "@mantine/core";
|
||||||
import { FC, useEffect, useRef, useState } from "react";
|
import { FC, useEffect, useRef, useState } from "react";
|
||||||
import ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx";
|
import ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx";
|
||||||
import { ServiceType } from "../../shared/enums/ServiceType.ts";
|
import { ServiceType } from "../../shared/enums/ServiceType.ts";
|
||||||
@@ -9,38 +15,47 @@ type ServiceProps = Omit<ObjectSelectProps<ServiceSchema>, "data">;
|
|||||||
type PriceProps = NumberInputProps;
|
type PriceProps = NumberInputProps;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
serviceProps: ServiceProps,
|
serviceProps: ServiceProps;
|
||||||
priceProps: PriceProps,
|
priceProps: PriceProps;
|
||||||
quantity: number,
|
quantity: number;
|
||||||
containerProps: FlexProps,
|
containerProps: FlexProps;
|
||||||
filterType?: ServiceType,
|
filterType?: ServiceType;
|
||||||
lockOnEdit?: boolean
|
lockOnEdit?: boolean;
|
||||||
category?: ServicePriceCategorySchema
|
category?: ServicePriceCategorySchema;
|
||||||
}
|
};
|
||||||
const ServiceWithPriceInput: FC<Props> = ({
|
const ServiceWithPriceInput: FC<Props> = ({
|
||||||
serviceProps,
|
serviceProps,
|
||||||
priceProps,
|
priceProps,
|
||||||
quantity,
|
quantity,
|
||||||
containerProps,
|
containerProps,
|
||||||
filterType = ServiceType.PRODUCT_SERVICE,
|
filterType = ServiceType.PRODUCT_SERVICE,
|
||||||
lockOnEdit = true,
|
lockOnEdit = true,
|
||||||
category,
|
category,
|
||||||
}) => {
|
}) => {
|
||||||
const [price, setPrice] = useState<number | undefined>(
|
const [price, setPrice] = useState<number | undefined>(
|
||||||
typeof priceProps.value === "number" ? priceProps.value : undefined);
|
typeof priceProps.value === "number" ? priceProps.value : undefined
|
||||||
const [service, setService] = useState<ServiceSchema | undefined>(serviceProps.value);
|
);
|
||||||
|
const [service, setService] = useState<ServiceSchema | undefined>(
|
||||||
|
serviceProps.value
|
||||||
|
);
|
||||||
const isFirstRender = useRef(true);
|
const isFirstRender = useRef(true);
|
||||||
const setPriceBasedOnQuantity = (): boolean => {
|
const setPriceBasedOnQuantity = (): boolean => {
|
||||||
if (!service || !service.priceRanges.length) return false;
|
if (!service || !service.priceRanges.length) return false;
|
||||||
const range = service.priceRanges.find(priceRange =>
|
const range =
|
||||||
quantity >= priceRange.fromQuantity && quantity <= priceRange.toQuantity) || service.priceRanges[0];
|
service.priceRanges.find(
|
||||||
|
priceRange =>
|
||||||
|
quantity >= priceRange.fromQuantity &&
|
||||||
|
quantity <= priceRange.toQuantity
|
||||||
|
) || service.priceRanges[0];
|
||||||
|
|
||||||
setPrice(range.price);
|
setPrice(range.price);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const setPriceBasedOnCategory = () => {
|
const setPriceBasedOnCategory = () => {
|
||||||
if (!category || !service) return false;
|
if (!category || !service) return false;
|
||||||
const categoryPrice = service.categoryPrices.find(categoryPrice => categoryPrice.category.id === category.id);
|
const categoryPrice = service.categoryPrices.find(
|
||||||
|
categoryPrice => categoryPrice.category.id === category.id
|
||||||
|
);
|
||||||
if (!categoryPrice) return false;
|
if (!categoryPrice) return false;
|
||||||
setPrice(categoryPrice.price);
|
setPrice(categoryPrice.price);
|
||||||
return true;
|
return true;
|
||||||
@@ -84,14 +99,11 @@ const ServiceWithPriceInput: FC<Props> = ({
|
|||||||
isFirstRender.current = false;
|
isFirstRender.current = false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
align={"center"}
|
align={"center"}
|
||||||
gap={rem(10)}
|
gap={rem(10)}
|
||||||
{...containerProps}
|
{...containerProps}>
|
||||||
>
|
|
||||||
|
|
||||||
{/*<ActionIcon variant={"default"}>*/}
|
{/*<ActionIcon variant={"default"}>*/}
|
||||||
{/*<IconReload onClick={() => onReload()}/>*/}
|
{/*<IconReload onClick={() => onReload()}/>*/}
|
||||||
{/*</ActionIcon>*/}
|
{/*</ActionIcon>*/}
|
||||||
@@ -108,9 +120,8 @@ const ServiceWithPriceInput: FC<Props> = ({
|
|||||||
// value={price}
|
// value={price}
|
||||||
defaultValue={priceProps.value}
|
defaultValue={priceProps.value}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ServiceWithPriceInput;
|
export default ServiceWithPriceInput;
|
||||||
|
|||||||
@@ -1,90 +1,93 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types";
|
||||||
import React, { useRef, useEffect } from 'react'
|
import React, { useRef, useEffect } from "react";
|
||||||
|
|
||||||
export interface TelegramUser {
|
export interface TelegramUser {
|
||||||
id: number
|
id: number;
|
||||||
first_name: string
|
first_name: string;
|
||||||
username: string
|
username: string;
|
||||||
photo_url: string
|
photo_url: string;
|
||||||
auth_date: number
|
auth_date: number;
|
||||||
hash: string
|
hash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
botName: string
|
botName: string;
|
||||||
usePic?: boolean
|
usePic?: boolean;
|
||||||
className?: string
|
className?: string;
|
||||||
cornerRadius?: number
|
cornerRadius?: number;
|
||||||
requestAccess?: boolean
|
requestAccess?: boolean;
|
||||||
dataAuthUrl?: string
|
dataAuthUrl?: string;
|
||||||
dataOnauth?: (user: TelegramUser) => void
|
dataOnauth?: (user: TelegramUser) => void;
|
||||||
buttonSize?: 'large' | 'medium' | 'small'
|
buttonSize?: "large" | "medium" | "small";
|
||||||
wrapperProps?: React.HTMLProps<HTMLDivElement>
|
wrapperProps?: React.HTMLProps<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
TelegramLoginWidget: {
|
TelegramLoginWidget: {
|
||||||
dataOnauth: (user: TelegramUser) => void
|
dataOnauth: (user: TelegramUser) => void;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TelegramLoginButton: React.FC<Props> = ({
|
const TelegramLoginButton: React.FC<Props> = ({
|
||||||
wrapperProps,
|
wrapperProps,
|
||||||
dataAuthUrl,
|
dataAuthUrl,
|
||||||
usePic = false,
|
usePic = false,
|
||||||
botName,
|
botName,
|
||||||
className,
|
className,
|
||||||
buttonSize = 'large',
|
buttonSize = "large",
|
||||||
dataOnauth,
|
dataOnauth,
|
||||||
cornerRadius,
|
cornerRadius,
|
||||||
requestAccess = true
|
requestAccess = true,
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current === null) return
|
if (ref.current === null) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof dataOnauth === 'undefined' &&
|
typeof dataOnauth === "undefined" &&
|
||||||
typeof dataAuthUrl === 'undefined'
|
typeof dataAuthUrl === "undefined"
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'One of this props should be defined: dataAuthUrl (redirect URL), dataOnauth (callback fn) should be defined.'
|
"One of this props should be defined: dataAuthUrl (redirect URL), dataOnauth (callback fn) should be defined."
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof dataOnauth === 'function') {
|
if (typeof dataOnauth === "function") {
|
||||||
window.TelegramLoginWidget = {
|
window.TelegramLoginWidget = {
|
||||||
dataOnauth: (user: TelegramUser) => dataOnauth(user)
|
dataOnauth: (user: TelegramUser) => dataOnauth(user),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = document.createElement('script')
|
const script = document.createElement("script");
|
||||||
script.src = 'https://telegram.org/js/telegram-widget.js?22'
|
script.src = "https://telegram.org/js/telegram-widget.js?22";
|
||||||
script.setAttribute('data-telegram-login', botName)
|
script.setAttribute("data-telegram-login", botName);
|
||||||
script.setAttribute('data-size', buttonSize)
|
script.setAttribute("data-size", buttonSize);
|
||||||
|
|
||||||
if (cornerRadius !== undefined) {
|
if (cornerRadius !== undefined) {
|
||||||
script.setAttribute('data-radius', cornerRadius.toString())
|
script.setAttribute("data-radius", cornerRadius.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestAccess) {
|
if (requestAccess) {
|
||||||
script.setAttribute('data-request-access', 'write')
|
script.setAttribute("data-request-access", "write");
|
||||||
}
|
}
|
||||||
|
|
||||||
script.setAttribute('data-userpic', usePic.toString())
|
script.setAttribute("data-userpic", usePic.toString());
|
||||||
|
|
||||||
if (typeof dataAuthUrl === 'string') {
|
if (typeof dataAuthUrl === "string") {
|
||||||
script.setAttribute('data-auth-url', dataAuthUrl)
|
script.setAttribute("data-auth-url", dataAuthUrl);
|
||||||
} else {
|
} else {
|
||||||
script.setAttribute('data-onauth', 'TelegramLoginWidget.dataOnauth(user)')
|
script.setAttribute(
|
||||||
|
"data-onauth",
|
||||||
|
"TelegramLoginWidget.dataOnauth(user)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
script.async = true
|
script.async = true;
|
||||||
|
|
||||||
ref.current.appendChild(script)
|
ref.current.appendChild(script);
|
||||||
}, [
|
}, [
|
||||||
botName,
|
botName,
|
||||||
buttonSize,
|
buttonSize,
|
||||||
@@ -93,11 +96,17 @@ const TelegramLoginButton: React.FC<Props> = ({
|
|||||||
requestAccess,
|
requestAccess,
|
||||||
usePic,
|
usePic,
|
||||||
ref,
|
ref,
|
||||||
dataAuthUrl
|
dataAuthUrl,
|
||||||
])
|
]);
|
||||||
|
|
||||||
return <div ref={ref} className={className} {...wrapperProps} />
|
return (
|
||||||
}
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={className}
|
||||||
|
{...wrapperProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
TelegramLoginButton.propTypes = {
|
TelegramLoginButton.propTypes = {
|
||||||
botName: PropTypes.string.isRequired,
|
botName: PropTypes.string.isRequired,
|
||||||
@@ -108,7 +117,7 @@ TelegramLoginButton.propTypes = {
|
|||||||
wrapperProps: PropTypes.object,
|
wrapperProps: PropTypes.object,
|
||||||
dataOnauth: PropTypes.func,
|
dataOnauth: PropTypes.func,
|
||||||
dataAuthUrl: PropTypes.string,
|
dataAuthUrl: PropTypes.string,
|
||||||
buttonSize: PropTypes.oneOf(['large', 'medium', 'small'])
|
buttonSize: PropTypes.oneOf(["large", "medium", "small"]),
|
||||||
}
|
};
|
||||||
|
|
||||||
export default TelegramLoginButton
|
export default TelegramLoginButton;
|
||||||
|
|||||||
@@ -1,59 +1,75 @@
|
|||||||
import cx from 'clsx';
|
import cx from "clsx";
|
||||||
import {Text} from '@mantine/core';
|
import { Text } from "@mantine/core";
|
||||||
import {useListState} from '@mantine/hooks';
|
import { useListState } from "@mantine/hooks";
|
||||||
import {DragDropContext, Droppable, Draggable} from '@hello-pangea/dnd';
|
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||||
import classes from './DndList.module.css';
|
import classes from "./DndList.module.css";
|
||||||
|
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{position: 6, mass: 12.011, symbol: 'C', name: 'Carbon'},
|
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
||||||
{position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen'},
|
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
||||||
{position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium'},
|
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
||||||
{position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium'},
|
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
||||||
{position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium'},
|
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DndList() {
|
export function DndList() {
|
||||||
const [state, handlers] = useListState(data);
|
const [state, handlers] = useListState(data);
|
||||||
|
|
||||||
const items = (listIndex: number) => state.map((item, index) => (
|
const items = (listIndex: number) =>
|
||||||
<Draggable key={item.symbol + `${listIndex}`} index={index} draggableId={item.symbol + `${listIndex}`}>
|
state.map((item, index) => (
|
||||||
{(provided, snapshot) => (
|
<Draggable
|
||||||
<div
|
key={item.symbol + `${listIndex}`}
|
||||||
className={cx(classes.item, {[classes.itemDragging]: snapshot.isDragging})}
|
index={index}
|
||||||
{...provided.draggableProps}
|
draggableId={item.symbol + `${listIndex}`}>
|
||||||
{...provided.dragHandleProps}
|
{(provided, snapshot) => (
|
||||||
ref={provided.innerRef}
|
<div
|
||||||
>
|
className={cx(classes.item, {
|
||||||
<Text className={classes.symbol}>{item.symbol}</Text>
|
[classes.itemDragging]: snapshot.isDragging,
|
||||||
<div>
|
})}
|
||||||
<Text>{item.name}</Text>
|
{...provided.draggableProps}
|
||||||
<Text c="dimmed" size="sm">
|
{...provided.dragHandleProps}
|
||||||
Position: {item.position} • Mass: {item.mass}
|
ref={provided.innerRef}>
|
||||||
</Text>
|
<Text className={classes.symbol}>{item.symbol}</Text>
|
||||||
|
<div>
|
||||||
|
<Text>{item.name}</Text>
|
||||||
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
size="sm">
|
||||||
|
Position: {item.position} • Mass: {item.mass}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Draggable>
|
||||||
</Draggable>
|
));
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
onDragEnd={({destination, source}) =>
|
onDragEnd={({ destination, source }) =>
|
||||||
handlers.reorder({from: source.index, to: destination?.index || 0})
|
handlers.reorder({
|
||||||
}
|
from: source.index,
|
||||||
>
|
to: destination?.index || 0,
|
||||||
<Droppable droppableId="dnd-list" direction="vertical">
|
})
|
||||||
{(provided) => (
|
}>
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
<Droppable
|
||||||
|
droppableId="dnd-list"
|
||||||
|
direction="vertical">
|
||||||
|
{provided => (
|
||||||
|
<div
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}>
|
||||||
{items(1)}
|
{items(1)}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
<Droppable droppableId="dnd-list-2" direction="vertical">
|
<Droppable
|
||||||
{(provided) => (
|
droppableId="dnd-list-2"
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
direction="vertical">
|
||||||
|
{provided => (
|
||||||
|
<div
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}>
|
||||||
{items(2)}
|
{items(2)}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
@@ -61,4 +77,4 @@ export function DndList() {
|
|||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import {jwtDecode, JwtPayload} from "jwt-decode";
|
import { jwtDecode, JwtPayload } from "jwt-decode";
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
isAuthorized: boolean;
|
isAuthorized: boolean;
|
||||||
@@ -15,34 +15,35 @@ const initialState = (): AuthState => {
|
|||||||
return {
|
return {
|
||||||
accessToken: "",
|
accessToken: "",
|
||||||
isAuthorized: false,
|
isAuthorized: false,
|
||||||
isGuest: false
|
isGuest: false,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const authSlice = createSlice({
|
const authSlice = createSlice({
|
||||||
name: 'auth',
|
name: "auth",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
login: (state, action: PayloadAction<{ accessToken: string }>) => {
|
login: (state, action: PayloadAction<{ accessToken: string }>) => {
|
||||||
try {
|
try {
|
||||||
const {sub} = jwtDecode<JwtPayload>(action.payload.accessToken);
|
const { sub } = jwtDecode<JwtPayload>(
|
||||||
|
action.payload.accessToken
|
||||||
|
);
|
||||||
state.accessToken = action.payload.accessToken;
|
state.accessToken = action.payload.accessToken;
|
||||||
state.isAuthorized = true;
|
state.isAuthorized = true;
|
||||||
if (sub === "guest")
|
if (sub === "guest") state.isGuest = true;
|
||||||
state.isGuest = true;
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
urlObj.search = '';
|
urlObj.search = "";
|
||||||
history.replaceState(null, '', urlObj);
|
history.replaceState(null, "", urlObj);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logout: (state) => {
|
logout: state => {
|
||||||
state.isAuthorized = false;
|
state.isAuthorized = false;
|
||||||
state.accessToken = '';
|
state.accessToken = "";
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
export const {login, logout} = authSlice.actions;
|
export const { login, logout } = authSlice.actions;
|
||||||
export default authSlice.reducer;
|
export default authSlice.reducer;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import {notifications} from "../shared/lib/notifications.ts";
|
import { notifications } from "../shared/lib/notifications.ts";
|
||||||
import {IconCheck, IconX} from "@tabler/icons-react";
|
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||||
import {rem} from "@mantine/core";
|
import { rem } from "@mantine/core";
|
||||||
|
|
||||||
export type TaskData = {
|
export type TaskData = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -43,13 +43,15 @@ const tasksSlice = createSlice({
|
|||||||
autoClose: false,
|
autoClose: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
withBorder: true,
|
withBorder: true,
|
||||||
radius: "sm"
|
radius: "sm",
|
||||||
});
|
});
|
||||||
state.tasks.push(task);
|
state.tasks.push(task);
|
||||||
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
||||||
},
|
},
|
||||||
removeTask: (state, action: PayloadAction<string>) => {
|
removeTask: (state, action: PayloadAction<string>) => {
|
||||||
state.tasks = state.tasks.filter((task) => task.id !== action.payload);
|
state.tasks = state.tasks.filter(
|
||||||
|
task => task.id !== action.payload
|
||||||
|
);
|
||||||
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
||||||
},
|
},
|
||||||
failTask: (state, action: PayloadAction<Task>) => {
|
failTask: (state, action: PayloadAction<Task>) => {
|
||||||
@@ -59,16 +61,20 @@ const tasksSlice = createSlice({
|
|||||||
if (!notificationId) return;
|
if (!notificationId) return;
|
||||||
notifications.update({
|
notifications.update({
|
||||||
id: notificationId,
|
id: notificationId,
|
||||||
color: 'red',
|
color: "red",
|
||||||
title: task.config.onErrorData.title,
|
title: task.config.onErrorData.title,
|
||||||
message: task.config.onErrorData.message,
|
message: task.config.onErrorData.message,
|
||||||
icon: <IconX style={{width: rem(18), height: rem(18)}}/>,
|
icon: <IconX style={{ width: rem(18), height: rem(18) }} />,
|
||||||
loading: false,
|
loading: false,
|
||||||
autoClose: 2000,
|
autoClose: 2000,
|
||||||
})
|
});
|
||||||
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id);
|
state.tasks = state.tasks.filter(
|
||||||
|
task => task.id !== action.payload.id
|
||||||
|
);
|
||||||
state.notificationTaskMap = Object.fromEntries(
|
state.notificationTaskMap = Object.fromEntries(
|
||||||
Object.entries(state.notificationTaskMap).filter(([taskId]) => taskId !== task.id)
|
Object.entries(state.notificationTaskMap).filter(
|
||||||
|
([taskId]) => taskId !== task.id
|
||||||
|
)
|
||||||
);
|
);
|
||||||
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
||||||
},
|
},
|
||||||
@@ -79,23 +85,28 @@ const tasksSlice = createSlice({
|
|||||||
if (!notificationId) return;
|
if (!notificationId) return;
|
||||||
notifications.update({
|
notifications.update({
|
||||||
id: notificationId,
|
id: notificationId,
|
||||||
color: 'teal',
|
color: "teal",
|
||||||
title: task.config.onSuccessData.title,
|
title: task.config.onSuccessData.title,
|
||||||
message: task.config.onSuccessData.message,
|
message: task.config.onSuccessData.message,
|
||||||
icon: <IconCheck style={{width: rem(18), height: rem(18)}}/>,
|
icon: <IconCheck style={{ width: rem(18), height: rem(18) }} />,
|
||||||
loading: false,
|
loading: false,
|
||||||
autoClose: 2000,
|
autoClose: 2000,
|
||||||
})
|
});
|
||||||
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id);
|
state.tasks = state.tasks.filter(
|
||||||
|
task => task.id !== action.payload.id
|
||||||
|
);
|
||||||
state.notificationTaskMap = Object.fromEntries(
|
state.notificationTaskMap = Object.fromEntries(
|
||||||
Object.entries(state.notificationTaskMap).filter(([taskId]) => taskId !== task.id)
|
Object.entries(state.notificationTaskMap).filter(
|
||||||
|
([taskId]) => taskId !== task.id
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {addTask, removeTask, successTask, failTask} = tasksSlice.actions;
|
export const { addTask, removeTask, successTask, failTask } =
|
||||||
|
tasksSlice.actions;
|
||||||
|
|
||||||
export default tasksSlice.reducer;
|
export default tasksSlice.reducer;
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
interface UIState {
|
interface UIState {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UIState = {
|
const initialState: UIState = {
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
const uiSlice = createSlice({
|
const uiSlice = createSlice({
|
||||||
name: 'ui',
|
name: "ui",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setIsLoading: (state, action: PayloadAction<boolean>) => {
|
setIsLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isLoading = action.payload;
|
state.isLoading = action.payload;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
)
|
|
||||||
|
|
||||||
export default uiSlice.reducer;
|
export default uiSlice.reducer;
|
||||||
export const {setIsLoading} = uiSlice.actions;
|
export const { setIsLoading } = uiSlice.actions;
|
||||||
|
|||||||
@@ -1,51 +1,68 @@
|
|||||||
import {QueryObserverResult, RefetchOptions, useQuery} from "@tanstack/react-query";
|
import {
|
||||||
import {CancelablePromise, PaginationInfoSchema} from "../client";
|
QueryObserverResult,
|
||||||
import {Pagination} from "../types/Pagination.ts";
|
RefetchOptions,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import { CancelablePromise, PaginationInfoSchema } from "../client";
|
||||||
|
import { Pagination } from "../types/Pagination.ts";
|
||||||
|
|
||||||
type Props<T, K> = {
|
type Props<T, K> = {
|
||||||
queryFn: () => CancelablePromise<T>,
|
queryFn: () => CancelablePromise<T>;
|
||||||
getObjectsFn: (response: T) => K[],
|
getObjectsFn: (response: T) => K[];
|
||||||
queryKey: string
|
queryKey: string;
|
||||||
}
|
};
|
||||||
type Response<T, K> = {
|
type Response<T, K> = {
|
||||||
objects: K[],
|
objects: K[];
|
||||||
refetch: (options?: RefetchOptions) => Promise<QueryObserverResult<T, Error>>
|
refetch: (
|
||||||
}
|
options?: RefetchOptions
|
||||||
const ObjectList = <T, K, >(props: Props<T, K>): Response<T, K> => {
|
) => Promise<QueryObserverResult<T, Error>>;
|
||||||
const {isPending, error, data, refetch} = useQuery({
|
};
|
||||||
|
const ObjectList = <T, K>(props: Props<T, K>): Response<T, K> => {
|
||||||
|
const { isPending, error, data, refetch } = useQuery({
|
||||||
queryKey: [props.queryKey],
|
queryKey: [props.queryKey],
|
||||||
queryFn: props.queryFn
|
queryFn: props.queryFn,
|
||||||
});
|
});
|
||||||
const objects = isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
|
const objects =
|
||||||
return {objects, refetch}
|
isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
|
||||||
}
|
return { objects, refetch };
|
||||||
|
};
|
||||||
|
|
||||||
interface ObjectWithPagination {
|
interface ObjectWithPagination {
|
||||||
paginationInfo: PaginationInfoSchema
|
paginationInfo: PaginationInfoSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropsWithPagination<T extends ObjectWithPagination, K> = {
|
type PropsWithPagination<T extends ObjectWithPagination, K> = {
|
||||||
queryFn: () => CancelablePromise<T>,
|
queryFn: () => CancelablePromise<T>;
|
||||||
getObjectsFn: (response: T) => K[],
|
getObjectsFn: (response: T) => K[];
|
||||||
queryKey: string,
|
queryKey: string;
|
||||||
pagination: Pagination
|
pagination: Pagination;
|
||||||
}
|
};
|
||||||
type ResponseWithPagination<T extends ObjectWithPagination, K> = {
|
type ResponseWithPagination<T extends ObjectWithPagination, K> = {
|
||||||
objects: K[],
|
objects: K[];
|
||||||
pagination?: PaginationInfoSchema,
|
pagination?: PaginationInfoSchema;
|
||||||
refetch: (options?: RefetchOptions) => Promise<QueryObserverResult<T, Error>>
|
refetch: (
|
||||||
}
|
options?: RefetchOptions
|
||||||
export const ObjectListWithPagination = <T extends ObjectWithPagination, K, >(props: PropsWithPagination<T, K>): ResponseWithPagination<T, K> => {
|
) => Promise<QueryObserverResult<T, Error>>;
|
||||||
const {isPending, error, data, refetch} = useQuery({
|
};
|
||||||
queryKey: [props.queryKey, props, props.pagination.itemsPerPage, props.pagination.page],
|
export const ObjectListWithPagination = <T extends ObjectWithPagination, K>(
|
||||||
|
props: PropsWithPagination<T, K>
|
||||||
|
): ResponseWithPagination<T, K> => {
|
||||||
|
const { isPending, error, data, refetch } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
props.queryKey,
|
||||||
|
props,
|
||||||
|
props.pagination.itemsPerPage,
|
||||||
|
props.pagination.page,
|
||||||
|
],
|
||||||
queryFn: props.queryFn,
|
queryFn: props.queryFn,
|
||||||
});
|
});
|
||||||
const objects = isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
|
const objects =
|
||||||
|
isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
|
||||||
return {
|
return {
|
||||||
objects,
|
objects,
|
||||||
pagination: data?.paginationInfo,
|
pagination: data?.paginationInfo,
|
||||||
refetch
|
refetch,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ObjectList
|
export default ObjectList;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import ObjectList from "./objectList.tsx";
|
import ObjectList from "./objectList.tsx";
|
||||||
import {MarketplaceService} from "../client";
|
import { MarketplaceService } from "../client";
|
||||||
|
|
||||||
const useBaseMarketplacesList = () => ObjectList({
|
const useBaseMarketplacesList = () =>
|
||||||
queryFn: MarketplaceService.getAllBaseMarketplaces,
|
ObjectList({
|
||||||
getObjectsFn: (response) => response.baseMarketplaces,
|
queryFn: MarketplaceService.getAllBaseMarketplaces,
|
||||||
queryKey: "getAllBaseMarketplaces"
|
getObjectsFn: response => response.baseMarketplaces,
|
||||||
})
|
queryKey: "getAllBaseMarketplaces",
|
||||||
export default useBaseMarketplacesList;
|
});
|
||||||
|
export default useBaseMarketplacesList;
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ type Props<T> = {
|
|||||||
export function useCRUD<T>(props: Props<T>) {
|
export function useCRUD<T>(props: Props<T>) {
|
||||||
const onCreate = (element: T) => {
|
const onCreate = (element: T) => {
|
||||||
props.onCreate(element);
|
props.onCreate(element);
|
||||||
}
|
};
|
||||||
const onChange = (element: T) => {
|
const onChange = (element: T) => {
|
||||||
props.onChange(element);
|
props.onChange(element);
|
||||||
|
};
|
||||||
}
|
|
||||||
const onDelete = (element: T) => {
|
const onDelete = (element: T) => {
|
||||||
props.onDelete(element);
|
props.onDelete(element);
|
||||||
}
|
};
|
||||||
|
|
||||||
return {onCreate, onChange, onDelete};
|
return { onCreate, onChange, onDelete };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {ObjectListWithPagination} from "./objectList.tsx";
|
import { ObjectListWithPagination } from "./objectList.tsx";
|
||||||
import {PayrollService} from "../client";
|
import { PayrollService } from "../client";
|
||||||
import {Pagination} from "../types/Pagination.ts";
|
import { Pagination } from "../types/Pagination.ts";
|
||||||
|
|
||||||
|
export const usePaymentRecordsList = (pagination: Pagination) =>
|
||||||
export const usePaymentRecordsList = (pagination: Pagination) => ObjectListWithPagination({
|
ObjectListWithPagination({
|
||||||
queryFn: () => PayrollService.getPaymentRecords(pagination),
|
queryFn: () => PayrollService.getPaymentRecords(pagination),
|
||||||
queryKey: "getPaymentRecords",
|
queryKey: "getPaymentRecords",
|
||||||
getObjectsFn: (response) => response.paymentRecords,
|
getObjectsFn: response => response.paymentRecords,
|
||||||
pagination
|
pagination,
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import ObjectList from "./objectList.tsx";
|
import ObjectList from "./objectList.tsx";
|
||||||
import {PayrollService} from "../client";
|
import { PayrollService } from "../client";
|
||||||
|
|
||||||
const usePayrollSchemasList = () => ObjectList({
|
const usePayrollSchemasList = () =>
|
||||||
queryFn: PayrollService.getAllPayrollSchemas,
|
ObjectList({
|
||||||
getObjectsFn: (response) => response.payrollSchemas,
|
queryFn: PayrollService.getAllPayrollSchemas,
|
||||||
queryKey: "getAllPayrollSchemas"
|
getObjectsFn: response => response.payrollSchemas,
|
||||||
})
|
queryKey: "getAllPayrollSchemas",
|
||||||
export default usePayrollSchemasList;
|
});
|
||||||
|
export default usePayrollSchemasList;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useRef, DependencyList} from 'react';
|
import { useEffect, useRef, DependencyList } from "react";
|
||||||
|
|
||||||
type UsePollingEffectOptions = {
|
type UsePollingEffectOptions = {
|
||||||
interval?: number;
|
interval?: number;
|
||||||
@@ -11,17 +11,13 @@ function usePollingEffect(
|
|||||||
dependencies: DependencyList = [],
|
dependencies: DependencyList = [],
|
||||||
options: UsePollingEffectOptions = {}
|
options: UsePollingEffectOptions = {}
|
||||||
): void {
|
): void {
|
||||||
const {
|
const { interval = 3000, isActive = true, onCleanUp = () => {} } = options;
|
||||||
interval = 3000,
|
|
||||||
isActive = true,
|
|
||||||
onCleanUp = () => {
|
|
||||||
}
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const timeoutIdRef = useRef<number | null>(null);
|
const timeoutIdRef = useRef<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive) { // If not active, don't do anything
|
if (!isActive) {
|
||||||
|
// If not active, don't do anything
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +28,10 @@ function usePollingEffect(
|
|||||||
await asyncCallback();
|
await asyncCallback();
|
||||||
} finally {
|
} finally {
|
||||||
if (!stopped) {
|
if (!stopped) {
|
||||||
timeoutIdRef.current = setTimeout(pollingCallback, interval);
|
timeoutIdRef.current = setTimeout(
|
||||||
|
pollingCallback,
|
||||||
|
interval
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ body {
|
|||||||
:root {
|
:root {
|
||||||
--item-border-size: 0.1rem;
|
--item-border-size: 0.1rem;
|
||||||
--item-border-radius: 0.5rem;
|
--item-border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/main.tsx
12
src/main.tsx
@@ -40,22 +40,24 @@ const queryClient = new QueryClient();
|
|||||||
|
|
||||||
// Configuring OpenAPI
|
// Configuring OpenAPI
|
||||||
OpenAPI.BASE = import.meta.env.VITE_API_URL;
|
OpenAPI.BASE = import.meta.env.VITE_API_URL;
|
||||||
OpenAPI.TOKEN = JSON.parse(localStorage.getItem("authState") || "{}")["accessToken"];
|
OpenAPI.TOKEN = JSON.parse(localStorage.getItem("authState") || "{}")[
|
||||||
|
"accessToken"
|
||||||
|
];
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<MantineProvider defaultColorScheme={"dark"}>
|
<MantineProvider defaultColorScheme={"dark"}>
|
||||||
<ModalsProvider labels={{ confirm: "Да", cancel: "Нет" }} modals={modals}>
|
<ModalsProvider
|
||||||
|
labels={{ confirm: "Да", cancel: "Нет" }}
|
||||||
|
modals={modals}>
|
||||||
<DatesProvider settings={{ locale: "ru" }}>
|
<DatesProvider settings={{ locale: "ru" }}>
|
||||||
<TasksProvider>
|
<TasksProvider>
|
||||||
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
</TasksProvider>
|
</TasksProvider>
|
||||||
|
|
||||||
</DatesProvider>
|
</DatesProvider>
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</Provider>,
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import {ContextModalProps} from "@mantine/modals";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
import {Button, Flex, rem, TextInput} from "@mantine/core";
|
import { Button, Flex, rem, TextInput } from "@mantine/core";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {ProductService} from "../../client";
|
import { ProductService } from "../../client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
productId: number;
|
productId: number;
|
||||||
onSubmit: (barcode: string) => void;
|
onSubmit: (barcode: string) => void;
|
||||||
}
|
};
|
||||||
const PrintBarcodeModal = ({
|
const PrintBarcodeModal = ({
|
||||||
id,
|
id,
|
||||||
context,
|
context,
|
||||||
innerProps
|
innerProps,
|
||||||
}: ContextModalProps<Props>) => {
|
}: ContextModalProps<Props>) => {
|
||||||
const {productId, onSubmit} = innerProps;
|
const { productId, onSubmit } = innerProps;
|
||||||
const [barcode, setBarcode] = useState<string | undefined>();
|
const [barcode, setBarcode] = useState<string | undefined>();
|
||||||
const [isBarcodeExist, setIsBarcodeExist] = useState<boolean>(true);
|
const [isBarcodeExist, setIsBarcodeExist] = useState<boolean>(true);
|
||||||
|
|
||||||
@@ -20,44 +20,41 @@ const PrintBarcodeModal = ({
|
|||||||
if (!barcode) return "Штрихкод не может быть пустым";
|
if (!barcode) return "Штрихкод не может быть пустым";
|
||||||
if (isBarcodeExist) return "Штрихкод уже существует";
|
if (isBarcodeExist) return "Штрихкод уже существует";
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!barcode) return;
|
if (!barcode) return;
|
||||||
ProductService.existsProductBarcode({
|
ProductService.existsProductBarcode({
|
||||||
productId: innerProps.productId,
|
productId: innerProps.productId,
|
||||||
barcode: barcode.trim()
|
barcode: barcode.trim(),
|
||||||
}).then((response) => {
|
}).then(response => {
|
||||||
setIsBarcodeExist(response.exists);
|
setIsBarcodeExist(response.exists);
|
||||||
})
|
});
|
||||||
}, [productId, barcode]);
|
}, [productId, barcode]);
|
||||||
|
|
||||||
const onSubmitClick = () => {
|
const onSubmitClick = () => {
|
||||||
if (!barcode || isBarcodeExist) return;
|
if (!barcode || isBarcodeExist) return;
|
||||||
onSubmit(barcode.trim());
|
onSubmit(barcode.trim());
|
||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
gap={rem(10)}
|
gap={rem(10)}
|
||||||
direction={"column"}
|
direction={"column"}>
|
||||||
>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
error={getErrorMessage()}
|
error={getErrorMessage()}
|
||||||
label={"Штрихкод"}
|
label={"Штрихкод"}
|
||||||
placeholder={"Введите или отсканируйте штрихкод"}
|
placeholder={"Введите или отсканируйте штрихкод"}
|
||||||
value={barcode}
|
value={barcode}
|
||||||
onChange={(event) => setBarcode(event.currentTarget.value)}
|
onChange={event => setBarcode(event.currentTarget.value)}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={onSubmitClick}
|
onClick={onSubmitClick}
|
||||||
disabled={!barcode || isBarcodeExist}
|
disabled={!barcode || isBarcodeExist}>
|
||||||
>
|
|
||||||
Добавить
|
Добавить
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PrintBarcodeModal;
|
export default PrintBarcodeModal;
|
||||||
|
|||||||
@@ -1,67 +1,68 @@
|
|||||||
import {UserSchema} from "../../client";
|
import { UserSchema } from "../../client";
|
||||||
import {ObjectSelectProps} from "../../components/ObjectSelect/ObjectSelect.tsx";
|
import { ObjectSelectProps } from "../../components/ObjectSelect/ObjectSelect.tsx";
|
||||||
import {ContextModalProps} from "@mantine/modals";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
import {Button, Flex, rem} from "@mantine/core";
|
import { Button, Flex, rem } from "@mantine/core";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import UserSelect from "../../components/Selects/UserSelect/UserSelect.tsx";
|
import UserSelect from "../../components/Selects/UserSelect/UserSelect.tsx";
|
||||||
import {notifications} from "../../shared/lib/notifications.ts";
|
import { notifications } from "../../shared/lib/notifications.ts";
|
||||||
|
|
||||||
type SelectProps = Omit<ObjectSelectProps<UserSchema>, 'data' | 'getValueFn' | 'getLabelFn' | 'onChange'>;
|
type SelectProps = Omit<
|
||||||
|
ObjectSelectProps<UserSchema>,
|
||||||
|
"data" | "getValueFn" | "getLabelFn" | "onChange"
|
||||||
|
>;
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelect: (user: UserSchema) => void
|
onSelect: (user: UserSchema) => void;
|
||||||
selectProps?: SelectProps;
|
selectProps?: SelectProps;
|
||||||
}
|
};
|
||||||
|
|
||||||
const EmployeeSelectModal = ({
|
const EmployeeSelectModal = ({
|
||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps,
|
innerProps,
|
||||||
}: ContextModalProps<Props>) => {
|
}: ContextModalProps<Props>) => {
|
||||||
const [innerValue, setInnerValue] = useState<UserSchema | undefined>();
|
const [innerValue, setInnerValue] = useState<UserSchema | undefined>();
|
||||||
|
|
||||||
const closeSelf = () => {
|
const closeSelf = () => {
|
||||||
context.closeContextModal(id);
|
context.closeContextModal(id);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onSelectClick = () => {
|
const onSelectClick = () => {
|
||||||
if (!innerValue) {
|
if (!innerValue) {
|
||||||
notifications.error({message: "Необходимо выбрать сотрудника"})
|
notifications.error({ message: "Необходимо выбрать сотрудника" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
innerProps.onSelect(innerValue);
|
innerProps.onSelect(innerValue);
|
||||||
closeSelf();
|
closeSelf();
|
||||||
}
|
};
|
||||||
const onCloseClick = () => {
|
const onCloseClick = () => {
|
||||||
closeSelf();
|
closeSelf();
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<Flex direction={"column"}
|
<Flex
|
||||||
gap={rem(10)}
|
direction={"column"}
|
||||||
>
|
gap={rem(10)}>
|
||||||
<Flex w={"100%"}>
|
<Flex w={"100%"}>
|
||||||
<UserSelect
|
<UserSelect
|
||||||
onChange={setInnerValue}
|
onChange={setInnerValue}
|
||||||
w={"100%"}
|
w={"100%"}
|
||||||
placeholder={"Выберите сотрудника"}
|
placeholder={"Выберите сотрудника"}
|
||||||
{...innerProps.selectProps}
|
{...innerProps.selectProps}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify={"flex-end"} gap={rem(10)}>
|
<Flex
|
||||||
|
justify={"flex-end"}
|
||||||
|
gap={rem(10)}>
|
||||||
<Button
|
<Button
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => onCloseClick()}
|
onClick={() => onCloseClick()}>
|
||||||
>
|
|
||||||
Отменить
|
Отменить
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={() => onSelectClick()}>
|
||||||
onClick={() => onSelectClick()}
|
|
||||||
>
|
|
||||||
Выбрать сотрудника
|
Выбрать сотрудника
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default EmployeeSelectModal
|
export default EmployeeSelectModal;
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
import SimpleUsersTable from "../../pages/LeadsPage/components/SimpleUsersTable/SimpleUsersTable.tsx";
|
import SimpleUsersTable from "../../pages/LeadsPage/components/SimpleUsersTable/SimpleUsersTable.tsx";
|
||||||
import {ContextModalProps} from "@mantine/modals";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
import {Flex, rem} from "@mantine/core";
|
import { Flex, rem } from "@mantine/core";
|
||||||
import {UserSchema} from "../../client";
|
import { UserSchema } from "../../client";
|
||||||
import {MutableRefObject, useEffect} from "react";
|
import { MutableRefObject, useEffect } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
items: MutableRefObject<UserSchema[]>
|
items: MutableRefObject<UserSchema[]>;
|
||||||
onChange: (items: UserSchema[]) => void;
|
onChange: (items: UserSchema[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmployeeTableModal = ({
|
const EmployeeTableModal = ({ innerProps }: ContextModalProps<Props>) => {
|
||||||
innerProps
|
useEffect(() => {}, [innerProps.items.current]);
|
||||||
}: ContextModalProps<Props>) => {
|
|
||||||
useEffect(() => {
|
|
||||||
}, [innerProps.items.current])
|
|
||||||
return (
|
return (
|
||||||
<Flex direction={"column"} gap={rem(10)}>
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={rem(10)}>
|
||||||
<SimpleUsersTable
|
<SimpleUsersTable
|
||||||
items={innerProps.items.current}
|
items={innerProps.items.current}
|
||||||
onChange={(event) => {
|
onChange={event => {
|
||||||
innerProps.onChange(event);
|
innerProps.onChange(event);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default EmployeeTableModal;
|
export default EmployeeTableModal;
|
||||||
|
|||||||
@@ -1,81 +1,83 @@
|
|||||||
import {ContextModalProps} from "@mantine/modals";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
import {Button, Flex, rem, Textarea} from "@mantine/core";
|
import { Button, Flex, rem, Textarea } from "@mantine/core";
|
||||||
import {DateTimePicker, DateValue} from "@mantine/dates";
|
import { DateTimePicker, DateValue } from "@mantine/dates";
|
||||||
import {useForm} from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import {DealSummaryReorderRequest} from "../../client";
|
import { DealSummaryReorderRequest } from "../../client";
|
||||||
|
|
||||||
import {dateWithoutTimezone} from "../../shared/lib/date.ts";
|
import { dateWithoutTimezone } from "../../shared/lib/date.ts";
|
||||||
|
|
||||||
type Deadline = {
|
type Deadline = {
|
||||||
deadline: DateValue,
|
deadline: DateValue;
|
||||||
comment: string
|
comment: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
request: Partial<DealSummaryReorderRequest>,
|
request: Partial<DealSummaryReorderRequest>;
|
||||||
onSubmit: (
|
onSubmit: (request: DealSummaryReorderRequest) => void;
|
||||||
request: DealSummaryReorderRequest,
|
};
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EnterDeadlineModal = ({
|
const EnterDeadlineModal = ({
|
||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps,
|
innerProps,
|
||||||
}: ContextModalProps<Props>) => {
|
}: ContextModalProps<Props>) => {
|
||||||
const form = useForm<Deadline>({
|
const form = useForm<Deadline>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
deadline: null,
|
deadline: null,
|
||||||
comment: '',
|
comment: "",
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
deadline: (datetime) => datetime !== null ? null : 'Необходимо ввести дедлайн',
|
deadline: datetime =>
|
||||||
}
|
datetime !== null ? null : "Необходимо ввести дедлайн",
|
||||||
})
|
},
|
||||||
|
});
|
||||||
const onCancelClick = () => {
|
const onCancelClick = () => {
|
||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
}
|
};
|
||||||
const onSubmit = (values: Deadline) => {
|
const onSubmit = (values: Deadline) => {
|
||||||
const {deadline, comment} = values;
|
const { deadline, comment } = values;
|
||||||
if (!deadline) return;
|
if (!deadline) return;
|
||||||
innerProps.onSubmit({
|
innerProps.onSubmit({
|
||||||
...innerProps.request,
|
...innerProps.request,
|
||||||
deadline: dateWithoutTimezone(deadline),
|
deadline: dateWithoutTimezone(deadline),
|
||||||
comment
|
comment,
|
||||||
} as unknown as DealSummaryReorderRequest);
|
} as unknown as DealSummaryReorderRequest);
|
||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
|
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||||
|
<Flex
|
||||||
<Flex direction={'column'} gap={10}>
|
direction={"column"}
|
||||||
<Flex direction={'column'} gap={rem(10)}>
|
gap={10}>
|
||||||
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={rem(10)}>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
required
|
required
|
||||||
label={'Дата и время'}
|
label={"Дата и время"}
|
||||||
placeholder={'Введите дату и время'}
|
placeholder={"Введите дату и время"}
|
||||||
minDate={new Date()}
|
minDate={new Date()}
|
||||||
{...form.getInputProps('deadline')}
|
{...form.getInputProps("deadline")}
|
||||||
/>
|
/>
|
||||||
<Textarea
|
<Textarea
|
||||||
label={'Коментарий'}
|
label={"Коментарий"}
|
||||||
placeholder={'Введите коментарий'}
|
placeholder={"Введите коментарий"}
|
||||||
{...form.getInputProps('comment')}
|
{...form.getInputProps("comment")}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex justify={'flex-end'} gap={rem(10)}>
|
<Flex
|
||||||
|
justify={"flex-end"}
|
||||||
|
gap={rem(10)}>
|
||||||
<Button
|
<Button
|
||||||
variant={'default'}
|
variant={"default"}
|
||||||
onClick={onCancelClick}
|
onClick={onCancelClick}>
|
||||||
>Отменить</Button>
|
Отменить
|
||||||
<Button
|
</Button>
|
||||||
type={'submit'}
|
<Button type={"submit"}>Сохранить</Button>
|
||||||
>Сохранить</Button>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</form>
|
</form>
|
||||||
|
);
|
||||||
)
|
|
||||||
};
|
};
|
||||||
export default EnterDeadlineModal;
|
export default EnterDeadlineModal;
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
import BaseFormModal, {CreateEditFormProps} from "../../pages/ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
import BaseFormModal, {
|
||||||
import {PositionSchema} from "../../client";
|
CreateEditFormProps,
|
||||||
import {ContextModalProps} from "@mantine/modals";
|
} from "../../pages/ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||||
import {useForm} from "@mantine/form";
|
import { PositionSchema } from "../../client";
|
||||||
import {Flex, rem, TextInput} from "@mantine/core";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
import {useEffect} from "react";
|
import { useForm } from "@mantine/form";
|
||||||
import CyrillicToTranslit from 'cyrillic-to-translit-js';
|
import { Flex, rem, TextInput } from "@mantine/core";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import CyrillicToTranslit from "cyrillic-to-translit-js";
|
||||||
|
|
||||||
type Props = CreateEditFormProps<PositionSchema>;
|
type Props = CreateEditFormProps<PositionSchema>;
|
||||||
|
|
||||||
const PositionFormModal = ({
|
const PositionFormModal = ({
|
||||||
id,
|
id,
|
||||||
context,
|
context,
|
||||||
innerProps
|
innerProps,
|
||||||
}: ContextModalProps<Props>) => {
|
}: ContextModalProps<Props>) => {
|
||||||
const translit = CyrillicToTranslit({preset: "ru"})
|
const translit = CyrillicToTranslit({ preset: "ru" });
|
||||||
const isEditing = 'element' in innerProps;
|
const isEditing = "element" in innerProps;
|
||||||
const initialValues: PositionSchema = isEditing ? innerProps.element : {
|
const initialValues: PositionSchema = isEditing
|
||||||
key: "",
|
? innerProps.element
|
||||||
name: ""
|
: {
|
||||||
}
|
key: "",
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
const form = useForm<PositionSchema>({
|
const form = useForm<PositionSchema>({
|
||||||
initialValues: initialValues
|
initialValues: initialValues,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing) return;
|
if (isEditing) return;
|
||||||
form.setFieldValue("key", translit.transform(form.values.name).toLowerCase());
|
form.setFieldValue(
|
||||||
|
"key",
|
||||||
|
translit.transform(form.values.name).toLowerCase()
|
||||||
|
);
|
||||||
}, [form.values.name]);
|
}, [form.values.name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,13 +40,11 @@ const PositionFormModal = ({
|
|||||||
closeOnSubmit
|
closeOnSubmit
|
||||||
form={form}
|
form={form}
|
||||||
onClose={() => context.closeContextModal(id)}
|
onClose={() => context.closeContextModal(id)}
|
||||||
{...innerProps}
|
{...innerProps}>
|
||||||
>
|
|
||||||
<BaseFormModal.Body>
|
<BaseFormModal.Body>
|
||||||
<Flex
|
<Flex
|
||||||
direction={"column"}
|
direction={"column"}
|
||||||
gap={rem(10)}
|
gap={rem(10)}>
|
||||||
>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label={"Название"}
|
label={"Название"}
|
||||||
placeholder={"Введите название должности"}
|
placeholder={"Введите название должности"}
|
||||||
@@ -53,7 +58,7 @@ const PositionFormModal = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
</BaseFormModal.Body>
|
</BaseFormModal.Body>
|
||||||
</BaseFormModal>
|
</BaseFormModal>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PositionFormModal;
|
export default PositionFormModal;
|
||||||
|
|||||||
@@ -1,72 +1,88 @@
|
|||||||
import {BarcodeAttributeSchema} from "../../client";
|
import { BarcodeAttributeSchema } from "../../client";
|
||||||
import {forwardRef} from "react";
|
import { forwardRef } from "react";
|
||||||
import styles from "./PrintBarcodeModal.module.css";
|
import styles from "./PrintBarcodeModal.module.css";
|
||||||
import {Flex, Text} from "@mantine/core";
|
import { Flex, Text } from "@mantine/core";
|
||||||
import Barcode from "react-barcode";
|
import Barcode from "react-barcode";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
attributes: BarcodeAttributeSchema[]
|
attributes: BarcodeAttributeSchema[];
|
||||||
barcode?: string;
|
barcode?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
additionalField?: string | null;
|
additionalField?: string | null;
|
||||||
}
|
};
|
||||||
type Ref = HTMLDivElement;
|
type Ref = HTMLDivElement;
|
||||||
const PrintBarcodeContainer = forwardRef<Ref, Props>(function PrintBarcodeContainer(props: Props, ref) {
|
const PrintBarcodeContainer = forwardRef<Ref, Props>(
|
||||||
const {attributes, barcode, quantity, additionalField} = props;
|
function PrintBarcodeContainer(props: Props, ref) {
|
||||||
|
const { attributes, barcode, quantity, additionalField } = props;
|
||||||
|
|
||||||
const MAX_ATTRIBUTES = additionalField && additionalField.length > 0 ? 5 : 6;
|
const MAX_ATTRIBUTES =
|
||||||
const MIN_BARCODE_SIZE = 30;
|
additionalField && additionalField.length > 0 ? 5 : 6;
|
||||||
const MAX_BARCODE_SIZE = 100;
|
const MIN_BARCODE_SIZE = 30;
|
||||||
const STEP = (MAX_BARCODE_SIZE - MIN_BARCODE_SIZE) / MAX_ATTRIBUTES;
|
const MAX_BARCODE_SIZE = 100;
|
||||||
const MAX_ATTRIBUTE_LENGTH = 35;
|
const STEP = (MAX_BARCODE_SIZE - MIN_BARCODE_SIZE) / MAX_ATTRIBUTES;
|
||||||
|
const MAX_ATTRIBUTE_LENGTH = 35;
|
||||||
|
|
||||||
const getBarcodeHeight = () => {
|
const getBarcodeHeight = () => {
|
||||||
return MIN_BARCODE_SIZE + (MAX_ATTRIBUTES - attributes.length) * STEP;
|
return (
|
||||||
}
|
MIN_BARCODE_SIZE + (MAX_ATTRIBUTES - attributes.length) * STEP
|
||||||
const getAttributeText = (attribute: BarcodeAttributeSchema) => {
|
);
|
||||||
let result = `${attribute.name}: ${attribute.value}`;
|
};
|
||||||
if (result.length > MAX_ATTRIBUTE_LENGTH) {
|
const getAttributeText = (attribute: BarcodeAttributeSchema) => {
|
||||||
result = result.slice(0, MAX_ATTRIBUTE_LENGTH - 1) + ".";
|
let result = `${attribute.name}: ${attribute.value}`;
|
||||||
}
|
if (result.length > MAX_ATTRIBUTE_LENGTH) {
|
||||||
return result;
|
result = result.slice(0, MAX_ATTRIBUTE_LENGTH - 1) + ".";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles["print-only"]}
|
||||||
|
ref={ref}>
|
||||||
|
{barcode &&
|
||||||
|
Array.from({ length: quantity }).map((_, index) => (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
className={styles["barcode-container"]}
|
||||||
|
key={index}
|
||||||
|
justify={"center"}
|
||||||
|
direction={"column"}>
|
||||||
|
<Flex
|
||||||
|
align={"center"}
|
||||||
|
justify={"center"}>
|
||||||
|
<Barcode
|
||||||
|
margin={0}
|
||||||
|
height={getBarcodeHeight()}
|
||||||
|
value={barcode}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
{attributes
|
||||||
|
.slice(0, MAX_ATTRIBUTES)
|
||||||
|
.map(attr => (
|
||||||
|
<Text
|
||||||
|
key={attr.name}
|
||||||
|
className={
|
||||||
|
styles["barcode-attribute-text"]
|
||||||
|
}
|
||||||
|
size={"xs"}>
|
||||||
|
{getAttributeText(attr)}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
{props.additionalField && (
|
||||||
|
<Text
|
||||||
|
className={
|
||||||
|
styles["barcode-attribute-text"]
|
||||||
|
}
|
||||||
|
size={"xs"}>
|
||||||
|
{props.additionalField}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
export default PrintBarcodeContainer;
|
||||||
<div className={styles['print-only']} ref={ref}>
|
|
||||||
{barcode && Array.from({length: quantity}).map((_, index) => (
|
|
||||||
<>
|
|
||||||
<Flex
|
|
||||||
className={styles['barcode-container']}
|
|
||||||
key={index}
|
|
||||||
justify={"center"}
|
|
||||||
direction={"column"}
|
|
||||||
>
|
|
||||||
<Flex align={"center"} justify={"center"}>
|
|
||||||
<Barcode margin={0} height={getBarcodeHeight()} value={barcode}/>
|
|
||||||
</Flex>
|
|
||||||
{attributes.slice(0, MAX_ATTRIBUTES).map((attr) => (
|
|
||||||
<Text
|
|
||||||
key={attr.name}
|
|
||||||
className={styles['barcode-attribute-text']}
|
|
||||||
size={"xs"}
|
|
||||||
>
|
|
||||||
{getAttributeText(attr)}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
{props.additionalField && (
|
|
||||||
<Text
|
|
||||||
className={styles['barcode-attribute-text']}
|
|
||||||
size={"xs"}
|
|
||||||
>
|
|
||||||
{props.additionalField}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
export default PrintBarcodeContainer;
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.barcode-container {
|
.barcode-container {
|
||||||
max-height: 1.45in;
|
max-height: 1.45in;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -19,4 +18,4 @@
|
|||||||
.barcode-attribute-text {
|
.barcode-attribute-text {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import {ContextModalProps, modals} from "@mantine/modals";
|
import { ContextModalProps, modals } from "@mantine/modals";
|
||||||
import {Button, Divider, Flex, NumberInput, rem, Select} from "@mantine/core";
|
import { Button, Divider, Flex, NumberInput, rem, Select } from "@mantine/core";
|
||||||
import {useEffect, useRef, useState} from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {BarcodeSchema, ProductService} from "../../client";
|
import { BarcodeSchema, ProductService } from "../../client";
|
||||||
import {useGetProductById} from "../../api/product/useGetProductById.tsx";
|
import { useGetProductById } from "../../api/product/useGetProductById.tsx";
|
||||||
import {notifications} from "../../shared/lib/notifications.ts";
|
import { notifications } from "../../shared/lib/notifications.ts";
|
||||||
import PrintBarcodeContainer from "./PrintBarcodeContainer.tsx";
|
import PrintBarcodeContainer from "./PrintBarcodeContainer.tsx";
|
||||||
import {base64ToBlob} from "../../shared/lib/utils.ts";
|
import { base64ToBlob } from "../../shared/lib/utils.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
productId: number;
|
productId: number;
|
||||||
defaultQuantity?: number;
|
defaultQuantity?: number;
|
||||||
}
|
};
|
||||||
const PrintBarcodeModal = ({
|
const PrintBarcodeModal = ({ innerProps }: ContextModalProps<Props>) => {
|
||||||
innerProps
|
const { productId, defaultQuantity = 1 } = innerProps;
|
||||||
}: ContextModalProps<Props>) => {
|
|
||||||
const {productId, defaultQuantity = 1} = innerProps;
|
|
||||||
const [quantity, setQuantity] = useState(defaultQuantity);
|
const [quantity, setQuantity] = useState(defaultQuantity);
|
||||||
const [barcode, setBarcode] = useState<string | undefined>()
|
const [barcode, setBarcode] = useState<string | undefined>();
|
||||||
const [barcodeData, setBarcodeData] = useState<BarcodeSchema | undefined>();
|
const [barcodeData, setBarcodeData] = useState<BarcodeSchema | undefined>();
|
||||||
|
|
||||||
const {product, refetch} = useGetProductById(productId);
|
const { product, refetch } = useGetProductById(productId);
|
||||||
|
|
||||||
const barcodeRef = useRef(null);
|
const barcodeRef = useRef(null);
|
||||||
// const handlePrint = useReactToPrint({
|
// const handlePrint = useReactToPrint({
|
||||||
@@ -27,47 +25,47 @@ const PrintBarcodeModal = ({
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
const onAdd = (newBarcode: string) => {
|
const onAdd = (newBarcode: string) => {
|
||||||
ProductService.addProductBarcode({requestBody: {productId, barcode: newBarcode}})
|
ProductService.addProductBarcode({
|
||||||
.then(async ({ok, message}) => {
|
requestBody: { productId, barcode: newBarcode },
|
||||||
notifications.guess(ok, {message});
|
}).then(async ({ ok, message }) => {
|
||||||
})
|
notifications.guess(ok, { message });
|
||||||
}
|
});
|
||||||
|
};
|
||||||
const onAddClick = () => {
|
const onAddClick = () => {
|
||||||
if (!product) return;
|
if (!product) return;
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
modal: "addBarcode",
|
modal: "addBarcode",
|
||||||
title: 'Добавление штрихкода',
|
title: "Добавление штрихкода",
|
||||||
withCloseButton: true,
|
withCloseButton: true,
|
||||||
innerProps: {
|
innerProps: {
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
onSubmit: onAdd
|
onSubmit: onAdd,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const onGenerateClick = () => {
|
const onGenerateClick = () => {
|
||||||
if (!product) return;
|
if (!product) return;
|
||||||
ProductService.generateProductBarcode({requestBody: {productId}})
|
ProductService.generateProductBarcode({
|
||||||
.then(async ({ok, message, barcode}) => {
|
requestBody: { productId },
|
||||||
notifications.guess(ok, {message});
|
}).then(async ({ ok, message, barcode }) => {
|
||||||
if (!ok) return;
|
notifications.guess(ok, { message });
|
||||||
await refetch();
|
if (!ok) return;
|
||||||
setBarcode(barcode);
|
await refetch();
|
||||||
})
|
setBarcode(barcode);
|
||||||
}
|
});
|
||||||
|
};
|
||||||
const fetchBarcodeData = () => {
|
const fetchBarcodeData = () => {
|
||||||
if (!barcode) return;
|
if (!barcode) return;
|
||||||
ProductService.getProductBarcode({
|
ProductService.getProductBarcode({
|
||||||
requestBody:
|
requestBody: { barcode, productId },
|
||||||
{barcode, productId}
|
}).then(({ barcode }) => {
|
||||||
}).then(({barcode}) => {
|
|
||||||
setBarcodeData(barcode);
|
setBarcodeData(barcode);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!product) return;
|
if (!product) return;
|
||||||
if (product.barcodes.length === 1)
|
if (product.barcodes.length === 1) setBarcode(product.barcodes[0]);
|
||||||
setBarcode(product.barcodes[0]);
|
|
||||||
}, [product]);
|
}, [product]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -79,11 +77,10 @@ const PrintBarcodeModal = ({
|
|||||||
<>
|
<>
|
||||||
<Flex
|
<Flex
|
||||||
gap={rem(10)}
|
gap={rem(10)}
|
||||||
direction={"column"}
|
direction={"column"}>
|
||||||
>
|
|
||||||
<Select
|
<Select
|
||||||
value={barcode}
|
value={barcode}
|
||||||
onChange={(value) => setBarcode(value || undefined)}
|
onChange={value => setBarcode(value || undefined)}
|
||||||
data={product?.barcodes}
|
data={product?.barcodes}
|
||||||
label={"Штрихкод"}
|
label={"Штрихкод"}
|
||||||
placeholder={"Выберите штрихкод"}
|
placeholder={"Выберите штрихкод"}
|
||||||
@@ -92,14 +89,16 @@ const PrintBarcodeModal = ({
|
|||||||
label={"Количество копий"}
|
label={"Количество копий"}
|
||||||
placeholder={"Введите количество копий"}
|
placeholder={"Введите количество копий"}
|
||||||
value={quantity}
|
value={quantity}
|
||||||
onChange={(value) => typeof value === "number" && setQuantity(value)}
|
onChange={value =>
|
||||||
|
typeof value === "number" && setQuantity(value)
|
||||||
|
}
|
||||||
min={1}
|
min={1}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider
|
<Divider my={rem(10)} />
|
||||||
my={rem(10)}
|
<Flex
|
||||||
/>
|
direction={"column"}
|
||||||
<Flex direction={"column"} gap={rem(10)}>
|
gap={rem(10)}>
|
||||||
<Flex gap={rem(10)}>
|
<Flex gap={rem(10)}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onAddClick()}
|
onClick={() => onAddClick()}
|
||||||
@@ -119,25 +118,30 @@ const PrintBarcodeModal = ({
|
|||||||
disabled={!barcode}
|
disabled={!barcode}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!barcode) return;
|
if (!barcode) return;
|
||||||
const response = await ProductService.getProductBarcodePdf({
|
const response =
|
||||||
requestBody: {
|
await ProductService.getProductBarcodePdf({
|
||||||
productId,
|
requestBody: {
|
||||||
barcode,
|
productId,
|
||||||
quantity
|
barcode,
|
||||||
}
|
quantity,
|
||||||
});
|
},
|
||||||
const pdfBlob = base64ToBlob(response.base64String, response.mimeType);
|
});
|
||||||
|
const pdfBlob = base64ToBlob(
|
||||||
|
response.base64String,
|
||||||
|
response.mimeType
|
||||||
|
);
|
||||||
const pdfUrl = URL.createObjectURL(pdfBlob);
|
const pdfUrl = URL.createObjectURL(pdfBlob);
|
||||||
const pdfWindow = window.open(pdfUrl);
|
const pdfWindow = window.open(pdfUrl);
|
||||||
if (!pdfWindow) {
|
if (!pdfWindow) {
|
||||||
notifications.error({message: "Ошибка"});
|
notifications.error({ message: "Ошибка" });
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
pdfWindow.onload = () => {
|
pdfWindow.onload = () => {
|
||||||
pdfWindow.print();
|
pdfWindow.print();
|
||||||
}
|
};
|
||||||
}}
|
}}>
|
||||||
>Печать</Button>
|
Печать
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<PrintBarcodeContainer
|
<PrintBarcodeContainer
|
||||||
@@ -148,7 +152,7 @@ const PrintBarcodeModal = ({
|
|||||||
additionalField={barcodeData?.additionalField}
|
additionalField={barcodeData?.additionalField}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PrintBarcodeModal;
|
export default PrintBarcodeModal;
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import {GetServiceKitSchema} from "../../client";
|
import { GetServiceKitSchema } from "../../client";
|
||||||
import {ContextModalProps} from "@mantine/modals";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import {Button, Flex, rem} from "@mantine/core";
|
import { Button, Flex, rem } from "@mantine/core";
|
||||||
import ServicesKitSelect from "../../components/Selects/ServicesKitSelect/ServicesKitSelect.tsx";
|
import ServicesKitSelect from "../../components/Selects/ServicesKitSelect/ServicesKitSelect.tsx";
|
||||||
import {notifications} from "../../shared/lib/notifications.ts";
|
import { notifications } from "../../shared/lib/notifications.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelect: (kit: GetServiceKitSchema) => void;
|
onSelect: (kit: GetServiceKitSchema) => void;
|
||||||
serviceType: number;
|
serviceType: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ServicesKitSelectModal = ({
|
const ServicesKitSelectModal = ({
|
||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps
|
innerProps,
|
||||||
}: ContextModalProps<Props>) => {
|
}: ContextModalProps<Props>) => {
|
||||||
const [kit, setKit] = useState<GetServiceKitSchema | undefined>();
|
const [kit, setKit] = useState<GetServiceKitSchema | undefined>();
|
||||||
const onSelectClick = () => {
|
const onSelectClick = () => {
|
||||||
if (!kit) {
|
if (!kit) {
|
||||||
notifications.error({message: "Выберите набор услуг"});
|
notifications.error({ message: "Выберите набор услуг" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
innerProps.onSelect(kit);
|
innerProps.onSelect(kit);
|
||||||
context.closeContextModal(id);
|
context.closeContextModal(id);
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
gap={rem(10)}
|
gap={rem(10)}
|
||||||
@@ -35,26 +35,27 @@ const ServicesKitSelectModal = ({
|
|||||||
placeholder={"Выберите набор услуг"}
|
placeholder={"Выберите набор услуг"}
|
||||||
value={kit}
|
value={kit}
|
||||||
onChange={setKit}
|
onChange={setKit}
|
||||||
filterBy={item => item.serviceType === innerProps.serviceType}
|
filterBy={item =>
|
||||||
|
item.serviceType === innerProps.serviceType
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify={"flex-end"} gap={rem(10)}>
|
<Flex
|
||||||
|
justify={"flex-end"}
|
||||||
|
gap={rem(10)}>
|
||||||
<Button
|
<Button
|
||||||
variant={"subtle"}
|
variant={"subtle"}
|
||||||
onClick={() => context.closeContextModal(id)}
|
onClick={() => context.closeContextModal(id)}>
|
||||||
>
|
|
||||||
Отменить
|
Отменить
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={onSelectClick}
|
onClick={onSelectClick}
|
||||||
variant={"default"}
|
variant={"default"}>
|
||||||
>
|
|
||||||
Добавить
|
Добавить
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ServicesKitSelectModal;
|
export default ServicesKitSelectModal;
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import AddDealServiceModal from "../pages/LeadsPage/modals/AddDealServiceModal.t
|
|||||||
import AddDealProductModal from "../pages/LeadsPage/modals/AddDealProductModal.tsx";
|
import AddDealProductModal from "../pages/LeadsPage/modals/AddDealProductModal.tsx";
|
||||||
import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx";
|
import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx";
|
||||||
import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
|
import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
|
||||||
import BarcodeTemplateFormModal
|
import BarcodeTemplateFormModal from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
|
||||||
from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
|
|
||||||
import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx";
|
import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx";
|
||||||
import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx";
|
import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx";
|
||||||
import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx";
|
import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx";
|
||||||
|
|||||||
@@ -1,75 +1,84 @@
|
|||||||
import styles from './AdminPage.module.css';
|
import styles from "./AdminPage.module.css";
|
||||||
import {Tabs} from "@mantine/core";
|
import { Tabs } from "@mantine/core";
|
||||||
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
|
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
|
||||||
import {IconBriefcase, IconCalendarUser, IconCurrencyDollar, IconUser} from "@tabler/icons-react";
|
import {
|
||||||
|
IconBriefcase,
|
||||||
|
IconCalendarUser,
|
||||||
|
IconCurrencyDollar,
|
||||||
|
IconUser,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx";
|
import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx";
|
||||||
import UsersTab from "./tabs/Users/UsersTab.tsx";
|
import UsersTab from "./tabs/Users/UsersTab.tsx";
|
||||||
import {motion} from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
|
import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
|
||||||
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
|
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
|
||||||
|
|
||||||
const AdminPage = () => {
|
const AdminPage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles['container']}>
|
<div className={styles["container"]}>
|
||||||
<PageBlock fullHeight>
|
<PageBlock fullHeight>
|
||||||
<Tabs variant={"outline"} keepMounted={false} defaultValue={"users"}>
|
<Tabs
|
||||||
|
variant={"outline"}
|
||||||
|
keepMounted={false}
|
||||||
|
defaultValue={"users"}>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab value={"users"} leftSection={<IconUser/>}>
|
<Tabs.Tab
|
||||||
|
value={"users"}
|
||||||
|
leftSection={<IconUser />}>
|
||||||
Пользователи
|
Пользователи
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value={"finances"} leftSection={<IconCurrencyDollar/>}>
|
<Tabs.Tab
|
||||||
|
value={"finances"}
|
||||||
|
leftSection={<IconCurrencyDollar />}>
|
||||||
Финансы
|
Финансы
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value={"rolesAndPositions"} leftSection={<IconBriefcase/>}>
|
<Tabs.Tab
|
||||||
|
value={"rolesAndPositions"}
|
||||||
|
leftSection={<IconBriefcase />}>
|
||||||
Должности
|
Должности
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value={"workTimeTable"} leftSection={<IconCalendarUser/>}>
|
<Tabs.Tab
|
||||||
|
value={"workTimeTable"}
|
||||||
|
leftSection={<IconCalendarUser />}>
|
||||||
Рабочее время
|
Рабочее время
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Panel value={"users"}>
|
<Tabs.Panel value={"users"}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{opacity: 0}}
|
initial={{ opacity: 0 }}
|
||||||
animate={{opacity: 1}}
|
animate={{ opacity: 1 }}
|
||||||
transition={{duration: 0.2}}
|
transition={{ duration: 0.2 }}>
|
||||||
>
|
<UsersTab />
|
||||||
<UsersTab/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value={"rolesAndPositions"}>
|
<Tabs.Panel value={"rolesAndPositions"}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{opacity: 0}}
|
initial={{ opacity: 0 }}
|
||||||
animate={{opacity: 1}}
|
animate={{ opacity: 1 }}
|
||||||
transition={{duration: 0.2}}
|
transition={{ duration: 0.2 }}>
|
||||||
>
|
<RolesAndPositionsTab />
|
||||||
<RolesAndPositionsTab/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value={"finances"}>
|
<Tabs.Panel value={"finances"}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{opacity: 0}}
|
initial={{ opacity: 0 }}
|
||||||
animate={{opacity: 1}}
|
animate={{ opacity: 1 }}
|
||||||
transition={{duration: 0.2}}
|
transition={{ duration: 0.2 }}>
|
||||||
>
|
<FinancesTab />
|
||||||
<FinancesTab/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value={"workTimeTable"}>
|
<Tabs.Panel value={"workTimeTable"}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{opacity: 0}}
|
initial={{ opacity: 0 }}
|
||||||
animate={{opacity: 1}}
|
animate={{ opacity: 1 }}
|
||||||
transition={{duration: 0.2}}
|
transition={{ duration: 0.2 }}>
|
||||||
>
|
<WorkTimeTable />
|
||||||
<WorkTimeTable/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</PageBlock>
|
</PageBlock>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AdminPage;
|
export default AdminPage;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||||
import {PayRateSchema} from "../../../../client";
|
import { PayRateSchema } from "../../../../client";
|
||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||||
import {usePayRatesTableColumns} from "./columns.tsx";
|
import { usePayRatesTableColumns } from "./columns.tsx";
|
||||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||||
import {modals} from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
import {MRT_TableOptions} from "mantine-react-table";
|
import { MRT_TableOptions } from "mantine-react-table";
|
||||||
|
|
||||||
type Props = CRUDTableProps<PayRateSchema>;
|
type Props = CRUDTableProps<PayRateSchema>;
|
||||||
|
|
||||||
const PayRateTable: FC<Props> = ({items, onCreate, onChange, onDelete}) => {
|
const PayRateTable: FC<Props> = ({ items, onCreate, onChange, onDelete }) => {
|
||||||
const columns = usePayRatesTableColumns();
|
const columns = usePayRatesTableColumns();
|
||||||
|
|
||||||
const onCreateClick = () => {
|
const onCreateClick = () => {
|
||||||
@@ -19,76 +19,77 @@ const PayRateTable: FC<Props> = ({items, onCreate, onChange, onDelete}) => {
|
|||||||
modal: "payRateForm",
|
modal: "payRateForm",
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
innerProps: {
|
innerProps: {
|
||||||
onCreate: onCreate
|
onCreate: onCreate,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
const onEditClick = (payRate: PayRateSchema) => {
|
const onEditClick = (payRate: PayRateSchema) => {
|
||||||
if (!onChange) return;
|
if (!onChange) return;
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
modal: "payRateForm",
|
modal: "payRateForm",
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
innerProps: {
|
innerProps: {
|
||||||
onChange: (event) => onChange({...event, id: payRate.id}),
|
onChange: event => onChange({ ...event, id: payRate.id }),
|
||||||
element: payRate
|
element: payRate,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
const onDeleteClick = (payRate: PayRateSchema) => {
|
const onDeleteClick = (payRate: PayRateSchema) => {
|
||||||
if (!onDelete) return;
|
if (!onDelete) return;
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Удаление тарифа',
|
title: "Удаление тарифа",
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Вы уверены что хотите удалить тариф {payRate.name}
|
Вы уверены что хотите удалить тариф {payRate.name}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
labels: {confirm: 'Да', cancel: "Нет"},
|
labels: { confirm: "Да", cancel: "Нет" },
|
||||||
confirmProps: {color: 'red'},
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => onDelete(payRate)
|
onConfirm: () => onDelete(payRate),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseTable
|
<BaseTable
|
||||||
data={items}
|
data={items}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
restProps={{
|
restProps={
|
||||||
enableSorting: false,
|
{
|
||||||
enableColumnActions: false,
|
enableSorting: false,
|
||||||
enableTopToolbar: true,
|
enableColumnActions: false,
|
||||||
renderTopToolbar: (
|
enableTopToolbar: true,
|
||||||
<Flex p={rem(10)}>
|
renderTopToolbar: (
|
||||||
|
<Flex p={rem(10)}>
|
||||||
<Button
|
<Button
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => onCreateClick()}
|
onClick={() => onCreateClick()}>
|
||||||
>
|
Создать тариф
|
||||||
Создать тариф
|
</Button>
|
||||||
</Button>
|
</Flex>
|
||||||
</Flex>
|
),
|
||||||
|
enableRowActions: true,
|
||||||
),
|
renderRowActions: ({ row }) => (
|
||||||
enableRowActions: true,
|
<Flex gap="md">
|
||||||
renderRowActions: ({row}) => (
|
<Tooltip label="Редактировать">
|
||||||
<Flex gap="md">
|
<ActionIcon
|
||||||
<Tooltip label="Редактировать">
|
onClick={() => onEditClick(row.original)}
|
||||||
<ActionIcon
|
variant={"default"}>
|
||||||
onClick={() => onEditClick(row.original)}
|
<IconEdit />
|
||||||
variant={"default"}>
|
</ActionIcon>
|
||||||
<IconEdit/>
|
</Tooltip>
|
||||||
</ActionIcon>
|
<Tooltip label="Удалить">
|
||||||
</Tooltip>
|
<ActionIcon
|
||||||
<Tooltip label="Удалить">
|
onClick={() => onDeleteClick(row.original)}
|
||||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
variant={"default"}>
|
||||||
<IconTrash/>
|
<IconTrash />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
),
|
||||||
} as MRT_TableOptions<PayRateSchema>}
|
} as MRT_TableOptions<PayRateSchema>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PayRateTable;
|
export default PayRateTable;
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
import {useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
import {MRT_ColumnDef} from "mantine-react-table";
|
import { MRT_ColumnDef } from "mantine-react-table";
|
||||||
import {PayRateSchema} from "../../../../client";
|
import { PayRateSchema } from "../../../../client";
|
||||||
|
|
||||||
export const usePayRatesTableColumns = () => {
|
export const usePayRatesTableColumns = () => {
|
||||||
return useMemo<MRT_ColumnDef<PayRateSchema>[]>(() => [
|
return useMemo<MRT_ColumnDef<PayRateSchema>[]>(
|
||||||
{
|
() => [
|
||||||
accessorKey: "name",
|
{
|
||||||
header: "Название тарифа"
|
accessorKey: "name",
|
||||||
},
|
header: "Название тарифа",
|
||||||
{
|
},
|
||||||
accessorKey: "payrollScheme.name",
|
{
|
||||||
header: "Система оплаты"
|
accessorKey: "payrollScheme.name",
|
||||||
},
|
header: "Система оплаты",
|
||||||
{
|
},
|
||||||
accessorKey: "baseRate",
|
{
|
||||||
header: "Базовая ставка",
|
accessorKey: "baseRate",
|
||||||
Cell: ({row}) => `${row.original.baseRate.toLocaleString("ru")}₽`
|
header: "Базовая ставка",
|
||||||
|
Cell: ({ row }) =>
|
||||||
},
|
`${row.original.baseRate.toLocaleString("ru")}₽`,
|
||||||
{
|
},
|
||||||
accessorKey: "overtimeThreshold",
|
{
|
||||||
header: "Порог сверхурочных"
|
accessorKey: "overtimeThreshold",
|
||||||
},
|
header: "Порог сверхурочных",
|
||||||
{
|
},
|
||||||
accessorKey: "overtimeRate",
|
{
|
||||||
header: "Сверхурочная ставка",
|
accessorKey: "overtimeRate",
|
||||||
Cell: ({row}) => row.original.overtimeRate && `${row.original.overtimeRate.toLocaleString("ru")}₽`
|
header: "Сверхурочная ставка",
|
||||||
|
Cell: ({ row }) =>
|
||||||
}
|
row.original.overtimeRate &&
|
||||||
], []);
|
`${row.original.overtimeRate.toLocaleString("ru")}₽`,
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import {FC, useEffect, useState} from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import {ActionIcon, Button, Flex, Pagination, rem, Text, Tooltip} from "@mantine/core";
|
import {
|
||||||
import {usePaymentRecordsList} from "../../../../hooks/usePaymentRecordsList.tsx";
|
ActionIcon,
|
||||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
Button,
|
||||||
import {usePaymentRecordsTableColumns} from "./columns.tsx";
|
Flex,
|
||||||
import {modals} from "@mantine/modals";
|
Pagination,
|
||||||
import {PaymentRecordCreateSchema, PaymentRecordGetSchema, PayrollService} from "../../../../client";
|
rem,
|
||||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
Text,
|
||||||
import {IconTrash} from "@tabler/icons-react";
|
Tooltip,
|
||||||
import {MRT_TableOptions} from "mantine-react-table";
|
} from "@mantine/core";
|
||||||
import {formatDate} from "../../../../types/utils.ts";
|
import { usePaymentRecordsList } from "../../../../hooks/usePaymentRecordsList.tsx";
|
||||||
|
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||||
|
import { usePaymentRecordsTableColumns } from "./columns.tsx";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import {
|
||||||
|
PaymentRecordCreateSchema,
|
||||||
|
PaymentRecordGetSchema,
|
||||||
|
PayrollService,
|
||||||
|
} from "../../../../client";
|
||||||
|
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||||
|
import { IconTrash } from "@tabler/icons-react";
|
||||||
|
import { MRT_TableOptions } from "mantine-react-table";
|
||||||
|
import { formatDate } from "../../../../types/utils.ts";
|
||||||
|
|
||||||
const PaymentRecordsTable: FC = () => {
|
const PaymentRecordsTable: FC = () => {
|
||||||
const [totalPages, setTotalPages] = useState(10);
|
const [totalPages, setTotalPages] = useState(10);
|
||||||
@@ -16,8 +28,8 @@ const PaymentRecordsTable: FC = () => {
|
|||||||
const {
|
const {
|
||||||
pagination: paginationInfo,
|
pagination: paginationInfo,
|
||||||
objects: paymentRecords,
|
objects: paymentRecords,
|
||||||
refetch
|
refetch,
|
||||||
} = usePaymentRecordsList({page: page, itemsPerPage: 10});
|
} = usePaymentRecordsList({ page: page, itemsPerPage: 10 });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!paginationInfo) return;
|
if (!paginationInfo) return;
|
||||||
setTotalPages(paginationInfo.totalPages);
|
setTotalPages(paginationInfo.totalPages);
|
||||||
@@ -26,98 +38,99 @@ const PaymentRecordsTable: FC = () => {
|
|||||||
const onCreate = (request: PaymentRecordCreateSchema) => {
|
const onCreate = (request: PaymentRecordCreateSchema) => {
|
||||||
PayrollService.createPaymentRecord({
|
PayrollService.createPaymentRecord({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
data: request
|
data: request,
|
||||||
}
|
},
|
||||||
}).then(async ({ok, message}) => {
|
}).then(async ({ ok, message }) => {
|
||||||
notifications.guess(ok, {message});
|
notifications.guess(ok, { message });
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
const onCreateClick = () => {
|
const onCreateClick = () => {
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
modal: "createPaymentRecord",
|
modal: "createPaymentRecord",
|
||||||
title: "Создание начисления",
|
title: "Создание начисления",
|
||||||
innerProps: {
|
innerProps: {
|
||||||
onCreate: onCreate
|
onCreate: onCreate,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
const onDelete = (record: PaymentRecordGetSchema) => {
|
const onDelete = (record: PaymentRecordGetSchema) => {
|
||||||
PayrollService.deletePaymentRecord({
|
PayrollService.deletePaymentRecord({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
paymentRecordId: record.id
|
paymentRecordId: record.id,
|
||||||
}
|
},
|
||||||
}).then(async ({ok, message}) => {
|
}).then(async ({ ok, message }) => {
|
||||||
notifications.guess(ok, {message});
|
notifications.guess(ok, { message });
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
const onDeleteClick = (record: PaymentRecordGetSchema) => {
|
const onDeleteClick = (record: PaymentRecordGetSchema) => {
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Удаление начисления',
|
title: "Удаление начисления",
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Вы уверены что хотите удалить начисление
|
Вы уверены что хотите удалить начисление пользователю{" "}
|
||||||
пользователю {record.user.firstName} {record.user.secondName} от {formatDate(record.createdAt)}
|
{record.user.firstName} {record.user.secondName} от{" "}
|
||||||
|
{formatDate(record.createdAt)}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
labels: {confirm: 'Да', cancel: "Нет"},
|
labels: { confirm: "Да", cancel: "Нет" },
|
||||||
confirmProps: {color: 'red'},
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => onDelete(record)
|
onConfirm: () => onDelete(record),
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
const columns = usePaymentRecordsTableColumns();
|
const columns = usePaymentRecordsTableColumns();
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
direction={"column"}
|
direction={"column"}
|
||||||
h={"100%"}
|
h={"100%"}
|
||||||
gap={rem(10)}
|
gap={rem(10)}>
|
||||||
>
|
|
||||||
<BaseTable
|
<BaseTable
|
||||||
data={paymentRecords}
|
data={paymentRecords}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
restProps={{
|
restProps={
|
||||||
enableSorting: false,
|
{
|
||||||
enableColumnActions: false,
|
enableSorting: false,
|
||||||
enableTopToolbar: true,
|
enableColumnActions: false,
|
||||||
renderTopToolbar: (
|
enableTopToolbar: true,
|
||||||
<Flex p={rem(10)}>
|
renderTopToolbar: (
|
||||||
|
<Flex p={rem(10)}>
|
||||||
<Button
|
<Button
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => onCreateClick()}
|
onClick={() => onCreateClick()}>
|
||||||
>
|
Создать начисление
|
||||||
Создать начисление
|
</Button>
|
||||||
</Button>
|
</Flex>
|
||||||
</Flex>
|
),
|
||||||
),
|
enableRowActions: true,
|
||||||
enableRowActions: true,
|
renderRowActions: ({ row }) => (
|
||||||
renderRowActions: ({row}) => (
|
<Flex gap="md">
|
||||||
<Flex gap="md">
|
<Tooltip label="Удалить">
|
||||||
|
<ActionIcon
|
||||||
<Tooltip label="Удалить">
|
onClick={() =>
|
||||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
onDeleteClick(row.original)
|
||||||
<IconTrash/>
|
}
|
||||||
</ActionIcon>
|
variant={"default"}>
|
||||||
</Tooltip>
|
<IconTrash />
|
||||||
</Flex>
|
</ActionIcon>
|
||||||
)
|
</Tooltip>
|
||||||
} as MRT_TableOptions<PaymentRecordGetSchema>}
|
</Flex>
|
||||||
|
),
|
||||||
|
} as MRT_TableOptions<PaymentRecordGetSchema>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{totalPages > 1 &&
|
{totalPages > 1 && (
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
style={{alignSelf: "flex-end"}}
|
style={{ alignSelf: "flex-end" }}
|
||||||
withEdges
|
withEdges
|
||||||
onChange={event => setPage(event)}
|
onChange={event => setPage(event)}
|
||||||
value={page}
|
value={page}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default PaymentRecordsTable;
|
export default PaymentRecordsTable;
|
||||||
|
|||||||
@@ -1,58 +1,79 @@
|
|||||||
import {useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
import {MRT_ColumnDef} from "mantine-react-table";
|
import { MRT_ColumnDef } from "mantine-react-table";
|
||||||
import {PaymentRecordGetSchema} from "../../../../client";
|
import { PaymentRecordGetSchema } from "../../../../client";
|
||||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||||
import {getPluralForm} from "../../../../shared/lib/utils.ts";
|
import { getPluralForm } from "../../../../shared/lib/utils.ts";
|
||||||
import {formatDate} from "../../../../types/utils.ts";
|
import { formatDate } from "../../../../types/utils.ts";
|
||||||
import {isEqual} from "lodash";
|
import { isEqual } from "lodash";
|
||||||
|
|
||||||
export const usePaymentRecordsTableColumns = () => {
|
export const usePaymentRecordsTableColumns = () => {
|
||||||
const getWorkUnitsText = (paymentRecord: PaymentRecordGetSchema) => {
|
const getWorkUnitsText = (paymentRecord: PaymentRecordGetSchema) => {
|
||||||
const payrollScheme = paymentRecord.payrollScheme;
|
const payrollScheme = paymentRecord.payrollScheme;
|
||||||
if (payrollScheme.key === PaySchemeType.HOURLY) {
|
if (payrollScheme.key === PaySchemeType.HOURLY) {
|
||||||
return getPluralForm(paymentRecord.workUnits, "час", "часа", "часов")
|
return getPluralForm(
|
||||||
} else if (
|
paymentRecord.workUnits,
|
||||||
payrollScheme.key === PaySchemeType.DAILY
|
"час",
|
||||||
) {
|
"часа",
|
||||||
return getPluralForm(paymentRecord.workUnits, "день", "дня", "дней")
|
"часов"
|
||||||
} else if (
|
);
|
||||||
payrollScheme.key === PaySchemeType.MONTHLY
|
} else if (payrollScheme.key === PaySchemeType.DAILY) {
|
||||||
) {
|
return getPluralForm(
|
||||||
return getPluralForm(paymentRecord.workUnits, "месяц", "месяца", "месяцев");
|
paymentRecord.workUnits,
|
||||||
|
"день",
|
||||||
|
"дня",
|
||||||
|
"дней"
|
||||||
|
);
|
||||||
|
} else if (payrollScheme.key === PaySchemeType.MONTHLY) {
|
||||||
|
return getPluralForm(
|
||||||
|
paymentRecord.workUnits,
|
||||||
|
"месяц",
|
||||||
|
"месяца",
|
||||||
|
"месяцев"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
};
|
||||||
const getDateRangesText = (paymentRecord: PaymentRecordGetSchema) => {
|
const getDateRangesText = (paymentRecord: PaymentRecordGetSchema) => {
|
||||||
if (paymentRecord.endDate && !isEqual(paymentRecord.startDate, paymentRecord.endDate)) {
|
if (
|
||||||
return `${formatDate(paymentRecord.startDate)} - ${formatDate(paymentRecord.endDate)}`
|
paymentRecord.endDate &&
|
||||||
|
!isEqual(paymentRecord.startDate, paymentRecord.endDate)
|
||||||
|
) {
|
||||||
|
return `${formatDate(paymentRecord.startDate)} - ${formatDate(paymentRecord.endDate)}`;
|
||||||
}
|
}
|
||||||
return `${formatDate(paymentRecord.startDate)}`;
|
return `${formatDate(paymentRecord.startDate)}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
return useMemo<MRT_ColumnDef<PaymentRecordGetSchema>[]>(() => [
|
return useMemo<MRT_ColumnDef<PaymentRecordGetSchema>[]>(
|
||||||
{
|
() => [
|
||||||
header: "Дата начисления",
|
{
|
||||||
Cell: ({row}) => new Date(row.original.createdAt).toLocaleString('ru-RU')
|
header: "Дата начисления",
|
||||||
},
|
Cell: ({ row }) =>
|
||||||
{
|
new Date(row.original.createdAt).toLocaleString("ru-RU"),
|
||||||
header: "Получил начисление",
|
},
|
||||||
Cell: ({row}) => `${row.original.user.firstName} ${row.original.user.secondName}`
|
{
|
||||||
},
|
header: "Получил начисление",
|
||||||
{
|
Cell: ({ row }) =>
|
||||||
header: "Создал начисление",
|
`${row.original.user.firstName} ${row.original.user.secondName}`,
|
||||||
Cell: ({row}) => `${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}`
|
},
|
||||||
},
|
{
|
||||||
{
|
header: "Создал начисление",
|
||||||
header: "Количество",
|
Cell: ({ row }) =>
|
||||||
Cell: ({row}) => `${row.original.workUnits} ${getWorkUnitsText(row.original)}`
|
`${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Сумма начисления",
|
header: "Количество",
|
||||||
Cell: ({row}) => row.original.amount.toLocaleString("ru-RU")
|
Cell: ({ row }) =>
|
||||||
},
|
`${row.original.workUnits} ${getWorkUnitsText(row.original)}`,
|
||||||
{
|
},
|
||||||
header: "Временной промежуток",
|
{
|
||||||
Cell: ({row}) => getDateRangesText(row.original)
|
header: "Сумма начисления",
|
||||||
}
|
Cell: ({ row }) => row.original.amount.toLocaleString("ru-RU"),
|
||||||
], [])
|
},
|
||||||
}
|
{
|
||||||
|
header: "Временной промежуток",
|
||||||
|
Cell: ({ row }) => getDateRangesText(row.original),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {PositionSchema} from "../../../../client";
|
ObjectSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { PositionSchema } from "../../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import usePositionsList from "../../hooks/usePositionsList.tsx";
|
import usePositionsList from "../../hooks/usePositionsList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<PositionSchema>, 'data' | 'getLabelFn' | 'getValueFn'>;
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<PositionSchema>,
|
||||||
|
"data" | "getLabelFn" | "getValueFn"
|
||||||
|
>;
|
||||||
|
|
||||||
const PositionSelect: FC<Props> = (props) => {
|
const PositionSelect: FC<Props> = props => {
|
||||||
const {objects: positions} = usePositionsList();
|
const { objects: positions } = usePositionsList();
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
getLabelFn={(position) => position.name}
|
getLabelFn={position => position.name}
|
||||||
getValueFn={(position) => position.key}
|
getValueFn={position => position.key}
|
||||||
data={positions}
|
data={positions}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default PositionSelect;
|
export default PositionSelect;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||||
import {PositionSchema} from "../../../../client";
|
import { PositionSchema } from "../../../../client";
|
||||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||||
import {usePositionsTableColumns} from "./columns.tsx";
|
import { usePositionsTableColumns } from "./columns.tsx";
|
||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||||
import {modals} from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import {IconTrash} from "@tabler/icons-react";
|
import { IconTrash } from "@tabler/icons-react";
|
||||||
import {MRT_TableOptions} from "mantine-react-table";
|
import { MRT_TableOptions } from "mantine-react-table";
|
||||||
|
|
||||||
type Props = CRUDTableProps<PositionSchema>;
|
type Props = CRUDTableProps<PositionSchema>;
|
||||||
|
|
||||||
const PositionsTable: FC<Props> = ({items, onCreate, onDelete}) => {
|
const PositionsTable: FC<Props> = ({ items, onCreate, onDelete }) => {
|
||||||
const columns = usePositionsTableColumns();
|
const columns = usePositionsTableColumns();
|
||||||
|
|
||||||
const onCreateClick = () => {
|
const onCreateClick = () => {
|
||||||
@@ -19,63 +19,60 @@ const PositionsTable: FC<Props> = ({items, onCreate, onDelete}) => {
|
|||||||
modal: "positionForm",
|
modal: "positionForm",
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
innerProps: {
|
innerProps: {
|
||||||
onCreate: onCreate
|
onCreate: onCreate,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const onDeleteClick = (position: PositionSchema) => {
|
const onDeleteClick = (position: PositionSchema) => {
|
||||||
if (!onDelete) return;
|
if (!onDelete) return;
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Удаление должности',
|
title: "Удаление должности",
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Вы уверены что хотите удалить должность {position.name}
|
Вы уверены что хотите удалить должность {position.name}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
labels: {confirm: 'Да', cancel: "Нет"},
|
labels: { confirm: "Да", cancel: "Нет" },
|
||||||
confirmProps: {color: 'red'},
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => onDelete(position)
|
onConfirm: () => onDelete(position),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<BaseTable
|
<BaseTable
|
||||||
data={items}
|
data={items}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
restProps={{
|
restProps={
|
||||||
enableTopToolbar: true,
|
{
|
||||||
enableSorting: false,
|
enableTopToolbar: true,
|
||||||
enableColumnActions: false,
|
enableSorting: false,
|
||||||
enableRowActions: true,
|
enableColumnActions: false,
|
||||||
renderTopToolbar: (
|
enableRowActions: true,
|
||||||
<Flex p={rem(10)}>
|
renderTopToolbar: (
|
||||||
|
<Flex p={rem(10)}>
|
||||||
<Button
|
<Button
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
onClick={() => onCreateClick()}
|
onClick={() => onCreateClick()}>
|
||||||
>
|
Создать должность
|
||||||
Создать должность
|
</Button>
|
||||||
</Button>
|
</Flex>
|
||||||
</Flex>
|
),
|
||||||
|
renderRowActions: ({ row }) => (
|
||||||
),
|
<Flex gap="md">
|
||||||
renderRowActions: ({row}) => (
|
<Tooltip label="Удалить">
|
||||||
<Flex gap="md">
|
<ActionIcon
|
||||||
|
onClick={() => onDeleteClick(row.original)}
|
||||||
<Tooltip label="Удалить">
|
variant={"default"}>
|
||||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
<IconTrash />
|
||||||
<IconTrash/>
|
</ActionIcon>
|
||||||
</ActionIcon>
|
</Tooltip>
|
||||||
</Tooltip>
|
</Flex>
|
||||||
</Flex>
|
),
|
||||||
)
|
} as MRT_TableOptions<PositionSchema>
|
||||||
} as MRT_TableOptions<PositionSchema>}
|
}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
)
|
export default PositionsTable;
|
||||||
}
|
|
||||||
|
|
||||||
export default PositionsTable;
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import {useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
import {MRT_ColumnDef} from "mantine-react-table";
|
import { MRT_ColumnDef } from "mantine-react-table";
|
||||||
import {PositionSchema} from "../../../../client";
|
import { PositionSchema } from "../../../../client";
|
||||||
|
|
||||||
export const usePositionsTableColumns = () => {
|
export const usePositionsTableColumns = () => {
|
||||||
return useMemo<MRT_ColumnDef<PositionSchema>[]>(() => [
|
return useMemo<MRT_ColumnDef<PositionSchema>[]>(
|
||||||
{
|
() => [
|
||||||
accessorKey: "name",
|
{
|
||||||
header: "Название должности"
|
accessorKey: "name",
|
||||||
},
|
header: "Название должности",
|
||||||
{
|
},
|
||||||
accessorKey: "key",
|
{
|
||||||
header: "Ключ"
|
accessorKey: "key",
|
||||||
},
|
header: "Ключ",
|
||||||
|
},
|
||||||
], []);
|
],
|
||||||
}
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
import ObjectSelect, {
|
||||||
import {RoleSchema} from "../../../../client";
|
ObjectSelectProps,
|
||||||
import {FC} from "react";
|
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||||
|
import { RoleSchema } from "../../../../client";
|
||||||
|
import { FC } from "react";
|
||||||
import useRolesList from "../../hooks/useRolesList.tsx";
|
import useRolesList from "../../hooks/useRolesList.tsx";
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<RoleSchema>, 'data' | 'getLabelFn' | 'getValueFn'>;
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<RoleSchema>,
|
||||||
|
"data" | "getLabelFn" | "getValueFn"
|
||||||
|
>;
|
||||||
|
|
||||||
const RolesSelect: FC<Props> = (props) => {
|
const RolesSelect: FC<Props> = props => {
|
||||||
const {objects: roles} = useRolesList();
|
const { objects: roles } = useRolesList();
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
getLabelFn={(position) => position.name}
|
getLabelFn={position => position.name}
|
||||||
getValueFn={(position) => position.key}
|
getValueFn={position => position.key}
|
||||||
data={roles}
|
data={roles}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default RolesSelect;
|
export default RolesSelect;
|
||||||
|
|||||||
@@ -1,102 +1,103 @@
|
|||||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||||
import {UserSchema} from "../../../../client";
|
import { UserSchema } from "../../../../client";
|
||||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||||
import {useUsersTableColumns} from "./columns.tsx";
|
import { useUsersTableColumns } from "./columns.tsx";
|
||||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
import {modals} from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import {MRT_TableOptions} from "mantine-react-table";
|
import { MRT_TableOptions } from "mantine-react-table";
|
||||||
|
|
||||||
type Props = CRUDTableProps<UserSchema>;
|
type Props = CRUDTableProps<UserSchema>;
|
||||||
|
|
||||||
const UsersTable: FC<Props> = ({items, onChange, onDelete, onCreate}) => {
|
const UsersTable: FC<Props> = ({ items, onChange, onDelete, onCreate }) => {
|
||||||
const columns = useUsersTableColumns();
|
const columns = useUsersTableColumns();
|
||||||
const onEditClick = (user: UserSchema) => {
|
const onEditClick = (user: UserSchema) => {
|
||||||
if (!onChange) return;
|
if (!onChange) return;
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
modal: "userFormModal",
|
modal: "userFormModal",
|
||||||
title: 'Редактирование пользователя',
|
title: "Редактирование пользователя",
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
innerProps: {
|
innerProps: {
|
||||||
onChange: onChange,
|
onChange: onChange,
|
||||||
element: user,
|
element: user,
|
||||||
},
|
},
|
||||||
size: "md"
|
size: "md",
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
const onDeleteClick = (user: UserSchema) => {
|
const onDeleteClick = (user: UserSchema) => {
|
||||||
if (!onDelete) return;
|
if (!onDelete) return;
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Удаление пользователя',
|
title: "Удаление пользователя",
|
||||||
// centered: true,
|
// centered: true,
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Вы уверены что хотите удалить пользователя {user.firstName} {user.secondName}
|
Вы уверены что хотите удалить пользователя {user.firstName}{" "}
|
||||||
|
{user.secondName}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
labels: {confirm: 'Да', cancel: "Нет"},
|
labels: { confirm: "Да", cancel: "Нет" },
|
||||||
confirmProps: {color: 'red'},
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => onDelete(user)
|
onConfirm: () => onDelete(user),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
const onCreateClick = () => {
|
const onCreateClick = () => {
|
||||||
if (!onCreate) return;
|
if (!onCreate) return;
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
modal: "userFormModal",
|
modal: "userFormModal",
|
||||||
title: 'Редактирование пользователя',
|
title: "Редактирование пользователя",
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
innerProps: {
|
innerProps: {
|
||||||
onCreate: onCreate
|
onCreate: onCreate,
|
||||||
},
|
},
|
||||||
size: "md"
|
size: "md",
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<BaseTable
|
<BaseTable
|
||||||
data={items}
|
data={items}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
restProps={{
|
restProps={
|
||||||
enableSorting: false,
|
{
|
||||||
enableColumnActions: false,
|
enableSorting: false,
|
||||||
enableTopToolbar: true,
|
enableColumnActions: false,
|
||||||
renderTopToolbar: (
|
enableTopToolbar: true,
|
||||||
<Flex p={rem(10)}>
|
renderTopToolbar: (
|
||||||
<Button
|
<Flex p={rem(10)}>
|
||||||
variant={"default"}
|
<Button
|
||||||
onClick={() => onCreateClick()}
|
variant={"default"}
|
||||||
>
|
onClick={() => onCreateClick()}>
|
||||||
Создать пользователя
|
Создать пользователя
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
),
|
),
|
||||||
enableRowActions: true,
|
enableRowActions: true,
|
||||||
renderRowActions: ({row}) => (
|
renderRowActions: ({ row }) => (
|
||||||
<Flex gap="md">
|
<Flex gap="md">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onEditClick(row.original)
|
onEditClick(row.original);
|
||||||
}}
|
}}
|
||||||
label="Редактировать">
|
label="Редактировать">
|
||||||
<ActionIcon
|
<ActionIcon variant={"default"}>
|
||||||
variant={"default"}>
|
<IconEdit />
|
||||||
<IconEdit/>
|
</ActionIcon>
|
||||||
</ActionIcon>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Tooltip
|
||||||
<Tooltip onClick={() => {
|
onClick={() => {
|
||||||
onDeleteClick(row.original);
|
onDeleteClick(row.original);
|
||||||
}} label="Удалить">
|
}}
|
||||||
<ActionIcon variant={"default"}>
|
label="Удалить">
|
||||||
<IconTrash/>
|
<ActionIcon variant={"default"}>
|
||||||
</ActionIcon>
|
<IconTrash />
|
||||||
</Tooltip>
|
</ActionIcon>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
),
|
</Flex>
|
||||||
} as MRT_TableOptions<UserSchema>}
|
),
|
||||||
|
} as MRT_TableOptions<UserSchema>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
)
|
export default UsersTable;
|
||||||
}
|
|
||||||
|
|
||||||
export default UsersTable;
|
|
||||||
|
|||||||
@@ -1,45 +1,49 @@
|
|||||||
import {useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
import {MRT_ColumnDef} from "mantine-react-table";
|
import { MRT_ColumnDef } from "mantine-react-table";
|
||||||
import {UserSchema} from "../../../../client";
|
import { UserSchema } from "../../../../client";
|
||||||
import {IconCheck, IconX} from "@tabler/icons-react";
|
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||||
|
|
||||||
export const useUsersTableColumns = () => {
|
export const useUsersTableColumns = () => {
|
||||||
return useMemo<MRT_ColumnDef<UserSchema>[]>(() => [
|
return useMemo<MRT_ColumnDef<UserSchema>[]>(
|
||||||
|
() => [
|
||||||
{
|
{
|
||||||
header: "ФИО",
|
header: "ФИО",
|
||||||
Cell: ({row}) => `${row.original.firstName} ${row.original.secondName}`
|
Cell: ({ row }) =>
|
||||||
},
|
`${row.original.firstName} ${row.original.secondName}`,
|
||||||
{
|
},
|
||||||
accessorKey: "phoneNumber",
|
{
|
||||||
header: "Номер телефона"
|
accessorKey: "phoneNumber",
|
||||||
},
|
header: "Номер телефона",
|
||||||
{
|
},
|
||||||
accessorKey: "role.name",
|
{
|
||||||
header: "Роль"
|
accessorKey: "role.name",
|
||||||
},
|
header: "Роль",
|
||||||
{
|
},
|
||||||
accessorKey: "position.name",
|
{
|
||||||
header: "Должность"
|
accessorKey: "position.name",
|
||||||
},
|
header: "Должность",
|
||||||
{
|
},
|
||||||
accessorKey: "payRate.name",
|
{
|
||||||
header: "Тариф"
|
accessorKey: "payRate.name",
|
||||||
},
|
header: "Тариф",
|
||||||
{
|
},
|
||||||
accessorKey: "comment",
|
{
|
||||||
header: "Дополнительная информация"
|
accessorKey: "comment",
|
||||||
},
|
header: "Дополнительная информация",
|
||||||
{
|
},
|
||||||
accessorKey: "isAdmin",
|
{
|
||||||
header: "Администратор",
|
accessorKey: "isAdmin",
|
||||||
Cell: ({row}) => row.original.isAdmin ? <IconCheck/> : <IconX/>
|
header: "Администратор",
|
||||||
},
|
Cell: ({ row }) =>
|
||||||
{
|
row.original.isAdmin ? <IconCheck /> : <IconX />,
|
||||||
accessorKey: "isBlocked",
|
},
|
||||||
header: "Заблокирован",
|
{
|
||||||
Cell: ({row}) => row.original.isBlocked ? <IconCheck/> : <IconX/>
|
accessorKey: "isBlocked",
|
||||||
},
|
header: "Заблокирован",
|
||||||
], []);
|
Cell: ({ row }) =>
|
||||||
}
|
row.original.isBlocked ? <IconCheck /> : <IconX />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {PayrollService} from "../../../client";
|
import { PayrollService } from "../../../client";
|
||||||
import ObjectList from "../../../hooks/objectList.tsx";
|
import ObjectList from "../../../hooks/objectList.tsx";
|
||||||
|
|
||||||
const usePayRatesList = () => ObjectList({
|
const usePayRatesList = () =>
|
||||||
queryFn: PayrollService.getAllPayRates,
|
ObjectList({
|
||||||
getObjectsFn: response => response.payRates,
|
queryFn: PayrollService.getAllPayRates,
|
||||||
queryKey: "getAllPayRates"
|
getObjectsFn: response => response.payRates,
|
||||||
})
|
queryKey: "getAllPayRates",
|
||||||
export default usePayRatesList;
|
});
|
||||||
|
export default usePayRatesList;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {PositionService} from "../../../client";
|
import { PositionService } from "../../../client";
|
||||||
import ObjectList from "../../../hooks/objectList.tsx";
|
import ObjectList from "../../../hooks/objectList.tsx";
|
||||||
|
|
||||||
const usePositionsList = () => ObjectList({
|
const usePositionsList = () =>
|
||||||
queryFn: PositionService.getAllPositions,
|
ObjectList({
|
||||||
getObjectsFn: response => response.positions,
|
queryFn: PositionService.getAllPositions,
|
||||||
queryKey: "getAllPositions"
|
getObjectsFn: response => response.positions,
|
||||||
})
|
queryKey: "getAllPositions",
|
||||||
export default usePositionsList;
|
});
|
||||||
|
export default usePositionsList;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import ObjectList from "../../../hooks/objectList.tsx";
|
import ObjectList from "../../../hooks/objectList.tsx";
|
||||||
import {RoleService} from "../../../client";
|
import { RoleService } from "../../../client";
|
||||||
|
|
||||||
const useRolesList = () => ObjectList({
|
const useRolesList = () =>
|
||||||
queryFn: RoleService.getAllRoles,
|
ObjectList({
|
||||||
getObjectsFn: response => response.roles,
|
queryFn: RoleService.getAllRoles,
|
||||||
queryKey: "getAllRoles"
|
getObjectsFn: response => response.roles,
|
||||||
})
|
queryKey: "getAllRoles",
|
||||||
export default useRolesList;
|
});
|
||||||
|
export default useRolesList;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import ObjectList from "../../../hooks/objectList.tsx";
|
import ObjectList from "../../../hooks/objectList.tsx";
|
||||||
import {UserService} from "../../../client";
|
import { UserService } from "../../../client";
|
||||||
|
|
||||||
|
const useUsersList = () =>
|
||||||
|
ObjectList({
|
||||||
|
queryFn: UserService.getAllUsers,
|
||||||
|
getObjectsFn: response => response.users,
|
||||||
|
queryKey: "getAllUsers",
|
||||||
|
});
|
||||||
|
|
||||||
const useUsersList = () => ObjectList({
|
export default useUsersList;
|
||||||
queryFn: UserService.getAllUsers,
|
|
||||||
getObjectsFn: (response) => response.users,
|
|
||||||
queryKey: "getAllUsers"
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useUsersList;
|
|
||||||
|
|||||||
@@ -1,41 +1,51 @@
|
|||||||
import BaseFormModal, {CreateProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
import BaseFormModal, {
|
||||||
import {PaymentRecordCreateSchema} from "../../../../client";
|
CreateProps,
|
||||||
import {ContextModalProps} from "@mantine/modals";
|
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||||
import {useForm} from "@mantine/form";
|
import { PaymentRecordCreateSchema } from "../../../../client";
|
||||||
import {Flex, NumberInput, rem} from "@mantine/core";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
import {DatePickerInput, MonthPickerInput} from "@mantine/dates";
|
import { useForm } from "@mantine/form";
|
||||||
import {useEffect, useState} from "react";
|
import { Flex, NumberInput, rem } from "@mantine/core";
|
||||||
|
import { DatePickerInput, MonthPickerInput } from "@mantine/dates";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import UserSelect from "../../../../components/Selects/UserSelect/UserSelect.tsx";
|
import UserSelect from "../../../../components/Selects/UserSelect/UserSelect.tsx";
|
||||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||||
import {motion} from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {dateWithoutTimezone} from "../../../../shared/lib/date.ts";
|
import { dateWithoutTimezone } from "../../../../shared/lib/date.ts";
|
||||||
|
|
||||||
type Props = CreateProps<PaymentRecordCreateSchema>;
|
type Props = CreateProps<PaymentRecordCreateSchema>;
|
||||||
const CreatePaymentRecordModal = ({
|
const CreatePaymentRecordModal = ({
|
||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps
|
innerProps,
|
||||||
}: ContextModalProps<Props>) => {
|
}: ContextModalProps<Props>) => {
|
||||||
const form = useForm<Partial<PaymentRecordCreateSchema>>({
|
const form = useForm<Partial<PaymentRecordCreateSchema>>({
|
||||||
validate: {
|
validate: {
|
||||||
user: (user) => !user && "Необходимо выбрать сотрудника",
|
user: user => !user && "Необходимо выбрать сотрудника",
|
||||||
startDate: (startDate) => !startDate && "Необходимо указать временной промежуток",
|
startDate: startDate =>
|
||||||
workUnits: (workUnits) => !workUnits && "Укажите количество"
|
!startDate && "Необходимо указать временной промежуток",
|
||||||
}
|
workUnits: workUnits => !workUnits && "Укажите количество",
|
||||||
})
|
},
|
||||||
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([null, null]);
|
});
|
||||||
|
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const setDates = (
|
||||||
const setDates = (start: string | undefined, end: string | undefined) => {
|
start: string | undefined,
|
||||||
|
end: string | undefined
|
||||||
|
) => {
|
||||||
form.setFieldValue("startDate", start);
|
form.setFieldValue("startDate", start);
|
||||||
form.setFieldValue("endDate", end);
|
form.setFieldValue("endDate", end);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dateRange.every(dr => dr == null)) {
|
if (dateRange.every(dr => dr == null)) {
|
||||||
setDates(undefined, undefined);
|
setDates(undefined, undefined);
|
||||||
return
|
return;
|
||||||
} else {
|
} else {
|
||||||
const notNullValues = dateRange.filter((dr): dr is Date => dr !== null).map(dateWithoutTimezone);
|
const notNullValues = dateRange
|
||||||
|
.filter((dr): dr is Date => dr !== null)
|
||||||
|
.map(dateWithoutTimezone);
|
||||||
const startDate = notNullValues[0];
|
const startDate = notNullValues[0];
|
||||||
const endDate = notNullValues[1] || startDate;
|
const endDate = notNullValues[1] || startDate;
|
||||||
setDates(startDate, endDate);
|
setDates(startDate, endDate);
|
||||||
@@ -43,14 +53,13 @@ const CreatePaymentRecordModal = ({
|
|||||||
}, [dateRange]);
|
}, [dateRange]);
|
||||||
|
|
||||||
const getDateRangeInput = () => {
|
const getDateRangeInput = () => {
|
||||||
if (!form.values.user) return <></>
|
if (!form.values.user) return <></>;
|
||||||
const payRate = form.values.user.payRate;
|
const payRate = form.values.user.payRate;
|
||||||
if (!payRate) return <></>;
|
if (!payRate) return <></>;
|
||||||
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
|
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
|
||||||
return (
|
return (
|
||||||
<MonthPickerInput
|
<MonthPickerInput
|
||||||
error={form.getInputProps("startDate").error}
|
error={form.getInputProps("startDate").error}
|
||||||
|
|
||||||
label={"Временной промежуток"}
|
label={"Временной промежуток"}
|
||||||
placeholder={"Выберите временной промежуток"}
|
placeholder={"Выберите временной промежуток"}
|
||||||
type={"range"}
|
type={"range"}
|
||||||
@@ -58,73 +67,78 @@ const CreatePaymentRecordModal = ({
|
|||||||
onChange={setDateRange}
|
onChange={setDateRange}
|
||||||
allowSingleDateInRange
|
allowSingleDateInRange
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
return (<DatePickerInput
|
return (
|
||||||
error={form.getInputProps("startDate").error}
|
<DatePickerInput
|
||||||
label={"Временной промежуток"}
|
error={form.getInputProps("startDate").error}
|
||||||
placeholder={"Выберите временной промежуток"}
|
label={"Временной промежуток"}
|
||||||
type={"range"}
|
placeholder={"Выберите временной промежуток"}
|
||||||
allowSingleDateInRange
|
type={"range"}
|
||||||
value={dateRange}
|
allowSingleDateInRange
|
||||||
onChange={setDateRange}
|
value={dateRange}
|
||||||
/>);
|
onChange={setDateRange}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
const getAmountLabel = () => {
|
const getAmountLabel = () => {
|
||||||
const user = form.values.user;
|
const user = form.values.user;
|
||||||
if (!user) return "";
|
if (!user) return "";
|
||||||
const payRate = user?.payRate;
|
const payRate = user?.payRate;
|
||||||
if (!payRate) return "";
|
if (!payRate) return "";
|
||||||
if (payRate.payrollScheme.key == PaySchemeType.HOURLY) return "Количество часов";
|
if (payRate.payrollScheme.key == PaySchemeType.HOURLY)
|
||||||
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY) return "Количество месяцев";
|
return "Количество часов";
|
||||||
if (payRate.payrollScheme.key == PaySchemeType.DAILY) return "Количество дней";
|
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
|
||||||
|
return "Количество месяцев";
|
||||||
|
if (payRate.payrollScheme.key == PaySchemeType.DAILY)
|
||||||
|
return "Количество дней";
|
||||||
return "";
|
return "";
|
||||||
}
|
};
|
||||||
const getAmountPlaceholder = () => {
|
const getAmountPlaceholder = () => {
|
||||||
return "Укажите " + getAmountLabel().toLowerCase();
|
return "Укажите " + getAmountLabel().toLowerCase();
|
||||||
}
|
};
|
||||||
return (<BaseFormModal
|
return (
|
||||||
form={form}
|
<BaseFormModal
|
||||||
closeOnSubmit
|
form={form}
|
||||||
onClose={() => context.closeContextModal(id)}
|
closeOnSubmit
|
||||||
{...innerProps}
|
onClose={() => context.closeContextModal(id)}
|
||||||
>
|
{...innerProps}>
|
||||||
<BaseFormModal.Body>
|
<BaseFormModal.Body>
|
||||||
<>
|
<>
|
||||||
<Flex direction={"column"} gap={rem(10)}>
|
<Flex
|
||||||
<UserSelect
|
direction={"column"}
|
||||||
label={"Сотрудник"}
|
gap={rem(10)}>
|
||||||
placeholder={"Выберите сотрудника"}
|
<UserSelect
|
||||||
searchable
|
label={"Сотрудник"}
|
||||||
filterBy={(user) => !!user.payRate}
|
placeholder={"Выберите сотрудника"}
|
||||||
{...form.getInputProps("user")}
|
searchable
|
||||||
/>
|
filterBy={user => !!user.payRate}
|
||||||
|
{...form.getInputProps("user")}
|
||||||
|
/>
|
||||||
|
|
||||||
{form.values.user &&
|
{form.values.user && (
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
initial={{opacity: 0}}
|
animate={{ opacity: 1 }}
|
||||||
animate={{opacity: 1}}
|
transition={{ duration: 0.3 }}>
|
||||||
transition={{duration: 0.3}}
|
<Flex
|
||||||
>
|
direction={"column"}
|
||||||
<Flex
|
gap={rem(10)}>
|
||||||
direction={"column"}
|
{getDateRangeInput()}
|
||||||
gap={rem(10)}>
|
<NumberInput
|
||||||
|
label={getAmountLabel()}
|
||||||
{getDateRangeInput()}
|
placeholder={getAmountPlaceholder()}
|
||||||
<NumberInput
|
hideControls
|
||||||
label={getAmountLabel()}
|
{...form.getInputProps("workUnits")}
|
||||||
placeholder={getAmountPlaceholder()}
|
/>
|
||||||
hideControls
|
</Flex>
|
||||||
{...form.getInputProps("workUnits")}
|
</motion.div>
|
||||||
/>
|
</>
|
||||||
</Flex>
|
)}
|
||||||
</motion.div>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
}
|
</BaseFormModal.Body>
|
||||||
</Flex>
|
</BaseFormModal>
|
||||||
</>
|
);
|
||||||
</BaseFormModal.Body>
|
};
|
||||||
</BaseFormModal>)
|
export default CreatePaymentRecordModal;
|
||||||
}
|
|
||||||
export default CreatePaymentRecordModal;
|
|
||||||
|
|||||||
@@ -1,41 +1,47 @@
|
|||||||
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
import BaseFormModal, {
|
||||||
import {PayRateSchemaBase} from "../../../../client";
|
CreateEditFormProps,
|
||||||
import {ContextModalProps} from "@mantine/modals";
|
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||||
import {useForm} from "@mantine/form";
|
import { PayRateSchemaBase } from "../../../../client";
|
||||||
import {Fieldset, Flex, NumberInput, rem, TextInput} from "@mantine/core";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { Fieldset, Flex, NumberInput, rem, TextInput } from "@mantine/core";
|
||||||
import PayrollSchemeSelect from "../../../../components/Selects/PayrollSchemeSelect/PayrollSchemeSelect.tsx";
|
import PayrollSchemeSelect from "../../../../components/Selects/PayrollSchemeSelect/PayrollSchemeSelect.tsx";
|
||||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||||
|
|
||||||
type Props = CreateEditFormProps<PayRateSchemaBase>
|
type Props = CreateEditFormProps<PayRateSchemaBase>;
|
||||||
|
|
||||||
const PayRateFormModal = ({
|
const PayRateFormModal = ({
|
||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps
|
innerProps,
|
||||||
}: ContextModalProps<Props>) => {
|
}: ContextModalProps<Props>) => {
|
||||||
const isEditing = 'element' in innerProps;
|
const isEditing = "element" in innerProps;
|
||||||
const initialValue: Partial<PayRateSchemaBase> = isEditing ? innerProps.element : {};
|
const initialValue: Partial<PayRateSchemaBase> = isEditing
|
||||||
|
? innerProps.element
|
||||||
|
: {};
|
||||||
|
|
||||||
const form = useForm<Partial<PayRateSchemaBase>>({
|
const form = useForm<Partial<PayRateSchemaBase>>({
|
||||||
initialValues: initialValue,
|
initialValues: initialValue,
|
||||||
validate: {
|
validate: {
|
||||||
name: (name) => !name && "Необходимо указать название тарифа",
|
name: name => !name && "Необходимо указать название тарифа",
|
||||||
payrollScheme: (scheme) => !scheme && "Необходимо выбрать систему оплаты",
|
payrollScheme: scheme =>
|
||||||
baseRate: (baseRate) => !baseRate && "Небходимо указать базовую ставку"
|
!scheme && "Необходимо выбрать систему оплаты",
|
||||||
}
|
baseRate: baseRate =>
|
||||||
|
!baseRate && "Небходимо указать базовую ставку",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<BaseFormModal
|
<BaseFormModal
|
||||||
form={form}
|
form={form}
|
||||||
closeOnSubmit
|
closeOnSubmit
|
||||||
onClose={() => context.closeContextModal(id)}
|
onClose={() => context.closeContextModal(id)}
|
||||||
{...innerProps}
|
{...innerProps}>
|
||||||
>
|
|
||||||
<BaseFormModal.Body>
|
<BaseFormModal.Body>
|
||||||
<>
|
<>
|
||||||
<Fieldset legend={"Общие параметры"}>
|
<Fieldset legend={"Общие параметры"}>
|
||||||
<Flex direction={"column"} gap={rem(10)}>
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={rem(10)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={"Название"}
|
label={"Название"}
|
||||||
placeholder={"Введите название тарифа"}
|
placeholder={"Введите название тарифа"}
|
||||||
@@ -47,11 +53,11 @@ const PayRateFormModal = ({
|
|||||||
{...form.getInputProps("payrollScheme")}
|
{...form.getInputProps("payrollScheme")}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
<Fieldset>
|
<Fieldset>
|
||||||
<Flex direction={"column"} gap={rem(10)}>
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={rem(10)}>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
allowNegative={false}
|
allowNegative={false}
|
||||||
hideControls
|
hideControls
|
||||||
@@ -61,38 +67,42 @@ const PayRateFormModal = ({
|
|||||||
thousandSeparator={" "}
|
thousandSeparator={" "}
|
||||||
suffix={"₽"}
|
suffix={"₽"}
|
||||||
{...form.getInputProps("baseRate")}
|
{...form.getInputProps("baseRate")}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
{form.values.payrollScheme?.key === PaySchemeType.HOURLY &&
|
{form.values.payrollScheme?.key ===
|
||||||
|
PaySchemeType.HOURLY && (
|
||||||
<>
|
<>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
allowNegative={false}
|
allowNegative={false}
|
||||||
hideControls
|
hideControls
|
||||||
allowDecimal={false}
|
allowDecimal={false}
|
||||||
label={"Порог сверхурочных"}
|
label={"Порог сверхурочных"}
|
||||||
placeholder={"Введите порог сверхурочных"}
|
placeholder={
|
||||||
{...form.getInputProps("overtimeThreshold")}
|
"Введите порог сверхурочных"
|
||||||
|
}
|
||||||
|
{...form.getInputProps(
|
||||||
|
"overtimeThreshold"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
allowNegative={false}
|
allowNegative={false}
|
||||||
hideControls
|
hideControls
|
||||||
decimalScale={2}
|
decimalScale={2}
|
||||||
label={"Сверхурочная ставка"}
|
label={"Сверхурочная ставка"}
|
||||||
placeholder={"Выберите сверхурочную ставку"}
|
placeholder={
|
||||||
|
"Выберите сверхурочную ставку"
|
||||||
|
}
|
||||||
thousandSeparator={" "}
|
thousandSeparator={" "}
|
||||||
suffix={"₽"}
|
suffix={"₽"}
|
||||||
{...form.getInputProps("overtimeRate")}
|
{...form.getInputProps("overtimeRate")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</>
|
</>
|
||||||
</BaseFormModal.Body>
|
</BaseFormModal.Body>
|
||||||
</BaseFormModal>
|
</BaseFormModal>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PayRateFormModal;
|
export default PayRateFormModal;
|
||||||
|
|||||||
@@ -1,46 +1,65 @@
|
|||||||
import {ContextModalProps} from "@mantine/modals";
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
import BaseFormModal, {
|
||||||
import {UserSchema} from "../../../../client";
|
CreateEditFormProps,
|
||||||
import {useForm} from "@mantine/form";
|
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||||
import {Checkbox, Fieldset, Input, Stack, Textarea, TextInput} from "@mantine/core";
|
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 RoleSelect from "../../components/RoleSelect/RoleSelect.tsx";
|
||||||
import PositionSelect from "../../components/PositionSelect/PositionSelect.tsx";
|
import PositionSelect from "../../components/PositionSelect/PositionSelect.tsx";
|
||||||
import {UserRoleEnum} from "../../../../shared/enums/UserRole.ts";
|
import { UserRoleEnum } from "../../../../shared/enums/UserRole.ts";
|
||||||
import {capitalize} from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import {IMaskInput} from "react-imask";
|
import { IMaskInput } from "react-imask";
|
||||||
import phone from "phone";
|
import phone from "phone";
|
||||||
import PayRateSelect from "../../../../components/Selects/PayRateSelect/PayRateSelect.tsx";
|
import PayRateSelect from "../../../../components/Selects/PayRateSelect/PayRateSelect.tsx";
|
||||||
|
|
||||||
type Props = CreateEditFormProps<UserSchema>;
|
type Props = CreateEditFormProps<UserSchema>;
|
||||||
const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
const UserFormModal = ({
|
||||||
const isEditing = 'element' in innerProps;
|
context,
|
||||||
const initialValues = isEditing ? innerProps.element : {
|
id,
|
||||||
isAdmin: false,
|
innerProps,
|
||||||
isBlocked: false,
|
}: ContextModalProps<Props>) => {
|
||||||
isDeleted: false,
|
const isEditing = "element" in innerProps;
|
||||||
comment: "",
|
const initialValues = isEditing
|
||||||
roleKey: UserRoleEnum.USER
|
? innerProps.element
|
||||||
};
|
: {
|
||||||
|
isAdmin: false,
|
||||||
|
isBlocked: false,
|
||||||
|
isDeleted: false,
|
||||||
|
comment: "",
|
||||||
|
roleKey: UserRoleEnum.USER,
|
||||||
|
};
|
||||||
|
|
||||||
const form = useForm<Partial<UserSchema>>({
|
const form = useForm<Partial<UserSchema>>({
|
||||||
initialValues: initialValues,
|
initialValues: initialValues,
|
||||||
validate: {
|
validate: {
|
||||||
firstName: value => !value?.trim() && "Укажите имя пользователя",
|
firstName: value => !value?.trim() && "Укажите имя пользователя",
|
||||||
secondName: value => !value?.trim() && "Укажите фамилию",
|
secondName: value => !value?.trim() && "Укажите фамилию",
|
||||||
position: (value, values) => ((values.role?.key === UserRoleEnum.EMPLOYEE) && (!value)) && 'Необходимо указать должность сотрудника',
|
position: (value, values) =>
|
||||||
phoneNumber: value => !phone(value || '', {
|
values.role?.key === UserRoleEnum.EMPLOYEE &&
|
||||||
country: "",
|
!value &&
|
||||||
strictDetection: false,
|
"Необходимо указать должность сотрудника",
|
||||||
validateMobilePrefix: false
|
phoneNumber: value =>
|
||||||
}).isValid && 'Неверно указан номер телефона',
|
!phone(value || "", {
|
||||||
}
|
country: "",
|
||||||
|
strictDetection: false,
|
||||||
|
validateMobilePrefix: false,
|
||||||
|
}).isValid && "Неверно указан номер телефона",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return (<BaseFormModal
|
return (
|
||||||
|
<BaseFormModal
|
||||||
form={form}
|
form={form}
|
||||||
closeOnSubmit
|
closeOnSubmit
|
||||||
onClose={() => context.closeContextModal(id)}
|
onClose={() => context.closeContextModal(id)}
|
||||||
{...innerProps}
|
{...innerProps}>
|
||||||
>
|
|
||||||
<BaseFormModal.Body>
|
<BaseFormModal.Body>
|
||||||
<>
|
<>
|
||||||
<Fieldset legend={"Общая информация"}>
|
<Fieldset legend={"Общая информация"}>
|
||||||
@@ -49,19 +68,33 @@ const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
|||||||
label={"Имя"}
|
label={"Имя"}
|
||||||
placeholder={"Введите имя пользователя"}
|
placeholder={"Введите имя пользователя"}
|
||||||
{...form.getInputProps("firstName")}
|
{...form.getInputProps("firstName")}
|
||||||
onChange={event => form.getInputProps('firstName').onChange(capitalize(event.target.value).trim())}
|
onChange={event =>
|
||||||
|
form
|
||||||
|
.getInputProps("firstName")
|
||||||
|
.onChange(
|
||||||
|
capitalize(
|
||||||
|
event.target.value
|
||||||
|
).trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
{...form.getInputProps("secondName")}
|
{...form.getInputProps("secondName")}
|
||||||
label={"Фамилия"}
|
label={"Фамилия"}
|
||||||
placeholder={"Введите фамилию пользователя"}
|
placeholder={"Введите фамилию пользователя"}
|
||||||
onChange={event => form.getInputProps('secondName').onChange(capitalize(event.target.value).trim())}
|
onChange={event =>
|
||||||
|
form
|
||||||
|
.getInputProps("secondName")
|
||||||
|
.onChange(
|
||||||
|
capitalize(
|
||||||
|
event.target.value
|
||||||
|
).trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input.Wrapper
|
<Input.Wrapper
|
||||||
label={"Номер телефона"}
|
label={"Номер телефона"}
|
||||||
error={form.getInputProps("phoneNumber").error}
|
error={form.getInputProps("phoneNumber").error}>
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
component={IMaskInput}
|
component={IMaskInput}
|
||||||
mask="+7 000 000-00-00"
|
mask="+7 000 000-00-00"
|
||||||
@@ -76,46 +109,53 @@ const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
|||||||
<RoleSelect
|
<RoleSelect
|
||||||
label={"Роль пользователя"}
|
label={"Роль пользователя"}
|
||||||
placeholder={"Выберите роль пользователя"}
|
placeholder={"Выберите роль пользователя"}
|
||||||
{...form.getInputProps('role')}
|
{...form.getInputProps("role")}
|
||||||
/>
|
/>
|
||||||
{form.values.role?.key === UserRoleEnum.EMPLOYEE &&
|
{form.values.role?.key ===
|
||||||
|
UserRoleEnum.EMPLOYEE && (
|
||||||
<>
|
<>
|
||||||
<PositionSelect
|
<PositionSelect
|
||||||
label={"Должность сотрудника"}
|
label={"Должность сотрудника"}
|
||||||
placeholder={"Выберите должность сотрудника"}
|
placeholder={
|
||||||
{...form.getInputProps('position')}
|
"Выберите должность сотрудника"
|
||||||
|
}
|
||||||
|
{...form.getInputProps("position")}
|
||||||
/>
|
/>
|
||||||
<PayRateSelect
|
<PayRateSelect
|
||||||
label={"Тариф"}
|
label={"Тариф"}
|
||||||
placeholder={"Выберите тариф сотрудника"}
|
placeholder={
|
||||||
|
"Выберите тариф сотрудника"
|
||||||
|
}
|
||||||
{...form.getInputProps("payRate")}
|
{...form.getInputProps("payRate")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
<Fieldset legend={"Дополнительные параметры"}>
|
<Fieldset legend={"Дополнительные параметры"}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={"Права администратора"}
|
label={"Права администратора"}
|
||||||
|
{...form.getInputProps("isAdmin", {
|
||||||
{...form.getInputProps('isAdmin', {type: "checkbox"})}
|
type: "checkbox",
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={"Заблокирован"}
|
label={"Заблокирован"}
|
||||||
{...form.getInputProps('isBlocked', {type: "checkbox"})}
|
{...form.getInputProps("isBlocked", {
|
||||||
|
type: "checkbox",
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<Textarea
|
<Textarea
|
||||||
label={"Дополнительная информация"}
|
label={"Дополнительная информация"}
|
||||||
{...form.getInputProps('comment')}
|
{...form.getInputProps("comment")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</>
|
</>
|
||||||
</BaseFormModal.Body>
|
</BaseFormModal.Body>
|
||||||
</BaseFormModal>
|
</BaseFormModal>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default UserFormModal;
|
export default UserFormModal;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user