feat: prettier

This commit is contained in:
2024-09-27 04:47:04 +03:00
parent c5f839d9ef
commit de4fe450ab
253 changed files with 11322 additions and 10004 deletions

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
public
node_modules
src/client

View File

@@ -1,11 +1,11 @@
{ {
"singleAttributePerLine": true, "singleAttributePerLine": true,
"singleQuote": false, "singleQuote": false,
"semi": true, "semi": true,
"quoteProps": "consistent", "quoteProps": "consistent",
"bracketSpacing": true, "bracketSpacing": true,
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 4, "tabWidth": 4,
"bracketSameLine": true, "bracketSameLine": true,
"arrowParens": "avoid" "arrowParens": "avoid"
} }

View File

@@ -4,27 +4,27 @@ This template provides a minimal setup to get React working in Vite with HMR and
Currently, two official plugins are available: Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration ## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this: - Configure the top-level `parserOptions` property like this:
```js ```js
export default { export default {
// other rules... // other rules...
parserOptions: { parserOptions: {
ecmaVersion: 'latest', ecmaVersion: "latest",
sourceType: 'module', sourceType: "module",
project: ['./tsconfig.json', './tsconfig.node.json'], project: ["./tsconfig.json", "./tsconfig.node.json"],
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
}, },
} };
``` ```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

View File

@@ -6,4 +6,4 @@ export default [
{ languageOptions: { globals: globals.browser } }, { languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended, pluginJs.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
]; ];

View File

@@ -1,13 +1,20 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icons/favicon.png" /> <link
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> rel="icon"
<title>DENCO: Fulfillment</title> type="image/svg+xml"
</head> href="/icons/favicon.png" />
<body> <meta
<div id="root"></div> name="viewport"
<script type="module" src="/src/main.tsx"></script> content="width=device-width, initial-scale=1.0" />
</body> <title>DENCO: Fulfillment</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"></script>
</body>
</html> </html>

7894
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +1,80 @@
{ {
"name": "fulfillment-frontend", "name": "fulfillment-frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview", "preview": "vite preview",
"generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes" "generate-client": "openapi --input http://127.0.0.1:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@mantine/core": "^7.11.2", "@mantine/core": "^7.11.2",
"@mantine/dates": "^7.11.2", "@mantine/dates": "^7.11.2",
"@mantine/dropzone": "^7.11.2", "@mantine/dropzone": "^7.11.2",
"@mantine/form": "^7.11.2", "@mantine/form": "^7.11.2",
"@mantine/hooks": "^7.11.2", "@mantine/hooks": "^7.11.2",
"@mantine/modals": "^7.11.2", "@mantine/modals": "^7.11.2",
"@mantine/notifications": "^7.11.2", "@mantine/notifications": "^7.11.2",
"@reduxjs/toolkit": "^2.2.6", "@reduxjs/toolkit": "^2.2.6",
"@tabler/icons-react": "^3.11.0", "@tabler/icons-react": "^3.11.0",
"@tanstack/react-query": "^5.51.9", "@tanstack/react-query": "^5.51.9",
"@tanstack/react-router": "^1.45.6", "@tanstack/react-router": "^1.45.6",
"@tanstack/router-devtools": "^1.45.6", "@tanstack/router-devtools": "^1.45.6",
"@tanstack/router-vite-plugin": "^1.45.3", "@tanstack/router-vite-plugin": "^1.45.3",
"axios": "^1.7.2", "axios": "^1.7.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cyrillic-to-translit-js": "^3.2.1", "cyrillic-to-translit-js": "^3.2.1",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"dot-object": "^2.1.5", "dot-object": "^2.1.5",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^11.3.8", "framer-motion": "^11.3.8",
"globals": "^15.8.0", "globals": "^15.8.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mantine-form-zod-resolver": "^1.1.0", "mantine-form-zod-resolver": "^1.1.0",
"mantine-react-table": "^2.0.0-beta.5", "mantine-react-table": "^2.0.0-beta.5",
"phone": "^3.1.49", "phone": "^3.1.49",
"react": "^18.3.1", "react": "^18.3.1",
"react-barcode": "^1.5.3", "react-barcode": "^1.5.3",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-imask": "^7.6.1", "react-imask": "^7.6.1",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-to-print": "^2.15.1", "react-to-print": "^2.15.1",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.7.0", "@eslint/js": "^9.7.0",
"@types/dot-object": "^2.1.6", "@types/dot-object": "^2.1.6",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1", "@typescript-eslint/parser": "^7.16.1",
"@vitejs/plugin-react-swc": "^3.7.0", "@vitejs/plugin-react-swc": "^3.7.0",
"eslint": "^9.7.0", "eslint": "^9.7.0",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.8", "eslint-plugin-react-refresh": "^0.4.8",
"openapi-typescript-codegen": "^0.29.0", "openapi-typescript-codegen": "^0.29.0",
"postcss": "^8.4.39", "postcss": "^8.4.39",
"postcss-preset-mantine": "^1.16.0", "postcss-preset-mantine": "^1.16.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"sass": "^1.77.8", "sass": "^1.77.8",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"typescript-eslint": "^7.16.1", "typescript-eslint": "^7.16.1",
"vite": "^5.3.4", "vite": "^5.3.4",
"yarn-upgrade-all": "^0.7.2" "yarn-upgrade-all": "^0.7.2"
}, },
"packageManager": "yarn@4.1.0" "packageManager": "yarn@4.1.0"
} }

View File

@@ -1,14 +1,14 @@
module.exports = { module.exports = {
plugins: { plugins: {
'postcss-preset-mantine': {}, "postcss-preset-mantine": {},
'postcss-simple-vars': { "postcss-simple-vars": {
variables: { variables: {
'mantine-breakpoint-xs': '36em', "mantine-breakpoint-xs": "36em",
'mantine-breakpoint-sm': '48em', "mantine-breakpoint-sm": "48em",
'mantine-breakpoint-md': '62em', "mantine-breakpoint-md": "62em",
'mantine-breakpoint-lg': '75em', "mantine-breakpoint-lg": "75em",
'mantine-breakpoint-xl': '88em', "mantine-breakpoint-xl": "88em",
}, },
}, },
}, },
}; };

View File

@@ -1,14 +1,14 @@
import {useQuery} from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import {BarcodeService} from "../../client"; import { BarcodeService } from "../../client";
const useGetAllBarcodeTemplates = () => { const useGetAllBarcodeTemplates = () => {
const {data, error, isLoading, refetch} = useQuery({ const { data, error, isLoading, refetch } = useQuery({
queryKey: ['getAllBarcodeTemplates'], queryKey: ["getAllBarcodeTemplates"],
queryFn: () => BarcodeService.getAllBarcodeTemplates(), queryFn: () => BarcodeService.getAllBarcodeTemplates(),
select: (data) => data.templates select: data => data.templates,
}); });
const barcodeTemplates = isLoading || error || !data ? [] : data; const barcodeTemplates = isLoading || error || !data ? [] : data;
return {barcodeTemplates, refetch} return { barcodeTemplates, refetch };
} };
export default useGetAllBarcodeTemplates; export default useGetAllBarcodeTemplates;

View File

@@ -1,11 +1,11 @@
import {ProductService} from "../../client"; import { ProductService } from "../../client";
import {useQuery} from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
export const useGetProductById = (id: number) => { export const useGetProductById = (id: number) => {
const {data, error, isLoading, refetch} = useQuery({ const { data, error, isLoading, refetch } = useQuery({
queryKey: ['getProductById', id], queryKey: ["getProductById", id],
queryFn: () => ProductService.getProductById({productId: id}), queryFn: () => ProductService.getProductById({ productId: id }),
select: (data) => data select: data => data,
}); });
return {product: data, error, isLoading, refetch} return { product: data, error, isLoading, refetch };
} };

View File

@@ -1,7 +1,7 @@
import {forwardRef, useContext, useRef} from "react"; import { forwardRef, useContext, useRef } from "react";
import {getRouterContext, Outlet} from "@tanstack/react-router"; import { getRouterContext, Outlet } from "@tanstack/react-router";
import {motion, useIsPresent} from "framer-motion"; import { motion, useIsPresent } from "framer-motion";
import {cloneDeep} from "lodash"; import { cloneDeep } from "lodash";
const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => { const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
const RouterContext = getRouterContext(); const RouterContext = getRouterContext();
@@ -17,26 +17,24 @@ const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
} }
return ( return (
<motion.div ref={ref} <motion.div
initial={{x: "-100%"}} ref={ref}
animate={{ initial={{ x: "-100%" }}
x: 0, animate={{
transform: "", x: 0,
transitionEnd: { transform: "",
transform: "none" transitionEnd: {
} transform: "none",
}} },
}}
transition={{ transition={{
duration: 0.4, duration: 0.4,
ease: "circInOut", ease: "circInOut",
}} }}>
>
<RouterContext.Provider value={renderedContext.current}> <RouterContext.Provider value={renderedContext.current}>
<Outlet/> <Outlet />
</RouterContext.Provider> </RouterContext.Provider>
</motion.div> </motion.div>
); );
}); });
export default AnimatedOutlet export default AnimatedOutlet;

View File

@@ -4,19 +4,18 @@ import {
MRT_RowData, MRT_RowData,
MRT_TableInstance, MRT_TableInstance,
MRT_TableOptions, MRT_TableOptions,
useMantineReactTable useMantineReactTable,
} from "mantine-react-table"; } from "mantine-react-table";
import {MRT_Localization_RU} from "mantine-react-table/locales/ru/index.cjs"; import { MRT_Localization_RU } from "mantine-react-table/locales/ru/index.cjs";
import {forwardRef, useEffect, useImperativeHandle} from 'react'; import { forwardRef, useEffect, useImperativeHandle } from "react";
type Props<T extends Record<string, unknown>, K extends keyof T> = { type Props<T extends Record<string, unknown>, K extends keyof T> = {
data: T[], data: T[];
onSelectionChange?: (selectedRows: T[]) => void, onSelectionChange?: (selectedRows: T[]) => void;
columns: MRT_ColumnDef<T, K>[], columns: MRT_ColumnDef<T, K>[];
restProps?: MRT_TableOptions<T>, restProps?: MRT_TableOptions<T>;
striped?: boolean striped?: boolean;
} };
// Экспортируем тип рефа, чтобы он мог быть использован в других компонентах // Экспортируем тип рефа, чтобы он мог быть использован в других компонентах
export type BaseTableRef<T extends MRT_RowData> = { export type BaseTableRef<T extends MRT_RowData> = {
@@ -25,33 +24,42 @@ export type BaseTableRef<T extends MRT_RowData> = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>((props, ref) => { export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>(
const {data, columns, restProps, striped = true, onSelectionChange} = props; (props, ref) => {
const {
data,
columns,
restProps,
striped = true,
onSelectionChange,
} = props;
const table = useMantineReactTable({ const table = useMantineReactTable({
localization: MRT_Localization_RU, localization: MRT_Localization_RU,
enablePagination: false, enablePagination: false,
data, data,
columns, columns,
mantineTableProps: { mantineTableProps: {
striped: striped striped: striped,
}, },
enableTopToolbar: false, enableTopToolbar: false,
enableBottomToolbar: false, enableBottomToolbar: false,
enableRowSelection: onSelectionChange !== undefined, enableRowSelection: onSelectionChange !== undefined,
...restProps, ...restProps,
}); });
useEffect(() => { useEffect(() => {
if (!onSelectionChange) return; if (!onSelectionChange) return;
onSelectionChange(table.getSelectedRowModel().rows.map(row => row.original)) onSelectionChange(
}, [table.getState().rowSelection]); table.getSelectedRowModel().rows.map(row => row.original)
);
}, [table.getState().rowSelection]);
// Используем useImperativeHandle для определения, что будет доступно через ref // Используем useImperativeHandle для определения, что будет доступно через ref
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
// @ts-ignore // @ts-ignore
getTable: () => table getTable: () => table,
})); }));
return <MantineReactTable table={table} />;
return <MantineReactTable table={table}/>; }
}); );

View File

@@ -1,15 +1,15 @@
import {Button, rem, Tooltip} from '@mantine/core'; import { Button, rem, Tooltip } from "@mantine/core";
import {IconCheck, IconCopy} from '@tabler/icons-react'; import { IconCheck, IconCopy } from "@tabler/icons-react";
import {FC} from "react"; import { FC } from "react";
import {useClipboard} from "@mantine/hooks"; import { useClipboard } from "@mantine/hooks";
type Props = { type Props = {
children: string; children: string;
value: string value: string;
onCopiedLabel: string; onCopiedLabel: string;
} };
export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => { export const ButtonCopy: FC<Props> = ({ children, onCopiedLabel, value }) => {
const clipboard = useClipboard(); const clipboard = useClipboard();
return ( return (
@@ -18,20 +18,19 @@ export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
offset={5} offset={5}
position="bottom" position="bottom"
radius="xl" radius="xl"
transitionProps={{duration: 100, transition: 'slide-down'}} transitionProps={{ duration: 100, transition: "slide-down" }}
opened={clipboard.copied} opened={clipboard.copied}>
>
<Button <Button
variant="light" variant="light"
rightSection={ rightSection={
clipboard.copied ? ( clipboard.copied ? (
<IconCheck <IconCheck
style={{width: rem(20), height: rem(20)}} style={{ width: rem(20), height: rem(20) }}
stroke={1.5} stroke={1.5}
/> />
) : ( ) : (
<IconCopy <IconCopy
style={{width: rem(20), height: rem(20)}} style={{ width: rem(20), height: rem(20) }}
stroke={1.5} stroke={1.5}
/> />
) )
@@ -42,13 +41,12 @@ export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
root: { root: {
paddingRight: rem(14), paddingRight: rem(14),
}, },
section: {marginLeft: rem(22)}, section: { marginLeft: rem(22) },
}} }}
onClick={() => clipboard.copy(value)} onClick={() => clipboard.copy(value)}>
>
{children} {children}
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export default ButtonCopy; export default ButtonCopy;

View File

@@ -1,35 +1,39 @@
import {Button, rem, Tooltip} from '@mantine/core'; import { Button, rem, Tooltip } from "@mantine/core";
import {IconCheck, IconCopy} from '@tabler/icons-react'; import { IconCheck, IconCopy } from "@tabler/icons-react";
import {FC} from "react"; import { FC } from "react";
type Props = { type Props = {
children: string; children: string;
onCopyClick: () => void; onCopyClick: () => void;
onCopiedLabel: string; onCopiedLabel: string;
copied: boolean; copied: boolean;
} };
export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopyClick, copied}) => { export const ButtonCopyControlled: FC<Props> = ({
children,
onCopiedLabel,
onCopyClick,
copied,
}) => {
return ( return (
<Tooltip <Tooltip
label={onCopiedLabel} label={onCopiedLabel}
offset={5} offset={5}
position="bottom" position="bottom"
radius="xl" radius="xl"
transitionProps={{duration: 100, transition: 'slide-down'}} transitionProps={{ duration: 100, transition: "slide-down" }}
opened={copied} opened={copied}>
>
<Button <Button
variant="light" variant="light"
rightSection={ rightSection={
copied ? ( copied ? (
<IconCheck <IconCheck
style={{width: rem(20), height: rem(20)}} style={{ width: rem(20), height: rem(20) }}
stroke={1.5} stroke={1.5}
/> />
) : ( ) : (
<IconCopy <IconCopy
style={{width: rem(20), height: rem(20)}} style={{ width: rem(20), height: rem(20) }}
stroke={1.5} stroke={1.5}
/> />
) )
@@ -40,12 +44,11 @@ export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopy
root: { root: {
paddingRight: rem(14), paddingRight: rem(14),
}, },
section: {marginLeft: rem(22)}, section: { marginLeft: rem(22) },
}} }}
onClick={onCopyClick} onClick={onCopyClick}>
>
{children} {children}
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };

View File

@@ -1,34 +1,34 @@
import {NumberInput, NumberInputProps} from "@mantine/core"; import { NumberInput, NumberInputProps } from "@mantine/core";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import {useDebouncedValue} from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import {isNumber, omit} from "lodash"; import { isNumber, omit } from "lodash";
type Props = NumberInputProps; type Props = NumberInputProps;
const DebouncedNumberInput = (props: Props) => { const DebouncedNumberInput = (props: Props) => {
const [value, setValue] = useState<number | string>(props.defaultValue || props.value || ''); const [value, setValue] = useState<number | string>(
props.defaultValue || props.value || ""
);
const [debounced] = useDebouncedValue(value, 200); const [debounced] = useDebouncedValue(value, 200);
useEffect(() => { useEffect(() => {
if (!props.onChange) return; if (!props.onChange) return;
props.onChange(debounced); props.onChange(debounced);
}, [debounced]) }, [debounced]);
useEffect(() => { useEffect(() => {
if (!isNumber(props.value)) return; if (!isNumber(props.value)) return;
setValue(props.value); setValue(props.value);
}, [props.value]) }, [props.value]);
const restProps = omit(props, ["onChange", "value"]) const restProps = omit(props, ["onChange", "value"]);
return ( return (
<NumberInput <NumberInput
value={value} value={value}
onChange={setValue} onChange={setValue}
{...restProps} {...restProps}
/> />
) );
} };
export default DebouncedNumberInput;
export default DebouncedNumberInput;

View File

@@ -3,7 +3,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/*background-color: green;*/ /*background-color: green;*/
} }
.header { .header {
@@ -34,7 +33,6 @@
} }
@mixin dark { @mixin dark {
background-color: var(--mantine-color-dark-5); background-color: var(--mantine-color-dark-5);
} }
border-radius: var(--item-border-radius); border-radius: var(--item-border-radius);
} }

View File

@@ -1,13 +1,13 @@
import {FC} from "react"; import { FC } from "react";
import styles from './Board.module.css'; import styles from "./Board.module.css";
import {Divider, Text, Title} from '@mantine/core'; import { Divider, Text, Title } from "@mantine/core";
import {Draggable, Droppable} from "@hello-pangea/dnd"; import { Draggable, Droppable } from "@hello-pangea/dnd";
import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx"; import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx";
import {DealSummary} from "../../../client"; import { DealSummary } from "../../../client";
import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx"; import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx";
import classNames from "classnames"; import classNames from "classnames";
import {getPluralForm} from "../../../shared/lib/utils.ts"; import { getPluralForm } from "../../../shared/lib/utils.ts";
import {sum} from "lodash"; import { sum } from "lodash";
type Props = { type Props = {
droppableId: string; droppableId: string;
@@ -15,66 +15,74 @@ type Props = {
withCreateButton?: boolean; withCreateButton?: boolean;
summaries: DealSummary[]; summaries: DealSummary[];
color: string; color: string;
} };
export const Board: FC<Props> = ({droppableId, title, summaries, color, withCreateButton = false}) => { export const Board: FC<Props> = ({
droppableId,
title,
summaries,
color,
withCreateButton = false,
}) => {
const getDealsText = () => { const getDealsText = () => {
const pluralForm = getPluralForm(summaries.length, 'сделка', 'сделки', 'сделок'); const pluralForm = getPluralForm(
summaries.length,
"сделка",
"сделки",
"сделок"
);
return `${summaries.length} ${pluralForm}: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}`; return `${summaries.length} ${pluralForm}: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}`;
} };
return ( return (
<div className={styles["container"]}> <div className={styles["container"]}>
<div className={styles["header"]}> <div className={styles["header"]}>
<Title size={"h4"}>{title}</Title> <Title size={"h4"}>{title}</Title>
<Text>{getDealsText()}</Text> <Text>{getDealsText()}</Text>
<Divider size={"xl"} my={10} color={color}/> <Divider
size={"xl"}
my={10}
color={color}
/>
</div> </div>
<Droppable <Droppable droppableId={droppableId}>
droppableId={droppableId}
>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div ref={provided.innerRef} <div
className={classNames( ref={provided.innerRef}
styles["items-list"], className={classNames(
(snapshot.isDraggingOver && !snapshot.draggingFromThisWith) styles["items-list"],
&& styles["items-list-drag-over"] snapshot.isDraggingOver &&
)} !snapshot.draggingFromThisWith &&
{...provided.droppableProps} styles["items-list-drag-over"]
> )}
{withCreateButton && {...provided.droppableProps}>
<CreateDealButton {withCreateButton && (
<CreateDealButton onClick={() => {}} />
onClick={() => { )}
}} {summaries.map(summary => (
/>} <Draggable
{summaries.map(summary => draggableId={summary.id.toString()}
( index={summary.rank}
<Draggable key={summary.id}>
draggableId={summary.id.toString()} {provided => (
index={summary.rank} <div
key={summary.id} {...provided.draggableProps}
> {...provided.dragHandleProps}
{(provided) => ( ref={provided.innerRef}>
<div {...provided.draggableProps} <DealSummaryCard
{...provided.dragHandleProps} dealSummary={summary}
ref={provided.innerRef} />
</div>
> )}
<DealSummaryCard dealSummary={summary}/> </Draggable>
</div> ))}
)}
</Draggable>
))}
{provided.placeholder} {provided.placeholder}
</div> </div>
)} )}
</Droppable> </Droppable>
</div> </div>
) );
} };
export default Board; export default Board;

View File

@@ -9,11 +9,11 @@
border: dashed var(--item-border-size) var(--mantine-color-default-border); border: dashed var(--item-border-size) var(--mantine-color-default-border);
border-radius: var(--item-border-radius); border-radius: var(--item-border-radius);
cursor: pointer; cursor: pointer;
} }
.container:hover { .container:hover {
background-color: light-dark(var(--mantine-color-default-hover), var(--mantine-color-gray-filled-hover)); background-color: light-dark(
var(--mantine-color-default-hover),
} var(--mantine-color-gray-filled-hover)
);
}

View File

@@ -1,60 +1,61 @@
import {FC, useState} from "react"; import { FC, useState } from "react";
import styles from './CreateDealButton.module.css'; import styles from "./CreateDealButton.module.css";
import {Text, Transition} from '@mantine/core'; import { Text, Transition } from "@mantine/core";
import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx"; import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
import {DealService} from "../../../client"; import { DealService } from "../../../client";
import {useQueryClient} from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import {dateWithoutTimezone} from "../../../shared/lib/date.ts"; import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
type Props = { type Props = {
onClick: () => void; onClick: () => void;
} };
const CreateDealButton: FC<Props> = () => { const CreateDealButton: FC<Props> = () => {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [isTransitionEnded, setIsTransitionEnded] = useState(true); const [isTransitionEnded, setIsTransitionEnded] = useState(true);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return ( return (
<div className={styles['container']} <div
onClick={() => { className={styles["container"]}
if (isCreating) return; onClick={() => {
setIsCreating(prevState => !prevState) if (isCreating) return;
setIsTransitionEnded(false); setIsCreating(prevState => !prevState);
}} setIsTransitionEnded(false);
> }}>
{(!isCreating && isTransitionEnded) && {!isCreating && isTransitionEnded && (
<Text>Быстрое добавление</Text> <Text>Быстрое добавление</Text>
} )}
<Transition <Transition
mounted={isCreating} mounted={isCreating}
transition={"scale-y"} transition={"scale-y"}
onExited={() => setIsTransitionEnded(true)} onExited={() => setIsTransitionEnded(true)}>
{styles => (
>
{(styles) => (
<div style={styles}> <div style={styles}>
<CreateDealFrom <CreateDealFrom
onCancel={() => { onCancel={() => {
setIsCreating(false) setIsCreating(false);
}} }}
onSubmit={(quickDeal) => { onSubmit={quickDeal => {
DealService.quickCreateDealQuickCreatePost({ DealService.quickCreateDealQuickCreatePost({
requestBody: { requestBody: {
...quickDeal, ...quickDeal,
acceptanceDate: dateWithoutTimezone(quickDeal.acceptanceDate) acceptanceDate: dateWithoutTimezone(
} quickDeal.acceptanceDate
),
},
}).then(async () => { }).then(async () => {
await queryClient.invalidateQueries({queryKey: ['getDealSummaries']}); await queryClient.invalidateQueries({
queryKey: ["getDealSummaries"],
});
setIsCreating(false); setIsCreating(false);
}) });
}} }}
/> />
</div> </div>
)} )}
</Transition> </Transition>
</div> </div>
) );
} };
export default CreateDealButton; export default CreateDealButton;

View File

@@ -1,7 +1,7 @@
.inputs { .inputs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100% width: 100%;
} }
.inputs * { .inputs * {
@@ -12,4 +12,3 @@
display: flex; display: flex;
gap: rem(10); gap: rem(10);
} }

View File

@@ -5,15 +5,14 @@ import { useForm } from "@mantine/form";
import styles from "./CreateDealForm.module.css"; import styles from "./CreateDealForm.module.css";
import ClientAutocomplete from "../../Selects/ClientAutocomplete/ClientAutocomplete.tsx"; import ClientAutocomplete from "../../Selects/ClientAutocomplete/ClientAutocomplete.tsx";
import { DateTimePicker } from "@mantine/dates"; import { DateTimePicker } from "@mantine/dates";
import ShippingWarehouseAutocomplete import ShippingWarehouseAutocomplete from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
import BaseMarketplaceSelect from "../../Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx"; import BaseMarketplaceSelect from "../../Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import ServicePriceCategorySelect from "../../Selects/ServicePriceCategorySelect/ServicePriceCategorySelect.tsx"; import ServicePriceCategorySelect from "../../Selects/ServicePriceCategorySelect/ServicePriceCategorySelect.tsx";
type Props = { type Props = {
onSubmit: (quickDeal: QuickDeal) => void onSubmit: (quickDeal: QuickDeal) => void;
onCancel: () => void; onCancel: () => void;
} };
const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => { const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
const form = useForm<QuickDeal>({ const form = useForm<QuickDeal>({
initialValues: { initialValues: {
@@ -33,15 +32,15 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
return ( return (
<form <form
style={{ width: "100%" }} style={{ width: "100%" }}
onSubmit={form.onSubmit((values) => onSubmit(values))} onSubmit={form.onSubmit(values => onSubmit(values))}>
> <div
<div style={{ style={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
width: "100%", width: "100%",
gap: rem(10), gap: rem(10),
padding: rem(10), padding: rem(10),
}}> }}>
<div className={styles["inputs"]}> <div className={styles["inputs"]}>
<TextInput <TextInput
placeholder={"Название сделки"} placeholder={"Название сделки"}
@@ -63,7 +62,6 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
{...form.getInputProps("shippingWarehouse")} {...form.getInputProps("shippingWarehouse")}
placeholder={"Склад отгрузки"} placeholder={"Склад отгрузки"}
/> />
</div> </div>
<div className={styles["inputs"]}> <div className={styles["inputs"]}>
<ServicePriceCategorySelect <ServicePriceCategorySelect
@@ -89,20 +87,17 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
/> />
</div> </div>
<div className={styles["buttons"]}> <div className={styles["buttons"]}>
<Button <Button type={"submit"}>Добавить</Button>
type={"submit"}
>Добавить</Button>
<Button <Button
variant={"outline"} variant={"outline"}
onClick={() => onCancel()} onClick={() => onCancel()}>
>Отменить</Button> Отменить
</Button>
</div> </div>
</div> </div>
</form> </form>
); );
}; };
export default CreateDealFrom; export default CreateDealFrom;

View File

@@ -19,7 +19,6 @@
@mixin dark { @mixin dark {
background-color: var(--mantine-color-dark-5); background-color: var(--mantine-color-dark-5);
} }
} }
.flex-row-left { .flex-row-left {
@@ -27,13 +26,11 @@
} }
.flex-row { .flex-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
} }
.flex-row-right { .flex-row-right {
align-items: flex-end; align-items: flex-end;
} }

View File

@@ -1,109 +1,147 @@
import {FC} from "react"; import { FC } from "react";
import {DealService, DealSummary} from "../../../client"; import { DealService, DealSummary } from "../../../client";
import styles from './DealSummaryCard.module.css'; import styles from "./DealSummaryCard.module.css";
import {ActionIcon, Badge, CopyButton, Flex, Image, Popover, rem, Text} from '@mantine/core'; import {
ActionIcon,
Badge,
CopyButton,
Flex,
Image,
Popover,
rem,
Text,
} from "@mantine/core";
import classNames from "classnames"; import classNames from "classnames";
import {useDealPageContext} from "../../../pages/LeadsPage/contexts/DealPageContext.tsx"; import { useDealPageContext } from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {faCheck} from "@fortawesome/free-solid-svg-icons"; import { faCheck } from "@fortawesome/free-solid-svg-icons";
type Props = { type Props = {
dealSummary: DealSummary dealSummary: DealSummary;
} };
const DealSummaryCard: FC<Props> = ({dealSummary}) => { const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
const {setSelectedDeal} = useDealPageContext(); const { setSelectedDeal } = useDealPageContext();
const onDealSummaryClick = () => { const onDealSummaryClick = () => {
DealService.getDealById({dealId: dealSummary.id}) DealService.getDealById({ dealId: dealSummary.id }).then(deal => {
.then((deal) => { setSelectedDeal(deal);
setSelectedDeal(deal); });
}) };
}
const getDeadlineTextColor = (deadline: string) => { const getDeadlineTextColor = (deadline: string) => {
// generate three colors, yellow for 1 day, red for 0 days, green for more than 1 day // generate three colors, yellow for 1 day, red for 0 days, green for more than 1 day
const deadlineDate = new Date(deadline); const deadlineDate = new Date(deadline);
const currentDate = new Date(); const currentDate = new Date();
const diff = deadlineDate.getTime() - currentDate.getTime(); const diff = deadlineDate.getTime() - currentDate.getTime();
const diffDays = Math.ceil(diff / (1000 * 3600 * 24)); const diffDays = Math.ceil(diff / (1000 * 3600 * 24));
if (diffDays < 0) if (diffDays < 0) return "red.8"; // for past deadlines
return 'red.8'; // for past deadlines
if (diffDays === 1) { if (diffDays === 1) {
return 'yellow.8'; return "yellow.8";
} }
if (diffDays === 0) { if (diffDays === 0) {
return 'orange.8'; return "orange.8";
} }
return 'green.8'; return "green.8";
} };
return ( return (
<div onClick={() => onDealSummaryClick()} className={styles['container']}> <div
<div className={classNames(styles['flex-row'], styles["flex-row-left"])}> onClick={() => onDealSummaryClick()}
className={styles["container"]}>
<div className={styles['flex-item']}> <div
<Text size={"sm"} c={"gray.6"}> className={classNames(
styles["flex-row"],
styles["flex-row-left"]
)}>
<div className={styles["flex-item"]}>
<Text
size={"sm"}
c={"gray.6"}>
Клиент: {dealSummary.clientName} Клиент: {dealSummary.clientName}
</Text> </Text>
</div> </div>
<div className={styles['flex-item']}> <div className={styles["flex-item"]}>
<Text size={"md"} c={"blue.5"}>{dealSummary.name}</Text> <Text
{dealSummary.shipmentWarehouseName && size={"md"}
<Text size={"sm"} c={"gray.6"}>{dealSummary.shipmentWarehouseName}</Text> c={"blue.5"}>
} {dealSummary.name}
</Text>
{dealSummary.shipmentWarehouseName && (
<Text
size={"sm"}
c={"gray.6"}>
{dealSummary.shipmentWarehouseName}
</Text>
)}
</div> </div>
<div className={styles['flex-item']}> <div className={styles["flex-item"]}>
<Text size={"sm"} c={"gray.6"}> <Text
{dealSummary.totalPrice.toLocaleString('ru-RU')} руб, {dealSummary.totalProducts.toLocaleString("ru-RU")} тов. size={"sm"}
c={"gray.6"}>
{dealSummary.totalPrice.toLocaleString("ru-RU")} руб,{" "}
{dealSummary.totalProducts.toLocaleString("ru-RU")} тов.
</Text> </Text>
</div> </div>
</div> </div>
<div <div
className={classNames(
className={classNames(styles['flex-row'], styles['flex-row-right'])}> styles["flex-row"],
<div className={styles['flex-item']}> styles["flex-row-right"]
)}>
<div className={styles["flex-item"]}>
<ActionIcon variant={"transparent"}> <ActionIcon variant={"transparent"}>
<Image src={dealSummary.baseMarketplace?.iconUrl || ""}/> <Image
src={dealSummary.baseMarketplace?.iconUrl || ""}
/>
</ActionIcon> </ActionIcon>
</div> </div>
<div className={styles['flex-item']}> <div className={styles["flex-item"]}>
<Text size={"sm"} c={getDeadlineTextColor(dealSummary.deadline)}> <Text
{new Date(dealSummary.deadline).toLocaleString('ru-RU').slice(0, -3)} size={"sm"}
c={getDeadlineTextColor(dealSummary.deadline)}>
{new Date(dealSummary.deadline)
.toLocaleString("ru-RU")
.slice(0, -3)}
</Text> </Text>
</div> </div>
<CopyButton value={"https://google.com"}> <CopyButton value={"https://google.com"}>
{({copy, copied}) => ( {({ copy, copied }) => (
<Popover opened={copied} withArrow> <Popover
opened={copied}
withArrow>
<Popover.Target> <Popover.Target>
<div <div
onClick={e => {
onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
copy(); copy();
}} }}
className={styles['flex-item']}> className={styles["flex-item"]}>
<Badge variant={"light"} radius={"sm"}> <Badge
variant={"light"}
radius={"sm"}>
ID: {dealSummary.id} ID: {dealSummary.id}
</Badge> </Badge>
</div> </div>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<Flex justify={"center"} align={"center"} gap={rem(5)}> <Flex
justify={"center"}
align={"center"}
gap={rem(5)}>
<FontAwesomeIcon <FontAwesomeIcon
bounce bounce
style={{animationIterationCount: 1}} style={{ animationIterationCount: 1 }}
icon={faCheck} icon={faCheck}
/> />
<Text size={"xs"}>ID сделки скопирован</Text> <Text size={"xs"}>
ID сделки скопирован
</Text>
</Flex> </Flex>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
)} )}
</CopyButton> </CopyButton>
</div> </div>
</div> </div>
) );
} };
export default DealSummaryCard; export default DealSummaryCard;

View File

@@ -2,9 +2,13 @@
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: var(--mantine-radius-md); border-radius: var(--mantine-radius-md);
border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); border: rem(1px) solid
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
padding: var(--mantine-spacing-sm) var(--mantine-spacing-xl); padding: var(--mantine-spacing-sm) var(--mantine-spacing-xl);
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-5)); background-color: light-dark(
var(--mantine-color-white),
var(--mantine-color-dark-5)
);
margin-bottom: var(--mantine-spacing-sm); margin-bottom: var(--mantine-spacing-sm);
} }
@@ -16,4 +20,4 @@
font-size: rem(30px); font-size: rem(30px);
font-weight: 700; font-weight: 700;
width: rem(60px); width: rem(60px);
} }

View File

@@ -1,26 +1,25 @@
import styles from './Header.module.css'; import styles from "./Header.module.css";
import {Button, TextInput} from "@mantine/core"; import { Button, TextInput } from "@mantine/core";
import {FC} from "react"; import { FC } from "react";
const Header: FC = ()=>{
const Header: FC = () => {
return ( return (
<div className={styles['header']}> <div className={styles["header"]}>
<TextInput <TextInput
radius={0} radius={0}
placeholder={"Поиск и фильтры"} placeholder={"Поиск и фильтры"}
size={"xl"} size={"xl"}
className={styles['header-input']} className={styles["header-input"]}
/> />
<Button <Button
radius={0} radius={0}
color={"gray"} color={"gray"}
variant={'default'} variant={"default"}
className={styles['header-button']} className={styles["header-button"]}>
>Поиск</Button> Поиск
</Button>
</div> </div>
) );
} };
export default Header; export default Header;

View File

@@ -1,31 +1,45 @@
import {Dropzone, DropzoneProps, FileWithPath} from "@mantine/dropzone"; import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
import {FC, useState} from "react"; import { FC, useState } from "react";
import {Button, Fieldset, Flex, Group, Image, Loader, rem, Text} from "@mantine/core"; import {
import {IconPhoto, IconUpload, IconX} from "@tabler/icons-react"; Button,
import {omit} from "lodash"; Fieldset,
import {BaseFormInputProps} from "../../types/utils.ts"; Flex,
import {notifications} from "../../shared/lib/notifications.ts"; Group,
import {ProductService} from "../../client"; Image,
Loader,
rem,
Text,
} from "@mantine/core";
import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { omit } from "lodash";
import { BaseFormInputProps } from "../../types/utils.ts";
import { notifications } from "../../shared/lib/notifications.ts";
import { ProductService } from "../../client";
interface RestProps { interface RestProps {
imageUrlInputProps?: BaseFormInputProps<string>; imageUrlInputProps?: BaseFormInputProps<string>;
productId?: number; productId?: number;
} }
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
type Props = Omit<DropzoneProps, 'onDrop'> & RestProps;
const ImageDropzone: FC<Props> = (props: Props) => { const ImageDropzone: FC<Props> = (props: Props) => {
const [showDropzone, setShowDropzone] = useState( const [showDropzone, setShowDropzone] = useState(
!(typeof props.imageUrlInputProps?.value === 'string' && !(
props.imageUrlInputProps.value.trim() !== '') typeof props.imageUrlInputProps?.value === "string" &&
props.imageUrlInputProps.value.trim() !== ""
)
); );
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const restProps = omit(props, ['imageUrl', 'productId', 'imageUrlInputProps']); const restProps = omit(props, [
"imageUrl",
"productId",
"imageUrlInputProps",
]);
const onDrop = (files: FileWithPath[]) => { const onDrop = (files: FileWithPath[]) => {
if (!props.productId || !props.imageUrlInputProps) return; if (!props.productId || !props.imageUrlInputProps) return;
if (files.length > 1) { if (files.length > 1) {
notifications.error({message: "Прикрепите одно изображение"}); notifications.error({ message: "Прикрепите одно изображение" });
return; return;
} }
const file = files[0]; const file = files[0];
@@ -34,33 +48,35 @@ const ImageDropzone: FC<Props> = (props: Props) => {
ProductService.uploadProductImage({ ProductService.uploadProductImage({
productId: props.productId, productId: props.productId,
formData: { formData: {
upload_file: file upload_file: file,
} },
}).then(({ok, message, imageUrl}) => { })
notifications.guess(ok, {message}); .then(({ ok, message, imageUrl }) => {
setIsLoading(false); notifications.guess(ok, { message });
setIsLoading(false);
if (!ok || !imageUrl) { if (!ok || !imageUrl) {
setShowDropzone(true);
return;
}
props.imageUrlInputProps?.onChange(imageUrl);
setShowDropzone(false);
})
.catch(error => {
notifications.error({ message: error.toString() });
setShowDropzone(true); setShowDropzone(true);
setIsLoading(false);
return; });
} };
props.imageUrlInputProps?.onChange(imageUrl);
setShowDropzone(false);
}).catch(error => {
notifications.error({message: error.toString()});
setShowDropzone(true);
setIsLoading(false);
});
}
const getBody = () => { const getBody = () => {
return props.imageUrlInputProps?.value && !showDropzone ? ( return props.imageUrlInputProps?.value && !showDropzone ? (
<Image src={props.imageUrlInputProps.value}/> <Image src={props.imageUrlInputProps.value} />
) : ( ) : (
<Dropzone <Dropzone
{...restProps} {...restProps}
accept={["image/png", accept={[
"image/png",
"image/jpeg", "image/jpeg",
"image/gif", "image/gif",
"image/bmp", "image/bmp",
@@ -68,58 +84,74 @@ const ImageDropzone: FC<Props> = (props: Props) => {
"image/x-icon", "image/x-icon",
"image/webp", "image/webp",
"image/svg+xml", "image/svg+xml",
"image/heic"]} "image/heic",
]}
multiple={false} multiple={false}
onDrop={onDrop} onDrop={onDrop}>
<Group
> justify="center"
<Group justify="center" gap="xl" style={{pointerEvents: 'none'}}> gap="xl"
style={{ pointerEvents: "none" }}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload <IconUpload
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)'}} style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-blue-6)",
}}
stroke={1.5} stroke={1.5}
/> />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX <IconX
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)'}} style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-red-6)",
}}
stroke={1.5} stroke={1.5}
/> />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto <IconPhoto
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-dimmed)'}} style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-dimmed)",
}}
stroke={1.5} stroke={1.5}
/> />
</Dropzone.Idle> </Dropzone.Idle>
<div style={{textAlign: "center"}}> <div style={{ textAlign: "center" }}>
<Text size="xl" inline> <Text
size="xl"
inline>
Перенесите изображение или нажмите чтоб выбрать файл Перенесите изображение или нажмите чтоб выбрать файл
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
); );
} };
return ( return (
<Flex <Flex
gap={rem(10)} gap={rem(10)}
direction={"column"} direction={"column"}>
<Fieldset legend={"Изображение"}>
>
<Fieldset
legend={'Изображение'}>
<Flex justify={"center"}> <Flex justify={"center"}>
{isLoading ? <Loader/> : getBody()} {isLoading ? <Loader /> : getBody()}
</Flex> </Flex>
</Fieldset> </Fieldset>
{!showDropzone && {!showDropzone && (
<Button onClick={() => setShowDropzone(true)} variant={"default"}>Заменить изображение {}</Button> <Button
} onClick={() => setShowDropzone(true)}
variant={"default"}>
Заменить изображение {}
</Button>
)}
</Flex> </Flex>
) );
} };
export default ImageDropzone; export default ImageDropzone;

View File

@@ -1,34 +1,47 @@
.control { .control {
font-weight: 500; font-weight: 500;
display: block; display: block;
width: 100%; width: 100%;
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md); padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
color: var(--mantine-color-text); color: var(--mantine-color-text);
font-size: var(--mantine-font-size-sm); font-size: var(--mantine-font-size-sm);
@mixin hover { @mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); background-color: light-dark(
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); var(--mantine-color-gray-0),
} var(--mantine-color-dark-7)
);
color: light-dark(
var(--mantine-color-black),
var(--mantine-color-dark-0)
);
}
} }
.link { .link {
font-weight: 500; font-weight: 500;
display: block; display: block;
text-decoration: none; text-decoration: none;
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md); padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
padding-left: var(--mantine-spacing-md); padding-left: var(--mantine-spacing-md);
margin-left: var(--mantine-spacing-xl); margin-left: var(--mantine-spacing-xl);
font-size: var(--mantine-font-size-sm); font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
border-left: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); border-left: 1px solid
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
@mixin hover { @mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); background-color: light-dark(
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); var(--mantine-color-gray-0),
} var(--mantine-color-dark-7)
);
color: light-dark(
var(--mantine-color-black),
var(--mantine-color-dark-0)
);
}
} }
.chevron { .chevron {
transition: transform 200ms ease; transition: transform 200ms ease;
} }

View File

@@ -1,8 +1,15 @@
import {useState} from 'react'; import { useState } from "react";
import {Box, Collapse, Group, rem, ThemeIcon, UnstyledButton} from '@mantine/core'; import {
import {IconCalendarStats, IconChevronRight} from '@tabler/icons-react'; Box,
import classes from './LinksGroup.module.css'; Collapse,
import {Link} from "@tanstack/react-router"; Group,
rem,
ThemeIcon,
UnstyledButton,
} from "@mantine/core";
import { IconCalendarStats, IconChevronRight } from "@tabler/icons-react";
import classes from "./LinksGroup.module.css";
import { Link } from "@tanstack/react-router";
interface LinksGroupProps { interface LinksGroupProps {
icon: React.FC<any>; icon: React.FC<any>;
@@ -11,28 +18,36 @@ interface LinksGroupProps {
links?: { label: string; link: string }[]; links?: { label: string; link: string }[];
} }
export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGroupProps) { export function LinksGroup({
icon: Icon,
label,
initiallyOpened,
links,
}: LinksGroupProps) {
const hasLinks = Array.isArray(links); const hasLinks = Array.isArray(links);
const [opened, setOpened] = useState(initiallyOpened || false); const [opened, setOpened] = useState(initiallyOpened || false);
const items = (hasLinks ? links : []).map((link) => ( const items = (hasLinks ? links : []).map(link => (
<Link to={link.link} <Link
className={classes.link} to={link.link}
key={link.label} className={classes.link}
key={link.label}>
>
{link.label} {link.label}
</Link> </Link>
)); ));
return ( return (
<> <>
<UnstyledButton onClick={() => setOpened((o) => !o)} className={classes.control}> <UnstyledButton
<Group justify="space-between" gap={0}> onClick={() => setOpened(o => !o)}
<Box style={{display: 'flex', alignItems: 'center'}}> className={classes.control}>
<ThemeIcon variant="light" size={30}> <Group
<Icon style={{width: rem(18), height: rem(18)}}/> justify="space-between"
gap={0}>
<Box style={{ display: "flex", alignItems: "center" }}>
<ThemeIcon
variant="light"
size={30}>
<Icon style={{ width: rem(18), height: rem(18) }} />
</ThemeIcon> </ThemeIcon>
<Box ml="md">{label}</Box> <Box ml="md">{label}</Box>
</Box> </Box>
@@ -43,7 +58,7 @@ export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGro
style={{ style={{
width: rem(16), width: rem(16),
height: rem(16), height: rem(16),
transform: opened ? 'rotate(-90deg)' : 'none', transform: opened ? "rotate(-90deg)" : "none",
}} }}
/> />
)} )}
@@ -55,19 +70,21 @@ export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGro
} }
const mockdata = { const mockdata = {
label: 'Releases', label: "Releases",
icon: IconCalendarStats, icon: IconCalendarStats,
links: [ links: [
{label: 'Upcoming releases', link: '/'}, { label: "Upcoming releases", link: "/" },
{label: 'Previous releases', link: '/'}, { label: "Previous releases", link: "/" },
{label: 'Releases schedule', link: '/'}, { label: "Releases schedule", link: "/" },
], ],
}; };
export function NavbarLinksGroup() { export function NavbarLinksGroup() {
return ( return (
<Box mih={220} p="md"> <Box
mih={220}
p="md">
<LinksGroup {...mockdata} /> <LinksGroup {...mockdata} />
</Box> </Box>
); );
} }

View File

@@ -1,12 +1,17 @@
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) { export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
return ( return (
<svg {...props} version="1.0" xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 1024.000000 1024.000000" {...props}
preserveAspectRatio="xMidYMid meet"> version="1.0"
xmlns="http://www.w3.org/2000/svg"
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)" viewBox="0 0 1024.000000 1024.000000"
fill="#2a75ec" stroke="none"> preserveAspectRatio="xMidYMid meet">
<path d="M2600 7290 l0 -760 690 0 690 0 0 196 0 195 728 -4 c613 -3 743 -7 <g
transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
fill="#2a75ec"
stroke="none">
<path
d="M2600 7290 l0 -760 690 0 690 0 0 196 0 195 728 -4 c613 -3 743 -7
831 -20 57 -10 106 -15 108 -13 7 7 341 -80 358 -93 8 -7 79 -34 95 -36 28 -4 831 -20 57 -10 106 -15 108 -13 7 7 341 -80 358 -93 8 -7 79 -34 95 -36 28 -4
293 -151 298 -166 2 -5 8 -9 13 -9 9 0 75 -51 177 -138 44 -38 120 -123 218 293 -151 298 -166 2 -5 8 -9 13 -9 9 0 75 -51 177 -138 44 -38 120 -123 218
-245 73 -91 215 -390 230 -484 3 -21 8 -42 11 -46 20 -33 46 -190 58 -355 12 -245 73 -91 215 -390 230 -484 3 -21 8 -42 11 -46 20 -33 46 -190 58 -355 12
@@ -27,11 +32,13 @@ export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
-40 57 -103 155 -5 9 -15 20 -21 26 -6 5 -40 48 -76 95 -127 165 -291 325 -40 57 -103 155 -5 9 -15 20 -21 26 -6 5 -40 48 -76 95 -127 165 -291 325
-473 459 -38 29 -83 62 -98 74 -16 12 -40 29 -55 37 -15 8 -67 39 -115 69 -99 -473 459 -38 29 -83 62 -98 74 -16 12 -40 29 -55 37 -15 8 -67 39 -115 69 -99
59 -339 177 -443 216 -245 93 -524 167 -774 205 -335 51 -306 50 -1877 50 59 -339 177 -443 216 -245 93 -524 167 -774 205 -335 51 -306 50 -1877 50
l-1473 0 0 -760z"/> l-1473 0 0 -760z"
<path d="M1730 5175 l0 -1095 1580 0 1580 0 0 1095 0 1095 -1580 0 -1580 0 0 />
-1095z"/> <path
d="M1730 5175 l0 -1095 1580 0 1580 0 0 1095 0 1095 -1580 0 -1580 0 0
-1095z"
/>
</g> </g>
</svg> </svg>
);
) }
}

View File

@@ -6,11 +6,15 @@
align-items: center; align-items: center;
@mixin dark { @mixin dark {
background-color: var(--mantine-color-body); background-color: var(--mantine-color-body);
box-shadow: 0 2px 4px var(--mantine-color-dark-7), 0 4px 24px var(--mantine-color-dark-7); box-shadow:
0 2px 4px var(--mantine-color-dark-7),
0 4px 24px var(--mantine-color-dark-7);
} }
@mixin light { @mixin light {
background-color: #f9f9f9; background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0, 0, 0, .08), 0 4px 24px rgba(0, 0, 0, .08); box-shadow:
0 2px 4px rgba(0, 0, 0, 0.08),
0 4px 24px rgba(0, 0, 0, 0.08);
} }
border-radius: rem(20); border-radius: rem(20);
width: 100%; width: 100%;
@@ -32,7 +36,10 @@
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
&:hover { &:hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5)); background-color: light-dark(
var(--mantine-color-gray-0),
var(--mantine-color-dark-5)
);
} }
&[data-active] { &[data-active] {
@@ -43,4 +50,3 @@
} }
} }
} }

View File

@@ -1,20 +1,31 @@
import {Center, Flex, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColorScheme} from '@mantine/core'; import {
Center,
Flex,
Image,
rem,
Stack,
Tooltip,
UnstyledButton,
useMantineColorScheme,
} from "@mantine/core";
import { import {
IconBarcode, IconBarcode,
IconBox, IconBuildingWarehouse, IconBox,
IconBuildingWarehouse,
IconCash, IconCash,
IconDashboard, IconDashboard,
IconFileBarcode, IconFileBarcode,
IconHome2, IconHome2,
IconLogout, IconLogout,
IconMan, IconMan,
IconMoon, IconShoppingCart, IconMoon,
IconShoppingCart,
IconSun, IconSun,
} from '@tabler/icons-react'; } from "@tabler/icons-react";
import classes from './Navbar.module.css'; import classes from "./Navbar.module.css";
import {useAppDispatch} from "../../redux/store.ts"; import { useAppDispatch } from "../../redux/store.ts";
import {logout} from "../../features/authSlice.ts"; import { logout } from "../../features/authSlice.ts";
import {useNavigate, useRouterState} from "@tanstack/react-router"; import { useNavigate, useRouterState } from "@tanstack/react-router";
interface NavbarLinkProps { interface NavbarLinkProps {
icon: typeof IconHome2; icon: typeof IconHome2;
@@ -28,24 +39,31 @@ interface NavbarLinkProps {
} }
function NavbarLink(props: NavbarLinkProps) { function NavbarLink(props: NavbarLinkProps) {
const {icon: Icon, label, active, onClick} = props; const { icon: Icon, label, active, onClick } = props;
return ( return (
<Tooltip display={!label ? "none" : "flex"} label={label} position="right" transitionProps={{duration: 0}}> <Tooltip
<UnstyledButton onClick={() => onClick && onClick(props)} display={!label ? "none" : "flex"}
className={classes.link} label={label}
data-active={active || undefined}> position="right"
<Icon style={{width: rem(20), height: rem(20)}} stroke={1.5}/> transitionProps={{ duration: 0 }}>
<UnstyledButton
onClick={() => onClick && onClick(props)}
className={classes.link}
data-active={active || undefined}>
<Icon
style={{ width: rem(20), height: rem(20) }}
stroke={1.5}
/>
</UnstyledButton> </UnstyledButton>
</Tooltip> </Tooltip>
); );
} }
const mockdata = [ const mockdata = [
{ {
icon: IconCash, icon: IconCash,
label: 'Сделки', label: "Сделки",
href: '/leads' href: "/leads",
}, },
// { // {
// icon: IconTable, // icon: IconTable,
@@ -54,48 +72,50 @@ const mockdata = [
// }, // },
{ {
icon: IconMan, icon: IconMan,
label: 'Клиенты', label: "Клиенты",
href: '/clients' href: "/clients",
}, },
{ {
icon: IconBox, icon: IconBox,
label: 'Услуги', label: "Услуги",
href: '/services' href: "/services",
}, },
{ {
icon: IconBarcode, icon: IconBarcode,
label: 'Товары', label: "Товары",
href: '/products' href: "/products",
}, },
{ {
icon: IconFileBarcode, icon: IconFileBarcode,
label: 'Штрихкоды', label: "Штрихкоды",
href: '/barcode' href: "/barcode",
}, },
{ {
icon: IconBuildingWarehouse, icon: IconBuildingWarehouse,
label: 'Склады отгрузки', label: "Склады отгрузки",
href: '/shipping_warehouses' href: "/shipping_warehouses",
}, },
{ {
icon:IconShoppingCart, icon: IconShoppingCart,
label: 'Маркетплейсы', label: "Маркетплейсы",
href: '/marketplaces' href: "/marketplaces",
} },
]; ];
export function Navbar() { export function Navbar() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const router = useRouterState(); const router = useRouterState();
const {colorScheme, toggleColorScheme} = useMantineColorScheme({keepTransitions: true}); const { colorScheme, toggleColorScheme } = useMantineColorScheme({
keepTransitions: true,
});
const onLogoutClick = () => { const onLogoutClick = () => {
dispatch(logout()); dispatch(logout());
navigate({to: '/login'}); navigate({ to: "/login" });
} };
const onNavlinkClick = (props: NavbarLinkProps) => { const onNavlinkClick = (props: NavbarLinkProps) => {
navigate({to: props.href}); navigate({ to: props.href });
} };
const links = mockdata.map((link, index) => ( const links = mockdata.map((link, index) => (
<NavbarLink <NavbarLink
{...link} {...link}
@@ -108,35 +128,62 @@ export function Navbar() {
return ( return (
<nav className={classes.navbar}> <nav className={classes.navbar}>
<Flex direction={"column"} gap={rem(30)}> <Flex
<Center direction={"column"}
p={rem(5)} gap={rem(30)}>
> <Center p={rem(5)}>
<Image <Image
flex={1} flex={1}
// style={{filter: "drop-shadow(0 0 30px #fff)"}} // style={{filter: "drop-shadow(0 0 30px #fff)"}}
src={colorScheme == "dark" ? "/icons/logo-light.png" : "/icons/logo.png"} src={
colorScheme == "dark"
? "/icons/logo-light.png"
: "/icons/logo.png"
}
/> />
</Center> </Center>
<div className={classes.navbarMain}> <div className={classes.navbarMain}>
<Stack justify="center" gap={rem(10)}> <Stack
justify="center"
gap={rem(10)}>
{links} {links}
</Stack> </Stack>
</div> </div>
</Flex> </Flex>
<Stack w={"100%"} justify="center" gap={0}> <Stack
<NavbarLink icon={IconDashboard} w={"100%"}
href={"/admin"} justify="center"
index={-1} gap={0}>
label={"Панель администратора"} <NavbarLink
onClick={() => onNavlinkClick({href: "/admin", index: -1, icon: IconDashboard})} icon={IconDashboard}
href={"/admin"}
index={-1}
label={"Панель администратора"}
onClick={() =>
onNavlinkClick({
href: "/admin",
index: -1,
icon: IconDashboard,
})
}
/>
<NavbarLink
label={"Сменить тему"}
onClick={toggleColorScheme}
icon={colorScheme == "dark" ? IconSun : IconMoon}
href={"#"}
index={-1}
/>
<NavbarLink
index={-1}
href={"#"}
onClick={onLogoutClick}
icon={IconLogout}
label="Выйти"
/> />
<NavbarLink label={"Сменить тему"} onClick={toggleColorScheme}
icon={colorScheme == "dark" ? IconSun : IconMoon} href={"#"} index={-1}/>
<NavbarLink index={-1} href={"#"} onClick={onLogoutClick} icon={IconLogout} label="Выйти"/>
</Stack> </Stack>
</nav> </nav>
); );
} }

View File

@@ -1,10 +1,13 @@
.user { .user {
display: block; display: block;
width: 100%; width: 100%;
padding: var(--mantine-spacing-md); padding: var(--mantine-spacing-md);
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
@mixin hover { @mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8)); background-color: light-dark(
} var(--mantine-color-gray-0),
} var(--mantine-color-dark-8)
);
}
}

View File

@@ -1,6 +1,6 @@
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core'; import { UnstyledButton, Group, Avatar, Text, rem } from "@mantine/core";
import { IconChevronRight } from '@tabler/icons-react'; import { IconChevronRight } from "@tabler/icons-react";
import classes from './UserButton.module.css'; import classes from "./UserButton.module.css";
export function UserButton() { export function UserButton() {
return ( return (
@@ -12,17 +12,24 @@ export function UserButton() {
/> />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Text size="sm" fw={500}> <Text
size="sm"
fw={500}>
Harriette Spoonlicker Harriette Spoonlicker
</Text> </Text>
<Text c="dimmed" size="xs"> <Text
c="dimmed"
size="xs">
hspoonlicker@outlook.com hspoonlicker@outlook.com
</Text> </Text>
</div> </div>
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} /> <IconChevronRight
style={{ width: rem(14), height: rem(14) }}
stroke={1.5}
/>
</Group> </Group>
</UnstyledButton> </UnstyledButton>
); );
} }

View File

@@ -1,25 +1,23 @@
import {IconAdjustments, IconGauge} from "@tabler/icons-react"; import { IconAdjustments, IconGauge } from "@tabler/icons-react";
export const NavbarLinks = [ export const NavbarLinks = [
{ {
label: 'Главная', label: "Главная",
icon: IconGauge, icon: IconGauge,
links: [ links: [
{ {
label: "123", label: "123",
link: "/login" link: "/login",
}, },
], ],
}, },
{ {
label: 'Настройки', label: "Настройки",
icon: IconAdjustments, icon: IconAdjustments,
links:[ links: [
{ {
label: "Профиль" label: "Профиль",
} },
] ],
}, },
]; ];

View File

@@ -1,40 +1,44 @@
import {Autocomplete, AutocompleteProps} from "@mantine/core"; import { Autocomplete, AutocompleteProps } from "@mantine/core";
import {useEffect, useMemo, useState} from "react"; import { useEffect, useMemo, useState } from "react";
import {ObjectWithNameAndId} from "../../types/utils.ts"; import { ObjectWithNameAndId } from "../../types/utils.ts";
import {omit} from "lodash"; import { omit } from "lodash";
export type AutocompleteObjectType<T extends ObjectWithNameAndId> = T; export type AutocompleteObjectType<T extends ObjectWithNameAndId> = T;
type ControlledValueProps<T extends ObjectWithNameAndId> = { type ControlledValueProps<T extends ObjectWithNameAndId> = {
value: AutocompleteObjectType<T>, value: AutocompleteObjectType<T>;
onChange: (value: string) => void; onChange: (value: string) => void;
} };
type RestProps<T extends ObjectWithNameAndId> = { type RestProps<T extends ObjectWithNameAndId> = {
defaultValue?: AutocompleteObjectType<T> defaultValue?: AutocompleteObjectType<T>;
onChange: (value: string) => void; onChange: (value: string) => void;
data: AutocompleteObjectType<T>[]; data: AutocompleteObjectType<T>[];
filterBy?: (item: AutocompleteObjectType<T>) => boolean; filterBy?: (item: AutocompleteObjectType<T>) => boolean;
} };
export type ObjectAutocompleteProps<T extends ObjectWithNameAndId> = export type ObjectAutocompleteProps<T extends ObjectWithNameAndId> =
(RestProps<T> & Partial<ControlledValueProps<T>>) (RestProps<T> & Partial<ControlledValueProps<T>>) &
& Omit<AutocompleteProps, 'value' | 'onChange' | 'data'>; Omit<AutocompleteProps, "value" | "onChange" | "data">;
const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutocompleteProps<T>) => { const ObjectAutocomplete = <T extends ObjectWithNameAndId>(
props: ObjectAutocompleteProps<T>
const isControlled = 'value' in props; ) => {
const [internalValue, setInternalValue] = useState<undefined | string>(props.defaultValue); const isControlled = "value" in props;
const [internalValue, setInternalValue] = useState<undefined | string>(
props.defaultValue
);
const value = isControlled ? props.value?.name : internalValue; const value = isControlled ? props.value?.name : internalValue;
const data = useMemo(() => { const data = useMemo(() => {
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data; const propsData = props.filterBy
? props.data.filter(props.filterBy)
: props.data;
return propsData.map(item => ({ return propsData.map(item => ({
label: item.name, label: item.name,
value: item.id.toString() value: item.id.toString(),
})); }));
}, [props.data]); }, [props.data]);
@@ -45,13 +49,13 @@ const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutoco
return; return;
} }
setInternalValue(event); setInternalValue(event);
} };
useEffect(() => { useEffect(() => {
if (isControlled || !internalValue) return; if (isControlled || !internalValue) return;
props.onChange(internalValue); props.onChange(internalValue);
}, [internalValue]); }, [internalValue]);
const restProps = omit(props, ['filterBy', 'groupBy']); const restProps = omit(props, ["filterBy", "groupBy"]);
return ( return (
<Autocomplete <Autocomplete
{...restProps} {...restProps}
@@ -59,7 +63,7 @@ const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutoco
onChange={handleOnChange} onChange={handleOnChange}
data={data} data={data}
/> />
) );
} };
export default ObjectAutocomplete; export default ObjectAutocomplete;

View File

@@ -1,83 +1,91 @@
import {MultiSelect, MultiSelectProps} from "@mantine/core"; import { MultiSelect, MultiSelectProps } from "@mantine/core";
import {useEffect, useMemo, useState} from "react"; import { useEffect, useMemo, useState } from "react";
import {groupBy} from "lodash"; import { groupBy } from "lodash";
interface ObjectWithIdAndName { interface ObjectWithIdAndName {
id: number, id: number;
name: string name: string;
} }
export type MultiselectObjectType<T> = T; export type MultiselectObjectType<T> = T;
type ControlledValueProps<T> = { type ControlledValueProps<T> = {
value: MultiselectObjectType<T>[], value: MultiselectObjectType<T>[];
onChange: (value: MultiselectObjectType<T>[]) => void; onChange: (value: MultiselectObjectType<T>[]) => void;
} };
type CustomLabelAndKeyProps<T> = { type CustomLabelAndKeyProps<T> = {
getLabelFn: (item: MultiselectObjectType<T>) => string; getLabelFn: (item: MultiselectObjectType<T>) => string;
getValueFn: (item: MultiselectObjectType<T>) => string; getValueFn: (item: MultiselectObjectType<T>) => string;
} };
type RestProps<T> = { type RestProps<T> = {
defaultValue?: MultiselectObjectType<T>[] defaultValue?: MultiselectObjectType<T>[];
onChange: (value: MultiselectObjectType<T>[]) => void; onChange: (value: MultiselectObjectType<T>[]) => void;
data: MultiselectObjectType<T>[]; data: MultiselectObjectType<T>[];
groupBy?: (item: MultiselectObjectType<T>) => string; groupBy?: (item: MultiselectObjectType<T>) => string;
filterBy?: (item: MultiselectObjectType<T>) => boolean; filterBy?: (item: MultiselectObjectType<T>) => boolean;
} };
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => { const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
return item.name; return item.name;
} };
const defaultGetValueFn = <T extends { id: number }>(item: T): string => { const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
return item.id.toString(); return item.id.toString();
} };
export type ObjectMultiSelectProps<T> = export type ObjectMultiSelectProps<T> = (RestProps<T> &
(RestProps<T> & Partial<ControlledValueProps<T>>) Partial<ControlledValueProps<T>>) &
& Omit<MultiSelectProps, 'value' | 'onChange' | 'data'> Omit<MultiSelectProps, "value" | "onChange" | "data"> &
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>); (T extends ObjectWithIdAndName
? Partial<CustomLabelAndKeyProps<T>>
: CustomLabelAndKeyProps<T>);
const ObjectMultiSelect = <T, >(props: ObjectMultiSelectProps<T>) => { const ObjectMultiSelect = <T,>(props: ObjectMultiSelectProps<T>) => {
const isControlled = "value" in props;
const haveGetValueFn = "getValueFn" in props;
const haveGetLabelFn = "getLabelFn" in props;
const isControlled = 'value' in props; const [internalValue, setInternalValue] = useState<
const haveGetValueFn = 'getValueFn' in props; MultiselectObjectType<T>[] | undefined
const haveGetLabelFn = 'getLabelFn' in props; >(props.defaultValue);
const [internalValue, setInternalValue] = useState<MultiselectObjectType<T>[] | undefined>(props.defaultValue);
const value = (isControlled ? props.value : internalValue) || []; const value = (isControlled ? props.value : internalValue) || [];
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn; const getValueFn =
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn; (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
const getLabelFn =
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
const data = useMemo(() => { const data = useMemo(() => {
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data; const propsData = props.filterBy
? props.data.filter(props.filterBy)
: props.data;
if (props.groupBy) { if (props.groupBy) {
const groupedData = groupBy(propsData, props.groupBy); const groupedData = groupBy(propsData, props.groupBy);
return Object.entries(groupedData).map(([group, items]) => ({ return Object.entries(groupedData).map(([group, items]) => ({
group, group,
items: items.map(item => ({ items: items.map(item => ({
label: getLabelFn(item), label: getLabelFn(item),
value: getValueFn(item) value: getValueFn(item),
})) })),
})); }));
} else { } else {
return propsData.map(item => ({ return propsData.map(item => ({
label: getLabelFn(item), label: getLabelFn(item),
value: getValueFn(item) value: getValueFn(item),
})); }));
} }
}, [props.data, props.groupBy]); }, [props.data, props.groupBy]);
const handleOnChange = (event: string[]) => { const handleOnChange = (event: string[]) => {
const objects = props.data.filter(item => event.includes(getValueFn(item))); const objects = props.data.filter(item =>
event.includes(getValueFn(item))
);
if (isControlled) { if (isControlled) {
props.onChange(objects); props.onChange(objects);
return; return;
} }
setInternalValue(objects); setInternalValue(objects);
} };
useEffect(() => { useEffect(() => {
if (isControlled || !internalValue) return; if (isControlled || !internalValue) return;
props.onChange(internalValue); props.onChange(internalValue);
@@ -90,7 +98,7 @@ const ObjectMultiSelect = <T, >(props: ObjectMultiSelectProps<T>) => {
onChange={handleOnChange} onChange={handleOnChange}
data={data} data={data}
/> />
) );
} };
export default ObjectMultiSelect; export default ObjectMultiSelect;

View File

@@ -1,73 +1,77 @@
import {Select, SelectProps} from "@mantine/core"; import { Select, SelectProps } from "@mantine/core";
import {useEffect, useMemo, useState} from "react"; import { useEffect, useMemo, useState } from "react";
import {groupBy, omit} from "lodash"; import { groupBy, omit } from "lodash";
interface ObjectWithIdAndName { interface ObjectWithIdAndName {
id: number, id: number;
name: string name: string;
} }
export type SelectObjectType<T> = T; export type SelectObjectType<T> = T;
type ControlledValueProps<T> = { type ControlledValueProps<T> = {
value: SelectObjectType<T>, value: SelectObjectType<T>;
onChange: (value: SelectObjectType<T>) => void; onChange: (value: SelectObjectType<T>) => void;
} };
type CustomLabelAndKeyProps<T> = { type CustomLabelAndKeyProps<T> = {
getLabelFn: (item: SelectObjectType<T>) => string; getLabelFn: (item: SelectObjectType<T>) => string;
getValueFn: (item: SelectObjectType<T>) => string; getValueFn: (item: SelectObjectType<T>) => string;
} };
type RestProps<T> = { type RestProps<T> = {
defaultValue?: SelectObjectType<T> defaultValue?: SelectObjectType<T>;
onChange: (value: SelectObjectType<T>) => void; onChange: (value: SelectObjectType<T>) => void;
data: SelectObjectType<T>[]; data: SelectObjectType<T>[];
groupBy?: (item: SelectObjectType<T>) => string; groupBy?: (item: SelectObjectType<T>) => string;
filterBy?: (item: SelectObjectType<T>) => boolean; filterBy?: (item: SelectObjectType<T>) => boolean;
}; };
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => { const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
return item.name; return item.name;
} };
const defaultGetValueFn = <T extends { id: number }>(item: T): string => { const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
if (!item) return item; if (!item) return item;
return item.id.toString(); return item.id.toString();
} };
export type ObjectSelectProps<T> = export type ObjectSelectProps<T> = (RestProps<T> &
(RestProps<T> & Partial<ControlledValueProps<T>>) Partial<ControlledValueProps<T>>) &
& Omit<SelectProps, 'value' | 'onChange' | 'data'> Omit<SelectProps, "value" | "onChange" | "data"> &
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>) (T extends ObjectWithIdAndName
? Partial<CustomLabelAndKeyProps<T>>
: CustomLabelAndKeyProps<T>);
const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => { const ObjectSelect = <T,>(props: ObjectSelectProps<T>) => {
const isControlled = "value" in props;
const isControlled = 'value' in props; const haveGetValueFn = "getValueFn" in props;
const haveGetValueFn = 'getValueFn' in props; const haveGetLabelFn = "getLabelFn" in props;
const haveGetLabelFn = 'getLabelFn' in props; const [internalValue, setInternalValue] = useState<
const [internalValue, setInternalValue] = useState<SelectObjectType<T> | undefined>(props.defaultValue); SelectObjectType<T> | undefined
>(props.defaultValue);
const value = isControlled ? props.value : internalValue; const value = isControlled ? props.value : internalValue;
const getValueFn =
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn; (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn; const getLabelFn =
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
const data = useMemo(() => { const data = useMemo(() => {
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data; const propsData = props.filterBy
? props.data.filter(props.filterBy)
: props.data;
if (props.groupBy) { if (props.groupBy) {
const groupedData = groupBy(propsData, props.groupBy); const groupedData = groupBy(propsData, props.groupBy);
return Object.entries(groupedData).map(([group, items]) => ({ return Object.entries(groupedData).map(([group, items]) => ({
group, group,
items: items.map(item => ({ items: items.map(item => ({
label: getLabelFn(item), label: getLabelFn(item),
value: getValueFn(item) value: getValueFn(item),
})) })),
})); }));
} else { } else {
return propsData.map(item => ({ return propsData.map(item => ({
label: getLabelFn(item), label: getLabelFn(item),
value: getValueFn(item) value: getValueFn(item),
})); }));
} }
}, [props.data, props.groupBy]); }, [props.data, props.groupBy]);
@@ -81,14 +85,19 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
return; return;
} }
setInternalValue(object); setInternalValue(object);
} };
useEffect(() => { useEffect(() => {
if (isControlled || !internalValue) return; if (isControlled || !internalValue) return;
props.onChange(internalValue); props.onChange(internalValue);
}, [internalValue]); }, [internalValue]);
const restProps = omit(props, ['filterBy', 'groupBy', 'getValueFn', 'getLabelFn']); const restProps = omit(props, [
"filterBy",
"groupBy",
"getValueFn",
"getLabelFn",
]);
return ( return (
<Select <Select
{...restProps} {...restProps}
@@ -96,7 +105,7 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
onChange={handleOnChange} onChange={handleOnChange}
data={data} data={data}
/> />
) );
} };
export default ObjectSelect; export default ObjectSelect;

View File

@@ -3,14 +3,17 @@
background-color: #f9f9f9; background-color: #f9f9f9;
@mixin dark { @mixin dark {
background-color: var(--mantine-color-body); background-color: var(--mantine-color-body);
box-shadow: 0 2px 4px var(--mantine-color-dark-7), 0 4px 24px var(--mantine-color-dark-7); box-shadow:
0 2px 4px var(--mantine-color-dark-7),
0 4px 24px var(--mantine-color-dark-7);
} }
@mixin light { @mixin light {
background-color: #f9f9f9; background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0, 0, 0, .08), 0 4px 24px rgba(0, 0, 0, .08); box-shadow:
0 2px 4px rgba(0, 0, 0, 0.08),
0 4px 24px rgba(0, 0, 0, 0.08);
} }
padding: rem(15); padding: rem(15);
} }
.container-fluid { .container-fluid {
@@ -23,5 +26,4 @@
.container-full-height-fixed { .container-full-height-fixed {
height: calc(100vh - (rem(20) * 2)); height: calc(100vh - (rem(20) * 2));
}
}

View File

@@ -1,27 +1,32 @@
import {CSSProperties, FC, ReactNode} from "react"; import { CSSProperties, FC, ReactNode } from "react";
import styles from './PageBlock.module.css'; import styles from "./PageBlock.module.css";
import classNames from "classnames"; import classNames from "classnames";
type Props = { type Props = {
children: ReactNode children: ReactNode;
fluid?: boolean; fluid?: boolean;
style?: CSSProperties; style?: CSSProperties;
fullHeight?: boolean; fullHeight?: boolean;
fullHeightFixed?: boolean; fullHeightFixed?: boolean;
} };
export const PageBlock: FC<Props> = ({children, style, fluid = true, fullHeight = false, fullHeightFixed = false}) => { export const PageBlock: FC<Props> = ({
children,
style,
fluid = true,
fullHeight = false,
fullHeightFixed = false,
}) => {
return ( return (
<div style={style} <div
className={ style={style}
classNames( className={classNames(
styles['container'], styles["container"],
fluid && styles['container-fluid'], fluid && styles["container-fluid"],
fullHeight && styles['container-full-height'], fullHeight && styles["container-full-height"],
fullHeightFixed && styles['container-full-height-fixed'] fullHeightFixed && styles["container-full-height-fixed"]
) )}>
}>
{children} {children}
</div> </div>
) );
} };
export default PageBlock; export default PageBlock;

View File

@@ -1,3 +1,3 @@
.number-input { .number-input {
width: rem(50); width: rem(50);
} }

View File

@@ -1,17 +1,17 @@
import {ActionIcon, Flex, NumberInput, rem} from "@mantine/core"; import { ActionIcon, Flex, NumberInput, rem } from "@mantine/core";
import {IconMinus, IconPlus} from "@tabler/icons-react"; import { IconMinus, IconPlus } from "@tabler/icons-react";
import styles from './PlusMinusInput.module.css'; import styles from "./PlusMinusInput.module.css";
import {FC, useEffect, useState} from "react"; import { FC, useEffect, useState } from "react";
type ControlledValueProps = { type ControlledValueProps = {
value: number; value: number;
onChange: (value: number) => void; onChange: (value: number) => void;
} };
type RestProps = { type RestProps = {
defaultValue?: number; defaultValue?: number;
onChange: (value: number) => void; onChange: (value: number) => void;
} };
type Props = RestProps & Partial<ControlledValueProps>; type Props = RestProps & Partial<ControlledValueProps>;
@@ -22,7 +22,6 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
const value = isControlled ? props.value : internalValue; const value = isControlled ? props.value : internalValue;
const onMinusClick = () => { const onMinusClick = () => {
const newValue = (value || 0) - 1; const newValue = (value || 0) - 1;
if (newValue < 0) { if (newValue < 0) {
return; return;
@@ -32,7 +31,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
} else { } else {
setInternalValue(newValue); setInternalValue(newValue);
} }
} };
const onPlusClick = () => { const onPlusClick = () => {
const newValue = (value || 0) + 1; const newValue = (value || 0) + 1;
@@ -41,7 +40,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
} else { } else {
setInternalValue(newValue); setInternalValue(newValue);
} }
} };
const handleInputChange = (event: number | string) => { const handleInputChange = (event: number | string) => {
let newValue = typeof event === "string" ? 0 : event; let newValue = typeof event === "string" ? 0 : event;
@@ -53,7 +52,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
} else { } else {
setInternalValue(newValue); setInternalValue(newValue);
} }
} };
useEffect(() => { useEffect(() => {
if (!isControlled) { if (!isControlled) {
@@ -64,34 +63,33 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
return ( return (
<Flex <Flex
align={"center"} align={"center"}
gap={rem(10)} gap={rem(10)}>
>
<ActionIcon <ActionIcon
disabled={value === 0} disabled={value === 0}
onClick={onMinusClick} onClick={onMinusClick}
variant={"default"}> variant={"default"}>
<IconMinus/> <IconMinus />
</ActionIcon> </ActionIcon>
<NumberInput <NumberInput
min={0} min={0}
styles={{ styles={{
input: { input: {
textAlign: "center" textAlign: "center",
} },
}} }}
allowNegative={false} allowNegative={false}
hideControls hideControls
value={value} value={value}
className={styles['number-input']} className={styles["number-input"]}
onChange={(event) => handleInputChange(event)} onChange={event => handleInputChange(event)}
/> />
<ActionIcon <ActionIcon
onClick={onPlusClick} onClick={onPlusClick}
variant={"default"}> variant={"default"}>
<IconPlus/> <IconPlus />
</ActionIcon> </ActionIcon>
</Flex> </Flex>
) );
} };
export default PlusMinusInput; export default PlusMinusInput;

View File

@@ -1,81 +1,113 @@
import {ProductSchema} from "../../client"; import { ProductSchema } from "../../client";
import {FC, useState} from "react"; import { FC, useState } from "react";
import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx"; import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx";
import {omit} from "lodash"; import { omit } from "lodash";
import ObjectSelect, {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {ComboboxItem, Image, Loader, OptionsFilter, rem, SelectProps, Text, Tooltip} from "@mantine/core"; ObjectSelectProps,
import {getProductFields} from "../../types/utils.ts"; } from "../ObjectSelect/ObjectSelect.tsx";
import {useDebouncedValue} from "@mantine/hooks"; import {
ComboboxItem,
Image,
Loader,
OptionsFilter,
rem,
SelectProps,
Text,
Tooltip,
} from "@mantine/core";
import { getProductFields } from "../../types/utils.ts";
import { useDebouncedValue } from "@mantine/hooks";
type RestProps = { type RestProps = {
clientId: number; clientId: number;
} };
const MAX_PRODUCTS = 200; const MAX_PRODUCTS = 200;
type Props = Omit<ObjectSelectProps<ProductSchema>, 'data'> & RestProps; type Props = Omit<ObjectSelectProps<ProductSchema>, "data"> & RestProps;
const ProductSelect: FC<Props> = (props: Props) => { const ProductSelect: FC<Props> = (props: Props) => {
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const [debounced] = useDebouncedValue(searchValue, 500); const [debounced] = useDebouncedValue(searchValue, 500);
const {products, isLoading} = useProductsList({ const { products, isLoading } = useProductsList({
clientId: props.clientId, clientId: props.clientId,
searchInput: debounced, searchInput: debounced,
page: 0, page: 0,
itemsPerPage: MAX_PRODUCTS itemsPerPage: MAX_PRODUCTS,
}); });
const restProps = omit(props, ['clientId']); const restProps = omit(props, ["clientId"]);
const renderOption: SelectProps['renderOption'] = (item) => { const renderOption: SelectProps["renderOption"] = item => {
const product = products.find(product => product.id == parseInt(item.option.value)); const product = products.find(
product => product.id == parseInt(item.option.value)
);
if (!product) return item.option.label; if (!product) return item.option.label;
const productFields = getProductFields(product); const productFields = getProductFields(product);
const imageUrl = product.images && product.images[0] ? product.images[0].imageUrl : undefined; const imageUrl =
product.images && product.images[0]
? product.images[0].imageUrl
: undefined;
return ( return (
<Tooltip <Tooltip
style={{whiteSpace: "pre-line"}} style={{ whiteSpace: "pre-line" }}
multiline multiline
disabled={productFields.length === 0} disabled={productFields.length === 0}
label={ label={
<> <>
{productFields.map(([key, value]) => { {productFields
return `${key.toString()}: ${value.toString()}`; .map(([key, value]) => {
}).join('\n')} return `${key.toString()}: ${value.toString()}`;
{imageUrl && <Image })
src={imageUrl} .join("\n")}
alt={product.name} {imageUrl && (
maw={rem(250)} <Image
/>} src={imageUrl}
alt={product.name}
maw={rem(250)}
/>
)}
</> </>
}> }>
<div> <div>
{product.name}<br/> {product.name}
{product.barcodes && <Text size={"xs"}>{product.barcodes[0]}</Text>} <br />
{product.barcodes && (
<Text size={"xs"}>{product.barcodes[0]}</Text>
)}
</div> </div>
</Tooltip>) </Tooltip>
}
const optionsFilter: OptionsFilter = ({options, search}) => {
return options;
const filtered = (options as ComboboxItem[]).filter((option) => {
const product = products.find(product => product.id == parseInt(option.value));
if (!product) return true;
return product.name.toLowerCase().includes(search.toLowerCase()) ||
product.barcodes.some((value) => value.toLowerCase().includes(search.toLowerCase())) ||
product.article?.toLowerCase() === search.toLowerCase();
}
); );
};
const optionsFilter: OptionsFilter = ({ options, search }) => {
return options;
const filtered = (options as ComboboxItem[]).filter(option => {
const product = products.find(
product => product.id == parseInt(option.value)
);
if (!product) return true;
return (
product.name.toLowerCase().includes(search.toLowerCase()) ||
product.barcodes.some(value =>
value.toLowerCase().includes(search.toLowerCase())
) ||
product.article?.toLowerCase() === search.toLowerCase()
);
});
filtered.sort((a, b) => a.label.localeCompare(b.label)); filtered.sort((a, b) => a.label.localeCompare(b.label));
return filtered.length > MAX_PRODUCTS ? filtered.slice(0, MAX_PRODUCTS) : filtered; return filtered.length > MAX_PRODUCTS
? filtered.slice(0, MAX_PRODUCTS)
: filtered;
}; };
const setSearchValueImpl = (value: string) => { const setSearchValueImpl = (value: string) => {
const names = products.map(product => product.name); const names = products.map(product => product.name);
if (names.includes(value)) return; if (names.includes(value)) return;
setSearchValue(value); setSearchValue(value);
} };
return ( return (
<ObjectSelect <ObjectSelect
// disabled={isLoading} // disabled={isLoading}
rightSection={ rightSection={
(isLoading || searchValue !== debounced) ? isLoading || searchValue !== debounced ? (
<Loader size={"sm"}/> : null <Loader size={"sm"} />
) : null
} }
onSearchChange={setSearchValueImpl} onSearchChange={setSearchValueImpl}
renderOption={renderOption} renderOption={renderOption}
@@ -84,8 +116,8 @@ const ProductSelect: FC<Props> = (props: Props) => {
data={products} data={products}
filter={optionsFilter} filter={optionsFilter}
/> />
) );
} };
export default ProductSelect; export default ProductSelect;
// type ControlledValueProps = { // type ControlledValueProps = {
// value: ProductSchema; // value: ProductSchema;
@@ -140,4 +172,4 @@ export default ProductSelect;
// /> // />
// ) // )
// } // }
// export default ProductSelect; // export default ProductSelect;

View File

@@ -1,18 +1,20 @@
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {BarcodeTemplateSchema} from "../../../client"; ObjectSelectProps,
} from "../../ObjectSelect/ObjectSelect.tsx";
import { BarcodeTemplateSchema } from "../../../client";
import useGetAllBarcodeTemplates from "../../../api/barcode/useGetAllBarcodeTemplates.tsx"; import useGetAllBarcodeTemplates from "../../../api/barcode/useGetAllBarcodeTemplates.tsx";
type Props = Omit<ObjectSelectProps<BarcodeTemplateSchema>, 'data'>; type Props = Omit<ObjectSelectProps<BarcodeTemplateSchema>, "data">;
const BarcodeTemplateSelect = (props: Props) => { const BarcodeTemplateSelect = (props: Props) => {
const {barcodeTemplates} = useGetAllBarcodeTemplates(); const { barcodeTemplates } = useGetAllBarcodeTemplates();
return ( return (
<ObjectSelect <ObjectSelect
data={barcodeTemplates} data={barcodeTemplates}
{...props} {...props}
/> />
) );
} };
export default BarcodeTemplateSelect; export default BarcodeTemplateSelect;

View File

@@ -1,45 +1,54 @@
import {BaseEnumListSchema, type CancelablePromise} from "../../../client"; import { BaseEnumListSchema, type CancelablePromise } from "../../../client";
import {FC, useEffect, useMemo, useState} from "react"; import { FC, useEffect, useMemo, useState } from "react";
import {useQuery} from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import {Select, SelectProps} from "@mantine/core"; import { Select, SelectProps } from "@mantine/core";
import {omit} from "lodash"; import { omit } from "lodash";
type ControlledValueProps = { type ControlledValueProps = {
value: number, value: number;
onChange: (value: number) => void; onChange: (value: number) => void;
} };
type RestProps = { type RestProps = {
defaultValue?: number; defaultValue?: number;
onChange: (value: number) => void; onChange: (value: number) => void;
fetchFn: () => CancelablePromise<BaseEnumListSchema>; fetchFn: () => CancelablePromise<BaseEnumListSchema>;
queryKey: string; queryKey: string;
} };
export type BaseEnumSelectProps = export type BaseEnumSelectProps = (RestProps & Partial<ControlledValueProps>) &
(RestProps & Partial<ControlledValueProps>) Omit<SelectProps, "value" | "onChange" | "data" | "defaultValue">;
& Omit<SelectProps, 'value' | 'onChange' | 'data' | 'defaultValue'>;
export type EnumSelectProps = Omit<BaseEnumSelectProps, 'fetchFn' | 'queryKey'>; export type EnumSelectProps = Omit<BaseEnumSelectProps, "fetchFn" | "queryKey">;
const BaseEnumSelect: FC<BaseEnumSelectProps> = (props: BaseEnumSelectProps) => { const BaseEnumSelect: FC<BaseEnumSelectProps> = (
const {data: queryData = []} = useQuery({ props: BaseEnumSelectProps
) => {
const { data: queryData = [] } = useQuery({
queryKey: [props.queryKey], queryKey: [props.queryKey],
queryFn: props.fetchFn, queryFn: props.fetchFn,
select: data => data.items || [] select: data => data.items || [],
}) });
const isControlled = 'value' in props; const isControlled = "value" in props;
const [internalValue, setInternalValue] = useState<number | undefined>(props.defaultValue); const [internalValue, setInternalValue] = useState<number | undefined>(
props.defaultValue
);
const value = isControlled ? props.value : internalValue; const value = isControlled ? props.value : internalValue;
const selectData = useMemo(() => queryData.reduce((acc, item) => { const selectData = useMemo(
acc.push({ () =>
label: item.name, queryData.reduce(
value: item.id.toString() (acc, item) => {
}); acc.push({
return acc; label: item.name,
}, [] as { label: string, value: string }[]), [queryData]); value: item.id.toString(),
});
return acc;
},
[] as { label: string; value: string }[]
),
[queryData]
);
const handleOnChange = (event: string | null) => { const handleOnChange = (event: string | null) => {
if (typeof event === 'undefined' || event === null) return; if (typeof event === "undefined" || event === null) return;
const object = queryData.find(item => event == item.id.toString()); const object = queryData.find(item => event == item.id.toString());
if (!object) return; if (!object) return;
if (isControlled) { if (isControlled) {
@@ -47,22 +56,23 @@ const BaseEnumSelect: FC<BaseEnumSelectProps> = (props: BaseEnumSelectProps) =>
return; return;
} }
setInternalValue(parseInt(event)); setInternalValue(parseInt(event));
} };
const restProps = omit(props, ['fetchFn', 'queryKey']) const restProps = omit(props, ["fetchFn", "queryKey"]);
useEffect(() => { useEffect(() => {
if (isControlled || typeof internalValue === 'undefined') return; if (isControlled || typeof internalValue === "undefined") return;
props.onChange(internalValue); props.onChange(internalValue);
}, [internalValue]); }, [internalValue]);
return ( return (
<Select <Select
{...restProps} {...restProps}
defaultValue={props.defaultValue ? props.defaultValue.toString() : undefined} defaultValue={
props.defaultValue ? props.defaultValue.toString() : undefined
}
value={value?.toString()} value={value?.toString()}
onChange={handleOnChange} onChange={handleOnChange}
data={selectData} data={selectData}
/> />
) );
} };
export default BaseEnumSelect; export default BaseEnumSelect;

View File

@@ -1,30 +1,42 @@
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {BaseMarketplaceSchema} from "../../../client"; ObjectSelectProps,
import {FC} from "react"; } from "../../ObjectSelect/ObjectSelect.tsx";
import { BaseMarketplaceSchema } from "../../../client";
import { FC } from "react";
import useBaseMarketplacesList from "../../../hooks/useBaseMarketplacesList.tsx"; import useBaseMarketplacesList from "../../../hooks/useBaseMarketplacesList.tsx";
import {ActionIcon, Image} from "@mantine/core"; import { ActionIcon, Image } from "@mantine/core";
type Props = Omit<ObjectSelectProps<BaseMarketplaceSchema>, 'data' | 'getValueFn' | 'getLabelFn'> type Props = Omit<
ObjectSelectProps<BaseMarketplaceSchema>,
"data" | "getValueFn" | "getLabelFn"
>;
const BaseMarketplaceSelect: FC<Props> = (props) => { const BaseMarketplaceSelect: FC<Props> = props => {
const {objects: baseMarketplaces} = useBaseMarketplacesList(); const { objects: baseMarketplaces } = useBaseMarketplacesList();
return ( return (
<ObjectSelect <ObjectSelect
renderOption={(baseMarketplace) => renderOption={baseMarketplace => (
<> <>
<ActionIcon radius={"md"} variant={"transparent"}> <ActionIcon
radius={"md"}
variant={"transparent"}>
<Image <Image
src={baseMarketplaces.find(el => baseMarketplace.option.value === el.key)?.iconUrl || ""}/> src={
baseMarketplaces.find(
el =>
baseMarketplace.option.value === el.key
)?.iconUrl || ""
}
/>
</ActionIcon> </ActionIcon>
{baseMarketplace.option.label} {baseMarketplace.option.label}
</> </>
)}
} getValueFn={baseMarketplace => baseMarketplace.key}
getValueFn={(baseMarketplace) => baseMarketplace.key} getLabelFn={baseMarketplace => baseMarketplace.name}
getLabelFn={(baseMarketplace) => baseMarketplace.name}
data={baseMarketplaces} data={baseMarketplaces}
{...props} {...props}
/> />
) );
} };
export default BaseMarketplaceSelect export default BaseMarketplaceSelect;

View File

@@ -1,54 +1,63 @@
import {useDebouncedValue} from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import {Autocomplete, AutocompleteProps, TextInputProps} from "@mantine/core"; import { Autocomplete, AutocompleteProps, TextInputProps } from "@mantine/core";
import {FC, useEffect, useState} from "react"; import { FC, useEffect, useState } from "react";
import {Client} from "../../../types/Client.ts"; import { Client } from "../../../types/Client.ts";
import {ClientService} from "../../../client"; import { ClientService } from "../../../client";
type Props = { type Props = {
onSelect?: (client: Client) => void; onSelect?: (client: Client) => void;
withAddress?: boolean; withAddress?: boolean;
nameRestProps?: AutocompleteProps; nameRestProps?: AutocompleteProps;
addressRestProps?: TextInputProps; addressRestProps?: TextInputProps;
} };
const ClientAutocomplete: FC<Props> = ({onSelect, addressRestProps, nameRestProps, withAddress = false}) => { const ClientAutocomplete: FC<Props> = ({
const [value, setValue] = useState(''); onSelect,
addressRestProps,
nameRestProps,
withAddress = false,
}) => {
const [value, setValue] = useState("");
const [debouncedValue] = useDebouncedValue(value, 200); const [debouncedValue] = useDebouncedValue(value, 200);
// const [isLoading, setIsLoading] = useState(false); // const [isLoading, setIsLoading] = useState(false);
const [clients, setClients] = useState<Client[]>([]) const [clients, setClients] = useState<Client[]>([]);
const [selectedClient, selectClient] = useState<Client>(); const [selectedClient, selectClient] = useState<Client>();
const handleChange = (value: string) => { const handleChange = (value: string) => {
setClients([]); setClients([]);
setValue(value); setValue(value);
} };
const handleDebouncedChange = () => { const handleDebouncedChange = () => {
if (!value.trim()) return; if (!value.trim()) return;
// setIsLoading(true); // setIsLoading(true);
ClientService.searchClients({name: value}).then(({clients}) => { ClientService.searchClients({ name: value }).then(({ clients }) => {
setClients(clients); setClients(clients);
// setIsLoading(false); // setIsLoading(false);
}) });
} };
useEffect(() => { useEffect(() => {
handleDebouncedChange(); handleDebouncedChange();
}, [debouncedValue]); }, [debouncedValue]);
useEffect(() => { useEffect(() => {
selectClient(clients.find(client => selectClient(
client.name.toLowerCase().trim() == value.toLowerCase().trim()) clients.find(
|| client =>
{ client.name.toLowerCase().trim() ==
value.toLowerCase().trim()
) || {
name: value, name: value,
id: -1, id: -1,
address: "" address: "",
}); }
);
}, [value]); }, [value]);
useEffect(() => { useEffect(() => {
if (!selectedClient) return; if (!selectedClient) return;
if (onSelect) onSelect(selectedClient); if (onSelect) onSelect(selectedClient);
if (nameRestProps?.onChange) nameRestProps.onChange(selectedClient.name); if (nameRestProps?.onChange)
nameRestProps.onChange(selectedClient.name);
if (addressRestProps?.onChange) { if (addressRestProps?.onChange) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
@@ -59,25 +68,22 @@ const ClientAutocomplete: FC<Props> = ({onSelect, addressRestProps, nameRestProp
<> <>
<Autocomplete <Autocomplete
{...nameRestProps} {...nameRestProps}
placeholder={'Клиент'} placeholder={"Клиент"}
onChange={handleChange} onChange={handleChange}
value={value} value={value}
data={clients.map(client => client.name)} data={clients.map(client => client.name)}
styles={withAddress ? { styles={
input: { withAddress
borderBottomLeftRadius: 0, ? {
borderBottomRightRadius: 0 input: {
} borderBottomLeftRadius: 0,
} : {}} borderBottomRightRadius: 0,
},
}
: {}
}
/> />
</> </>
);
) };
}
export default ClientAutocomplete; export default ClientAutocomplete;

View File

@@ -1,31 +1,39 @@
import {FC} from "react"; import { FC } from "react";
import {Select} from "@mantine/core"; import { Select } from "@mantine/core";
import {ClientSchema} from "../../../client"; import { ClientSchema } from "../../../client";
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx"; import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
type Props = { type Props = {
value?: ClientSchema; value?: ClientSchema;
onChange: (client: ClientSchema) => void; onChange: (client: ClientSchema) => void;
withLabel?: boolean; withLabel?: boolean;
} };
const ClientSelect: FC<Props> = ({value, onChange, withLabel = false}) => { const ClientSelect: FC<Props> = ({ value, onChange, withLabel = false }) => {
const {clients} = useClientsList(); const { clients } = useClientsList();
const options = clients.map(client => ({label: client.name, value: client.id.toString()})) const options = clients.map(client => ({
label: client.name,
value: client.id.toString(),
}));
return ( return (
<Select <Select
searchable searchable
placeholder={"Выберите клиента"} placeholder={"Выберите клиента"}
value={value && options.find(client => client.value == value.id.toString())?.value} value={
value &&
options.find(client => client.value == value.id.toString())
?.value
}
onChange={event => { onChange={event => {
if (!event) return; if (!event) return;
const client = clients.find(client => client.id == parseInt(event)); const client = clients.find(
client => client.id == parseInt(event)
);
if (!client) return; if (!client) return;
onChange(client); onChange(client);
}} }}
data={options} data={options}
label={withLabel && "Клиент"} label={withLabel && "Клиент"}
/> />
) );
} };
export default ClientSelect; export default ClientSelect;

View File

@@ -1,18 +1,20 @@
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {ClientSchema} from "../../../client"; ObjectSelectProps,
import {FC} from "react"; } from "../../ObjectSelect/ObjectSelect.tsx";
import { ClientSchema } from "../../../client";
import { FC } from "react";
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx"; import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
type Props = Omit<ObjectSelectProps<ClientSchema>, 'data'> type Props = Omit<ObjectSelectProps<ClientSchema>, "data">;
const ClientSelectNew: FC<Props> = (props) => { const ClientSelectNew: FC<Props> = props => {
const {clients} = useClientsList(); const { clients } = useClientsList();
return ( return (
<ObjectSelect <ObjectSelect
searchable searchable
data={clients} data={clients}
{...props} {...props}
/> />
) );
} };
export default ClientSelectNew; export default ClientSelectNew;

View File

@@ -1,19 +1,24 @@
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {PayRateSchema} from "../../../client"; ObjectSelectProps,
import {FC} from "react"; } from "../../ObjectSelect/ObjectSelect.tsx";
import { PayRateSchema } from "../../../client";
import { FC } from "react";
import usePayRatesList from "../../../pages/AdminPage/hooks/usePayRatesList.tsx"; import usePayRatesList from "../../../pages/AdminPage/hooks/usePayRatesList.tsx";
type Props = Omit<ObjectSelectProps<PayRateSchema>, 'data' | 'getValueFn' | 'getLabelFn'> type Props = Omit<
ObjectSelectProps<PayRateSchema>,
"data" | "getValueFn" | "getLabelFn"
>;
const PayRateSelect: FC<Props> = (props) => { const PayRateSelect: FC<Props> = props => {
const {objects: payRates} = usePayRatesList(); const { objects: payRates } = usePayRatesList();
return ( return (
<ObjectSelect <ObjectSelect
getValueFn={(baseMarketplace) => baseMarketplace.id.toLocaleString()} getValueFn={baseMarketplace => baseMarketplace.id.toLocaleString()}
getLabelFn={(baseMarketplace) => baseMarketplace.name} getLabelFn={baseMarketplace => baseMarketplace.name}
data={payRates} data={payRates}
{...props} {...props}
/> />
) );
} };
export default PayRateSelect; export default PayRateSelect;

View File

@@ -1,19 +1,24 @@
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {PayrollSchemeSchema} from "../../../client"; ObjectSelectProps,
import {FC} from "react"; } from "../../ObjectSelect/ObjectSelect.tsx";
import { PayrollSchemeSchema } from "../../../client";
import { FC } from "react";
import usePayrollSchemasList from "../../../hooks/usePayrollSchemasList.tsx"; import usePayrollSchemasList from "../../../hooks/usePayrollSchemasList.tsx";
type Props = Omit<ObjectSelectProps<PayrollSchemeSchema>, 'data' | 'getValueFn' | 'getLabelFn'> type Props = Omit<
ObjectSelectProps<PayrollSchemeSchema>,
"data" | "getValueFn" | "getLabelFn"
>;
const PayrollSchemeSelect: FC<Props> = (props) => { const PayrollSchemeSelect: FC<Props> = props => {
const {objects: payrollSchemeSchemas} = usePayrollSchemasList(); const { objects: payrollSchemeSchemas } = usePayrollSchemasList();
return ( return (
<ObjectSelect <ObjectSelect
getValueFn={(baseMarketplace) => baseMarketplace.key} getValueFn={baseMarketplace => baseMarketplace.key}
getLabelFn={(baseMarketplace) => baseMarketplace.name} getLabelFn={baseMarketplace => baseMarketplace.name}
data={payrollSchemeSchemas} data={payrollSchemeSchemas}
{...props} {...props}
/> />
) );
} };
export default PayrollSchemeSelect export default PayrollSchemeSelect;

View File

@@ -1,8 +1,10 @@
import ObjectSelect, { ObjectSelectProps } from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
ObjectSelectProps,
} from "../../ObjectSelect/ObjectSelect.tsx";
import { ServicePriceCategorySchema } from "../../../client"; import { ServicePriceCategorySchema } from "../../../client";
import useServicePriceCategoriesList from "../../../pages/ServicesPage/hooks/useServicePriceCategoriesList.tsx"; import useServicePriceCategoriesList from "../../../pages/ServicesPage/hooks/useServicePriceCategoriesList.tsx";
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data"> type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">;
const ServicePriceCategorySelect = (props: Props) => { const ServicePriceCategorySelect = (props: Props) => {
const { objects } = useServicePriceCategoriesList(); const { objects } = useServicePriceCategoriesList();
@@ -14,4 +16,4 @@ const ServicePriceCategorySelect = (props: Props) => {
); );
}; };
export default ServicePriceCategorySelect; export default ServicePriceCategorySelect;

View File

@@ -1,31 +1,36 @@
import {FC} from "react"; import { FC } from "react";
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {ServiceSchema} from "../../../client"; ObjectSelectProps,
} from "../../ObjectSelect/ObjectSelect.tsx";
import { ServiceSchema } from "../../../client";
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx"; import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
import {omit} from "lodash"; import { omit } from "lodash";
import {ServiceType} from "../../../shared/enums/ServiceType.ts"; import { ServiceType } from "../../../shared/enums/ServiceType.ts";
import {ComboboxItem, OptionsFilter} from "@mantine/core"; import { ComboboxItem, OptionsFilter } from "@mantine/core";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
import {ComboboxParsedItemGroup} from "@mantine/core/lib/components/Combobox/Combobox.types"; import { ComboboxParsedItemGroup } from "@mantine/core/lib/components/Combobox/Combobox.types";
type RestProps = { type RestProps = {
filterType?: ServiceType; filterType?: ServiceType;
} };
type Props = Omit<ObjectSelectProps<ServiceSchema>, 'data'> & RestProps; type Props = Omit<ObjectSelectProps<ServiceSchema>, "data"> & RestProps;
const ServiceSelectNew: FC<Props> = (props: Props) => { const ServiceSelectNew: FC<Props> = (props: Props) => {
const {services} = useServicesList(); const { services } = useServicesList();
const data = props.filterType ? services.filter(service => service.serviceType === props.filterType) : services; const data = props.filterType
? services.filter(service => service.serviceType === props.filterType)
: services;
const restProps = omit(props, ['filterType']); const restProps = omit(props, ["filterType"]);
const optionsFilter: OptionsFilter = ({options, search}) => { const optionsFilter: OptionsFilter = ({ options, search }) => {
return (options as ComboboxParsedItemGroup<ComboboxItem>[]).map((option) => { return (options as ComboboxParsedItemGroup<ComboboxItem>[]).map(
option => {
return { return {
...option, ...option,
items: items: option.items.filter((item: ComboboxItem) =>
option.items.filter((item: ComboboxItem) => item.label.toLowerCase().includes(search.toLowerCase())) item.label.toLowerCase().includes(search.toLowerCase())
} ),
};
} }
); );
}; };
@@ -37,6 +42,6 @@ const ServiceSelectNew: FC<Props> = (props: Props) => {
groupBy={item => item.category.name} groupBy={item => item.category.name}
filter={optionsFilter} filter={optionsFilter}
/> />
) );
} };
export default ServiceSelectNew; export default ServiceSelectNew;

View File

@@ -1,17 +1,19 @@
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {GetServiceKitSchema} from "../../../client"; ObjectSelectProps,
import {FC} from "react"; } from "../../ObjectSelect/ObjectSelect.tsx";
import { GetServiceKitSchema } from "../../../client";
import { FC } from "react";
import useServicesKitsList from "../../../pages/ServicesPage/hooks/useServicesKitsList.tsx"; import useServicesKitsList from "../../../pages/ServicesPage/hooks/useServicesKitsList.tsx";
type Props = Omit<ObjectSelectProps<GetServiceKitSchema>, 'data'> type Props = Omit<ObjectSelectProps<GetServiceKitSchema>, "data">;
const ServicesKitSelect: FC<Props> = (props) => { const ServicesKitSelect: FC<Props> = props => {
const {objects} = useServicesKitsList(); const { objects } = useServicesKitsList();
return ( return (
<ObjectSelect <ObjectSelect
data={objects} data={objects}
{...props} {...props}
/> />
) );
} };
export default ServicesKitSelect; export default ServicesKitSelect;

View File

@@ -1,16 +1,18 @@
import ObjectMultiSelect, {ObjectMultiSelectProps} from "../../ObjectMultiSelect/ObjectMultiSelect.tsx"; import ObjectMultiSelect, {
import {ServiceSchema} from "../../../client"; ObjectMultiSelectProps,
import {FC} from "react"; } from "../../ObjectMultiSelect/ObjectMultiSelect.tsx";
import { ServiceSchema } from "../../../client";
import { FC } from "react";
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx"; import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
type Props = Omit<ObjectMultiSelectProps<ServiceSchema>, 'data'> type Props = Omit<ObjectMultiSelectProps<ServiceSchema>, "data">;
const ServicesMultiselect: FC<Props> = (props: Props) => { const ServicesMultiselect: FC<Props> = (props: Props) => {
const {services} = useServicesList(); const { services } = useServicesList();
return ( return (
<ObjectMultiSelect <ObjectMultiSelect
data={services} data={services}
{...props} {...props}
/> />
) );
} };
export default ServicesMultiselect; export default ServicesMultiselect;

View File

@@ -1,17 +1,19 @@
import ObjectAutocomplete, {ObjectAutocompleteProps} from "../../ObjectAutocomplete/ObjectAutocomplete.tsx"; import ObjectAutocomplete, {
ObjectAutocompleteProps,
} from "../../ObjectAutocomplete/ObjectAutocomplete.tsx";
import useShippingWarehousesList from "./hooks/useShippingWarehousesList.tsx"; import useShippingWarehousesList from "./hooks/useShippingWarehousesList.tsx";
import {FC} from "react"; import { FC } from "react";
import {ShippingWarehouseSchema} from "../../../client"; import { ShippingWarehouseSchema } from "../../../client";
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, 'data'>; type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, "data">;
const ShippingWarehouseAutocomplete: FC<Props> = (props) => { const ShippingWarehouseAutocomplete: FC<Props> = props => {
const {shippingWarehouses} = useShippingWarehousesList(); const { shippingWarehouses } = useShippingWarehousesList();
return ( return (
<ObjectAutocomplete <ObjectAutocomplete
{...props} {...props}
data={shippingWarehouses} data={shippingWarehouses}
/> />
) );
} };
export default ShippingWarehouseAutocomplete; export default ShippingWarehouseAutocomplete;

View File

@@ -1,13 +1,14 @@
import {useQuery} from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import {ShippingWarehouseService} from "../../../../client"; import { ShippingWarehouseService } from "../../../../client";
const useShippingWarehousesList = () => { const useShippingWarehousesList = () => {
const {isPending, error, data, refetch} = useQuery({ const { isPending, error, data, refetch } = useQuery({
queryKey: ['getAllShippingWarehouses'], queryKey: ["getAllShippingWarehouses"],
queryFn: ShippingWarehouseService.getAllShippingWarehouses queryFn: ShippingWarehouseService.getAllShippingWarehouses,
}); });
const shippingWarehouses = isPending || error || !data ? [] : data.shippingWarehouses; const shippingWarehouses =
isPending || error || !data ? [] : data.shippingWarehouses;
return {shippingWarehouses, refetch} return { shippingWarehouses, refetch };
} };
export default useShippingWarehousesList; export default useShippingWarehousesList;

View File

@@ -1,16 +1,23 @@
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx"; import ObjectSelect, {
import {UserSchema} from "../../../client"; ObjectSelectProps,
import {FC} from "react"; } from "../../ObjectSelect/ObjectSelect.tsx";
import { UserSchema } from "../../../client";
import { FC } from "react";
import useUsersList from "../../../pages/AdminPage/hooks/useUsersList.tsx"; import useUsersList from "../../../pages/AdminPage/hooks/useUsersList.tsx";
type Props = Omit<ObjectSelectProps<UserSchema>, 'data' | 'getValueFn' | 'getLabelFn'> type Props = Omit<
const UserSelect: FC<Props> = (props) => { ObjectSelectProps<UserSchema>,
const {objects: users} = useUsersList(); "data" | "getValueFn" | "getLabelFn"
return (<ObjectSelect >;
data={users} const UserSelect: FC<Props> = props => {
getLabelFn={(user) => `${user.firstName} ${user.secondName}`} const { objects: users } = useUsersList();
getValueFn={(user) => user.id.toString()} return (
{...props} <ObjectSelect
/>) data={users}
} getLabelFn={user => `${user.firstName} ${user.secondName}`}
export default UserSelect; getValueFn={user => user.id.toString()}
{...props}
/>
);
};
export default UserSelect;

View File

@@ -1,48 +1,66 @@
import {ServiceSchema} from "../../client"; import { ServiceSchema } from "../../client";
import {FC, useEffect, useMemo, useState} from "react"; import { FC, useEffect, useMemo, useState } from "react";
import {Select, SelectProps} from "@mantine/core"; import { Select, SelectProps } from "@mantine/core";
import useServicesList from "../../pages/ServicesPage/hooks/useServicesList.tsx"; import useServicesList from "../../pages/ServicesPage/hooks/useServicesList.tsx";
type ControlledValueProps = { type ControlledValueProps = {
value: ServiceSchema; value: ServiceSchema;
onChange: (value: ServiceSchema) => void; onChange: (value: ServiceSchema) => void;
} };
type RestProps = { type RestProps = {
defaultValue?: ServiceSchema; defaultValue?: ServiceSchema;
onChange: (value: ServiceSchema) => void; onChange: (value: ServiceSchema) => void;
} };
type Props = (RestProps & Partial<ControlledValueProps>) & Omit<SelectProps, 'value' | 'onChange'>; type Props = (RestProps & Partial<ControlledValueProps>) &
Omit<SelectProps, "value" | "onChange">;
const ServiceSelect: FC<Props> = props => {
const ServiceSelect: FC<Props> = (props) => {
const isControlled = props.value !== undefined; const isControlled = props.value !== undefined;
const [internalValue, setInternalValue] = useState<ServiceSchema | undefined>(props.defaultValue); const [internalValue, setInternalValue] = useState<
ServiceSchema | undefined
>(props.defaultValue);
const value = isControlled ? props.value : internalValue; const value = isControlled ? props.value : internalValue;
const {services} = useServicesList(); const { services } = useServicesList();
const categories = useMemo(() => services.reduce((acc, service) => { const categories = useMemo(
if (!acc.includes(service.category.name)) { () =>
acc.push(service.category.name); services.reduce((acc, service) => {
} if (!acc.includes(service.category.name)) {
return acc; acc.push(service.category.name);
}, [] as string[]), [services]); }
return acc;
const data = useMemo(() => categories.map(category => ({ }, [] as string[]),
group: category, [services]
items: services.filter(service => service.category.name === category) );
.map(service => ({
value: service.id.toString(),
label: service.name
}))
})), [services, categories]);
const data = useMemo(
() =>
categories.map(category => ({
group: category,
items: services
.filter(service => service.category.name === category)
.map(service => ({
value: service.id.toString(),
label: service.name,
})),
})),
[services, categories]
);
const handleOnChange = (value: string) => { const handleOnChange = (value: string) => {
if (isControlled) { if (isControlled) {
props.onChange(services.find(service => service.id.toString() === value) as ServiceSchema); props.onChange(
services.find(
service => service.id.toString() === value
) as ServiceSchema
);
return; return;
} }
setInternalValue(services.find(service => service.id.toString() === value) as ServiceSchema); setInternalValue(
} services.find(
service => service.id.toString() === value
) as ServiceSchema
);
};
useEffect(() => { useEffect(() => {
if (!isControlled) { if (!isControlled) {
props.onChange(internalValue as ServiceSchema); props.onChange(internalValue as ServiceSchema);
@@ -57,7 +75,7 @@ const ServiceSelect: FC<Props> = (props) => {
onChange={event => event && handleOnChange(event)} onChange={event => event && handleOnChange(event)}
data={data} data={data}
/> />
) );
} };
export default ServiceSelect; export default ServiceSelect;

View File

@@ -1,6 +1,12 @@
import { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx"; import { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx";
import { ServicePriceCategorySchema, ServiceSchema } from "../../client"; import { ServicePriceCategorySchema, ServiceSchema } from "../../client";
import { Flex, FlexProps, NumberInput, NumberInputProps, rem } from "@mantine/core"; import {
Flex,
FlexProps,
NumberInput,
NumberInputProps,
rem,
} from "@mantine/core";
import { FC, useEffect, useRef, useState } from "react"; import { FC, useEffect, useRef, useState } from "react";
import ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx"; import ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx";
import { ServiceType } from "../../shared/enums/ServiceType.ts"; import { ServiceType } from "../../shared/enums/ServiceType.ts";
@@ -9,38 +15,47 @@ type ServiceProps = Omit<ObjectSelectProps<ServiceSchema>, "data">;
type PriceProps = NumberInputProps; type PriceProps = NumberInputProps;
type Props = { type Props = {
serviceProps: ServiceProps, serviceProps: ServiceProps;
priceProps: PriceProps, priceProps: PriceProps;
quantity: number, quantity: number;
containerProps: FlexProps, containerProps: FlexProps;
filterType?: ServiceType, filterType?: ServiceType;
lockOnEdit?: boolean lockOnEdit?: boolean;
category?: ServicePriceCategorySchema category?: ServicePriceCategorySchema;
} };
const ServiceWithPriceInput: FC<Props> = ({ const ServiceWithPriceInput: FC<Props> = ({
serviceProps, serviceProps,
priceProps, priceProps,
quantity, quantity,
containerProps, containerProps,
filterType = ServiceType.PRODUCT_SERVICE, filterType = ServiceType.PRODUCT_SERVICE,
lockOnEdit = true, lockOnEdit = true,
category, category,
}) => { }) => {
const [price, setPrice] = useState<number | undefined>( const [price, setPrice] = useState<number | undefined>(
typeof priceProps.value === "number" ? priceProps.value : undefined); typeof priceProps.value === "number" ? priceProps.value : undefined
const [service, setService] = useState<ServiceSchema | undefined>(serviceProps.value); );
const [service, setService] = useState<ServiceSchema | undefined>(
serviceProps.value
);
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
const setPriceBasedOnQuantity = (): boolean => { const setPriceBasedOnQuantity = (): boolean => {
if (!service || !service.priceRanges.length) return false; if (!service || !service.priceRanges.length) return false;
const range = service.priceRanges.find(priceRange => const range =
quantity >= priceRange.fromQuantity && quantity <= priceRange.toQuantity) || service.priceRanges[0]; service.priceRanges.find(
priceRange =>
quantity >= priceRange.fromQuantity &&
quantity <= priceRange.toQuantity
) || service.priceRanges[0];
setPrice(range.price); setPrice(range.price);
return true; return true;
}; };
const setPriceBasedOnCategory = () => { const setPriceBasedOnCategory = () => {
if (!category || !service) return false; if (!category || !service) return false;
const categoryPrice = service.categoryPrices.find(categoryPrice => categoryPrice.category.id === category.id); const categoryPrice = service.categoryPrices.find(
categoryPrice => categoryPrice.category.id === category.id
);
if (!categoryPrice) return false; if (!categoryPrice) return false;
setPrice(categoryPrice.price); setPrice(categoryPrice.price);
return true; return true;
@@ -84,14 +99,11 @@ const ServiceWithPriceInput: FC<Props> = ({
isFirstRender.current = false; isFirstRender.current = false;
}, []); }, []);
return ( return (
<Flex <Flex
align={"center"} align={"center"}
gap={rem(10)} gap={rem(10)}
{...containerProps} {...containerProps}>
>
{/*<ActionIcon variant={"default"}>*/} {/*<ActionIcon variant={"default"}>*/}
{/*<IconReload onClick={() => onReload()}/>*/} {/*<IconReload onClick={() => onReload()}/>*/}
{/*</ActionIcon>*/} {/*</ActionIcon>*/}
@@ -108,9 +120,8 @@ const ServiceWithPriceInput: FC<Props> = ({
// value={price} // value={price}
defaultValue={priceProps.value} defaultValue={priceProps.value}
/> />
</Flex> </Flex>
); );
}; };
export default ServiceWithPriceInput; export default ServiceWithPriceInput;

View File

@@ -1,90 +1,93 @@
import PropTypes from 'prop-types' import PropTypes from "prop-types";
import React, { useRef, useEffect } from 'react' import React, { useRef, useEffect } from "react";
export interface TelegramUser { export interface TelegramUser {
id: number id: number;
first_name: string first_name: string;
username: string username: string;
photo_url: string photo_url: string;
auth_date: number auth_date: number;
hash: string hash: string;
} }
interface Props { interface Props {
botName: string botName: string;
usePic?: boolean usePic?: boolean;
className?: string className?: string;
cornerRadius?: number cornerRadius?: number;
requestAccess?: boolean requestAccess?: boolean;
dataAuthUrl?: string dataAuthUrl?: string;
dataOnauth?: (user: TelegramUser) => void dataOnauth?: (user: TelegramUser) => void;
buttonSize?: 'large' | 'medium' | 'small' buttonSize?: "large" | "medium" | "small";
wrapperProps?: React.HTMLProps<HTMLDivElement> wrapperProps?: React.HTMLProps<HTMLDivElement>;
} }
declare global { declare global {
interface Window { interface Window {
TelegramLoginWidget: { TelegramLoginWidget: {
dataOnauth: (user: TelegramUser) => void dataOnauth: (user: TelegramUser) => void;
} };
} }
} }
const TelegramLoginButton: React.FC<Props> = ({ const TelegramLoginButton: React.FC<Props> = ({
wrapperProps, wrapperProps,
dataAuthUrl, dataAuthUrl,
usePic = false, usePic = false,
botName, botName,
className, className,
buttonSize = 'large', buttonSize = "large",
dataOnauth, dataOnauth,
cornerRadius, cornerRadius,
requestAccess = true requestAccess = true,
}) => { }) => {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (ref.current === null) return if (ref.current === null) return;
if ( if (
typeof dataOnauth === 'undefined' && typeof dataOnauth === "undefined" &&
typeof dataAuthUrl === 'undefined' typeof dataAuthUrl === "undefined"
) { ) {
throw new Error( throw new Error(
'One of this props should be defined: dataAuthUrl (redirect URL), dataOnauth (callback fn) should be defined.' "One of this props should be defined: dataAuthUrl (redirect URL), dataOnauth (callback fn) should be defined."
) );
} }
if (typeof dataOnauth === 'function') { if (typeof dataOnauth === "function") {
window.TelegramLoginWidget = { window.TelegramLoginWidget = {
dataOnauth: (user: TelegramUser) => dataOnauth(user) dataOnauth: (user: TelegramUser) => dataOnauth(user),
} };
} }
const script = document.createElement('script') const script = document.createElement("script");
script.src = 'https://telegram.org/js/telegram-widget.js?22' script.src = "https://telegram.org/js/telegram-widget.js?22";
script.setAttribute('data-telegram-login', botName) script.setAttribute("data-telegram-login", botName);
script.setAttribute('data-size', buttonSize) script.setAttribute("data-size", buttonSize);
if (cornerRadius !== undefined) { if (cornerRadius !== undefined) {
script.setAttribute('data-radius', cornerRadius.toString()) script.setAttribute("data-radius", cornerRadius.toString());
} }
if (requestAccess) { if (requestAccess) {
script.setAttribute('data-request-access', 'write') script.setAttribute("data-request-access", "write");
} }
script.setAttribute('data-userpic', usePic.toString()) script.setAttribute("data-userpic", usePic.toString());
if (typeof dataAuthUrl === 'string') { if (typeof dataAuthUrl === "string") {
script.setAttribute('data-auth-url', dataAuthUrl) script.setAttribute("data-auth-url", dataAuthUrl);
} else { } else {
script.setAttribute('data-onauth', 'TelegramLoginWidget.dataOnauth(user)') script.setAttribute(
"data-onauth",
"TelegramLoginWidget.dataOnauth(user)"
);
} }
script.async = true script.async = true;
ref.current.appendChild(script) ref.current.appendChild(script);
}, [ }, [
botName, botName,
buttonSize, buttonSize,
@@ -93,11 +96,17 @@ const TelegramLoginButton: React.FC<Props> = ({
requestAccess, requestAccess,
usePic, usePic,
ref, ref,
dataAuthUrl dataAuthUrl,
]) ]);
return <div ref={ref} className={className} {...wrapperProps} /> return (
} <div
ref={ref}
className={className}
{...wrapperProps}
/>
);
};
TelegramLoginButton.propTypes = { TelegramLoginButton.propTypes = {
botName: PropTypes.string.isRequired, botName: PropTypes.string.isRequired,
@@ -108,7 +117,7 @@ TelegramLoginButton.propTypes = {
wrapperProps: PropTypes.object, wrapperProps: PropTypes.object,
dataOnauth: PropTypes.func, dataOnauth: PropTypes.func,
dataAuthUrl: PropTypes.string, dataAuthUrl: PropTypes.string,
buttonSize: PropTypes.oneOf(['large', 'medium', 'small']) buttonSize: PropTypes.oneOf(["large", "medium", "small"]),
} };
export default TelegramLoginButton export default TelegramLoginButton;

View File

@@ -1,59 +1,75 @@
import cx from 'clsx'; import cx from "clsx";
import {Text} from '@mantine/core'; import { Text } from "@mantine/core";
import {useListState} from '@mantine/hooks'; import { useListState } from "@mantine/hooks";
import {DragDropContext, Droppable, Draggable} from '@hello-pangea/dnd'; import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
import classes from './DndList.module.css'; import classes from "./DndList.module.css";
const data = [ const data = [
{position: 6, mass: 12.011, symbol: 'C', name: 'Carbon'}, { position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen'}, { position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium'}, { position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium'}, { position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium'}, { position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
]; ];
export function DndList() { export function DndList() {
const [state, handlers] = useListState(data); const [state, handlers] = useListState(data);
const items = (listIndex: number) => state.map((item, index) => ( const items = (listIndex: number) =>
<Draggable key={item.symbol + `${listIndex}`} index={index} draggableId={item.symbol + `${listIndex}`}> state.map((item, index) => (
{(provided, snapshot) => ( <Draggable
<div key={item.symbol + `${listIndex}`}
className={cx(classes.item, {[classes.itemDragging]: snapshot.isDragging})} index={index}
{...provided.draggableProps} draggableId={item.symbol + `${listIndex}`}>
{...provided.dragHandleProps} {(provided, snapshot) => (
ref={provided.innerRef} <div
> className={cx(classes.item, {
<Text className={classes.symbol}>{item.symbol}</Text> [classes.itemDragging]: snapshot.isDragging,
<div> })}
<Text>{item.name}</Text> {...provided.draggableProps}
<Text c="dimmed" size="sm"> {...provided.dragHandleProps}
Position: {item.position} Mass: {item.mass} ref={provided.innerRef}>
</Text> <Text className={classes.symbol}>{item.symbol}</Text>
<div>
<Text>{item.name}</Text>
<Text
c="dimmed"
size="sm">
Position: {item.position} Mass: {item.mass}
</Text>
</div>
</div> </div>
</div> )}
)} </Draggable>
</Draggable> ));
));
return ( return (
<DragDropContext <DragDropContext
onDragEnd={({destination, source}) => onDragEnd={({ destination, source }) =>
handlers.reorder({from: source.index, to: destination?.index || 0}) handlers.reorder({
} from: source.index,
> to: destination?.index || 0,
<Droppable droppableId="dnd-list" direction="vertical"> })
{(provided) => ( }>
<div {...provided.droppableProps} ref={provided.innerRef}> <Droppable
droppableId="dnd-list"
direction="vertical">
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}>
{items(1)} {items(1)}
{provided.placeholder} {provided.placeholder}
</div> </div>
)} )}
</Droppable> </Droppable>
<Droppable droppableId="dnd-list-2" direction="vertical"> <Droppable
{(provided) => ( droppableId="dnd-list-2"
<div {...provided.droppableProps} ref={provided.innerRef}> direction="vertical">
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}>
{items(2)} {items(2)}
{provided.placeholder} {provided.placeholder}
</div> </div>
@@ -61,4 +77,4 @@ export function DndList() {
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
); );
} }

View File

@@ -1,5 +1,5 @@
import {createSlice, PayloadAction} from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import {jwtDecode, JwtPayload} from "jwt-decode"; import { jwtDecode, JwtPayload } from "jwt-decode";
interface AuthState { interface AuthState {
isAuthorized: boolean; isAuthorized: boolean;
@@ -15,34 +15,35 @@ const initialState = (): AuthState => {
return { return {
accessToken: "", accessToken: "",
isAuthorized: false, isAuthorized: false,
isGuest: false isGuest: false,
} };
} };
const authSlice = createSlice({ const authSlice = createSlice({
name: 'auth', name: "auth",
initialState, initialState,
reducers: { reducers: {
login: (state, action: PayloadAction<{ accessToken: string }>) => { login: (state, action: PayloadAction<{ accessToken: string }>) => {
try { try {
const {sub} = jwtDecode<JwtPayload>(action.payload.accessToken); const { sub } = jwtDecode<JwtPayload>(
action.payload.accessToken
);
state.accessToken = action.payload.accessToken; state.accessToken = action.payload.accessToken;
state.isAuthorized = true; state.isAuthorized = true;
if (sub === "guest") if (sub === "guest") state.isGuest = true;
state.isGuest = true;
} catch (_) { } catch (_) {
const url = window.location.href; const url = window.location.href;
const urlObj = new URL(url); const urlObj = new URL(url);
urlObj.search = ''; urlObj.search = "";
history.replaceState(null, '', urlObj); history.replaceState(null, "", urlObj);
window.location.reload(); window.location.reload();
} }
}, },
logout: (state) => { logout: state => {
state.isAuthorized = false; state.isAuthorized = false;
state.accessToken = ''; state.accessToken = "";
} },
} },
}) });
export const {login, logout} = authSlice.actions; export const { login, logout } = authSlice.actions;
export default authSlice.reducer; export default authSlice.reducer;

View File

@@ -1,7 +1,7 @@
import {createSlice, PayloadAction} from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import {notifications} from "../shared/lib/notifications.ts"; import { notifications } from "../shared/lib/notifications.ts";
import {IconCheck, IconX} from "@tabler/icons-react"; import { IconCheck, IconX } from "@tabler/icons-react";
import {rem} from "@mantine/core"; import { rem } from "@mantine/core";
export type TaskData = { export type TaskData = {
title: string; title: string;
@@ -43,13 +43,15 @@ const tasksSlice = createSlice({
autoClose: false, autoClose: false,
withCloseButton: false, withCloseButton: false,
withBorder: true, withBorder: true,
radius: "sm" radius: "sm",
}); });
state.tasks.push(task); state.tasks.push(task);
localStorage.setItem("tasks", JSON.stringify(state.tasks)); localStorage.setItem("tasks", JSON.stringify(state.tasks));
}, },
removeTask: (state, action: PayloadAction<string>) => { removeTask: (state, action: PayloadAction<string>) => {
state.tasks = state.tasks.filter((task) => task.id !== action.payload); state.tasks = state.tasks.filter(
task => task.id !== action.payload
);
localStorage.setItem("tasks", JSON.stringify(state.tasks)); localStorage.setItem("tasks", JSON.stringify(state.tasks));
}, },
failTask: (state, action: PayloadAction<Task>) => { failTask: (state, action: PayloadAction<Task>) => {
@@ -59,16 +61,20 @@ const tasksSlice = createSlice({
if (!notificationId) return; if (!notificationId) return;
notifications.update({ notifications.update({
id: notificationId, id: notificationId,
color: 'red', color: "red",
title: task.config.onErrorData.title, title: task.config.onErrorData.title,
message: task.config.onErrorData.message, message: task.config.onErrorData.message,
icon: <IconX style={{width: rem(18), height: rem(18)}}/>, icon: <IconX style={{ width: rem(18), height: rem(18) }} />,
loading: false, loading: false,
autoClose: 2000, autoClose: 2000,
}) });
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id); state.tasks = state.tasks.filter(
task => task.id !== action.payload.id
);
state.notificationTaskMap = Object.fromEntries( state.notificationTaskMap = Object.fromEntries(
Object.entries(state.notificationTaskMap).filter(([taskId]) => taskId !== task.id) Object.entries(state.notificationTaskMap).filter(
([taskId]) => taskId !== task.id
)
); );
localStorage.setItem("tasks", JSON.stringify(state.tasks)); localStorage.setItem("tasks", JSON.stringify(state.tasks));
}, },
@@ -79,23 +85,28 @@ const tasksSlice = createSlice({
if (!notificationId) return; if (!notificationId) return;
notifications.update({ notifications.update({
id: notificationId, id: notificationId,
color: 'teal', color: "teal",
title: task.config.onSuccessData.title, title: task.config.onSuccessData.title,
message: task.config.onSuccessData.message, message: task.config.onSuccessData.message,
icon: <IconCheck style={{width: rem(18), height: rem(18)}}/>, icon: <IconCheck style={{ width: rem(18), height: rem(18) }} />,
loading: false, loading: false,
autoClose: 2000, autoClose: 2000,
}) });
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id); state.tasks = state.tasks.filter(
task => task.id !== action.payload.id
);
state.notificationTaskMap = Object.fromEntries( state.notificationTaskMap = Object.fromEntries(
Object.entries(state.notificationTaskMap).filter(([taskId]) => taskId !== task.id) Object.entries(state.notificationTaskMap).filter(
([taskId]) => taskId !== task.id
)
); );
localStorage.setItem("tasks", JSON.stringify(state.tasks)); localStorage.setItem("tasks", JSON.stringify(state.tasks));
} },
}, },
}); });
export const {addTask, removeTask, successTask, failTask} = tasksSlice.actions; export const { addTask, removeTask, successTask, failTask } =
tasksSlice.actions;
export default tasksSlice.reducer; export default tasksSlice.reducer;

View File

@@ -1,23 +1,22 @@
import {createSlice, PayloadAction} from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface UIState { interface UIState {
isLoading: boolean; isLoading: boolean;
} }
const initialState: UIState = { const initialState: UIState = {
isLoading: false isLoading: false,
} };
const uiSlice = createSlice({ const uiSlice = createSlice({
name: 'ui', name: "ui",
initialState, initialState,
reducers: { reducers: {
setIsLoading: (state, action: PayloadAction<boolean>) => { setIsLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload; state.isLoading = action.payload;
} },
} },
} });
)
export default uiSlice.reducer; export default uiSlice.reducer;
export const {setIsLoading} = uiSlice.actions; export const { setIsLoading } = uiSlice.actions;

View File

@@ -1,51 +1,68 @@
import {QueryObserverResult, RefetchOptions, useQuery} from "@tanstack/react-query"; import {
import {CancelablePromise, PaginationInfoSchema} from "../client"; QueryObserverResult,
import {Pagination} from "../types/Pagination.ts"; RefetchOptions,
useQuery,
} from "@tanstack/react-query";
import { CancelablePromise, PaginationInfoSchema } from "../client";
import { Pagination } from "../types/Pagination.ts";
type Props<T, K> = { type Props<T, K> = {
queryFn: () => CancelablePromise<T>, queryFn: () => CancelablePromise<T>;
getObjectsFn: (response: T) => K[], getObjectsFn: (response: T) => K[];
queryKey: string queryKey: string;
} };
type Response<T, K> = { type Response<T, K> = {
objects: K[], objects: K[];
refetch: (options?: RefetchOptions) => Promise<QueryObserverResult<T, Error>> refetch: (
} options?: RefetchOptions
const ObjectList = <T, K, >(props: Props<T, K>): Response<T, K> => { ) => Promise<QueryObserverResult<T, Error>>;
const {isPending, error, data, refetch} = useQuery({ };
const ObjectList = <T, K>(props: Props<T, K>): Response<T, K> => {
const { isPending, error, data, refetch } = useQuery({
queryKey: [props.queryKey], queryKey: [props.queryKey],
queryFn: props.queryFn queryFn: props.queryFn,
}); });
const objects = isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data); const objects =
return {objects, refetch} isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
} return { objects, refetch };
};
interface ObjectWithPagination { interface ObjectWithPagination {
paginationInfo: PaginationInfoSchema paginationInfo: PaginationInfoSchema;
} }
type PropsWithPagination<T extends ObjectWithPagination, K> = { type PropsWithPagination<T extends ObjectWithPagination, K> = {
queryFn: () => CancelablePromise<T>, queryFn: () => CancelablePromise<T>;
getObjectsFn: (response: T) => K[], getObjectsFn: (response: T) => K[];
queryKey: string, queryKey: string;
pagination: Pagination pagination: Pagination;
} };
type ResponseWithPagination<T extends ObjectWithPagination, K> = { type ResponseWithPagination<T extends ObjectWithPagination, K> = {
objects: K[], objects: K[];
pagination?: PaginationInfoSchema, pagination?: PaginationInfoSchema;
refetch: (options?: RefetchOptions) => Promise<QueryObserverResult<T, Error>> refetch: (
} options?: RefetchOptions
export const ObjectListWithPagination = <T extends ObjectWithPagination, K, >(props: PropsWithPagination<T, K>): ResponseWithPagination<T, K> => { ) => Promise<QueryObserverResult<T, Error>>;
const {isPending, error, data, refetch} = useQuery({ };
queryKey: [props.queryKey, props, props.pagination.itemsPerPage, props.pagination.page], export const ObjectListWithPagination = <T extends ObjectWithPagination, K>(
props: PropsWithPagination<T, K>
): ResponseWithPagination<T, K> => {
const { isPending, error, data, refetch } = useQuery({
queryKey: [
props.queryKey,
props,
props.pagination.itemsPerPage,
props.pagination.page,
],
queryFn: props.queryFn, queryFn: props.queryFn,
}); });
const objects = isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data); const objects =
isPending || error || !data ? ([] as K[]) : props.getObjectsFn(data);
return { return {
objects, objects,
pagination: data?.paginationInfo, pagination: data?.paginationInfo,
refetch refetch,
} };
} };
export default ObjectList export default ObjectList;

View File

@@ -1,9 +1,10 @@
import ObjectList from "./objectList.tsx"; import ObjectList from "./objectList.tsx";
import {MarketplaceService} from "../client"; import { MarketplaceService } from "../client";
const useBaseMarketplacesList = () => ObjectList({ const useBaseMarketplacesList = () =>
queryFn: MarketplaceService.getAllBaseMarketplaces, ObjectList({
getObjectsFn: (response) => response.baseMarketplaces, queryFn: MarketplaceService.getAllBaseMarketplaces,
queryKey: "getAllBaseMarketplaces" getObjectsFn: response => response.baseMarketplaces,
}) queryKey: "getAllBaseMarketplaces",
export default useBaseMarketplacesList; });
export default useBaseMarketplacesList;

View File

@@ -7,14 +7,13 @@ type Props<T> = {
export function useCRUD<T>(props: Props<T>) { export function useCRUD<T>(props: Props<T>) {
const onCreate = (element: T) => { const onCreate = (element: T) => {
props.onCreate(element); props.onCreate(element);
} };
const onChange = (element: T) => { const onChange = (element: T) => {
props.onChange(element); props.onChange(element);
};
}
const onDelete = (element: T) => { const onDelete = (element: T) => {
props.onDelete(element); props.onDelete(element);
} };
return {onCreate, onChange, onDelete}; return { onCreate, onChange, onDelete };
} }

View File

@@ -1,11 +1,11 @@
import {ObjectListWithPagination} from "./objectList.tsx"; import { ObjectListWithPagination } from "./objectList.tsx";
import {PayrollService} from "../client"; import { PayrollService } from "../client";
import {Pagination} from "../types/Pagination.ts"; import { Pagination } from "../types/Pagination.ts";
export const usePaymentRecordsList = (pagination: Pagination) =>
export const usePaymentRecordsList = (pagination: Pagination) => ObjectListWithPagination({ ObjectListWithPagination({
queryFn: () => PayrollService.getPaymentRecords(pagination), queryFn: () => PayrollService.getPaymentRecords(pagination),
queryKey: "getPaymentRecords", queryKey: "getPaymentRecords",
getObjectsFn: (response) => response.paymentRecords, getObjectsFn: response => response.paymentRecords,
pagination pagination,
}) });

View File

@@ -1,9 +1,10 @@
import ObjectList from "./objectList.tsx"; import ObjectList from "./objectList.tsx";
import {PayrollService} from "../client"; import { PayrollService } from "../client";
const usePayrollSchemasList = () => ObjectList({ const usePayrollSchemasList = () =>
queryFn: PayrollService.getAllPayrollSchemas, ObjectList({
getObjectsFn: (response) => response.payrollSchemas, queryFn: PayrollService.getAllPayrollSchemas,
queryKey: "getAllPayrollSchemas" getObjectsFn: response => response.payrollSchemas,
}) queryKey: "getAllPayrollSchemas",
export default usePayrollSchemasList; });
export default usePayrollSchemasList;

View File

@@ -1,4 +1,4 @@
import {useEffect, useRef, DependencyList} from 'react'; import { useEffect, useRef, DependencyList } from "react";
type UsePollingEffectOptions = { type UsePollingEffectOptions = {
interval?: number; interval?: number;
@@ -11,17 +11,13 @@ function usePollingEffect(
dependencies: DependencyList = [], dependencies: DependencyList = [],
options: UsePollingEffectOptions = {} options: UsePollingEffectOptions = {}
): void { ): void {
const { const { interval = 3000, isActive = true, onCleanUp = () => {} } = options;
interval = 3000,
isActive = true,
onCleanUp = () => {
}
} = options;
const timeoutIdRef = useRef<number | null>(null); const timeoutIdRef = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
if (!isActive) { // If not active, don't do anything if (!isActive) {
// If not active, don't do anything
return; return;
} }
@@ -32,7 +28,10 @@ function usePollingEffect(
await asyncCallback(); await asyncCallback();
} finally { } finally {
if (!stopped) { if (!stopped) {
timeoutIdRef.current = setTimeout(pollingCallback, interval); timeoutIdRef.current = setTimeout(
pollingCallback,
interval
);
} }
} }
}; };

View File

@@ -15,4 +15,4 @@ body {
:root { :root {
--item-border-size: 0.1rem; --item-border-size: 0.1rem;
--item-border-radius: 0.5rem; --item-border-radius: 0.5rem;
} }

View File

@@ -40,22 +40,24 @@ const queryClient = new QueryClient();
// Configuring OpenAPI // Configuring OpenAPI
OpenAPI.BASE = import.meta.env.VITE_API_URL; OpenAPI.BASE = import.meta.env.VITE_API_URL;
OpenAPI.TOKEN = JSON.parse(localStorage.getItem("authState") || "{}")["accessToken"]; OpenAPI.TOKEN = JSON.parse(localStorage.getItem("authState") || "{}")[
"accessToken"
];
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<Provider store={store}> <Provider store={store}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<MantineProvider defaultColorScheme={"dark"}> <MantineProvider defaultColorScheme={"dark"}>
<ModalsProvider labels={{ confirm: "Да", cancel: "Нет" }} modals={modals}> <ModalsProvider
labels={{ confirm: "Да", cancel: "Нет" }}
modals={modals}>
<DatesProvider settings={{ locale: "ru" }}> <DatesProvider settings={{ locale: "ru" }}>
<TasksProvider> <TasksProvider>
<RouterProvider router={router} /> <RouterProvider router={router} />
<Notifications /> <Notifications />
</TasksProvider> </TasksProvider>
</DatesProvider> </DatesProvider>
</ModalsProvider> </ModalsProvider>
</MantineProvider> </MantineProvider>
</QueryClientProvider> </QueryClientProvider>
</Provider>, </Provider>
); );

View File

@@ -1,18 +1,18 @@
import {ContextModalProps} from "@mantine/modals"; import { ContextModalProps } from "@mantine/modals";
import {Button, Flex, rem, TextInput} from "@mantine/core"; import { Button, Flex, rem, TextInput } from "@mantine/core";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import {ProductService} from "../../client"; import { ProductService } from "../../client";
type Props = { type Props = {
productId: number; productId: number;
onSubmit: (barcode: string) => void; onSubmit: (barcode: string) => void;
} };
const PrintBarcodeModal = ({ const PrintBarcodeModal = ({
id, id,
context, context,
innerProps innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const {productId, onSubmit} = innerProps; const { productId, onSubmit } = innerProps;
const [barcode, setBarcode] = useState<string | undefined>(); const [barcode, setBarcode] = useState<string | undefined>();
const [isBarcodeExist, setIsBarcodeExist] = useState<boolean>(true); const [isBarcodeExist, setIsBarcodeExist] = useState<boolean>(true);
@@ -20,44 +20,41 @@ const PrintBarcodeModal = ({
if (!barcode) return "Штрихкод не может быть пустым"; if (!barcode) return "Штрихкод не может быть пустым";
if (isBarcodeExist) return "Штрихкод уже существует"; if (isBarcodeExist) return "Штрихкод уже существует";
return undefined; return undefined;
} };
useEffect(() => { useEffect(() => {
if (!barcode) return; if (!barcode) return;
ProductService.existsProductBarcode({ ProductService.existsProductBarcode({
productId: innerProps.productId, productId: innerProps.productId,
barcode: barcode.trim() barcode: barcode.trim(),
}).then((response) => { }).then(response => {
setIsBarcodeExist(response.exists); setIsBarcodeExist(response.exists);
}) });
}, [productId, barcode]); }, [productId, barcode]);
const onSubmitClick = () => { const onSubmitClick = () => {
if (!barcode || isBarcodeExist) return; if (!barcode || isBarcodeExist) return;
onSubmit(barcode.trim()); onSubmit(barcode.trim());
context.closeModal(id); context.closeModal(id);
} };
return ( return (
<Flex <Flex
gap={rem(10)} gap={rem(10)}
direction={"column"} direction={"column"}>
>
<TextInput <TextInput
required required
error={getErrorMessage()} error={getErrorMessage()}
label={"Штрихкод"} label={"Штрихкод"}
placeholder={"Введите или отсканируйте штрихкод"} placeholder={"Введите или отсканируйте штрихкод"}
value={barcode} value={barcode}
onChange={(event) => setBarcode(event.currentTarget.value)} onChange={event => setBarcode(event.currentTarget.value)}
/> />
<Button <Button
onClick={onSubmitClick} onClick={onSubmitClick}
disabled={!barcode || isBarcodeExist} disabled={!barcode || isBarcodeExist}>
>
Добавить Добавить
</Button> </Button>
</Flex> </Flex>
) );
} };
export default PrintBarcodeModal; export default PrintBarcodeModal;

View File

@@ -1,67 +1,68 @@
import {UserSchema} from "../../client"; import { UserSchema } from "../../client";
import {ObjectSelectProps} from "../../components/ObjectSelect/ObjectSelect.tsx"; import { ObjectSelectProps } from "../../components/ObjectSelect/ObjectSelect.tsx";
import {ContextModalProps} from "@mantine/modals"; import { ContextModalProps } from "@mantine/modals";
import {Button, Flex, rem} from "@mantine/core"; import { Button, Flex, rem } from "@mantine/core";
import {useState} from "react"; import { useState } from "react";
import UserSelect from "../../components/Selects/UserSelect/UserSelect.tsx"; import UserSelect from "../../components/Selects/UserSelect/UserSelect.tsx";
import {notifications} from "../../shared/lib/notifications.ts"; import { notifications } from "../../shared/lib/notifications.ts";
type SelectProps = Omit<ObjectSelectProps<UserSchema>, 'data' | 'getValueFn' | 'getLabelFn' | 'onChange'>; type SelectProps = Omit<
ObjectSelectProps<UserSchema>,
"data" | "getValueFn" | "getLabelFn" | "onChange"
>;
type Props = { type Props = {
onSelect: (user: UserSchema) => void onSelect: (user: UserSchema) => void;
selectProps?: SelectProps; selectProps?: SelectProps;
} };
const EmployeeSelectModal = ({ const EmployeeSelectModal = ({
context, context,
id, id,
innerProps, innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const [innerValue, setInnerValue] = useState<UserSchema | undefined>(); const [innerValue, setInnerValue] = useState<UserSchema | undefined>();
const closeSelf = () => { const closeSelf = () => {
context.closeContextModal(id); context.closeContextModal(id);
} };
const onSelectClick = () => { const onSelectClick = () => {
if (!innerValue) { if (!innerValue) {
notifications.error({message: "Необходимо выбрать сотрудника"}) notifications.error({ message: "Необходимо выбрать сотрудника" });
return; return;
} }
innerProps.onSelect(innerValue); innerProps.onSelect(innerValue);
closeSelf(); closeSelf();
} };
const onCloseClick = () => { const onCloseClick = () => {
closeSelf(); closeSelf();
} };
return ( return (
<Flex direction={"column"} <Flex
gap={rem(10)} direction={"column"}
> gap={rem(10)}>
<Flex w={"100%"}> <Flex w={"100%"}>
<UserSelect <UserSelect
onChange={setInnerValue} onChange={setInnerValue}
w={"100%"} w={"100%"}
placeholder={"Выберите сотрудника"} placeholder={"Выберите сотрудника"}
{...innerProps.selectProps} {...innerProps.selectProps}
/> />
</Flex> </Flex>
<Flex justify={"flex-end"} gap={rem(10)}> <Flex
justify={"flex-end"}
gap={rem(10)}>
<Button <Button
variant={"default"} variant={"default"}
onClick={() => onCloseClick()} onClick={() => onCloseClick()}>
>
Отменить Отменить
</Button> </Button>
<Button <Button onClick={() => onSelectClick()}>
onClick={() => onSelectClick()}
>
Выбрать сотрудника Выбрать сотрудника
</Button> </Button>
</Flex> </Flex>
</Flex> </Flex>
) );
} };
export default EmployeeSelectModal export default EmployeeSelectModal;

View File

@@ -1,29 +1,27 @@
import SimpleUsersTable from "../../pages/LeadsPage/components/SimpleUsersTable/SimpleUsersTable.tsx"; import SimpleUsersTable from "../../pages/LeadsPage/components/SimpleUsersTable/SimpleUsersTable.tsx";
import {ContextModalProps} from "@mantine/modals"; import { ContextModalProps } from "@mantine/modals";
import {Flex, rem} from "@mantine/core"; import { Flex, rem } from "@mantine/core";
import {UserSchema} from "../../client"; import { UserSchema } from "../../client";
import {MutableRefObject, useEffect} from "react"; import { MutableRefObject, useEffect } from "react";
type Props = { type Props = {
items: MutableRefObject<UserSchema[]> items: MutableRefObject<UserSchema[]>;
onChange: (items: UserSchema[]) => void; onChange: (items: UserSchema[]) => void;
}; };
const EmployeeTableModal = ({ const EmployeeTableModal = ({ innerProps }: ContextModalProps<Props>) => {
innerProps useEffect(() => {}, [innerProps.items.current]);
}: ContextModalProps<Props>) => {
useEffect(() => {
}, [innerProps.items.current])
return ( return (
<Flex direction={"column"} gap={rem(10)}> <Flex
direction={"column"}
gap={rem(10)}>
<SimpleUsersTable <SimpleUsersTable
items={innerProps.items.current} items={innerProps.items.current}
onChange={(event) => { onChange={event => {
innerProps.onChange(event); innerProps.onChange(event);
}} }}
/> />
</Flex> </Flex>
) );
} };
export default EmployeeTableModal; export default EmployeeTableModal;

View File

@@ -1,81 +1,83 @@
import {ContextModalProps} from "@mantine/modals"; import { ContextModalProps } from "@mantine/modals";
import {Button, Flex, rem, Textarea} from "@mantine/core"; import { Button, Flex, rem, Textarea } from "@mantine/core";
import {DateTimePicker, DateValue} from "@mantine/dates"; import { DateTimePicker, DateValue } from "@mantine/dates";
import {useForm} from "@mantine/form"; import { useForm } from "@mantine/form";
import {DealSummaryReorderRequest} from "../../client"; import { DealSummaryReorderRequest } from "../../client";
import {dateWithoutTimezone} from "../../shared/lib/date.ts"; import { dateWithoutTimezone } from "../../shared/lib/date.ts";
type Deadline = { type Deadline = {
deadline: DateValue, deadline: DateValue;
comment: string comment: string;
} };
type Props = { type Props = {
request: Partial<DealSummaryReorderRequest>, request: Partial<DealSummaryReorderRequest>;
onSubmit: ( onSubmit: (request: DealSummaryReorderRequest) => void;
request: DealSummaryReorderRequest, };
) => void;
}
const EnterDeadlineModal = ({ const EnterDeadlineModal = ({
context, context,
id, id,
innerProps, innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const form = useForm<Deadline>({ const form = useForm<Deadline>({
initialValues: { initialValues: {
deadline: null, deadline: null,
comment: '', comment: "",
}, },
validate: { validate: {
deadline: (datetime) => datetime !== null ? null : 'Необходимо ввести дедлайн', deadline: datetime =>
} datetime !== null ? null : "Необходимо ввести дедлайн",
}) },
});
const onCancelClick = () => { const onCancelClick = () => {
context.closeModal(id); context.closeModal(id);
} };
const onSubmit = (values: Deadline) => { const onSubmit = (values: Deadline) => {
const {deadline, comment} = values; const { deadline, comment } = values;
if (!deadline) return; if (!deadline) return;
innerProps.onSubmit({ innerProps.onSubmit({
...innerProps.request, ...innerProps.request,
deadline: dateWithoutTimezone(deadline), deadline: dateWithoutTimezone(deadline),
comment comment,
} as unknown as DealSummaryReorderRequest); } as unknown as DealSummaryReorderRequest);
context.closeModal(id); context.closeModal(id);
} };
return ( return (
<form onSubmit={form.onSubmit((values) => onSubmit(values))}> <form onSubmit={form.onSubmit(values => onSubmit(values))}>
<Flex
<Flex direction={'column'} gap={10}> direction={"column"}
<Flex direction={'column'} gap={rem(10)}> gap={10}>
<Flex
direction={"column"}
gap={rem(10)}>
<DateTimePicker <DateTimePicker
required required
label={'Дата и время'} label={"Дата и время"}
placeholder={'Введите дату и время'} placeholder={"Введите дату и время"}
minDate={new Date()} minDate={new Date()}
{...form.getInputProps('deadline')} {...form.getInputProps("deadline")}
/> />
<Textarea <Textarea
label={'Коментарий'} label={"Коментарий"}
placeholder={'Введите коментарий'} placeholder={"Введите коментарий"}
{...form.getInputProps('comment')} {...form.getInputProps("comment")}
/> />
</Flex> </Flex>
<Flex justify={'flex-end'} gap={rem(10)}> <Flex
justify={"flex-end"}
gap={rem(10)}>
<Button <Button
variant={'default'} variant={"default"}
onClick={onCancelClick} onClick={onCancelClick}>
>Отменить</Button> Отменить
<Button </Button>
type={'submit'} <Button type={"submit"}>Сохранить</Button>
>Сохранить</Button>
</Flex> </Flex>
</Flex> </Flex>
</form> </form>
);
)
}; };
export default EnterDeadlineModal; export default EnterDeadlineModal;

View File

@@ -1,31 +1,38 @@
import BaseFormModal, {CreateEditFormProps} from "../../pages/ClientsPage/modals/BaseFormModal/BaseFormModal.tsx"; import BaseFormModal, {
import {PositionSchema} from "../../client"; CreateEditFormProps,
import {ContextModalProps} from "@mantine/modals"; } from "../../pages/ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {useForm} from "@mantine/form"; import { PositionSchema } from "../../client";
import {Flex, rem, TextInput} from "@mantine/core"; import { ContextModalProps } from "@mantine/modals";
import {useEffect} from "react"; import { useForm } from "@mantine/form";
import CyrillicToTranslit from 'cyrillic-to-translit-js'; import { Flex, rem, TextInput } from "@mantine/core";
import { useEffect } from "react";
import CyrillicToTranslit from "cyrillic-to-translit-js";
type Props = CreateEditFormProps<PositionSchema>; type Props = CreateEditFormProps<PositionSchema>;
const PositionFormModal = ({ const PositionFormModal = ({
id, id,
context, context,
innerProps innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const translit = CyrillicToTranslit({preset: "ru"}) const translit = CyrillicToTranslit({ preset: "ru" });
const isEditing = 'element' in innerProps; const isEditing = "element" in innerProps;
const initialValues: PositionSchema = isEditing ? innerProps.element : { const initialValues: PositionSchema = isEditing
key: "", ? innerProps.element
name: "" : {
} key: "",
name: "",
};
const form = useForm<PositionSchema>({ const form = useForm<PositionSchema>({
initialValues: initialValues initialValues: initialValues,
}) });
useEffect(() => { useEffect(() => {
if (isEditing) return; if (isEditing) return;
form.setFieldValue("key", translit.transform(form.values.name).toLowerCase()); form.setFieldValue(
"key",
translit.transform(form.values.name).toLowerCase()
);
}, [form.values.name]); }, [form.values.name]);
return ( return (
@@ -33,13 +40,11 @@ const PositionFormModal = ({
closeOnSubmit closeOnSubmit
form={form} form={form}
onClose={() => context.closeContextModal(id)} onClose={() => context.closeContextModal(id)}
{...innerProps} {...innerProps}>
>
<BaseFormModal.Body> <BaseFormModal.Body>
<Flex <Flex
direction={"column"} direction={"column"}
gap={rem(10)} gap={rem(10)}>
>
<TextInput <TextInput
label={"Название"} label={"Название"}
placeholder={"Введите название должности"} placeholder={"Введите название должности"}
@@ -53,7 +58,7 @@ const PositionFormModal = ({
</Flex> </Flex>
</BaseFormModal.Body> </BaseFormModal.Body>
</BaseFormModal> </BaseFormModal>
) );
} };
export default PositionFormModal; export default PositionFormModal;

View File

@@ -1,72 +1,88 @@
import {BarcodeAttributeSchema} from "../../client"; import { BarcodeAttributeSchema } from "../../client";
import {forwardRef} from "react"; import { forwardRef } from "react";
import styles from "./PrintBarcodeModal.module.css"; import styles from "./PrintBarcodeModal.module.css";
import {Flex, Text} from "@mantine/core"; import { Flex, Text } from "@mantine/core";
import Barcode from "react-barcode"; import Barcode from "react-barcode";
type Props = { type Props = {
attributes: BarcodeAttributeSchema[] attributes: BarcodeAttributeSchema[];
barcode?: string; barcode?: string;
quantity: number; quantity: number;
additionalField?: string | null; additionalField?: string | null;
} };
type Ref = HTMLDivElement; type Ref = HTMLDivElement;
const PrintBarcodeContainer = forwardRef<Ref, Props>(function PrintBarcodeContainer(props: Props, ref) { const PrintBarcodeContainer = forwardRef<Ref, Props>(
const {attributes, barcode, quantity, additionalField} = props; function PrintBarcodeContainer(props: Props, ref) {
const { attributes, barcode, quantity, additionalField } = props;
const MAX_ATTRIBUTES = additionalField && additionalField.length > 0 ? 5 : 6; const MAX_ATTRIBUTES =
const MIN_BARCODE_SIZE = 30; additionalField && additionalField.length > 0 ? 5 : 6;
const MAX_BARCODE_SIZE = 100; const MIN_BARCODE_SIZE = 30;
const STEP = (MAX_BARCODE_SIZE - MIN_BARCODE_SIZE) / MAX_ATTRIBUTES; const MAX_BARCODE_SIZE = 100;
const MAX_ATTRIBUTE_LENGTH = 35; const STEP = (MAX_BARCODE_SIZE - MIN_BARCODE_SIZE) / MAX_ATTRIBUTES;
const MAX_ATTRIBUTE_LENGTH = 35;
const getBarcodeHeight = () => { const getBarcodeHeight = () => {
return MIN_BARCODE_SIZE + (MAX_ATTRIBUTES - attributes.length) * STEP; return (
} MIN_BARCODE_SIZE + (MAX_ATTRIBUTES - attributes.length) * STEP
const getAttributeText = (attribute: BarcodeAttributeSchema) => { );
let result = `${attribute.name}: ${attribute.value}`; };
if (result.length > MAX_ATTRIBUTE_LENGTH) { const getAttributeText = (attribute: BarcodeAttributeSchema) => {
result = result.slice(0, MAX_ATTRIBUTE_LENGTH - 1) + "."; let result = `${attribute.name}: ${attribute.value}`;
} if (result.length > MAX_ATTRIBUTE_LENGTH) {
return result; result = result.slice(0, MAX_ATTRIBUTE_LENGTH - 1) + ".";
}
return result;
};
return (
<div
className={styles["print-only"]}
ref={ref}>
{barcode &&
Array.from({ length: quantity }).map((_, index) => (
<>
<Flex
className={styles["barcode-container"]}
key={index}
justify={"center"}
direction={"column"}>
<Flex
align={"center"}
justify={"center"}>
<Barcode
margin={0}
height={getBarcodeHeight()}
value={barcode}
/>
</Flex>
{attributes
.slice(0, MAX_ATTRIBUTES)
.map(attr => (
<Text
key={attr.name}
className={
styles["barcode-attribute-text"]
}
size={"xs"}>
{getAttributeText(attr)}
</Text>
))}
{props.additionalField && (
<Text
className={
styles["barcode-attribute-text"]
}
size={"xs"}>
{props.additionalField}
</Text>
)}
</Flex>
</>
))}
</div>
);
} }
);
return ( export default PrintBarcodeContainer;
<div className={styles['print-only']} ref={ref}>
{barcode && Array.from({length: quantity}).map((_, index) => (
<>
<Flex
className={styles['barcode-container']}
key={index}
justify={"center"}
direction={"column"}
>
<Flex align={"center"} justify={"center"}>
<Barcode margin={0} height={getBarcodeHeight()} value={barcode}/>
</Flex>
{attributes.slice(0, MAX_ATTRIBUTES).map((attr) => (
<Text
key={attr.name}
className={styles['barcode-attribute-text']}
size={"xs"}
>
{getAttributeText(attr)}
</Text>
))}
{props.additionalField && (
<Text
className={styles['barcode-attribute-text']}
size={"xs"}
>
{props.additionalField}
</Text>
)}
</Flex>
</>
))}
</div>
)
});
export default PrintBarcodeContainer;

View File

@@ -7,7 +7,6 @@
display: block; display: block;
} }
.barcode-container { .barcode-container {
max-height: 1.45in; max-height: 1.45in;
text-align: left; text-align: left;
@@ -19,4 +18,4 @@
.barcode-attribute-text { .barcode-attribute-text {
line-height: 1; line-height: 1;
} }
} }

View File

@@ -1,25 +1,23 @@
import {ContextModalProps, modals} from "@mantine/modals"; import { ContextModalProps, modals } from "@mantine/modals";
import {Button, Divider, Flex, NumberInput, rem, Select} from "@mantine/core"; import { Button, Divider, Flex, NumberInput, rem, Select } from "@mantine/core";
import {useEffect, useRef, useState} from "react"; import { useEffect, useRef, useState } from "react";
import {BarcodeSchema, ProductService} from "../../client"; import { BarcodeSchema, ProductService } from "../../client";
import {useGetProductById} from "../../api/product/useGetProductById.tsx"; import { useGetProductById } from "../../api/product/useGetProductById.tsx";
import {notifications} from "../../shared/lib/notifications.ts"; import { notifications } from "../../shared/lib/notifications.ts";
import PrintBarcodeContainer from "./PrintBarcodeContainer.tsx"; import PrintBarcodeContainer from "./PrintBarcodeContainer.tsx";
import {base64ToBlob} from "../../shared/lib/utils.ts"; import { base64ToBlob } from "../../shared/lib/utils.ts";
type Props = { type Props = {
productId: number; productId: number;
defaultQuantity?: number; defaultQuantity?: number;
} };
const PrintBarcodeModal = ({ const PrintBarcodeModal = ({ innerProps }: ContextModalProps<Props>) => {
innerProps const { productId, defaultQuantity = 1 } = innerProps;
}: ContextModalProps<Props>) => {
const {productId, defaultQuantity = 1} = innerProps;
const [quantity, setQuantity] = useState(defaultQuantity); const [quantity, setQuantity] = useState(defaultQuantity);
const [barcode, setBarcode] = useState<string | undefined>() const [barcode, setBarcode] = useState<string | undefined>();
const [barcodeData, setBarcodeData] = useState<BarcodeSchema | undefined>(); const [barcodeData, setBarcodeData] = useState<BarcodeSchema | undefined>();
const {product, refetch} = useGetProductById(productId); const { product, refetch } = useGetProductById(productId);
const barcodeRef = useRef(null); const barcodeRef = useRef(null);
// const handlePrint = useReactToPrint({ // const handlePrint = useReactToPrint({
@@ -27,47 +25,47 @@ const PrintBarcodeModal = ({
// }); // });
const onAdd = (newBarcode: string) => { const onAdd = (newBarcode: string) => {
ProductService.addProductBarcode({requestBody: {productId, barcode: newBarcode}}) ProductService.addProductBarcode({
.then(async ({ok, message}) => { requestBody: { productId, barcode: newBarcode },
notifications.guess(ok, {message}); }).then(async ({ ok, message }) => {
}) notifications.guess(ok, { message });
} });
};
const onAddClick = () => { const onAddClick = () => {
if (!product) return; if (!product) return;
modals.openContextModal({ modals.openContextModal({
modal: "addBarcode", modal: "addBarcode",
title: 'Добавление штрихкода', title: "Добавление штрихкода",
withCloseButton: true, withCloseButton: true,
innerProps: { innerProps: {
productId: product.id, productId: product.id,
onSubmit: onAdd onSubmit: onAdd,
} },
}) });
} };
const onGenerateClick = () => { const onGenerateClick = () => {
if (!product) return; if (!product) return;
ProductService.generateProductBarcode({requestBody: {productId}}) ProductService.generateProductBarcode({
.then(async ({ok, message, barcode}) => { requestBody: { productId },
notifications.guess(ok, {message}); }).then(async ({ ok, message, barcode }) => {
if (!ok) return; notifications.guess(ok, { message });
await refetch(); if (!ok) return;
setBarcode(barcode); await refetch();
}) setBarcode(barcode);
} });
};
const fetchBarcodeData = () => { const fetchBarcodeData = () => {
if (!barcode) return; if (!barcode) return;
ProductService.getProductBarcode({ ProductService.getProductBarcode({
requestBody: requestBody: { barcode, productId },
{barcode, productId} }).then(({ barcode }) => {
}).then(({barcode}) => {
setBarcodeData(barcode); setBarcodeData(barcode);
}) });
} };
useEffect(() => { useEffect(() => {
if (!product) return; if (!product) return;
if (product.barcodes.length === 1) if (product.barcodes.length === 1) setBarcode(product.barcodes[0]);
setBarcode(product.barcodes[0]);
}, [product]); }, [product]);
useEffect(() => { useEffect(() => {
@@ -79,11 +77,10 @@ const PrintBarcodeModal = ({
<> <>
<Flex <Flex
gap={rem(10)} gap={rem(10)}
direction={"column"} direction={"column"}>
>
<Select <Select
value={barcode} value={barcode}
onChange={(value) => setBarcode(value || undefined)} onChange={value => setBarcode(value || undefined)}
data={product?.barcodes} data={product?.barcodes}
label={"Штрихкод"} label={"Штрихкод"}
placeholder={"Выберите штрихкод"} placeholder={"Выберите штрихкод"}
@@ -92,14 +89,16 @@ const PrintBarcodeModal = ({
label={"Количество копий"} label={"Количество копий"}
placeholder={"Введите количество копий"} placeholder={"Введите количество копий"}
value={quantity} value={quantity}
onChange={(value) => typeof value === "number" && setQuantity(value)} onChange={value =>
typeof value === "number" && setQuantity(value)
}
min={1} min={1}
/> />
<Divider <Divider my={rem(10)} />
my={rem(10)} <Flex
/> direction={"column"}
<Flex direction={"column"} gap={rem(10)}> gap={rem(10)}>
<Flex gap={rem(10)}> <Flex gap={rem(10)}>
<Button <Button
onClick={() => onAddClick()} onClick={() => onAddClick()}
@@ -119,25 +118,30 @@ const PrintBarcodeModal = ({
disabled={!barcode} disabled={!barcode}
onClick={async () => { onClick={async () => {
if (!barcode) return; if (!barcode) return;
const response = await ProductService.getProductBarcodePdf({ const response =
requestBody: { await ProductService.getProductBarcodePdf({
productId, requestBody: {
barcode, productId,
quantity barcode,
} quantity,
}); },
const pdfBlob = base64ToBlob(response.base64String, response.mimeType); });
const pdfBlob = base64ToBlob(
response.base64String,
response.mimeType
);
const pdfUrl = URL.createObjectURL(pdfBlob); const pdfUrl = URL.createObjectURL(pdfBlob);
const pdfWindow = window.open(pdfUrl); const pdfWindow = window.open(pdfUrl);
if (!pdfWindow) { if (!pdfWindow) {
notifications.error({message: "Ошибка"}); notifications.error({ message: "Ошибка" });
return return;
} }
pdfWindow.onload = () => { pdfWindow.onload = () => {
pdfWindow.print(); pdfWindow.print();
} };
}} }}>
>Печать</Button> Печать
</Button>
</Flex> </Flex>
</Flex> </Flex>
<PrintBarcodeContainer <PrintBarcodeContainer
@@ -148,7 +152,7 @@ const PrintBarcodeModal = ({
additionalField={barcodeData?.additionalField} additionalField={barcodeData?.additionalField}
/> />
</> </>
) );
} };
export default PrintBarcodeModal; export default PrintBarcodeModal;

View File

@@ -1,29 +1,29 @@
import {GetServiceKitSchema} from "../../client"; import { GetServiceKitSchema } from "../../client";
import {ContextModalProps} from "@mantine/modals"; import { ContextModalProps } from "@mantine/modals";
import {useState} from "react"; import { useState } from "react";
import {Button, Flex, rem} from "@mantine/core"; import { Button, Flex, rem } from "@mantine/core";
import ServicesKitSelect from "../../components/Selects/ServicesKitSelect/ServicesKitSelect.tsx"; import ServicesKitSelect from "../../components/Selects/ServicesKitSelect/ServicesKitSelect.tsx";
import {notifications} from "../../shared/lib/notifications.ts"; import { notifications } from "../../shared/lib/notifications.ts";
type Props = { type Props = {
onSelect: (kit: GetServiceKitSchema) => void; onSelect: (kit: GetServiceKitSchema) => void;
serviceType: number; serviceType: number;
} };
const ServicesKitSelectModal = ({ const ServicesKitSelectModal = ({
context, context,
id, id,
innerProps innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const [kit, setKit] = useState<GetServiceKitSchema | undefined>(); const [kit, setKit] = useState<GetServiceKitSchema | undefined>();
const onSelectClick = () => { const onSelectClick = () => {
if (!kit) { if (!kit) {
notifications.error({message: "Выберите набор услуг"}); notifications.error({ message: "Выберите набор услуг" });
return; return;
} }
innerProps.onSelect(kit); innerProps.onSelect(kit);
context.closeContextModal(id); context.closeContextModal(id);
} };
return ( return (
<Flex <Flex
gap={rem(10)} gap={rem(10)}
@@ -35,26 +35,27 @@ const ServicesKitSelectModal = ({
placeholder={"Выберите набор услуг"} placeholder={"Выберите набор услуг"}
value={kit} value={kit}
onChange={setKit} onChange={setKit}
filterBy={item => item.serviceType === innerProps.serviceType} filterBy={item =>
item.serviceType === innerProps.serviceType
}
/> />
</Flex> </Flex>
<Flex justify={"flex-end"} gap={rem(10)}> <Flex
justify={"flex-end"}
gap={rem(10)}>
<Button <Button
variant={"subtle"} variant={"subtle"}
onClick={() => context.closeContextModal(id)} onClick={() => context.closeContextModal(id)}>
>
Отменить Отменить
</Button> </Button>
<Button <Button
onClick={onSelectClick} onClick={onSelectClick}
variant={"default"} variant={"default"}>
>
Добавить Добавить
</Button> </Button>
</Flex> </Flex>
</Flex> </Flex>
) );
} };
export default ServicesKitSelectModal; export default ServicesKitSelectModal;

View File

@@ -7,8 +7,7 @@ import AddDealServiceModal from "../pages/LeadsPage/modals/AddDealServiceModal.t
import AddDealProductModal from "../pages/LeadsPage/modals/AddDealProductModal.tsx"; import AddDealProductModal from "../pages/LeadsPage/modals/AddDealProductModal.tsx";
import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx"; import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx";
import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx"; import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
import BarcodeTemplateFormModal import BarcodeTemplateFormModal from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx"; import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx";
import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx"; import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx";
import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx"; import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx";

View File

@@ -1,75 +1,84 @@
import styles from './AdminPage.module.css'; import styles from "./AdminPage.module.css";
import {Tabs} from "@mantine/core"; import { Tabs } from "@mantine/core";
import PageBlock from "../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../components/PageBlock/PageBlock.tsx";
import {IconBriefcase, IconCalendarUser, IconCurrencyDollar, IconUser} from "@tabler/icons-react"; import {
IconBriefcase,
IconCalendarUser,
IconCurrencyDollar,
IconUser,
} from "@tabler/icons-react";
import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx"; import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx";
import UsersTab from "./tabs/Users/UsersTab.tsx"; import UsersTab from "./tabs/Users/UsersTab.tsx";
import {motion} from "framer-motion"; import { motion } from "framer-motion";
import FinancesTab from "./tabs/Finances/FinancesTab.tsx"; import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx"; import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
const AdminPage = () => { const AdminPage = () => {
return ( return (
<div className={styles['container']}> <div className={styles["container"]}>
<PageBlock fullHeight> <PageBlock fullHeight>
<Tabs variant={"outline"} keepMounted={false} defaultValue={"users"}> <Tabs
variant={"outline"}
keepMounted={false}
defaultValue={"users"}>
<Tabs.List> <Tabs.List>
<Tabs.Tab value={"users"} leftSection={<IconUser/>}> <Tabs.Tab
value={"users"}
leftSection={<IconUser />}>
Пользователи Пользователи
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value={"finances"} leftSection={<IconCurrencyDollar/>}> <Tabs.Tab
value={"finances"}
leftSection={<IconCurrencyDollar />}>
Финансы Финансы
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value={"rolesAndPositions"} leftSection={<IconBriefcase/>}> <Tabs.Tab
value={"rolesAndPositions"}
leftSection={<IconBriefcase />}>
Должности Должности
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value={"workTimeTable"} leftSection={<IconCalendarUser/>}> <Tabs.Tab
value={"workTimeTable"}
leftSection={<IconCalendarUser />}>
Рабочее время Рабочее время
</Tabs.Tab> </Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value={"users"}> <Tabs.Panel value={"users"}>
<motion.div <motion.div
initial={{opacity: 0}} initial={{ opacity: 0 }}
animate={{opacity: 1}} animate={{ opacity: 1 }}
transition={{duration: 0.2}} transition={{ duration: 0.2 }}>
> <UsersTab />
<UsersTab/>
</motion.div> </motion.div>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value={"rolesAndPositions"}> <Tabs.Panel value={"rolesAndPositions"}>
<motion.div <motion.div
initial={{opacity: 0}} initial={{ opacity: 0 }}
animate={{opacity: 1}} animate={{ opacity: 1 }}
transition={{duration: 0.2}} transition={{ duration: 0.2 }}>
> <RolesAndPositionsTab />
<RolesAndPositionsTab/>
</motion.div> </motion.div>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value={"finances"}> <Tabs.Panel value={"finances"}>
<motion.div <motion.div
initial={{opacity: 0}} initial={{ opacity: 0 }}
animate={{opacity: 1}} animate={{ opacity: 1 }}
transition={{duration: 0.2}} transition={{ duration: 0.2 }}>
> <FinancesTab />
<FinancesTab/>
</motion.div> </motion.div>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value={"workTimeTable"}> <Tabs.Panel value={"workTimeTable"}>
<motion.div <motion.div
initial={{opacity: 0}} initial={{ opacity: 0 }}
animate={{opacity: 1}} animate={{ opacity: 1 }}
transition={{duration: 0.2}} transition={{ duration: 0.2 }}>
> <WorkTimeTable />
<WorkTimeTable/>
</motion.div> </motion.div>
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>
</PageBlock> </PageBlock>
</div> </div>
) );
} };
export default AdminPage; export default AdminPage;

View File

@@ -1,16 +1,16 @@
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx"; import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import {PayRateSchema} from "../../../../client"; import { PayRateSchema } from "../../../../client";
import {FC} from "react"; import { FC } from "react";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx"; import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import {usePayRatesTableColumns} from "./columns.tsx"; import { usePayRatesTableColumns } from "./columns.tsx";
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core"; import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
import {modals} from "@mantine/modals"; import { modals } from "@mantine/modals";
import {IconEdit, IconTrash} from "@tabler/icons-react"; import { IconEdit, IconTrash } from "@tabler/icons-react";
import {MRT_TableOptions} from "mantine-react-table"; import { MRT_TableOptions } from "mantine-react-table";
type Props = CRUDTableProps<PayRateSchema>; type Props = CRUDTableProps<PayRateSchema>;
const PayRateTable: FC<Props> = ({items, onCreate, onChange, onDelete}) => { const PayRateTable: FC<Props> = ({ items, onCreate, onChange, onDelete }) => {
const columns = usePayRatesTableColumns(); const columns = usePayRatesTableColumns();
const onCreateClick = () => { const onCreateClick = () => {
@@ -19,76 +19,77 @@ const PayRateTable: FC<Props> = ({items, onCreate, onChange, onDelete}) => {
modal: "payRateForm", modal: "payRateForm",
withCloseButton: false, withCloseButton: false,
innerProps: { innerProps: {
onCreate: onCreate onCreate: onCreate,
} },
}) });
} };
const onEditClick = (payRate: PayRateSchema) => { const onEditClick = (payRate: PayRateSchema) => {
if (!onChange) return; if (!onChange) return;
modals.openContextModal({ modals.openContextModal({
modal: "payRateForm", modal: "payRateForm",
withCloseButton: false, withCloseButton: false,
innerProps: { innerProps: {
onChange: (event) => onChange({...event, id: payRate.id}), onChange: event => onChange({ ...event, id: payRate.id }),
element: payRate element: payRate,
} },
}) });
} };
const onDeleteClick = (payRate: PayRateSchema) => { const onDeleteClick = (payRate: PayRateSchema) => {
if (!onDelete) return; if (!onDelete) return;
modals.openConfirmModal({ modals.openConfirmModal({
title: 'Удаление тарифа', title: "Удаление тарифа",
children: ( children: (
<Text size="sm"> <Text size="sm">
Вы уверены что хотите удалить тариф {payRate.name} Вы уверены что хотите удалить тариф {payRate.name}
</Text> </Text>
), ),
labels: {confirm: 'Да', cancel: "Нет"}, labels: { confirm: "Да", cancel: "Нет" },
confirmProps: {color: 'red'}, confirmProps: { color: "red" },
onConfirm: () => onDelete(payRate) onConfirm: () => onDelete(payRate),
}); });
} };
return ( return (
<BaseTable <BaseTable
data={items} data={items}
columns={columns} columns={columns}
restProps={{ restProps={
enableSorting: false, {
enableColumnActions: false, enableSorting: false,
enableTopToolbar: true, enableColumnActions: false,
renderTopToolbar: ( enableTopToolbar: true,
<Flex p={rem(10)}> renderTopToolbar: (
<Flex p={rem(10)}>
<Button <Button
variant={"default"} variant={"default"}
onClick={() => onCreateClick()} onClick={() => onCreateClick()}>
> Создать тариф
Создать тариф </Button>
</Button> </Flex>
</Flex> ),
enableRowActions: true,
), renderRowActions: ({ row }) => (
enableRowActions: true, <Flex gap="md">
renderRowActions: ({row}) => ( <Tooltip label="Редактировать">
<Flex gap="md"> <ActionIcon
<Tooltip label="Редактировать"> onClick={() => onEditClick(row.original)}
<ActionIcon variant={"default"}>
onClick={() => onEditClick(row.original)} <IconEdit />
variant={"default"}> </ActionIcon>
<IconEdit/> </Tooltip>
</ActionIcon> <Tooltip label="Удалить">
</Tooltip> <ActionIcon
<Tooltip label="Удалить"> onClick={() => onDeleteClick(row.original)}
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}> variant={"default"}>
<IconTrash/> <IconTrash />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Flex> </Flex>
) ),
} as MRT_TableOptions<PayRateSchema>} } as MRT_TableOptions<PayRateSchema>
}
/> />
) );
} };
export default PayRateTable; export default PayRateTable;

View File

@@ -1,32 +1,36 @@
import {useMemo} from "react"; import { useMemo } from "react";
import {MRT_ColumnDef} from "mantine-react-table"; import { MRT_ColumnDef } from "mantine-react-table";
import {PayRateSchema} from "../../../../client"; import { PayRateSchema } from "../../../../client";
export const usePayRatesTableColumns = () => { export const usePayRatesTableColumns = () => {
return useMemo<MRT_ColumnDef<PayRateSchema>[]>(() => [ return useMemo<MRT_ColumnDef<PayRateSchema>[]>(
{ () => [
accessorKey: "name", {
header: "Название тарифа" accessorKey: "name",
}, header: "Название тарифа",
{ },
accessorKey: "payrollScheme.name", {
header: "Система оплаты" accessorKey: "payrollScheme.name",
}, header: "Система оплаты",
{ },
accessorKey: "baseRate", {
header: "Базовая ставка", accessorKey: "baseRate",
Cell: ({row}) => `${row.original.baseRate.toLocaleString("ru")}` header: "Базовая ставка",
Cell: ({ row }) =>
}, `${row.original.baseRate.toLocaleString("ru")}`,
{ },
accessorKey: "overtimeThreshold", {
header: "Порог сверхурочных" accessorKey: "overtimeThreshold",
}, header: "Порог сверхурочных",
{ },
accessorKey: "overtimeRate", {
header: "Сверхурочная ставка", accessorKey: "overtimeRate",
Cell: ({row}) => row.original.overtimeRate && `${row.original.overtimeRate.toLocaleString("ru")}` header: "Сверхурочная ставка",
Cell: ({ row }) =>
} row.original.overtimeRate &&
], []); `${row.original.overtimeRate.toLocaleString("ru")}`,
} },
],
[]
);
};

View File

@@ -1,14 +1,26 @@
import {FC, useEffect, useState} from "react"; import { FC, useEffect, useState } from "react";
import {ActionIcon, Button, Flex, Pagination, rem, Text, Tooltip} from "@mantine/core"; import {
import {usePaymentRecordsList} from "../../../../hooks/usePaymentRecordsList.tsx"; ActionIcon,
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx"; Button,
import {usePaymentRecordsTableColumns} from "./columns.tsx"; Flex,
import {modals} from "@mantine/modals"; Pagination,
import {PaymentRecordCreateSchema, PaymentRecordGetSchema, PayrollService} from "../../../../client"; rem,
import {notifications} from "../../../../shared/lib/notifications.ts"; Text,
import {IconTrash} from "@tabler/icons-react"; Tooltip,
import {MRT_TableOptions} from "mantine-react-table"; } from "@mantine/core";
import {formatDate} from "../../../../types/utils.ts"; import { usePaymentRecordsList } from "../../../../hooks/usePaymentRecordsList.tsx";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { usePaymentRecordsTableColumns } from "./columns.tsx";
import { modals } from "@mantine/modals";
import {
PaymentRecordCreateSchema,
PaymentRecordGetSchema,
PayrollService,
} from "../../../../client";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { IconTrash } from "@tabler/icons-react";
import { MRT_TableOptions } from "mantine-react-table";
import { formatDate } from "../../../../types/utils.ts";
const PaymentRecordsTable: FC = () => { const PaymentRecordsTable: FC = () => {
const [totalPages, setTotalPages] = useState(10); const [totalPages, setTotalPages] = useState(10);
@@ -16,8 +28,8 @@ const PaymentRecordsTable: FC = () => {
const { const {
pagination: paginationInfo, pagination: paginationInfo,
objects: paymentRecords, objects: paymentRecords,
refetch refetch,
} = usePaymentRecordsList({page: page, itemsPerPage: 10}); } = usePaymentRecordsList({ page: page, itemsPerPage: 10 });
useEffect(() => { useEffect(() => {
if (!paginationInfo) return; if (!paginationInfo) return;
setTotalPages(paginationInfo.totalPages); setTotalPages(paginationInfo.totalPages);
@@ -26,98 +38,99 @@ const PaymentRecordsTable: FC = () => {
const onCreate = (request: PaymentRecordCreateSchema) => { const onCreate = (request: PaymentRecordCreateSchema) => {
PayrollService.createPaymentRecord({ PayrollService.createPaymentRecord({
requestBody: { requestBody: {
data: request data: request,
} },
}).then(async ({ok, message}) => { }).then(async ({ ok, message }) => {
notifications.guess(ok, {message}); notifications.guess(ok, { message });
if (!ok) return; if (!ok) return;
await refetch(); await refetch();
}) });
} };
const onCreateClick = () => { const onCreateClick = () => {
modals.openContextModal({ modals.openContextModal({
modal: "createPaymentRecord", modal: "createPaymentRecord",
title: "Создание начисления", title: "Создание начисления",
innerProps: { innerProps: {
onCreate: onCreate onCreate: onCreate,
}, },
}) });
} };
const onDelete = (record: PaymentRecordGetSchema) => { const onDelete = (record: PaymentRecordGetSchema) => {
PayrollService.deletePaymentRecord({ PayrollService.deletePaymentRecord({
requestBody: { requestBody: {
paymentRecordId: record.id paymentRecordId: record.id,
} },
}).then(async ({ok, message}) => { }).then(async ({ ok, message }) => {
notifications.guess(ok, {message}); notifications.guess(ok, { message });
if (!ok) return; if (!ok) return;
await refetch(); await refetch();
}) });
} };
const onDeleteClick = (record: PaymentRecordGetSchema) => { const onDeleteClick = (record: PaymentRecordGetSchema) => {
modals.openConfirmModal({ modals.openConfirmModal({
title: 'Удаление начисления', title: "Удаление начисления",
children: ( children: (
<Text size="sm"> <Text size="sm">
Вы уверены что хотите удалить начисление Вы уверены что хотите удалить начисление пользователю{" "}
пользователю {record.user.firstName} {record.user.secondName} от {formatDate(record.createdAt)} {record.user.firstName} {record.user.secondName} от{" "}
{formatDate(record.createdAt)}
</Text> </Text>
), ),
labels: {confirm: 'Да', cancel: "Нет"}, labels: { confirm: "Да", cancel: "Нет" },
confirmProps: {color: 'red'}, confirmProps: { color: "red" },
onConfirm: () => onDelete(record) onConfirm: () => onDelete(record),
}) });
} };
const columns = usePaymentRecordsTableColumns(); const columns = usePaymentRecordsTableColumns();
return ( return (
<Flex <Flex
direction={"column"} direction={"column"}
h={"100%"} h={"100%"}
gap={rem(10)} gap={rem(10)}>
>
<BaseTable <BaseTable
data={paymentRecords} data={paymentRecords}
columns={columns} columns={columns}
restProps={{ restProps={
enableSorting: false, {
enableColumnActions: false, enableSorting: false,
enableTopToolbar: true, enableColumnActions: false,
renderTopToolbar: ( enableTopToolbar: true,
<Flex p={rem(10)}> renderTopToolbar: (
<Flex p={rem(10)}>
<Button <Button
variant={"default"} variant={"default"}
onClick={() => onCreateClick()} onClick={() => onCreateClick()}>
> Создать начисление
Создать начисление </Button>
</Button> </Flex>
</Flex> ),
), enableRowActions: true,
enableRowActions: true, renderRowActions: ({ row }) => (
renderRowActions: ({row}) => ( <Flex gap="md">
<Flex gap="md"> <Tooltip label="Удалить">
<ActionIcon
<Tooltip label="Удалить"> onClick={() =>
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}> onDeleteClick(row.original)
<IconTrash/> }
</ActionIcon> variant={"default"}>
</Tooltip> <IconTrash />
</Flex> </ActionIcon>
) </Tooltip>
} as MRT_TableOptions<PaymentRecordGetSchema>} </Flex>
),
} as MRT_TableOptions<PaymentRecordGetSchema>
}
/> />
{totalPages > 1 && {totalPages > 1 && (
<Pagination <Pagination
style={{alignSelf: "flex-end"}} style={{ alignSelf: "flex-end" }}
withEdges withEdges
onChange={event => setPage(event)} onChange={event => setPage(event)}
value={page} value={page}
total={totalPages} total={totalPages}
/> />
} )}
</Flex> </Flex>
) );
} };
export default PaymentRecordsTable; export default PaymentRecordsTable;

View File

@@ -1,58 +1,79 @@
import {useMemo} from "react"; import { useMemo } from "react";
import {MRT_ColumnDef} from "mantine-react-table"; import { MRT_ColumnDef } from "mantine-react-table";
import {PaymentRecordGetSchema} from "../../../../client"; import { PaymentRecordGetSchema } from "../../../../client";
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts"; import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
import {getPluralForm} from "../../../../shared/lib/utils.ts"; import { getPluralForm } from "../../../../shared/lib/utils.ts";
import {formatDate} from "../../../../types/utils.ts"; import { formatDate } from "../../../../types/utils.ts";
import {isEqual} from "lodash"; import { isEqual } from "lodash";
export const usePaymentRecordsTableColumns = () => { export const usePaymentRecordsTableColumns = () => {
const getWorkUnitsText = (paymentRecord: PaymentRecordGetSchema) => { const getWorkUnitsText = (paymentRecord: PaymentRecordGetSchema) => {
const payrollScheme = paymentRecord.payrollScheme; const payrollScheme = paymentRecord.payrollScheme;
if (payrollScheme.key === PaySchemeType.HOURLY) { if (payrollScheme.key === PaySchemeType.HOURLY) {
return getPluralForm(paymentRecord.workUnits, "час", "часа", "часов") return getPluralForm(
} else if ( paymentRecord.workUnits,
payrollScheme.key === PaySchemeType.DAILY "час",
) { "часа",
return getPluralForm(paymentRecord.workUnits, "день", "дня", "дней") "часов"
} else if ( );
payrollScheme.key === PaySchemeType.MONTHLY } else if (payrollScheme.key === PaySchemeType.DAILY) {
) { return getPluralForm(
return getPluralForm(paymentRecord.workUnits, "месяц", "месяца", "месяцев"); paymentRecord.workUnits,
"день",
"дня",
"дней"
);
} else if (payrollScheme.key === PaySchemeType.MONTHLY) {
return getPluralForm(
paymentRecord.workUnits,
"месяц",
"месяца",
"месяцев"
);
} }
return ""; return "";
} };
const getDateRangesText = (paymentRecord: PaymentRecordGetSchema) => { const getDateRangesText = (paymentRecord: PaymentRecordGetSchema) => {
if (paymentRecord.endDate && !isEqual(paymentRecord.startDate, paymentRecord.endDate)) { if (
return `${formatDate(paymentRecord.startDate)} - ${formatDate(paymentRecord.endDate)}` paymentRecord.endDate &&
!isEqual(paymentRecord.startDate, paymentRecord.endDate)
) {
return `${formatDate(paymentRecord.startDate)} - ${formatDate(paymentRecord.endDate)}`;
} }
return `${formatDate(paymentRecord.startDate)}`; return `${formatDate(paymentRecord.startDate)}`;
} };
return useMemo<MRT_ColumnDef<PaymentRecordGetSchema>[]>(() => [ return useMemo<MRT_ColumnDef<PaymentRecordGetSchema>[]>(
{ () => [
header: "Дата начисления", {
Cell: ({row}) => new Date(row.original.createdAt).toLocaleString('ru-RU') header: "Дата начисления",
}, Cell: ({ row }) =>
{ new Date(row.original.createdAt).toLocaleString("ru-RU"),
header: "Получил начисление", },
Cell: ({row}) => `${row.original.user.firstName} ${row.original.user.secondName}` {
}, header: "Получил начисление",
{ Cell: ({ row }) =>
header: "Создал начисление", `${row.original.user.firstName} ${row.original.user.secondName}`,
Cell: ({row}) => `${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}` },
}, {
{ header: "Создал начисление",
header: "Количество", Cell: ({ row }) =>
Cell: ({row}) => `${row.original.workUnits} ${getWorkUnitsText(row.original)}` `${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}`,
}, },
{ {
header: "Сумма начисления", header: "Количество",
Cell: ({row}) => row.original.amount.toLocaleString("ru-RU") Cell: ({ row }) =>
}, `${row.original.workUnits} ${getWorkUnitsText(row.original)}`,
{ },
header: "Временной промежуток", {
Cell: ({row}) => getDateRangesText(row.original) header: "Сумма начисления",
} Cell: ({ row }) => row.original.amount.toLocaleString("ru-RU"),
], []) },
} {
header: "Временной промежуток",
Cell: ({ row }) => getDateRangesText(row.original),
},
],
[]
);
};

View File

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

View File

@@ -1,16 +1,16 @@
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx"; import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import {PositionSchema} from "../../../../client"; import { PositionSchema } from "../../../../client";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx"; import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import {usePositionsTableColumns} from "./columns.tsx"; import { usePositionsTableColumns } from "./columns.tsx";
import {FC} from "react"; import { FC } from "react";
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core"; import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
import {modals} from "@mantine/modals"; import { modals } from "@mantine/modals";
import {IconTrash} from "@tabler/icons-react"; import { IconTrash } from "@tabler/icons-react";
import {MRT_TableOptions} from "mantine-react-table"; import { MRT_TableOptions } from "mantine-react-table";
type Props = CRUDTableProps<PositionSchema>; type Props = CRUDTableProps<PositionSchema>;
const PositionsTable: FC<Props> = ({items, onCreate, onDelete}) => { const PositionsTable: FC<Props> = ({ items, onCreate, onDelete }) => {
const columns = usePositionsTableColumns(); const columns = usePositionsTableColumns();
const onCreateClick = () => { const onCreateClick = () => {
@@ -19,63 +19,60 @@ const PositionsTable: FC<Props> = ({items, onCreate, onDelete}) => {
modal: "positionForm", modal: "positionForm",
withCloseButton: false, withCloseButton: false,
innerProps: { innerProps: {
onCreate: onCreate onCreate: onCreate,
} },
}) });
} };
const onDeleteClick = (position: PositionSchema) => { const onDeleteClick = (position: PositionSchema) => {
if (!onDelete) return; if (!onDelete) return;
modals.openConfirmModal({ modals.openConfirmModal({
title: 'Удаление должности', title: "Удаление должности",
children: ( children: (
<Text size="sm"> <Text size="sm">
Вы уверены что хотите удалить должность {position.name} Вы уверены что хотите удалить должность {position.name}
</Text> </Text>
), ),
labels: {confirm: 'Да', cancel: "Нет"}, labels: { confirm: "Да", cancel: "Нет" },
confirmProps: {color: 'red'}, confirmProps: { color: "red" },
onConfirm: () => onDelete(position) onConfirm: () => onDelete(position),
}); });
} };
return ( return (
<BaseTable <BaseTable
data={items} data={items}
columns={columns} columns={columns}
restProps={{ restProps={
enableTopToolbar: true, {
enableSorting: false, enableTopToolbar: true,
enableColumnActions: false, enableSorting: false,
enableRowActions: true, enableColumnActions: false,
renderTopToolbar: ( enableRowActions: true,
<Flex p={rem(10)}> renderTopToolbar: (
<Flex p={rem(10)}>
<Button <Button
variant={"default"} variant={"default"}
onClick={() => onCreateClick()} onClick={() => onCreateClick()}>
> Создать должность
Создать должность </Button>
</Button> </Flex>
</Flex> ),
renderRowActions: ({ row }) => (
), <Flex gap="md">
renderRowActions: ({row}) => ( <Tooltip label="Удалить">
<Flex gap="md"> <ActionIcon
onClick={() => onDeleteClick(row.original)}
<Tooltip label="Удалить"> variant={"default"}>
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}> <IconTrash />
<IconTrash/> </ActionIcon>
</ActionIcon> </Tooltip>
</Tooltip> </Flex>
</Flex> ),
) } as MRT_TableOptions<PositionSchema>
} as MRT_TableOptions<PositionSchema>} }
/> />
);
};
) export default PositionsTable;
}
export default PositionsTable;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
import {PayrollService} from "../../../client"; import { PayrollService } from "../../../client";
import ObjectList from "../../../hooks/objectList.tsx"; import ObjectList from "../../../hooks/objectList.tsx";
const usePayRatesList = () => ObjectList({ const usePayRatesList = () =>
queryFn: PayrollService.getAllPayRates, ObjectList({
getObjectsFn: response => response.payRates, queryFn: PayrollService.getAllPayRates,
queryKey: "getAllPayRates" getObjectsFn: response => response.payRates,
}) queryKey: "getAllPayRates",
export default usePayRatesList; });
export default usePayRatesList;

View File

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

View File

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

View File

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

View File

@@ -1,41 +1,51 @@
import BaseFormModal, {CreateProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx"; import BaseFormModal, {
import {PaymentRecordCreateSchema} from "../../../../client"; CreateProps,
import {ContextModalProps} from "@mantine/modals"; } from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {useForm} from "@mantine/form"; import { PaymentRecordCreateSchema } from "../../../../client";
import {Flex, NumberInput, rem} from "@mantine/core"; import { ContextModalProps } from "@mantine/modals";
import {DatePickerInput, MonthPickerInput} from "@mantine/dates"; import { useForm } from "@mantine/form";
import {useEffect, useState} from "react"; import { Flex, NumberInput, rem } from "@mantine/core";
import { DatePickerInput, MonthPickerInput } from "@mantine/dates";
import { useEffect, useState } from "react";
import UserSelect from "../../../../components/Selects/UserSelect/UserSelect.tsx"; import UserSelect from "../../../../components/Selects/UserSelect/UserSelect.tsx";
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts"; import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
import {motion} from "framer-motion"; import { motion } from "framer-motion";
import {dateWithoutTimezone} from "../../../../shared/lib/date.ts"; import { dateWithoutTimezone } from "../../../../shared/lib/date.ts";
type Props = CreateProps<PaymentRecordCreateSchema>; type Props = CreateProps<PaymentRecordCreateSchema>;
const CreatePaymentRecordModal = ({ const CreatePaymentRecordModal = ({
context, context,
id, id,
innerProps innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const form = useForm<Partial<PaymentRecordCreateSchema>>({ const form = useForm<Partial<PaymentRecordCreateSchema>>({
validate: { validate: {
user: (user) => !user && "Необходимо выбрать сотрудника", user: user => !user && "Необходимо выбрать сотрудника",
startDate: (startDate) => !startDate && "Необходимо указать временной промежуток", startDate: startDate =>
workUnits: (workUnits) => !workUnits && "Укажите количество" !startDate && "Необходимо указать временной промежуток",
} workUnits: workUnits => !workUnits && "Укажите количество",
}) },
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([null, null]); });
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([
null,
null,
]);
useEffect(() => { useEffect(() => {
const setDates = (
const setDates = (start: string | undefined, end: string | undefined) => { start: string | undefined,
end: string | undefined
) => {
form.setFieldValue("startDate", start); form.setFieldValue("startDate", start);
form.setFieldValue("endDate", end); form.setFieldValue("endDate", end);
}; };
if (dateRange.every(dr => dr == null)) { if (dateRange.every(dr => dr == null)) {
setDates(undefined, undefined); setDates(undefined, undefined);
return return;
} else { } else {
const notNullValues = dateRange.filter((dr): dr is Date => dr !== null).map(dateWithoutTimezone); const notNullValues = dateRange
.filter((dr): dr is Date => dr !== null)
.map(dateWithoutTimezone);
const startDate = notNullValues[0]; const startDate = notNullValues[0];
const endDate = notNullValues[1] || startDate; const endDate = notNullValues[1] || startDate;
setDates(startDate, endDate); setDates(startDate, endDate);
@@ -43,14 +53,13 @@ const CreatePaymentRecordModal = ({
}, [dateRange]); }, [dateRange]);
const getDateRangeInput = () => { const getDateRangeInput = () => {
if (!form.values.user) return <></> if (!form.values.user) return <></>;
const payRate = form.values.user.payRate; const payRate = form.values.user.payRate;
if (!payRate) return <></>; if (!payRate) return <></>;
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY) if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
return ( return (
<MonthPickerInput <MonthPickerInput
error={form.getInputProps("startDate").error} error={form.getInputProps("startDate").error}
label={"Временной промежуток"} label={"Временной промежуток"}
placeholder={"Выберите временной промежуток"} placeholder={"Выберите временной промежуток"}
type={"range"} type={"range"}
@@ -58,73 +67,78 @@ const CreatePaymentRecordModal = ({
onChange={setDateRange} onChange={setDateRange}
allowSingleDateInRange allowSingleDateInRange
/> />
) );
return (<DatePickerInput return (
error={form.getInputProps("startDate").error} <DatePickerInput
label={"Временной промежуток"} error={form.getInputProps("startDate").error}
placeholder={"Выберите временной промежуток"} label={"Временной промежуток"}
type={"range"} placeholder={"Выберите временной промежуток"}
allowSingleDateInRange type={"range"}
value={dateRange} allowSingleDateInRange
onChange={setDateRange} value={dateRange}
/>); onChange={setDateRange}
} />
);
};
const getAmountLabel = () => { const getAmountLabel = () => {
const user = form.values.user; const user = form.values.user;
if (!user) return ""; if (!user) return "";
const payRate = user?.payRate; const payRate = user?.payRate;
if (!payRate) return ""; if (!payRate) return "";
if (payRate.payrollScheme.key == PaySchemeType.HOURLY) return "Количество часов"; if (payRate.payrollScheme.key == PaySchemeType.HOURLY)
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY) return "Количество месяцев"; return "Количество часов";
if (payRate.payrollScheme.key == PaySchemeType.DAILY) return "Количество дней"; if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
return "Количество месяцев";
if (payRate.payrollScheme.key == PaySchemeType.DAILY)
return "Количество дней";
return ""; return "";
} };
const getAmountPlaceholder = () => { const getAmountPlaceholder = () => {
return "Укажите " + getAmountLabel().toLowerCase(); return "Укажите " + getAmountLabel().toLowerCase();
} };
return (<BaseFormModal return (
form={form} <BaseFormModal
closeOnSubmit form={form}
onClose={() => context.closeContextModal(id)} closeOnSubmit
{...innerProps} onClose={() => context.closeContextModal(id)}
> {...innerProps}>
<BaseFormModal.Body> <BaseFormModal.Body>
<> <>
<Flex direction={"column"} gap={rem(10)}> <Flex
<UserSelect direction={"column"}
label={"Сотрудник"} gap={rem(10)}>
placeholder={"Выберите сотрудника"} <UserSelect
searchable label={"Сотрудник"}
filterBy={(user) => !!user.payRate} placeholder={"Выберите сотрудника"}
{...form.getInputProps("user")} searchable
/> filterBy={user => !!user.payRate}
{...form.getInputProps("user")}
/>
{form.values.user && {form.values.user && (
<> <>
<motion.div <motion.div
initial={{ opacity: 0 }}
initial={{opacity: 0}} animate={{ opacity: 1 }}
animate={{opacity: 1}} transition={{ duration: 0.3 }}>
transition={{duration: 0.3}} <Flex
> direction={"column"}
<Flex gap={rem(10)}>
direction={"column"} {getDateRangeInput()}
gap={rem(10)}> <NumberInput
label={getAmountLabel()}
{getDateRangeInput()} placeholder={getAmountPlaceholder()}
<NumberInput hideControls
label={getAmountLabel()} {...form.getInputProps("workUnits")}
placeholder={getAmountPlaceholder()} />
hideControls </Flex>
{...form.getInputProps("workUnits")} </motion.div>
/> </>
</Flex> )}
</motion.div> </Flex>
</> </>
} </BaseFormModal.Body>
</Flex> </BaseFormModal>
</> );
</BaseFormModal.Body> };
</BaseFormModal>) export default CreatePaymentRecordModal;
}
export default CreatePaymentRecordModal;

View File

@@ -1,41 +1,47 @@
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx"; import BaseFormModal, {
import {PayRateSchemaBase} from "../../../../client"; CreateEditFormProps,
import {ContextModalProps} from "@mantine/modals"; } from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {useForm} from "@mantine/form"; import { PayRateSchemaBase } from "../../../../client";
import {Fieldset, Flex, NumberInput, rem, TextInput} from "@mantine/core"; import { ContextModalProps } from "@mantine/modals";
import { useForm } from "@mantine/form";
import { Fieldset, Flex, NumberInput, rem, TextInput } from "@mantine/core";
import PayrollSchemeSelect from "../../../../components/Selects/PayrollSchemeSelect/PayrollSchemeSelect.tsx"; import PayrollSchemeSelect from "../../../../components/Selects/PayrollSchemeSelect/PayrollSchemeSelect.tsx";
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts"; import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
type Props = CreateEditFormProps<PayRateSchemaBase> type Props = CreateEditFormProps<PayRateSchemaBase>;
const PayRateFormModal = ({ const PayRateFormModal = ({
context, context,
id, id,
innerProps innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const isEditing = 'element' in innerProps; const isEditing = "element" in innerProps;
const initialValue: Partial<PayRateSchemaBase> = isEditing ? innerProps.element : {}; const initialValue: Partial<PayRateSchemaBase> = isEditing
? innerProps.element
: {};
const form = useForm<Partial<PayRateSchemaBase>>({ const form = useForm<Partial<PayRateSchemaBase>>({
initialValues: initialValue, initialValues: initialValue,
validate: { validate: {
name: (name) => !name && "Необходимо указать название тарифа", name: name => !name && "Необходимо указать название тарифа",
payrollScheme: (scheme) => !scheme && "Необходимо выбрать систему оплаты", payrollScheme: scheme =>
baseRate: (baseRate) => !baseRate && "Небходимо указать базовую ставку" !scheme && "Необходимо выбрать систему оплаты",
} baseRate: baseRate =>
!baseRate && "Небходимо указать базовую ставку",
},
}); });
return ( return (
<BaseFormModal <BaseFormModal
form={form} form={form}
closeOnSubmit closeOnSubmit
onClose={() => context.closeContextModal(id)} onClose={() => context.closeContextModal(id)}
{...innerProps} {...innerProps}>
>
<BaseFormModal.Body> <BaseFormModal.Body>
<> <>
<Fieldset legend={"Общие параметры"}> <Fieldset legend={"Общие параметры"}>
<Flex direction={"column"} gap={rem(10)}> <Flex
direction={"column"}
gap={rem(10)}>
<TextInput <TextInput
label={"Название"} label={"Название"}
placeholder={"Введите название тарифа"} placeholder={"Введите название тарифа"}
@@ -47,11 +53,11 @@ const PayRateFormModal = ({
{...form.getInputProps("payrollScheme")} {...form.getInputProps("payrollScheme")}
/> />
</Flex> </Flex>
</Fieldset> </Fieldset>
<Fieldset> <Fieldset>
<Flex direction={"column"} gap={rem(10)}> <Flex
direction={"column"}
gap={rem(10)}>
<NumberInput <NumberInput
allowNegative={false} allowNegative={false}
hideControls hideControls
@@ -61,38 +67,42 @@ const PayRateFormModal = ({
thousandSeparator={" "} thousandSeparator={" "}
suffix={"₽"} suffix={"₽"}
{...form.getInputProps("baseRate")} {...form.getInputProps("baseRate")}
/> />
{form.values.payrollScheme?.key === PaySchemeType.HOURLY && {form.values.payrollScheme?.key ===
PaySchemeType.HOURLY && (
<> <>
<NumberInput <NumberInput
allowNegative={false} allowNegative={false}
hideControls hideControls
allowDecimal={false} allowDecimal={false}
label={"Порог сверхурочных"} label={"Порог сверхурочных"}
placeholder={"Введите порог сверхурочных"} placeholder={
{...form.getInputProps("overtimeThreshold")} "Введите порог сверхурочных"
}
{...form.getInputProps(
"overtimeThreshold"
)}
/> />
<NumberInput <NumberInput
allowNegative={false} allowNegative={false}
hideControls hideControls
decimalScale={2} decimalScale={2}
label={"Сверхурочная ставка"} label={"Сверхурочная ставка"}
placeholder={"Выберите сверхурочную ставку"} placeholder={
"Выберите сверхурочную ставку"
}
thousandSeparator={" "} thousandSeparator={" "}
suffix={"₽"} suffix={"₽"}
{...form.getInputProps("overtimeRate")} {...form.getInputProps("overtimeRate")}
/> />
</> </>
} )}
</Flex> </Flex>
</Fieldset> </Fieldset>
</> </>
</BaseFormModal.Body> </BaseFormModal.Body>
</BaseFormModal> </BaseFormModal>
) );
} };
export default PayRateFormModal; export default PayRateFormModal;

View File

@@ -1,46 +1,65 @@
import {ContextModalProps} from "@mantine/modals"; import { ContextModalProps } from "@mantine/modals";
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx"; import BaseFormModal, {
import {UserSchema} from "../../../../client"; CreateEditFormProps,
import {useForm} from "@mantine/form"; } from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
import {Checkbox, Fieldset, Input, Stack, Textarea, TextInput} from "@mantine/core"; import { UserSchema } from "../../../../client";
import { useForm } from "@mantine/form";
import {
Checkbox,
Fieldset,
Input,
Stack,
Textarea,
TextInput,
} from "@mantine/core";
import RoleSelect from "../../components/RoleSelect/RoleSelect.tsx"; import RoleSelect from "../../components/RoleSelect/RoleSelect.tsx";
import PositionSelect from "../../components/PositionSelect/PositionSelect.tsx"; import PositionSelect from "../../components/PositionSelect/PositionSelect.tsx";
import {UserRoleEnum} from "../../../../shared/enums/UserRole.ts"; import { UserRoleEnum } from "../../../../shared/enums/UserRole.ts";
import {capitalize} from "lodash"; import { capitalize } from "lodash";
import {IMaskInput} from "react-imask"; import { IMaskInput } from "react-imask";
import phone from "phone"; import phone from "phone";
import PayRateSelect from "../../../../components/Selects/PayRateSelect/PayRateSelect.tsx"; import PayRateSelect from "../../../../components/Selects/PayRateSelect/PayRateSelect.tsx";
type Props = CreateEditFormProps<UserSchema>; type Props = CreateEditFormProps<UserSchema>;
const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => { const UserFormModal = ({
const isEditing = 'element' in innerProps; context,
const initialValues = isEditing ? innerProps.element : { id,
isAdmin: false, innerProps,
isBlocked: false, }: ContextModalProps<Props>) => {
isDeleted: false, const isEditing = "element" in innerProps;
comment: "", const initialValues = isEditing
roleKey: UserRoleEnum.USER ? innerProps.element
}; : {
isAdmin: false,
isBlocked: false,
isDeleted: false,
comment: "",
roleKey: UserRoleEnum.USER,
};
const form = useForm<Partial<UserSchema>>({ const form = useForm<Partial<UserSchema>>({
initialValues: initialValues, initialValues: initialValues,
validate: { validate: {
firstName: value => !value?.trim() && "Укажите имя пользователя", firstName: value => !value?.trim() && "Укажите имя пользователя",
secondName: value => !value?.trim() && "Укажите фамилию", secondName: value => !value?.trim() && "Укажите фамилию",
position: (value, values) => ((values.role?.key === UserRoleEnum.EMPLOYEE) && (!value)) && 'Необходимо указать должность сотрудника', position: (value, values) =>
phoneNumber: value => !phone(value || '', { values.role?.key === UserRoleEnum.EMPLOYEE &&
country: "", !value &&
strictDetection: false, "Необходимо указать должность сотрудника",
validateMobilePrefix: false phoneNumber: value =>
}).isValid && 'Неверно указан номер телефона', !phone(value || "", {
} country: "",
strictDetection: false,
validateMobilePrefix: false,
}).isValid && "Неверно указан номер телефона",
},
}); });
return (<BaseFormModal return (
<BaseFormModal
form={form} form={form}
closeOnSubmit closeOnSubmit
onClose={() => context.closeContextModal(id)} onClose={() => context.closeContextModal(id)}
{...innerProps} {...innerProps}>
>
<BaseFormModal.Body> <BaseFormModal.Body>
<> <>
<Fieldset legend={"Общая информация"}> <Fieldset legend={"Общая информация"}>
@@ -49,19 +68,33 @@ const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
label={"Имя"} label={"Имя"}
placeholder={"Введите имя пользователя"} placeholder={"Введите имя пользователя"}
{...form.getInputProps("firstName")} {...form.getInputProps("firstName")}
onChange={event => form.getInputProps('firstName').onChange(capitalize(event.target.value).trim())} onChange={event =>
form
.getInputProps("firstName")
.onChange(
capitalize(
event.target.value
).trim()
)
}
/> />
<TextInput <TextInput
{...form.getInputProps("secondName")} {...form.getInputProps("secondName")}
label={"Фамилия"} label={"Фамилия"}
placeholder={"Введите фамилию пользователя"} placeholder={"Введите фамилию пользователя"}
onChange={event => form.getInputProps('secondName').onChange(capitalize(event.target.value).trim())} onChange={event =>
form
.getInputProps("secondName")
.onChange(
capitalize(
event.target.value
).trim()
)
}
/> />
<Input.Wrapper <Input.Wrapper
label={"Номер телефона"} label={"Номер телефона"}
error={form.getInputProps("phoneNumber").error} error={form.getInputProps("phoneNumber").error}>
>
<Input <Input
component={IMaskInput} component={IMaskInput}
mask="+7 000 000-00-00" mask="+7 000 000-00-00"
@@ -76,46 +109,53 @@ const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
<RoleSelect <RoleSelect
label={"Роль пользователя"} label={"Роль пользователя"}
placeholder={"Выберите роль пользователя"} placeholder={"Выберите роль пользователя"}
{...form.getInputProps('role')} {...form.getInputProps("role")}
/> />
{form.values.role?.key === UserRoleEnum.EMPLOYEE && {form.values.role?.key ===
UserRoleEnum.EMPLOYEE && (
<> <>
<PositionSelect <PositionSelect
label={"Должность сотрудника"} label={"Должность сотрудника"}
placeholder={"Выберите должность сотрудника"} placeholder={
{...form.getInputProps('position')} "Выберите должность сотрудника"
}
{...form.getInputProps("position")}
/> />
<PayRateSelect <PayRateSelect
label={"Тариф"} label={"Тариф"}
placeholder={"Выберите тариф сотрудника"} placeholder={
"Выберите тариф сотрудника"
}
{...form.getInputProps("payRate")} {...form.getInputProps("payRate")}
/> />
</> </>
)}
}
</Stack> </Stack>
</Fieldset> </Fieldset>
<Fieldset legend={"Дополнительные параметры"}> <Fieldset legend={"Дополнительные параметры"}>
<Stack> <Stack>
<Checkbox <Checkbox
label={"Права администратора"} label={"Права администратора"}
{...form.getInputProps("isAdmin", {
{...form.getInputProps('isAdmin', {type: "checkbox"})} type: "checkbox",
})}
/> />
<Checkbox <Checkbox
label={"Заблокирован"} label={"Заблокирован"}
{...form.getInputProps('isBlocked', {type: "checkbox"})} {...form.getInputProps("isBlocked", {
type: "checkbox",
})}
/> />
<Textarea <Textarea
label={"Дополнительная информация"} label={"Дополнительная информация"}
{...form.getInputProps('comment')} {...form.getInputProps("comment")}
/> />
</Stack> </Stack>
</Fieldset> </Fieldset>
</> </>
</BaseFormModal.Body> </BaseFormModal.Body>
</BaseFormModal> </BaseFormModal>
) );
} };
export default UserFormModal; export default UserFormModal;

Some files were not shown because too many files have changed in this diff Show More