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,
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"quoteProps": "consistent",
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"bracketSameLine": true,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
"singleAttributePerLine": true,
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"quoteProps": "consistent",
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"bracketSameLine": true,
|
||||
"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:
|
||||
|
||||
- [@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](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
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
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
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: ["./tsconfig.json", "./tsconfig.node.json"],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
- 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`
|
||||
- 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
|
||||
- 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`
|
||||
- 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 } },
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
];
|
||||
];
|
||||
|
||||
27
index.html
27
index.html
@@ -1,13 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icons/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DENCO: Fulfillment</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/icons/favicon.png" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0" />
|
||||
<title>DENCO: Fulfillment</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</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",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@mantine/core": "^7.11.2",
|
||||
"@mantine/dates": "^7.11.2",
|
||||
"@mantine/dropzone": "^7.11.2",
|
||||
"@mantine/form": "^7.11.2",
|
||||
"@mantine/hooks": "^7.11.2",
|
||||
"@mantine/modals": "^7.11.2",
|
||||
"@mantine/notifications": "^7.11.2",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"@tabler/icons-react": "^3.11.0",
|
||||
"@tanstack/react-query": "^5.51.9",
|
||||
"@tanstack/react-router": "^1.45.6",
|
||||
"@tanstack/router-devtools": "^1.45.6",
|
||||
"@tanstack/router-vite-plugin": "^1.45.3",
|
||||
"axios": "^1.7.2",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cyrillic-to-translit-js": "^3.2.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dot-object": "^2.1.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.3.8",
|
||||
"globals": "^15.8.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mantine-form-zod-resolver": "^1.1.0",
|
||||
"mantine-react-table": "^2.0.0-beta.5",
|
||||
"phone": "^3.1.49",
|
||||
"react": "^18.3.1",
|
||||
"react-barcode": "^1.5.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-imask": "^7.6.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-to-print": "^2.15.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.7.0",
|
||||
"@types/dot-object": "^2.1.6",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.8",
|
||||
"openapi-typescript-codegen": "^0.29.0",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss-preset-mantine": "^1.16.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^7.16.1",
|
||||
"vite": "^5.3.4",
|
||||
"yarn-upgrade-all": "^0.7.2"
|
||||
},
|
||||
"packageManager": "yarn@4.1.0"
|
||||
"name": "fulfillment-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@mantine/core": "^7.11.2",
|
||||
"@mantine/dates": "^7.11.2",
|
||||
"@mantine/dropzone": "^7.11.2",
|
||||
"@mantine/form": "^7.11.2",
|
||||
"@mantine/hooks": "^7.11.2",
|
||||
"@mantine/modals": "^7.11.2",
|
||||
"@mantine/notifications": "^7.11.2",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"@tabler/icons-react": "^3.11.0",
|
||||
"@tanstack/react-query": "^5.51.9",
|
||||
"@tanstack/react-router": "^1.45.6",
|
||||
"@tanstack/router-devtools": "^1.45.6",
|
||||
"@tanstack/router-vite-plugin": "^1.45.3",
|
||||
"axios": "^1.7.2",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cyrillic-to-translit-js": "^3.2.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dot-object": "^2.1.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.3.8",
|
||||
"globals": "^15.8.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mantine-form-zod-resolver": "^1.1.0",
|
||||
"mantine-react-table": "^2.0.0-beta.5",
|
||||
"phone": "^3.1.49",
|
||||
"react": "^18.3.1",
|
||||
"react-barcode": "^1.5.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-imask": "^7.6.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-to-print": "^2.15.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.7.0",
|
||||
"@types/dot-object": "^2.1.6",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.8",
|
||||
"openapi-typescript-codegen": "^0.29.0",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss-preset-mantine": "^1.16.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^7.16.1",
|
||||
"vite": "^5.3.4",
|
||||
"yarn-upgrade-all": "^0.7.2"
|
||||
},
|
||||
"packageManager": "yarn@4.1.0"
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
"postcss-preset-mantine": {},
|
||||
"postcss-simple-vars": {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
"mantine-breakpoint-xs": "36em",
|
||||
"mantine-breakpoint-sm": "48em",
|
||||
"mantine-breakpoint-md": "62em",
|
||||
"mantine-breakpoint-lg": "75em",
|
||||
"mantine-breakpoint-xl": "88em",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {BarcodeService} from "../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { BarcodeService } from "../../client";
|
||||
|
||||
const useGetAllBarcodeTemplates = () => {
|
||||
const {data, error, isLoading, refetch} = useQuery({
|
||||
queryKey: ['getAllBarcodeTemplates'],
|
||||
const { data, error, isLoading, refetch } = useQuery({
|
||||
queryKey: ["getAllBarcodeTemplates"],
|
||||
queryFn: () => BarcodeService.getAllBarcodeTemplates(),
|
||||
select: (data) => data.templates
|
||||
select: data => data.templates,
|
||||
});
|
||||
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 {useQuery} from "@tanstack/react-query";
|
||||
import { ProductService } from "../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export const useGetProductById = (id: number) => {
|
||||
const {data, error, isLoading, refetch} = useQuery({
|
||||
queryKey: ['getProductById', id],
|
||||
queryFn: () => ProductService.getProductById({productId: id}),
|
||||
select: (data) => data
|
||||
const { data, error, isLoading, refetch } = useQuery({
|
||||
queryKey: ["getProductById", id],
|
||||
queryFn: () => ProductService.getProductById({ productId: id }),
|
||||
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 {getRouterContext, Outlet} from "@tanstack/react-router";
|
||||
import {motion, useIsPresent} from "framer-motion";
|
||||
import {cloneDeep} from "lodash";
|
||||
import { forwardRef, useContext, useRef } from "react";
|
||||
import { getRouterContext, Outlet } from "@tanstack/react-router";
|
||||
import { motion, useIsPresent } from "framer-motion";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const RouterContext = getRouterContext();
|
||||
@@ -17,26 +17,24 @@ const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div ref={ref}
|
||||
initial={{x: "-100%"}}
|
||||
animate={{
|
||||
x: 0,
|
||||
transform: "",
|
||||
transitionEnd: {
|
||||
transform: "none"
|
||||
}
|
||||
}}
|
||||
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: "circInOut",
|
||||
}}
|
||||
|
||||
>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{
|
||||
x: 0,
|
||||
transform: "",
|
||||
transitionEnd: {
|
||||
transform: "none",
|
||||
},
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: "circInOut",
|
||||
}}>
|
||||
<RouterContext.Provider value={renderedContext.current}>
|
||||
<Outlet/>
|
||||
<Outlet />
|
||||
</RouterContext.Provider>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
export default AnimatedOutlet
|
||||
export default AnimatedOutlet;
|
||||
|
||||
@@ -4,19 +4,18 @@ import {
|
||||
MRT_RowData,
|
||||
MRT_TableInstance,
|
||||
MRT_TableOptions,
|
||||
useMantineReactTable
|
||||
useMantineReactTable,
|
||||
} from "mantine-react-table";
|
||||
import {MRT_Localization_RU} from "mantine-react-table/locales/ru/index.cjs";
|
||||
import {forwardRef, useEffect, useImperativeHandle} from 'react';
|
||||
import { MRT_Localization_RU } from "mantine-react-table/locales/ru/index.cjs";
|
||||
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
||||
|
||||
type Props<T extends Record<string, unknown>, K extends keyof T> = {
|
||||
data: T[],
|
||||
onSelectionChange?: (selectedRows: T[]) => void,
|
||||
columns: MRT_ColumnDef<T, K>[],
|
||||
restProps?: MRT_TableOptions<T>,
|
||||
striped?: boolean
|
||||
}
|
||||
|
||||
data: T[];
|
||||
onSelectionChange?: (selectedRows: T[]) => void;
|
||||
columns: MRT_ColumnDef<T, K>[];
|
||||
restProps?: MRT_TableOptions<T>;
|
||||
striped?: boolean;
|
||||
};
|
||||
|
||||
// Экспортируем тип рефа, чтобы он мог быть использован в других компонентах
|
||||
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
|
||||
// @ts-expect-error
|
||||
export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>((props, ref) => {
|
||||
const {data, columns, restProps, striped = true, onSelectionChange} = props;
|
||||
export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
restProps,
|
||||
striped = true,
|
||||
onSelectionChange,
|
||||
} = props;
|
||||
|
||||
const table = useMantineReactTable({
|
||||
localization: MRT_Localization_RU,
|
||||
enablePagination: false,
|
||||
data,
|
||||
columns,
|
||||
mantineTableProps: {
|
||||
striped: striped
|
||||
},
|
||||
enableTopToolbar: false,
|
||||
enableBottomToolbar: false,
|
||||
enableRowSelection: onSelectionChange !== undefined,
|
||||
...restProps,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!onSelectionChange) return;
|
||||
onSelectionChange(table.getSelectedRowModel().rows.map(row => row.original))
|
||||
}, [table.getState().rowSelection]);
|
||||
const table = useMantineReactTable({
|
||||
localization: MRT_Localization_RU,
|
||||
enablePagination: false,
|
||||
data,
|
||||
columns,
|
||||
mantineTableProps: {
|
||||
striped: striped,
|
||||
},
|
||||
enableTopToolbar: false,
|
||||
enableBottomToolbar: false,
|
||||
enableRowSelection: onSelectionChange !== undefined,
|
||||
...restProps,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!onSelectionChange) return;
|
||||
onSelectionChange(
|
||||
table.getSelectedRowModel().rows.map(row => row.original)
|
||||
);
|
||||
}, [table.getState().rowSelection]);
|
||||
|
||||
// Используем useImperativeHandle для определения, что будет доступно через ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
// @ts-ignore
|
||||
getTable: () => table
|
||||
}));
|
||||
// Используем useImperativeHandle для определения, что будет доступно через ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
// @ts-ignore
|
||||
getTable: () => table,
|
||||
}));
|
||||
|
||||
|
||||
return <MantineReactTable table={table}/>;
|
||||
});
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {Button, rem, Tooltip} from '@mantine/core';
|
||||
import {IconCheck, IconCopy} from '@tabler/icons-react';
|
||||
import {FC} from "react";
|
||||
import {useClipboard} from "@mantine/hooks";
|
||||
import { Button, rem, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
|
||||
type Props = {
|
||||
children: string;
|
||||
value: string
|
||||
value: string;
|
||||
onCopiedLabel: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
||||
export const ButtonCopy: FC<Props> = ({ children, onCopiedLabel, value }) => {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return (
|
||||
@@ -18,20 +18,19 @@ export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
||||
offset={5}
|
||||
position="bottom"
|
||||
radius="xl"
|
||||
transitionProps={{duration: 100, transition: 'slide-down'}}
|
||||
opened={clipboard.copied}
|
||||
>
|
||||
transitionProps={{ duration: 100, transition: "slide-down" }}
|
||||
opened={clipboard.copied}>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={
|
||||
clipboard.copied ? (
|
||||
<IconCheck
|
||||
style={{width: rem(20), height: rem(20)}}
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
) : (
|
||||
<IconCopy
|
||||
style={{width: rem(20), height: rem(20)}}
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
)
|
||||
@@ -42,13 +41,12 @@ export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
||||
root: {
|
||||
paddingRight: rem(14),
|
||||
},
|
||||
section: {marginLeft: rem(22)},
|
||||
section: { marginLeft: rem(22) },
|
||||
}}
|
||||
onClick={() => clipboard.copy(value)}
|
||||
>
|
||||
onClick={() => clipboard.copy(value)}>
|
||||
{children}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
export default ButtonCopy;
|
||||
};
|
||||
export default ButtonCopy;
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
import {Button, rem, Tooltip} from '@mantine/core';
|
||||
import {IconCheck, IconCopy} from '@tabler/icons-react';
|
||||
import {FC} from "react";
|
||||
import { Button, rem, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
children: string;
|
||||
onCopyClick: () => void;
|
||||
onCopiedLabel: string;
|
||||
copied: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopyClick, copied}) => {
|
||||
export const ButtonCopyControlled: FC<Props> = ({
|
||||
children,
|
||||
onCopiedLabel,
|
||||
onCopyClick,
|
||||
copied,
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip
|
||||
label={onCopiedLabel}
|
||||
offset={5}
|
||||
position="bottom"
|
||||
radius="xl"
|
||||
transitionProps={{duration: 100, transition: 'slide-down'}}
|
||||
opened={copied}
|
||||
>
|
||||
transitionProps={{ duration: 100, transition: "slide-down" }}
|
||||
opened={copied}>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={
|
||||
copied ? (
|
||||
<IconCheck
|
||||
style={{width: rem(20), height: rem(20)}}
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
) : (
|
||||
<IconCopy
|
||||
style={{width: rem(20), height: rem(20)}}
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
)
|
||||
@@ -40,12 +44,11 @@ export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopy
|
||||
root: {
|
||||
paddingRight: rem(14),
|
||||
},
|
||||
section: {marginLeft: rem(22)},
|
||||
section: { marginLeft: rem(22) },
|
||||
}}
|
||||
onClick={onCopyClick}
|
||||
>
|
||||
onClick={onCopyClick}>
|
||||
{children}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import {NumberInput, NumberInputProps} from "@mantine/core";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useDebouncedValue} from "@mantine/hooks";
|
||||
import {isNumber, omit} from "lodash";
|
||||
|
||||
import { NumberInput, NumberInputProps } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { isNumber, omit } from "lodash";
|
||||
|
||||
type Props = NumberInputProps;
|
||||
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.onChange) return;
|
||||
props.onChange(debounced);
|
||||
}, [debounced])
|
||||
}, [debounced]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNumber(props.value)) return;
|
||||
setValue(props.value);
|
||||
}, [props.value])
|
||||
}, [props.value]);
|
||||
|
||||
const restProps = omit(props, ["onChange", "value"])
|
||||
const restProps = omit(props, ["onChange", "value"]);
|
||||
return (
|
||||
<NumberInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
{...restProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default DebouncedNumberInput;
|
||||
export default DebouncedNumberInput;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/*background-color: green;*/
|
||||
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -34,7 +33,6 @@
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
|
||||
}
|
||||
border-radius: var(--item-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {FC} from "react";
|
||||
import styles from './Board.module.css';
|
||||
import {Divider, Text, Title} from '@mantine/core';
|
||||
import {Draggable, Droppable} from "@hello-pangea/dnd";
|
||||
import { FC } from "react";
|
||||
import styles from "./Board.module.css";
|
||||
import { Divider, Text, Title } from "@mantine/core";
|
||||
import { Draggable, Droppable } from "@hello-pangea/dnd";
|
||||
import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx";
|
||||
import {DealSummary} from "../../../client";
|
||||
import { DealSummary } from "../../../client";
|
||||
import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx";
|
||||
import classNames from "classnames";
|
||||
import {getPluralForm} from "../../../shared/lib/utils.ts";
|
||||
import {sum} from "lodash";
|
||||
import { getPluralForm } from "../../../shared/lib/utils.ts";
|
||||
import { sum } from "lodash";
|
||||
|
||||
type Props = {
|
||||
droppableId: string;
|
||||
@@ -15,66 +15,74 @@ type Props = {
|
||||
withCreateButton?: boolean;
|
||||
summaries: DealSummary[];
|
||||
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 pluralForm = getPluralForm(summaries.length, 'сделка', 'сделки', 'сделок');
|
||||
const pluralForm = getPluralForm(
|
||||
summaries.length,
|
||||
"сделка",
|
||||
"сделки",
|
||||
"сделок"
|
||||
);
|
||||
return `${summaries.length} ${pluralForm}: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}₽`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["header"]}>
|
||||
<Title size={"h4"}>{title}</Title>
|
||||
<Text>{getDealsText()}</Text>
|
||||
<Divider size={"xl"} my={10} color={color}/>
|
||||
<Divider
|
||||
size={"xl"}
|
||||
my={10}
|
||||
color={color}
|
||||
/>
|
||||
</div>
|
||||
<Droppable
|
||||
droppableId={droppableId}
|
||||
>
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}
|
||||
className={classNames(
|
||||
styles["items-list"],
|
||||
(snapshot.isDraggingOver && !snapshot.draggingFromThisWith)
|
||||
&& styles["items-list-drag-over"]
|
||||
)}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{withCreateButton &&
|
||||
<CreateDealButton
|
||||
|
||||
onClick={() => {
|
||||
}}
|
||||
/>}
|
||||
{summaries.map(summary =>
|
||||
(
|
||||
<Draggable
|
||||
draggableId={summary.id.toString()}
|
||||
index={summary.rank}
|
||||
key={summary.id}
|
||||
>
|
||||
{(provided) => (
|
||||
<div {...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
|
||||
>
|
||||
<DealSummaryCard dealSummary={summary}/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
className={classNames(
|
||||
styles["items-list"],
|
||||
snapshot.isDraggingOver &&
|
||||
!snapshot.draggingFromThisWith &&
|
||||
styles["items-list-drag-over"]
|
||||
)}
|
||||
{...provided.droppableProps}>
|
||||
{withCreateButton && (
|
||||
<CreateDealButton onClick={() => {}} />
|
||||
)}
|
||||
{summaries.map(summary => (
|
||||
<Draggable
|
||||
draggableId={summary.id.toString()}
|
||||
index={summary.rank}
|
||||
key={summary.id}>
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}>
|
||||
<DealSummaryCard
|
||||
dealSummary={summary}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Board;
|
||||
export default Board;
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.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 {Text, Transition} from '@mantine/core';
|
||||
import styles from "./CreateDealButton.module.css";
|
||||
import { Text, Transition } from "@mantine/core";
|
||||
import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
|
||||
import {DealService} from "../../../client";
|
||||
import {useQueryClient} from "@tanstack/react-query";
|
||||
import {dateWithoutTimezone} from "../../../shared/lib/date.ts";
|
||||
import { DealService } from "../../../client";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
}
|
||||
};
|
||||
const CreateDealButton: FC<Props> = () => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return (
|
||||
<div className={styles['container']}
|
||||
onClick={() => {
|
||||
if (isCreating) return;
|
||||
setIsCreating(prevState => !prevState)
|
||||
setIsTransitionEnded(false);
|
||||
}}
|
||||
>
|
||||
{(!isCreating && isTransitionEnded) &&
|
||||
<div
|
||||
className={styles["container"]}
|
||||
onClick={() => {
|
||||
if (isCreating) return;
|
||||
setIsCreating(prevState => !prevState);
|
||||
setIsTransitionEnded(false);
|
||||
}}>
|
||||
{!isCreating && isTransitionEnded && (
|
||||
<Text>Быстрое добавление</Text>
|
||||
}
|
||||
)}
|
||||
<Transition
|
||||
mounted={isCreating}
|
||||
transition={"scale-y"}
|
||||
onExited={() => setIsTransitionEnded(true)}
|
||||
|
||||
>
|
||||
{(styles) => (
|
||||
onExited={() => setIsTransitionEnded(true)}>
|
||||
{styles => (
|
||||
<div style={styles}>
|
||||
<CreateDealFrom
|
||||
onCancel={() => {
|
||||
setIsCreating(false)
|
||||
setIsCreating(false);
|
||||
}}
|
||||
onSubmit={(quickDeal) => {
|
||||
onSubmit={quickDeal => {
|
||||
DealService.quickCreateDealQuickCreatePost({
|
||||
requestBody: {
|
||||
...quickDeal,
|
||||
acceptanceDate: dateWithoutTimezone(quickDeal.acceptanceDate)
|
||||
}
|
||||
acceptanceDate: dateWithoutTimezone(
|
||||
quickDeal.acceptanceDate
|
||||
),
|
||||
},
|
||||
}).then(async () => {
|
||||
await queryClient.invalidateQueries({queryKey: ['getDealSummaries']});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["getDealSummaries"],
|
||||
});
|
||||
setIsCreating(false);
|
||||
})
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default CreateDealButton;
|
||||
);
|
||||
};
|
||||
export default CreateDealButton;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputs * {
|
||||
@@ -12,4 +12,3 @@
|
||||
display: flex;
|
||||
gap: rem(10);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ import { useForm } from "@mantine/form";
|
||||
import styles from "./CreateDealForm.module.css";
|
||||
import ClientAutocomplete from "../../Selects/ClientAutocomplete/ClientAutocomplete.tsx";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
import ShippingWarehouseAutocomplete
|
||||
from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
||||
import ShippingWarehouseAutocomplete from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
||||
import BaseMarketplaceSelect from "../../Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import ServicePriceCategorySelect from "../../Selects/ServicePriceCategorySelect/ServicePriceCategorySelect.tsx";
|
||||
|
||||
type Props = {
|
||||
onSubmit: (quickDeal: QuickDeal) => void
|
||||
onSubmit: (quickDeal: QuickDeal) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
};
|
||||
const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
const form = useForm<QuickDeal>({
|
||||
initialValues: {
|
||||
@@ -33,15 +32,15 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
return (
|
||||
<form
|
||||
style={{ width: "100%" }}
|
||||
onSubmit={form.onSubmit((values) => onSubmit(values))}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: rem(10),
|
||||
padding: rem(10),
|
||||
}}>
|
||||
onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: rem(10),
|
||||
padding: rem(10),
|
||||
}}>
|
||||
<div className={styles["inputs"]}>
|
||||
<TextInput
|
||||
placeholder={"Название сделки"}
|
||||
@@ -63,7 +62,6 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
{...form.getInputProps("shippingWarehouse")}
|
||||
placeholder={"Склад отгрузки"}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className={styles["inputs"]}>
|
||||
<ServicePriceCategorySelect
|
||||
@@ -89,20 +87,17 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={styles["buttons"]}>
|
||||
<Button
|
||||
type={"submit"}
|
||||
>Добавить</Button>
|
||||
<Button type={"submit"}>Добавить</Button>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => onCancel()}
|
||||
>Отменить</Button>
|
||||
onClick={() => onCancel()}>
|
||||
Отменить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDealFrom;
|
||||
export default CreateDealFrom;
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.flex-row-left {
|
||||
@@ -27,13 +26,11 @@
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-row-right {
|
||||
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,147 @@
|
||||
import {FC} from "react";
|
||||
import {DealService, DealSummary} from "../../../client";
|
||||
import styles from './DealSummaryCard.module.css';
|
||||
import { FC } from "react";
|
||||
import { DealService, DealSummary } from "../../../client";
|
||||
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 {useDealPageContext} from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCheck} from "@fortawesome/free-solid-svg-icons";
|
||||
import { useDealPageContext } from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
type Props = {
|
||||
dealSummary: DealSummary
|
||||
}
|
||||
dealSummary: DealSummary;
|
||||
};
|
||||
|
||||
const DealSummaryCard: FC<Props> = ({dealSummary}) => {
|
||||
const {setSelectedDeal} = useDealPageContext();
|
||||
const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
const onDealSummaryClick = () => {
|
||||
DealService.getDealById({dealId: dealSummary.id})
|
||||
.then((deal) => {
|
||||
setSelectedDeal(deal);
|
||||
})
|
||||
}
|
||||
DealService.getDealById({ dealId: dealSummary.id }).then(deal => {
|
||||
setSelectedDeal(deal);
|
||||
});
|
||||
};
|
||||
const getDeadlineTextColor = (deadline: string) => {
|
||||
// generate three colors, yellow for 1 day, red for 0 days, green for more than 1 day
|
||||
const deadlineDate = new Date(deadline);
|
||||
const currentDate = new Date();
|
||||
const diff = deadlineDate.getTime() - currentDate.getTime();
|
||||
const diffDays = Math.ceil(diff / (1000 * 3600 * 24));
|
||||
if (diffDays < 0)
|
||||
return 'red.8'; // for past deadlines
|
||||
if (diffDays < 0) return "red.8"; // for past deadlines
|
||||
if (diffDays === 1) {
|
||||
return 'yellow.8';
|
||||
return "yellow.8";
|
||||
}
|
||||
if (diffDays === 0) {
|
||||
return 'orange.8';
|
||||
return "orange.8";
|
||||
}
|
||||
return 'green.8';
|
||||
}
|
||||
return "green.8";
|
||||
};
|
||||
return (
|
||||
<div onClick={() => onDealSummaryClick()} className={styles['container']}>
|
||||
<div className={classNames(styles['flex-row'], styles["flex-row-left"])}>
|
||||
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={"gray.6"}>
|
||||
<div
|
||||
onClick={() => onDealSummaryClick()}
|
||||
className={styles["container"]}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles["flex-row"],
|
||||
styles["flex-row-left"]
|
||||
)}>
|
||||
<div className={styles["flex-item"]}>
|
||||
<Text
|
||||
size={"sm"}
|
||||
c={"gray.6"}>
|
||||
Клиент: {dealSummary.clientName}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"md"} c={"blue.5"}>{dealSummary.name}</Text>
|
||||
{dealSummary.shipmentWarehouseName &&
|
||||
<Text size={"sm"} c={"gray.6"}>{dealSummary.shipmentWarehouseName}</Text>
|
||||
}
|
||||
<div className={styles["flex-item"]}>
|
||||
<Text
|
||||
size={"md"}
|
||||
c={"blue.5"}>
|
||||
{dealSummary.name}
|
||||
</Text>
|
||||
{dealSummary.shipmentWarehouseName && (
|
||||
<Text
|
||||
size={"sm"}
|
||||
c={"gray.6"}>
|
||||
{dealSummary.shipmentWarehouseName}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={"gray.6"}>
|
||||
{dealSummary.totalPrice.toLocaleString('ru-RU')} руб, {dealSummary.totalProducts.toLocaleString("ru-RU")} тов.
|
||||
<div className={styles["flex-item"]}>
|
||||
<Text
|
||||
size={"sm"}
|
||||
c={"gray.6"}>
|
||||
{dealSummary.totalPrice.toLocaleString("ru-RU")} руб,{" "}
|
||||
{dealSummary.totalProducts.toLocaleString("ru-RU")} тов.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
className={classNames(styles['flex-row'], styles['flex-row-right'])}>
|
||||
<div className={styles['flex-item']}>
|
||||
className={classNames(
|
||||
styles["flex-row"],
|
||||
styles["flex-row-right"]
|
||||
)}>
|
||||
<div className={styles["flex-item"]}>
|
||||
<ActionIcon variant={"transparent"}>
|
||||
<Image src={dealSummary.baseMarketplace?.iconUrl || ""}/>
|
||||
<Image
|
||||
src={dealSummary.baseMarketplace?.iconUrl || ""}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={getDeadlineTextColor(dealSummary.deadline)}>
|
||||
{new Date(dealSummary.deadline).toLocaleString('ru-RU').slice(0, -3)}
|
||||
<div className={styles["flex-item"]}>
|
||||
<Text
|
||||
size={"sm"}
|
||||
c={getDeadlineTextColor(dealSummary.deadline)}>
|
||||
{new Date(dealSummary.deadline)
|
||||
.toLocaleString("ru-RU")
|
||||
.slice(0, -3)}
|
||||
</Text>
|
||||
</div>
|
||||
<CopyButton value={"https://google.com"}>
|
||||
{({copy, copied}) => (
|
||||
<Popover opened={copied} withArrow>
|
||||
{({ copy, copied }) => (
|
||||
<Popover
|
||||
opened={copied}
|
||||
withArrow>
|
||||
<Popover.Target>
|
||||
<div
|
||||
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
copy();
|
||||
}}
|
||||
className={styles['flex-item']}>
|
||||
<Badge variant={"light"} radius={"sm"}>
|
||||
className={styles["flex-item"]}>
|
||||
<Badge
|
||||
variant={"light"}
|
||||
radius={"sm"}>
|
||||
ID: {dealSummary.id}
|
||||
</Badge>
|
||||
</div>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Flex justify={"center"} align={"center"} gap={rem(5)}>
|
||||
<Flex
|
||||
justify={"center"}
|
||||
align={"center"}
|
||||
gap={rem(5)}>
|
||||
<FontAwesomeIcon
|
||||
bounce
|
||||
style={{animationIterationCount: 1}}
|
||||
style={{ animationIterationCount: 1 }}
|
||||
icon={faCheck}
|
||||
/>
|
||||
<Text size={"xs"}>ID сделки скопирован</Text>
|
||||
<Text size={"xs"}>
|
||||
ID сделки скопирован
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)}
|
||||
</CopyButton>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
export default DealSummaryCard;
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -16,4 +20,4 @@
|
||||
font-size: rem(30px);
|
||||
font-weight: 700;
|
||||
width: rem(60px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import styles from './Header.module.css';
|
||||
import {Button, TextInput} from "@mantine/core";
|
||||
import {FC} from "react";
|
||||
|
||||
const Header: FC = ()=>{
|
||||
import styles from "./Header.module.css";
|
||||
import { Button, TextInput } from "@mantine/core";
|
||||
import { FC } from "react";
|
||||
|
||||
const Header: FC = () => {
|
||||
return (
|
||||
<div className={styles['header']}>
|
||||
|
||||
<div className={styles["header"]}>
|
||||
<TextInput
|
||||
radius={0}
|
||||
placeholder={"Поиск и фильтры"}
|
||||
size={"xl"}
|
||||
className={styles['header-input']}
|
||||
className={styles["header-input"]}
|
||||
/>
|
||||
<Button
|
||||
radius={0}
|
||||
color={"gray"}
|
||||
variant={'default'}
|
||||
className={styles['header-button']}
|
||||
>Поиск</Button>
|
||||
variant={"default"}
|
||||
className={styles["header-button"]}>
|
||||
Поиск
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
||||
@@ -1,31 +1,45 @@
|
||||
import {Dropzone, DropzoneProps, FileWithPath} from "@mantine/dropzone";
|
||||
import {FC, useState} from "react";
|
||||
import {Button, Fieldset, Flex, Group, 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";
|
||||
import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
|
||||
import { FC, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Fieldset,
|
||||
Flex,
|
||||
Group,
|
||||
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 {
|
||||
imageUrlInputProps?: BaseFormInputProps<string>;
|
||||
productId?: number;
|
||||
}
|
||||
|
||||
|
||||
type Props = Omit<DropzoneProps, 'onDrop'> & RestProps;
|
||||
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
|
||||
|
||||
const ImageDropzone: FC<Props> = (props: Props) => {
|
||||
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 restProps = omit(props, ['imageUrl', 'productId', 'imageUrlInputProps']);
|
||||
const restProps = omit(props, [
|
||||
"imageUrl",
|
||||
"productId",
|
||||
"imageUrlInputProps",
|
||||
]);
|
||||
const onDrop = (files: FileWithPath[]) => {
|
||||
if (!props.productId || !props.imageUrlInputProps) return;
|
||||
if (files.length > 1) {
|
||||
notifications.error({message: "Прикрепите одно изображение"});
|
||||
notifications.error({ message: "Прикрепите одно изображение" });
|
||||
return;
|
||||
}
|
||||
const file = files[0];
|
||||
@@ -34,33 +48,35 @@ const ImageDropzone: FC<Props> = (props: Props) => {
|
||||
ProductService.uploadProductImage({
|
||||
productId: props.productId,
|
||||
formData: {
|
||||
upload_file: file
|
||||
}
|
||||
}).then(({ok, message, imageUrl}) => {
|
||||
notifications.guess(ok, {message});
|
||||
setIsLoading(false);
|
||||
upload_file: file,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message, imageUrl }) => {
|
||||
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);
|
||||
|
||||
return;
|
||||
}
|
||||
props.imageUrlInputProps?.onChange(imageUrl);
|
||||
setShowDropzone(false);
|
||||
}).catch(error => {
|
||||
notifications.error({message: error.toString()});
|
||||
setShowDropzone(true);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
const getBody = () => {
|
||||
return props.imageUrlInputProps?.value && !showDropzone ? (
|
||||
<Image src={props.imageUrlInputProps.value}/>
|
||||
<Image src={props.imageUrlInputProps.value} />
|
||||
) : (
|
||||
|
||||
<Dropzone
|
||||
{...restProps}
|
||||
accept={["image/png",
|
||||
accept={[
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/bmp",
|
||||
@@ -68,58 +84,74 @@ const ImageDropzone: FC<Props> = (props: Props) => {
|
||||
"image/x-icon",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"image/heic"]}
|
||||
"image/heic",
|
||||
]}
|
||||
multiple={false}
|
||||
onDrop={onDrop}
|
||||
|
||||
>
|
||||
<Group justify="center" gap="xl" style={{pointerEvents: 'none'}}>
|
||||
onDrop={onDrop}>
|
||||
<Group
|
||||
justify="center"
|
||||
gap="xl"
|
||||
style={{ pointerEvents: "none" }}>
|
||||
<Dropzone.Accept>
|
||||
<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}
|
||||
/>
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<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}
|
||||
/>
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<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}
|
||||
/>
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div style={{textAlign: "center"}}>
|
||||
<Text size="xl" inline>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Text
|
||||
size="xl"
|
||||
inline>
|
||||
Перенесите изображение или нажмите чтоб выбрать файл
|
||||
</Text>
|
||||
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}
|
||||
|
||||
>
|
||||
<Fieldset
|
||||
legend={'Изображение'}>
|
||||
direction={"column"}>
|
||||
<Fieldset legend={"Изображение"}>
|
||||
<Flex justify={"center"}>
|
||||
{isLoading ? <Loader/> : getBody()}
|
||||
{isLoading ? <Loader /> : getBody()}
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
{!showDropzone &&
|
||||
<Button onClick={() => setShowDropzone(true)} variant={"default"}>Заменить изображение {}</Button>
|
||||
}
|
||||
{!showDropzone && (
|
||||
<Button
|
||||
onClick={() => setShowDropzone(true)}
|
||||
variant={"default"}>
|
||||
Заменить изображение {}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageDropzone;
|
||||
export default ImageDropzone;
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
.control {
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
color: var(--mantine-color-text);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
color: var(--mantine-color-text);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
}
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-7)
|
||||
);
|
||||
color: light-dark(
|
||||
var(--mantine-color-black),
|
||||
var(--mantine-color-dark-0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
margin-left: var(--mantine-spacing-xl);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
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));
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
margin-left: var(--mantine-spacing-xl);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
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));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
}
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-7)
|
||||
);
|
||||
color: light-dark(
|
||||
var(--mantine-color-black),
|
||||
var(--mantine-color-dark-0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import {useState} from 'react';
|
||||
import {Box, Collapse, 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";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
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 {
|
||||
icon: React.FC<any>;
|
||||
@@ -11,28 +18,36 @@ interface LinksGroupProps {
|
||||
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 [opened, setOpened] = useState(initiallyOpened || false);
|
||||
const items = (hasLinks ? links : []).map((link) => (
|
||||
<Link to={link.link}
|
||||
className={classes.link}
|
||||
key={link.label}
|
||||
|
||||
>
|
||||
|
||||
const items = (hasLinks ? links : []).map(link => (
|
||||
<Link
|
||||
to={link.link}
|
||||
className={classes.link}
|
||||
key={link.label}>
|
||||
{link.label}
|
||||
</Link>
|
||||
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnstyledButton onClick={() => setOpened((o) => !o)} className={classes.control}>
|
||||
<Group justify="space-between" gap={0}>
|
||||
<Box style={{display: 'flex', alignItems: 'center'}}>
|
||||
<ThemeIcon variant="light" size={30}>
|
||||
<Icon style={{width: rem(18), height: rem(18)}}/>
|
||||
<UnstyledButton
|
||||
onClick={() => setOpened(o => !o)}
|
||||
className={classes.control}>
|
||||
<Group
|
||||
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>
|
||||
<Box ml="md">{label}</Box>
|
||||
</Box>
|
||||
@@ -43,7 +58,7 @@ export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGro
|
||||
style={{
|
||||
width: 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 = {
|
||||
label: 'Releases',
|
||||
label: "Releases",
|
||||
icon: IconCalendarStats,
|
||||
links: [
|
||||
{label: 'Upcoming releases', link: '/'},
|
||||
{label: 'Previous releases', link: '/'},
|
||||
{label: 'Releases schedule', link: '/'},
|
||||
{ label: "Upcoming releases", link: "/" },
|
||||
{ label: "Previous releases", link: "/" },
|
||||
{ label: "Releases schedule", link: "/" },
|
||||
],
|
||||
};
|
||||
|
||||
export function NavbarLinksGroup() {
|
||||
return (
|
||||
<Box mih={220} p="md">
|
||||
<Box
|
||||
mih={220}
|
||||
p="md">
|
||||
<LinksGroup {...mockdata} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg {...props} version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1024.000000 1024.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<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
|
||||
<svg
|
||||
{...props}
|
||||
version="1.0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1024.000000 1024.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<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
|
||||
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
|
||||
@@ -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
|
||||
-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
|
||||
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"/>
|
||||
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"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@
|
||||
align-items: center;
|
||||
@mixin dark {
|
||||
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 {
|
||||
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);
|
||||
width: 100%;
|
||||
@@ -32,7 +36,10 @@
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
|
||||
&: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] {
|
||||
@@ -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 {
|
||||
IconBarcode,
|
||||
IconBox, IconBuildingWarehouse,
|
||||
IconBox,
|
||||
IconBuildingWarehouse,
|
||||
IconCash,
|
||||
IconDashboard,
|
||||
IconFileBarcode,
|
||||
IconHome2,
|
||||
IconLogout,
|
||||
IconMan,
|
||||
IconMoon, IconShoppingCart,
|
||||
IconMoon,
|
||||
IconShoppingCart,
|
||||
IconSun,
|
||||
} from '@tabler/icons-react';
|
||||
import classes from './Navbar.module.css';
|
||||
import {useAppDispatch} from "../../redux/store.ts";
|
||||
import {logout} from "../../features/authSlice.ts";
|
||||
import {useNavigate, useRouterState} from "@tanstack/react-router";
|
||||
} from "@tabler/icons-react";
|
||||
import classes from "./Navbar.module.css";
|
||||
import { useAppDispatch } from "../../redux/store.ts";
|
||||
import { logout } from "../../features/authSlice.ts";
|
||||
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
|
||||
interface NavbarLinkProps {
|
||||
icon: typeof IconHome2;
|
||||
@@ -28,24 +39,31 @@ interface NavbarLinkProps {
|
||||
}
|
||||
|
||||
function NavbarLink(props: NavbarLinkProps) {
|
||||
const {icon: Icon, label, active, onClick} = props;
|
||||
const { icon: Icon, label, active, onClick } = props;
|
||||
return (
|
||||
<Tooltip display={!label ? "none" : "flex"} label={label} position="right" 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}/>
|
||||
<Tooltip
|
||||
display={!label ? "none" : "flex"}
|
||||
label={label}
|
||||
position="right"
|
||||
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>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const mockdata = [
|
||||
|
||||
{
|
||||
icon: IconCash,
|
||||
label: 'Сделки',
|
||||
href: '/leads'
|
||||
label: "Сделки",
|
||||
href: "/leads",
|
||||
},
|
||||
// {
|
||||
// icon: IconTable,
|
||||
@@ -54,48 +72,50 @@ const mockdata = [
|
||||
// },
|
||||
{
|
||||
icon: IconMan,
|
||||
label: 'Клиенты',
|
||||
href: '/clients'
|
||||
label: "Клиенты",
|
||||
href: "/clients",
|
||||
},
|
||||
{
|
||||
icon: IconBox,
|
||||
label: 'Услуги',
|
||||
href: '/services'
|
||||
label: "Услуги",
|
||||
href: "/services",
|
||||
},
|
||||
{
|
||||
icon: IconBarcode,
|
||||
label: 'Товары',
|
||||
href: '/products'
|
||||
label: "Товары",
|
||||
href: "/products",
|
||||
},
|
||||
{
|
||||
icon: IconFileBarcode,
|
||||
label: 'Штрихкоды',
|
||||
href: '/barcode'
|
||||
label: "Штрихкоды",
|
||||
href: "/barcode",
|
||||
},
|
||||
{
|
||||
icon: IconBuildingWarehouse,
|
||||
label: 'Склады отгрузки',
|
||||
href: '/shipping_warehouses'
|
||||
label: "Склады отгрузки",
|
||||
href: "/shipping_warehouses",
|
||||
},
|
||||
{
|
||||
icon:IconShoppingCart,
|
||||
label: 'Маркетплейсы',
|
||||
href: '/marketplaces'
|
||||
}
|
||||
icon: IconShoppingCart,
|
||||
label: "Маркетплейсы",
|
||||
href: "/marketplaces",
|
||||
},
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouterState();
|
||||
const {colorScheme, toggleColorScheme} = useMantineColorScheme({keepTransitions: true});
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme({
|
||||
keepTransitions: true,
|
||||
});
|
||||
const onLogoutClick = () => {
|
||||
dispatch(logout());
|
||||
navigate({to: '/login'});
|
||||
}
|
||||
navigate({ to: "/login" });
|
||||
};
|
||||
const onNavlinkClick = (props: NavbarLinkProps) => {
|
||||
navigate({to: props.href});
|
||||
}
|
||||
navigate({ to: props.href });
|
||||
};
|
||||
const links = mockdata.map((link, index) => (
|
||||
<NavbarLink
|
||||
{...link}
|
||||
@@ -108,35 +128,62 @@ export function Navbar() {
|
||||
|
||||
return (
|
||||
<nav className={classes.navbar}>
|
||||
<Flex direction={"column"} gap={rem(30)}>
|
||||
<Center
|
||||
p={rem(5)}
|
||||
>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(30)}>
|
||||
<Center p={rem(5)}>
|
||||
<Image
|
||||
flex={1}
|
||||
// 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>
|
||||
|
||||
<div className={classes.navbarMain}>
|
||||
<Stack justify="center" gap={rem(10)}>
|
||||
<Stack
|
||||
justify="center"
|
||||
gap={rem(10)}>
|
||||
{links}
|
||||
</Stack>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<Stack w={"100%"} justify="center" gap={0}>
|
||||
<NavbarLink icon={IconDashboard}
|
||||
href={"/admin"}
|
||||
index={-1}
|
||||
label={"Панель администратора"}
|
||||
onClick={() => onNavlinkClick({href: "/admin", index: -1, icon: IconDashboard})}
|
||||
<Stack
|
||||
w={"100%"}
|
||||
justify="center"
|
||||
gap={0}>
|
||||
<NavbarLink
|
||||
icon={IconDashboard}
|
||||
href={"/admin"}
|
||||
index={-1}
|
||||
label={"Панель администратора"}
|
||||
onClick={() =>
|
||||
onNavlinkClick({
|
||||
href: "/admin",
|
||||
index: -1,
|
||||
icon: IconDashboard,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavbarLink
|
||||
label={"Сменить тему"}
|
||||
onClick={toggleColorScheme}
|
||||
icon={colorScheme == "dark" ? IconSun : IconMoon}
|
||||
href={"#"}
|
||||
index={-1}
|
||||
/>
|
||||
<NavbarLink
|
||||
index={-1}
|
||||
href={"#"}
|
||||
onClick={onLogoutClick}
|
||||
icon={IconLogout}
|
||||
label="Выйти"
|
||||
/>
|
||||
<NavbarLink label={"Сменить тему"} onClick={toggleColorScheme}
|
||||
icon={colorScheme == "dark" ? IconSun : IconMoon} href={"#"} index={-1}/>
|
||||
<NavbarLink index={-1} href={"#"} onClick={onLogoutClick} icon={IconLogout} label="Выйти"/>
|
||||
</Stack>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
.user {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
}
|
||||
@mixin hover {
|
||||
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 { IconChevronRight } from '@tabler/icons-react';
|
||||
import classes from './UserButton.module.css';
|
||||
import { UnstyledButton, Group, Avatar, Text, rem } from "@mantine/core";
|
||||
import { IconChevronRight } from "@tabler/icons-react";
|
||||
import classes from "./UserButton.module.css";
|
||||
|
||||
export function UserButton() {
|
||||
return (
|
||||
@@ -12,17 +12,24 @@ export function UserButton() {
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}>
|
||||
Harriette Spoonlicker
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="xs">
|
||||
hspoonlicker@outlook.com
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} />
|
||||
<IconChevronRight
|
||||
style={{ width: rem(14), height: rem(14) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import {IconAdjustments, IconGauge} from "@tabler/icons-react";
|
||||
import { IconAdjustments, IconGauge } from "@tabler/icons-react";
|
||||
|
||||
export const NavbarLinks = [
|
||||
{
|
||||
label: 'Главная',
|
||||
label: "Главная",
|
||||
icon: IconGauge,
|
||||
links: [
|
||||
{
|
||||
label: "123",
|
||||
link: "/login"
|
||||
link: "/login",
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Настройки',
|
||||
label: "Настройки",
|
||||
icon: IconAdjustments,
|
||||
links:[
|
||||
links: [
|
||||
{
|
||||
label: "Профиль"
|
||||
}
|
||||
]
|
||||
label: "Профиль",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
import {Autocomplete, AutocompleteProps} from "@mantine/core";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {ObjectWithNameAndId} from "../../types/utils.ts";
|
||||
import {omit} from "lodash";
|
||||
|
||||
import { Autocomplete, AutocompleteProps } from "@mantine/core";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ObjectWithNameAndId } from "../../types/utils.ts";
|
||||
import { omit } from "lodash";
|
||||
|
||||
export type AutocompleteObjectType<T extends ObjectWithNameAndId> = T;
|
||||
|
||||
type ControlledValueProps<T extends ObjectWithNameAndId> = {
|
||||
value: AutocompleteObjectType<T>,
|
||||
value: AutocompleteObjectType<T>;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type RestProps<T extends ObjectWithNameAndId> = {
|
||||
defaultValue?: AutocompleteObjectType<T>
|
||||
defaultValue?: AutocompleteObjectType<T>;
|
||||
onChange: (value: string) => void;
|
||||
data: AutocompleteObjectType<T>[];
|
||||
filterBy?: (item: AutocompleteObjectType<T>) => boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type ObjectAutocompleteProps<T extends ObjectWithNameAndId> =
|
||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
||||
& Omit<AutocompleteProps, 'value' | 'onChange' | 'data'>;
|
||||
(RestProps<T> & Partial<ControlledValueProps<T>>) &
|
||||
Omit<AutocompleteProps, "value" | "onChange" | "data">;
|
||||
|
||||
const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutocompleteProps<T>) => {
|
||||
|
||||
const isControlled = 'value' in props;
|
||||
const [internalValue, setInternalValue] = useState<undefined | string>(props.defaultValue);
|
||||
const ObjectAutocomplete = <T extends ObjectWithNameAndId>(
|
||||
props: ObjectAutocompleteProps<T>
|
||||
) => {
|
||||
const isControlled = "value" in props;
|
||||
const [internalValue, setInternalValue] = useState<undefined | string>(
|
||||
props.defaultValue
|
||||
);
|
||||
|
||||
const value = isControlled ? props.value?.name : internalValue;
|
||||
|
||||
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 => ({
|
||||
label: item.name,
|
||||
value: item.id.toString()
|
||||
value: item.id.toString(),
|
||||
}));
|
||||
}, [props.data]);
|
||||
|
||||
@@ -45,13 +49,13 @@ const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutoco
|
||||
return;
|
||||
}
|
||||
setInternalValue(event);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled || !internalValue) return;
|
||||
props.onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
const restProps = omit(props, ['filterBy', 'groupBy']);
|
||||
const restProps = omit(props, ["filterBy", "groupBy"]);
|
||||
return (
|
||||
<Autocomplete
|
||||
{...restProps}
|
||||
@@ -59,7 +63,7 @@ const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutoco
|
||||
onChange={handleOnChange}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectAutocomplete;
|
||||
export default ObjectAutocomplete;
|
||||
|
||||
@@ -1,83 +1,91 @@
|
||||
import {MultiSelect, MultiSelectProps} from "@mantine/core";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {groupBy} from "lodash";
|
||||
import { MultiSelect, MultiSelectProps } from "@mantine/core";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { groupBy } from "lodash";
|
||||
|
||||
interface ObjectWithIdAndName {
|
||||
id: number,
|
||||
name: string
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type MultiselectObjectType<T> = T;
|
||||
|
||||
type ControlledValueProps<T> = {
|
||||
value: MultiselectObjectType<T>[],
|
||||
value: MultiselectObjectType<T>[];
|
||||
onChange: (value: MultiselectObjectType<T>[]) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type CustomLabelAndKeyProps<T> = {
|
||||
getLabelFn: (item: MultiselectObjectType<T>) => string;
|
||||
getValueFn: (item: MultiselectObjectType<T>) => string;
|
||||
}
|
||||
};
|
||||
type RestProps<T> = {
|
||||
defaultValue?: MultiselectObjectType<T>[]
|
||||
defaultValue?: MultiselectObjectType<T>[];
|
||||
onChange: (value: MultiselectObjectType<T>[]) => void;
|
||||
data: MultiselectObjectType<T>[];
|
||||
groupBy?: (item: MultiselectObjectType<T>) => string;
|
||||
filterBy?: (item: MultiselectObjectType<T>) => boolean;
|
||||
}
|
||||
};
|
||||
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||
return item.name;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||
return item.id.toString();
|
||||
}
|
||||
export type ObjectMultiSelectProps<T> =
|
||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
||||
& Omit<MultiSelectProps, 'value' | 'onChange' | 'data'>
|
||||
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>);
|
||||
};
|
||||
export type ObjectMultiSelectProps<T> = (RestProps<T> &
|
||||
Partial<ControlledValueProps<T>>) &
|
||||
Omit<MultiSelectProps, "value" | "onChange" | "data"> &
|
||||
(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 haveGetValueFn = 'getValueFn' in props;
|
||||
const haveGetLabelFn = 'getLabelFn' in props;
|
||||
|
||||
const [internalValue, setInternalValue] = useState<MultiselectObjectType<T>[] | undefined>(props.defaultValue);
|
||||
const [internalValue, setInternalValue] = useState<
|
||||
MultiselectObjectType<T>[] | undefined
|
||||
>(props.defaultValue);
|
||||
|
||||
const value = (isControlled ? props.value : internalValue) || [];
|
||||
|
||||
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
const getValueFn =
|
||||
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn =
|
||||
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
|
||||
const propsData = props.filterBy
|
||||
? props.data.filter(props.filterBy)
|
||||
: props.data;
|
||||
if (props.groupBy) {
|
||||
|
||||
const groupedData = groupBy(propsData, props.groupBy);
|
||||
return Object.entries(groupedData).map(([group, items]) => ({
|
||||
group,
|
||||
items: items.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item)
|
||||
}))
|
||||
value: getValueFn(item),
|
||||
})),
|
||||
}));
|
||||
} else {
|
||||
return propsData.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item)
|
||||
value: getValueFn(item),
|
||||
}));
|
||||
}
|
||||
}, [props.data, props.groupBy]);
|
||||
|
||||
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) {
|
||||
props.onChange(objects);
|
||||
return;
|
||||
}
|
||||
setInternalValue(objects);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isControlled || !internalValue) return;
|
||||
props.onChange(internalValue);
|
||||
@@ -90,7 +98,7 @@ const ObjectMultiSelect = <T, >(props: ObjectMultiSelectProps<T>) => {
|
||||
onChange={handleOnChange}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectMultiSelect;
|
||||
export default ObjectMultiSelect;
|
||||
|
||||
@@ -1,73 +1,77 @@
|
||||
import {Select, SelectProps} from "@mantine/core";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {groupBy, omit} from "lodash";
|
||||
import { Select, SelectProps } from "@mantine/core";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { groupBy, omit } from "lodash";
|
||||
|
||||
interface ObjectWithIdAndName {
|
||||
id: number,
|
||||
name: string
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type SelectObjectType<T> = T;
|
||||
|
||||
type ControlledValueProps<T> = {
|
||||
value: SelectObjectType<T>,
|
||||
value: SelectObjectType<T>;
|
||||
onChange: (value: SelectObjectType<T>) => void;
|
||||
}
|
||||
};
|
||||
type CustomLabelAndKeyProps<T> = {
|
||||
getLabelFn: (item: SelectObjectType<T>) => string;
|
||||
getValueFn: (item: SelectObjectType<T>) => string;
|
||||
}
|
||||
};
|
||||
|
||||
type RestProps<T> = {
|
||||
defaultValue?: SelectObjectType<T>
|
||||
defaultValue?: SelectObjectType<T>;
|
||||
onChange: (value: SelectObjectType<T>) => void;
|
||||
data: SelectObjectType<T>[];
|
||||
groupBy?: (item: SelectObjectType<T>) => string;
|
||||
filterBy?: (item: SelectObjectType<T>) => boolean;
|
||||
};
|
||||
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||
|
||||
return item.name;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||
if (!item) return item;
|
||||
return item.id.toString();
|
||||
}
|
||||
export type ObjectSelectProps<T> =
|
||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
||||
& Omit<SelectProps, 'value' | 'onChange' | 'data'>
|
||||
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>)
|
||||
};
|
||||
export type ObjectSelectProps<T> = (RestProps<T> &
|
||||
Partial<ControlledValueProps<T>>) &
|
||||
Omit<SelectProps, "value" | "onChange" | "data"> &
|
||||
(T extends ObjectWithIdAndName
|
||||
? Partial<CustomLabelAndKeyProps<T>>
|
||||
: CustomLabelAndKeyProps<T>);
|
||||
|
||||
const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
||||
|
||||
const isControlled = 'value' in props;
|
||||
const haveGetValueFn = 'getValueFn' in props;
|
||||
const haveGetLabelFn = 'getLabelFn' in props;
|
||||
const [internalValue, setInternalValue] = useState<SelectObjectType<T> | undefined>(props.defaultValue);
|
||||
const ObjectSelect = <T,>(props: ObjectSelectProps<T>) => {
|
||||
const isControlled = "value" in props;
|
||||
const haveGetValueFn = "getValueFn" in props;
|
||||
const haveGetLabelFn = "getLabelFn" in props;
|
||||
const [internalValue, setInternalValue] = useState<
|
||||
SelectObjectType<T> | undefined
|
||||
>(props.defaultValue);
|
||||
|
||||
const value = isControlled ? props.value : internalValue;
|
||||
|
||||
|
||||
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
const getValueFn =
|
||||
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn =
|
||||
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
|
||||
const propsData = props.filterBy
|
||||
? props.data.filter(props.filterBy)
|
||||
: props.data;
|
||||
if (props.groupBy) {
|
||||
|
||||
const groupedData = groupBy(propsData, props.groupBy);
|
||||
return Object.entries(groupedData).map(([group, items]) => ({
|
||||
group,
|
||||
items: items.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item)
|
||||
}))
|
||||
value: getValueFn(item),
|
||||
})),
|
||||
}));
|
||||
} else {
|
||||
return propsData.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item)
|
||||
value: getValueFn(item),
|
||||
}));
|
||||
}
|
||||
}, [props.data, props.groupBy]);
|
||||
@@ -81,14 +85,19 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
||||
return;
|
||||
}
|
||||
setInternalValue(object);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled || !internalValue) return;
|
||||
props.onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
|
||||
const restProps = omit(props, ['filterBy', 'groupBy', 'getValueFn', 'getLabelFn']);
|
||||
const restProps = omit(props, [
|
||||
"filterBy",
|
||||
"groupBy",
|
||||
"getValueFn",
|
||||
"getLabelFn",
|
||||
]);
|
||||
return (
|
||||
<Select
|
||||
{...restProps}
|
||||
@@ -96,7 +105,7 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
||||
onChange={handleOnChange}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectSelect;
|
||||
export default ObjectSelect;
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
background-color: #f9f9f9;
|
||||
@mixin dark {
|
||||
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 {
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
@@ -23,5 +26,4 @@
|
||||
|
||||
.container-full-height-fixed {
|
||||
height: calc(100vh - (rem(20) * 2));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import {CSSProperties, FC, ReactNode} from "react";
|
||||
import styles from './PageBlock.module.css';
|
||||
import { CSSProperties, FC, ReactNode } from "react";
|
||||
import styles from "./PageBlock.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
children: ReactNode;
|
||||
fluid?: boolean;
|
||||
style?: CSSProperties;
|
||||
fullHeight?: 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 (
|
||||
<div style={style}
|
||||
className={
|
||||
classNames(
|
||||
styles['container'],
|
||||
fluid && styles['container-fluid'],
|
||||
fullHeight && styles['container-full-height'],
|
||||
fullHeightFixed && styles['container-full-height-fixed']
|
||||
)
|
||||
}>
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
styles["container"],
|
||||
fluid && styles["container-fluid"],
|
||||
fullHeight && styles["container-full-height"],
|
||||
fullHeightFixed && styles["container-full-height-fixed"]
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default PageBlock;
|
||||
);
|
||||
};
|
||||
export default PageBlock;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.number-input {
|
||||
width: rem(50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {ActionIcon, Flex, NumberInput, rem} from "@mantine/core";
|
||||
import {IconMinus, IconPlus} from "@tabler/icons-react";
|
||||
import styles from './PlusMinusInput.module.css';
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import { ActionIcon, Flex, NumberInput, rem } from "@mantine/core";
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react";
|
||||
import styles from "./PlusMinusInput.module.css";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
type ControlledValueProps = {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type RestProps = {
|
||||
defaultValue?: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = RestProps & Partial<ControlledValueProps>;
|
||||
|
||||
@@ -22,7 +22,6 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
const value = isControlled ? props.value : internalValue;
|
||||
|
||||
const onMinusClick = () => {
|
||||
|
||||
const newValue = (value || 0) - 1;
|
||||
if (newValue < 0) {
|
||||
return;
|
||||
@@ -32,7 +31,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
} else {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPlusClick = () => {
|
||||
const newValue = (value || 0) + 1;
|
||||
@@ -41,7 +40,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
} else {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event: number | string) => {
|
||||
let newValue = typeof event === "string" ? 0 : event;
|
||||
@@ -53,7 +52,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
} else {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isControlled) {
|
||||
@@ -64,34 +63,33 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<Flex
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
<ActionIcon
|
||||
disabled={value === 0}
|
||||
onClick={onMinusClick}
|
||||
variant={"default"}>
|
||||
<IconMinus/>
|
||||
<IconMinus />
|
||||
</ActionIcon>
|
||||
<NumberInput
|
||||
min={0}
|
||||
styles={{
|
||||
input: {
|
||||
textAlign: "center"
|
||||
}
|
||||
textAlign: "center",
|
||||
},
|
||||
}}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
value={value}
|
||||
className={styles['number-input']}
|
||||
onChange={(event) => handleInputChange(event)}
|
||||
className={styles["number-input"]}
|
||||
onChange={event => handleInputChange(event)}
|
||||
/>
|
||||
<ActionIcon
|
||||
onClick={onPlusClick}
|
||||
variant={"default"}>
|
||||
<IconPlus/>
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PlusMinusInput;
|
||||
export default PlusMinusInput;
|
||||
|
||||
@@ -1,81 +1,113 @@
|
||||
import {ProductSchema} from "../../client";
|
||||
import {FC, useState} from "react";
|
||||
import { ProductSchema } from "../../client";
|
||||
import { FC, useState } from "react";
|
||||
import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx";
|
||||
import {omit} from "lodash";
|
||||
import ObjectSelect, {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx";
|
||||
import {ComboboxItem, Image, Loader, OptionsFilter, rem, SelectProps, Text, Tooltip} from "@mantine/core";
|
||||
import {getProductFields} from "../../types/utils.ts";
|
||||
import {useDebouncedValue} from "@mantine/hooks";
|
||||
import { omit } from "lodash";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../ObjectSelect/ObjectSelect.tsx";
|
||||
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 = {
|
||||
clientId: number;
|
||||
}
|
||||
};
|
||||
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 [searchValue, setSearchValue] = useState("");
|
||||
const [debounced] = useDebouncedValue(searchValue, 500);
|
||||
const {products, isLoading} = useProductsList({
|
||||
const { products, isLoading } = useProductsList({
|
||||
clientId: props.clientId,
|
||||
searchInput: debounced,
|
||||
page: 0,
|
||||
itemsPerPage: MAX_PRODUCTS
|
||||
itemsPerPage: MAX_PRODUCTS,
|
||||
});
|
||||
const restProps = omit(props, ['clientId']);
|
||||
const renderOption: SelectProps['renderOption'] = (item) => {
|
||||
const product = products.find(product => product.id == parseInt(item.option.value));
|
||||
const restProps = omit(props, ["clientId"]);
|
||||
const renderOption: SelectProps["renderOption"] = item => {
|
||||
const product = products.find(
|
||||
product => product.id == parseInt(item.option.value)
|
||||
);
|
||||
if (!product) return item.option.label;
|
||||
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 (
|
||||
<Tooltip
|
||||
style={{whiteSpace: "pre-line"}}
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
multiline
|
||||
disabled={productFields.length === 0}
|
||||
label={
|
||||
<>
|
||||
{productFields.map(([key, value]) => {
|
||||
return `${key.toString()}: ${value.toString()}`;
|
||||
}).join('\n')}
|
||||
{imageUrl && <Image
|
||||
src={imageUrl}
|
||||
alt={product.name}
|
||||
maw={rem(250)}
|
||||
/>}
|
||||
{productFields
|
||||
.map(([key, value]) => {
|
||||
return `${key.toString()}: ${value.toString()}`;
|
||||
})
|
||||
.join("\n")}
|
||||
{imageUrl && (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={product.name}
|
||||
maw={rem(250)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}>
|
||||
<div>
|
||||
{product.name}<br/>
|
||||
{product.barcodes && <Text size={"xs"}>{product.barcodes[0]}</Text>}
|
||||
{product.name}
|
||||
<br />
|
||||
{product.barcodes && (
|
||||
<Text size={"xs"}>{product.barcodes[0]}</Text>
|
||||
)}
|
||||
</div>
|
||||
</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();
|
||||
}
|
||||
</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()
|
||||
);
|
||||
});
|
||||
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 names = products.map(product => product.name);
|
||||
if (names.includes(value)) return;
|
||||
setSearchValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
// disabled={isLoading}
|
||||
rightSection={
|
||||
(isLoading || searchValue !== debounced) ?
|
||||
<Loader size={"sm"}/> : null
|
||||
isLoading || searchValue !== debounced ? (
|
||||
<Loader size={"sm"} />
|
||||
) : null
|
||||
}
|
||||
onSearchChange={setSearchValueImpl}
|
||||
renderOption={renderOption}
|
||||
@@ -84,8 +116,8 @@ const ProductSelect: FC<Props> = (props: Props) => {
|
||||
data={products}
|
||||
filter={optionsFilter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
export default ProductSelect;
|
||||
// type ControlledValueProps = {
|
||||
// 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 {BarcodeTemplateSchema} from "../../../client";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { BarcodeTemplateSchema } from "../../../client";
|
||||
import useGetAllBarcodeTemplates from "../../../api/barcode/useGetAllBarcodeTemplates.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<BarcodeTemplateSchema>, 'data'>;
|
||||
type Props = Omit<ObjectSelectProps<BarcodeTemplateSchema>, "data">;
|
||||
|
||||
const BarcodeTemplateSelect = (props: Props) => {
|
||||
const {barcodeTemplates} = useGetAllBarcodeTemplates();
|
||||
const { barcodeTemplates } = useGetAllBarcodeTemplates();
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={barcodeTemplates}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodeTemplateSelect;
|
||||
export default BarcodeTemplateSelect;
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
import {BaseEnumListSchema, type CancelablePromise} from "../../../client";
|
||||
import {FC, useEffect, useMemo, useState} from "react";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {Select, SelectProps} from "@mantine/core";
|
||||
import {omit} from "lodash";
|
||||
import { BaseEnumListSchema, type CancelablePromise } from "../../../client";
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Select, SelectProps } from "@mantine/core";
|
||||
import { omit } from "lodash";
|
||||
|
||||
type ControlledValueProps = {
|
||||
value: number,
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
type RestProps = {
|
||||
defaultValue?: number;
|
||||
onChange: (value: number) => void;
|
||||
fetchFn: () => CancelablePromise<BaseEnumListSchema>;
|
||||
queryKey: string;
|
||||
}
|
||||
export type BaseEnumSelectProps =
|
||||
(RestProps & Partial<ControlledValueProps>)
|
||||
& Omit<SelectProps, 'value' | 'onChange' | 'data' | 'defaultValue'>;
|
||||
};
|
||||
export type BaseEnumSelectProps = (RestProps & Partial<ControlledValueProps>) &
|
||||
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 {data: queryData = []} = useQuery({
|
||||
const BaseEnumSelect: FC<BaseEnumSelectProps> = (
|
||||
props: BaseEnumSelectProps
|
||||
) => {
|
||||
const { data: queryData = [] } = useQuery({
|
||||
queryKey: [props.queryKey],
|
||||
queryFn: props.fetchFn,
|
||||
select: data => data.items || []
|
||||
})
|
||||
const isControlled = 'value' in props;
|
||||
const [internalValue, setInternalValue] = useState<number | undefined>(props.defaultValue);
|
||||
select: data => data.items || [],
|
||||
});
|
||||
const isControlled = "value" in props;
|
||||
const [internalValue, setInternalValue] = useState<number | undefined>(
|
||||
props.defaultValue
|
||||
);
|
||||
const value = isControlled ? props.value : internalValue;
|
||||
const selectData = useMemo(() => queryData.reduce((acc, item) => {
|
||||
acc.push({
|
||||
label: item.name,
|
||||
value: item.id.toString()
|
||||
});
|
||||
return acc;
|
||||
}, [] as { label: string, value: string }[]), [queryData]);
|
||||
const selectData = useMemo(
|
||||
() =>
|
||||
queryData.reduce(
|
||||
(acc, item) => {
|
||||
acc.push({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
[] as { label: string; value: string }[]
|
||||
),
|
||||
[queryData]
|
||||
);
|
||||
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());
|
||||
if (!object) return;
|
||||
if (isControlled) {
|
||||
@@ -47,22 +56,23 @@ const BaseEnumSelect: FC<BaseEnumSelectProps> = (props: BaseEnumSelectProps) =>
|
||||
return;
|
||||
}
|
||||
setInternalValue(parseInt(event));
|
||||
}
|
||||
const restProps = omit(props, ['fetchFn', 'queryKey'])
|
||||
};
|
||||
const restProps = omit(props, ["fetchFn", "queryKey"]);
|
||||
useEffect(() => {
|
||||
if (isControlled || typeof internalValue === 'undefined') return;
|
||||
if (isControlled || typeof internalValue === "undefined") return;
|
||||
props.onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
return (
|
||||
<Select
|
||||
|
||||
{...restProps}
|
||||
defaultValue={props.defaultValue ? props.defaultValue.toString() : undefined}
|
||||
defaultValue={
|
||||
props.defaultValue ? props.defaultValue.toString() : undefined
|
||||
}
|
||||
value={value?.toString()}
|
||||
onChange={handleOnChange}
|
||||
data={selectData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseEnumSelect;
|
||||
export default BaseEnumSelect;
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {BaseMarketplaceSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { BaseMarketplaceSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
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 {objects: baseMarketplaces} = useBaseMarketplacesList();
|
||||
const BaseMarketplaceSelect: FC<Props> = props => {
|
||||
const { objects: baseMarketplaces } = useBaseMarketplacesList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
renderOption={(baseMarketplace) =>
|
||||
renderOption={baseMarketplace => (
|
||||
<>
|
||||
<ActionIcon radius={"md"} variant={"transparent"}>
|
||||
<ActionIcon
|
||||
radius={"md"}
|
||||
variant={"transparent"}>
|
||||
<Image
|
||||
src={baseMarketplaces.find(el => baseMarketplace.option.value === el.key)?.iconUrl || ""}/>
|
||||
src={
|
||||
baseMarketplaces.find(
|
||||
el =>
|
||||
baseMarketplace.option.value === el.key
|
||||
)?.iconUrl || ""
|
||||
}
|
||||
/>
|
||||
</ActionIcon>
|
||||
{baseMarketplace.option.label}
|
||||
</>
|
||||
|
||||
}
|
||||
getValueFn={(baseMarketplace) => baseMarketplace.key}
|
||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
||||
)}
|
||||
getValueFn={baseMarketplace => baseMarketplace.key}
|
||||
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||
data={baseMarketplaces}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default BaseMarketplaceSelect
|
||||
);
|
||||
};
|
||||
export default BaseMarketplaceSelect;
|
||||
|
||||
@@ -1,54 +1,63 @@
|
||||
import {useDebouncedValue} from "@mantine/hooks";
|
||||
import {Autocomplete, AutocompleteProps, TextInputProps} from "@mantine/core";
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {Client} from "../../../types/Client.ts";
|
||||
import {ClientService} from "../../../client";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { Autocomplete, AutocompleteProps, TextInputProps } from "@mantine/core";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { Client } from "../../../types/Client.ts";
|
||||
import { ClientService } from "../../../client";
|
||||
|
||||
type Props = {
|
||||
onSelect?: (client: Client) => void;
|
||||
withAddress?: boolean;
|
||||
nameRestProps?: AutocompleteProps;
|
||||
addressRestProps?: TextInputProps;
|
||||
}
|
||||
const ClientAutocomplete: FC<Props> = ({onSelect, addressRestProps, nameRestProps, withAddress = false}) => {
|
||||
const [value, setValue] = useState('');
|
||||
};
|
||||
const ClientAutocomplete: FC<Props> = ({
|
||||
onSelect,
|
||||
addressRestProps,
|
||||
nameRestProps,
|
||||
withAddress = false,
|
||||
}) => {
|
||||
const [value, setValue] = useState("");
|
||||
const [debouncedValue] = useDebouncedValue(value, 200);
|
||||
|
||||
// const [isLoading, setIsLoading] = useState(false);
|
||||
const [clients, setClients] = useState<Client[]>([])
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [selectedClient, selectClient] = useState<Client>();
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setClients([]);
|
||||
setValue(value);
|
||||
}
|
||||
};
|
||||
const handleDebouncedChange = () => {
|
||||
if (!value.trim()) return;
|
||||
// setIsLoading(true);
|
||||
ClientService.searchClients({name: value}).then(({clients}) => {
|
||||
ClientService.searchClients({ name: value }).then(({ clients }) => {
|
||||
setClients(clients);
|
||||
// setIsLoading(false);
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleDebouncedChange();
|
||||
}, [debouncedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
selectClient(clients.find(client =>
|
||||
client.name.toLowerCase().trim() == value.toLowerCase().trim())
|
||||
||
|
||||
{
|
||||
selectClient(
|
||||
clients.find(
|
||||
client =>
|
||||
client.name.toLowerCase().trim() ==
|
||||
value.toLowerCase().trim()
|
||||
) || {
|
||||
name: value,
|
||||
id: -1,
|
||||
address: ""
|
||||
});
|
||||
address: "",
|
||||
}
|
||||
);
|
||||
}, [value]);
|
||||
useEffect(() => {
|
||||
if (!selectedClient) return;
|
||||
if (onSelect) onSelect(selectedClient);
|
||||
if (nameRestProps?.onChange) nameRestProps.onChange(selectedClient.name);
|
||||
if (nameRestProps?.onChange)
|
||||
nameRestProps.onChange(selectedClient.name);
|
||||
if (addressRestProps?.onChange) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
@@ -59,25 +68,22 @@ const ClientAutocomplete: FC<Props> = ({onSelect, addressRestProps, nameRestProp
|
||||
<>
|
||||
<Autocomplete
|
||||
{...nameRestProps}
|
||||
placeholder={'Клиент'}
|
||||
placeholder={"Клиент"}
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
data={clients.map(client => client.name)}
|
||||
styles={withAddress ? {
|
||||
input: {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0
|
||||
}
|
||||
} : {}}
|
||||
styles={
|
||||
withAddress
|
||||
? {
|
||||
input: {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
export default ClientAutocomplete;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
import {FC} from "react";
|
||||
import {Select} from "@mantine/core";
|
||||
import {ClientSchema} from "../../../client";
|
||||
import { FC } from "react";
|
||||
import { Select } from "@mantine/core";
|
||||
import { ClientSchema } from "../../../client";
|
||||
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
value?: ClientSchema;
|
||||
onChange: (client: ClientSchema) => void;
|
||||
withLabel?: boolean;
|
||||
}
|
||||
const ClientSelect: FC<Props> = ({value, onChange, withLabel = false}) => {
|
||||
const {clients} = useClientsList();
|
||||
const options = clients.map(client => ({label: client.name, value: client.id.toString()}))
|
||||
};
|
||||
const ClientSelect: FC<Props> = ({ value, onChange, withLabel = false }) => {
|
||||
const { clients } = useClientsList();
|
||||
const options = clients.map(client => ({
|
||||
label: client.name,
|
||||
value: client.id.toString(),
|
||||
}));
|
||||
return (
|
||||
<Select
|
||||
searchable
|
||||
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 => {
|
||||
if (!event) return;
|
||||
const client = clients.find(client => client.id == parseInt(event));
|
||||
const client = clients.find(
|
||||
client => client.id == parseInt(event)
|
||||
);
|
||||
if (!client) return;
|
||||
onChange(client);
|
||||
}}
|
||||
data={options}
|
||||
label={withLabel && "Клиент"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default ClientSelect;
|
||||
);
|
||||
};
|
||||
export default ClientSelect;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {ClientSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { ClientSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
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 {clients} = useClientsList();
|
||||
const ClientSelectNew: FC<Props> = props => {
|
||||
const { clients } = useClientsList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
searchable
|
||||
data={clients}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default ClientSelectNew;
|
||||
);
|
||||
};
|
||||
export default ClientSelectNew;
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {PayRateSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { PayRateSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
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 {objects: payRates} = usePayRatesList();
|
||||
const PayRateSelect: FC<Props> = props => {
|
||||
const { objects: payRates } = usePayRatesList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
getValueFn={(baseMarketplace) => baseMarketplace.id.toLocaleString()}
|
||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
||||
getValueFn={baseMarketplace => baseMarketplace.id.toLocaleString()}
|
||||
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||
data={payRates}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default PayRateSelect;
|
||||
);
|
||||
};
|
||||
export default PayRateSelect;
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {PayrollSchemeSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { PayrollSchemeSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
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 {objects: payrollSchemeSchemas} = usePayrollSchemasList();
|
||||
const PayrollSchemeSelect: FC<Props> = props => {
|
||||
const { objects: payrollSchemeSchemas } = usePayrollSchemasList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
getValueFn={(baseMarketplace) => baseMarketplace.key}
|
||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
||||
getValueFn={baseMarketplace => baseMarketplace.key}
|
||||
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||
data={payrollSchemeSchemas}
|
||||
{...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 useServicePriceCategoriesList from "../../../pages/ServicesPage/hooks/useServicePriceCategoriesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">
|
||||
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">;
|
||||
|
||||
const ServicePriceCategorySelect = (props: Props) => {
|
||||
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 ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {ServiceSchema} from "../../../client";
|
||||
import { FC } from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { ServiceSchema } from "../../../client";
|
||||
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
|
||||
import {omit} from "lodash";
|
||||
import {ServiceType} from "../../../shared/enums/ServiceType.ts";
|
||||
import {ComboboxItem, OptionsFilter} from "@mantine/core";
|
||||
import { omit } from "lodash";
|
||||
import { ServiceType } from "../../../shared/enums/ServiceType.ts";
|
||||
import { ComboboxItem, OptionsFilter } from "@mantine/core";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @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 = {
|
||||
filterType?: ServiceType;
|
||||
}
|
||||
type Props = Omit<ObjectSelectProps<ServiceSchema>, 'data'> & RestProps;
|
||||
};
|
||||
type Props = Omit<ObjectSelectProps<ServiceSchema>, "data"> & RestProps;
|
||||
const ServiceSelectNew: FC<Props> = (props: Props) => {
|
||||
const {services} = useServicesList();
|
||||
const data = props.filterType ? services.filter(service => service.serviceType === props.filterType) : services;
|
||||
const { services } = useServicesList();
|
||||
const data = props.filterType
|
||||
? services.filter(service => service.serviceType === props.filterType)
|
||||
: services;
|
||||
|
||||
const restProps = omit(props, ['filterType']);
|
||||
const optionsFilter: OptionsFilter = ({options, search}) => {
|
||||
return (options as ComboboxParsedItemGroup<ComboboxItem>[]).map((option) => {
|
||||
const restProps = omit(props, ["filterType"]);
|
||||
const optionsFilter: OptionsFilter = ({ options, search }) => {
|
||||
return (options as ComboboxParsedItemGroup<ComboboxItem>[]).map(
|
||||
option => {
|
||||
return {
|
||||
...option,
|
||||
items:
|
||||
option.items.filter((item: ComboboxItem) => item.label.toLowerCase().includes(search.toLowerCase()))
|
||||
}
|
||||
items: option.items.filter((item: ComboboxItem) =>
|
||||
item.label.toLowerCase().includes(search.toLowerCase())
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -37,6 +42,6 @@ const ServiceSelectNew: FC<Props> = (props: Props) => {
|
||||
groupBy={item => item.category.name}
|
||||
filter={optionsFilter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default ServiceSelectNew;
|
||||
);
|
||||
};
|
||||
export default ServiceSelectNew;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {GetServiceKitSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { GetServiceKitSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import useServicesKitsList from "../../../pages/ServicesPage/hooks/useServicesKitsList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<GetServiceKitSchema>, 'data'>
|
||||
const ServicesKitSelect: FC<Props> = (props) => {
|
||||
const {objects} = useServicesKitsList();
|
||||
type Props = Omit<ObjectSelectProps<GetServiceKitSchema>, "data">;
|
||||
const ServicesKitSelect: FC<Props> = props => {
|
||||
const { objects } = useServicesKitsList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={objects}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicesKitSelect;
|
||||
export default ServicesKitSelect;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import ObjectMultiSelect, {ObjectMultiSelectProps} from "../../ObjectMultiSelect/ObjectMultiSelect.tsx";
|
||||
import {ServiceSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectMultiSelect, {
|
||||
ObjectMultiSelectProps,
|
||||
} from "../../ObjectMultiSelect/ObjectMultiSelect.tsx";
|
||||
import { ServiceSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
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 {services} = useServicesList();
|
||||
const { services } = useServicesList();
|
||||
return (
|
||||
<ObjectMultiSelect
|
||||
data={services}
|
||||
{...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 {FC} from "react";
|
||||
import {ShippingWarehouseSchema} from "../../../client";
|
||||
import { FC } from "react";
|
||||
import { ShippingWarehouseSchema } from "../../../client";
|
||||
|
||||
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, 'data'>;
|
||||
const ShippingWarehouseAutocomplete: FC<Props> = (props) => {
|
||||
const {shippingWarehouses} = useShippingWarehousesList();
|
||||
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, "data">;
|
||||
const ShippingWarehouseAutocomplete: FC<Props> = props => {
|
||||
const { shippingWarehouses } = useShippingWarehousesList();
|
||||
return (
|
||||
<ObjectAutocomplete
|
||||
{...props}
|
||||
data={shippingWarehouses}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ShippingWarehouseAutocomplete;
|
||||
export default ShippingWarehouseAutocomplete;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {ShippingWarehouseService} from "../../../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ShippingWarehouseService } from "../../../../client";
|
||||
|
||||
const useShippingWarehousesList = () => {
|
||||
const {isPending, error, data, refetch} = useQuery({
|
||||
queryKey: ['getAllShippingWarehouses'],
|
||||
queryFn: ShippingWarehouseService.getAllShippingWarehouses
|
||||
const { isPending, error, data, refetch } = useQuery({
|
||||
queryKey: ["getAllShippingWarehouses"],
|
||||
queryFn: ShippingWarehouseService.getAllShippingWarehouses,
|
||||
});
|
||||
const shippingWarehouses = isPending || error || !data ? [] : data.shippingWarehouses;
|
||||
const shippingWarehouses =
|
||||
isPending || error || !data ? [] : data.shippingWarehouses;
|
||||
|
||||
return {shippingWarehouses, refetch}
|
||||
}
|
||||
export default useShippingWarehousesList;
|
||||
return { shippingWarehouses, refetch };
|
||||
};
|
||||
export default useShippingWarehousesList;
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {UserSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { UserSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import useUsersList from "../../../pages/AdminPage/hooks/useUsersList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<UserSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
||||
const UserSelect: FC<Props> = (props) => {
|
||||
const {objects: users} = useUsersList();
|
||||
return (<ObjectSelect
|
||||
data={users}
|
||||
getLabelFn={(user) => `${user.firstName} ${user.secondName}`}
|
||||
getValueFn={(user) => user.id.toString()}
|
||||
{...props}
|
||||
/>)
|
||||
}
|
||||
export default UserSelect;
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<UserSchema>,
|
||||
"data" | "getValueFn" | "getLabelFn"
|
||||
>;
|
||||
const UserSelect: FC<Props> = props => {
|
||||
const { objects: users } = useUsersList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={users}
|
||||
getLabelFn={user => `${user.firstName} ${user.secondName}`}
|
||||
getValueFn={user => user.id.toString()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default UserSelect;
|
||||
|
||||
@@ -1,48 +1,66 @@
|
||||
import {ServiceSchema} from "../../client";
|
||||
import {FC, useEffect, useMemo, useState} from "react";
|
||||
import {Select, SelectProps} from "@mantine/core";
|
||||
import { ServiceSchema } from "../../client";
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import { Select, SelectProps } from "@mantine/core";
|
||||
import useServicesList from "../../pages/ServicesPage/hooks/useServicesList.tsx";
|
||||
|
||||
type ControlledValueProps = {
|
||||
value: ServiceSchema;
|
||||
onChange: (value: ServiceSchema) => void;
|
||||
}
|
||||
};
|
||||
type RestProps = {
|
||||
defaultValue?: ServiceSchema;
|
||||
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 [internalValue, setInternalValue] = useState<ServiceSchema | undefined>(props.defaultValue);
|
||||
const [internalValue, setInternalValue] = useState<
|
||||
ServiceSchema | undefined
|
||||
>(props.defaultValue);
|
||||
const value = isControlled ? props.value : internalValue;
|
||||
const {services} = useServicesList();
|
||||
const categories = useMemo(() => services.reduce((acc, service) => {
|
||||
if (!acc.includes(service.category.name)) {
|
||||
acc.push(service.category.name);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]), [services]);
|
||||
|
||||
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 { services } = useServicesList();
|
||||
const categories = useMemo(
|
||||
() =>
|
||||
services.reduce((acc, service) => {
|
||||
if (!acc.includes(service.category.name)) {
|
||||
acc.push(service.category.name);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]),
|
||||
[services]
|
||||
);
|
||||
|
||||
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) => {
|
||||
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;
|
||||
}
|
||||
setInternalValue(services.find(service => service.id.toString() === value) as ServiceSchema);
|
||||
}
|
||||
setInternalValue(
|
||||
services.find(
|
||||
service => service.id.toString() === value
|
||||
) as ServiceSchema
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!isControlled) {
|
||||
props.onChange(internalValue as ServiceSchema);
|
||||
@@ -57,7 +75,7 @@ const ServiceSelect: FC<Props> = (props) => {
|
||||
onChange={event => event && handleOnChange(event)}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceSelect;
|
||||
export default ServiceSelect;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx";
|
||||
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 ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx";
|
||||
import { ServiceType } from "../../shared/enums/ServiceType.ts";
|
||||
@@ -9,38 +15,47 @@ type ServiceProps = Omit<ObjectSelectProps<ServiceSchema>, "data">;
|
||||
type PriceProps = NumberInputProps;
|
||||
|
||||
type Props = {
|
||||
serviceProps: ServiceProps,
|
||||
priceProps: PriceProps,
|
||||
quantity: number,
|
||||
containerProps: FlexProps,
|
||||
filterType?: ServiceType,
|
||||
lockOnEdit?: boolean
|
||||
category?: ServicePriceCategorySchema
|
||||
}
|
||||
serviceProps: ServiceProps;
|
||||
priceProps: PriceProps;
|
||||
quantity: number;
|
||||
containerProps: FlexProps;
|
||||
filterType?: ServiceType;
|
||||
lockOnEdit?: boolean;
|
||||
category?: ServicePriceCategorySchema;
|
||||
};
|
||||
const ServiceWithPriceInput: FC<Props> = ({
|
||||
serviceProps,
|
||||
priceProps,
|
||||
quantity,
|
||||
containerProps,
|
||||
filterType = ServiceType.PRODUCT_SERVICE,
|
||||
lockOnEdit = true,
|
||||
category,
|
||||
}) => {
|
||||
serviceProps,
|
||||
priceProps,
|
||||
quantity,
|
||||
containerProps,
|
||||
filterType = ServiceType.PRODUCT_SERVICE,
|
||||
lockOnEdit = true,
|
||||
category,
|
||||
}) => {
|
||||
const [price, setPrice] = useState<number | undefined>(
|
||||
typeof priceProps.value === "number" ? priceProps.value : undefined);
|
||||
const [service, setService] = useState<ServiceSchema | undefined>(serviceProps.value);
|
||||
typeof priceProps.value === "number" ? priceProps.value : undefined
|
||||
);
|
||||
const [service, setService] = useState<ServiceSchema | undefined>(
|
||||
serviceProps.value
|
||||
);
|
||||
const isFirstRender = useRef(true);
|
||||
const setPriceBasedOnQuantity = (): boolean => {
|
||||
if (!service || !service.priceRanges.length) return false;
|
||||
const range = service.priceRanges.find(priceRange =>
|
||||
quantity >= priceRange.fromQuantity && quantity <= priceRange.toQuantity) || service.priceRanges[0];
|
||||
const range =
|
||||
service.priceRanges.find(
|
||||
priceRange =>
|
||||
quantity >= priceRange.fromQuantity &&
|
||||
quantity <= priceRange.toQuantity
|
||||
) || service.priceRanges[0];
|
||||
|
||||
setPrice(range.price);
|
||||
return true;
|
||||
};
|
||||
const setPriceBasedOnCategory = () => {
|
||||
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;
|
||||
setPrice(categoryPrice.price);
|
||||
return true;
|
||||
@@ -84,14 +99,11 @@ const ServiceWithPriceInput: FC<Props> = ({
|
||||
isFirstRender.current = false;
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
{...containerProps}
|
||||
>
|
||||
|
||||
{...containerProps}>
|
||||
{/*<ActionIcon variant={"default"}>*/}
|
||||
{/*<IconReload onClick={() => onReload()}/>*/}
|
||||
{/*</ActionIcon>*/}
|
||||
@@ -108,9 +120,8 @@ const ServiceWithPriceInput: FC<Props> = ({
|
||||
// value={price}
|
||||
defaultValue={priceProps.value}
|
||||
/>
|
||||
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceWithPriceInput;
|
||||
export default ServiceWithPriceInput;
|
||||
|
||||
@@ -1,90 +1,93 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import PropTypes from "prop-types";
|
||||
import React, { useRef, useEffect } from "react";
|
||||
|
||||
export interface TelegramUser {
|
||||
id: number
|
||||
first_name: string
|
||||
username: string
|
||||
photo_url: string
|
||||
auth_date: number
|
||||
hash: string
|
||||
id: number;
|
||||
first_name: string;
|
||||
username: string;
|
||||
photo_url: string;
|
||||
auth_date: number;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
botName: string
|
||||
usePic?: boolean
|
||||
className?: string
|
||||
cornerRadius?: number
|
||||
requestAccess?: boolean
|
||||
dataAuthUrl?: string
|
||||
dataOnauth?: (user: TelegramUser) => void
|
||||
buttonSize?: 'large' | 'medium' | 'small'
|
||||
wrapperProps?: React.HTMLProps<HTMLDivElement>
|
||||
botName: string;
|
||||
usePic?: boolean;
|
||||
className?: string;
|
||||
cornerRadius?: number;
|
||||
requestAccess?: boolean;
|
||||
dataAuthUrl?: string;
|
||||
dataOnauth?: (user: TelegramUser) => void;
|
||||
buttonSize?: "large" | "medium" | "small";
|
||||
wrapperProps?: React.HTMLProps<HTMLDivElement>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
TelegramLoginWidget: {
|
||||
dataOnauth: (user: TelegramUser) => void
|
||||
}
|
||||
dataOnauth: (user: TelegramUser) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const TelegramLoginButton: React.FC<Props> = ({
|
||||
wrapperProps,
|
||||
dataAuthUrl,
|
||||
usePic = false,
|
||||
botName,
|
||||
className,
|
||||
buttonSize = 'large',
|
||||
dataOnauth,
|
||||
cornerRadius,
|
||||
requestAccess = true
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
wrapperProps,
|
||||
dataAuthUrl,
|
||||
usePic = false,
|
||||
botName,
|
||||
className,
|
||||
buttonSize = "large",
|
||||
dataOnauth,
|
||||
cornerRadius,
|
||||
requestAccess = true,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current === null) return
|
||||
if (ref.current === null) return;
|
||||
|
||||
if (
|
||||
typeof dataOnauth === 'undefined' &&
|
||||
typeof dataAuthUrl === 'undefined'
|
||||
typeof dataOnauth === "undefined" &&
|
||||
typeof dataAuthUrl === "undefined"
|
||||
) {
|
||||
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 = {
|
||||
dataOnauth: (user: TelegramUser) => dataOnauth(user)
|
||||
}
|
||||
dataOnauth: (user: TelegramUser) => dataOnauth(user),
|
||||
};
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://telegram.org/js/telegram-widget.js?22'
|
||||
script.setAttribute('data-telegram-login', botName)
|
||||
script.setAttribute('data-size', buttonSize)
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://telegram.org/js/telegram-widget.js?22";
|
||||
script.setAttribute("data-telegram-login", botName);
|
||||
script.setAttribute("data-size", buttonSize);
|
||||
|
||||
if (cornerRadius !== undefined) {
|
||||
script.setAttribute('data-radius', cornerRadius.toString())
|
||||
script.setAttribute("data-radius", cornerRadius.toString());
|
||||
}
|
||||
|
||||
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') {
|
||||
script.setAttribute('data-auth-url', dataAuthUrl)
|
||||
if (typeof dataAuthUrl === "string") {
|
||||
script.setAttribute("data-auth-url", dataAuthUrl);
|
||||
} 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,
|
||||
buttonSize,
|
||||
@@ -93,11 +96,17 @@ const TelegramLoginButton: React.FC<Props> = ({
|
||||
requestAccess,
|
||||
usePic,
|
||||
ref,
|
||||
dataAuthUrl
|
||||
])
|
||||
dataAuthUrl,
|
||||
]);
|
||||
|
||||
return <div ref={ref} className={className} {...wrapperProps} />
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
{...wrapperProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TelegramLoginButton.propTypes = {
|
||||
botName: PropTypes.string.isRequired,
|
||||
@@ -108,7 +117,7 @@ TelegramLoginButton.propTypes = {
|
||||
wrapperProps: PropTypes.object,
|
||||
dataOnauth: PropTypes.func,
|
||||
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 {Text} from '@mantine/core';
|
||||
import {useListState} from '@mantine/hooks';
|
||||
import {DragDropContext, Droppable, Draggable} from '@hello-pangea/dnd';
|
||||
import classes from './DndList.module.css';
|
||||
|
||||
import cx from "clsx";
|
||||
import { Text } from "@mantine/core";
|
||||
import { useListState } from "@mantine/hooks";
|
||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||
import classes from "./DndList.module.css";
|
||||
|
||||
const data = [
|
||||
{position: 6, mass: 12.011, symbol: 'C', name: 'Carbon'},
|
||||
{position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen'},
|
||||
{position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium'},
|
||||
{position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium'},
|
||||
{position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium'},
|
||||
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
||||
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
||||
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
||||
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
||||
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
||||
];
|
||||
|
||||
export function DndList() {
|
||||
const [state, handlers] = useListState(data);
|
||||
|
||||
const items = (listIndex: number) => state.map((item, index) => (
|
||||
<Draggable key={item.symbol + `${listIndex}`} index={index} draggableId={item.symbol + `${listIndex}`}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={cx(classes.item, {[classes.itemDragging]: snapshot.isDragging})}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<Text className={classes.symbol}>{item.symbol}</Text>
|
||||
<div>
|
||||
<Text>{item.name}</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
Position: {item.position} • Mass: {item.mass}
|
||||
</Text>
|
||||
const items = (listIndex: number) =>
|
||||
state.map((item, index) => (
|
||||
<Draggable
|
||||
key={item.symbol + `${listIndex}`}
|
||||
index={index}
|
||||
draggableId={item.symbol + `${listIndex}`}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={cx(classes.item, {
|
||||
[classes.itemDragging]: snapshot.isDragging,
|
||||
})}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}>
|
||||
<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>
|
||||
)}
|
||||
</Draggable>
|
||||
));
|
||||
)}
|
||||
</Draggable>
|
||||
));
|
||||
|
||||
return (
|
||||
<DragDropContext
|
||||
onDragEnd={({destination, source}) =>
|
||||
handlers.reorder({from: source.index, to: destination?.index || 0})
|
||||
}
|
||||
>
|
||||
<Droppable droppableId="dnd-list" direction="vertical">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
onDragEnd={({ destination, source }) =>
|
||||
handlers.reorder({
|
||||
from: source.index,
|
||||
to: destination?.index || 0,
|
||||
})
|
||||
}>
|
||||
<Droppable
|
||||
droppableId="dnd-list"
|
||||
direction="vertical">
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}>
|
||||
{items(1)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
<Droppable droppableId="dnd-list-2" direction="vertical">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<Droppable
|
||||
droppableId="dnd-list-2"
|
||||
direction="vertical">
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}>
|
||||
{items(2)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
@@ -61,4 +77,4 @@ export function DndList() {
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
||||
import {jwtDecode, JwtPayload} from "jwt-decode";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { jwtDecode, JwtPayload } from "jwt-decode";
|
||||
|
||||
interface AuthState {
|
||||
isAuthorized: boolean;
|
||||
@@ -15,34 +15,35 @@ const initialState = (): AuthState => {
|
||||
return {
|
||||
accessToken: "",
|
||||
isAuthorized: false,
|
||||
isGuest: false
|
||||
}
|
||||
}
|
||||
isGuest: false,
|
||||
};
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
login: (state, action: PayloadAction<{ accessToken: string }>) => {
|
||||
try {
|
||||
const {sub} = jwtDecode<JwtPayload>(action.payload.accessToken);
|
||||
const { sub } = jwtDecode<JwtPayload>(
|
||||
action.payload.accessToken
|
||||
);
|
||||
state.accessToken = action.payload.accessToken;
|
||||
state.isAuthorized = true;
|
||||
if (sub === "guest")
|
||||
state.isGuest = true;
|
||||
if (sub === "guest") state.isGuest = true;
|
||||
} catch (_) {
|
||||
const url = window.location.href;
|
||||
const urlObj = new URL(url);
|
||||
urlObj.search = '';
|
||||
history.replaceState(null, '', urlObj);
|
||||
urlObj.search = "";
|
||||
history.replaceState(null, "", urlObj);
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
logout: (state) => {
|
||||
logout: state => {
|
||||
state.isAuthorized = false;
|
||||
state.accessToken = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
export const {login, logout} = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
state.accessToken = "";
|
||||
},
|
||||
},
|
||||
});
|
||||
export const { login, logout } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
||||
import {notifications} from "../shared/lib/notifications.ts";
|
||||
import {IconCheck, IconX} from "@tabler/icons-react";
|
||||
import {rem} from "@mantine/core";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { notifications } from "../shared/lib/notifications.ts";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
import { rem } from "@mantine/core";
|
||||
|
||||
export type TaskData = {
|
||||
title: string;
|
||||
@@ -43,13 +43,15 @@ const tasksSlice = createSlice({
|
||||
autoClose: false,
|
||||
withCloseButton: false,
|
||||
withBorder: true,
|
||||
radius: "sm"
|
||||
radius: "sm",
|
||||
});
|
||||
state.tasks.push(task);
|
||||
localStorage.setItem("tasks", JSON.stringify(state.tasks));
|
||||
},
|
||||
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));
|
||||
},
|
||||
failTask: (state, action: PayloadAction<Task>) => {
|
||||
@@ -59,16 +61,20 @@ const tasksSlice = createSlice({
|
||||
if (!notificationId) return;
|
||||
notifications.update({
|
||||
id: notificationId,
|
||||
color: 'red',
|
||||
color: "red",
|
||||
title: task.config.onErrorData.title,
|
||||
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,
|
||||
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(
|
||||
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));
|
||||
},
|
||||
@@ -79,23 +85,28 @@ const tasksSlice = createSlice({
|
||||
if (!notificationId) return;
|
||||
notifications.update({
|
||||
id: notificationId,
|
||||
color: 'teal',
|
||||
color: "teal",
|
||||
title: task.config.onSuccessData.title,
|
||||
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,
|
||||
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(
|
||||
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));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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 {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const initialState: UIState = {
|
||||
isLoading: false
|
||||
}
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
name: 'ui',
|
||||
initialState,
|
||||
reducers: {
|
||||
setIsLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
name: "ui",
|
||||
initialState,
|
||||
reducers: {
|
||||
setIsLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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 {CancelablePromise, PaginationInfoSchema} from "../client";
|
||||
import {Pagination} from "../types/Pagination.ts";
|
||||
import {
|
||||
QueryObserverResult,
|
||||
RefetchOptions,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { CancelablePromise, PaginationInfoSchema } from "../client";
|
||||
import { Pagination } from "../types/Pagination.ts";
|
||||
|
||||
type Props<T, K> = {
|
||||
queryFn: () => CancelablePromise<T>,
|
||||
getObjectsFn: (response: T) => K[],
|
||||
queryKey: string
|
||||
}
|
||||
queryFn: () => CancelablePromise<T>;
|
||||
getObjectsFn: (response: T) => K[];
|
||||
queryKey: string;
|
||||
};
|
||||
type Response<T, K> = {
|
||||
objects: K[],
|
||||
refetch: (options?: RefetchOptions) => Promise<QueryObserverResult<T, Error>>
|
||||
}
|
||||
const ObjectList = <T, K, >(props: Props<T, K>): Response<T, K> => {
|
||||
const {isPending, error, data, refetch} = useQuery({
|
||||
objects: K[];
|
||||
refetch: (
|
||||
options?: RefetchOptions
|
||||
) => Promise<QueryObserverResult<T, Error>>;
|
||||
};
|
||||
const ObjectList = <T, K>(props: Props<T, K>): Response<T, K> => {
|
||||
const { isPending, error, data, refetch } = useQuery({
|
||||
queryKey: [props.queryKey],
|
||||
queryFn: props.queryFn
|
||||
queryFn: props.queryFn,
|
||||
});
|
||||
const objects = isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
|
||||
return {objects, refetch}
|
||||
}
|
||||
const objects =
|
||||
isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
|
||||
return { objects, refetch };
|
||||
};
|
||||
|
||||
interface ObjectWithPagination {
|
||||
paginationInfo: PaginationInfoSchema
|
||||
paginationInfo: PaginationInfoSchema;
|
||||
}
|
||||
|
||||
type PropsWithPagination<T extends ObjectWithPagination, K> = {
|
||||
queryFn: () => CancelablePromise<T>,
|
||||
getObjectsFn: (response: T) => K[],
|
||||
queryKey: string,
|
||||
pagination: Pagination
|
||||
}
|
||||
queryFn: () => CancelablePromise<T>;
|
||||
getObjectsFn: (response: T) => K[];
|
||||
queryKey: string;
|
||||
pagination: Pagination;
|
||||
};
|
||||
type ResponseWithPagination<T extends ObjectWithPagination, K> = {
|
||||
objects: K[],
|
||||
pagination?: PaginationInfoSchema,
|
||||
refetch: (options?: RefetchOptions) => Promise<QueryObserverResult<T, Error>>
|
||||
}
|
||||
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],
|
||||
objects: K[];
|
||||
pagination?: PaginationInfoSchema;
|
||||
refetch: (
|
||||
options?: RefetchOptions
|
||||
) => Promise<QueryObserverResult<T, Error>>;
|
||||
};
|
||||
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,
|
||||
});
|
||||
const objects = isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
|
||||
const objects =
|
||||
isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
|
||||
return {
|
||||
objects,
|
||||
pagination: data?.paginationInfo,
|
||||
refetch
|
||||
}
|
||||
}
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default ObjectList
|
||||
export default ObjectList;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import ObjectList from "./objectList.tsx";
|
||||
import {MarketplaceService} from "../client";
|
||||
import { MarketplaceService } from "../client";
|
||||
|
||||
const useBaseMarketplacesList = () => ObjectList({
|
||||
queryFn: MarketplaceService.getAllBaseMarketplaces,
|
||||
getObjectsFn: (response) => response.baseMarketplaces,
|
||||
queryKey: "getAllBaseMarketplaces"
|
||||
})
|
||||
export default useBaseMarketplacesList;
|
||||
const useBaseMarketplacesList = () =>
|
||||
ObjectList({
|
||||
queryFn: MarketplaceService.getAllBaseMarketplaces,
|
||||
getObjectsFn: response => response.baseMarketplaces,
|
||||
queryKey: "getAllBaseMarketplaces",
|
||||
});
|
||||
export default useBaseMarketplacesList;
|
||||
|
||||
@@ -7,14 +7,13 @@ type Props<T> = {
|
||||
export function useCRUD<T>(props: Props<T>) {
|
||||
const onCreate = (element: T) => {
|
||||
props.onCreate(element);
|
||||
}
|
||||
};
|
||||
const onChange = (element: T) => {
|
||||
props.onChange(element);
|
||||
|
||||
}
|
||||
};
|
||||
const onDelete = (element: T) => {
|
||||
props.onDelete(element);
|
||||
}
|
||||
};
|
||||
|
||||
return {onCreate, onChange, onDelete};
|
||||
}
|
||||
return { onCreate, onChange, onDelete };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {ObjectListWithPagination} from "./objectList.tsx";
|
||||
import {PayrollService} from "../client";
|
||||
import {Pagination} from "../types/Pagination.ts";
|
||||
import { ObjectListWithPagination } from "./objectList.tsx";
|
||||
import { PayrollService } from "../client";
|
||||
import { Pagination } from "../types/Pagination.ts";
|
||||
|
||||
|
||||
export const usePaymentRecordsList = (pagination: Pagination) => ObjectListWithPagination({
|
||||
queryFn: () => PayrollService.getPaymentRecords(pagination),
|
||||
queryKey: "getPaymentRecords",
|
||||
getObjectsFn: (response) => response.paymentRecords,
|
||||
pagination
|
||||
})
|
||||
export const usePaymentRecordsList = (pagination: Pagination) =>
|
||||
ObjectListWithPagination({
|
||||
queryFn: () => PayrollService.getPaymentRecords(pagination),
|
||||
queryKey: "getPaymentRecords",
|
||||
getObjectsFn: response => response.paymentRecords,
|
||||
pagination,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import ObjectList from "./objectList.tsx";
|
||||
import {PayrollService} from "../client";
|
||||
import { PayrollService } from "../client";
|
||||
|
||||
const usePayrollSchemasList = () => ObjectList({
|
||||
queryFn: PayrollService.getAllPayrollSchemas,
|
||||
getObjectsFn: (response) => response.payrollSchemas,
|
||||
queryKey: "getAllPayrollSchemas"
|
||||
})
|
||||
export default usePayrollSchemasList;
|
||||
const usePayrollSchemasList = () =>
|
||||
ObjectList({
|
||||
queryFn: PayrollService.getAllPayrollSchemas,
|
||||
getObjectsFn: response => response.payrollSchemas,
|
||||
queryKey: "getAllPayrollSchemas",
|
||||
});
|
||||
export default usePayrollSchemasList;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useEffect, useRef, DependencyList} from 'react';
|
||||
import { useEffect, useRef, DependencyList } from "react";
|
||||
|
||||
type UsePollingEffectOptions = {
|
||||
interval?: number;
|
||||
@@ -11,17 +11,13 @@ function usePollingEffect(
|
||||
dependencies: DependencyList = [],
|
||||
options: UsePollingEffectOptions = {}
|
||||
): void {
|
||||
const {
|
||||
interval = 3000,
|
||||
isActive = true,
|
||||
onCleanUp = () => {
|
||||
}
|
||||
} = options;
|
||||
const { interval = 3000, isActive = true, onCleanUp = () => {} } = options;
|
||||
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) { // If not active, don't do anything
|
||||
if (!isActive) {
|
||||
// If not active, don't do anything
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,7 +28,10 @@ function usePollingEffect(
|
||||
await asyncCallback();
|
||||
} finally {
|
||||
if (!stopped) {
|
||||
timeoutIdRef.current = setTimeout(pollingCallback, interval);
|
||||
timeoutIdRef.current = setTimeout(
|
||||
pollingCallback,
|
||||
interval
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,4 +15,4 @@ body {
|
||||
:root {
|
||||
--item-border-size: 0.1rem;
|
||||
--item-border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
12
src/main.tsx
12
src/main.tsx
@@ -40,22 +40,24 @@ const queryClient = new QueryClient();
|
||||
|
||||
// Configuring OpenAPI
|
||||
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(
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider defaultColorScheme={"dark"}>
|
||||
<ModalsProvider labels={{ confirm: "Да", cancel: "Нет" }} modals={modals}>
|
||||
<ModalsProvider
|
||||
labels={{ confirm: "Да", cancel: "Нет" }}
|
||||
modals={modals}>
|
||||
<DatesProvider settings={{ locale: "ru" }}>
|
||||
<TasksProvider>
|
||||
|
||||
<RouterProvider router={router} />
|
||||
<Notifications />
|
||||
</TasksProvider>
|
||||
|
||||
</DatesProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>,
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {Button, Flex, rem, TextInput} from "@mantine/core";
|
||||
import {useEffect, useState} from "react";
|
||||
import {ProductService} from "../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Flex, rem, TextInput } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ProductService } from "../../client";
|
||||
|
||||
type Props = {
|
||||
productId: number;
|
||||
onSubmit: (barcode: string) => void;
|
||||
}
|
||||
};
|
||||
const PrintBarcodeModal = ({
|
||||
id,
|
||||
context,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
const {productId, onSubmit} = innerProps;
|
||||
id,
|
||||
context,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const { productId, onSubmit } = innerProps;
|
||||
const [barcode, setBarcode] = useState<string | undefined>();
|
||||
const [isBarcodeExist, setIsBarcodeExist] = useState<boolean>(true);
|
||||
|
||||
@@ -20,44 +20,41 @@ const PrintBarcodeModal = ({
|
||||
if (!barcode) return "Штрихкод не может быть пустым";
|
||||
if (isBarcodeExist) return "Штрихкод уже существует";
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!barcode) return;
|
||||
ProductService.existsProductBarcode({
|
||||
productId: innerProps.productId,
|
||||
barcode: barcode.trim()
|
||||
}).then((response) => {
|
||||
barcode: barcode.trim(),
|
||||
}).then(response => {
|
||||
setIsBarcodeExist(response.exists);
|
||||
})
|
||||
});
|
||||
}, [productId, barcode]);
|
||||
|
||||
const onSubmitClick = () => {
|
||||
if (!barcode || isBarcodeExist) return;
|
||||
onSubmit(barcode.trim());
|
||||
context.closeModal(id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}
|
||||
>
|
||||
direction={"column"}>
|
||||
<TextInput
|
||||
required
|
||||
error={getErrorMessage()}
|
||||
label={"Штрихкод"}
|
||||
placeholder={"Введите или отсканируйте штрихкод"}
|
||||
value={barcode}
|
||||
onChange={(event) => setBarcode(event.currentTarget.value)}
|
||||
|
||||
onChange={event => setBarcode(event.currentTarget.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={onSubmitClick}
|
||||
disabled={!barcode || isBarcodeExist}
|
||||
>
|
||||
disabled={!barcode || isBarcodeExist}>
|
||||
Добавить
|
||||
</Button>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PrintBarcodeModal;
|
||||
export default PrintBarcodeModal;
|
||||
|
||||
@@ -1,67 +1,68 @@
|
||||
import {UserSchema} from "../../client";
|
||||
import {ObjectSelectProps} from "../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {Button, Flex, rem} from "@mantine/core";
|
||||
import {useState} from "react";
|
||||
import { UserSchema } from "../../client";
|
||||
import { ObjectSelectProps } from "../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Flex, rem } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
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 = {
|
||||
onSelect: (user: UserSchema) => void
|
||||
onSelect: (user: UserSchema) => void;
|
||||
selectProps?: SelectProps;
|
||||
}
|
||||
};
|
||||
|
||||
const EmployeeSelectModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const [innerValue, setInnerValue] = useState<UserSchema | undefined>();
|
||||
|
||||
const closeSelf = () => {
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectClick = () => {
|
||||
if (!innerValue) {
|
||||
notifications.error({message: "Необходимо выбрать сотрудника"})
|
||||
notifications.error({ message: "Необходимо выбрать сотрудника" });
|
||||
return;
|
||||
}
|
||||
innerProps.onSelect(innerValue);
|
||||
closeSelf();
|
||||
}
|
||||
};
|
||||
const onCloseClick = () => {
|
||||
closeSelf();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<Flex w={"100%"}>
|
||||
<UserSelect
|
||||
onChange={setInnerValue}
|
||||
w={"100%"}
|
||||
placeholder={"Выберите сотрудника"}
|
||||
{...innerProps.selectProps}
|
||||
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify={"flex-end"} gap={rem(10)}>
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
gap={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCloseClick()}
|
||||
>
|
||||
onClick={() => onCloseClick()}>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onSelectClick()}
|
||||
>
|
||||
<Button onClick={() => onSelectClick()}>
|
||||
Выбрать сотрудника
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeSelectModal
|
||||
export default EmployeeSelectModal;
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import SimpleUsersTable from "../../pages/LeadsPage/components/SimpleUsersTable/SimpleUsersTable.tsx";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {Flex, rem} from "@mantine/core";
|
||||
import {UserSchema} from "../../client";
|
||||
import {MutableRefObject, useEffect} from "react";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Flex, rem } from "@mantine/core";
|
||||
import { UserSchema } from "../../client";
|
||||
import { MutableRefObject, useEffect } from "react";
|
||||
|
||||
type Props = {
|
||||
items: MutableRefObject<UserSchema[]>
|
||||
items: MutableRefObject<UserSchema[]>;
|
||||
onChange: (items: UserSchema[]) => void;
|
||||
};
|
||||
|
||||
const EmployeeTableModal = ({
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
useEffect(() => {
|
||||
}, [innerProps.items.current])
|
||||
const EmployeeTableModal = ({ innerProps }: ContextModalProps<Props>) => {
|
||||
useEffect(() => {}, [innerProps.items.current]);
|
||||
return (
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<SimpleUsersTable
|
||||
items={innerProps.items.current}
|
||||
onChange={(event) => {
|
||||
onChange={event => {
|
||||
innerProps.onChange(event);
|
||||
}}
|
||||
/>
|
||||
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
export default EmployeeTableModal;
|
||||
);
|
||||
};
|
||||
export default EmployeeTableModal;
|
||||
|
||||
@@ -1,81 +1,83 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {Button, Flex, rem, Textarea} from "@mantine/core";
|
||||
import {DateTimePicker, DateValue} from "@mantine/dates";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {DealSummaryReorderRequest} from "../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Flex, rem, Textarea } from "@mantine/core";
|
||||
import { DateTimePicker, DateValue } from "@mantine/dates";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { DealSummaryReorderRequest } from "../../client";
|
||||
|
||||
import {dateWithoutTimezone} from "../../shared/lib/date.ts";
|
||||
import { dateWithoutTimezone } from "../../shared/lib/date.ts";
|
||||
|
||||
type Deadline = {
|
||||
deadline: DateValue,
|
||||
comment: string
|
||||
}
|
||||
deadline: DateValue;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
request: Partial<DealSummaryReorderRequest>,
|
||||
onSubmit: (
|
||||
request: DealSummaryReorderRequest,
|
||||
) => void;
|
||||
}
|
||||
request: Partial<DealSummaryReorderRequest>;
|
||||
onSubmit: (request: DealSummaryReorderRequest) => void;
|
||||
};
|
||||
|
||||
const EnterDeadlineModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const form = useForm<Deadline>({
|
||||
initialValues: {
|
||||
deadline: null,
|
||||
comment: '',
|
||||
comment: "",
|
||||
},
|
||||
validate: {
|
||||
deadline: (datetime) => datetime !== null ? null : 'Необходимо ввести дедлайн',
|
||||
}
|
||||
})
|
||||
deadline: datetime =>
|
||||
datetime !== null ? null : "Необходимо ввести дедлайн",
|
||||
},
|
||||
});
|
||||
const onCancelClick = () => {
|
||||
context.closeModal(id);
|
||||
}
|
||||
};
|
||||
const onSubmit = (values: Deadline) => {
|
||||
const {deadline, comment} = values;
|
||||
const { deadline, comment } = values;
|
||||
if (!deadline) return;
|
||||
innerProps.onSubmit({
|
||||
...innerProps.request,
|
||||
deadline: dateWithoutTimezone(deadline),
|
||||
comment
|
||||
comment,
|
||||
} as unknown as DealSummaryReorderRequest);
|
||||
context.closeModal(id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
|
||||
|
||||
<Flex direction={'column'} gap={10}>
|
||||
<Flex direction={'column'} gap={rem(10)}>
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={10}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<DateTimePicker
|
||||
required
|
||||
label={'Дата и время'}
|
||||
placeholder={'Введите дату и время'}
|
||||
label={"Дата и время"}
|
||||
placeholder={"Введите дату и время"}
|
||||
minDate={new Date()}
|
||||
{...form.getInputProps('deadline')}
|
||||
{...form.getInputProps("deadline")}
|
||||
/>
|
||||
<Textarea
|
||||
label={'Коментарий'}
|
||||
placeholder={'Введите коментарий'}
|
||||
{...form.getInputProps('comment')}
|
||||
label={"Коментарий"}
|
||||
placeholder={"Введите коментарий"}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex justify={'flex-end'} gap={rem(10)}>
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
gap={rem(10)}>
|
||||
<Button
|
||||
variant={'default'}
|
||||
onClick={onCancelClick}
|
||||
>Отменить</Button>
|
||||
<Button
|
||||
type={'submit'}
|
||||
>Сохранить</Button>
|
||||
variant={"default"}
|
||||
onClick={onCancelClick}>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button type={"submit"}>Сохранить</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
|
||||
)
|
||||
);
|
||||
};
|
||||
export default EnterDeadlineModal;
|
||||
export default EnterDeadlineModal;
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
import BaseFormModal, {CreateEditFormProps} from "../../pages/ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {PositionSchema} from "../../client";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Flex, rem, TextInput} from "@mantine/core";
|
||||
import {useEffect} from "react";
|
||||
import CyrillicToTranslit from 'cyrillic-to-translit-js';
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../pages/ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { PositionSchema } from "../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { Flex, rem, TextInput } from "@mantine/core";
|
||||
import { useEffect } from "react";
|
||||
import CyrillicToTranslit from "cyrillic-to-translit-js";
|
||||
|
||||
type Props = CreateEditFormProps<PositionSchema>;
|
||||
|
||||
const PositionFormModal = ({
|
||||
id,
|
||||
context,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
const translit = CyrillicToTranslit({preset: "ru"})
|
||||
const isEditing = 'element' in innerProps;
|
||||
const initialValues: PositionSchema = isEditing ? innerProps.element : {
|
||||
key: "",
|
||||
name: ""
|
||||
}
|
||||
id,
|
||||
context,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const translit = CyrillicToTranslit({ preset: "ru" });
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValues: PositionSchema = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
key: "",
|
||||
name: "",
|
||||
};
|
||||
const form = useForm<PositionSchema>({
|
||||
initialValues: initialValues
|
||||
})
|
||||
initialValues: initialValues,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) return;
|
||||
form.setFieldValue("key", translit.transform(form.values.name).toLowerCase());
|
||||
form.setFieldValue(
|
||||
"key",
|
||||
translit.transform(form.values.name).toLowerCase()
|
||||
);
|
||||
}, [form.values.name]);
|
||||
|
||||
return (
|
||||
@@ -33,13 +40,11 @@ const PositionFormModal = ({
|
||||
closeOnSubmit
|
||||
form={form}
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}
|
||||
>
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
placeholder={"Введите название должности"}
|
||||
@@ -53,7 +58,7 @@ const PositionFormModal = ({
|
||||
</Flex>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PositionFormModal;
|
||||
export default PositionFormModal;
|
||||
|
||||
@@ -1,72 +1,88 @@
|
||||
import {BarcodeAttributeSchema} from "../../client";
|
||||
import {forwardRef} from "react";
|
||||
import { BarcodeAttributeSchema } from "../../client";
|
||||
import { forwardRef } from "react";
|
||||
import styles from "./PrintBarcodeModal.module.css";
|
||||
import {Flex, Text} from "@mantine/core";
|
||||
import { Flex, Text } from "@mantine/core";
|
||||
import Barcode from "react-barcode";
|
||||
|
||||
type Props = {
|
||||
attributes: BarcodeAttributeSchema[]
|
||||
attributes: BarcodeAttributeSchema[];
|
||||
barcode?: string;
|
||||
quantity: number;
|
||||
additionalField?: string | null;
|
||||
}
|
||||
};
|
||||
type Ref = HTMLDivElement;
|
||||
const PrintBarcodeContainer = forwardRef<Ref, Props>(function PrintBarcodeContainer(props: Props, ref) {
|
||||
const {attributes, barcode, quantity, additionalField} = props;
|
||||
const PrintBarcodeContainer = forwardRef<Ref, Props>(
|
||||
function PrintBarcodeContainer(props: Props, ref) {
|
||||
const { attributes, barcode, quantity, additionalField } = props;
|
||||
|
||||
const MAX_ATTRIBUTES = additionalField && additionalField.length > 0 ? 5 : 6;
|
||||
const MIN_BARCODE_SIZE = 30;
|
||||
const MAX_BARCODE_SIZE = 100;
|
||||
const STEP = (MAX_BARCODE_SIZE - MIN_BARCODE_SIZE) / MAX_ATTRIBUTES;
|
||||
const MAX_ATTRIBUTE_LENGTH = 35;
|
||||
const MAX_ATTRIBUTES =
|
||||
additionalField && additionalField.length > 0 ? 5 : 6;
|
||||
const MIN_BARCODE_SIZE = 30;
|
||||
const MAX_BARCODE_SIZE = 100;
|
||||
const STEP = (MAX_BARCODE_SIZE - MIN_BARCODE_SIZE) / MAX_ATTRIBUTES;
|
||||
const MAX_ATTRIBUTE_LENGTH = 35;
|
||||
|
||||
const getBarcodeHeight = () => {
|
||||
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) {
|
||||
result = result.slice(0, MAX_ATTRIBUTE_LENGTH - 1) + ".";
|
||||
}
|
||||
return result;
|
||||
const getBarcodeHeight = () => {
|
||||
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) {
|
||||
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 (
|
||||
<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;
|
||||
export default PrintBarcodeContainer;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.barcode-container {
|
||||
max-height: 1.45in;
|
||||
text-align: left;
|
||||
@@ -19,4 +18,4 @@
|
||||
.barcode-attribute-text {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import {ContextModalProps, modals} from "@mantine/modals";
|
||||
import {Button, Divider, Flex, NumberInput, rem, Select} from "@mantine/core";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {BarcodeSchema, ProductService} from "../../client";
|
||||
import {useGetProductById} from "../../api/product/useGetProductById.tsx";
|
||||
import {notifications} from "../../shared/lib/notifications.ts";
|
||||
import { ContextModalProps, modals } from "@mantine/modals";
|
||||
import { Button, Divider, Flex, NumberInput, rem, Select } from "@mantine/core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { BarcodeSchema, ProductService } from "../../client";
|
||||
import { useGetProductById } from "../../api/product/useGetProductById.tsx";
|
||||
import { notifications } from "../../shared/lib/notifications.ts";
|
||||
import PrintBarcodeContainer from "./PrintBarcodeContainer.tsx";
|
||||
import {base64ToBlob} from "../../shared/lib/utils.ts";
|
||||
import { base64ToBlob } from "../../shared/lib/utils.ts";
|
||||
|
||||
type Props = {
|
||||
productId: number;
|
||||
defaultQuantity?: number;
|
||||
}
|
||||
const PrintBarcodeModal = ({
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
const {productId, defaultQuantity = 1} = innerProps;
|
||||
};
|
||||
const PrintBarcodeModal = ({ innerProps }: ContextModalProps<Props>) => {
|
||||
const { productId, defaultQuantity = 1 } = innerProps;
|
||||
const [quantity, setQuantity] = useState(defaultQuantity);
|
||||
const [barcode, setBarcode] = useState<string | undefined>()
|
||||
const [barcode, setBarcode] = useState<string | undefined>();
|
||||
const [barcodeData, setBarcodeData] = useState<BarcodeSchema | undefined>();
|
||||
|
||||
const {product, refetch} = useGetProductById(productId);
|
||||
const { product, refetch } = useGetProductById(productId);
|
||||
|
||||
const barcodeRef = useRef(null);
|
||||
// const handlePrint = useReactToPrint({
|
||||
@@ -27,47 +25,47 @@ const PrintBarcodeModal = ({
|
||||
// });
|
||||
|
||||
const onAdd = (newBarcode: string) => {
|
||||
ProductService.addProductBarcode({requestBody: {productId, barcode: newBarcode}})
|
||||
.then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
})
|
||||
}
|
||||
ProductService.addProductBarcode({
|
||||
requestBody: { productId, barcode: newBarcode },
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
});
|
||||
};
|
||||
const onAddClick = () => {
|
||||
if (!product) return;
|
||||
modals.openContextModal({
|
||||
modal: "addBarcode",
|
||||
title: 'Добавление штрихкода',
|
||||
title: "Добавление штрихкода",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
productId: product.id,
|
||||
onSubmit: onAdd
|
||||
}
|
||||
})
|
||||
}
|
||||
onSubmit: onAdd,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onGenerateClick = () => {
|
||||
if (!product) return;
|
||||
ProductService.generateProductBarcode({requestBody: {productId}})
|
||||
.then(async ({ok, message, barcode}) => {
|
||||
notifications.guess(ok, {message});
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
setBarcode(barcode);
|
||||
})
|
||||
}
|
||||
ProductService.generateProductBarcode({
|
||||
requestBody: { productId },
|
||||
}).then(async ({ ok, message, barcode }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
setBarcode(barcode);
|
||||
});
|
||||
};
|
||||
const fetchBarcodeData = () => {
|
||||
if (!barcode) return;
|
||||
ProductService.getProductBarcode({
|
||||
requestBody:
|
||||
{barcode, productId}
|
||||
}).then(({barcode}) => {
|
||||
requestBody: { barcode, productId },
|
||||
}).then(({ barcode }) => {
|
||||
setBarcodeData(barcode);
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!product) return;
|
||||
if (product.barcodes.length === 1)
|
||||
setBarcode(product.barcodes[0]);
|
||||
if (product.barcodes.length === 1) setBarcode(product.barcodes[0]);
|
||||
}, [product]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -79,11 +77,10 @@ const PrintBarcodeModal = ({
|
||||
<>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}
|
||||
>
|
||||
direction={"column"}>
|
||||
<Select
|
||||
value={barcode}
|
||||
onChange={(value) => setBarcode(value || undefined)}
|
||||
onChange={value => setBarcode(value || undefined)}
|
||||
data={product?.barcodes}
|
||||
label={"Штрихкод"}
|
||||
placeholder={"Выберите штрихкод"}
|
||||
@@ -92,14 +89,16 @@ const PrintBarcodeModal = ({
|
||||
label={"Количество копий"}
|
||||
placeholder={"Введите количество копий"}
|
||||
value={quantity}
|
||||
onChange={(value) => typeof value === "number" && setQuantity(value)}
|
||||
onChange={value =>
|
||||
typeof value === "number" && setQuantity(value)
|
||||
}
|
||||
min={1}
|
||||
/>
|
||||
|
||||
<Divider
|
||||
my={rem(10)}
|
||||
/>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<Divider my={rem(10)} />
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<Flex gap={rem(10)}>
|
||||
<Button
|
||||
onClick={() => onAddClick()}
|
||||
@@ -119,25 +118,30 @@ const PrintBarcodeModal = ({
|
||||
disabled={!barcode}
|
||||
onClick={async () => {
|
||||
if (!barcode) return;
|
||||
const response = await ProductService.getProductBarcodePdf({
|
||||
requestBody: {
|
||||
productId,
|
||||
barcode,
|
||||
quantity
|
||||
}
|
||||
});
|
||||
const pdfBlob = base64ToBlob(response.base64String, response.mimeType);
|
||||
const response =
|
||||
await ProductService.getProductBarcodePdf({
|
||||
requestBody: {
|
||||
productId,
|
||||
barcode,
|
||||
quantity,
|
||||
},
|
||||
});
|
||||
const pdfBlob = base64ToBlob(
|
||||
response.base64String,
|
||||
response.mimeType
|
||||
);
|
||||
const pdfUrl = URL.createObjectURL(pdfBlob);
|
||||
const pdfWindow = window.open(pdfUrl);
|
||||
if (!pdfWindow) {
|
||||
notifications.error({message: "Ошибка"});
|
||||
return
|
||||
notifications.error({ message: "Ошибка" });
|
||||
return;
|
||||
}
|
||||
pdfWindow.onload = () => {
|
||||
pdfWindow.print();
|
||||
}
|
||||
}}
|
||||
>Печать</Button>
|
||||
};
|
||||
}}>
|
||||
Печать
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<PrintBarcodeContainer
|
||||
@@ -148,7 +152,7 @@ const PrintBarcodeModal = ({
|
||||
additionalField={barcodeData?.additionalField}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PrintBarcodeModal;
|
||||
export default PrintBarcodeModal;
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import {GetServiceKitSchema} from "../../client";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {useState} from "react";
|
||||
import {Button, Flex, rem} from "@mantine/core";
|
||||
import { GetServiceKitSchema } from "../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useState } from "react";
|
||||
import { Button, Flex, rem } from "@mantine/core";
|
||||
import ServicesKitSelect from "../../components/Selects/ServicesKitSelect/ServicesKitSelect.tsx";
|
||||
import {notifications} from "../../shared/lib/notifications.ts";
|
||||
import { notifications } from "../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
onSelect: (kit: GetServiceKitSchema) => void;
|
||||
serviceType: number;
|
||||
}
|
||||
};
|
||||
|
||||
const ServicesKitSelectModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const [kit, setKit] = useState<GetServiceKitSchema | undefined>();
|
||||
const onSelectClick = () => {
|
||||
if (!kit) {
|
||||
notifications.error({message: "Выберите набор услуг"});
|
||||
notifications.error({ message: "Выберите набор услуг" });
|
||||
return;
|
||||
}
|
||||
innerProps.onSelect(kit);
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
@@ -35,26 +35,27 @@ const ServicesKitSelectModal = ({
|
||||
placeholder={"Выберите набор услуг"}
|
||||
value={kit}
|
||||
onChange={setKit}
|
||||
filterBy={item => item.serviceType === innerProps.serviceType}
|
||||
filterBy={item =>
|
||||
item.serviceType === innerProps.serviceType
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify={"flex-end"} gap={rem(10)}>
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
gap={rem(10)}>
|
||||
<Button
|
||||
variant={"subtle"}
|
||||
onClick={() => context.closeContextModal(id)}
|
||||
>
|
||||
onClick={() => context.closeContextModal(id)}>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSelectClick}
|
||||
variant={"default"}
|
||||
>
|
||||
variant={"default"}>
|
||||
Добавить
|
||||
</Button>
|
||||
|
||||
</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 PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx";
|
||||
import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
|
||||
import BarcodeTemplateFormModal
|
||||
from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
|
||||
import BarcodeTemplateFormModal from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
|
||||
import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx";
|
||||
import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx";
|
||||
import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx";
|
||||
|
||||
@@ -1,75 +1,84 @@
|
||||
import styles from './AdminPage.module.css';
|
||||
import {Tabs} from "@mantine/core";
|
||||
import styles from "./AdminPage.module.css";
|
||||
import { Tabs } from "@mantine/core";
|
||||
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 UsersTab from "./tabs/Users/UsersTab.tsx";
|
||||
import {motion} from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
|
||||
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
|
||||
|
||||
const AdminPage = () => {
|
||||
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<div className={styles["container"]}>
|
||||
<PageBlock fullHeight>
|
||||
<Tabs variant={"outline"} keepMounted={false} defaultValue={"users"}>
|
||||
<Tabs
|
||||
variant={"outline"}
|
||||
keepMounted={false}
|
||||
defaultValue={"users"}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value={"users"} leftSection={<IconUser/>}>
|
||||
<Tabs.Tab
|
||||
value={"users"}
|
||||
leftSection={<IconUser />}>
|
||||
Пользователи
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"finances"} leftSection={<IconCurrencyDollar/>}>
|
||||
<Tabs.Tab
|
||||
value={"finances"}
|
||||
leftSection={<IconCurrencyDollar />}>
|
||||
Финансы
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"rolesAndPositions"} leftSection={<IconBriefcase/>}>
|
||||
<Tabs.Tab
|
||||
value={"rolesAndPositions"}
|
||||
leftSection={<IconBriefcase />}>
|
||||
Должности
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"workTimeTable"} leftSection={<IconCalendarUser/>}>
|
||||
<Tabs.Tab
|
||||
value={"workTimeTable"}
|
||||
leftSection={<IconCalendarUser />}>
|
||||
Рабочее время
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value={"users"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<UsersTab/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<UsersTab />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"rolesAndPositions"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<RolesAndPositionsTab/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<RolesAndPositionsTab />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"finances"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<FinancesTab/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<FinancesTab />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"workTimeTable"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<WorkTimeTable/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<WorkTimeTable />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
|
||||
</Tabs>
|
||||
</PageBlock>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPage;
|
||||
export default AdminPage;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {PayRateSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {usePayRatesTableColumns} from "./columns.tsx";
|
||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { PayRateSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { usePayRatesTableColumns } from "./columns.tsx";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
type Props = CRUDTableProps<PayRateSchema>;
|
||||
|
||||
const PayRateTable: FC<Props> = ({items, onCreate, onChange, onDelete}) => {
|
||||
const PayRateTable: FC<Props> = ({ items, onCreate, onChange, onDelete }) => {
|
||||
const columns = usePayRatesTableColumns();
|
||||
|
||||
const onCreateClick = () => {
|
||||
@@ -19,76 +19,77 @@ const PayRateTable: FC<Props> = ({items, onCreate, onChange, onDelete}) => {
|
||||
modal: "payRateForm",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
}
|
||||
})
|
||||
}
|
||||
onCreate: onCreate,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onEditClick = (payRate: PayRateSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "payRateForm",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: (event) => onChange({...event, id: payRate.id}),
|
||||
element: payRate
|
||||
}
|
||||
})
|
||||
}
|
||||
onChange: event => onChange({ ...event, id: payRate.id }),
|
||||
element: payRate,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onDeleteClick = (payRate: PayRateSchema) => {
|
||||
if (!onDelete) return;
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление тарифа',
|
||||
title: "Удаление тарифа",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить тариф {payRate.name}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(payRate)
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(payRate),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
Создать тариф
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<PayRateSchema>}
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать тариф
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<PayRateSchema>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PayRateTable;
|
||||
export default PayRateTable;
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {PayRateSchema} from "../../../../client";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { PayRateSchema } from "../../../../client";
|
||||
|
||||
export const usePayRatesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<PayRateSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название тарифа"
|
||||
},
|
||||
{
|
||||
accessorKey: "payrollScheme.name",
|
||||
header: "Система оплаты"
|
||||
},
|
||||
{
|
||||
accessorKey: "baseRate",
|
||||
header: "Базовая ставка",
|
||||
Cell: ({row}) => `${row.original.baseRate.toLocaleString("ru")}₽`
|
||||
|
||||
},
|
||||
{
|
||||
accessorKey: "overtimeThreshold",
|
||||
header: "Порог сверхурочных"
|
||||
},
|
||||
{
|
||||
accessorKey: "overtimeRate",
|
||||
header: "Сверхурочная ставка",
|
||||
Cell: ({row}) => row.original.overtimeRate && `${row.original.overtimeRate.toLocaleString("ru")}₽`
|
||||
|
||||
}
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<PayRateSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название тарифа",
|
||||
},
|
||||
{
|
||||
accessorKey: "payrollScheme.name",
|
||||
header: "Система оплаты",
|
||||
},
|
||||
{
|
||||
accessorKey: "baseRate",
|
||||
header: "Базовая ставка",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.baseRate.toLocaleString("ru")}₽`,
|
||||
},
|
||||
{
|
||||
accessorKey: "overtimeThreshold",
|
||||
header: "Порог сверхурочных",
|
||||
},
|
||||
{
|
||||
accessorKey: "overtimeRate",
|
||||
header: "Сверхурочная ставка",
|
||||
Cell: ({ row }) =>
|
||||
row.original.overtimeRate &&
|
||||
`${row.original.overtimeRate.toLocaleString("ru")}₽`,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {ActionIcon, Button, Flex, Pagination, rem, Text, Tooltip} from "@mantine/core";
|
||||
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";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Flex,
|
||||
Pagination,
|
||||
rem,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
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 [totalPages, setTotalPages] = useState(10);
|
||||
@@ -16,8 +28,8 @@ const PaymentRecordsTable: FC = () => {
|
||||
const {
|
||||
pagination: paginationInfo,
|
||||
objects: paymentRecords,
|
||||
refetch
|
||||
} = usePaymentRecordsList({page: page, itemsPerPage: 10});
|
||||
refetch,
|
||||
} = usePaymentRecordsList({ page: page, itemsPerPage: 10 });
|
||||
useEffect(() => {
|
||||
if (!paginationInfo) return;
|
||||
setTotalPages(paginationInfo.totalPages);
|
||||
@@ -26,98 +38,99 @@ const PaymentRecordsTable: FC = () => {
|
||||
const onCreate = (request: PaymentRecordCreateSchema) => {
|
||||
PayrollService.createPaymentRecord({
|
||||
requestBody: {
|
||||
data: request
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
data: request,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onCreateClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "createPaymentRecord",
|
||||
title: "Создание начисления",
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
onCreate: onCreate,
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDelete = (record: PaymentRecordGetSchema) => {
|
||||
PayrollService.deletePaymentRecord({
|
||||
requestBody: {
|
||||
paymentRecordId: record.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
paymentRecordId: record.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDeleteClick = (record: PaymentRecordGetSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление начисления',
|
||||
title: "Удаление начисления",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить начисление
|
||||
пользователю {record.user.firstName} {record.user.secondName} от {formatDate(record.createdAt)}
|
||||
Вы уверены что хотите удалить начисление пользователю{" "}
|
||||
{record.user.firstName} {record.user.secondName} от{" "}
|
||||
{formatDate(record.createdAt)}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(record)
|
||||
})
|
||||
}
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(record),
|
||||
});
|
||||
};
|
||||
const columns = usePaymentRecordsTableColumns();
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
h={"100%"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
<BaseTable
|
||||
data={paymentRecords}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
Создать начисление
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<PaymentRecordGetSchema>}
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать начисление
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onDeleteClick(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<PaymentRecordGetSchema>
|
||||
}
|
||||
/>
|
||||
{totalPages > 1 &&
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
style={{alignSelf: "flex-end"}}
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
withEdges
|
||||
onChange={event => setPage(event)}
|
||||
value={page}
|
||||
total={totalPages}
|
||||
/>
|
||||
}
|
||||
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
export default PaymentRecordsTable;
|
||||
);
|
||||
};
|
||||
export default PaymentRecordsTable;
|
||||
|
||||
@@ -1,58 +1,79 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {PaymentRecordGetSchema} from "../../../../client";
|
||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import {getPluralForm} from "../../../../shared/lib/utils.ts";
|
||||
import {formatDate} from "../../../../types/utils.ts";
|
||||
import {isEqual} from "lodash";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { PaymentRecordGetSchema } from "../../../../client";
|
||||
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import { getPluralForm } from "../../../../shared/lib/utils.ts";
|
||||
import { formatDate } from "../../../../types/utils.ts";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
export const usePaymentRecordsTableColumns = () => {
|
||||
const getWorkUnitsText = (paymentRecord: PaymentRecordGetSchema) => {
|
||||
const payrollScheme = paymentRecord.payrollScheme;
|
||||
if (payrollScheme.key === PaySchemeType.HOURLY) {
|
||||
return getPluralForm(paymentRecord.workUnits, "час", "часа", "часов")
|
||||
} else if (
|
||||
payrollScheme.key === PaySchemeType.DAILY
|
||||
) {
|
||||
return getPluralForm(paymentRecord.workUnits, "день", "дня", "дней")
|
||||
} else if (
|
||||
payrollScheme.key === PaySchemeType.MONTHLY
|
||||
) {
|
||||
return getPluralForm(paymentRecord.workUnits, "месяц", "месяца", "месяцев");
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"час",
|
||||
"часа",
|
||||
"часов"
|
||||
);
|
||||
} else if (payrollScheme.key === PaySchemeType.DAILY) {
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"день",
|
||||
"дня",
|
||||
"дней"
|
||||
);
|
||||
} else if (payrollScheme.key === PaySchemeType.MONTHLY) {
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"месяц",
|
||||
"месяца",
|
||||
"месяцев"
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
const getDateRangesText = (paymentRecord: PaymentRecordGetSchema) => {
|
||||
if (paymentRecord.endDate && !isEqual(paymentRecord.startDate, paymentRecord.endDate)) {
|
||||
return `${formatDate(paymentRecord.startDate)} - ${formatDate(paymentRecord.endDate)}`
|
||||
if (
|
||||
paymentRecord.endDate &&
|
||||
!isEqual(paymentRecord.startDate, paymentRecord.endDate)
|
||||
) {
|
||||
return `${formatDate(paymentRecord.startDate)} - ${formatDate(paymentRecord.endDate)}`;
|
||||
}
|
||||
return `${formatDate(paymentRecord.startDate)}`;
|
||||
}
|
||||
};
|
||||
|
||||
return useMemo<MRT_ColumnDef<PaymentRecordGetSchema>[]>(() => [
|
||||
{
|
||||
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}) => `${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}`
|
||||
},
|
||||
{
|
||||
header: "Количество",
|
||||
Cell: ({row}) => `${row.original.workUnits} ${getWorkUnitsText(row.original)}`
|
||||
},
|
||||
{
|
||||
header: "Сумма начисления",
|
||||
Cell: ({row}) => row.original.amount.toLocaleString("ru-RU")
|
||||
},
|
||||
{
|
||||
header: "Временной промежуток",
|
||||
Cell: ({row}) => getDateRangesText(row.original)
|
||||
}
|
||||
], [])
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<PaymentRecordGetSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
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 }) =>
|
||||
`${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}`,
|
||||
},
|
||||
{
|
||||
header: "Количество",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.workUnits} ${getWorkUnitsText(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 {PositionSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { PositionSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import usePositionsList from "../../hooks/usePositionsList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<PositionSchema>, 'data' | 'getLabelFn' | 'getValueFn'>;
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<PositionSchema>,
|
||||
"data" | "getLabelFn" | "getValueFn"
|
||||
>;
|
||||
|
||||
const PositionSelect: FC<Props> = (props) => {
|
||||
const {objects: positions} = usePositionsList();
|
||||
const PositionSelect: FC<Props> = props => {
|
||||
const { objects: positions } = usePositionsList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
getLabelFn={(position) => position.name}
|
||||
getValueFn={(position) => position.key}
|
||||
getLabelFn={position => position.name}
|
||||
getValueFn={position => position.key}
|
||||
data={positions}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default PositionSelect;
|
||||
);
|
||||
};
|
||||
export default PositionSelect;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {PositionSchema} from "../../../../client";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {usePositionsTableColumns} from "./columns.tsx";
|
||||
import {FC} from "react";
|
||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {IconTrash} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { PositionSchema } from "../../../../client";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { usePositionsTableColumns } from "./columns.tsx";
|
||||
import { FC } from "react";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
type Props = CRUDTableProps<PositionSchema>;
|
||||
|
||||
const PositionsTable: FC<Props> = ({items, onCreate, onDelete}) => {
|
||||
const PositionsTable: FC<Props> = ({ items, onCreate, onDelete }) => {
|
||||
const columns = usePositionsTableColumns();
|
||||
|
||||
const onCreateClick = () => {
|
||||
@@ -19,63 +19,60 @@ const PositionsTable: FC<Props> = ({items, onCreate, onDelete}) => {
|
||||
modal: "positionForm",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
}
|
||||
})
|
||||
}
|
||||
onCreate: onCreate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteClick = (position: PositionSchema) => {
|
||||
if (!onDelete) return;
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление должности',
|
||||
title: "Удаление должности",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить должность {position.name}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(position)
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(position),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableTopToolbar: true,
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
Создать должность
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
),
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<PositionSchema>}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableTopToolbar: true,
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать должность
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<PositionSchema>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default PositionsTable;
|
||||
export default PositionsTable;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {PositionSchema} from "../../../../client";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { PositionSchema } from "../../../../client";
|
||||
|
||||
export const usePositionsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<PositionSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название должности"
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Ключ"
|
||||
},
|
||||
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<PositionSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название должности",
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Ключ",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import {RoleSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { RoleSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import useRolesList from "../../hooks/useRolesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<RoleSchema>, 'data' | 'getLabelFn' | 'getValueFn'>;
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<RoleSchema>,
|
||||
"data" | "getLabelFn" | "getValueFn"
|
||||
>;
|
||||
|
||||
const RolesSelect: FC<Props> = (props) => {
|
||||
const {objects: roles} = useRolesList();
|
||||
const RolesSelect: FC<Props> = props => {
|
||||
const { objects: roles } = useRolesList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
getLabelFn={(position) => position.name}
|
||||
getValueFn={(position) => position.key}
|
||||
getLabelFn={position => position.name}
|
||||
getValueFn={position => position.key}
|
||||
data={roles}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default RolesSelect;
|
||||
);
|
||||
};
|
||||
export default RolesSelect;
|
||||
|
||||
@@ -1,102 +1,103 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {UserSchema} from "../../../../client";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {FC} from "react";
|
||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
||||
import {useUsersTableColumns} from "./columns.tsx";
|
||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { UserSchema } from "../../../../client";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { FC } from "react";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { useUsersTableColumns } from "./columns.tsx";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
type Props = CRUDTableProps<UserSchema>;
|
||||
|
||||
const UsersTable: FC<Props> = ({items, onChange, onDelete, onCreate}) => {
|
||||
const UsersTable: FC<Props> = ({ items, onChange, onDelete, onCreate }) => {
|
||||
const columns = useUsersTableColumns();
|
||||
const onEditClick = (user: UserSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "userFormModal",
|
||||
title: 'Редактирование пользователя',
|
||||
title: "Редактирование пользователя",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: onChange,
|
||||
element: user,
|
||||
},
|
||||
size: "md"
|
||||
})
|
||||
}
|
||||
size: "md",
|
||||
});
|
||||
};
|
||||
const onDeleteClick = (user: UserSchema) => {
|
||||
if (!onDelete) return;
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление пользователя',
|
||||
title: "Удаление пользователя",
|
||||
// centered: true,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить пользователя {user.firstName} {user.secondName}
|
||||
Вы уверены что хотите удалить пользователя {user.firstName}{" "}
|
||||
{user.secondName}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(user)
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(user),
|
||||
});
|
||||
}
|
||||
};
|
||||
const onCreateClick = () => {
|
||||
if (!onCreate) return;
|
||||
modals.openContextModal({
|
||||
modal: "userFormModal",
|
||||
title: 'Редактирование пользователя',
|
||||
title: "Редактирование пользователя",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
onCreate: onCreate,
|
||||
},
|
||||
size: "md"
|
||||
})
|
||||
}
|
||||
size: "md",
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
Создать пользователя
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip
|
||||
onClick={() => {
|
||||
onEditClick(row.original)
|
||||
}}
|
||||
label="Редактировать">
|
||||
<ActionIcon
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip onClick={() => {
|
||||
onDeleteClick(row.original);
|
||||
}} label="Удалить">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<UserSchema>}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать пользователя
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip
|
||||
onClick={() => {
|
||||
onEditClick(row.original);
|
||||
}}
|
||||
label="Редактировать">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
onClick={() => {
|
||||
onDeleteClick(row.original);
|
||||
}}
|
||||
label="Удалить">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<UserSchema>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default UsersTable;
|
||||
export default UsersTable;
|
||||
|
||||
@@ -1,45 +1,49 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {UserSchema} from "../../../../client";
|
||||
import {IconCheck, IconX} from "@tabler/icons-react";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { UserSchema } from "../../../../client";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
|
||||
export const useUsersTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<UserSchema>[]>(() => [
|
||||
|
||||
{
|
||||
header: "ФИО",
|
||||
Cell: ({row}) => `${row.original.firstName} ${row.original.secondName}`
|
||||
},
|
||||
{
|
||||
accessorKey: "phoneNumber",
|
||||
header: "Номер телефона"
|
||||
},
|
||||
{
|
||||
accessorKey: "role.name",
|
||||
header: "Роль"
|
||||
},
|
||||
{
|
||||
accessorKey: "position.name",
|
||||
header: "Должность"
|
||||
},
|
||||
{
|
||||
accessorKey: "payRate.name",
|
||||
header: "Тариф"
|
||||
},
|
||||
{
|
||||
accessorKey: "comment",
|
||||
header: "Дополнительная информация"
|
||||
},
|
||||
{
|
||||
accessorKey: "isAdmin",
|
||||
header: "Администратор",
|
||||
Cell: ({row}) => row.original.isAdmin ? <IconCheck/> : <IconX/>
|
||||
},
|
||||
{
|
||||
accessorKey: "isBlocked",
|
||||
header: "Заблокирован",
|
||||
Cell: ({row}) => row.original.isBlocked ? <IconCheck/> : <IconX/>
|
||||
},
|
||||
], []);
|
||||
}
|
||||
|
||||
return useMemo<MRT_ColumnDef<UserSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "ФИО",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.firstName} ${row.original.secondName}`,
|
||||
},
|
||||
{
|
||||
accessorKey: "phoneNumber",
|
||||
header: "Номер телефона",
|
||||
},
|
||||
{
|
||||
accessorKey: "role.name",
|
||||
header: "Роль",
|
||||
},
|
||||
{
|
||||
accessorKey: "position.name",
|
||||
header: "Должность",
|
||||
},
|
||||
{
|
||||
accessorKey: "payRate.name",
|
||||
header: "Тариф",
|
||||
},
|
||||
{
|
||||
accessorKey: "comment",
|
||||
header: "Дополнительная информация",
|
||||
},
|
||||
{
|
||||
accessorKey: "isAdmin",
|
||||
header: "Администратор",
|
||||
Cell: ({ row }) =>
|
||||
row.original.isAdmin ? <IconCheck /> : <IconX />,
|
||||
},
|
||||
{
|
||||
accessorKey: "isBlocked",
|
||||
header: "Заблокирован",
|
||||
Cell: ({ row }) =>
|
||||
row.original.isBlocked ? <IconCheck /> : <IconX />,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {PayrollService} from "../../../client";
|
||||
import { PayrollService } from "../../../client";
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
|
||||
const usePayRatesList = () => ObjectList({
|
||||
queryFn: PayrollService.getAllPayRates,
|
||||
getObjectsFn: response => response.payRates,
|
||||
queryKey: "getAllPayRates"
|
||||
})
|
||||
export default usePayRatesList;
|
||||
const usePayRatesList = () =>
|
||||
ObjectList({
|
||||
queryFn: PayrollService.getAllPayRates,
|
||||
getObjectsFn: response => response.payRates,
|
||||
queryKey: "getAllPayRates",
|
||||
});
|
||||
export default usePayRatesList;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {PositionService} from "../../../client";
|
||||
import { PositionService } from "../../../client";
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
|
||||
const usePositionsList = () => ObjectList({
|
||||
queryFn: PositionService.getAllPositions,
|
||||
getObjectsFn: response => response.positions,
|
||||
queryKey: "getAllPositions"
|
||||
})
|
||||
export default usePositionsList;
|
||||
const usePositionsList = () =>
|
||||
ObjectList({
|
||||
queryFn: PositionService.getAllPositions,
|
||||
getObjectsFn: response => response.positions,
|
||||
queryKey: "getAllPositions",
|
||||
});
|
||||
export default usePositionsList;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
import {RoleService} from "../../../client";
|
||||
import { RoleService } from "../../../client";
|
||||
|
||||
const useRolesList = () => ObjectList({
|
||||
queryFn: RoleService.getAllRoles,
|
||||
getObjectsFn: response => response.roles,
|
||||
queryKey: "getAllRoles"
|
||||
})
|
||||
export default useRolesList;
|
||||
const useRolesList = () =>
|
||||
ObjectList({
|
||||
queryFn: RoleService.getAllRoles,
|
||||
getObjectsFn: response => response.roles,
|
||||
queryKey: "getAllRoles",
|
||||
});
|
||||
export default useRolesList;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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({
|
||||
queryFn: UserService.getAllUsers,
|
||||
getObjectsFn: (response) => response.users,
|
||||
queryKey: "getAllUsers"
|
||||
});
|
||||
|
||||
export default useUsersList;
|
||||
export default useUsersList;
|
||||
|
||||
@@ -1,41 +1,51 @@
|
||||
import BaseFormModal, {CreateProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {PaymentRecordCreateSchema} from "../../../../client";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Flex, NumberInput, rem} from "@mantine/core";
|
||||
import {DatePickerInput, MonthPickerInput} from "@mantine/dates";
|
||||
import {useEffect, useState} from "react";
|
||||
import BaseFormModal, {
|
||||
CreateProps,
|
||||
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { PaymentRecordCreateSchema } from "../../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm } from "@mantine/form";
|
||||
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 {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import {motion} from "framer-motion";
|
||||
import {dateWithoutTimezone} from "../../../../shared/lib/date.ts";
|
||||
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import { motion } from "framer-motion";
|
||||
import { dateWithoutTimezone } from "../../../../shared/lib/date.ts";
|
||||
|
||||
type Props = CreateProps<PaymentRecordCreateSchema>;
|
||||
const CreatePaymentRecordModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const form = useForm<Partial<PaymentRecordCreateSchema>>({
|
||||
validate: {
|
||||
user: (user) => !user && "Необходимо выбрать сотрудника",
|
||||
startDate: (startDate) => !startDate && "Необходимо указать временной промежуток",
|
||||
workUnits: (workUnits) => !workUnits && "Укажите количество"
|
||||
}
|
||||
})
|
||||
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([null, null]);
|
||||
user: user => !user && "Необходимо выбрать сотрудника",
|
||||
startDate: startDate =>
|
||||
!startDate && "Необходимо указать временной промежуток",
|
||||
workUnits: workUnits => !workUnits && "Укажите количество",
|
||||
},
|
||||
});
|
||||
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
useEffect(() => {
|
||||
|
||||
const setDates = (start: string | undefined, end: string | undefined) => {
|
||||
const setDates = (
|
||||
start: string | undefined,
|
||||
end: string | undefined
|
||||
) => {
|
||||
form.setFieldValue("startDate", start);
|
||||
form.setFieldValue("endDate", end);
|
||||
};
|
||||
|
||||
if (dateRange.every(dr => dr == null)) {
|
||||
setDates(undefined, undefined);
|
||||
return
|
||||
return;
|
||||
} 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 endDate = notNullValues[1] || startDate;
|
||||
setDates(startDate, endDate);
|
||||
@@ -43,14 +53,13 @@ const CreatePaymentRecordModal = ({
|
||||
}, [dateRange]);
|
||||
|
||||
const getDateRangeInput = () => {
|
||||
if (!form.values.user) return <></>
|
||||
if (!form.values.user) return <></>;
|
||||
const payRate = form.values.user.payRate;
|
||||
if (!payRate) return <></>;
|
||||
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
|
||||
return (
|
||||
<MonthPickerInput
|
||||
error={form.getInputProps("startDate").error}
|
||||
|
||||
label={"Временной промежуток"}
|
||||
placeholder={"Выберите временной промежуток"}
|
||||
type={"range"}
|
||||
@@ -58,73 +67,78 @@ const CreatePaymentRecordModal = ({
|
||||
onChange={setDateRange}
|
||||
allowSingleDateInRange
|
||||
/>
|
||||
)
|
||||
return (<DatePickerInput
|
||||
error={form.getInputProps("startDate").error}
|
||||
label={"Временной промежуток"}
|
||||
placeholder={"Выберите временной промежуток"}
|
||||
type={"range"}
|
||||
allowSingleDateInRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
/>);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<DatePickerInput
|
||||
error={form.getInputProps("startDate").error}
|
||||
label={"Временной промежуток"}
|
||||
placeholder={"Выберите временной промежуток"}
|
||||
type={"range"}
|
||||
allowSingleDateInRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const getAmountLabel = () => {
|
||||
const user = form.values.user;
|
||||
if (!user) return "";
|
||||
const payRate = user?.payRate;
|
||||
if (!payRate) return "";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.HOURLY) return "Количество часов";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY) return "Количество месяцев";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.DAILY) return "Количество дней";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.HOURLY)
|
||||
return "Количество часов";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
|
||||
return "Количество месяцев";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.DAILY)
|
||||
return "Количество дней";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
const getAmountPlaceholder = () => {
|
||||
return "Укажите " + getAmountLabel().toLowerCase();
|
||||
}
|
||||
return (<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}
|
||||
>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<UserSelect
|
||||
label={"Сотрудник"}
|
||||
placeholder={"Выберите сотрудника"}
|
||||
searchable
|
||||
filterBy={(user) => !!user.payRate}
|
||||
{...form.getInputProps("user")}
|
||||
/>
|
||||
};
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<UserSelect
|
||||
label={"Сотрудник"}
|
||||
placeholder={"Выберите сотрудника"}
|
||||
searchable
|
||||
filterBy={user => !!user.payRate}
|
||||
{...form.getInputProps("user")}
|
||||
/>
|
||||
|
||||
{form.values.user &&
|
||||
<>
|
||||
<motion.div
|
||||
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.3}}
|
||||
>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
|
||||
{getDateRangeInput()}
|
||||
<NumberInput
|
||||
label={getAmountLabel()}
|
||||
placeholder={getAmountPlaceholder()}
|
||||
hideControls
|
||||
{...form.getInputProps("workUnits")}
|
||||
/>
|
||||
</Flex>
|
||||
</motion.div>
|
||||
</>
|
||||
}
|
||||
</Flex>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>)
|
||||
}
|
||||
export default CreatePaymentRecordModal;
|
||||
{form.values.user && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
{getDateRangeInput()}
|
||||
<NumberInput
|
||||
label={getAmountLabel()}
|
||||
placeholder={getAmountPlaceholder()}
|
||||
hideControls
|
||||
{...form.getInputProps("workUnits")}
|
||||
/>
|
||||
</Flex>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
export default CreatePaymentRecordModal;
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {PayRateSchemaBase} from "../../../../client";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Fieldset, Flex, NumberInput, rem, TextInput} from "@mantine/core";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { PayRateSchemaBase } from "../../../../client";
|
||||
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 {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||
|
||||
type Props = CreateEditFormProps<PayRateSchemaBase>
|
||||
type Props = CreateEditFormProps<PayRateSchemaBase>;
|
||||
|
||||
const PayRateFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = 'element' in innerProps;
|
||||
const initialValue: Partial<PayRateSchemaBase> = isEditing ? innerProps.element : {};
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValue: Partial<PayRateSchemaBase> = isEditing
|
||||
? innerProps.element
|
||||
: {};
|
||||
|
||||
const form = useForm<Partial<PayRateSchemaBase>>({
|
||||
initialValues: initialValue,
|
||||
validate: {
|
||||
name: (name) => !name && "Необходимо указать название тарифа",
|
||||
payrollScheme: (scheme) => !scheme && "Необходимо выбрать систему оплаты",
|
||||
baseRate: (baseRate) => !baseRate && "Небходимо указать базовую ставку"
|
||||
}
|
||||
name: name => !name && "Необходимо указать название тарифа",
|
||||
payrollScheme: scheme =>
|
||||
!scheme && "Необходимо выбрать систему оплаты",
|
||||
baseRate: baseRate =>
|
||||
!baseRate && "Небходимо указать базовую ставку",
|
||||
},
|
||||
});
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}
|
||||
>
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Общие параметры"}>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
placeholder={"Введите название тарифа"}
|
||||
@@ -47,11 +53,11 @@ const PayRateFormModal = ({
|
||||
{...form.getInputProps("payrollScheme")}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
</Fieldset>
|
||||
<Fieldset>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<NumberInput
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
@@ -61,38 +67,42 @@ const PayRateFormModal = ({
|
||||
thousandSeparator={" "}
|
||||
suffix={"₽"}
|
||||
{...form.getInputProps("baseRate")}
|
||||
|
||||
/>
|
||||
{form.values.payrollScheme?.key === PaySchemeType.HOURLY &&
|
||||
{form.values.payrollScheme?.key ===
|
||||
PaySchemeType.HOURLY && (
|
||||
<>
|
||||
<NumberInput
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
allowDecimal={false}
|
||||
label={"Порог сверхурочных"}
|
||||
placeholder={"Введите порог сверхурочных"}
|
||||
{...form.getInputProps("overtimeThreshold")}
|
||||
placeholder={
|
||||
"Введите порог сверхурочных"
|
||||
}
|
||||
{...form.getInputProps(
|
||||
"overtimeThreshold"
|
||||
)}
|
||||
/>
|
||||
<NumberInput
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
decimalScale={2}
|
||||
label={"Сверхурочная ставка"}
|
||||
placeholder={"Выберите сверхурочную ставку"}
|
||||
placeholder={
|
||||
"Выберите сверхурочную ставку"
|
||||
}
|
||||
thousandSeparator={" "}
|
||||
suffix={"₽"}
|
||||
{...form.getInputProps("overtimeRate")}
|
||||
/>
|
||||
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
</Fieldset>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PayRateFormModal;
|
||||
export default PayRateFormModal;
|
||||
|
||||
@@ -1,46 +1,65 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {UserSchema} from "../../../../client";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Checkbox, Fieldset, Input, Stack, Textarea, TextInput} from "@mantine/core";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { UserSchema } from "../../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import {
|
||||
Checkbox,
|
||||
Fieldset,
|
||||
Input,
|
||||
Stack,
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import RoleSelect from "../../components/RoleSelect/RoleSelect.tsx";
|
||||
import PositionSelect from "../../components/PositionSelect/PositionSelect.tsx";
|
||||
import {UserRoleEnum} from "../../../../shared/enums/UserRole.ts";
|
||||
import {capitalize} from "lodash";
|
||||
import {IMaskInput} from "react-imask";
|
||||
import { UserRoleEnum } from "../../../../shared/enums/UserRole.ts";
|
||||
import { capitalize } from "lodash";
|
||||
import { IMaskInput } from "react-imask";
|
||||
import phone from "phone";
|
||||
import PayRateSelect from "../../../../components/Selects/PayRateSelect/PayRateSelect.tsx";
|
||||
|
||||
type Props = CreateEditFormProps<UserSchema>;
|
||||
const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
||||
const isEditing = 'element' in innerProps;
|
||||
const initialValues = isEditing ? innerProps.element : {
|
||||
isAdmin: false,
|
||||
isBlocked: false,
|
||||
isDeleted: false,
|
||||
comment: "",
|
||||
roleKey: UserRoleEnum.USER
|
||||
};
|
||||
const UserFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValues = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
isAdmin: false,
|
||||
isBlocked: false,
|
||||
isDeleted: false,
|
||||
comment: "",
|
||||
roleKey: UserRoleEnum.USER,
|
||||
};
|
||||
|
||||
const form = useForm<Partial<UserSchema>>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
firstName: value => !value?.trim() && "Укажите имя пользователя",
|
||||
secondName: value => !value?.trim() && "Укажите фамилию",
|
||||
position: (value, values) => ((values.role?.key === UserRoleEnum.EMPLOYEE) && (!value)) && 'Необходимо указать должность сотрудника',
|
||||
phoneNumber: value => !phone(value || '', {
|
||||
country: "",
|
||||
strictDetection: false,
|
||||
validateMobilePrefix: false
|
||||
}).isValid && 'Неверно указан номер телефона',
|
||||
}
|
||||
position: (value, values) =>
|
||||
values.role?.key === UserRoleEnum.EMPLOYEE &&
|
||||
!value &&
|
||||
"Необходимо указать должность сотрудника",
|
||||
phoneNumber: value =>
|
||||
!phone(value || "", {
|
||||
country: "",
|
||||
strictDetection: false,
|
||||
validateMobilePrefix: false,
|
||||
}).isValid && "Неверно указан номер телефона",
|
||||
},
|
||||
});
|
||||
return (<BaseFormModal
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}
|
||||
>
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Общая информация"}>
|
||||
@@ -49,19 +68,33 @@ const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
||||
label={"Имя"}
|
||||
placeholder={"Введите имя пользователя"}
|
||||
{...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
|
||||
{...form.getInputProps("secondName")}
|
||||
label={"Фамилия"}
|
||||
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
|
||||
label={"Номер телефона"}
|
||||
error={form.getInputProps("phoneNumber").error}
|
||||
>
|
||||
error={form.getInputProps("phoneNumber").error}>
|
||||
<Input
|
||||
component={IMaskInput}
|
||||
mask="+7 000 000-00-00"
|
||||
@@ -76,46 +109,53 @@ const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
||||
<RoleSelect
|
||||
label={"Роль пользователя"}
|
||||
placeholder={"Выберите роль пользователя"}
|
||||
{...form.getInputProps('role')}
|
||||
{...form.getInputProps("role")}
|
||||
/>
|
||||
{form.values.role?.key === UserRoleEnum.EMPLOYEE &&
|
||||
{form.values.role?.key ===
|
||||
UserRoleEnum.EMPLOYEE && (
|
||||
<>
|
||||
<PositionSelect
|
||||
label={"Должность сотрудника"}
|
||||
placeholder={"Выберите должность сотрудника"}
|
||||
{...form.getInputProps('position')}
|
||||
placeholder={
|
||||
"Выберите должность сотрудника"
|
||||
}
|
||||
{...form.getInputProps("position")}
|
||||
/>
|
||||
<PayRateSelect
|
||||
label={"Тариф"}
|
||||
placeholder={"Выберите тариф сотрудника"}
|
||||
placeholder={
|
||||
"Выберите тариф сотрудника"
|
||||
}
|
||||
{...form.getInputProps("payRate")}
|
||||
/>
|
||||
</>
|
||||
|
||||
}
|
||||
)}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Дополнительные параметры"}>
|
||||
<Stack>
|
||||
<Checkbox
|
||||
label={"Права администратора"}
|
||||
|
||||
{...form.getInputProps('isAdmin', {type: "checkbox"})}
|
||||
{...form.getInputProps("isAdmin", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Заблокирован"}
|
||||
{...form.getInputProps('isBlocked', {type: "checkbox"})}
|
||||
{...form.getInputProps("isBlocked", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Textarea
|
||||
label={"Дополнительная информация"}
|
||||
{...form.getInputProps('comment')}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</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