Compare commits
135 Commits
a169600908
...
pragmatic-
| Author | SHA1 | Date | |
|---|---|---|---|
| 36c2a3a2af | |||
| fc176ec9e4 | |||
| 4a4b05769d | |||
| 2052737561 | |||
| a899177623 | |||
| 5e56daa765 | |||
| 92602549f8 | |||
| 6351642838 | |||
| 4db3c3a1bf | |||
| 5aa3b4d9e2 | |||
| 73e3fd4ba2 | |||
| 8af4fcce2f | |||
| 820d9b4d33 | |||
| 428a6aca82 | |||
| 7b0b3bc529 | |||
| b316cf4f7a | |||
| 665625557d | |||
| b35961329e | |||
| 0fcf086861 | |||
| d14920df7d | |||
| 50ade0e832 | |||
| e9bfd39ab4 | |||
| f641e9ef8c | |||
| 1a2895da59 | |||
| f3a0179467 | |||
| b51467cbf6 | |||
| 61f0a9069b | |||
| 47533ad7f5 | |||
| 14140826a7 | |||
| a83328492e | |||
| 41ff994ad1 | |||
| 6d6c430e88 | |||
| 6e445d5ebf | |||
| 30e0de5c5e | |||
| de82e639b2 | |||
| e7416155be | |||
| 05edac23f1 | |||
| 9ba22b9bdf | |||
| e049494fa5 | |||
| 79189bea9a | |||
| 053c1da5db | |||
| 0805a86335 | |||
| a95d05e28b | |||
| 6b4e2f193a | |||
| 4c5b9c7734 | |||
| 681c2c3bc8 | |||
| d927da46df | |||
| 553e76d610 | |||
| f2746b8b65 | |||
| c76304b7bc | |||
| c4381d86c7 | |||
| 0515dd8a49 | |||
| d76dc82cb8 | |||
| 67780b5251 | |||
| 0236379898 | |||
| d0c734d481 | |||
| 7694b4ae03 | |||
| a5afb03be6 | |||
| dce4dec2f5 | |||
| 0be2b8bb6b | |||
| 96ea0bba5e | |||
| 6d58add2e7 | |||
| b0e2703479 | |||
| 018c6a06ea | |||
| dcf069aa1b | |||
| 604238ca43 | |||
| b5934a7ed2 | |||
| d69dee7caa | |||
| 5f621c295b | |||
| 9d8ec496a1 | |||
| 492b7ac32e | |||
| dca7d5f6a5 | |||
| 72ed69db24 | |||
| a6d8948e9d | |||
| 48d539154c | |||
| ab7ef1e753 | |||
| 26c7209de0 | |||
| d0948fb583 | |||
| db5b886455 | |||
| b363554c46 | |||
| 1b97739063 | |||
| a0522357d4 | |||
| 9d3028e4c9 | |||
| 568bd4ad36 | |||
| 8b06d08664 | |||
| 50d4705c5e | |||
| 658d7a2a0e | |||
| 3dec614f2a | |||
| 9404091d69 | |||
| 19e5ef2a7e | |||
| 4323695069 | |||
| e9b8cdb010 | |||
| a280f7ad12 | |||
| e6001ed59e | |||
| 44766bb7aa | |||
| 4a758e4cf0 | |||
| 31bd888357 | |||
| 5b5c2fe230 | |||
| e0f86f2018 | |||
| 226e52a1c6 | |||
| cc5ccf86a4 | |||
| e5602551c5 | |||
| d5be9ce61a | |||
| 10f50ac254 | |||
| 6ad813ea1d | |||
| f2084ae3d4 | |||
| b105510c23 | |||
| b5753ed3a2 | |||
| cb67c913ad | |||
| f3df8840df | |||
| d5b6e28311 | |||
| e3acf3aa89 | |||
| 32ea2aa060 | |||
| 7dba5b5ed9 | |||
| de7e334453 | |||
| 179b89c786 | |||
| be034ebbd0 | |||
| d3d8c5117b | |||
| 0bb546940a | |||
| 83432b3f33 | |||
| 49b1a235be | |||
| 19a386319c | |||
| 3ccebeb123 | |||
| e5e87f775d | |||
| 85ed974f5e | |||
| 92efe3fb66 | |||
| c405c802aa | |||
| 4ff663536e | |||
| 2e9ed02722 | |||
| a4bcd62189 | |||
| 0a13070d9e | |||
| 219689b947 | |||
| 3ece4677fb | |||
| 3d213cb0d9 | |||
| 6d0c48be23 |
@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_API_URL=http://your.api/api
|
NEXT_PUBLIC_API_URL=http://test.crm.logidex.ru/api
|
||||||
|
|||||||
@ -11,9 +11,11 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
name: "zod",
|
name: "zod",
|
||||||
requests: true,
|
requests: true,
|
||||||
responses: true,
|
|
||||||
definitions: true,
|
definitions: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
dates: {
|
||||||
|
offset: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "@hey-api/sdk",
|
name: "@hey-api/sdk",
|
||||||
|
|||||||
30
package.json
30
package.json
@ -3,18 +3,27 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev --turbo",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client"
|
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client",
|
||||||
|
"generate-modules": "sudo npx tsc ./src/modules/modulesFileGen/modulesFileGen.ts && mv -f ./src/modules/modulesFileGen/modulesFileGen.js ./src/modules/modulesFileGen/modulesFileGen.cjs && sudo node ./src/modules/modulesFileGen/modulesFileGen.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atlaskit/avatar": "^25.4.2",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.7",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-live-region": "^1.3.1",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.7",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@mantine/carousel": "^8.2.4",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@mantine/core": "8.1.2",
|
"@mantine/core": "8.1.2",
|
||||||
|
"@mantine/dates": "^8.2.7",
|
||||||
|
"@mantine/dropzone": "^8.3.1",
|
||||||
"@mantine/form": "^8.1.3",
|
"@mantine/form": "^8.1.3",
|
||||||
"@mantine/hooks": "8.1.2",
|
"@mantine/hooks": "8.1.2",
|
||||||
"@mantine/modals": "^8.2.1",
|
"@mantine/modals": "^8.2.1",
|
||||||
@ -24,21 +33,28 @@
|
|||||||
"@tabler/icons-react": "^3.34.0",
|
"@tabler/icons-react": "^3.34.0",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
"axios": "^1.11.0",
|
"@types/react-dom": "19.1.2",
|
||||||
|
"axios": "1.12.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"embla-carousel": "^8.6.0",
|
"clsx": "^2.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
|
"dayjs": "^1.11.15",
|
||||||
"framer-motion": "^12.23.7",
|
"framer-motion": "^12.23.7",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"lexorank": "^1.0.5",
|
"lexorank": "^1.0.5",
|
||||||
"libphonenumber-js": "^1.12.10",
|
"libphonenumber-js": "^1.12.10",
|
||||||
"next": "15.3.3",
|
"mantine-datatable": "^8.2.0",
|
||||||
|
"next": "15.4.7",
|
||||||
|
"phone": "^3.1.67",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
|
"swiper": "^11.2.10",
|
||||||
"zod": "^4.0.14"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
15
src/app/actions/components/Action/Action.module.css
Normal file
15
src/app/actions/components/Action/Action.module.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.link {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--mantine-radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
color: var(--mantine-color-gray-7);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
color: var(--mantine-color-dark-0);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/app/actions/components/Action/Action.tsx
Normal file
36
src/app/actions/components/Action/Action.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button, Stack, Text } from "@mantine/core";
|
||||||
|
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
|
||||||
|
import LinkData from "@/types/LinkData";
|
||||||
|
import styles from "./Action.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
linkData: LinkData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Action: FC<Props> = ({ linkData }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={linkData.href}
|
||||||
|
className={styles.link}>
|
||||||
|
<Button
|
||||||
|
w={"100%"}
|
||||||
|
h={"100px"}
|
||||||
|
variant={"default"}>
|
||||||
|
<Stack
|
||||||
|
px={"xs"}
|
||||||
|
w={"100%"}
|
||||||
|
align={"center"}
|
||||||
|
gap={"xs"}>
|
||||||
|
<ThemeIcon size={"sm"}>
|
||||||
|
<linkData.icon />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text>{linkData.label}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Action;
|
||||||
48
src/app/actions/components/PageBody/PageBody.tsx
Normal file
48
src/app/actions/components/PageBody/PageBody.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { SimpleGrid, Stack } from "@mantine/core";
|
||||||
|
import Action from "@/app/actions/components/Action/Action";
|
||||||
|
import mobileButtonsData from "@/app/actions/data/mobileButtonsData";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
|
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||||
|
|
||||||
|
const PageBody = () => {
|
||||||
|
const { selectedProject, setSelectedProjectId, projects, modulesSet } =
|
||||||
|
useProjectsContext();
|
||||||
|
|
||||||
|
const filteredMobileButtonsData = useMemo(
|
||||||
|
() =>
|
||||||
|
mobileButtonsData.filter(
|
||||||
|
link => !link.moduleName || modulesSet.has(link.moduleName)
|
||||||
|
),
|
||||||
|
[modulesSet]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageBlock fullScreenMobile>
|
||||||
|
<Stack p={"xs"}>
|
||||||
|
<ProjectSelect
|
||||||
|
onChange={project =>
|
||||||
|
setSelectedProjectId(project?.id ?? null)
|
||||||
|
}
|
||||||
|
value={selectedProject}
|
||||||
|
data={projects}
|
||||||
|
/>
|
||||||
|
<SimpleGrid
|
||||||
|
type={"container"}
|
||||||
|
cols={2}>
|
||||||
|
{filteredMobileButtonsData.map((data, index) => (
|
||||||
|
<Action
|
||||||
|
linkData={data}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</PageBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageBody;
|
||||||
37
src/app/actions/data/mobileButtonsData.tsx
Normal file
37
src/app/actions/data/mobileButtonsData.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
IconBox,
|
||||||
|
IconColumns,
|
||||||
|
IconFileBarcode,
|
||||||
|
IconUsers,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { ModuleNames } from "@/modules/modules";
|
||||||
|
import LinkData from "@/types/LinkData";
|
||||||
|
|
||||||
|
const mobileButtonsData: LinkData[] = [
|
||||||
|
{
|
||||||
|
icon: IconUsers,
|
||||||
|
label: "Клиенты",
|
||||||
|
href: "/clients",
|
||||||
|
moduleName: ModuleNames.CLIENTS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: IconColumns,
|
||||||
|
label: "Услуги",
|
||||||
|
href: "/services",
|
||||||
|
moduleName: ModuleNames.FULFILLMENT_BASE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: IconBox,
|
||||||
|
label: "Товары",
|
||||||
|
href: "/products",
|
||||||
|
moduleName: ModuleNames.FULFILLMENT_BASE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: IconFileBarcode,
|
||||||
|
label: "Шаблоны штрихкодов",
|
||||||
|
href: "/barcode-templates",
|
||||||
|
moduleName: ModuleNames.FULFILLMENT_BASE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default mobileButtonsData;
|
||||||
19
src/app/actions/page.tsx
Normal file
19
src/app/actions/page.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { Center, Loader } from "@mantine/core";
|
||||||
|
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||||
|
import PageBody from "./components/PageBody/PageBody";
|
||||||
|
|
||||||
|
export default async function ActionsPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Center h="50vh">
|
||||||
|
<Loader size="lg" />
|
||||||
|
</Center>
|
||||||
|
}>
|
||||||
|
<PageContainer>
|
||||||
|
<PageBody />
|
||||||
|
</PageContainer>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCreateClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BarcodeTemplatesDesktopHeader: FC<Props> = ({ onCreateClick }) => {
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<InlineButton onClick={onCreateClick}>
|
||||||
|
Создать шаблон
|
||||||
|
</InlineButton>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BarcodeTemplatesDesktopHeader;
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCreateClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BarcodeTemplatesMobileHeader: FC<Props> = ({ onCreateClick }) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
px={"xs"}
|
||||||
|
pt={"xs"}>
|
||||||
|
<InlineButton
|
||||||
|
onClick={onCreateClick}
|
||||||
|
w={"100%"}>
|
||||||
|
Создать шаблон
|
||||||
|
</InlineButton>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BarcodeTemplatesMobileHeader;
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import useBarcodeTemplateAttributesList from "@/app/barcode-templates/hooks/useBarcodeTemplateAttributesList";
|
||||||
|
import ObjectMultiSelect, {
|
||||||
|
ObjectMultiSelectProps,
|
||||||
|
} from "@/components/selects/ObjectMultiSelect/ObjectMultiSelect";
|
||||||
|
import { BarcodeTemplateAttributeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = Omit<
|
||||||
|
ObjectMultiSelectProps<BarcodeTemplateAttributeSchema>,
|
||||||
|
"data" | "getLabelFn" | "getValueFn"
|
||||||
|
>;
|
||||||
|
|
||||||
|
const BarcodeTemplateAttributeMultiselect: FC<Props> = (props: Props) => {
|
||||||
|
const { barcodeTemplateAttributes } = useBarcodeTemplateAttributesList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectMultiSelect
|
||||||
|
data={barcodeTemplateAttributes}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BarcodeTemplateAttributeMultiselect;
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import useBarcodeTemplateSizesList from "@/app/barcode-templates/hooks/useBarcodeTemplateSizesList";
|
||||||
|
import ObjectSelect, {
|
||||||
|
ObjectSelectProps,
|
||||||
|
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||||
|
import { BarcodeTemplateSizeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = Omit<ObjectSelectProps<BarcodeTemplateSizeSchema>, "data">;
|
||||||
|
|
||||||
|
const BarcodeTemplateSizeSelect = (props: Props) => {
|
||||||
|
const { barcodeTemplateSizes } = useBarcodeTemplateSizesList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectSelect
|
||||||
|
data={barcodeTemplateSizes}
|
||||||
|
getLabelFn={size => `${size.name} (${size.width}x${size.height})`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default BarcodeTemplateSizeSelect;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { useBarcodeTemplatesTableColumns } from "@/app/barcode-templates/components/shared/BarcodeTemplatesTable/columns";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { BarcodeTemplateSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: BarcodeTemplateSchema[];
|
||||||
|
onDelete: (template: BarcodeTemplateSchema) => void;
|
||||||
|
onChange: (template: BarcodeTemplateSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BarcodeTemplatesTable: FC<Props> = ({ items, ...props }) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const columns = useBarcodeTemplatesTableColumns(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTable
|
||||||
|
striped
|
||||||
|
withTableBorder
|
||||||
|
records={items}
|
||||||
|
columns={columns}
|
||||||
|
groups={undefined}
|
||||||
|
verticalSpacing={"md"}
|
||||||
|
mx={isMobile ? "xs" : ""}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BarcodeTemplatesTable;
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||||
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
|
import { Center } from "@mantine/core";
|
||||||
|
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
||||||
|
import { BarcodeTemplateSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDelete: (template: BarcodeTemplateSchema) => void;
|
||||||
|
onChange: (template: BarcodeTemplateSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBarcodeTemplatesTableColumns = ({
|
||||||
|
onDelete,
|
||||||
|
onChange,
|
||||||
|
}: Props) => {
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
accessor: "actions",
|
||||||
|
title: <Center>Действия</Center>,
|
||||||
|
width: "0%",
|
||||||
|
render: template => (
|
||||||
|
<UpdateDeleteTableActions
|
||||||
|
onDelete={() => onDelete(template)}
|
||||||
|
onChange={() => onChange(template)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "name",
|
||||||
|
title: "Название",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "attributes",
|
||||||
|
title: "Атрибуты",
|
||||||
|
render: template => (
|
||||||
|
<>
|
||||||
|
{template.attributes
|
||||||
|
.map(attr => attr.name)
|
||||||
|
.join(", ")}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "size.name",
|
||||||
|
title: "Размер",
|
||||||
|
render: template => `${template.size.name} (${template.size.width}x${template.size.height})`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "isDefault",
|
||||||
|
title: "По умолчанию",
|
||||||
|
render: template =>
|
||||||
|
template.isDefault ? <IconCheck /> : <IconX />,
|
||||||
|
},
|
||||||
|
] as DataTableColumn<BarcodeTemplateSchema>[],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Stack } from "@mantine/core";
|
||||||
|
import BarcodeTemplatesDesktopHeader from "@/app/barcode-templates/components/desktop/BarcodeTemplatesDesktopHeader/BarcodeTemplatesDesktopHeader";
|
||||||
|
import BarcodeTemplatesMobileHeader from "@/app/barcode-templates/components/mobile/BarcodeTemplatesMobileHeader/BarcodeTemplatesMobileHeader";
|
||||||
|
import BarcodeTemplatesTable from "@/app/barcode-templates/components/shared/BarcodeTemplatesTable/BarcodeTemplatesTable";
|
||||||
|
import useBarcodeTemplateActions from "@/app/barcode-templates/hooks/useBarcodeTemplateActions";
|
||||||
|
import { useBarcodeTemplatesCrud } from "@/app/barcode-templates/hooks/useBarcodeTemplatesCrud";
|
||||||
|
import useBarcodeTemplatesList from "@/app/barcode-templates/hooks/useBarcodeTemplatesList";
|
||||||
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
const PageBody = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { barcodeTemplates, queryKey } = useBarcodeTemplatesList();
|
||||||
|
const barcodeTemplatesCrud = useBarcodeTemplatesCrud({ queryKey });
|
||||||
|
|
||||||
|
const { onCreate, onChange } = useBarcodeTemplateActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack h={"100%"}>
|
||||||
|
{!isMobile && (
|
||||||
|
<PageBlock>
|
||||||
|
<BarcodeTemplatesDesktopHeader onCreateClick={onCreate} />
|
||||||
|
</PageBlock>
|
||||||
|
)}
|
||||||
|
<PageBlock
|
||||||
|
style={{ flex: 1, minHeight: 0 }}
|
||||||
|
fullScreenMobile>
|
||||||
|
<Stack
|
||||||
|
gap={"xs"}
|
||||||
|
h={"100%"}>
|
||||||
|
{isMobile && (
|
||||||
|
<BarcodeTemplatesMobileHeader
|
||||||
|
onCreateClick={onCreate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1, overflow: "auto" }}>
|
||||||
|
<BarcodeTemplatesTable
|
||||||
|
items={barcodeTemplates}
|
||||||
|
onChange={onChange}
|
||||||
|
onDelete={barcodeTemplatesCrud.onDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</PageBlock>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageBody;
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import { useBarcodeTemplatesCrud } from "@/app/barcode-templates/hooks/useBarcodeTemplatesCrud";
|
||||||
|
import useBarcodeTemplatesList from "@/app/barcode-templates/hooks/useBarcodeTemplatesList";
|
||||||
|
import { BarcodeTemplateSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
const useBarcodeTemplateActions = () => {
|
||||||
|
const { queryKey } = useBarcodeTemplatesList();
|
||||||
|
const barcodeTemplatesCrud = useBarcodeTemplatesCrud({ queryKey });
|
||||||
|
|
||||||
|
const onChange = (template: BarcodeTemplateSchema) => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "barcodeTemplateEditorModal",
|
||||||
|
title: "Редактирование шаблона",
|
||||||
|
withCloseButton: false,
|
||||||
|
innerProps: {
|
||||||
|
onChange: updated =>
|
||||||
|
barcodeTemplatesCrud.onUpdate(template.id, updated),
|
||||||
|
entity: template,
|
||||||
|
isEditing: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreate = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "barcodeTemplateEditorModal",
|
||||||
|
title: "Создание шаблона",
|
||||||
|
withCloseButton: false,
|
||||||
|
innerProps: {
|
||||||
|
onCreate: barcodeTemplatesCrud.onCreate,
|
||||||
|
isEditing: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onChange,
|
||||||
|
onCreate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBarcodeTemplateActions;
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getBarcodeTemplateAttributesOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const useBarcodeTemplateAttributesList = () => {
|
||||||
|
const { isLoading, data, refetch } = useQuery(
|
||||||
|
getBarcodeTemplateAttributesOptions()
|
||||||
|
);
|
||||||
|
|
||||||
|
return { barcodeTemplateAttributes: data?.items ?? [], refetch, isLoading };
|
||||||
|
};
|
||||||
|
export default useBarcodeTemplateAttributesList;
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getBarcodeTemplateSizesOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const useBarcodeTemplateSizesList = () => {
|
||||||
|
const { isLoading, data, refetch } = useQuery(
|
||||||
|
getBarcodeTemplateSizesOptions()
|
||||||
|
);
|
||||||
|
|
||||||
|
return { barcodeTemplateSizes: data?.items ?? [], refetch, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBarcodeTemplateSizesList;
|
||||||
50
src/app/barcode-templates/hooks/useBarcodeTemplatesCrud.tsx
Normal file
50
src/app/barcode-templates/hooks/useBarcodeTemplatesCrud.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
||||||
|
import {
|
||||||
|
BarcodeTemplateSchema,
|
||||||
|
CreateBarcodeTemplateSchema,
|
||||||
|
UpdateBarcodeTemplateSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import {
|
||||||
|
createBarcodeTemplateMutation,
|
||||||
|
deleteBarcodeTemplateMutation,
|
||||||
|
updateBarcodeTemplateMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
type UseBarcodeTemplateOperationsProps = {
|
||||||
|
queryKey: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BarcodeTemplateCrud = {
|
||||||
|
onCreate: (template: CreateBarcodeTemplateSchema) => void;
|
||||||
|
onUpdate: (
|
||||||
|
templateId: number,
|
||||||
|
template: UpdateBarcodeTemplateSchema
|
||||||
|
) => void;
|
||||||
|
onDelete: (template: BarcodeTemplateSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBarcodeTemplatesCrud = ({
|
||||||
|
queryKey,
|
||||||
|
}: UseBarcodeTemplateOperationsProps): BarcodeTemplateCrud => {
|
||||||
|
return useCrudOperations<
|
||||||
|
BarcodeTemplateSchema,
|
||||||
|
UpdateBarcodeTemplateSchema,
|
||||||
|
CreateBarcodeTemplateSchema
|
||||||
|
>({
|
||||||
|
key: "getBarcodeTemplates",
|
||||||
|
queryKey,
|
||||||
|
mutations: {
|
||||||
|
create: createBarcodeTemplateMutation(),
|
||||||
|
update: updateBarcodeTemplateMutation(),
|
||||||
|
delete: deleteBarcodeTemplateMutation(),
|
||||||
|
},
|
||||||
|
getUpdateEntity: (old, update) => ({
|
||||||
|
...old,
|
||||||
|
name: update.name ?? old.name,
|
||||||
|
attributes: update.attributes ?? old.attributes,
|
||||||
|
size: update.size ?? old.size,
|
||||||
|
isDefault: update.isDefault ?? old.isDefault,
|
||||||
|
}),
|
||||||
|
getDeleteConfirmTitle: () => "Удаление шаблона штрихкода",
|
||||||
|
});
|
||||||
|
};
|
||||||
15
src/app/barcode-templates/hooks/useBarcodeTemplatesList.tsx
Normal file
15
src/app/barcode-templates/hooks/useBarcodeTemplatesList.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
getBarcodeTemplatesOptions,
|
||||||
|
getBarcodeTemplatesQueryKey,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const useBarcodeTemplatesList = () => {
|
||||||
|
const { isLoading, data, refetch } = useQuery(getBarcodeTemplatesOptions());
|
||||||
|
|
||||||
|
const queryKey = getBarcodeTemplatesQueryKey();
|
||||||
|
|
||||||
|
return { barcodeTemplates: data?.items ?? [], queryKey, refetch, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBarcodeTemplatesList;
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Checkbox, Flex, TextInput } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
|
import BarcodeTemplateAttributeMultiselect from "@/app/barcode-templates/components/shared/BarcodeTemplateAttributeMultiselect/BarcodeTemplateAttributeMultiselect";
|
||||||
|
import BarcodeTemplateSizeSelect from "@/app/barcode-templates/components/shared/BarcodeTemplateSizeSelect/BarcodeTemplateSizeSelect";
|
||||||
|
import {
|
||||||
|
BarcodeTemplateSchema,
|
||||||
|
CreateBarcodeTemplateSchema,
|
||||||
|
UpdateBarcodeTemplateSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import BaseFormModal, {
|
||||||
|
CreateEditFormProps,
|
||||||
|
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
||||||
|
|
||||||
|
type Props = CreateEditFormProps<
|
||||||
|
BarcodeTemplateSchema,
|
||||||
|
CreateBarcodeTemplateSchema,
|
||||||
|
UpdateBarcodeTemplateSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
const BarcodeTemplateEditorModal = ({
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<Props>) => {
|
||||||
|
const initialValues = innerProps.isEditing
|
||||||
|
? innerProps.entity
|
||||||
|
: ({
|
||||||
|
name: "",
|
||||||
|
isDefault: false,
|
||||||
|
attributes: [],
|
||||||
|
} as Partial<CreateBarcodeTemplateSchema>);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues,
|
||||||
|
validate: {
|
||||||
|
attributes: attributes =>
|
||||||
|
!attributes && "Необходимо добавить хотя бы один атрибут",
|
||||||
|
name: name =>
|
||||||
|
!name ||
|
||||||
|
(name.trim() === "" && "Необходимо ввести название шаблона"),
|
||||||
|
size: size => !size && "Необходимо выбрать размер шаблона",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseFormModal
|
||||||
|
{...innerProps}
|
||||||
|
closeOnSubmit
|
||||||
|
form={form}
|
||||||
|
onClose={() => context.closeContextModal(id)}>
|
||||||
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={"md"}>
|
||||||
|
<TextInput
|
||||||
|
label={"Название"}
|
||||||
|
placeholder={"Введите название шаблона"}
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
<BarcodeTemplateSizeSelect
|
||||||
|
label={"Размер"}
|
||||||
|
placeholder={"Выберите размер шаблона"}
|
||||||
|
{...form.getInputProps("size")}
|
||||||
|
/>
|
||||||
|
<BarcodeTemplateAttributeMultiselect
|
||||||
|
label={"Стандартные атрибуты"}
|
||||||
|
placeholder={
|
||||||
|
!form.values.attributes?.length
|
||||||
|
? "Выберите атрибуты"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
{...form.getInputProps("attributes")}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={"Использовать по умолчанию"}
|
||||||
|
{...form.getInputProps("isDefault", {
|
||||||
|
type: "checkbox",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</BaseFormModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BarcodeTemplateEditorModal;
|
||||||
19
src/app/barcode-templates/page.tsx
Normal file
19
src/app/barcode-templates/page.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { Center, Loader } from "@mantine/core";
|
||||||
|
import PageBody from "@/app/barcode-templates/components/shared/PageBody/PageBody";
|
||||||
|
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||||
|
|
||||||
|
export default async function BarcodeTemplatesPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Center h="50vh">
|
||||||
|
<Loader size="lg" />
|
||||||
|
</Center>
|
||||||
|
}>
|
||||||
|
<PageContainer>
|
||||||
|
<PageBody />
|
||||||
|
</PageContainer>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { Group, TextInput } from "@mantine/core";
|
||||||
|
import { useClientsContext } from "@/app/clients/contexts/ClientsContext";
|
||||||
|
import useClientsActions from "@/app/clients/hooks/utils/useClientsActions";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
|
||||||
|
const ClientDesktopHeader: FC = () => {
|
||||||
|
const { search, setSearch } = useClientsContext();
|
||||||
|
const { onCreateClick } = useClientsActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group gap={"xs"}>
|
||||||
|
<InlineButton onClick={onCreateClick}>Создать клиента</InlineButton>
|
||||||
|
<TextInput
|
||||||
|
placeholder={"Поиск"}
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientDesktopHeader;
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { Flex, TextInput } from "@mantine/core";
|
||||||
|
import { useClientsContext } from "@/app/clients/contexts/ClientsContext";
|
||||||
|
import useClientsActions from "@/app/clients/hooks/utils/useClientsActions";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
|
||||||
|
const ClientMobileHeader: FC = () => {
|
||||||
|
const { search, setSearch } = useClientsContext();
|
||||||
|
const { onCreateClick } = useClientsActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap={"xs"}
|
||||||
|
px={"xs"}
|
||||||
|
pt={"xs"}>
|
||||||
|
<InlineButton
|
||||||
|
w={"100%"}
|
||||||
|
onClick={onCreateClick}>
|
||||||
|
Создать клиента
|
||||||
|
</InlineButton>
|
||||||
|
<TextInput
|
||||||
|
w={"100%"}
|
||||||
|
placeholder={"Поиск"}
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientMobileHeader;
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { useClientsTableColumns } from "@/app/clients/components/shared/ClientsTable/columns";
|
||||||
|
import { useClientsContext } from "@/app/clients/contexts/ClientsContext";
|
||||||
|
import useClientsActions from "@/app/clients/hooks/utils/useClientsActions";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { ClientSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
const ClientsTable: FC = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { modulesSet } = useProjectsContext();
|
||||||
|
const { clientsCrud, clients } = useClientsContext();
|
||||||
|
const { onUpdateClick } = useClientsActions();
|
||||||
|
const { openDrawer } = useDrawersContext();
|
||||||
|
|
||||||
|
const onOpenMarketplacesList = (client: ClientSchema) => {
|
||||||
|
openDrawer({
|
||||||
|
key: "clientMarketplaceDrawer",
|
||||||
|
props: { client },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useClientsTableColumns({
|
||||||
|
onDelete: clientsCrud.onDelete,
|
||||||
|
onChange: onUpdateClick,
|
||||||
|
onOpenMarketplacesList,
|
||||||
|
modulesSet,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTable
|
||||||
|
withTableBorder
|
||||||
|
records={clients}
|
||||||
|
columns={columns}
|
||||||
|
verticalSpacing={"md"}
|
||||||
|
mx={isMobile ? "xs" : 0}
|
||||||
|
groups={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientsTable;
|
||||||
78
src/app/clients/components/shared/ClientsTable/columns.tsx
Normal file
78
src/app/clients/components/shared/ClientsTable/columns.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { IconBasket } from "@tabler/icons-react";
|
||||||
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
|
import { Center } from "@mantine/core";
|
||||||
|
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
||||||
|
import { ClientSchema } from "@/lib/client";
|
||||||
|
import { ModuleNames } from "@/modules/modules";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onChange: (client: ClientSchema) => void;
|
||||||
|
onDelete: (client: ClientSchema) => void;
|
||||||
|
onOpenMarketplacesList: (client: ClientSchema) => void;
|
||||||
|
modulesSet: Set<ModuleNames>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useClientsTableColumns = ({
|
||||||
|
onChange,
|
||||||
|
onDelete,
|
||||||
|
onOpenMarketplacesList,
|
||||||
|
modulesSet,
|
||||||
|
}: Props) => {
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
accessor: "actions",
|
||||||
|
title: <Center>Действия</Center>,
|
||||||
|
width: "0%",
|
||||||
|
render: client => (
|
||||||
|
<UpdateDeleteTableActions
|
||||||
|
onDelete={() => onDelete(client)}
|
||||||
|
onChange={() => onChange(client)}
|
||||||
|
otherActions={[
|
||||||
|
{
|
||||||
|
label: "Маркетплейсы",
|
||||||
|
icon: <IconBasket />,
|
||||||
|
onClick: () =>
|
||||||
|
onOpenMarketplacesList(client),
|
||||||
|
hidden: !modulesSet.has(
|
||||||
|
ModuleNames.FULFILLMENT_BASE
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "name",
|
||||||
|
title: "Имя",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "details.telegram",
|
||||||
|
title: "Телеграм",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "details.email",
|
||||||
|
title: "Почта",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "details.phoneNumber",
|
||||||
|
title: "Телефон",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "details.inn",
|
||||||
|
title: "ИНН",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "companyName",
|
||||||
|
title: "Название компании",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "comment",
|
||||||
|
title: "Комментарий",
|
||||||
|
},
|
||||||
|
] as DataTableColumn<ClientSchema>[],
|
||||||
|
[onChange, onDelete]
|
||||||
|
);
|
||||||
|
};
|
||||||
37
src/app/clients/components/shared/PageBody/PageBody.tsx
Normal file
37
src/app/clients/components/shared/PageBody/PageBody.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Stack } from "@mantine/core";
|
||||||
|
import ClientDesktopHeader from "@/app/clients/components/desktop/ClientDesktopHeader/ClientDesktopHeader";
|
||||||
|
import ClientsTable from "@/app/clients/components/shared/ClientsTable/ClientsTable";
|
||||||
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import ClientMobileHeader from "@/app/clients/components/mobile/ClientMobileHeader/ClientMobileHeader";
|
||||||
|
|
||||||
|
const PageBody: FC = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack h={"100%"}>
|
||||||
|
{!isMobile && (
|
||||||
|
<PageBlock>
|
||||||
|
<ClientDesktopHeader />
|
||||||
|
</PageBlock>
|
||||||
|
)}
|
||||||
|
<PageBlock
|
||||||
|
style={{ flex: 1, minHeight: 0 }}
|
||||||
|
fullScreenMobile>
|
||||||
|
<Stack
|
||||||
|
gap={"xs"}
|
||||||
|
h={"100%"}>
|
||||||
|
{isMobile && <ClientMobileHeader />}
|
||||||
|
<div style={{ flex: 1, overflow: "auto" }}>
|
||||||
|
<ClientsTable />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</PageBlock>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageBody;
|
||||||
39
src/app/clients/contexts/ClientsContext.tsx
Normal file
39
src/app/clients/contexts/ClientsContext.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import {
|
||||||
|
ClientsCrud,
|
||||||
|
useClientsCrud,
|
||||||
|
} from "@/app/clients/hooks/cruds/useClientsCrud";
|
||||||
|
import useClientsFilter from "@/app/clients/hooks/utils/useClientsFilter";
|
||||||
|
import { ClientSchema } from "@/lib/client";
|
||||||
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
|
import useClientsList from "../hooks/lists/useClientsList";
|
||||||
|
|
||||||
|
type ClientsContextState = {
|
||||||
|
clients: ClientSchema[];
|
||||||
|
refetchClients: () => void;
|
||||||
|
search: string;
|
||||||
|
setSearch: Dispatch<SetStateAction<string>>;
|
||||||
|
clientsCrud: ClientsCrud;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useClientsContextState = (): ClientsContextState => {
|
||||||
|
const clientsList = useClientsList();
|
||||||
|
|
||||||
|
const { filteredClients, search, setSearch } =
|
||||||
|
useClientsFilter(clientsList);
|
||||||
|
|
||||||
|
const clientsCrud = useClientsCrud(clientsList);
|
||||||
|
|
||||||
|
return {
|
||||||
|
clients: filteredClients,
|
||||||
|
refetchClients: clientsList.refetch,
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
clientsCrud,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const [ClientsContextProvider, useClientsContext] =
|
||||||
|
makeContext<ClientsContextState>(useClientsContextState, "Clients");
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Drawer } from "@mantine/core";
|
||||||
|
import DrawerBody from "@/app/clients/drawers/ClientMarketplacesDrawer/components/DrawerBody";
|
||||||
|
import { MarketplacesContextProvider } from "@/app/clients/drawers/ClientMarketplacesDrawer/contexts/MarketplacesContext";
|
||||||
|
import { DrawerProps } from "@/drawers/types";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { ClientSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
client: ClientSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClientMarketplaceDrawer: FC<DrawerProps<Props>> = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
props,
|
||||||
|
}) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
size={isMobile ? "100%" : "40%"}
|
||||||
|
position={"right"}
|
||||||
|
onClose={onClose}
|
||||||
|
removeScrollProps={{ allowPinchZoom: true }}
|
||||||
|
withCloseButton={isMobile}
|
||||||
|
opened={opened}
|
||||||
|
trapFocus={false}
|
||||||
|
title={isMobile ? "Маркетплейсы" : ""}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: isMobile ? "var(--mobile-page-height)" : "100%",
|
||||||
|
padding: isMobile ? 0 : "var(--mantine-spacing-xs)",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
paddingBlock: 0,
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
<MarketplacesContextProvider client={props.client}>
|
||||||
|
<DrawerBody />
|
||||||
|
</MarketplacesContextProvider>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientMarketplaceDrawer;
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
ComboboxItem,
|
||||||
|
ComboboxLikeRenderOptionInput,
|
||||||
|
Image,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import useBaseMarketplacesList from "@/app/clients/hooks/lists/useBaseMarketplacesList";
|
||||||
|
import ObjectSelect, {
|
||||||
|
ObjectSelectProps,
|
||||||
|
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||||
|
import { BaseMarketplaceSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<BaseMarketplaceSchema>,
|
||||||
|
"data" | "getValueFn" | "getLabelFn"
|
||||||
|
>;
|
||||||
|
|
||||||
|
const BaseMarketplaceSelect: FC<Props> = props => {
|
||||||
|
const { baseMarketplaces } = useBaseMarketplacesList();
|
||||||
|
|
||||||
|
const renderOption = (
|
||||||
|
baseMarketplace: ComboboxLikeRenderOptionInput<ComboboxItem>
|
||||||
|
) => (
|
||||||
|
<>
|
||||||
|
<ActionIcon
|
||||||
|
radius={"md"}
|
||||||
|
variant={"transparent"}>
|
||||||
|
<Image
|
||||||
|
src={
|
||||||
|
baseMarketplaces.find(
|
||||||
|
el =>
|
||||||
|
baseMarketplace.option.value ===
|
||||||
|
el.id.toString()
|
||||||
|
)?.iconUrl || ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ActionIcon>
|
||||||
|
{baseMarketplace.option.label}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectSelect
|
||||||
|
renderOption={renderOption}
|
||||||
|
getValueFn={baseMarketplace => baseMarketplace.id.toString()}
|
||||||
|
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||||
|
data={baseMarketplaces}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default BaseMarketplaceSelect;
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Flex } from "@mantine/core";
|
||||||
|
import MarketplacesHeader from "@/app/clients/drawers/ClientMarketplacesDrawer/components/MarketplacesHeader";
|
||||||
|
import MarketplacesTable from "@/app/clients/drawers/ClientMarketplacesDrawer/components/MarketplacesTable";
|
||||||
|
|
||||||
|
const DrawerBody = () => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap={"xs"}
|
||||||
|
h={"100%"}
|
||||||
|
direction={"column"}>
|
||||||
|
<MarketplacesHeader />
|
||||||
|
<MarketplacesTable />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrawerBody;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import BaseMarketplaceType from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/baseMarketplaceType";
|
||||||
|
import OzonInputs from "./components/OzonInputs";
|
||||||
|
import WildberriesInputs from "./components/WildberriesInputs";
|
||||||
|
import YandexMarketInputs from "./components/YandexMarketInputs";
|
||||||
|
import MpAuthDataInputProps from "./types/MpAuthDataInputProps";
|
||||||
|
|
||||||
|
const MarketplaceAuthDataInput: FC<MpAuthDataInputProps> = props => {
|
||||||
|
switch (props.baseMarketplace.id) {
|
||||||
|
case BaseMarketplaceType.WILDBERRIES:
|
||||||
|
return <WildberriesInputs {...props} />;
|
||||||
|
case BaseMarketplaceType.OZON:
|
||||||
|
return <OzonInputs {...props} />;
|
||||||
|
case BaseMarketplaceType.YANDEX_MARKET:
|
||||||
|
return <YandexMarketInputs {...props} />;
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketplaceAuthDataInput;
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { omit } from "lodash";
|
||||||
|
import { NumberInput, TextInput } from "@mantine/core";
|
||||||
|
import MpAuthDataInputProps from "../types/MpAuthDataInputProps";
|
||||||
|
|
||||||
|
const OzonInputs: FC<MpAuthDataInputProps> = props => {
|
||||||
|
const restProps = omit(props, ["baseMarketplace"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NumberInput
|
||||||
|
{...restProps}
|
||||||
|
label={"Client-Id"}
|
||||||
|
placeholder={"Введите Client-Id"}
|
||||||
|
value={props.value?.["Client-Id"] || undefined}
|
||||||
|
onChange={value =>
|
||||||
|
props.onChange({
|
||||||
|
...props.value,
|
||||||
|
"Client-Id": value.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
{...restProps}
|
||||||
|
label={"Api-Key"}
|
||||||
|
placeholder={"Введите Api-Key"}
|
||||||
|
value={props.value?.["Api-Key"] || ""}
|
||||||
|
onChange={value =>
|
||||||
|
props.onChange({
|
||||||
|
...props.value,
|
||||||
|
"Api-Key": value.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OzonInputs;
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { omit } from "lodash";
|
||||||
|
import { TextInput } from "@mantine/core";
|
||||||
|
import MpAuthDataInputProps from "../types/MpAuthDataInputProps";
|
||||||
|
|
||||||
|
const WildberriesInputs: FC<MpAuthDataInputProps> = props => {
|
||||||
|
const restProps = omit(props, ["baseMarketplace"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
{...restProps}
|
||||||
|
label={"Ключ авторизации"}
|
||||||
|
placeholder={"Введите ключ авторизации"}
|
||||||
|
value={props.value?.Authorization || ""}
|
||||||
|
onChange={value =>
|
||||||
|
props.onChange({
|
||||||
|
...props.value,
|
||||||
|
Authorization: value.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WildberriesInputs;
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { omit } from "lodash";
|
||||||
|
import { TextInput } from "@mantine/core";
|
||||||
|
import MpAuthDataInputProps from "../types/MpAuthDataInputProps";
|
||||||
|
|
||||||
|
const YandexMarketInputs: FC<MpAuthDataInputProps> = props => {
|
||||||
|
const restProps = omit(props, ["baseMarketplace"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
{...restProps}
|
||||||
|
label={"Api-Key"}
|
||||||
|
placeholder={"Введите Api-Key"}
|
||||||
|
value={props.value?.["Api-Key"] || ""}
|
||||||
|
onChange={value => {
|
||||||
|
props.onChange({
|
||||||
|
...props.value,
|
||||||
|
"Api-Key": value.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default YandexMarketInputs;
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import BaseFormInputProps from "@/utils/baseFormInputProps";
|
||||||
|
import { BaseMarketplaceSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type RestProps = {
|
||||||
|
baseMarketplace: BaseMarketplaceSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MarketplaceAuthData = Record<string, string>;
|
||||||
|
|
||||||
|
type MpAuthDataInputProps = BaseFormInputProps<MarketplaceAuthData> & RestProps;
|
||||||
|
|
||||||
|
export default MpAuthDataInputProps;
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import useMarketplacesActions from "@/app/clients/drawers/ClientMarketplacesDrawer/hooks/useMarketplaceActions";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
const MarketplacesHeader = () => {
|
||||||
|
const { onCreateClick } = useMarketplacesActions();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<InlineButton
|
||||||
|
onClick={onCreateClick}
|
||||||
|
mx={isMobile ? "xs" : ""}
|
||||||
|
w={isMobile ? "100%" : "auto"}>
|
||||||
|
<IconPlus />
|
||||||
|
Создать
|
||||||
|
</InlineButton>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketplacesHeader;
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { IconMoodSad } from "@tabler/icons-react";
|
||||||
|
import { Group, Text } from "@mantine/core";
|
||||||
|
import { useMarketplacesContext } from "@/app/clients/drawers/ClientMarketplacesDrawer/contexts/MarketplacesContext";
|
||||||
|
import useMarketplacesActions from "@/app/clients/drawers/ClientMarketplacesDrawer/hooks/useMarketplaceActions";
|
||||||
|
import { useMarketplacesTableColumns } from "@/app/clients/drawers/ClientMarketplacesDrawer/hooks/useMarketplacesTableColumns";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
const MarketplacesTable = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { onUpdateClick } = useMarketplacesActions();
|
||||||
|
const { marketplaces, marketplacesCrud } = useMarketplacesContext();
|
||||||
|
|
||||||
|
const columns = useMarketplacesTableColumns({
|
||||||
|
onChange: onUpdateClick,
|
||||||
|
onDelete: marketplacesCrud.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTable
|
||||||
|
withTableBorder
|
||||||
|
records={marketplaces}
|
||||||
|
columns={columns}
|
||||||
|
groups={undefined}
|
||||||
|
verticalSpacing={"md"}
|
||||||
|
mx={isMobile ? "xs" : ""}
|
||||||
|
styles={{
|
||||||
|
table: {
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
emptyState={
|
||||||
|
<Group mt={marketplaces.length === 0 ? "xl" : 0}>
|
||||||
|
<Text>Нет маркетплейсов</Text>
|
||||||
|
<IconMoodSad />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketplacesTable;
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MarketplacesCrud,
|
||||||
|
useMarketplacesCrud,
|
||||||
|
} from "@/app/clients/hooks/cruds/useMarketplacesCrud";
|
||||||
|
import useMarketplacesList from "@/app/clients/hooks/lists/useMarketplacesList";
|
||||||
|
import { ClientSchema, MarketplaceSchema } from "@/lib/client";
|
||||||
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
|
|
||||||
|
type MarketplacesContextState = {
|
||||||
|
client: ClientSchema;
|
||||||
|
marketplaces: MarketplaceSchema[];
|
||||||
|
refetchMarketplaces: () => void;
|
||||||
|
marketplacesCrud: MarketplacesCrud;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
client: ClientSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useMarketplacesContextState = ({
|
||||||
|
client,
|
||||||
|
}: Props): MarketplacesContextState => {
|
||||||
|
const marketplacesList = useMarketplacesList({ clientId: client.id });
|
||||||
|
|
||||||
|
const marketplacesCrud = useMarketplacesCrud(marketplacesList);
|
||||||
|
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
marketplaces: marketplacesList.marketplaces,
|
||||||
|
refetchMarketplaces: marketplacesList.refetch,
|
||||||
|
marketplacesCrud,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const [MarketplacesContextProvider, useMarketplacesContext] =
|
||||||
|
makeContext<MarketplacesContextState, Props>(
|
||||||
|
useMarketplacesContextState,
|
||||||
|
"Marketplaces"
|
||||||
|
);
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import { useMarketplacesContext } from "@/app/clients/drawers/ClientMarketplacesDrawer/contexts/MarketplacesContext";
|
||||||
|
import { MarketplaceSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
const useMarketplacesActions = () => {
|
||||||
|
const { marketplacesCrud, client } = useMarketplacesContext();
|
||||||
|
|
||||||
|
const onCreateClick = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "marketplaceEditorModal",
|
||||||
|
title: "Создание маркетплейса",
|
||||||
|
innerProps: {
|
||||||
|
onCreate: values =>
|
||||||
|
marketplacesCrud.onCreate({ ...values, client }),
|
||||||
|
isEditing: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateClick = (marketplace: MarketplaceSchema) => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "marketplaceEditorModal",
|
||||||
|
title: "Редактирование маркетплейса",
|
||||||
|
innerProps: {
|
||||||
|
onChange: updates =>
|
||||||
|
marketplacesCrud.onUpdate(marketplace.id, updates),
|
||||||
|
entity: marketplace,
|
||||||
|
isEditing: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onCreateClick,
|
||||||
|
onUpdateClick,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMarketplacesActions;
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
|
import { ActionIcon, Center, Flex, Image } from "@mantine/core";
|
||||||
|
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
||||||
|
import { MarketplaceSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDelete: (mp: MarketplaceSchema) => void;
|
||||||
|
onChange: (mp: MarketplaceSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMarketplacesTableColumns = ({ onDelete, onChange }: Props) => {
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
accessor: "actions",
|
||||||
|
title: <Center>Действия</Center>,
|
||||||
|
width: "0%",
|
||||||
|
render: mp => (
|
||||||
|
<UpdateDeleteTableActions
|
||||||
|
onDelete={() => onDelete(mp)}
|
||||||
|
onChange={() => onChange(mp)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Маркетплейс",
|
||||||
|
accessor: "baseMarketplace",
|
||||||
|
cellsStyle: () => ({}),
|
||||||
|
render: mp => (
|
||||||
|
<Flex key={`${mp.id}mp`}>
|
||||||
|
<ActionIcon variant={"transparent"}>
|
||||||
|
<Image
|
||||||
|
src={mp.baseMarketplace?.iconUrl || ""}
|
||||||
|
/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "name",
|
||||||
|
title: "Название",
|
||||||
|
},
|
||||||
|
] as DataTableColumn<MarketplaceSchema>[],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import ClientMarketplaceDrawer from "./ClientMarketplacesDrawer";
|
||||||
|
|
||||||
|
export default ClientMarketplaceDrawer;
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Stack, TextInput } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
|
import BaseMarketplaceSelect from "@/app/clients/drawers/ClientMarketplacesDrawer/components/BaseMarketplaceSelect";
|
||||||
|
import MarketplaceAuthDataInput from "@/app/clients/drawers/ClientMarketplacesDrawer/components/MarketplaceAuthDataInput/MarketplaceAuthDataInput";
|
||||||
|
import {
|
||||||
|
CreateMarketplaceSchema,
|
||||||
|
MarketplaceSchema,
|
||||||
|
UpdateMarketplaceSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import BaseFormModal, {
|
||||||
|
CreateEditFormProps,
|
||||||
|
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
||||||
|
|
||||||
|
type Props = CreateEditFormProps<
|
||||||
|
MarketplaceSchema,
|
||||||
|
CreateMarketplaceSchema,
|
||||||
|
UpdateMarketplaceSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
const MarketplaceEditorModal = ({
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<Props>) => {
|
||||||
|
const initialValues: UpdateMarketplaceSchema | CreateMarketplaceSchema =
|
||||||
|
innerProps.isEditing
|
||||||
|
? innerProps.entity
|
||||||
|
: {
|
||||||
|
name: "",
|
||||||
|
authData: {
|
||||||
|
"Authorization": "",
|
||||||
|
"Client-Id": "",
|
||||||
|
"Api-Key": "",
|
||||||
|
"CampaignId": "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues,
|
||||||
|
validate: {
|
||||||
|
name: name =>
|
||||||
|
(!name || name.trim() === "") &&
|
||||||
|
"Необходимо ввести название маркетплейса",
|
||||||
|
baseMarketplace: baseMarketplace =>
|
||||||
|
!baseMarketplace && "Необходимо указать базовый маркетплейс",
|
||||||
|
authData: authData =>
|
||||||
|
!authData && "Необходимо указать данные авторизации",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseFormModal
|
||||||
|
{...innerProps}
|
||||||
|
closeOnSubmit
|
||||||
|
form={form}
|
||||||
|
onClose={() => context.closeContextModal(id)}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<TextInput
|
||||||
|
label={"Название"}
|
||||||
|
placeholder={"Введите название маркетплейса"}
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
<BaseMarketplaceSelect
|
||||||
|
label={"Базовый маркетплейс"}
|
||||||
|
placeholder={"Выберите базовый маркетплейс"}
|
||||||
|
{...form.getInputProps("baseMarketplace")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.values.baseMarketplace && (
|
||||||
|
<MarketplaceAuthDataInput
|
||||||
|
baseMarketplace={form.values.baseMarketplace}
|
||||||
|
value={form.values.authData as Record<string, string>}
|
||||||
|
onChange={value =>
|
||||||
|
form.setFieldValue("authData", value)
|
||||||
|
}
|
||||||
|
error={form.getInputProps("authData").error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</BaseFormModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketplaceEditorModal;
|
||||||
49
src/app/clients/hooks/cruds/useClientsCrud.tsx
Normal file
49
src/app/clients/hooks/cruds/useClientsCrud.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
||||||
|
import {
|
||||||
|
ClientSchema,
|
||||||
|
CreateClientSchema,
|
||||||
|
UpdateClientSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import {
|
||||||
|
createClientMutation,
|
||||||
|
deleteClientMutation,
|
||||||
|
updateClientMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
type UseClientsProps = {
|
||||||
|
queryKey: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClientsCrud = {
|
||||||
|
onCreate: (client: CreateClientSchema) => void;
|
||||||
|
onUpdate: (
|
||||||
|
clientId: number,
|
||||||
|
client: UpdateClientSchema,
|
||||||
|
onSuccess?: () => void
|
||||||
|
) => void;
|
||||||
|
onDelete: (client: ClientSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useClientsCrud = ({ queryKey }: UseClientsProps): ClientsCrud => {
|
||||||
|
return useCrudOperations<
|
||||||
|
ClientSchema,
|
||||||
|
UpdateClientSchema,
|
||||||
|
CreateClientSchema
|
||||||
|
>({
|
||||||
|
key: "getClients",
|
||||||
|
queryKey,
|
||||||
|
mutations: {
|
||||||
|
create: createClientMutation(),
|
||||||
|
update: updateClientMutation(),
|
||||||
|
delete: deleteClientMutation(),
|
||||||
|
},
|
||||||
|
getUpdateEntity: (old, update) => ({
|
||||||
|
...old,
|
||||||
|
details: update.details ?? old.details,
|
||||||
|
name: update.name ?? old.name,
|
||||||
|
companyName: update.companyName ?? old.companyName,
|
||||||
|
comment: update.comment ?? old.comment,
|
||||||
|
}),
|
||||||
|
getDeleteConfirmTitle: () => "Удаление клиента",
|
||||||
|
});
|
||||||
|
};
|
||||||
50
src/app/clients/hooks/cruds/useMarketplacesCrud.tsx
Normal file
50
src/app/clients/hooks/cruds/useMarketplacesCrud.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
||||||
|
import {
|
||||||
|
CreateMarketplaceSchema,
|
||||||
|
MarketplaceSchema,
|
||||||
|
UpdateMarketplaceSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import {
|
||||||
|
createMarketplaceMutation,
|
||||||
|
deleteMarketplaceMutation,
|
||||||
|
updateMarketplaceMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
type UseMarketplacesProps = {
|
||||||
|
queryKey: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MarketplacesCrud = {
|
||||||
|
onCreate: (mp: CreateMarketplaceSchema) => void;
|
||||||
|
onUpdate: (
|
||||||
|
mpId: number,
|
||||||
|
mp: UpdateMarketplaceSchema,
|
||||||
|
onSuccess?: () => void
|
||||||
|
) => void;
|
||||||
|
onDelete: (mp: MarketplaceSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMarketplacesCrud = ({
|
||||||
|
queryKey,
|
||||||
|
}: UseMarketplacesProps): MarketplacesCrud => {
|
||||||
|
return useCrudOperations<
|
||||||
|
MarketplaceSchema,
|
||||||
|
UpdateMarketplaceSchema,
|
||||||
|
CreateMarketplaceSchema
|
||||||
|
>({
|
||||||
|
key: "getMarketplaces",
|
||||||
|
queryKey,
|
||||||
|
mutations: {
|
||||||
|
create: createMarketplaceMutation(),
|
||||||
|
update: updateMarketplaceMutation(),
|
||||||
|
delete: deleteMarketplaceMutation(),
|
||||||
|
},
|
||||||
|
getUpdateEntity: (old, update) => ({
|
||||||
|
...old,
|
||||||
|
baseMarketplace: update.baseMarketplace ?? old.baseMarketplace,
|
||||||
|
name: update.name ?? old.name,
|
||||||
|
authData: update.authData ?? old.authData,
|
||||||
|
}),
|
||||||
|
getDeleteConfirmTitle: () => "Удаление маркетплейса",
|
||||||
|
});
|
||||||
|
};
|
||||||
15
src/app/clients/hooks/lists/useBaseMarketplacesList.tsx
Normal file
15
src/app/clients/hooks/lists/useBaseMarketplacesList.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
getBaseMarketplacesOptions,
|
||||||
|
getBaseMarketplacesQueryKey,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const useBaseMarketplacesList = () => {
|
||||||
|
const { data, refetch } = useQuery(getBaseMarketplacesOptions());
|
||||||
|
|
||||||
|
const queryKey = getBaseMarketplacesQueryKey();
|
||||||
|
|
||||||
|
return { baseMarketplaces: data?.items ?? [], refetch, queryKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBaseMarketplacesList;
|
||||||
25
src/app/clients/hooks/lists/useClientsList.tsx
Normal file
25
src/app/clients/hooks/lists/useClientsList.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
getClientsOptions,
|
||||||
|
getClientsQueryKey,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
includeDeleted?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useClientsList = (
|
||||||
|
{ includeDeleted = false }: Props = { includeDeleted: false }
|
||||||
|
) => {
|
||||||
|
const { data, refetch } = useQuery(
|
||||||
|
getClientsOptions({ query: { includeDeleted } })
|
||||||
|
);
|
||||||
|
const clients = useMemo(() => data?.items ?? [], [data]);
|
||||||
|
|
||||||
|
const queryKey = getClientsQueryKey();
|
||||||
|
|
||||||
|
return { clients, refetch, queryKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useClientsList;
|
||||||
21
src/app/clients/hooks/lists/useMarketplacesList.tsx
Normal file
21
src/app/clients/hooks/lists/useMarketplacesList.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
getMarketplacesOptions,
|
||||||
|
getMarketplacesQueryKey,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
clientId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useMarketplacesList = ({ clientId }: Props) => {
|
||||||
|
const { data, refetch } = useQuery(
|
||||||
|
getMarketplacesOptions({ path: { clientId } })
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryKey = getMarketplacesQueryKey({ path: { clientId } });
|
||||||
|
|
||||||
|
return { marketplaces: data?.items ?? [], refetch, queryKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMarketplacesList;
|
||||||
37
src/app/clients/hooks/utils/useClientsActions.tsx
Normal file
37
src/app/clients/hooks/utils/useClientsActions.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import { useClientsContext } from "@/app/clients/contexts/ClientsContext";
|
||||||
|
import { ClientSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
const useClientsActions = () => {
|
||||||
|
const { clientsCrud } = useClientsContext();
|
||||||
|
|
||||||
|
const onCreateClick = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "clientEditorModal",
|
||||||
|
title: "Создание клиента",
|
||||||
|
innerProps: {
|
||||||
|
onCreate: clientsCrud.onCreate,
|
||||||
|
isEditing: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateClick = (client: ClientSchema) => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "clientEditorModal",
|
||||||
|
title: "Редактирование клиента",
|
||||||
|
innerProps: {
|
||||||
|
onChange: updates => clientsCrud.onUpdate(client.id, updates),
|
||||||
|
entity: client,
|
||||||
|
isEditing: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onCreateClick,
|
||||||
|
onUpdateClick,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useClientsActions;
|
||||||
46
src/app/clients/hooks/utils/useClientsFilter.tsx
Normal file
46
src/app/clients/hooks/utils/useClientsFilter.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import { ClientSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
clients: ClientSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useClientsFilter = ({ clients }: Props) => {
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 400);
|
||||||
|
const [filteredClients, setFilteredClients] = useState<ClientSchema[]>([]);
|
||||||
|
|
||||||
|
const filterClients = () => {
|
||||||
|
if (debouncedSearch.length === 0) {
|
||||||
|
setFilteredClients(clients);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loweredSearch = debouncedSearch.toLowerCase();
|
||||||
|
const filtered = clients.filter(
|
||||||
|
client =>
|
||||||
|
client.name.toLowerCase().includes(loweredSearch) ||
|
||||||
|
client.details?.inn?.includes(loweredSearch) ||
|
||||||
|
client.details?.email?.toLowerCase().includes(loweredSearch) ||
|
||||||
|
client.details?.telegram
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(loweredSearch) ||
|
||||||
|
client.details?.phoneNumber?.includes(loweredSearch) ||
|
||||||
|
client.companyName.toLowerCase().includes(loweredSearch)
|
||||||
|
);
|
||||||
|
setFilteredClients(filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
filterClients();
|
||||||
|
}, [debouncedSearch, clients]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
filteredClients,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useClientsFilter;
|
||||||
105
src/app/clients/modals/ClientFormModal/ClientFormModal.tsx
Normal file
105
src/app/clients/modals/ClientFormModal/ClientFormModal.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Fieldset, Stack, Textarea, TextInput } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
|
import isValidInn from "@/app/clients/utils/isValidInn";
|
||||||
|
import {
|
||||||
|
ClientSchema,
|
||||||
|
CreateClientSchema,
|
||||||
|
UpdateClientSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import BaseFormModal, {
|
||||||
|
CreateEditFormProps,
|
||||||
|
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
||||||
|
|
||||||
|
type Props = CreateEditFormProps<
|
||||||
|
ClientSchema,
|
||||||
|
CreateClientSchema,
|
||||||
|
UpdateClientSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
const ClientEditorModal = ({
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<Props>) => {
|
||||||
|
const initialValues = innerProps.isEditing
|
||||||
|
? innerProps.entity
|
||||||
|
: ({
|
||||||
|
name: "",
|
||||||
|
companyName: "",
|
||||||
|
details: {
|
||||||
|
telegram: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
email: "",
|
||||||
|
inn: "",
|
||||||
|
},
|
||||||
|
comment: "",
|
||||||
|
} as CreateClientSchema);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues,
|
||||||
|
validate: {
|
||||||
|
name: name =>
|
||||||
|
(!name || name.trim() === "") &&
|
||||||
|
"Необходимо ввести название клиента",
|
||||||
|
details: {
|
||||||
|
inn: inn => inn.length > 0 && !isValidInn(inn) && "Некорректный ИНН",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseFormModal
|
||||||
|
{...innerProps}
|
||||||
|
closeOnSubmit
|
||||||
|
form={form}
|
||||||
|
onClose={() => context.closeContextModal(id)}>
|
||||||
|
<Fieldset legend={"Основная информация"}>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label={"Название клиента"}
|
||||||
|
placeholder={"Введите название клиента"}
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend={"Дополнительная информация"}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<TextInput
|
||||||
|
label={"Телеграм"}
|
||||||
|
placeholder={"Введите телеграм"}
|
||||||
|
{...form.getInputProps("details.telegram")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={"Номер телефона"}
|
||||||
|
placeholder={"Введите номер телефона"}
|
||||||
|
{...form.getInputProps("details.phoneNumber")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={"Почта"}
|
||||||
|
placeholder={"Введите почту"}
|
||||||
|
{...form.getInputProps("details.email")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={"ИНН"}
|
||||||
|
placeholder={"Введите ИНН"}
|
||||||
|
{...form.getInputProps("details.inn")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={"Название компании"}
|
||||||
|
placeholder={"Введите название компании"}
|
||||||
|
{...form.getInputProps("companyName")}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
label={"Комментарий"}
|
||||||
|
placeholder={"Введите комментарий"}
|
||||||
|
{...form.getInputProps("comment")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Fieldset>
|
||||||
|
</BaseFormModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientEditorModal;
|
||||||
22
src/app/clients/page.tsx
Normal file
22
src/app/clients/page.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { Center, Loader } from "@mantine/core";
|
||||||
|
import { ClientsContextProvider } from "@/app/clients/contexts/ClientsContext";
|
||||||
|
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||||
|
import PageBody from "@/app/clients/components/shared/PageBody/PageBody";
|
||||||
|
|
||||||
|
export default async function ClientsPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Center h="50vh">
|
||||||
|
<Loader size="lg" />
|
||||||
|
</Center>
|
||||||
|
}>
|
||||||
|
<PageContainer>
|
||||||
|
<ClientsContextProvider>
|
||||||
|
<PageBody />
|
||||||
|
</ClientsContextProvider>
|
||||||
|
</PageContainer>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/app/clients/utils/isValidInn.ts
Normal file
5
src/app/clients/utils/isValidInn.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const isValidInn = (inn: string | null | undefined) => {
|
||||||
|
return inn && inn.match(/^(\d{12}|\d{10})$/);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isValidInn;
|
||||||
@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
.board {
|
|
||||||
min-width: 130px;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import React, { FC, useState } from "react";
|
|
||||||
import { Box, Flex, Group, Text } from "@mantine/core";
|
|
||||||
import styles from "@/app/deals/components/desktop/Board/Board.module.css";
|
|
||||||
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
|
|
||||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
board: BoardSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Board: FC<Props> = ({ board }) => {
|
|
||||||
const { selectedBoard } = useBoardsContext();
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const { onUpdateBoard } = useBoardsContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex p={"lg"}>
|
|
||||||
<SmallPageBlock active={selectedBoard?.id === board.id}>
|
|
||||||
<Group
|
|
||||||
px={"md"}
|
|
||||||
py={"xs"}
|
|
||||||
bdrs={"lg"}
|
|
||||||
justify={"space-between"}
|
|
||||||
className={styles.board}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}>
|
|
||||||
<InPlaceInput
|
|
||||||
defaultValue={board.name}
|
|
||||||
onComplete={value =>
|
|
||||||
onUpdateBoard(board.id, { name: value })
|
|
||||||
}
|
|
||||||
inputStyles={{
|
|
||||||
input: {
|
|
||||||
height: 25,
|
|
||||||
minHeight: 25,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
getChildren={startEditing => (
|
|
||||||
<>
|
|
||||||
<Box>
|
|
||||||
<Text>{board.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<BoardMenu
|
|
||||||
isHovered={isHovered}
|
|
||||||
board={board}
|
|
||||||
startEditing={startEditing}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
modalTitle={"Редактирование доски"}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</SmallPageBlock>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Board;
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Group, ScrollArea } from "@mantine/core";
|
|
||||||
import Board from "@/app/deals/components/desktop/Board/Board";
|
|
||||||
import CreateBoardButton from "@/app/deals/components/desktop/CreateBoardButton/CreateBoardButton";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const Boards = () => {
|
|
||||||
const { boards, setSelectedBoard, onUpdateBoard } = useBoardsContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const renderBoard = (board: BoardSchema) => <Board board={board} />;
|
|
||||||
|
|
||||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
|
||||||
onUpdateBoard(itemId, { lexorank: newLexorank });
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectBoard = (board: BoardSchema) => {
|
|
||||||
setSelectedBoard(board);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea
|
|
||||||
offsetScrollbars={"x"}
|
|
||||||
scrollbars={"x"}
|
|
||||||
scrollbarSize={0}
|
|
||||||
w={"100%"}>
|
|
||||||
<Group
|
|
||||||
wrap={"nowrap"}
|
|
||||||
gap={0}>
|
|
||||||
<SortableDnd
|
|
||||||
initialItems={boards}
|
|
||||||
renderItem={renderBoard}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
onItemClick={selectBoard}
|
|
||||||
containerStyle={{ flexWrap: "nowrap" }}
|
|
||||||
dragHandleStyle={{ cursor: "pointer" }}
|
|
||||||
disabled={isMobile}
|
|
||||||
/>
|
|
||||||
<CreateBoardButton />
|
|
||||||
</Group>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Boards;
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { IconPlus } from "@tabler/icons-react";
|
|
||||||
import { Box } from "@mantine/core";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
|
|
||||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
|
||||||
|
|
||||||
const CreateBoardButton = () => {
|
|
||||||
const { onCreateBoard } = useBoardsContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SmallPageBlock style={{ cursor: "pointer" }}>
|
|
||||||
<InPlaceInput
|
|
||||||
placeholder={"Название доски"}
|
|
||||||
onComplete={onCreateBoard}
|
|
||||||
getChildren={startEditing => (
|
|
||||||
<Box
|
|
||||||
p={"sm"}
|
|
||||||
onClick={startEditing}>
|
|
||||||
<IconPlus />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
modalTitle={"Создание доски"}
|
|
||||||
inputStyles={{
|
|
||||||
wrapper: {
|
|
||||||
marginLeft: 15,
|
|
||||||
marginRight: 15,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SmallPageBlock>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateBoardButton;
|
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { IconFilter } from "@tabler/icons-react";
|
||||||
|
import { Button, Divider, Flex, Group, Indicator } from "@mantine/core";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import style from "@/app/deals/components/desktop/ViewSelectButton/ViewSelectButton.module.css";
|
||||||
|
import ViewSelector from "@/app/deals/components/desktop/ViewSelector/ViewSelector";
|
||||||
|
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
|
||||||
|
import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
export enum View {
|
||||||
|
BOARD = "board",
|
||||||
|
TABLE = "table",
|
||||||
|
SCHEDULE = "schedule"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
view: View;
|
||||||
|
setView: (view: View) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TopToolPanel: FC<Props> = ({ view, setView }) => {
|
||||||
|
const { dealsFiltersForm, isChangedFilters } = useDealsContext();
|
||||||
|
const { selectedProject } = useProjectsContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
if (isMobile) return;
|
||||||
|
|
||||||
|
const viewFiltersModalMap = {
|
||||||
|
table: "dealsTableFiltersModal",
|
||||||
|
board: "dealsBoardFiltersModal",
|
||||||
|
schedule: "dealsScheduleFiltersModal",
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFiltersClick = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: viewFiltersModalMap[view],
|
||||||
|
title: "Фильтры",
|
||||||
|
withCloseButton: true,
|
||||||
|
innerProps: {
|
||||||
|
value: dealsFiltersForm.values,
|
||||||
|
onChange: (values: DealsFiltersForm) =>
|
||||||
|
dealsFiltersForm.setValues(values),
|
||||||
|
project: selectedProject,
|
||||||
|
boardAndStatusEnabled: view === View.TABLE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<ViewSelector
|
||||||
|
value={view}
|
||||||
|
onChange={setView}
|
||||||
|
/>
|
||||||
|
<Divider orientation={"vertical"} />
|
||||||
|
<Flex
|
||||||
|
wrap={"nowrap"}
|
||||||
|
align={"center"}
|
||||||
|
gap={"sm"}>
|
||||||
|
<Indicator
|
||||||
|
zIndex={100}
|
||||||
|
disabled={!isChangedFilters}
|
||||||
|
offset={5}
|
||||||
|
size={8}>
|
||||||
|
<SmallPageBlock
|
||||||
|
style={{ borderRadius: "var(--mantine-radius-xl)" }}>
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
onClick={onFiltersClick}
|
||||||
|
radius="xl"
|
||||||
|
className={style.container}>
|
||||||
|
<IconFilter />
|
||||||
|
</Button>
|
||||||
|
</SmallPageBlock>
|
||||||
|
</Indicator>
|
||||||
|
</Flex>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopToolPanel;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--mantine-radius-xl);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--mantine-spacing-xs);
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, PropsWithChildren } from "react";
|
||||||
|
import { Button } from "@mantine/core";
|
||||||
|
import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
|
||||||
|
import style from "./ViewSelectButton.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selected: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ViewSelectButton: FC<PropsWithChildren<Props>> = ({
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<SmallPageBlock
|
||||||
|
active={selected}
|
||||||
|
style={{ borderRadius: "var(--mantine-radius-xl)" }}>
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
onClick={onSelect}
|
||||||
|
radius="xl"
|
||||||
|
className={style.container}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</SmallPageBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewSelectButton;
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import {
|
||||||
|
IconCalendarWeekFilled,
|
||||||
|
IconLayoutDashboard,
|
||||||
|
IconMenu2,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import { View } from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel";
|
||||||
|
import ViewSelectButton from "@/app/deals/components/desktop/ViewSelectButton/ViewSelectButton";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: View;
|
||||||
|
onChange: (view: View) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ViewSelector: FC<Props> = ({ value, onChange }) => {
|
||||||
|
const views = [
|
||||||
|
{
|
||||||
|
value: View.BOARD,
|
||||||
|
icon: <IconLayoutDashboard />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: View.TABLE,
|
||||||
|
icon: <IconMenu2 />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: View.SCHEDULE,
|
||||||
|
icon: <IconCalendarWeekFilled />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
{views.map(view => (
|
||||||
|
<ViewSelectButton
|
||||||
|
key={view.value}
|
||||||
|
selected={value === view.value}
|
||||||
|
onSelect={() => onChange(view.value)}>
|
||||||
|
{view.icon}
|
||||||
|
</ViewSelectButton>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewSelector;
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import React, { FC } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { Box, Text } from "@mantine/core";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
import styles from "./BoardMobile.module.css";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
board: BoardSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BoardMobile: FC<Props> = ({ board }) => {
|
|
||||||
const { selectedBoard } = useBoardsContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
px={"md"}
|
|
||||||
py={"xs"}
|
|
||||||
className={classNames(
|
|
||||||
styles["board-mobile"],
|
|
||||||
selectedBoard?.id === board.id &&
|
|
||||||
styles["board-mobile-selected"]
|
|
||||||
)}>
|
|
||||||
<Text style={{ textWrap: "nowrap" }}>{board.name}</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BoardMobile;
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Group, ScrollArea } from "@mantine/core";
|
|
||||||
import BoardMobile from "@/app/deals/components/mobile/BoardMobile/BoardMobile";
|
|
||||||
import CreateBoardButtonMobile from "@/app/deals/components/mobile/CreateBoardButtonMobile/CreateBoardButtonMobile";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const BoardsMobile = () => {
|
|
||||||
const { boards, setSelectedBoard, onUpdateBoard } = useBoardsContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const renderBoard = (board: BoardSchema) => <BoardMobile board={board} />;
|
|
||||||
|
|
||||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
|
||||||
onUpdateBoard(itemId, { lexorank: newLexorank });
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectBoard = (board: BoardSchema) => {
|
|
||||||
setSelectedBoard(board);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea
|
|
||||||
offsetScrollbars={"x"}
|
|
||||||
scrollbars={"x"}
|
|
||||||
scrollbarSize={0}
|
|
||||||
w={"100vw"}
|
|
||||||
mt={5}>
|
|
||||||
<Group
|
|
||||||
wrap={"nowrap"}
|
|
||||||
gap={0}>
|
|
||||||
<SortableDnd
|
|
||||||
initialItems={boards}
|
|
||||||
renderItem={renderBoard}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
onItemClick={selectBoard}
|
|
||||||
containerStyle={{ flexWrap: "nowrap" }}
|
|
||||||
dragHandleStyle={{ cursor: "pointer" }}
|
|
||||||
disabled={isMobile}
|
|
||||||
/>
|
|
||||||
<CreateBoardButtonMobile />
|
|
||||||
</Group>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BoardsMobile;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
.create-button {
|
|
||||||
padding: 10px 10px 11px 10px;
|
|
||||||
border-bottom: 2px solid gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
height: 46px;
|
|
||||||
border-bottom: 2px solid gray;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { IconPlus } from "@tabler/icons-react";
|
|
||||||
import { Box, Space } from "@mantine/core";
|
|
||||||
import styles from "@/app/deals/components/mobile/CreateBoardButtonMobile/CreateBoardButtonMobile.module.css";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
|
||||||
|
|
||||||
const CreateBoardButtonMobile = () => {
|
|
||||||
const { onCreateBoard } = useBoardsContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<InPlaceInput
|
|
||||||
placeholder={"Название доски"}
|
|
||||||
onComplete={onCreateBoard}
|
|
||||||
getChildren={startEditing => (
|
|
||||||
<Box
|
|
||||||
onClick={startEditing}
|
|
||||||
className={styles["create-button"]}>
|
|
||||||
<IconPlus />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
modalTitle={"Создание доски"}
|
|
||||||
inputStyles={{
|
|
||||||
wrapper: {
|
|
||||||
marginLeft: 15,
|
|
||||||
marginRight: 15,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Space className={styles.spacer} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateBoardButtonMobile;
|
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
|
||||||
|
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||||
|
import Boards from "@/app/deals/components/shared/Boards/Boards";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
const MainBlockHeader = () => {
|
||||||
|
const { setSelectedProjectId, refetchProjects, selectedProject } =
|
||||||
|
useProjectsContext();
|
||||||
|
const { refetchBoards } = useBoardsContext();
|
||||||
|
const { openDrawer } = useDrawersContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const selectProjectId = async (projectId: number | null) => {
|
||||||
|
await refetchProjects();
|
||||||
|
setSelectedProjectId(projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openProjectsEditorDrawer = () => {
|
||||||
|
openDrawer({
|
||||||
|
key: "projectsMobileEditorDrawer",
|
||||||
|
props: {
|
||||||
|
onSelect: project => selectProjectId(project?.id ?? null),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openBoardsEditorDrawer = () => {
|
||||||
|
if (!selectedProject) return;
|
||||||
|
openDrawer({
|
||||||
|
key: "boardsMobileEditorDrawer",
|
||||||
|
props: {
|
||||||
|
project: selectedProject,
|
||||||
|
},
|
||||||
|
onClose: refetchBoards,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
gap={0}
|
||||||
|
w={"100%"}>
|
||||||
|
{isMobile && (
|
||||||
|
<Group justify={"space-between"}>
|
||||||
|
<Box
|
||||||
|
p={"md"}
|
||||||
|
onClick={openProjectsEditorDrawer}>
|
||||||
|
<IconChevronLeft />
|
||||||
|
</Box>
|
||||||
|
<Text>{selectedProject?.name}</Text>
|
||||||
|
<Box
|
||||||
|
p={"md"}
|
||||||
|
onClick={openBoardsEditorDrawer}>
|
||||||
|
<IconSettings />
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
<Group
|
||||||
|
wrap={"nowrap"}
|
||||||
|
gap={0}
|
||||||
|
align={"end"}>
|
||||||
|
<Boards />
|
||||||
|
<Box
|
||||||
|
flex={1}
|
||||||
|
style={{ borderBottom: "2px solid gray" }}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainBlockHeader;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
.board-mobile {
|
.board {
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
@ -8,7 +8,7 @@
|
|||||||
border-bottom: 2px solid gray;
|
border-bottom: 2px solid gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-mobile-selected {
|
.board-selected {
|
||||||
border: 2px solid gray;
|
border: 2px solid gray;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
67
src/app/deals/components/shared/Board/Board.tsx
Normal file
67
src/app/deals/components/shared/Board/Board.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
|
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
import styles from "./Board.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
board: BoardSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Board: FC<Props> = ({ board }) => {
|
||||||
|
const { selectedBoard, boardsCrud } = useBoardsContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
px={"md"}
|
||||||
|
py={"xs"}
|
||||||
|
className={classNames(
|
||||||
|
styles.board,
|
||||||
|
selectedBoard?.id === board.id && styles["board-selected"]
|
||||||
|
)}
|
||||||
|
justify={"space-between"}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}>
|
||||||
|
<InPlaceInput
|
||||||
|
value={board.name}
|
||||||
|
onChange={value =>
|
||||||
|
boardsCrud.onUpdate(board.id, { name: value })
|
||||||
|
}
|
||||||
|
inputStyles={{
|
||||||
|
input: {
|
||||||
|
height: 24,
|
||||||
|
minHeight: 24,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
getChildren={startEditing => (
|
||||||
|
<Group wrap={"nowrap"}>
|
||||||
|
<Box>
|
||||||
|
<Text style={{ textWrap: "nowrap" }}>
|
||||||
|
{board.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
{!isMobile && (
|
||||||
|
<BoardMenu
|
||||||
|
isHovered={
|
||||||
|
selectedBoard?.id === board.id || isHovered
|
||||||
|
}
|
||||||
|
onDeleteBoard={boardsCrud.onDelete}
|
||||||
|
board={board}
|
||||||
|
startEditing={startEditing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
modalTitle={"Редактирование доски"}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Board;
|
||||||
@ -1,52 +1,48 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
import { Box, Menu } from "@mantine/core";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
|
||||||
|
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
|
||||||
import { BoardSchema } from "@/lib/client";
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
board: BoardSchema;
|
board: BoardSchema;
|
||||||
startEditing: () => void;
|
startEditing: () => void;
|
||||||
|
onDeleteBoard: (board: BoardSchema) => void;
|
||||||
isHovered?: boolean;
|
isHovered?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardMenu: FC<Props> = ({ board, startEditing, isHovered = true }) => {
|
const BoardMenu: FC<Props> = ({
|
||||||
const { selectedBoard, onDeleteBoard } = useBoardsContext();
|
board,
|
||||||
|
startEditing,
|
||||||
|
onDeleteBoard,
|
||||||
|
isHovered = true,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
opacity:
|
opacity: isHovered ? 1 : 0,
|
||||||
isHovered || selectedBoard?.id === board.id ? 1 : 0,
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={e => e.stopPropagation()}>
|
onClick={e => e.stopPropagation()}>
|
||||||
|
<ThemeIcon size={"sm"}>
|
||||||
<IconDotsVertical />
|
<IconDotsVertical />
|
||||||
|
</ThemeIcon>
|
||||||
</Box>
|
</Box>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<DropdownMenuItem
|
||||||
onClick={e => {
|
onClick={startEditing}
|
||||||
e.stopPropagation();
|
icon={<IconEdit />}
|
||||||
startEditing();
|
label={"Переименовать"}
|
||||||
}}>
|
/>
|
||||||
<Group wrap={"nowrap"}>
|
<DropdownMenuItem
|
||||||
<IconEdit />
|
onClick={() => onDeleteBoard(board)}
|
||||||
<Text>Переименовать</Text>
|
icon={<IconTrash />}
|
||||||
</Group>
|
label={"Удалить"}
|
||||||
</Menu.Item>
|
/>
|
||||||
<Menu.Item
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDeleteBoard(board);
|
|
||||||
}}>
|
|
||||||
<Group wrap={"nowrap"}>
|
|
||||||
<IconTrash />
|
|
||||||
<Text>Удалить</Text>
|
|
||||||
</Group>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
8
src/app/deals/components/shared/Boards/Boards.module.css
Normal file
8
src/app/deals/components/shared/Boards/Boards.module.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.container {
|
||||||
|
@media (min-width: 48em) {
|
||||||
|
max-width: calc(100vw - 210px - var(--mantine-spacing-md));
|
||||||
|
}
|
||||||
|
@media (max-width: 48em) {
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/app/deals/components/shared/Boards/Boards.tsx
Normal file
54
src/app/deals/components/shared/Boards/Boards.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Flex, ScrollArea } from "@mantine/core";
|
||||||
|
import Board from "@/app/deals/components/shared/Board/Board";
|
||||||
|
import CreateBoardButton from "@/app/deals/components/shared/CreateBoardButton/CreateBoardButton";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import SortableDnd from "@/components/dnd/SortableDnd";
|
||||||
|
import useHorizontalWheel from "@/hooks/utils/useHorizontalWheel";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
import styles from "./Boards.module.css";
|
||||||
|
|
||||||
|
const Boards = () => {
|
||||||
|
const { boards, setSelectedBoardId, boardsCrud } = useBoardsContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { ref, onWheel } = useHorizontalWheel<HTMLDivElement>();
|
||||||
|
|
||||||
|
const renderBoard = (board: BoardSchema) => <Board board={board} />;
|
||||||
|
|
||||||
|
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||||
|
boardsCrud.onUpdate(itemId, { lexorank: newLexorank });
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectBoard = (board: BoardSchema) => {
|
||||||
|
setSelectedBoardId(board.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align={"end"}
|
||||||
|
className={styles.container}>
|
||||||
|
<ScrollArea
|
||||||
|
viewportRef={ref}
|
||||||
|
onWheel={onWheel}
|
||||||
|
offsetScrollbars={"x"}
|
||||||
|
scrollbars={"x"}
|
||||||
|
scrollbarSize={0}>
|
||||||
|
<SortableDnd
|
||||||
|
initialItems={boards}
|
||||||
|
renderItem={renderBoard}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onItemClick={selectBoard}
|
||||||
|
containerStyle={{ flexWrap: "nowrap" }}
|
||||||
|
dragHandleStyle={{ cursor: "pointer" }}
|
||||||
|
disabled={isMobile}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
<CreateBoardButton />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Boards;
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
.create-button {
|
||||||
|
padding: 10px 10px 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
height: 45px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
|
import { Box, Flex, rem } from "@mantine/core";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
|
import styles from "./CreateBoardButton.module.css";
|
||||||
|
|
||||||
|
const CreateBoardButton = () => {
|
||||||
|
const { boardsCrud } = useBoardsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex style={{ borderBottom: "2px solid gray" }}>
|
||||||
|
<InPlaceInput
|
||||||
|
placeholder={"Название доски"}
|
||||||
|
onChange={name => boardsCrud.onCreate({ name })}
|
||||||
|
getChildren={startEditing => (
|
||||||
|
<Box
|
||||||
|
onClick={startEditing}
|
||||||
|
className={styles["create-button"]}>
|
||||||
|
<IconPlus />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
inputStyles={{
|
||||||
|
wrapper: {
|
||||||
|
marginRight: "var(--mantine-spacing-xs)",
|
||||||
|
paddingBlock: rem(3),
|
||||||
|
paddingLeft: "var(--mantine-spacing-xs)",
|
||||||
|
backgroundColor:
|
||||||
|
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))",
|
||||||
|
borderTopLeftRadius: "var(--mantine-spacing-xs)",
|
||||||
|
borderTopRightRadius: "var(--mantine-spacing-xs)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
modalTitle={"Создание доски"}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateBoardButton;
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
.create-button {
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: max-content;
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background-color: var(--color-light-white-blue);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-7);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
|
import { Card, Center, Group, Text, Transition } from "@mantine/core";
|
||||||
|
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||||
|
import CreateCardForm, { CreateDealForm } from "./components/CreateCardForm";
|
||||||
|
import styles from "./CreateDealButton.module.css";
|
||||||
|
|
||||||
|
const CreateCardButton = () => {
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
|
||||||
|
const { dealsCrud } = useDealsContext();
|
||||||
|
|
||||||
|
const onSubmit = (values: CreateDealForm) => {
|
||||||
|
dealsCrud.onCreate(values);
|
||||||
|
setIsCreating(prevState => !prevState);
|
||||||
|
setIsTransitionEnded(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles["create-button"]}
|
||||||
|
onClick={() => {
|
||||||
|
if (isCreating) return;
|
||||||
|
setIsCreating(prevState => !prevState);
|
||||||
|
setIsTransitionEnded(false);
|
||||||
|
}}>
|
||||||
|
{!isCreating && isTransitionEnded && (
|
||||||
|
<Center>
|
||||||
|
<Group gap={"xs"}>
|
||||||
|
<IconPlus />
|
||||||
|
<Text>Добавить</Text>
|
||||||
|
</Group>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<Transition
|
||||||
|
mounted={isCreating}
|
||||||
|
transition={"scale-y"}
|
||||||
|
onExited={() => setIsTransitionEnded(true)}>
|
||||||
|
{styles => (
|
||||||
|
<div style={styles}>
|
||||||
|
<CreateCardForm
|
||||||
|
onCancel={() => setIsCreating(false)}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default CreateCardButton;
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||||
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { ClientSchema } from "@/lib/client";
|
||||||
|
import ClientSelect from "@/modules/dealModularEditorTabs/Clients/components/ClientSelect";
|
||||||
|
import { ModuleNames } from "@/modules/modules";
|
||||||
|
|
||||||
|
export type CreateDealForm = {
|
||||||
|
name: string;
|
||||||
|
client?: ClientSchema;
|
||||||
|
clientId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSubmit: (values: CreateDealForm) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||||
|
const { modulesSet } = useProjectsContext();
|
||||||
|
|
||||||
|
const form = useForm<CreateDealForm>({
|
||||||
|
initialValues: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
name: value => !value && "Введите название",
|
||||||
|
client: client =>
|
||||||
|
modulesSet.has(ModuleNames.CLIENTS) &&
|
||||||
|
!client &&
|
||||||
|
"Выберите клиента",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(values => {
|
||||||
|
onSubmit(values);
|
||||||
|
form.reset();
|
||||||
|
})}>
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
placeholder={"Название"}
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
{modulesSet.has(ModuleNames.CLIENTS) && (
|
||||||
|
<ClientSelect
|
||||||
|
placeholder={"Клиент"}
|
||||||
|
{...form.getInputProps("client")}
|
||||||
|
onChange={client => {
|
||||||
|
form.setFieldValue("client", client);
|
||||||
|
form.setFieldValue("clientId", client?.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Group wrap={"nowrap"}>
|
||||||
|
<Button
|
||||||
|
variant={"default"}
|
||||||
|
w={"100%"}
|
||||||
|
onClick={onCancel}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={"default"}
|
||||||
|
w={"100%"}
|
||||||
|
type={"submit"}>
|
||||||
|
<IconCheck />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateCardForm;
|
||||||
@ -1,12 +1,23 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
flex-wrap: nowrap;
|
|
||||||
border-bottom: solid dodgerblue 3px;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
@media (max-width: 48em) {
|
@media (max-width: 48em) {
|
||||||
width: 80vw;
|
width: 80vw;
|
||||||
margin-right: 8vw;
|
height: 73.5vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-container {
|
||||||
|
border-radius: var(--mantine-spacing-md);
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background-color: var(--color-light-aqua);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,50 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { Box, Center, Group, Stack, Text } from "@mantine/core";
|
import { Box, Center, Group, Text } from "@mantine/core";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
import styles from "./CreateStatusButton.module.css";
|
import styles from "./CreateStatusButton.module.css";
|
||||||
|
|
||||||
const CreateStatusButton = () => {
|
const CreateStatusButton = () => {
|
||||||
const { onCreateStatus } = useStatusesContext();
|
const { statusesCrud } = useStatusesContext();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
|
||||||
<Box className={styles.container}>
|
<Box className={styles.container}>
|
||||||
|
<Box
|
||||||
|
p={isMobile ? "sm" : 0}
|
||||||
|
className={styles["inner-container"]}>
|
||||||
<InPlaceInput
|
<InPlaceInput
|
||||||
placeholder={"Название колонки"}
|
placeholder={"Название колонки"}
|
||||||
onComplete={onCreateStatus}
|
onChange={name => statusesCrud.onCreate({ name })}
|
||||||
getChildren={startEditing => (
|
getChildren={startEditing => (
|
||||||
<Center
|
<Center
|
||||||
p={"sm"}
|
p={"sm"}
|
||||||
onClick={() => startEditing()}>
|
onClick={() => startEditing()}>
|
||||||
<Group gap={"xs"} wrap={"nowrap"} align={"start"}>
|
<Group
|
||||||
|
gap={"xs"}
|
||||||
|
wrap={"nowrap"}
|
||||||
|
align={"center"}>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
{isMobile && (
|
{isMobile && <Text>Добавить</Text>}
|
||||||
<Text>Добавить</Text>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
modalTitle={"Создание колонки"}
|
modalTitle={"Создание колонки"}
|
||||||
inputStyles={{
|
inputStyles={{
|
||||||
wrapper: {
|
wrapper: {
|
||||||
padding: 4,
|
width: 250,
|
||||||
|
paddingInline: "var(--mantine-spacing-md)",
|
||||||
|
paddingBlock: "var(--mantine-spacing-xs)",
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
width: 250,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,46 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
@mixin light {
|
@mixin light {
|
||||||
background-color: var(--color-light-aqua);
|
background-color: var(--color-light-white-blue);
|
||||||
}
|
}
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: var(--mantine-color-dark-7);
|
background-color: var(--mantine-color-dark-7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deal-data {
|
||||||
|
padding: var(--mantine-spacing-xs);
|
||||||
|
gap: var(--mantine-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deal-id {
|
||||||
|
border-top-right-radius: var(--mantine-radius-md);
|
||||||
|
border-bottom-left-radius: var(--mantine-radius-md);
|
||||||
|
padding-inline: var(--mantine-spacing-xs);
|
||||||
|
@mixin light {
|
||||||
|
background-color: lightblue;
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--color-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-tag {
|
||||||
|
@mixin light {
|
||||||
|
background-color: lightblue;
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: darkslateblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-tag {
|
||||||
|
@mixin light {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { Card } from "@mantine/core";
|
import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core";
|
||||||
|
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
import { DealSchema } from "@/lib/client";
|
import { DealSchema } from "@/lib/client";
|
||||||
|
import { ModuleNames } from "@/modules/modules";
|
||||||
import styles from "./DealCard.module.css";
|
import styles from "./DealCard.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -7,7 +11,63 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DealCard = ({ deal }: Props) => {
|
const DealCard = ({ deal }: Props) => {
|
||||||
return <Card className={styles.container}>{deal.name}</Card>;
|
const { selectedProject, modulesSet } = useProjectsContext();
|
||||||
|
const { dealsCrud, refetchDeals } = useDealsContext();
|
||||||
|
const { openDrawer } = useDrawersContext();
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
openDrawer({
|
||||||
|
key: "dealEditorDrawer",
|
||||||
|
props: {
|
||||||
|
value: deal,
|
||||||
|
onChange: deal => dealsCrud.onUpdate(deal.id, deal),
|
||||||
|
onDelete: dealsCrud.onDelete,
|
||||||
|
project: selectedProject,
|
||||||
|
},
|
||||||
|
onClose: refetchDeals,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
onClick={onClick}
|
||||||
|
className={styles.container}>
|
||||||
|
<Group
|
||||||
|
justify={"space-between"}
|
||||||
|
wrap={"nowrap"}
|
||||||
|
pl={"xs"}
|
||||||
|
gap={"xs"}
|
||||||
|
align={"start"}>
|
||||||
|
<Text
|
||||||
|
c={"dodgerblue"}
|
||||||
|
mt={"xs"}>
|
||||||
|
{deal.name}
|
||||||
|
</Text>
|
||||||
|
<Box className={styles["deal-id"]}>
|
||||||
|
<Text style={{ textWrap: "nowrap" }}>ID: {deal.id}</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
<Stack className={styles["deal-data"]}>
|
||||||
|
<Stack gap={0}>
|
||||||
|
{modulesSet.has(ModuleNames.CLIENTS) && (
|
||||||
|
<Text>{deal.client?.name}</Text>
|
||||||
|
)}
|
||||||
|
{modulesSet.has(ModuleNames.FULFILLMENT_BASE) && (
|
||||||
|
<>
|
||||||
|
<Text key={"price"}>{deal.totalPrice} руб.</Text>
|
||||||
|
<Text key={"count"}>
|
||||||
|
{deal.productsQuantity} тов.
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<Group gap={"xs"}>
|
||||||
|
<Pill className={styles["first-tag"]}>Срочно</Pill>
|
||||||
|
<Pill className={styles["second-tag"]}>Бесплатно</Pill>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DealCard;
|
export default DealCard;
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
import React, { FC, useMemo } from "react";
|
|
||||||
import { Box } from "@mantine/core";
|
|
||||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
|
||||||
import SortableItem from "@/components/dnd/SortableItem";
|
|
||||||
import { DealSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
deal: DealSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DealContainer: FC<Props> = ({ deal }) => {
|
|
||||||
const dealBody = useMemo(() => <DealCard deal={deal} />, [deal]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<SortableItem
|
|
||||||
dragHandleStyle={{ cursor: "pointer" }}
|
|
||||||
id={deal.id}
|
|
||||||
renderItem={() => dealBody}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DealContainer;
|
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { IconGripHorizontal } from "@tabler/icons-react";
|
||||||
|
import { Flex, rem, TextInput, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||||
|
import FulfillmentGroupInfo from "@/app/deals/components/shared/DealGroupCard/components/FulfillmentGroupInfo";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
import { ModuleNames } from "@/modules/modules";
|
||||||
|
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
group: GroupWithDealsSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DealGroupCard: FC<Props> = ({ group }) => {
|
||||||
|
const theme = useMantineColorScheme();
|
||||||
|
const [name, setName] = useState<string>(group.name ?? "");
|
||||||
|
const [debouncedName] = useDebouncedValue(name, 200);
|
||||||
|
const { modulesSet } = useProjectsContext();
|
||||||
|
const isServicesAndProductsIncluded = modulesSet.has(
|
||||||
|
ModuleNames.FULFILLMENT_BASE
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateName = () => {
|
||||||
|
if (debouncedName === group.name) return;
|
||||||
|
CardGroupService.updateCardGroup({
|
||||||
|
requestBody: {
|
||||||
|
data: {
|
||||||
|
...group,
|
||||||
|
name: debouncedName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) return;
|
||||||
|
setName(group.name || "");
|
||||||
|
notifications.guess(response.ok, { message: response.message });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateName();
|
||||||
|
}, [debouncedName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
border: "dashed var(--item-border-size) var(--mantine-color-default-border)",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
}}
|
||||||
|
p={rem(5)}
|
||||||
|
py={"xs"}
|
||||||
|
bg={
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? "var(--mantine-color-dark-5)"
|
||||||
|
: "var(--mantine-color-gray-1)"
|
||||||
|
}
|
||||||
|
gap={"xs"}
|
||||||
|
direction={"column"}>
|
||||||
|
<Flex
|
||||||
|
justify={"space-between"}
|
||||||
|
align={"center"}
|
||||||
|
gap={"xs"}
|
||||||
|
px={"xs"}>
|
||||||
|
<TextInput
|
||||||
|
value={name}
|
||||||
|
onChange={event => setName(event.currentTarget.value)}
|
||||||
|
variant={"unstyled"}
|
||||||
|
/>
|
||||||
|
<IconGripHorizontal />
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={"xs"}>
|
||||||
|
{group.deals?.map(deal => (
|
||||||
|
<DealCard
|
||||||
|
key={deal.id}
|
||||||
|
deal={deal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
{isServicesAndProductsIncluded && (
|
||||||
|
<FulfillmentGroupInfo group={group} />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DealGroupCard;
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { Flex, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { FC, useMemo } from "react";
|
||||||
|
import { DealGroupSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
group: DealGroupSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FulfillmentGroupInfo: FC<Props> = ({ group }) => {
|
||||||
|
const theme = useMantineColorScheme();
|
||||||
|
|
||||||
|
const totalPrice = useMemo(
|
||||||
|
() =>
|
||||||
|
group.deals?.reduce((acc, deal) => acc + (deal.totalPrice ?? 0), 0),
|
||||||
|
[group.deals]
|
||||||
|
);
|
||||||
|
const totalProducts = useMemo(
|
||||||
|
() =>
|
||||||
|
group.deals?.reduce(
|
||||||
|
(acc, deal) => acc + (deal.productsQuantity ?? 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
[group.deals]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
p={"xs"}
|
||||||
|
direction={"column"}
|
||||||
|
bg={
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? "var(--mantine-color-dark-6)"
|
||||||
|
: "var(--mantine-color-gray-2)"
|
||||||
|
}
|
||||||
|
style={{ borderRadius: "0.5rem" }}>
|
||||||
|
<Text
|
||||||
|
c={"gray.6"}
|
||||||
|
size={"xs"}>
|
||||||
|
Сумма: {totalPrice?.toLocaleString("ru-RU")} руб.
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
c={"gray.6"}
|
||||||
|
size={"xs"}>
|
||||||
|
Всего товаров: {totalProducts?.toLocaleString("ru-RU")}{" "}
|
||||||
|
шт.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FulfillmentGroupInfo;
|
||||||
94
src/app/deals/components/shared/DealsTable/DealsTable.tsx
Normal file
94
src/app/deals/components/shared/DealsTable/DealsTable.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { FC, useCallback } from "react";
|
||||||
|
import { IconMoodSad } from "@tabler/icons-react";
|
||||||
|
import { Group, Pagination, Stack, Text } from "@mantine/core";
|
||||||
|
import useDealsTableColumns from "@/app/deals/components/shared/DealsTable/useDealsTableColumns";
|
||||||
|
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
const DealsTable: FC = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { selectedProject, modulesSet } = useProjectsContext();
|
||||||
|
const {
|
||||||
|
deals,
|
||||||
|
paginationInfo,
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
sortingForm,
|
||||||
|
dealsCrud,
|
||||||
|
refetchDeals,
|
||||||
|
} = useDealsContext();
|
||||||
|
const { openDrawer } = useDrawersContext();
|
||||||
|
|
||||||
|
const onEditClick = useCallback(
|
||||||
|
(deal: DealSchema) => {
|
||||||
|
openDrawer({
|
||||||
|
key: "dealEditorDrawer",
|
||||||
|
props: {
|
||||||
|
value: deal,
|
||||||
|
onChange: deal => dealsCrud.onUpdate(deal.id, deal),
|
||||||
|
onDelete: dealsCrud.onDelete,
|
||||||
|
project: selectedProject,
|
||||||
|
},
|
||||||
|
onClose: refetchDeals,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[openDrawer, dealsCrud]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useDealsTableColumns({ onEditClick, modulesSet });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
p={isMobile ? "xs" : ""}
|
||||||
|
gap={"xs"}
|
||||||
|
h={"100%"}>
|
||||||
|
<BaseTable
|
||||||
|
withTableBorder
|
||||||
|
records={[...deals]}
|
||||||
|
columns={columns}
|
||||||
|
sortStatus={{
|
||||||
|
columnAccessor: sortingForm.values.sortingField ?? "",
|
||||||
|
direction: sortingForm.values.sortingDirection,
|
||||||
|
}}
|
||||||
|
onSortStatusChange={sorting => {
|
||||||
|
sortingForm.setFieldValue(
|
||||||
|
"sortingField",
|
||||||
|
sorting.columnAccessor
|
||||||
|
);
|
||||||
|
sortingForm.setFieldValue(
|
||||||
|
"sortingDirection",
|
||||||
|
sorting.direction
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
emptyState={
|
||||||
|
<Group
|
||||||
|
align={"center"}
|
||||||
|
gap={"xs"}>
|
||||||
|
<Text>Нет сделок</Text>
|
||||||
|
<IconMoodSad />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
groups={undefined}
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{paginationInfo && paginationInfo.totalPages > 1 && (
|
||||||
|
<Group justify={"flex-end"}>
|
||||||
|
<Pagination
|
||||||
|
withEdges
|
||||||
|
total={paginationInfo.totalPages}
|
||||||
|
value={page}
|
||||||
|
onChange={setPage}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DealsTable;
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { IconEdit } from "@tabler/icons-react";
|
||||||
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
|
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
import { ModuleNames } from "@/modules/modules";
|
||||||
|
import { utcDateTimeToLocalString } from "@/utils/datetime";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onEditClick: (deal: DealSchema) => void;
|
||||||
|
modulesSet: Set<ModuleNames>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDealsTableColumns = ({ onEditClick, modulesSet }: Props) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
accessor: "actions",
|
||||||
|
title: isMobile ? "" : "Действия",
|
||||||
|
sortable: false,
|
||||||
|
textAlign: "center",
|
||||||
|
width: "0%",
|
||||||
|
render: deal => (
|
||||||
|
<ActionIconWithTip
|
||||||
|
tipLabel={"Редактировать"}
|
||||||
|
onClick={() => onEditClick(deal)}
|
||||||
|
variant={isMobile ? "subtle" : "default"}>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "id",
|
||||||
|
title: isMobile ? "№" : "Номер",
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "name",
|
||||||
|
title: "Название",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Дата создания",
|
||||||
|
accessor: "createdAt",
|
||||||
|
render: deal => utcDateTimeToLocalString(deal.createdAt),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Клиент",
|
||||||
|
accessor: "client.name",
|
||||||
|
hidden: !modulesSet.has(ModuleNames.CLIENTS),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Общая стоимость",
|
||||||
|
accessor: "totalPrice",
|
||||||
|
render: deal =>
|
||||||
|
deal.totalPrice
|
||||||
|
? `${deal.totalPrice.toLocaleString("ru")}₽`
|
||||||
|
: "0₽",
|
||||||
|
hidden: !modulesSet.has(ModuleNames.FULFILLMENT_BASE),
|
||||||
|
},
|
||||||
|
] as DataTableColumn<DealSchema>[],
|
||||||
|
[onEditClick]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDealsTableColumns;
|
||||||
@ -1,89 +1,69 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC } from "react";
|
||||||
import { Group, ScrollArea } from "@mantine/core";
|
import { Box } from "@mantine/core";
|
||||||
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
|
||||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||||
import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader";
|
||||||
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||||
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
|
import DndFunnel from "@/components/dnd-pragmatic/DndFunnel/DndFunnel";
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
|
||||||
import { sortByLexorank } from "@/utils/lexorank";
|
|
||||||
|
|
||||||
const Funnel: FC = () => {
|
const Funnel: FC = () => {
|
||||||
const { deals } = useDealsContext();
|
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
|
||||||
const isMobile = useIsMobile();
|
const { dealsWithoutGroup, groupsWithDeals, deals, setDeals, dealsCrud } =
|
||||||
|
useDealsContext();
|
||||||
|
|
||||||
const {
|
const updateStatus = (statusId: number, lexorank: string) => {
|
||||||
sortedStatuses,
|
setStatuses(
|
||||||
handleDragStart,
|
statuses.map(status =>
|
||||||
handleDragOver,
|
status.id === statusId ? { ...status, lexorank } : status
|
||||||
handleDragEnd,
|
|
||||||
activeStatus,
|
|
||||||
activeDeal,
|
|
||||||
} = useDealsAndStatusesDnd();
|
|
||||||
|
|
||||||
const renderFunnelDnd = () => (
|
|
||||||
<FunnelDnd
|
|
||||||
containers={sortedStatuses}
|
|
||||||
items={deals}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
|
||||||
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
|
||||||
sortByLexorank(
|
|
||||||
items.filter(deal => deal.statusId === status.id)
|
|
||||||
)
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
statusesCrud.onUpdate(statusId, { lexorank });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDeal = (dealId: number, lexorank: string, statusId: number) => {
|
||||||
|
const status = statuses.find(s => s.id === statusId);
|
||||||
|
if (!status) return;
|
||||||
|
setDeals(
|
||||||
|
deals.map(deal =>
|
||||||
|
deal.id === dealId ? { ...deal, lexorank, status } : deal
|
||||||
|
)
|
||||||
|
);
|
||||||
|
dealsCrud.onUpdate(dealId, { lexorank, statusId });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndFunnel
|
||||||
|
columns={statuses}
|
||||||
|
updateColumn={updateStatus}
|
||||||
|
items={dealsWithoutGroup}
|
||||||
|
groups={groupsWithDeals}
|
||||||
|
updateItem={updateDeal}
|
||||||
|
getColumnItemsGroups={statusId =>
|
||||||
|
sortByLexorank([
|
||||||
|
...dealsWithoutGroup.filter(d => d.status.id === statusId),
|
||||||
|
...groupsWithDeals.filter(
|
||||||
|
g =>
|
||||||
|
g.items.length > 0 &&
|
||||||
|
g.items[0].status.id === statusId
|
||||||
|
),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
renderContainer={(
|
renderColumnHeader={status => (
|
||||||
status: StatusSchema,
|
<StatusColumnHeader status={status} />
|
||||||
funnelColumnComponent: ReactNode
|
|
||||||
) => (
|
|
||||||
<StatusColumnWrapper
|
|
||||||
status={status}
|
|
||||||
isDragging={activeStatus?.id === status.id}>
|
|
||||||
{funnelColumnComponent}
|
|
||||||
</StatusColumnWrapper>
|
|
||||||
)}
|
)}
|
||||||
renderItem={(deal: DealSchema) => (
|
renderItem={deal => (
|
||||||
<DealContainer
|
<DealCard
|
||||||
key={deal.id}
|
key={deal.id}
|
||||||
deal={deal}
|
deal={deal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
activeContainer={activeStatus}
|
renderGroup={group => <Box flex={1}>{group.name}</Box>}
|
||||||
activeItem={activeDeal}
|
|
||||||
renderItemOverlay={(deal: DealSchema) => <DealCard deal={deal} />}
|
|
||||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
|
||||||
<StatusColumnWrapper
|
|
||||||
status={status}
|
|
||||||
isDragging>
|
|
||||||
{children}
|
|
||||||
</StatusColumnWrapper>
|
|
||||||
)}
|
|
||||||
disabledColumns={isMobile}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) return renderFunnelDnd();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea
|
|
||||||
offsetScrollbars={"x"}
|
|
||||||
scrollbarSize={"0.5rem"}>
|
|
||||||
<Group
|
|
||||||
align={"start"}
|
|
||||||
wrap={"nowrap"}
|
|
||||||
gap={"xs"}>
|
|
||||||
{renderFunnelDnd()}
|
|
||||||
<CreateStatusButton />
|
|
||||||
</Group>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Funnel;
|
export default Funnel;
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
|
|
||||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
|
||||||
import Boards from "@/app/deals/components/desktop/Boards/Boards";
|
|
||||||
import BoardsMobile from "@/app/deals/components/mobile/BoardsMobile/BoardsMobile";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
|
||||||
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
|
||||||
import { ColorSchemeToggle } from "@/components/ui/ColorSchemeToggle/ColorSchemeToggle";
|
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
const {
|
|
||||||
projects,
|
|
||||||
setSelectedProject,
|
|
||||||
selectedProject,
|
|
||||||
setIsEditorDrawerOpened: setIsProjectsDrawerOpened,
|
|
||||||
} = useProjectsContext();
|
|
||||||
const { setIsEditorDrawerOpened } = useBoardsContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const getDesktopHeader = () => {
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
w={"100%"}
|
|
||||||
justify={"space-between"}
|
|
||||||
wrap={"nowrap"}
|
|
||||||
pr={"md"}>
|
|
||||||
<Boards />
|
|
||||||
<ColorSchemeToggle />
|
|
||||||
<ProjectSelect
|
|
||||||
data={projects}
|
|
||||||
value={selectedProject}
|
|
||||||
onChange={value => value && setSelectedProject(value)}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMobileHeader = () => {
|
|
||||||
return (
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Group justify={"space-between"}>
|
|
||||||
<Box
|
|
||||||
p={"md"}
|
|
||||||
onClick={() => setIsProjectsDrawerOpened(true)}>
|
|
||||||
<IconChevronLeft />
|
|
||||||
</Box>
|
|
||||||
<Text>{selectedProject?.name}</Text>
|
|
||||||
<Box
|
|
||||||
p={"md"}
|
|
||||||
onClick={() => setIsEditorDrawerOpened(true)}>
|
|
||||||
<IconSettings />
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
<BoardsMobile />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
justify={"flex-end"}
|
|
||||||
w={"100%"}>
|
|
||||||
{isMobile ? getMobileHeader() : getDesktopHeader()}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
59
src/app/deals/components/shared/PageBody/PageBody.tsx
Normal file
59
src/app/deals/components/shared/PageBody/PageBody.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Flex } from "@mantine/core";
|
||||||
|
import TopToolPanel from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel";
|
||||||
|
import {
|
||||||
|
BoardView,
|
||||||
|
ScheduleView,
|
||||||
|
TableView,
|
||||||
|
} from "@/app/deals/components/shared/views";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import { DealsContextProvider } from "@/app/deals/contexts/DealsContext";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import useView from "@/app/deals/hooks/useView";
|
||||||
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
|
|
||||||
|
const PageBody = () => {
|
||||||
|
const { selectedBoard } = useBoardsContext();
|
||||||
|
const { selectedProject } = useProjectsContext();
|
||||||
|
|
||||||
|
const { view, setView } = useView();
|
||||||
|
|
||||||
|
const getViewContent = () => {
|
||||||
|
switch (view) {
|
||||||
|
case "board":
|
||||||
|
return <BoardView />;
|
||||||
|
case "table":
|
||||||
|
return <TableView />;
|
||||||
|
default:
|
||||||
|
return <ScheduleView />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContextProps = () => {
|
||||||
|
if (view === "table") {
|
||||||
|
return { withPagination: true, projectId: selectedProject?.id };
|
||||||
|
}
|
||||||
|
return { boardId: selectedBoard?.id };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DealsContextProvider {...getContextProps()}>
|
||||||
|
<TopToolPanel
|
||||||
|
view={view}
|
||||||
|
setView={setView}
|
||||||
|
/>
|
||||||
|
<PageBlock
|
||||||
|
fullScreenMobile
|
||||||
|
style={{ flex: 1 }}>
|
||||||
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
h={"100%"}>
|
||||||
|
{getViewContent()}
|
||||||
|
</Flex>
|
||||||
|
</PageBlock>
|
||||||
|
</DealsContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageBody;
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { Group, Text } from "@mantine/core";
|
||||||
|
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
|
import { StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status: StatusSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusColumnHeader: FC<Props> = ({ status }) => {
|
||||||
|
const { statusesCrud, refetchStatuses } = useStatusesContext();
|
||||||
|
const { selectedBoard } = useBoardsContext();
|
||||||
|
|
||||||
|
const handleSave = (value: string) => {
|
||||||
|
const newValue = value.trim();
|
||||||
|
if (newValue && newValue !== status.name) {
|
||||||
|
statusesCrud.onUpdate(status.id, { name: newValue });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify={"space-between"}
|
||||||
|
p={"sm"}
|
||||||
|
wrap={"nowrap"}
|
||||||
|
mb={"xs"}
|
||||||
|
w={"100%"}
|
||||||
|
style={{
|
||||||
|
borderBottom: `solid ${status.color} 3px`,
|
||||||
|
}}>
|
||||||
|
<InPlaceInput
|
||||||
|
value={status.name}
|
||||||
|
onChange={value => handleSave(value)}
|
||||||
|
inputStyles={{
|
||||||
|
input: {
|
||||||
|
height: 25,
|
||||||
|
minHeight: 25,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
getChildren={startEditing => (
|
||||||
|
<>
|
||||||
|
<Text>{status.name}</Text>
|
||||||
|
<StatusMenu
|
||||||
|
board={selectedBoard}
|
||||||
|
status={status}
|
||||||
|
handleEdit={startEditing}
|
||||||
|
onStatusColorChange={color =>
|
||||||
|
statusesCrud.onUpdate(status.id, { color })
|
||||||
|
}
|
||||||
|
refetchStatuses={refetchStatuses}
|
||||||
|
onDeleteStatus={statusesCrud.onDelete}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
modalTitle={"Редактирование статуса"}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusColumnHeader;
|
||||||
@ -1,13 +1,23 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-width: 150px;
|
height: calc(100vh - 215px);
|
||||||
width: 15vw;
|
|
||||||
|
|
||||||
@media (max-width: 48em) {
|
@media (max-width: 48em) {
|
||||||
width: 80vw;
|
width: 80vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.inner-container {
|
||||||
border-bottom: solid dodgerblue 3px;
|
border-radius: var(--mantine-spacing-lg);
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
@media (max-width: 48em) {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background-color: var(--color-light-aqua);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,68 +1,41 @@
|
|||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, ScrollArea, Stack } from "@mantine/core";
|
||||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
import CreateCardButton from "@/app/deals/components/shared/CreateDealButton/CreateDealButton";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
|
||||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
|
||||||
import { StatusSchema } from "@/lib/client";
|
import { StatusSchema } from "@/lib/client";
|
||||||
import styles from "./StatusColumnWrapper.module.css";
|
import styles from "./StatusColumnWrapper.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: StatusSchema;
|
status: StatusSchema;
|
||||||
isDragging?: boolean;
|
renderHeader: () => ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
createFormEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusColumnWrapper = ({
|
const StatusColumnWrapper = ({
|
||||||
status,
|
renderHeader,
|
||||||
children,
|
children,
|
||||||
isDragging = false,
|
createFormEnabled = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { onUpdateStatus } = useStatusesContext();
|
|
||||||
|
|
||||||
const handleSave = (value: string) => {
|
|
||||||
const newValue = value.trim();
|
|
||||||
if (newValue && newValue !== status.name) {
|
|
||||||
onUpdateStatus(status.id, { name: newValue });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.container}>
|
<Box className={styles.container}>
|
||||||
<Group
|
<Stack
|
||||||
justify={"space-between"}
|
px={"xs"}
|
||||||
p={"sm"}
|
pb={"xs"}
|
||||||
wrap={"nowrap"}
|
className={styles["inner-container"]}>
|
||||||
mb={"xs"}
|
{renderHeader()}
|
||||||
className={styles.header}>
|
<ScrollArea
|
||||||
<InPlaceInput
|
offsetScrollbars={"present"}
|
||||||
defaultValue={status.name}
|
scrollbarSize={10}
|
||||||
onComplete={value => handleSave(value)}
|
type={"always"}
|
||||||
inputStyles={{
|
scrollbars={"y"}>
|
||||||
input: {
|
<Stack
|
||||||
height: 25,
|
gap={"xs"}
|
||||||
minHeight: 25,
|
mah={"calc(100vh - 285px)"}>
|
||||||
},
|
{createFormEnabled && <CreateCardButton />}
|
||||||
}}
|
|
||||||
getChildren={startEditing => (
|
|
||||||
<>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
cursor: "grab",
|
|
||||||
userSelect: "none",
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
}}>
|
|
||||||
{status.name}
|
|
||||||
</Text>
|
|
||||||
<StatusMenu
|
|
||||||
status={status}
|
|
||||||
handleEdit={startEditing}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
modalTitle={"Редактирование статуса"}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
{children}
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,65 +3,98 @@ import {
|
|||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconExchange,
|
IconExchange,
|
||||||
|
IconPalette,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
import { Box, Menu } from "@mantine/core";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { modals } from "@mantine/modals";
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
import statusColors from "@/app/deals/utils/statusColors";
|
||||||
import { StatusSchema } from "@/lib/client";
|
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
|
||||||
|
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
|
||||||
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { BoardSchema, StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: StatusSchema;
|
status: StatusSchema;
|
||||||
handleEdit: () => void;
|
handleEdit: () => void;
|
||||||
|
onStatusColorChange: (color: string) => void;
|
||||||
|
board: BoardSchema | null;
|
||||||
|
onDeleteStatus: (status: StatusSchema) => void;
|
||||||
|
refetchStatuses?: () => void;
|
||||||
|
withChangeOrderButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
|
const StatusMenu: FC<Props> = ({
|
||||||
|
status,
|
||||||
|
handleEdit,
|
||||||
|
onStatusColorChange,
|
||||||
|
board,
|
||||||
|
onDeleteStatus,
|
||||||
|
refetchStatuses,
|
||||||
|
withChangeOrderButton = true,
|
||||||
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { onDeleteStatus, setIsEditorDrawerOpened } = useStatusesContext();
|
const { openDrawer } = useDrawersContext();
|
||||||
|
|
||||||
|
const openStatusesMobileEditor = () => {
|
||||||
|
if (!board) return;
|
||||||
|
openDrawer({
|
||||||
|
key: "statusesMobileEditorDrawer",
|
||||||
|
props: {
|
||||||
|
board,
|
||||||
|
},
|
||||||
|
onClose: refetchStatuses,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openStatusColorPicker = () => {
|
||||||
|
if (!board) return;
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "statusColorPickerModal",
|
||||||
|
title: "Изменение цвета статуса",
|
||||||
|
withCloseButton: false,
|
||||||
|
innerProps: {
|
||||||
|
color: status.color,
|
||||||
|
onChange: onStatusColorChange,
|
||||||
|
switches: statusColors,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{ cursor: "pointer" }}
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={e => e.stopPropagation()}>
|
onClick={e => e.stopPropagation()}>
|
||||||
|
<ThemeIcon size={"sm"}>
|
||||||
<IconDotsVertical />
|
<IconDotsVertical />
|
||||||
|
</ThemeIcon>
|
||||||
</Box>
|
</Box>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<DropdownMenuItem
|
||||||
onClick={e => {
|
onClick={handleEdit}
|
||||||
e.stopPropagation();
|
icon={<IconEdit />}
|
||||||
handleEdit();
|
label={"Переименовать"}
|
||||||
}}>
|
/>
|
||||||
<Group wrap={"nowrap"}>
|
<DropdownMenuItem
|
||||||
<IconEdit />
|
onClick={openStatusColorPicker}
|
||||||
<Text>Переименовать</Text>
|
icon={<IconPalette />}
|
||||||
</Group>
|
label={"Изменить цвет"}
|
||||||
</Menu.Item>
|
/>
|
||||||
<Menu.Item
|
<DropdownMenuItem
|
||||||
onClick={e => {
|
onClick={() => onDeleteStatus(status)}
|
||||||
e.stopPropagation();
|
icon={<IconTrash />}
|
||||||
onDeleteStatus(status);
|
label={"Удалить"}
|
||||||
}}>
|
/>
|
||||||
<Group wrap={"nowrap"}>
|
{isMobile && withChangeOrderButton && (
|
||||||
<IconTrash />
|
<DropdownMenuItem
|
||||||
<Text>Удалить</Text>
|
onClick={openStatusesMobileEditor}
|
||||||
</Group>
|
icon={<IconExchange />}
|
||||||
</Menu.Item>
|
label={"Изменить порядок"}
|
||||||
{isMobile && (
|
/>
|
||||||
<Menu.Item
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsEditorDrawerOpened(true);
|
|
||||||
}}>
|
|
||||||
<Group wrap={"nowrap"}>
|
|
||||||
<IconExchange />
|
|
||||||
<Text>Изменить порядок</Text>
|
|
||||||
</Group>
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
)}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
11
src/app/deals/components/shared/views/BoardView.tsx
Normal file
11
src/app/deals/components/shared/views/BoardView.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Space } from "@mantine/core";
|
||||||
|
import MainBlockHeader from "@/app/deals/components/mobile/MainBlockHeader/MainBlockHeader";
|
||||||
|
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
|
||||||
|
|
||||||
|
export const BoardView = () => (
|
||||||
|
<>
|
||||||
|
<MainBlockHeader />
|
||||||
|
<Space h="md" />
|
||||||
|
<Funnel />
|
||||||
|
</>
|
||||||
|
);
|
||||||
3
src/app/deals/components/shared/views/ScheduleView.tsx
Normal file
3
src/app/deals/components/shared/views/ScheduleView.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const ScheduleView = () => {
|
||||||
|
return <>-</>;
|
||||||
|
};
|
||||||
3
src/app/deals/components/shared/views/TableView.tsx
Normal file
3
src/app/deals/components/shared/views/TableView.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import DealsTable from "../DealsTable/DealsTable";
|
||||||
|
|
||||||
|
export const TableView = () => <DealsTable />;
|
||||||
3
src/app/deals/components/shared/views/index.ts
Normal file
3
src/app/deals/components/shared/views/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { BoardView } from "./BoardView";
|
||||||
|
export { TableView } from "./TableView";
|
||||||
|
export { ScheduleView } from "./ScheduleView";
|
||||||
@ -1,106 +1,53 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, {
|
import React, { useState } from "react";
|
||||||
createContext,
|
|
||||||
FC,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import useBoardsList from "@/hooks/useBoardsList";
|
import { BoardsCrud, useBoardsCrud } from "@/hooks/cruds/useBoardsCrud";
|
||||||
import { useBoardsOperations } from "@/hooks/useBoardsOperations";
|
import useBoardsList from "@/hooks/lists/useBoardsList";
|
||||||
import { BoardSchema, UpdateBoardSchema } from "@/lib/client";
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
|
|
||||||
type BoardsContextState = {
|
type BoardsContextState = {
|
||||||
boards: BoardSchema[];
|
boards: BoardSchema[];
|
||||||
setBoards: React.Dispatch<React.SetStateAction<BoardSchema[]>>;
|
setBoards: (boards: BoardSchema[]) => void;
|
||||||
selectedBoard: BoardSchema | null;
|
selectedBoard: BoardSchema | null;
|
||||||
setSelectedBoard: React.Dispatch<React.SetStateAction<BoardSchema | null>>;
|
setSelectedBoardId: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
refetchBoards: () => void;
|
refetchBoards: () => void;
|
||||||
onCreateBoard: (name: string) => void;
|
boardsCrud: BoardsCrud;
|
||||||
onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void;
|
|
||||||
onDeleteBoard: (board: BoardSchema) => void;
|
|
||||||
isEditorDrawerOpened: boolean;
|
|
||||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardsContext = createContext<BoardsContextState | undefined>(undefined);
|
const useBoardsContextState = (): BoardsContextState => {
|
||||||
|
|
||||||
const useBoardsContextState = () => {
|
|
||||||
const { selectedProject: project } = useProjectsContext();
|
const { selectedProject: project } = useProjectsContext();
|
||||||
const {
|
const {
|
||||||
boards,
|
boards,
|
||||||
setBoards,
|
setBoards,
|
||||||
refetch: refetchBoards,
|
refetch: refetchBoards,
|
||||||
|
queryKey,
|
||||||
} = useBoardsList({ projectId: project?.id });
|
} = useBoardsList({ projectId: project?.id });
|
||||||
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const [selectedBoardId, setSelectedBoardId] = useState<number | null>(null);
|
||||||
if (boards.length > 0 && selectedBoard === null) {
|
const selectedBoard =
|
||||||
setSelectedBoard(boards[0]);
|
boards.find(board => board.id === selectedBoardId) ?? null;
|
||||||
return;
|
|
||||||
|
if (selectedBoard === null && boards.length > 0) {
|
||||||
|
setSelectedBoardId(boards[0].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedBoard) return;
|
const boardsCrud = useBoardsCrud({
|
||||||
|
|
||||||
let newBoard = boards.find(board => board.id === selectedBoard.id);
|
|
||||||
|
|
||||||
if (!newBoard && boards.length > 0) {
|
|
||||||
newBoard = boards[0];
|
|
||||||
}
|
|
||||||
setSelectedBoard(newBoard ?? null);
|
|
||||||
}, [boards]);
|
|
||||||
|
|
||||||
const { onCreateBoard, onUpdateBoard, onDeleteBoard } = useBoardsOperations(
|
|
||||||
{
|
|
||||||
boards,
|
boards,
|
||||||
setBoards,
|
queryKey,
|
||||||
refetchBoards,
|
|
||||||
projectId: project?.id,
|
projectId: project?.id,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
boards,
|
boards,
|
||||||
setBoards,
|
setBoards,
|
||||||
selectedBoard,
|
selectedBoard,
|
||||||
setSelectedBoard,
|
setSelectedBoardId,
|
||||||
refetchBoards,
|
refetchBoards,
|
||||||
onCreateBoard,
|
boardsCrud,
|
||||||
onUpdateBoard,
|
|
||||||
onDeleteBoard,
|
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type BoardsContextProviderProps = {
|
export const [BoardsContextProvider, useBoardsContext] =
|
||||||
children: React.ReactNode;
|
makeContext<BoardsContextState>(useBoardsContextState, "Boards");
|
||||||
};
|
|
||||||
|
|
||||||
export const BoardsContextProvider: FC<BoardsContextProviderProps> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const state = useBoardsContextState();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BoardsContext.Provider value={state}>
|
|
||||||
{children}
|
|
||||||
</BoardsContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useBoardsContext = () => {
|
|
||||||
const context = useContext(BoardsContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"useBoardsContext must be used within a BoardsContextProvider"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,80 +1,69 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, FC, useContext } from "react";
|
import React from "react";
|
||||||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
import { UseFormReturnType } from "@mantine/form";
|
||||||
import { AxiosError } from "axios";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import useDealsAndGroups from "@/app/deals/hooks/useDealsAndGroups";
|
||||||
import useDealsList from "@/hooks/useDealsList";
|
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
|
||||||
import {
|
import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud";
|
||||||
DealSchema,
|
import useDealsList from "@/hooks/lists/useDealsList";
|
||||||
HttpValidationError,
|
import { SortingForm } from "@/hooks/utils/useSorting";
|
||||||
Options,
|
import { DealSchema, PaginationInfoSchema } from "@/lib/client";
|
||||||
UpdateDealData,
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
UpdateDealResponse,
|
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||||
} from "@/lib/client";
|
|
||||||
import { updateDealMutation } from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
import { notifications } from "@/lib/notifications";
|
|
||||||
|
|
||||||
type DealsContextState = {
|
type DealsContextState = {
|
||||||
deals: DealSchema[];
|
deals: DealSchema[];
|
||||||
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
|
setDeals: (deals: DealSchema[]) => void;
|
||||||
updateDeal: UseMutationResult<
|
dealsWithoutGroup: DealSchema[];
|
||||||
UpdateDealResponse,
|
groupsWithDeals: GroupWithDealsSchema[];
|
||||||
AxiosError<HttpValidationError>,
|
|
||||||
Options<UpdateDealData>
|
|
||||||
>;
|
|
||||||
refetchDeals: () => void;
|
refetchDeals: () => void;
|
||||||
|
dealsCrud: DealsCrud;
|
||||||
|
paginationInfo?: PaginationInfoSchema;
|
||||||
|
page: number;
|
||||||
|
setPage: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
dealsFiltersForm: UseFormReturnType<DealsFiltersForm>;
|
||||||
|
isChangedFilters: boolean;
|
||||||
|
sortingForm: UseFormReturnType<SortingForm>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DealsContext = createContext<DealsContextState | undefined>(undefined);
|
type Props = {
|
||||||
|
withPagination?: boolean;
|
||||||
|
boardId?: number;
|
||||||
|
projectId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const useDealsContextState = () => {
|
const useDealsContextState = ({
|
||||||
const { selectedBoard } = useBoardsContext();
|
boardId,
|
||||||
const {
|
projectId,
|
||||||
deals,
|
withPagination = false,
|
||||||
setDeals,
|
}: Props): DealsContextState => {
|
||||||
refetch: refetchDeals,
|
const { statuses } = useStatusesContext();
|
||||||
} = useDealsList({ boardId: selectedBoard?.id });
|
|
||||||
|
|
||||||
const updateDeal = useMutation({
|
const dealsListObjects = useDealsList({
|
||||||
...updateDealMutation(),
|
boardId,
|
||||||
onError: error => {
|
projectId,
|
||||||
console.error(error);
|
withPagination,
|
||||||
notifications.error({
|
|
||||||
message: error.response?.data?.detail as string | undefined,
|
|
||||||
});
|
});
|
||||||
refetchDeals();
|
|
||||||
},
|
const dealsCrud = useDealsCrud({
|
||||||
|
...dealsListObjects,
|
||||||
|
boardId,
|
||||||
|
statuses,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { dealsWithoutGroup, groupsWithDeals } =
|
||||||
|
useDealsAndGroups(dealsListObjects);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deals,
|
...dealsListObjects,
|
||||||
setDeals,
|
dealsWithoutGroup,
|
||||||
updateDeal,
|
groupsWithDeals,
|
||||||
refetchDeals,
|
dealsCrud,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type DealsContextProviderProps = {
|
export const [DealsContextProvider, useDealsContext] = makeContext<
|
||||||
children: React.ReactNode;
|
DealsContextState,
|
||||||
};
|
Props
|
||||||
|
>(useDealsContextState, "Deals");
|
||||||
export const DealsContextProvider: FC<DealsContextProviderProps> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const state = useDealsContextState();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DealsContext.Provider value={state}>{children}</DealsContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDealsContext = () => {
|
|
||||||
const context = useContext(DealsContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"useDealsContext must be used within a DealsContextProvider"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,101 +1,66 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, {
|
import { useMemo, useState } from "react";
|
||||||
createContext,
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
FC,
|
import { ProjectsCrud, useProjectsCrud } from "@/hooks/cruds/useProjectsCrud";
|
||||||
useContext,
|
import useProjectsList from "@/hooks/lists/useProjectsList";
|
||||||
useEffect,
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
useState,
|
import { ProjectSchema } from "@/lib/client";
|
||||||
} from "react";
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
import useProjectsList from "@/hooks/useProjectsList";
|
import { ModuleNames } from "@/modules/modules";
|
||||||
import { useProjectsOperations } from "@/hooks/useProjectsOperations";
|
|
||||||
import { ProjectSchema, UpdateProjectSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type ProjectsContextState = {
|
type ProjectsContextState = {
|
||||||
selectedProject: ProjectSchema | null;
|
selectedProject: ProjectSchema | null;
|
||||||
setSelectedProject: React.Dispatch<
|
setSelectedProjectId: (id: number | null) => void;
|
||||||
React.SetStateAction<ProjectSchema | null>
|
refetchProjects: () => void;
|
||||||
>;
|
|
||||||
projects: ProjectSchema[];
|
projects: ProjectSchema[];
|
||||||
onCreateProject: (name: string) => void;
|
projectsCrud: ProjectsCrud;
|
||||||
onUpdateProject: (projectId: number, project: UpdateProjectSchema) => void;
|
modulesSet: Set<ModuleNames>;
|
||||||
onDeleteProject: (project: ProjectSchema) => void;
|
|
||||||
isEditorDrawerOpened: boolean;
|
|
||||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
const useProjectsContextState = (): ProjectsContextState => {
|
||||||
undefined
|
const { projects, refetch: refetchProjects, queryKey } = useProjectsList();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const selectedProject = useMemo(
|
||||||
|
() =>
|
||||||
|
projects.find(project => project.id === selectedProjectId) ?? null,
|
||||||
|
[projects, selectedProjectId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const useProjectsContextState = () => {
|
const modulesSet = useMemo(
|
||||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
() =>
|
||||||
useState<boolean>(false);
|
new Set(
|
||||||
const {
|
selectedProject?.builtInModules.map(m => m.key as ModuleNames)
|
||||||
projects,
|
),
|
||||||
setProjects,
|
[selectedProject]
|
||||||
refetch: refetchProjects,
|
|
||||||
} = useProjectsList();
|
|
||||||
const [selectedProject, setSelectedProject] =
|
|
||||||
useState<ProjectSchema | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projects.length > 0) {
|
|
||||||
if (selectedProject) {
|
|
||||||
setSelectedProject(
|
|
||||||
projects.find(
|
|
||||||
project => project.id === selectedProject.id
|
|
||||||
) ?? null
|
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedProject(projects[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedProject(null);
|
|
||||||
}, [projects]);
|
|
||||||
|
|
||||||
const { onCreateProject, onUpdateProject, onDeleteProject } =
|
if (selectedProject === null && projects.length > 0) {
|
||||||
useProjectsOperations({
|
setSelectedProjectId(projects[0].id);
|
||||||
projects,
|
}
|
||||||
setProjects,
|
|
||||||
refetchProjects,
|
const projectsCrud = useProjectsCrud({ queryKey });
|
||||||
});
|
|
||||||
|
const handleSetSelectedProjectId = (id: number | null) => {
|
||||||
|
if (!isMobile && pathname !== "/deals") router.push("/deals");
|
||||||
|
setSelectedProjectId(id);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
setSelectedProject,
|
refetchProjects,
|
||||||
onCreateProject,
|
setSelectedProjectId: handleSetSelectedProjectId,
|
||||||
onUpdateProject,
|
projectsCrud,
|
||||||
onDeleteProject,
|
modulesSet,
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProjectsContextProviderProps = {
|
export const [ProjectsContextProvider, useProjectsContext] =
|
||||||
children: React.ReactNode;
|
makeContext<ProjectsContextState>(useProjectsContextState, "Projects");
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectsContextProvider: FC<ProjectsContextProviderProps> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const state = useProjectsContextState();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProjectsContext.Provider value={state}>
|
|
||||||
{children}
|
|
||||||
</ProjectsContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useProjectsContext = () => {
|
|
||||||
const context = useContext(ProjectsContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"useProjectsContext must be used within a ProjectsContextProvider"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,118 +1,42 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
FC,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
import useStatusesList from "@/hooks/useStatusesList";
|
import { StatusesCrud, useStatusesCrud } from "@/hooks/cruds/useStatusesCrud";
|
||||||
import { useStatusesOperations } from "@/hooks/useStatusesOperations";
|
import useStatusesList from "@/hooks/lists/useStatusesList";
|
||||||
import {
|
import { StatusSchema } from "@/lib/client";
|
||||||
HttpValidationError,
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
Options,
|
|
||||||
StatusSchema,
|
|
||||||
UpdateStatusData,
|
|
||||||
UpdateStatusResponse,
|
|
||||||
UpdateStatusSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import { updateStatusMutation } from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
import { notifications } from "@/lib/notifications";
|
|
||||||
|
|
||||||
type StatusesContextState = {
|
type StatusesContextState = {
|
||||||
statuses: StatusSchema[];
|
statuses: StatusSchema[];
|
||||||
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
setStatuses: (statuses: StatusSchema[]) => void;
|
||||||
updateStatus: UseMutationResult<
|
|
||||||
UpdateStatusResponse,
|
|
||||||
AxiosError<HttpValidationError>,
|
|
||||||
Options<UpdateStatusData>
|
|
||||||
>;
|
|
||||||
refetchStatuses: () => void;
|
refetchStatuses: () => void;
|
||||||
onCreateStatus: (name: string) => void;
|
statusesCrud: StatusesCrud;
|
||||||
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
|
|
||||||
onDeleteStatus: (status: StatusSchema) => void;
|
|
||||||
isEditorDrawerOpened: boolean;
|
|
||||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusesContext = createContext<StatusesContextState | undefined>(
|
const useStatusesContextState = (): StatusesContextState => {
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const useStatusesContextState = () => {
|
|
||||||
const { selectedBoard } = useBoardsContext();
|
const { selectedBoard } = useBoardsContext();
|
||||||
const {
|
const {
|
||||||
statuses,
|
statuses,
|
||||||
setStatuses,
|
setStatuses,
|
||||||
refetch: refetchStatuses,
|
refetch: refetchStatuses,
|
||||||
|
queryKey,
|
||||||
} = useStatusesList({
|
} = useStatusesList({
|
||||||
boardId: selectedBoard?.id,
|
boardId: selectedBoard?.id,
|
||||||
});
|
});
|
||||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const statusesCrud = useStatusesCrud({
|
||||||
refetchStatuses();
|
|
||||||
}, [selectedBoard]);
|
|
||||||
|
|
||||||
const updateStatus = useMutation({
|
|
||||||
...updateStatusMutation(),
|
|
||||||
onError: error => {
|
|
||||||
console.error(error);
|
|
||||||
notifications.error({
|
|
||||||
message: error.response?.data?.detail as string | undefined,
|
|
||||||
});
|
|
||||||
refetchStatuses();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { onCreateStatus, onUpdateStatus, onDeleteStatus } =
|
|
||||||
useStatusesOperations({
|
|
||||||
statuses,
|
statuses,
|
||||||
setStatuses,
|
queryKey,
|
||||||
refetchStatuses,
|
|
||||||
boardId: selectedBoard?.id,
|
boardId: selectedBoard?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statuses,
|
statuses,
|
||||||
setStatuses,
|
setStatuses,
|
||||||
updateStatus,
|
|
||||||
refetchStatuses,
|
refetchStatuses,
|
||||||
onCreateStatus,
|
statusesCrud,
|
||||||
onUpdateStatus,
|
|
||||||
onDeleteStatus,
|
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type StatusesContextProviderProps = {
|
export const [StatusesContextProvider, useStatusesContext] =
|
||||||
children: React.ReactNode;
|
makeContext<StatusesContextState>(useStatusesContextState, "Statuses");
|
||||||
};
|
|
||||||
|
|
||||||
export const StatusesContextProvider: FC<StatusesContextProviderProps> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const state = useStatusesContextState();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatusesContext.Provider value={state}>
|
|
||||||
{children}
|
|
||||||
</StatusesContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useStatusesContext = () => {
|
|
||||||
const context = useContext(StatusesContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"useStatusesContext must be used within a StatusesContextProvider"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user