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,
"singleQuote": false,
"semi": true,
"quoteProps": "consistent",
"bracketSpacing": true,
"trailingComma": "es5",
"tabWidth": 4,
"bracketSameLine": true,
"arrowParens": "avoid"
}
"singleAttributePerLine": true,
"singleQuote": false,
"semi": true,
"quoteProps": "consistent",
"bracketSpacing": true,
"trailingComma": "es5",
"tabWidth": 4,
"bracketSameLine": true,
"arrowParens": "avoid"
}

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:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
// other rules...
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: ["./tsconfig.json", "./tsconfig.node.json"],
tsconfigRootDir: __dirname,
},
};
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

View File

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

View File

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

7894
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 useServicePriceCategoriesList from "../../../pages/ServicesPage/hooks/useServicePriceCategoriesList.tsx";
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">;
const ServicePriceCategorySelect = (props: Props) => {
const { objects } = useServicePriceCategoriesList();
@@ -14,4 +16,4 @@ const ServicePriceCategorySelect = (props: Props) => {
);
};
export default ServicePriceCategorySelect;
export default ServicePriceCategorySelect;

View File

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

View File

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

View File

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

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 {FC} from "react";
import {ShippingWarehouseSchema} from "../../../client";
import { FC } from "react";
import { ShippingWarehouseSchema } from "../../../client";
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, 'data'>;
const ShippingWarehouseAutocomplete: FC<Props> = (props) => {
const {shippingWarehouses} = useShippingWarehousesList();
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, "data">;
const ShippingWarehouseAutocomplete: FC<Props> = props => {
const { shippingWarehouses } = useShippingWarehousesList();
return (
<ObjectAutocomplete
{...props}
data={shippingWarehouses}
/>
)
}
);
};
export default ShippingWarehouseAutocomplete;
export default ShippingWarehouseAutocomplete;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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