Compare commits

..

135 Commits

Author SHA1 Message Date
36c2a3a2af feat: funnel dnd using pragmatic, not finished groups 2025-10-16 15:26:53 +04:00
fc176ec9e4 fix: fixed border radius 2025-10-14 21:29:44 +04:00
4a4b05769d feat: marketplaces editor in clients page 2025-10-13 12:47:31 +04:00
2052737561 feat: total price and products count display for deals 2025-10-11 16:21:31 +04:00
a899177623 feat: statuses colors 2025-10-11 12:15:03 +04:00
5e56daa765 fix: fixed product editor usage 2025-10-11 09:19:29 +04:00
92602549f8 fix: disable duplicate services when only 1 product added 2025-10-10 23:02:06 +04:00
6351642838 fix: centered project select 2025-10-10 22:55:41 +04:00
4db3c3a1bf fix: centered deal services title 2025-10-10 22:33:44 +04:00
5aa3b4d9e2 fix: hide service type select when editing 2025-10-10 20:57:30 +04:00
73e3fd4ba2 feat: barcodes printing 2025-10-10 20:47:44 +04:00
8af4fcce2f feat: products page 2025-10-08 22:32:16 +04:00
820d9b4d33 fix: fixed redirecting on project select 2025-10-07 11:14:06 +04:00
428a6aca82 feat: actions page for mobiles 2025-10-07 09:48:05 +04:00
7b0b3bc529 feat: schedule view for mobiles 2025-10-06 12:38:07 +04:00
b316cf4f7a feat: table view for mobiles 2025-10-06 09:37:58 +04:00
665625557d feat: navbar buttons depended on selected project 2025-10-05 19:54:02 +04:00
b35961329e refactor: replaced rem with mantine constants 2025-10-05 12:25:14 +04:00
0fcf086861 feat: client tab in deal editor 2025-10-05 12:05:23 +04:00
d14920df7d fix: fixed navbar link tooltips 2025-10-04 19:38:10 +04:00
50ade0e832 fix: fixed client table spacing 2025-10-04 19:25:28 +04:00
e9bfd39ab4 feat: clients page 2025-10-04 18:18:17 +04:00
f641e9ef8c feat: barcode templates page 2025-10-04 10:15:58 +04:00
1a2895da59 feat: services table with dnd 2025-10-03 09:07:02 +04:00
f3a0179467 services table dnd to fix 2025-09-30 23:21:34 +04:00
b51467cbf6 fix: fixed services table header 2025-09-28 12:55:18 +04:00
61f0a9069b feat: actions for services and categories 2025-09-28 12:46:57 +04:00
47533ad7f5 feat: services table, base segmented control 2025-09-27 18:24:22 +04:00
14140826a7 feat: services kits table in service page 2025-09-25 09:36:22 +04:00
a83328492e feat: products and services tabs for mobile 2025-09-23 10:41:55 +04:00
41ff994ad1 fix: ru locale for numbers 2025-09-21 10:18:06 +04:00
6d6c430e88 feat: a few tabs for module, deal services tab for mobiles 2025-09-21 09:47:55 +04:00
6e445d5ebf feat: deal status history table 2025-09-20 10:06:33 +04:00
30e0de5c5e refactor: removed extra folder from modules 2025-09-19 20:13:45 +04:00
de82e639b2 fix: deal service price input fixed 2025-09-19 20:03:52 +04:00
e7416155be fix: total price for deal 2025-09-19 18:15:55 +04:00
05edac23f1 fix: scroll of deal services fixed 2025-09-19 17:22:36 +04:00
9ba22b9bdf fix: styles for deal drawer fixed 2025-09-19 11:55:08 +04:00
e049494fa5 fix: fixed labels in tabs in deal editor 2025-09-19 09:24:49 +04:00
79189bea9a fix: product quantity input and modules dependencies title fixed 2025-09-18 20:37:35 +04:00
053c1da5db fix: dots icons color and dnd border radius fixed 2025-09-18 19:46:59 +04:00
0805a86335 feat: module dependencies 2025-09-18 17:53:26 +04:00
a95d05e28b feat: styles for deal drawer 2025-09-18 09:50:06 +04:00
6b4e2f193a fix: fixed price in productServiceEditorModal 2025-09-17 12:04:33 +04:00
4c5b9c7734 fix: fixed deal service editor modal 2025-09-16 19:51:37 +04:00
681c2c3bc8 feat: total price in product services table 2025-09-16 19:27:00 +04:00
d927da46df feat: adding services kit to deal 2025-09-16 18:13:35 +04:00
553e76d610 feat: modules, products, services, services kits 2025-09-16 10:56:10 +04:00
f2746b8b65 refactor: using isDirty in forms 2025-09-13 09:02:07 +04:00
c76304b7bc fix: fixed status column droppable area 2025-09-10 16:05:25 +04:00
c4381d86c7 fix: fixed input for desktop 2025-09-08 11:13:49 +04:00
0515dd8a49 refactor: deal drawer fixed 2025-09-06 11:29:28 +04:00
d76dc82cb8 refactor: drawers refactored 2025-09-06 11:09:42 +04:00
67780b5251 refactor: styles refactored 2025-09-05 15:55:20 +04:00
0236379898 fix: deals table column width fixed 2025-09-05 14:49:25 +04:00
d0c734d481 refactor: modals refactored 2025-09-05 14:25:36 +04:00
7694b4ae03 refactor: filters modal with context 2025-09-04 14:54:20 +04:00
a5afb03be6 refactor: removed unnecessary view context 2025-09-04 13:00:51 +04:00
dce4dec2f5 refactor: moved logic from columns to table 2025-09-04 12:19:51 +04:00
0be2b8bb6b fix: gap under column for mobiles 2025-09-04 12:05:49 +04:00
96ea0bba5e fix: equal gaps above and under column 2025-09-04 12:03:50 +04:00
6d58add2e7 fix: changes height of create status button 2025-09-04 10:48:11 +04:00
b0e2703479 fix: smoother shadow 2025-09-04 10:35:39 +04:00
018c6a06ea fix: controls hidden in id input in filters 2025-09-04 10:25:33 +04:00
dcf069aa1b fix: theme icon fixed 2025-09-04 10:23:55 +04:00
604238ca43 refactor: theme icon for icon size setting 2025-09-04 09:56:05 +04:00
b5934a7ed2 fix: centered button add status 2025-09-03 14:40:43 +04:00
d69dee7caa fix: fixed swiping of a deal during dragging on mobile 2025-09-03 14:03:19 +04:00
5f621c295b feat: header in deal editor for mobile 2025-09-03 12:17:49 +04:00
9d8ec496a1 fix: dots icons smaller 2025-09-03 11:06:07 +04:00
492b7ac32e fix: fixed status select when deal form cleared 2025-09-02 19:48:22 +04:00
dca7d5f6a5 refactor: refactored filters 2025-09-02 18:19:08 +04:00
72ed69db24 feat: board and status selects in deal editor 2025-09-02 14:41:28 +04:00
a6d8948e9d feat: deals filters indicator and refactoring 2025-09-01 18:50:29 +04:00
48d539154c feat: deals filters 2025-09-01 17:54:31 +04:00
ab7ef1e753 feat: loading and error pages 2025-08-30 23:46:46 +04:00
26c7209de0 refactor: height for a logo 2025-08-30 19:47:04 +04:00
d0948fb583 fix: projects prefetch fixed 2025-08-30 18:04:13 +04:00
db5b886455 fix: dark shadow changed 2025-08-30 15:41:45 +04:00
b363554c46 feat: project editor 2025-08-30 15:31:42 +04:00
1b97739063 feat: project creating 2025-08-30 14:46:56 +04:00
a0522357d4 fix: query invalidating fixed in crud operations 2025-08-30 08:29:37 +04:00
9d3028e4c9 fix: create deal button on mobile fixed 2025-08-29 23:47:43 +04:00
568bd4ad36 fix: only tanstack usage in optimistic updates 2025-08-29 23:39:51 +04:00
8b06d08664 fix: display pages when more than one 2025-08-29 14:45:48 +04:00
50d4705c5e feat: project select width 2025-08-29 14:18:33 +04:00
658d7a2a0e feat: narrowed navbar 2025-08-29 14:06:29 +04:00
3dec614f2a feat: deal ids 2025-08-29 14:01:40 +04:00
9404091d69 fix: shortened datetimes and set background color for navbar 2025-08-29 09:36:15 +04:00
19e5ef2a7e feat: deals table 2025-08-28 20:23:58 +04:00
4323695069 feat: select view buttons 2025-08-28 11:00:41 +04:00
e9b8cdb010 fix: links instead of buttons for navigation 2025-08-27 15:24:45 +04:00
a280f7ad12 fix: closing input on click on swiper 2025-08-27 15:12:13 +04:00
e6001ed59e fix: roboto font, column scrolling fixed, column input width 2025-08-27 14:44:38 +04:00
44766bb7aa fix: boards scrolling with wheel 2025-08-27 09:38:07 +04:00
4a758e4cf0 feat: providers combiner 2025-08-26 16:53:47 +04:00
31bd888357 feat: context factory 2025-08-26 16:11:40 +04:00
5b5c2fe230 feat: ssr prefetch of projects and boards 2025-08-26 12:03:36 +04:00
e0f86f2018 fix: optimized rerenders caused by useList hooks 2025-08-26 10:21:11 +04:00
226e52a1c6 fix: boards gap on mobile fixed 2025-08-25 20:15:31 +04:00
cc5ccf86a4 fix: deal editor drawer size changed 2025-08-25 19:37:53 +04:00
e5602551c5 feat: datetimes with timezones 2025-08-24 14:54:10 +04:00
d5be9ce61a feat: deal create, update, delete 2025-08-24 12:49:19 +04:00
10f50ac254 refactor: sorted hooks 2025-08-23 19:01:21 +04:00
6ad813ea1d refactor: crud objects in contexts 2025-08-23 11:20:32 +04:00
f2084ae3d4 refactor: base crud hook 2025-08-23 10:28:31 +04:00
b105510c23 fix: fixed drawers sorting 2025-08-22 17:32:01 +04:00
b5753ed3a2 feat: drawers registry 2025-08-22 17:04:59 +04:00
cb67c913ad fix: boards rerender optimization 2025-08-21 16:45:04 +04:00
f3df8840df feat: board inplace input in form of tab 2025-08-21 09:08:55 +04:00
d5b6e28311 fix: fixed boards scroll 2025-08-20 23:56:27 +04:00
e3acf3aa89 fix: fixed swiping during deal holding 2025-08-20 22:56:18 +04:00
32ea2aa060 feat: temp shitty fixes to alexender know how to do better 2025-08-20 12:17:19 +03:00
7dba5b5ed9 feat: column scroll offset only when scroll needed 2025-08-19 18:18:02 +04:00
de7e334453 feat: project select in the boards row 2025-08-19 18:11:17 +04:00
179b89c786 fix: create board and status inputs fixed 2025-08-19 12:13:15 +04:00
be034ebbd0 feat: navbar and footer 2025-08-19 11:59:58 +04:00
d3d8c5117b feat: permanent scrollbar in funnel column 2025-08-18 11:52:53 +04:00
0bb546940a refactor: removed unused page block 2025-08-18 11:36:10 +04:00
83432b3f33 feat: boards on desktop as on mobile 2025-08-18 11:35:52 +04:00
49b1a235be feat: scrollable columns with deals 2025-08-18 09:45:54 +04:00
19a386319c feat: mock deal view 2025-08-17 20:28:50 +04:00
3ccebeb123 fix: centered columns in funnel dnd for mobile 2025-08-17 19:35:21 +04:00
e5e87f775d fix: fixed deal dragging from another column to the current 2025-08-17 18:33:05 +04:00
85ed974f5e fix: fixed status button for mobile 2025-08-17 10:48:51 +04:00
92efe3fb66 fix: set default collision detection algorithm for funnel 2025-08-17 10:47:07 +04:00
c405c802aa fix: fixed columns draggables and styles 2025-08-17 10:38:28 +04:00
4ff663536e fix: removed mantine carousel from dependencies 2025-08-16 20:00:48 +04:00
2e9ed02722 feat: swiper for boards on desktop 2025-08-16 19:57:22 +04:00
a4bcd62189 fix: fixed scroll color and width of columns slides on desktop 2025-08-16 19:03:53 +04:00
0a13070d9e feat: swiper 2025-08-16 14:59:37 +04:00
219689b947 feat: selected board style, boards spacing, text font size 2025-08-16 09:20:01 +04:00
3ece4677fb fix: hidden creating statuses when board is not selected 2025-08-15 11:11:29 +04:00
3d213cb0d9 Merge remote-tracking branch 'origin/main' 2025-08-15 11:03:22 +04:00
6d0c48be23 feat: margin for a carousel container 2025-08-15 11:03:06 +04:00
399 changed files with 26309 additions and 2685 deletions

View File

@ -1 +1 @@
NEXT_PUBLIC_API_URL=http://your.api/api
NEXT_PUBLIC_API_URL=http://test.crm.logidex.ru/api

View File

@ -11,9 +11,11 @@ export default defineConfig({
{
name: "zod",
requests: true,
responses: true,
definitions: true,
metadata: true,
dates: {
offset: true,
},
},
{
name: "@hey-api/sdk",

View File

@ -3,18 +3,27 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"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": {
"@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/modifiers": "^9.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/dates": "^8.2.7",
"@mantine/dropzone": "^8.3.1",
"@mantine/form": "^8.1.3",
"@mantine/hooks": "8.1.2",
"@mantine/modals": "^8.2.1",
@ -24,21 +33,28 @@
"@tabler/icons-react": "^3.34.0",
"@tailwindcss/postcss": "^4.1.11",
"@tanstack/react-query": "^5.83.0",
"axios": "^1.11.0",
"@types/react-dom": "19.1.2",
"axios": "1.12.0",
"classnames": "^2.5.1",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dayjs": "^1.11.15",
"framer-motion": "^12.23.7",
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.14.0",
"lexorank": "^1.0.5",
"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-dom": "19.1.0",
"react-imask": "^7.6.1",
"react-redux": "^9.2.0",
"redux-persist": "^6.0.0",
"sharp": "^0.34.3",
"swiper": "^11.2.10",
"zod": "^4.0.14"
},
"devDependencies": {

View 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);
}
}

View 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;

View 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;

View 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
View 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>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>[],
[]
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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: () => "Удаление шаблона штрихкода",
});
};

View 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;

View File

@ -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;

View 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>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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]
);
};

View 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;

View 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");

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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"
);

View File

@ -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;

View File

@ -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>[],
[]
);
};

View File

@ -0,0 +1,3 @@
import ClientMarketplaceDrawer from "./ClientMarketplacesDrawer";
export default ClientMarketplaceDrawer;

View File

@ -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;

View 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: () => "Удаление клиента",
});
};

View 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: () => "Удаление маркетплейса",
});
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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>
);
}

View File

@ -0,0 +1,5 @@
const isValidInn = (inn: string | null | undefined) => {
return inn && inn.match(/^(\d{12}|\d{10})$/);
};
export default isValidInn;

View File

@ -1,7 +0,0 @@
.board {
min-width: 130px;
flex-wrap: nowrap;
gap: 3px;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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%;
}

View File

@ -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;

View File

@ -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;

View File

@ -1,5 +1,5 @@
.board-mobile {
.board {
min-width: 50px;
flex-wrap: nowrap;
gap: 3px;
@ -8,7 +8,7 @@
border-bottom: 2px solid gray;
}
.board-mobile-selected {
.board-selected {
border: 2px solid gray;
border-bottom: 0;
}

View 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;

View File

@ -1,52 +1,48 @@
import React, { FC } from "react";
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
import { Box, Group, Menu, Text } from "@mantine/core";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { Box, Menu } from "@mantine/core";
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
import { BoardSchema } from "@/lib/client";
type Props = {
board: BoardSchema;
startEditing: () => void;
onDeleteBoard: (board: BoardSchema) => void;
isHovered?: boolean;
};
const BoardMenu: FC<Props> = ({ board, startEditing, isHovered = true }) => {
const { selectedBoard, onDeleteBoard } = useBoardsContext();
const BoardMenu: FC<Props> = ({
board,
startEditing,
onDeleteBoard,
isHovered = true,
}) => {
return (
<Menu>
<Menu.Target>
<Box
style={{
opacity:
isHovered || selectedBoard?.id === board.id ? 1 : 0,
opacity: isHovered ? 1 : 0,
cursor: "pointer",
}}
onClick={e => e.stopPropagation()}>
<IconDotsVertical />
<ThemeIcon size={"sm"}>
<IconDotsVertical />
</ThemeIcon>
</Box>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={e => {
e.stopPropagation();
startEditing();
}}>
<Group wrap={"nowrap"}>
<IconEdit />
<Text>Переименовать</Text>
</Group>
</Menu.Item>
<Menu.Item
onClick={e => {
e.stopPropagation();
onDeleteBoard(board);
}}>
<Group wrap={"nowrap"}>
<IconTrash />
<Text>Удалить</Text>
</Group>
</Menu.Item>
<DropdownMenuItem
onClick={startEditing}
icon={<IconEdit />}
label={"Переименовать"}
/>
<DropdownMenuItem
onClick={() => onDeleteBoard(board)}
icon={<IconTrash />}
label={"Удалить"}
/>
</Menu.Dropdown>
</Menu>
);

View 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;
}
}

View 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;

View File

@ -0,0 +1,10 @@
.create-button {
padding: 10px 10px 9px;
cursor: pointer;
}
.spacer {
height: 45px;
width: 100%;
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -1,12 +1,23 @@
.container {
flex-wrap: nowrap;
border-bottom: solid dodgerblue 3px;
margin-bottom: 3px;
cursor: pointer;
height: 100%;
width: fit-content;
@media (max-width: 48em) {
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);
}
}

View File

@ -1,42 +1,50 @@
import React from "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 InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
import useIsMobile from "@/hooks/useIsMobile";
import useIsMobile from "@/hooks/utils/useIsMobile";
import styles from "./CreateStatusButton.module.css";
const CreateStatusButton = () => {
const { onCreateStatus } = useStatusesContext();
const { statusesCrud } = useStatusesContext();
const isMobile = useIsMobile();
return (
<Stack>
<Box className={styles.container}>
<Box className={styles.container}>
<Box
p={isMobile ? "sm" : 0}
className={styles["inner-container"]}>
<InPlaceInput
placeholder={"Название колонки"}
onComplete={onCreateStatus}
onChange={name => statusesCrud.onCreate({ name })}
getChildren={startEditing => (
<Center
p={"sm"}
onClick={() => startEditing()}>
<Group gap={"xs"} wrap={"nowrap"} align={"start"}>
<Group
gap={"xs"}
wrap={"nowrap"}
align={"center"}>
<IconPlus />
{isMobile && (
<Text>Добавить</Text>
)}
{isMobile && <Text>Добавить</Text>}
</Group>
</Center>
)}
modalTitle={"Создание колонки"}
inputStyles={{
wrapper: {
padding: 4,
width: 250,
paddingInline: "var(--mantine-spacing-md)",
paddingBlock: "var(--mantine-spacing-xs)",
},
input: {
width: 250,
},
}}
/>
</Box>
</Stack>
</Box>
);
};

View File

@ -1,9 +1,46 @@
.container {
flex: 1;
padding: 0;
@mixin light {
background-color: var(--color-light-aqua);
background-color: var(--color-light-white-blue);
}
@mixin dark {
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);
}
}

View File

@ -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 { ModuleNames } from "@/modules/modules";
import styles from "./DealCard.module.css";
type Props = {
@ -7,7 +11,63 @@ type 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;

View File

@ -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;

View File

@ -1,89 +1,69 @@
"use client";
import React, { FC, ReactNode } from "react";
import { Group, ScrollArea } from "@mantine/core";
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
import React, { FC } from "react";
import { Box } from "@mantine/core";
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
import useIsMobile from "@/hooks/useIsMobile";
import { DealSchema, StatusSchema } from "@/lib/client";
import { sortByLexorank } from "@/utils/lexorank";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import DndFunnel from "@/components/dnd-pragmatic/DndFunnel/DndFunnel";
import { sortByLexorank } from "@/utils/lexorank/sort";
const Funnel: FC = () => {
const { deals } = useDealsContext();
const isMobile = useIsMobile();
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
const { dealsWithoutGroup, groupsWithDeals, deals, setDeals, dealsCrud } =
useDealsContext();
const {
sortedStatuses,
handleDragStart,
handleDragOver,
handleDragEnd,
activeStatus,
activeDeal,
} = useDealsAndStatusesDnd();
const updateStatus = (statusId: number, lexorank: string) => {
setStatuses(
statuses.map(status =>
status.id === statusId ? { ...status, lexorank } : status
)
);
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={(
status: StatusSchema,
funnelColumnComponent: ReactNode
) => (
<StatusColumnWrapper
status={status}
isDragging={activeStatus?.id === status.id}>
{funnelColumnComponent}
</StatusColumnWrapper>
renderColumnHeader={status => (
<StatusColumnHeader status={status} />
)}
renderItem={(deal: DealSchema) => (
<DealContainer
renderItem={deal => (
<DealCard
key={deal.id}
deal={deal}
/>
)}
activeContainer={activeStatus}
activeItem={activeDeal}
renderItemOverlay={(deal: DealSchema) => <DealCard deal={deal} />}
renderContainerOverlay={(status: StatusSchema, children) => (
<StatusColumnWrapper
status={status}
isDragging>
{children}
</StatusColumnWrapper>
)}
disabledColumns={isMobile}
renderGroup={group => <Box flex={1}>{group.name}</Box>}
/>
);
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;

View File

@ -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;

View 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;

View File

@ -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;

View File

@ -1,13 +1,23 @@
.container {
min-width: 150px;
width: 15vw;
height: calc(100vh - 215px);
@media (max-width: 48em) {
width: 80vw;
}
}
.header {
border-bottom: solid dodgerblue 3px;
.inner-container {
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);
}
}

View File

@ -1,68 +1,41 @@
import React, { ReactNode } from "react";
import { Box, Group, Text } from "@mantine/core";
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
import { Box, ScrollArea, Stack } from "@mantine/core";
import CreateCardButton from "@/app/deals/components/shared/CreateDealButton/CreateDealButton";
import { StatusSchema } from "@/lib/client";
import styles from "./StatusColumnWrapper.module.css";
type Props = {
status: StatusSchema;
isDragging?: boolean;
renderHeader: () => ReactNode;
children: ReactNode;
createFormEnabled?: boolean;
};
const StatusColumnWrapper = ({
status,
renderHeader,
children,
isDragging = false,
createFormEnabled = false,
}: Props) => {
const { onUpdateStatus } = useStatusesContext();
const handleSave = (value: string) => {
const newValue = value.trim();
if (newValue && newValue !== status.name) {
onUpdateStatus(status.id, { name: newValue });
}
};
return (
<Box className={styles.container}>
<Group
justify={"space-between"}
p={"sm"}
wrap={"nowrap"}
mb={"xs"}
className={styles.header}>
<InPlaceInput
defaultValue={status.name}
onComplete={value => handleSave(value)}
inputStyles={{
input: {
height: 25,
minHeight: 25,
},
}}
getChildren={startEditing => (
<>
<Text
style={{
cursor: "grab",
userSelect: "none",
opacity: isDragging ? 0.5 : 1,
}}>
{status.name}
</Text>
<StatusMenu
status={status}
handleEdit={startEditing}
/>
</>
)}
modalTitle={"Редактирование статуса"}
/>
</Group>
{children}
<Stack
px={"xs"}
pb={"xs"}
className={styles["inner-container"]}>
{renderHeader()}
<ScrollArea
offsetScrollbars={"present"}
scrollbarSize={10}
type={"always"}
scrollbars={"y"}>
<Stack
gap={"xs"}
mah={"calc(100vh - 285px)"}>
{createFormEnabled && <CreateCardButton />}
{children}
</Stack>
</ScrollArea>
</Stack>
</Box>
);
};

View File

@ -3,65 +3,98 @@ import {
IconDotsVertical,
IconEdit,
IconExchange,
IconPalette,
IconTrash,
} from "@tabler/icons-react";
import { Box, Group, Menu, Text } from "@mantine/core";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useIsMobile from "@/hooks/useIsMobile";
import { StatusSchema } from "@/lib/client";
import { Box, Menu } from "@mantine/core";
import { modals } from "@mantine/modals";
import statusColors from "@/app/deals/utils/statusColors";
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 = {
status: StatusSchema;
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 { 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 (
<Menu>
<Menu.Target>
<Box
style={{
cursor: "pointer",
}}
style={{ cursor: "pointer" }}
onClick={e => e.stopPropagation()}>
<IconDotsVertical />
<ThemeIcon size={"sm"}>
<IconDotsVertical />
</ThemeIcon>
</Box>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={e => {
e.stopPropagation();
handleEdit();
}}>
<Group wrap={"nowrap"}>
<IconEdit />
<Text>Переименовать</Text>
</Group>
</Menu.Item>
<Menu.Item
onClick={e => {
e.stopPropagation();
onDeleteStatus(status);
}}>
<Group wrap={"nowrap"}>
<IconTrash />
<Text>Удалить</Text>
</Group>
</Menu.Item>
{isMobile && (
<Menu.Item
onClick={e => {
e.stopPropagation();
setIsEditorDrawerOpened(true);
}}>
<Group wrap={"nowrap"}>
<IconExchange />
<Text>Изменить порядок</Text>
</Group>
</Menu.Item>
<DropdownMenuItem
onClick={handleEdit}
icon={<IconEdit />}
label={"Переименовать"}
/>
<DropdownMenuItem
onClick={openStatusColorPicker}
icon={<IconPalette />}
label={"Изменить цвет"}
/>
<DropdownMenuItem
onClick={() => onDeleteStatus(status)}
icon={<IconTrash />}
label={"Удалить"}
/>
{isMobile && withChangeOrderButton && (
<DropdownMenuItem
onClick={openStatusesMobileEditor}
icon={<IconExchange />}
label={"Изменить порядок"}
/>
)}
</Menu.Dropdown>
</Menu>

View 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 />
</>
);

View File

@ -0,0 +1,3 @@
export const ScheduleView = () => {
return <>-</>;
};

View File

@ -0,0 +1,3 @@
import DealsTable from "../DealsTable/DealsTable";
export const TableView = () => <DealsTable />;

View File

@ -0,0 +1,3 @@
export { BoardView } from "./BoardView";
export { TableView } from "./TableView";
export { ScheduleView } from "./ScheduleView";

View File

@ -1,106 +1,53 @@
"use client";
import React, {
createContext,
FC,
useContext,
useEffect,
useState,
} from "react";
import React, { useState } from "react";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import useBoardsList from "@/hooks/useBoardsList";
import { useBoardsOperations } from "@/hooks/useBoardsOperations";
import { BoardSchema, UpdateBoardSchema } from "@/lib/client";
import { BoardsCrud, useBoardsCrud } from "@/hooks/cruds/useBoardsCrud";
import useBoardsList from "@/hooks/lists/useBoardsList";
import { BoardSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
type BoardsContextState = {
boards: BoardSchema[];
setBoards: React.Dispatch<React.SetStateAction<BoardSchema[]>>;
setBoards: (boards: BoardSchema[]) => void;
selectedBoard: BoardSchema | null;
setSelectedBoard: React.Dispatch<React.SetStateAction<BoardSchema | null>>;
setSelectedBoardId: React.Dispatch<React.SetStateAction<number | null>>;
refetchBoards: () => void;
onCreateBoard: (name: string) => void;
onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void;
onDeleteBoard: (board: BoardSchema) => void;
isEditorDrawerOpened: boolean;
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
boardsCrud: BoardsCrud;
};
const BoardsContext = createContext<BoardsContextState | undefined>(undefined);
const useBoardsContextState = () => {
const useBoardsContextState = (): BoardsContextState => {
const { selectedProject: project } = useProjectsContext();
const {
boards,
setBoards,
refetch: refetchBoards,
queryKey,
} = useBoardsList({ projectId: project?.id });
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(
null
);
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
useState<boolean>(false);
useEffect(() => {
if (boards.length > 0 && selectedBoard === null) {
setSelectedBoard(boards[0]);
return;
}
const [selectedBoardId, setSelectedBoardId] = useState<number | null>(null);
const selectedBoard =
boards.find(board => board.id === selectedBoardId) ?? null;
if (!selectedBoard) return;
if (selectedBoard === null && boards.length > 0) {
setSelectedBoardId(boards[0].id);
}
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,
setBoards,
refetchBoards,
projectId: project?.id,
}
);
const boardsCrud = useBoardsCrud({
boards,
queryKey,
projectId: project?.id,
});
return {
boards,
setBoards,
selectedBoard,
setSelectedBoard,
setSelectedBoardId,
refetchBoards,
onCreateBoard,
onUpdateBoard,
onDeleteBoard,
isEditorDrawerOpened,
setIsEditorDrawerOpened,
boardsCrud,
};
};
type BoardsContextProviderProps = {
children: React.ReactNode;
};
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;
};
export const [BoardsContextProvider, useBoardsContext] =
makeContext<BoardsContextState>(useBoardsContextState, "Boards");

View File

@ -1,80 +1,69 @@
"use client";
import React, { createContext, FC, useContext } from "react";
import { useMutation, UseMutationResult } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import useDealsList from "@/hooks/useDealsList";
import {
DealSchema,
HttpValidationError,
Options,
UpdateDealData,
UpdateDealResponse,
} from "@/lib/client";
import { updateDealMutation } from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications";
import React from "react";
import { UseFormReturnType } from "@mantine/form";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useDealsAndGroups from "@/app/deals/hooks/useDealsAndGroups";
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud";
import useDealsList from "@/hooks/lists/useDealsList";
import { SortingForm } from "@/hooks/utils/useSorting";
import { DealSchema, PaginationInfoSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
type DealsContextState = {
deals: DealSchema[];
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
updateDeal: UseMutationResult<
UpdateDealResponse,
AxiosError<HttpValidationError>,
Options<UpdateDealData>
>;
setDeals: (deals: DealSchema[]) => void;
dealsWithoutGroup: DealSchema[];
groupsWithDeals: GroupWithDealsSchema[];
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 { selectedBoard } = useBoardsContext();
const {
deals,
setDeals,
refetch: refetchDeals,
} = useDealsList({ boardId: selectedBoard?.id });
const useDealsContextState = ({
boardId,
projectId,
withPagination = false,
}: Props): DealsContextState => {
const { statuses } = useStatusesContext();
const updateDeal = useMutation({
...updateDealMutation(),
onError: error => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
refetchDeals();
},
const dealsListObjects = useDealsList({
boardId,
projectId,
withPagination,
});
const dealsCrud = useDealsCrud({
...dealsListObjects,
boardId,
statuses,
});
const { dealsWithoutGroup, groupsWithDeals } =
useDealsAndGroups(dealsListObjects);
return {
deals,
setDeals,
updateDeal,
refetchDeals,
...dealsListObjects,
dealsWithoutGroup,
groupsWithDeals,
dealsCrud,
};
};
type DealsContextProviderProps = {
children: React.ReactNode;
};
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;
};
export const [DealsContextProvider, useDealsContext] = makeContext<
DealsContextState,
Props
>(useDealsContextState, "Deals");

View File

@ -1,101 +1,66 @@
"use client";
import React, {
createContext,
FC,
useContext,
useEffect,
useState,
} from "react";
import useProjectsList from "@/hooks/useProjectsList";
import { useProjectsOperations } from "@/hooks/useProjectsOperations";
import { ProjectSchema, UpdateProjectSchema } from "@/lib/client";
import { useMemo, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { ProjectsCrud, useProjectsCrud } from "@/hooks/cruds/useProjectsCrud";
import useProjectsList from "@/hooks/lists/useProjectsList";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ProjectSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
import { ModuleNames } from "@/modules/modules";
type ProjectsContextState = {
selectedProject: ProjectSchema | null;
setSelectedProject: React.Dispatch<
React.SetStateAction<ProjectSchema | null>
>;
setSelectedProjectId: (id: number | null) => void;
refetchProjects: () => void;
projects: ProjectSchema[];
onCreateProject: (name: string) => void;
onUpdateProject: (projectId: number, project: UpdateProjectSchema) => void;
onDeleteProject: (project: ProjectSchema) => void;
isEditorDrawerOpened: boolean;
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
projectsCrud: ProjectsCrud;
modulesSet: Set<ModuleNames>;
};
const ProjectsContext = createContext<ProjectsContextState | undefined>(
undefined
);
const useProjectsContextState = (): ProjectsContextState => {
const { projects, refetch: refetchProjects, queryKey } = useProjectsList();
const isMobile = useIsMobile();
const pathname = usePathname();
const router = useRouter();
const useProjectsContextState = () => {
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
useState<boolean>(false);
const {
projects,
setProjects,
refetch: refetchProjects,
} = useProjectsList();
const [selectedProject, setSelectedProject] =
useState<ProjectSchema | null>(null);
const [selectedProjectId, setSelectedProjectId] = useState<number | null>(
null
);
const selectedProject = useMemo(
() =>
projects.find(project => project.id === selectedProjectId) ?? null,
[projects, selectedProjectId]
);
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 modulesSet = useMemo(
() =>
new Set(
selectedProject?.builtInModules.map(m => m.key as ModuleNames)
),
[selectedProject]
);
const { onCreateProject, onUpdateProject, onDeleteProject } =
useProjectsOperations({
projects,
setProjects,
refetchProjects,
});
if (selectedProject === null && projects.length > 0) {
setSelectedProjectId(projects[0].id);
}
const projectsCrud = useProjectsCrud({ queryKey });
const handleSetSelectedProjectId = (id: number | null) => {
if (!isMobile && pathname !== "/deals") router.push("/deals");
setSelectedProjectId(id);
};
return {
projects,
selectedProject,
setSelectedProject,
onCreateProject,
onUpdateProject,
onDeleteProject,
isEditorDrawerOpened,
setIsEditorDrawerOpened,
refetchProjects,
setSelectedProjectId: handleSetSelectedProjectId,
projectsCrud,
modulesSet,
};
};
type ProjectsContextProviderProps = {
children: React.ReactNode;
};
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;
};
export const [ProjectsContextProvider, useProjectsContext] =
makeContext<ProjectsContextState>(useProjectsContextState, "Projects");

View File

@ -1,118 +1,42 @@
"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 useStatusesList from "@/hooks/useStatusesList";
import { useStatusesOperations } from "@/hooks/useStatusesOperations";
import {
HttpValidationError,
Options,
StatusSchema,
UpdateStatusData,
UpdateStatusResponse,
UpdateStatusSchema,
} from "@/lib/client";
import { updateStatusMutation } from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications";
import { StatusesCrud, useStatusesCrud } from "@/hooks/cruds/useStatusesCrud";
import useStatusesList from "@/hooks/lists/useStatusesList";
import { StatusSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
type StatusesContextState = {
statuses: StatusSchema[];
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
updateStatus: UseMutationResult<
UpdateStatusResponse,
AxiosError<HttpValidationError>,
Options<UpdateStatusData>
>;
setStatuses: (statuses: StatusSchema[]) => void;
refetchStatuses: () => void;
onCreateStatus: (name: string) => void;
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
onDeleteStatus: (status: StatusSchema) => void;
isEditorDrawerOpened: boolean;
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
statusesCrud: StatusesCrud;
};
const StatusesContext = createContext<StatusesContextState | undefined>(
undefined
);
const useStatusesContextState = () => {
const useStatusesContextState = (): StatusesContextState => {
const { selectedBoard } = useBoardsContext();
const {
statuses,
setStatuses,
refetch: refetchStatuses,
queryKey,
} = useStatusesList({
boardId: selectedBoard?.id,
});
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
useState<boolean>(false);
useEffect(() => {
refetchStatuses();
}, [selectedBoard]);
const updateStatus = useMutation({
...updateStatusMutation(),
onError: error => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
refetchStatuses();
},
const statusesCrud = useStatusesCrud({
statuses,
queryKey,
boardId: selectedBoard?.id,
});
const { onCreateStatus, onUpdateStatus, onDeleteStatus } =
useStatusesOperations({
statuses,
setStatuses,
refetchStatuses,
boardId: selectedBoard?.id,
});
return {
statuses,
setStatuses,
updateStatus,
refetchStatuses,
onCreateStatus,
onUpdateStatus,
onDeleteStatus,
isEditorDrawerOpened,
setIsEditorDrawerOpened,
statusesCrud,
};
};
type StatusesContextProviderProps = {
children: React.ReactNode;
};
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;
};
export const [StatusesContextProvider, useStatusesContext] =
makeContext<StatusesContextState>(useStatusesContextState, "Statuses");

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