186 Commits

Author SHA1 Message Date
44b89245f0 fix: added react-dom dep 2025-11-07 11:28:15 +04:00
b0eab6cce7 feat: create first project modal 2025-11-07 10:13:32 +04:00
1a1f584b81 feat: header in module editor page for mobiles 2025-11-06 12:23:54 +04:00
cc910c8495 feat: modules page and module editor page for mobiles 2025-11-06 12:09:27 +04:00
d3270a3532 feat: dnd for options in select editor 2025-11-05 20:52:08 +04:00
38ae35795b fix: fixed attributes page header 2025-11-04 16:22:44 +04:00
3b1aca2df1 fix: product view barcodes display fixed 2025-11-04 16:05:11 +04:00
4d3264a92f fix: fixed options editor 2025-11-04 12:51:06 +04:00
33dd1e1c0f feat: attribute selects and options editors 2025-11-04 12:19:44 +04:00
311210394f fix: fixed showing default option label 2025-11-03 10:42:13 +04:00
03be3903cb feat: attributes page 2025-11-02 16:07:49 +04:00
8020561da6 fix: showing only unused attributes in the left part of module editor 2025-11-02 12:39:08 +04:00
a383f218f1 refactor: refactored useAttributesActions hook 2025-11-02 12:21:55 +04:00
fd5e878c29 fix: fixed deal module attributes refetch after module editing 2025-11-02 11:26:17 +04:00
e8ffafa6c5 fix: fixed attribute creating 2025-11-01 23:17:43 +04:00
95cc7f6259 fix: fixed height of swiper on mobiles 2025-11-01 16:05:57 +04:00
69bf7848e1 feat: deal barcodes printing, refactored ff module 2025-11-01 14:15:11 +04:00
4370684c58 fix: fixed ui of deal ff info editor 2025-11-01 09:46:46 +04:00
2948189291 feat: ff deal attributes editor 2025-10-31 20:54:04 +04:00
ee90ebe0f0 fix: fixed inner attr table header overlaying on outer table header 2025-10-31 17:52:57 +04:00
418e4b6b63 fix: fixed ActionIcon bg for mobiles 2025-10-31 17:49:36 +04:00
3355c73b8b fix: fixed statuses mobile editor 2025-10-31 17:45:14 +04:00
07736ee374 feat: mobile drawer editors ui enhancement 2025-10-31 17:32:20 +04:00
42f1c75603 fix: scrollable table in tags table for mobiles 2025-10-31 16:04:23 +04:00
4342ab17ed fix: try-catch in prefetching function for correct building 2025-10-30 20:58:43 +04:00
e9c9f6b65d fix: fixed client utils generation 2025-10-30 20:52:24 +04:00
d38454f3af fix: added dayjs pkg 2025-10-30 20:05:27 +04:00
0c2fca2c23 fix: product barcode image showing 2025-10-30 15:41:13 +04:00
9207bd915d fix: left only button when deal services table is empty on mobiles 2025-10-30 14:04:48 +04:00
da769fa2c0 refactor: refactored product services table 2025-10-30 13:52:57 +04:00
4cc6360bb4 feat: deal attributes with select and options 2025-10-29 19:36:58 +04:00
8019fa566c feat: setting default attributes after deal creating 2025-10-28 17:21:21 +04:00
ea6a6df371 fix: applied timezone to default values, removed value nesting 2025-10-28 11:43:13 +04:00
3575b9f34a fix: removed attr is_shown_on_dashboard 2025-10-27 17:31:48 +04:00
71c9d08b8c fix: removed unused dayjs 2025-10-27 10:06:08 +04:00
d4c0eac4a0 feat: deal attributes editing 2025-10-27 10:05:29 +04:00
e39df47520 refactor: refactored products utils 2025-10-25 19:35:00 +04:00
d9da3d1bc5 feat: product info in product select 2025-10-25 19:25:58 +04:00
5b754865cf feat: modules creation 2025-10-25 18:05:49 +04:00
2bdbebc453 feat: modules and module-editor pages 2025-10-25 12:11:14 +04:00
57a7ab0871 refactor: renamed built_in_modules into modules 2025-10-21 12:34:42 +04:00
4d5723bc72 feat: product barcode images 2025-10-21 11:10:27 +04:00
82f08b4f83 feat: product images upload and display 2025-10-20 16:13:05 +04:00
8cc11bca67 feat: barcodes display and printing in deal editor 2025-10-19 21:00:38 +04:00
e44691d118 feat: tags page for mobiles 2025-10-19 20:47:53 +04:00
3a1d8e23e3 feat: deal tags 2025-10-19 12:12:28 +04:00
9023b07c65 refactor: moved ProjectEditorDrawer into common drawers directory 2025-10-18 13:06:37 +04:00
159d6948c7 feat: groups creating on mobile 2025-10-18 10:23:13 +04:00
f90b335ee1 feat: context menu for deal groups 2025-10-18 00:33:12 +04:00
5e59d54011 feat: fixed group name input 2025-10-18 00:26:31 +04:00
f117605ea3 feat: routers client and version prefixes 2025-10-17 21:41:20 +04:00
30bc7bbee4 feat: creating and updating groups 2025-10-17 19:47:47 +04:00
daa9d12983 fix: fixed swiping slider during holding group 2025-10-17 12:02:07 +04:00
0fe41656f8 feat: displaying and sorting groups of deals 2025-10-17 11:52:19 +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
493 changed files with 34959 additions and 2478 deletions

View File

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

25
fix-client.ts Normal file
View File

@ -0,0 +1,25 @@
import * as fs from "fs";
const zodPath = "src/lib/client/zod.gen.ts";
let content = fs.readFileSync(zodPath, "utf8");
// Replace only for the upload schema
const zodTarget = "upload_file: z.string";
while (content.includes(zodTarget)) {
content = content.replace(zodTarget, "upload_file: z.any");
}
fs.writeFileSync(zodPath, content);
console.log("✅ Fixed zod schema for upload_file");
const utilsPath = "src/lib/client/client/utils.ts";
content = fs.readFileSync(utilsPath, "utf8");
const utilsTarget = "@ts-expect-error";
while (content.includes(utilsTarget)) {
content = content.replace(utilsTarget, "@ts-ignore");
}
content = content.replace(
"...(mergedHeaders[key] ?? []),",
"...(mergedHeaders[key] ?? []) as any,"
);
fs.writeFileSync(utilsPath, content);
console.log("✅ Fixed utils.ts");

View File

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

View File

@ -3,18 +3,21 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev --turbo",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client" "generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client & sudo npx tsc fix-client.ts && mv -f ./fix-client.js ./fix-client.cjs && sudo node ./fix-client.cjs",
"generate-modules": "sudo npx tsc ./src/modules/modulesFileGen/modulesFileGen.ts && mv -f ./src/modules/modulesFileGen/modulesFileGen.js ./src/modules/modulesFileGen/modulesFileGen.cjs && sudo node ./src/modules/modulesFileGen/modulesFileGen.cjs"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@mantine/carousel": "^8.2.4", "@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "8.1.2", "@mantine/core": "8.1.2",
"@mantine/dates": "^8.2.7",
"@mantine/dropzone": "^8.3.1",
"@mantine/form": "^8.1.3", "@mantine/form": "^8.1.3",
"@mantine/hooks": "8.1.2", "@mantine/hooks": "8.1.2",
"@mantine/modals": "^8.2.1", "@mantine/modals": "^8.2.1",
@ -24,21 +27,28 @@
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.34.0",
"@tailwindcss/postcss": "^4.1.11", "@tailwindcss/postcss": "^4.1.11",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^5.83.0",
"axios": "^1.11.0", "axios": "1.12.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"embla-carousel": "^8.6.0", "clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dayjs": "^1.11.18",
"framer-motion": "^12.23.7", "framer-motion": "^12.23.7",
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.14.0", "i18n-iso-countries": "^7.14.0",
"lexorank": "^1.0.5", "lexorank": "^1.0.5",
"libphonenumber-js": "^1.12.10", "libphonenumber-js": "^1.12.10",
"next": "15.3.3", "mantine-contextmenu": "^8.2.0",
"mantine-datatable": "^8.2.0",
"next": "15.4.7",
"phone": "^3.1.67",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "^19.2.0",
"react-imask": "^7.6.1", "react-imask": "^7.6.1",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"sharp": "^0.34.3", "sharp": "^0.34.3",
"swiper": "^11.2.10",
"zod": "^4.0.14" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
@ -59,6 +69,7 @@
"@types/lodash": "^4.17.20", "@types/lodash": "^4.17.20",
"@types/node": "^22.13.11", "@types/node": "^22.13.11",
"@types/react": "19.1.8", "@types/react": "19.1.8",
"@types/react-dom": "^19",
"@types/react-redux": "^7.1.34", "@types/react-redux": "^7.1.34",
"@types/react-slick": "^0", "@types/react-slick": "^0",
"@types/redux-persist": "^4.3.1", "@types/redux-persist": "^4.3.1",

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,71 @@
"use client";
import { RefObject, useMemo, useRef } from "react";
import { IconApps, IconList, IconTag } from "@tabler/icons-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";
import BuiltInLinkData from "@/types/BuiltInLinkData";
const PageBody = () => {
const { selectedProject, setSelectedProjectId, projects, modulesSet } =
useProjectsContext();
const filteredMobileButtonsData = useMemo(
() =>
mobileButtonsData.filter(
link => !link.moduleName || modulesSet.has(link.moduleName)
),
[modulesSet]
);
const commonActionsData: RefObject<BuiltInLinkData[]> = useRef([
{
icon: IconList,
label: "Атрибуты",
href: "/attributes",
},
{
icon: IconApps,
label: "Модули",
href: "/modules",
},
{
icon: IconTag,
label: "Теги",
href: "/tags",
},
]);
return (
<PageBlock fullScreenMobile>
<Stack p={"xs"}>
<ProjectSelect
onChange={project =>
setSelectedProjectId(project?.id ?? null)
}
value={selectedProject}
data={projects}
/>
<SimpleGrid
type={"container"}
cols={2}>
{[
...commonActionsData.current,
...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,27 @@
import { FC } from "react";
import AttributePageView from "@/app/attributes/types/view";
import BaseSegmentedControl, {
BaseSegmentedControlProps,
} from "@/components/ui/BaseSegmentedControl/BaseSegmentedControl";
type Props = Omit<BaseSegmentedControlProps<AttributePageView>, "data">;
const data = [
{
label: "Аттрибуты",
value: AttributePageView.ATTRIBUTES,
},
{
label: "Справочники",
value: AttributePageView.SELECTS,
},
];
const AttrViewSegmentedControl: FC<Props> = props => (
<BaseSegmentedControl
data={data}
{...props}
/>
);
export default AttrViewSegmentedControl;

View File

@ -0,0 +1,87 @@
"use client";
import { Dispatch, FC, SetStateAction, useMemo } from "react";
import { Divider, Flex, Group, TextInput } from "@mantine/core";
import AttrViewSegmentedControl from "@/app/attributes/components/AttrViewSegmentedControl";
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
import AttributePageView from "@/app/attributes/types/view";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
import useIsMobile from "@/hooks/utils/useIsMobile";
type Props = {
view: AttributePageView;
setView: Dispatch<SetStateAction<AttributePageView>>;
};
const AttributesHeader: FC<Props> = ({ view, setView }) => {
const {
attributesActions,
selectsActions,
attrSearch,
setAttrSearch,
selectSearch,
setSelectSearch,
} = useAttributesContext();
const isMobile = useIsMobile();
const attributeActions = useMemo(
() => (
<Group wrap={"nowrap"}>
<InlineButton
onClick={attributesActions.onCreate}
w={isMobile ? "100%" : "auto"}>
Создать атрибут
</InlineButton>
<TextInput
value={attrSearch}
onChange={e => setAttrSearch(e.currentTarget.value)}
w={isMobile ? "100%" : "auto"}
placeholder={"Поиск..."}
/>
</Group>
),
[isMobile, attrSearch]
);
const selectActions = useMemo(
() => (
<Group wrap={"nowrap"}>
<InlineButton
onClick={selectsActions.onCreate}
w={isMobile ? "100%" : "auto"}>
Создать справочник
</InlineButton>
<TextInput
value={selectSearch}
onChange={e => setSelectSearch(e.currentTarget.value)}
w={isMobile ? "100%" : "auto"}
placeholder={"Поиск..."}
/>
</Group>
),
[isMobile, selectSearch]
);
return (
<Flex
wrap={"nowrap"}
gap={"xs"}
align={isMobile ? "unset" : "center"}
mt={isMobile ? "xs" : ""}
mx={isMobile ? "xs" : ""}
direction={isMobile ? "column-reverse" : "row"}
justify={"space-between"}>
{view === AttributePageView.ATTRIBUTES
? attributeActions
: selectActions}
{isMobile && <Divider />}
<AttrViewSegmentedControl
value={view}
onChange={setView}
styles={{ root: { margin: 0, padding: 0 } }}
/>
</Flex>
);
};
export default AttributesHeader;

View File

@ -0,0 +1,40 @@
"use client";
import { FC } from "react";
import { IconMoodSad } from "@tabler/icons-react";
import { Group, Text } from "@mantine/core";
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
import useAttributesTableColumns from "@/app/attributes/hooks/useAttributesTableColumns";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
import useIsMobile from "@/hooks/utils/useIsMobile";
const AttributesTable: FC = () => {
const isMobile = useIsMobile();
const { attributes } = useAttributesContext();
const columns = useAttributesTableColumns();
return (
<BaseTable
withTableBorder
columns={columns}
records={attributes}
verticalSpacing={"md"}
emptyState={
<Group mt={attributes.length === 0 ? "xl" : 0}>
<Text>Нет атрибутов</Text>
<IconMoodSad />
</Group>
}
groups={undefined}
styles={{
table: {
width: "100%",
},
header: { zIndex: 1 },
}}
mx={isMobile ? "xs" : 0}
/>
);
};
export default AttributesTable;

View File

@ -0,0 +1,42 @@
"use client";
import { useState } from "react";
import AttributesHeader from "@/app/attributes/components/AttributesHeader";
import AttributesTable from "@/app/attributes/components/AttributesTable";
import SelectsTable from "@/app/attributes/components/SelectsTable";
import AttributePageView from "@/app/attributes/types/view";
import PageBlock from "@/components/layout/PageBlock/PageBlock";
const PageBody = () => {
const [view, setView] = useState<AttributePageView>(
AttributePageView.ATTRIBUTES
);
return (
<PageBlock
style={{ flex: 1, minHeight: 0 }}
fullScreenMobile>
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
gap: "var(--mantine-spacing-md)",
}}>
<AttributesHeader
view={view}
setView={setView}
/>
<div style={{ flex: 1, overflow: "auto" }}>
{view === AttributePageView.ATTRIBUTES ? (
<AttributesTable />
) : (
<SelectsTable />
)}
</div>
</div>
</PageBlock>
);
};
export default PageBody;

View File

@ -0,0 +1,36 @@
import { IconMoodSad } from "@tabler/icons-react";
import { Group, Text } from "@mantine/core";
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
import useSelectsTableColumns from "@/app/attributes/hooks/useSelectsTableColumns";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
import useIsMobile from "@/hooks/utils/useIsMobile";
const SelectsTable = () => {
const { selects } = useAttributesContext();
const isMobile = useIsMobile();
const columns = useSelectsTableColumns();
return (
<BaseTable
withTableBorder
columns={columns}
records={selects}
verticalSpacing={"md"}
emptyState={
<Group mt={selects.length === 0 ? "xl" : 0}>
<Text>Нет справочников</Text>
<IconMoodSad />
</Group>
}
groups={undefined}
styles={{
table: {
width: "100%",
},
}}
mx={isMobile ? "xs" : 0}
/>
);
};
export default SelectsTable;

View File

@ -0,0 +1,62 @@
"use client";
import { Dispatch, SetStateAction } from "react";
import useFilteredAttributes from "@/app/attributes/hooks/useFilteredAttributes";
import useSelectsActions, {
SelectsActions,
} from "@/app/attributes/hooks/useSelectsActions";
import useAttributesActions, {
AttributesActions,
} from "@/app/module-editor/[moduleId]/hooks/useAttributesActions";
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
import useAttrSelectsList from "@/hooks/lists/useAttrSelectsList";
import { AttributeSchema, AttrSelectSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
import useFilteredSelects from "@/app/attributes/hooks/useFilteredSelects";
type AttributesContextState = {
attributes: AttributeSchema[];
attributesActions: AttributesActions;
attrSearch: string;
setAttrSearch: Dispatch<SetStateAction<string>>;
selects: AttrSelectSchema[];
selectsActions: SelectsActions;
selectSearch: string;
setSelectSearch: Dispatch<SetStateAction<string>>;
};
const useAttributesContextState = (): AttributesContextState => {
const { attributes, refetch: refetchAttributes } = useAttributesList();
const attributesActions = useAttributesActions({
refetchAttributes,
});
const {
search: attrSearch,
setSearch: setAttrSearch,
filteredAttributes,
} = useFilteredAttributes({ attributes });
const { selects, queryKey } = useAttrSelectsList();
const selectsActions = useSelectsActions({ queryKey });
const {
search: selectSearch,
setSearch: setSelectSearch,
filteredSelects,
} = useFilteredSelects({ selects });
return {
attributes: filteredAttributes,
attributesActions,
attrSearch,
setAttrSearch,
selects: filteredSelects,
selectsActions,
selectSearch,
setSelectSearch,
};
};
export const [AttributesContextProvider, useAttributesContext] =
makeContext<AttributesContextState>(useAttributesContextState, "Attribute");

View File

@ -0,0 +1,49 @@
"use client";
import React, { FC } from "react";
import { Drawer } from "@mantine/core";
import EditorBody from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/EditorBody";
import { SelectEditorContextProvider } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import { DrawerProps } from "@/drawers/types";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { AttrSelectSchema, UpdateAttrSelectSchema } from "@/lib/client";
type Props = {
select: AttrSelectSchema;
onSelectChange: (
values: UpdateAttrSelectSchema,
onSuccess: () => void
) => void;
};
const AttrSelectEditorDrawer: FC<DrawerProps<Props>> = ({
onClose,
opened,
props,
}) => {
const isMobile = useIsMobile();
return (
<Drawer
size={isMobile ? "100%" : "30%"}
title={"Редактирование справочника"}
position={"left"}
onClose={onClose}
removeScrollProps={{ allowPinchZoom: true }}
withCloseButton
opened={opened}
trapFocus={false}
styles={{
body: {
display: "flex",
flexDirection: "column",
},
}}>
<SelectEditorContextProvider {...props}>
<EditorBody />
</SelectEditorContextProvider>
</Drawer>
);
};
export default AttrSelectEditorDrawer;

View File

@ -0,0 +1,38 @@
import { Button, Flex, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import { UpdateAttrSelectSchema } from "@/lib/client";
const CommonInfoEditor = () => {
const { select, onSelectChange } = useSelectEditorContext();
const form = useForm<UpdateAttrSelectSchema>({
initialValues: select || {
name: "",
},
validate: {
name: name => !name && "Введите название",
},
});
return (
<form onSubmit={form.onSubmit(values => onSelectChange(values))}>
<Flex
gap={"xs"}
direction={"column"}>
<TextInput
label={"Название справочника"}
{...form.getInputProps("name")}
data-autofocus
/>
<Button
variant={"default"}
type={"submit"}>
Сохранить
</Button>
</Flex>
</form>
);
};
export default CommonInfoEditor;

View File

@ -0,0 +1,47 @@
import { IconCheck } from "@tabler/icons-react";
import { Flex, TextInput } from "@mantine/core";
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
const CreateOptionButton = () => {
const {
optionsActions: {
isCreatingOption,
createOptionForm,
onStartCreating,
onFinishCreating,
},
} = useSelectEditorContext();
if (!isCreatingOption) {
return (
<Flex flex={1}>
<InlineButton
fullWidth
onClick={onStartCreating}>
Добавить опцию
</InlineButton>
</Flex>
);
}
return (
<Flex
gap={"xs"}
flex={1}>
<TextInput
{...createOptionForm.getInputProps("name")}
flex={1}
placeholder={"Название"}
/>
<ActionIconWithTip
tipLabel={"Сохранить"}
onClick={onFinishCreating}>
<IconCheck />
</ActionIconWithTip>
</Flex>
);
};
export default CreateOptionButton;

View File

@ -0,0 +1,22 @@
import { Divider, Flex } from "@mantine/core";
import CommonInfoEditor from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/CommonInfoEditor";
import CreateOptionButton from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/CreateOptionButton";
import OptionsTable from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/OptionsTable";
const EditorBody = () => {
return (
<Flex
gap={"xs"}
direction={"column"}>
<CommonInfoEditor />
<Divider
label={"Опции"}
my={"xs"}
/>
<CreateOptionButton />
<OptionsTable />
</Flex>
);
};
export default EditorBody;

View File

@ -0,0 +1,82 @@
import React, { FC, ReactNode } from "react";
import { IconCheck, IconEdit, IconTrash } from "@tabler/icons-react";
import { Divider, Flex, Group, Stack, TextInput } from "@mantine/core";
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import { AttrOptionSchema } from "@/lib/client";
type Props = {
option: AttrOptionSchema;
renderDraggable?: (item: AttrOptionSchema) => ReactNode;
};
const OptionTableRow: FC<Props> = ({ option, renderDraggable }) => {
const {
optionsActions: {
onStartEditing,
onFinishEditing,
onDelete,
editingOptionsData,
setEditingOptionsData,
},
} = useSelectEditorContext();
const onChange = (
e: React.ChangeEvent<HTMLInputElement>,
optionId: number
) => {
setEditingOptionsData(prev => {
prev.set(optionId, e.currentTarget.value);
return new Map(prev);
});
};
return (
<Stack
gap={"xs"}
mt={"xs"}>
<Group
wrap={"nowrap"}
justify={"space-between"}>
<Group wrap={"nowrap"}>
{renderDraggable && renderDraggable(option)}
{editingOptionsData.has(option.id) ? (
<TextInput
value={editingOptionsData.get(option.id)}
onChange={e => onChange(e, option.id)}
/>
) : (
option.name
)}
</Group>
<Flex
justify={"center"}
gap={"xs"}>
{editingOptionsData.has(option.id) ? (
<ActionIconWithTip
onClick={() => onFinishEditing(option)}
tipLabel={"Сохранить"}>
<IconCheck />
</ActionIconWithTip>
) : (
<ActionIconWithTip
onClick={() => onStartEditing(option)}
tipLabel={"Редактировать"}>
<IconEdit />
</ActionIconWithTip>
)}
<ActionIconWithTip
color={"red"}
onClick={() => onDelete(option)}
tipLabel={"Удалить"}>
<IconTrash />
</ActionIconWithTip>
</Flex>
</Group>
<Divider />
</Stack>
);
};
export default OptionTableRow;

View File

@ -0,0 +1,38 @@
import React from "react";
import { IconGripVertical } from "@tabler/icons-react";
import { Box, Divider, Stack } from "@mantine/core";
import OptionTableRow from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/OptionTableRow";
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import SortableDnd from "@/components/dnd/SortableDnd";
const OptionsTable = () => {
const { options } = useSelectEditorContext();
const { onDragEnd } = useSelectEditorContext();
const renderDraggable = () => (
<Box p={"xs"}>
<IconGripVertical />
</Box>
);
return (
<Stack gap={0}>
<Divider />
<SortableDnd
initialItems={options}
onDragEnd={onDragEnd}
renderItem={(item, renderDraggable) => (
<OptionTableRow
option={item}
renderDraggable={renderDraggable}
/>
)}
renderDraggable={renderDraggable}
dragHandleStyle={{ width: "auto" }}
vertical
/>
</Stack>
);
};
export default OptionsTable;

View File

@ -0,0 +1,62 @@
"use client";
import useAttrOptionsList from "@/app/attributes/drawers/AttrSelectEditorDrawer/hooks/useAttrOptionsList";
import {
AttrOptionSchema,
AttrSelectSchema,
UpdateAttrSelectSchema,
} from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
import { notifications } from "@/lib/notifications";
import useOptionsActions, { OptionsActions } from "../hooks/useOptionsActions";
type SelectEditorContextState = {
select: AttrSelectSchema;
onSelectChange: (values: UpdateAttrSelectSchema) => void;
options: AttrOptionSchema[];
optionsActions: OptionsActions;
onDragEnd: (itemId: number, newLexorank: string) => void;
};
type Props = {
select: AttrSelectSchema;
onSelectChange: (
values: UpdateAttrSelectSchema,
onSuccess: () => void
) => void;
};
const useSelectEditorContextState = ({
select,
onSelectChange,
}: Props): SelectEditorContextState => {
const { options, queryKey } = useAttrOptionsList({ selectId: select.id });
const optionsActions = useOptionsActions({ queryKey, select, options });
const onSelectChangeWithMsg = (values: UpdateAttrSelectSchema) => {
onSelectChange(values, () => {
notifications.success({
message: "Название справочника сохранено",
});
});
};
const onDragEnd = (itemId: number, newLexorank: string) => {
optionsActions.onUpdate(itemId, { lexorank: newLexorank });
};
return {
select,
onSelectChange: onSelectChangeWithMsg,
options,
optionsActions,
onDragEnd,
};
};
export const [SelectEditorContextProvider, useSelectEditorContext] =
makeContext<SelectEditorContextState, Props>(
useSelectEditorContextState,
"SelectEditor"
);

View File

@ -0,0 +1,68 @@
import { LexoRank } from "lexorank";
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
import {
AttrOptionSchema,
CreateAttrOptionSchema,
UpdateAttrOptionSchema,
} from "@/lib/client";
import {
createAttrOptionMutation,
deleteAttrOptionMutation,
updateAttrOptionMutation,
} from "@/lib/client/@tanstack/react-query.gen";
import { getNewLexorank } from "@/utils/lexorank/generation";
import { getMaxByLexorank } from "@/utils/lexorank/max";
type Props = {
queryKey: any[];
options: AttrOptionSchema[];
};
export type AttrOptionsCrud = {
onCreate: (
data: Partial<CreateAttrOptionSchema>,
onSuccess?: () => void
) => void;
onUpdate: (
optionId: number,
option: UpdateAttrOptionSchema,
onSuccess?: () => void
) => void;
onDelete: (option: AttrOptionSchema, onSuccess?: () => void) => void;
};
export const useAttrOptionsCrud = ({
queryKey,
options,
}: Props): AttrOptionsCrud => {
return useCrudOperations<
AttrOptionSchema,
UpdateAttrOptionSchema,
CreateAttrOptionSchema
>({
key: "getAttrOptions",
queryKey,
mutations: {
create: createAttrOptionMutation(),
update: updateAttrOptionMutation(),
delete: deleteAttrOptionMutation(),
},
getCreateEntity: data => {
const lastOption = getMaxByLexorank(options);
const newLexorank = getNewLexorank(
lastOption ? LexoRank.parse(lastOption.lexorank) : null
);
return {
name: data.name!,
selectId: data.selectId!,
lexorank: newLexorank.toString(),
};
},
getUpdateEntity: (old, update) => ({
...old,
name: update.name ?? old.name,
lexorank: update.lexorank ?? old.lexorank,
}),
getDeleteConfirmTitle: () => "Удаление опции",
});
};

View File

@ -0,0 +1,38 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { AttrOptionSchema } from "@/lib/client";
import {
getAttrOptionsOptions,
getAttrOptionsQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
import { sortByLexorank } from "@/utils/lexorank/sort";
type Props = {
selectId: number;
};
const useAttrOptionsList = ({ selectId }: Props) => {
const queryClient = useQueryClient();
const options = { path: { selectId } };
const { data, refetch } = useQuery(getAttrOptionsOptions(options));
const queryKey = getAttrOptionsQueryKey(options);
const setOptions = (options: AttrOptionSchema[]) => {
queryClient.setQueryData(
queryKey,
(old: { items: AttrOptionSchema[] }) => ({
...old,
items: options,
})
);
};
return {
options: sortByLexorank(data?.items ?? []),
setOptions,
refetch,
queryKey,
};
};
export default useAttrOptionsList;

View File

@ -0,0 +1,107 @@
import { Dispatch, SetStateAction, useState } from "react";
import { useForm, UseFormReturnType } from "@mantine/form";
import {
AttrOptionSchema,
AttrSelectSchema,
CreateAttrOptionSchema,
UpdateAttrOptionSchema,
} from "@/lib/client";
import { notifications } from "@/lib/notifications";
import { useAttrOptionsCrud } from "@/app/attributes/drawers/AttrSelectEditorDrawer/hooks/useAttrOptionsCrud";
type Props = {
queryKey: any[];
select: AttrSelectSchema;
options: AttrOptionSchema[];
};
export type OptionsActions = {
isCreatingOption: boolean;
createOptionForm: UseFormReturnType<CreateAttrOptionSchema>;
onStartCreating: () => void;
onFinishCreating: () => void;
editingOptionsData: Map<number, string>;
setEditingOptionsData: Dispatch<SetStateAction<Map<number, string>>>;
onStartEditing: (option: AttrOptionSchema) => void;
onFinishEditing: (option: AttrOptionSchema) => void;
onUpdate: (optionId: number, data: UpdateAttrOptionSchema) => void;
onDelete: (option: AttrOptionSchema) => void;
};
const useOptionsActions = ({ queryKey, select, options }: Props) => {
const [isCreatingOption, setIsCreatingOption] = useState<boolean>(false);
const [editingOptionsData, setEditingOptionsData] = useState<
Map<number, string>
>(new Map());
const createOptionForm = useForm<CreateAttrOptionSchema>({
initialValues: {
name: "",
lexorank: "",
selectId: select.id,
},
validate: {
name: name => !name && "Введите название",
},
});
const optionCrud = useAttrOptionsCrud({ queryKey, options });
const onStartCreating = () => {
setIsCreatingOption(true);
};
const onFinishCreating = () => {
if (createOptionForm.validate().hasErrors) return;
optionCrud.onCreate(createOptionForm.values, () => {
notifications.success({ message: "Опция успешно создана" });
createOptionForm.reset();
setIsCreatingOption(false);
});
};
const onStartEditing = (option: AttrOptionSchema) => {
setEditingOptionsData(prev => {
prev.set(option.id, option.name);
return new Map(prev);
});
};
const onFinishEditing = (option: AttrOptionSchema) => {
if (!editingOptionsData.has(option.id)) return;
const newName = editingOptionsData.get(option.id);
if (!newName) {
notifications.error({ message: "Название не может быть пустым" });
return;
}
optionCrud.onUpdate(option.id, { ...option, name: newName }, () => {
notifications.success({ message: "Опция сохранена" });
setEditingOptionsData(prev => {
prev.delete(option.id);
return new Map(prev);
});
});
};
const onDelete = (option: AttrOptionSchema) => {
optionCrud.onDelete(option, () =>
notifications.success({ message: "Опция удалена" })
);
};
const onUpdate = optionCrud.onUpdate;
return {
isCreatingOption,
createOptionForm,
onStartCreating,
onFinishCreating,
editingOptionsData,
setEditingOptionsData,
onStartEditing,
onFinishEditing,
onDelete,
onUpdate,
};
};
export default useOptionsActions;

View File

@ -0,0 +1,72 @@
"use client";
import { useMemo } from "react";
import { IconCheck, IconX } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable";
import { Box, Center } from "@mantine/core";
import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions";
import AttributeDefaultValue from "@/components/ui/AttributeDefaultValue/AttributeDefaultValue";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { AttributeSchema } from "@/lib/client";
import { useAttributesContext } from "../contexts/AttributesContext";
const useAttributesTableColumns = () => {
const isMobile = useIsMobile();
const { attributesActions } = useAttributesContext();
const renderCheck = (value: boolean) => (value ? <IconCheck /> : <IconX />);
return useMemo(
() =>
[
{
title: "Название атрибута",
accessor: "label",
},
{
title: "Тип",
accessor: "type.name",
render: attr =>
attr.type.type === "select"
? `Выбор "${attr.label}"`
: attr.type.name,
},
{
title: "Значение по умолчанию",
accessor: "defaultValue",
render: attr => <AttributeDefaultValue attribute={attr} />,
},
{
title: isMobile
? "Синх. в группе"
: "Синхронизировано в группе",
accessor: "isApplicableToGroup",
render: attr => renderCheck(attr.isApplicableToGroup),
},
{
title: "Может быть пустым",
accessor: "isNullable",
render: attr => renderCheck(attr.isNullable),
},
{
title: "Описаниие",
accessor: "description",
render: attr => <Box>{attr.description}</Box>,
},
{
accessor: "actions",
title: <Center>Действия</Center>,
width: "0%",
render: attribute => (
<AttributeTableActions
attribute={attribute}
onUpdate={attributesActions.onUpdate}
onDelete={attributesActions.onDelete}
/>
),
},
] as DataTableColumn<AttributeSchema>[],
[isMobile]
);
};
export default useAttributesTableColumns;

View File

@ -0,0 +1,29 @@
import { useMemo, useState } from "react";
import { AttributeSchema } from "@/lib/client";
type Props = {
attributes: AttributeSchema[];
};
const useFilteredAttributes = ({ attributes }: Props) => {
const [search, setSearch] = useState<string>("");
const filteredAttributes = useMemo(
() =>
attributes.filter(
attr =>
attr.type.name.includes(search) ||
attr.label.includes(search) ||
attr.description.includes(search)
),
[attributes, search]
);
return {
search,
setSearch,
filteredAttributes,
};
};
export default useFilteredAttributes;

View File

@ -0,0 +1,23 @@
import { useMemo, useState } from "react";
import { AttrSelectSchema } from "@/lib/client";
type Props = {
selects: AttrSelectSchema[];
};
const useFilteredSelects = ({ selects }: Props) => {
const [search, setSearch] = useState<string>("");
const filteredSelects = useMemo(
() => selects.filter(s => s.name.includes(search)),
[selects, search]
);
return {
search,
setSearch,
filteredSelects,
};
};
export default useFilteredSelects;

View File

@ -0,0 +1,50 @@
import { modals } from "@mantine/modals";
import { useDrawersContext } from "@/drawers/DrawersContext";
import { useAttrSelectsCrud } from "@/hooks/cruds/useSelectsCrud";
import { AttrSelectSchema } from "@/lib/client";
type Props = {
queryKey: any[];
};
export type SelectsActions = {
onCreate: () => void;
onUpdate: (select: AttrSelectSchema) => void;
onDelete: (select: AttrSelectSchema) => void;
};
const useSelectsActions = (props: Props): SelectsActions => {
const attrSelectsCrud = useAttrSelectsCrud(props);
const { openDrawer } = useDrawersContext();
const onCreate = () => {
modals.openContextModal({
modal: "enterNameModal",
title: "Создание справочника",
innerProps: {
onChange: values => attrSelectsCrud.onCreate(values),
},
});
};
const onUpdate = (select: AttrSelectSchema) => {
openDrawer({
key: "attrSelectEditorDrawer",
props: {
onSelectChange: (values, onSuccess) =>
attrSelectsCrud.onUpdate(select.id, values, onSuccess),
select,
},
});
};
const onDelete = attrSelectsCrud.onDelete;
return {
onCreate,
onUpdate,
onDelete,
};
};
export default useSelectsActions;

View File

@ -0,0 +1,38 @@
"use client";
import { useMemo } from "react";
import { DataTableColumn } from "mantine-datatable";
import { Center } from "@mantine/core";
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { AttrSelectSchema } from "@/lib/client";
import { useAttributesContext } from "../contexts/AttributesContext";
const useSelectsTableColumns = () => {
const isMobile = useIsMobile();
const { selectsActions } = useAttributesContext();
return useMemo(
() =>
[
{
title: "Название справочника",
accessor: "name",
},
{
accessor: "actions",
title: <Center>Действия</Center>,
width: "0%",
render: select => (
<UpdateDeleteTableActions
onDelete={() => selectsActions.onDelete(select)}
onChange={() => selectsActions.onUpdate(select)}
/>
),
},
] as DataTableColumn<AttrSelectSchema>[],
[isMobile]
);
};
export default useSelectsTableColumns;

View File

@ -0,0 +1,22 @@
import { Suspense } from "react";
import { Center, Loader } from "@mantine/core";
import PageBody from "@/app/attributes/components/PageBody";
import { AttributesContextProvider } from "@/app/attributes/contexts/AttributesContext";
import PageContainer from "@/components/layout/PageContainer/PageContainer";
export default async function AttributesPage() {
return (
<Suspense
fallback={
<Center h="50vh">
<Loader size="lg" />
</Center>
}>
<PageContainer>
<AttributesContextProvider>
<PageBody />
</AttributesContextProvider>
</PageContainer>
</Suspense>
);
}

View File

@ -0,0 +1,6 @@
enum AttributePageView {
ATTRIBUTES,
SELECTS,
}
export default AttributePageView;

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,41 @@
import React, { FC } from "react";
import { IconCheckbox, IconDotsVertical, IconTrash } from "@tabler/icons-react";
import { Box, Menu } from "@mantine/core";
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
type Props = {
onDelete: () => void;
startDealsSelecting: () => void;
};
const GroupMenu: FC<Props> = ({ onDelete, startDealsSelecting }) => {
return (
<Menu>
<Menu.Target>
<Box
px={"md"}
style={{ cursor: "pointer" }}
onClick={e => e.stopPropagation()}>
<ThemeIcon size={"sm"}>
<IconDotsVertical />
</ThemeIcon>
</Box>
</Menu.Target>
<Menu.Dropdown>
<DropdownMenuItem
onClick={onDelete}
icon={<IconTrash />}
label={"Удалить группу"}
/>
<DropdownMenuItem
onClick={startDealsSelecting}
icon={<IconCheckbox />}
label={"Добавить/удалить сделки"}
/>
</Menu.Dropdown>
</Menu>
);
};
export default GroupMenu;

View File

@ -0,0 +1,76 @@
"use client";
import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
import { Box, Group, Stack, Title } 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>
<Title order={6}>{selectedProject?.name}</Title>
<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; min-width: 50px;
flex-wrap: nowrap; flex-wrap: nowrap;
gap: 3px; gap: 3px;
@ -8,7 +8,7 @@
border-bottom: 2px solid gray; border-bottom: 2px solid gray;
} }
.board-mobile-selected { .board-selected {
border: 2px solid gray; border: 2px solid gray;
border-bottom: 0; border-bottom: 0;
} }

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

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,13 @@
.create-button {
cursor: pointer;
min-height: max-content;
border: 1px dashed;
@mixin light {
background-color: var(--color-light-white-blue);
border-color: lightblue;
}
@mixin dark {
background-color: var(--mantine-color-dark-7);
border-color: var(--mantine-color-dark-5);
}
}

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;

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