diff --git a/package.json b/package.json index 2caa562..e96bb75 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", + "@hello-pangea/dnd": "^18.0.1", "@mantine/core": "8.1.2", "@mantine/dates": "^8.2.7", "@mantine/dropzone": "^8.3.1", diff --git a/src/app/services/components/shared/ServicesTable/ServicesTable.module.css b/src/app/services/components/shared/ServicesTable/ServicesTable.module.css new file mode 100644 index 0000000..8a09005 --- /dev/null +++ b/src/app/services/components/shared/ServicesTable/ServicesTable.module.css @@ -0,0 +1,4 @@ +.draggable-row[data-is-dragging='true'] td { + opacity: 0.75; + border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); +} diff --git a/src/app/services/components/shared/ServicesTable/ServicesTable.tsx b/src/app/services/components/shared/ServicesTable/ServicesTable.tsx index eb22586..02c9f91 100644 --- a/src/app/services/components/shared/ServicesTable/ServicesTable.tsx +++ b/src/app/services/components/shared/ServicesTable/ServicesTable.tsx @@ -1,13 +1,20 @@ "use client"; import { FC, useMemo, useState } from "react"; -import useServicesInnerTableColumns from "@/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns"; +import { DragDropContext, Draggable } from "@hello-pangea/dnd"; +import { IconGripVertical } from "@tabler/icons-react"; +import clsx from "clsx"; +import { DataTableDraggableRow } from "mantine-datatable"; +import { Center, TableTd } from "@mantine/core"; +import InnerServicesTableDndWrapper from "@/app/services/components/shared/ServicesTable/components/InnerServicesTableDndWrapper"; import useServicesOuterTableColumns from "@/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns"; import { GroupedServices } from "@/app/services/components/shared/ServicesTable/types/GroupedServices"; import { useServicesContext } from "@/app/services/contexts/ServicesContext"; +import { useServicesDndContext } from "@/app/services/contexts/ServicesDndContext"; +import Droppable from "@/components/dnd-pangea/Droppable/Droppable"; import BaseTable from "@/components/ui/BaseTable/BaseTable"; -import useIsMobile from "@/hooks/utils/useIsMobile"; import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service"; +import classes from "./ServicesTable.module.css"; type Props = { serviceType: ServiceType; @@ -15,13 +22,12 @@ type Props = { const ServicesTable: FC = ({ serviceType }) => { const { servicesList } = useServicesContext(); - const isMobile = useIsMobile(); + const { onDragEnd, onDragStart } = useServicesDndContext(); const [expandedCategoryIds, setExpandedCategoryIds] = useState( [] ); - const innerColumns = useServicesInnerTableColumns(); const outerColumns = useServicesOuterTableColumns({ expandedCategoryIds, setExpandedCategoryIds, @@ -48,36 +54,75 @@ const ServicesTable: FC = ({ serviceType }) => { }, [servicesList.services, serviceType]); return ( - ( - - ), - }} - style={{ - marginInline: isMobile ? "var(--mantine-spacing-xs)" : "0", - }} - /> + + ( + + {children} + + )} + rowExpansion={{ + allowMultiple: true, + expanded: { + recordIds: expandedCategoryIds, + onRecordIdsChange: setExpandedCategoryIds, + }, + content: ({ record }) => ( + + ), + }} + styles={{ table: { tableLayout: "fixed" } }} + rowFactory={({ + record, + index, + rowProps, + children, + expandedElement, + }) => ( + <> + + {(provided, snapshot) => ( + + +
+ +
+
+ {children} +
+ )} +
+ {expandedElement} + + )} + /> +
); }; diff --git a/src/app/services/components/shared/ServicesTable/components/InnerServicesTable.tsx b/src/app/services/components/shared/ServicesTable/components/InnerServicesTable.tsx new file mode 100644 index 0000000..e83146a --- /dev/null +++ b/src/app/services/components/shared/ServicesTable/components/InnerServicesTable.tsx @@ -0,0 +1,57 @@ +import { FC } from "react"; +import { Draggable } from "@hello-pangea/dnd"; +import { IconGripVertical } from "@tabler/icons-react"; +import clsx from "clsx"; +import { DataTableDraggableRow } from "mantine-datatable"; +import { Center, TableTd } from "@mantine/core"; +import useServicesInnerTableColumns from "@/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns"; +import classes from "@/app/services/components/shared/ServicesTable/ServicesTable.module.css"; +import BaseTable from "@/components/ui/BaseTable/BaseTable"; +import { ServiceSchema } from "@/lib/client"; + +type Props = { + services: ServiceSchema[]; +}; + +const InnerServicesTable: FC = ({ services }) => { + const innerColumns = useServicesInnerTableColumns(); + + return ( + ( + + {(provided, snapshot) => ( + + +
+ +
+
+ {children} +
+ )} +
+ )} + /> + ); +}; + +export default InnerServicesTable; diff --git a/src/app/services/components/shared/ServicesTable/components/InnerServicesTableDndWrapper.tsx b/src/app/services/components/shared/ServicesTable/components/InnerServicesTableDndWrapper.tsx new file mode 100644 index 0000000..edba0c3 --- /dev/null +++ b/src/app/services/components/shared/ServicesTable/components/InnerServicesTableDndWrapper.tsx @@ -0,0 +1,21 @@ +import { FC } from "react"; +import InnerServicesTable from "@/app/services/components/shared/ServicesTable/components/InnerServicesTable"; +import Droppable from "@/components/dnd-pangea/Droppable/Droppable"; +import { ServiceSchema } from "@/lib/client"; + +type Props = { + services: ServiceSchema[]; +}; + +const InnerServicesTableDndWrapper: FC = ({ services }) => { + return ( + + + + ); +}; + +export default InnerServicesTableDndWrapper; diff --git a/src/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns.tsx b/src/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns.tsx index bfbe09b..2df0640 100644 --- a/src/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns.tsx +++ b/src/app/services/components/shared/ServicesTable/hooks/servicesInnerTableColumns.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { DataTableColumn } from "mantine-datatable"; -import { List, Text } from "@mantine/core"; +import { Center, List, Text } from "@mantine/core"; import { useServicesContext } from "@/app/services/contexts/ServicesContext"; import useServicesActions from "@/app/services/hooks/useServicesActions"; import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions"; @@ -34,32 +34,38 @@ const useServicesInnerTableColumns = () => { return useMemo( () => [ + { accessor: "", hiddenContent: true, width: 0 }, { accessor: "name", title: "Название", + width: 350, }, { accessor: "price", title: "Цена", render: service => getPriceRow(service), + width: 200, }, { accessor: "cost", title: isMobile ? "Себестоим." : "Себестоимость", render: service => `${service.cost?.toLocaleString("ru")}₽`, + width: 200, }, { accessor: "actions", title: isMobile ? "" : "Действия", sortable: false, textAlign: "center", - width: "0%", + width: 70, render: service => ( - servicesCrud.onDelete(service)} - onChange={() => onChangeService(service)} - dotsForMobile - /> +
+ servicesCrud.onDelete(service)} + onChange={() => onChangeService(service)} + dotsForMobile + /> +
), }, ] as DataTableColumn[], diff --git a/src/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns.tsx b/src/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns.tsx index 1db4d32..61bfd7a 100644 --- a/src/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns.tsx +++ b/src/app/services/components/shared/ServicesTable/hooks/servicesOuterTableColumns.tsx @@ -7,11 +7,12 @@ import { IconChevronUp, } from "@tabler/icons-react"; import { DataTableColumn } from "mantine-datatable"; -import { Box, Group, Text } from "@mantine/core"; +import { Box, Center, Group, Text } from "@mantine/core"; import { GroupedServices } from "@/app/services/components/shared/ServicesTable/types/GroupedServices"; import { useServicesContext } from "@/app/services/contexts/ServicesContext"; import useCategoriesActions from "@/app/services/hooks/useCategoriesActions"; import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions"; +import useIsMobile from "@/hooks/utils/useIsMobile"; type Props = { expandedCategoryIds: number[]; @@ -22,6 +23,7 @@ const useServicesOuterTableColumns = ({ expandedCategoryIds, setExpandedCategoryIds, }: Props) => { + const isMobile = useIsMobile(); const { onChangeCategory } = useCategoriesActions(); const { categoriesCrud, categories } = useServicesContext(); @@ -43,8 +45,9 @@ const useServicesOuterTableColumns = ({ return useMemo( () => [ + { accessor: "", hiddenContent: true, width: 3 }, { - accessor: "name", + accessor: "category.name", title: ( {name} ), + width: 450, }, { accessor: "actions", - title: "Действия", + title: isMobile ? "" : "Действия", sortable: false, textAlign: "center", - width: "0%", render: ({ category }) => ( - categoriesCrud.onDelete(category)} - onChange={() => onChangeCategory(category)} - /> +
+ + categoriesCrud.onDelete(category) + } + onChange={() => onChangeCategory(category)} + dotsForMobile + /> +
), + width: 50, }, ] as DataTableColumn[], [expandedCategoryIds, categories] diff --git a/src/app/services/contexts/ServicesContext.tsx b/src/app/services/contexts/ServicesContext.tsx index c05c37a..43f9356 100644 --- a/src/app/services/contexts/ServicesContext.tsx +++ b/src/app/services/contexts/ServicesContext.tsx @@ -30,7 +30,7 @@ type ServicesContextState = { categories: ServiceCategorySchema[]; }; -const useFulfillmentBaseContextState = (): ServicesContextState => { +const useServicesContextState = (): ServicesContextState => { const servicesList = useServicesList(); const servicesCrud = useServicesCrud(servicesList); @@ -54,7 +54,4 @@ const useFulfillmentBaseContextState = (): ServicesContextState => { }; export const [ServicesContextProvider, useServicesContext] = - makeContext( - useFulfillmentBaseContextState, - "Services" - ); + makeContext(useServicesContextState, "Services"); diff --git a/src/app/services/contexts/ServicesDndContext.tsx b/src/app/services/contexts/ServicesDndContext.tsx new file mode 100644 index 0000000..9c4ccf1 --- /dev/null +++ b/src/app/services/contexts/ServicesDndContext.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { Dispatch, SetStateAction, useState } from "react"; +import type { DragStart, DropResult } from "@hello-pangea/dnd"; +import ServiceDragState from "@/app/services/enums/DragState"; +import makeContext from "@/lib/contextFactory/contextFactory"; + +type ServicesDndContextState = { + onDragStart: (start: DragStart) => void; + onDragEnd: (result: DropResult) => void; + dragState: ServiceDragState; + setDragState: Dispatch>; +}; + +const useServiceDndContextState = (): ServicesDndContextState => { + const [dragState, setDragState] = useState( + ServiceDragState.DRAG_ENDED + ); + + const [value, setValue] = useState(false); + + const onDragStart = (start: DragStart) => { + setValue(prev => !prev); + }; + + const onDragEnd = (result: DropResult) => { + if (!result.destination) return; + + const sourceId = result.source.droppableId; + const destinationId = result.destination.droppableId; + + console.log(destinationId); + + // const items = Array.from(records); + // const sourceIndex = result.source.index; + // const destinationIndex = result.destination.index; + // const [reorderedItem] = items.splice(sourceIndex, 1); + // items.splice(destinationIndex, 0, reorderedItem); + // + // setRecords(items); + // notifications.show({ + // title: 'Table reordered', + // message: `The company named "${items[sourceIndex].name}" has been moved from position ${sourceIndex + 1} to ${destinationIndex + 1}.`, + // color: 'blue', + // }); + }; + + return { + dragState, + setDragState, + onDragStart, + onDragEnd, + }; +}; + +export const [ServicesDndContextProvider, useServicesDndContext] = + makeContext( + useServiceDndContextState, + "ServicesDnd" + ); diff --git a/src/app/services/enums/DragState.ts b/src/app/services/enums/DragState.ts new file mode 100644 index 0000000..9b0317c --- /dev/null +++ b/src/app/services/enums/DragState.ts @@ -0,0 +1,7 @@ +enum ServiceDragState { + DRAG_ENDED, + DRAG_SERVICE, + DRAG_CATEGORY, +} + +export default ServiceDragState; diff --git a/src/app/services/page.tsx b/src/app/services/page.tsx index 2839eab..6ced2c2 100644 --- a/src/app/services/page.tsx +++ b/src/app/services/page.tsx @@ -2,6 +2,7 @@ import { Suspense } from "react"; import { Center, Loader } from "@mantine/core"; import PageBody from "@/app/services/components/shared/PageBody/PageBody"; import { ServicesContextProvider } from "@/app/services/contexts/ServicesContext"; +import { ServicesDndContextProvider } from "@/app/services/contexts/ServicesDndContext"; import PageContainer from "@/components/layout/PageContainer/PageContainer"; export default async function ServicesPage() { @@ -13,9 +14,11 @@ export default async function ServicesPage() { }> - - - + + + + + ); diff --git a/src/components/dnd-pangea/Droppable/Droppable.tsx b/src/components/dnd-pangea/Droppable/Droppable.tsx new file mode 100644 index 0000000..78f246f --- /dev/null +++ b/src/components/dnd-pangea/Droppable/Droppable.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { FC, ReactNode } from "react"; +import { + Droppable as DroppablePangea, + DroppableProps, +} from "@hello-pangea/dnd"; + +type Props = Omit & { + children: ReactNode; +}; + +const Droppable: FC = ({ children, ...restProps }) => ( + + {provided => ( +
+ {children} + {provided.placeholder} +
+ )} +
+); + +export default Droppable; diff --git a/yarn.lock b/yarn.lock index 4f9ae88..a63450e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1418,6 +1418,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.26.7": + version: 7.28.4 + resolution: "@babel/runtime@npm:7.28.4" + checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7 + languageName: node + linkType: hard + "@babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" @@ -1950,6 +1957,22 @@ __metadata: languageName: node linkType: hard +"@hello-pangea/dnd@npm:^18.0.1": + version: 18.0.1 + resolution: "@hello-pangea/dnd@npm:18.0.1" + dependencies: + "@babel/runtime": "npm:^7.26.7" + css-box-model: "npm:^1.2.1" + raf-schd: "npm:^4.0.3" + react-redux: "npm:^9.2.0" + redux: "npm:^5.0.1" + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + checksum: 10c0/30c47ac8048f85e5c6d39c0b5a492cf2cc9e5f532cee12c5ecc77688596c8846670be142bd716212db789f161cd769601a5da135fa99ac65824fbb6a07d4d137 + languageName: node + linkType: hard + "@hey-api/client-axios@npm:^0.9.1": version: 0.9.1 resolution: "@hey-api/client-axios@npm:0.9.1" @@ -6123,6 +6146,7 @@ __metadata: "@dnd-kit/modifiers": "npm:^9.0.0" "@dnd-kit/sortable": "npm:^10.0.0" "@eslint/js": "npm:^9.29.0" + "@hello-pangea/dnd": "npm:^18.0.1" "@hey-api/client-axios": "npm:^0.9.1" "@hey-api/client-next": "npm:^0.5.1" "@hey-api/openapi-ts": "npm:^0.80.1" @@ -6230,6 +6254,15 @@ __metadata: languageName: node linkType: hard +"css-box-model@npm:^1.2.1": + version: 1.2.1 + resolution: "css-box-model@npm:1.2.1" + dependencies: + tiny-invariant: "npm:^1.0.6" + checksum: 10c0/611e56d76b16e4e21956ed9fa53f1936fbbfaccd378659587e9c929f342037fc6c062f8af9447226e11fe7c95e31e6c007a37e592f9bff4c2d40e6915553104a + languageName: node + linkType: hard + "css-functions-list@npm:^3.2.3": version: 3.2.3 resolution: "css-functions-list@npm:3.2.3" @@ -11629,6 +11662,13 @@ __metadata: languageName: node linkType: hard +"raf-schd@npm:^4.0.3": + version: 4.0.3 + resolution: "raf-schd@npm:4.0.3" + checksum: 10c0/ecabf0957c05fad059779bddcd992f1a9d3a35dfea439a6f0935c382fcf4f7f7fa60489e467b4c2db357a3665167d2a379782586b59712bb36c766e02824709b + languageName: node + linkType: hard + "randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -13452,7 +13492,7 @@ __metadata: languageName: node linkType: hard -"tiny-invariant@npm:^1.3.3": +"tiny-invariant@npm:^1.0.6, tiny-invariant@npm:^1.3.3": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" checksum: 10c0/65af4a07324b591a059b35269cd696aba21bef2107f29b9f5894d83cc143159a204b299553435b03874ebb5b94d019afa8b8eff241c8a4cfee95872c2e1c1c4a