diff --git a/package.json b/package.json
index 0ccee7e..6c60339 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"i18n-iso-countries": "^7.14.0",
"lexorank": "^1.0.5",
"libphonenumber-js": "^1.12.10",
+ "mantine-contextmenu": "^8.2.0",
"mantine-datatable": "^8.2.0",
"next": "15.4.7",
"phone": "^3.1.67",
diff --git a/src/app/deals/components/shared/DealCard/DealCard.module.css b/src/app/deals/components/shared/DealCard/DealCard.module.css
index 1a749f6..c32cb37 100644
--- a/src/app/deals/components/shared/DealCard/DealCard.module.css
+++ b/src/app/deals/components/shared/DealCard/DealCard.module.css
@@ -12,6 +12,26 @@
}
}
+.container-selected {
+ border: 2px dashed !important;
+ @mixin light {
+ border-color: dodgerblue !important;
+ }
+ @mixin dark {
+ border-color: dodgerblue !important;
+ }
+}
+
+.container-mainly-selected {
+ border: 2px solid;
+ @mixin light {
+ border-color: dodgerblue !important;
+ }
+ @mixin dark {
+ border-color: dodgerblue !important;
+ }
+}
+
.container-in-group {
padding: 0;
border: 1px dashed;
diff --git a/src/app/deals/components/shared/DealCard/DealCard.tsx b/src/app/deals/components/shared/DealCard/DealCard.tsx
index 7c5ecb6..e5be5a6 100644
--- a/src/app/deals/components/shared/DealCard/DealCard.tsx
+++ b/src/app/deals/components/shared/DealCard/DealCard.tsx
@@ -1,3 +1,6 @@
+import { IconCategoryPlus } from "@tabler/icons-react";
+import classNames from "classnames";
+import { useContextMenu } from "mantine-contextmenu";
import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
@@ -13,10 +16,16 @@ type Props = {
const DealCard = ({ deal, isInGroup = false }: Props) => {
const { selectedProject, modulesSet } = useProjectsContext();
- const { dealsCrud, refetchDeals } = useDealsContext();
+ const { dealsCrud, refetchDeals, groupDealsSelection } = useDealsContext();
const { openDrawer } = useDrawersContext();
const onClick = () => {
+ if (groupDealsSelection.isDealsSelecting) {
+ if (groupDealsSelection.selectedBaseDealId !== deal.id)
+ groupDealsSelection.toggleDeal(deal.id);
+ return;
+ }
+
openDrawer({
key: "dealEditorDrawer",
props: {
@@ -29,12 +38,37 @@ const DealCard = ({ deal, isInGroup = false }: Props) => {
});
};
+ const { showContextMenu } = useContextMenu();
+
+ const dealContextMenu = deal.group
+ ? []
+ : [
+ {
+ key: "startGroupForming",
+ onClick: () =>
+ groupDealsSelection.startSelectingWithDeal(deal.id),
+ title: "Создать группу",
+ icon: ,
+ },
+ ];
+
+ const getSelectedStyles = () => {
+ if (groupDealsSelection.selectedBaseDealId === deal.id) {
+ return styles["container-mainly-selected"];
+ }
+ if (groupDealsSelection.selectedDealIds.has(deal.id)) {
+ return styles["container-selected"];
+ }
+ };
+
return (
+ )}
+ onContextMenu={showContextMenu(dealContextMenu)}>
= ({ group }) => {
+ const [groupName, setGroupName] = useState(group.name ?? "");
+ const [debouncedGroupName] = useDebouncedValue(groupName, 600);
+ const { groupsCrud, groupDealsSelection } = useDealsContext();
+
+ useEffect(() => {
+ if (debouncedGroupName === group.name) return;
+ groupsCrud.onUpdate(group.id, { name: debouncedGroupName });
+ }, [debouncedGroupName]);
+
return (
- {group.name}
+
+ setGroupName(e.target.value)}
+ variant={"unstyled"}
+ />
+
+
{group.items.map(deal => (
{
+ const {
+ groupDealsSelection: {
+ selectedBaseDealId,
+ selectedGroupId,
+ finishDealsSelecting,
+ cancelDealsSelecting,
+ },
+ } = useDealsContext();
+
+ return (
+
+
+ {transitionStyles => (
+
+
+ Выбор сделок для группы
+
+
+
+ Отмена
+
+
+ Сохранить
+
+
+
+ )}
+
+
+ );
+};
+
+export default GroupDealsSelectionAffix;
diff --git a/src/app/deals/components/shared/GroupMenu/GroupMenu.tsx b/src/app/deals/components/shared/GroupMenu/GroupMenu.tsx
new file mode 100644
index 0000000..2588c07
--- /dev/null
+++ b/src/app/deals/components/shared/GroupMenu/GroupMenu.tsx
@@ -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";
+import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
+
+
+type Props = {
+ group: GroupWithDealsSchema;
+ onDelete: (groupId: number) => void;
+ onStartDealsSelecting: (group: GroupWithDealsSchema) => void;
+};
+
+const GroupMenu: FC = ({ group, onDelete, onStartDealsSelecting }) => {
+ return (
+
+ );
+};
+
+export default GroupMenu;
diff --git a/src/app/deals/components/shared/views/BoardView.tsx b/src/app/deals/components/shared/views/BoardView.tsx
index c065c03..1c955d7 100644
--- a/src/app/deals/components/shared/views/BoardView.tsx
+++ b/src/app/deals/components/shared/views/BoardView.tsx
@@ -1,11 +1,13 @@
import { Space } from "@mantine/core";
import MainBlockHeader from "@/app/deals/components/mobile/MainBlockHeader/MainBlockHeader";
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
+import GroupDealsSelectionAffix from "@/app/deals/components/shared/GroupDealsSelectionAffix/GroupDealsSelectionAffix";
export const BoardView = () => (
<>
+
>
);
diff --git a/src/app/deals/contexts/DealsContext.tsx b/src/app/deals/contexts/DealsContext.tsx
index 292b2b9..7ad04c9 100644
--- a/src/app/deals/contexts/DealsContext.tsx
+++ b/src/app/deals/contexts/DealsContext.tsx
@@ -1,10 +1,13 @@
"use client";
-import React from "react";
+import { Dispatch, SetStateAction } from "react";
import { UseFormReturnType } from "@mantine/form";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useDealsAndGroups from "@/app/deals/hooks/useDealsAndGroups";
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
+import useGroupDealsSelection, {
+ GroupDealsSelection,
+} from "@/app/deals/hooks/useGroupDealsSelection";
import useDealGroupCrud, { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud";
import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud";
import useDealsList from "@/hooks/lists/useDealsList";
@@ -23,10 +26,11 @@ type DealsContextState = {
groupsCrud: GroupsCrud;
paginationInfo?: PaginationInfoSchema;
page: number;
- setPage: React.Dispatch>;
+ setPage: Dispatch>;
dealsFiltersForm: UseFormReturnType;
isChangedFilters: boolean;
sortingForm: UseFormReturnType;
+ groupDealsSelection: GroupDealsSelection;
};
type Props = {
@@ -56,6 +60,8 @@ const useDealsContextState = ({
const groupsCrud = useDealGroupCrud();
+ const groupDealsSelection = useGroupDealsSelection({ groupsCrud });
+
const { dealsWithoutGroup, groupsWithDeals } =
useDealsAndGroups(dealsListObjects);
@@ -65,6 +71,7 @@ const useDealsContextState = ({
groupsCrud,
dealsWithoutGroup,
groupsWithDeals,
+ groupDealsSelection,
};
};
diff --git a/src/app/deals/hooks/useGroupDealsSelection.tsx b/src/app/deals/hooks/useGroupDealsSelection.tsx
new file mode 100644
index 0000000..653fd9f
--- /dev/null
+++ b/src/app/deals/hooks/useGroupDealsSelection.tsx
@@ -0,0 +1,92 @@
+import { Dispatch, SetStateAction, useState } from "react";
+import { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud";
+import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
+
+type Props = {
+ groupsCrud: GroupsCrud;
+};
+
+export type GroupDealsSelection = {
+ isDealsSelecting: boolean;
+ selectedBaseDealId: number | null;
+ startSelectingWithDeal: (dealId: number) => void;
+ selectedGroupId: number | null;
+ startSelectingWithExistingGroup: (group: GroupWithDealsSchema) => void;
+ selectedDealIds: Set;
+ setSelectedDealIds: Dispatch>>;
+ toggleDeal: (dealId: number) => void;
+ finishDealsSelecting: () => void;
+ cancelDealsSelecting: () => void;
+};
+
+const useGroupDealsSelection = ({ groupsCrud }: Props): GroupDealsSelection => {
+ const [selectedDealIds, setSelectedDealIds] = useState>(
+ new Set()
+ );
+ const [isDealsSelecting, setIsDealsSelecting] = useState(false);
+ const [selectedBaseDealId, setSelectedBaseDealId] = useState(
+ null
+ );
+ const [selectedGroupId, setSelectedGroupId] = useState(null);
+
+ const toggleDeal = (dealId: number) => {
+ if (selectedDealIds.has(dealId)) {
+ selectedDealIds.delete(dealId);
+ } else {
+ selectedDealIds.add(dealId);
+ }
+ setSelectedDealIds(new Set(selectedDealIds));
+ };
+
+ const finishDealsSelecting = () => {
+ if (selectedBaseDealId) {
+ groupsCrud.onCreate(
+ selectedBaseDealId,
+ selectedDealIds.values().toArray()
+ );
+ setSelectedBaseDealId(null);
+ } else if (selectedGroupId) {
+ groupsCrud.onUpdateDealsInGroup(
+ selectedGroupId,
+ selectedDealIds.values().toArray()
+ );
+ setSelectedGroupId(null);
+ }
+ setIsDealsSelecting(false);
+ setSelectedDealIds(new Set());
+ };
+
+ const cancelDealsSelecting = () => {
+ setSelectedDealIds(new Set());
+ setSelectedBaseDealId(null);
+ setSelectedGroupId(null);
+ setIsDealsSelecting(false);
+ };
+
+ const startSelectingWithExistingGroup = (group: GroupWithDealsSchema) => {
+ setSelectedDealIds(new Set(group.items.map(item => item.id)));
+ setSelectedGroupId(group.id);
+ setIsDealsSelecting(true);
+ };
+
+ const startSelectingWithDeal = (dealId: number) => {
+ setSelectedDealIds(new Set([dealId]));
+ setSelectedBaseDealId(dealId);
+ setIsDealsSelecting(true);
+ };
+
+ return {
+ isDealsSelecting,
+ selectedBaseDealId,
+ startSelectingWithDeal,
+ selectedGroupId,
+ startSelectingWithExistingGroup,
+ selectedDealIds,
+ setSelectedDealIds,
+ toggleDeal,
+ finishDealsSelecting,
+ cancelDealsSelecting,
+ };
+};
+
+export default useGroupDealsSelection;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 0a9d350..96c23fd 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,6 +2,7 @@ import "@mantine/core/styles.css";
import "mantine-datatable/styles.layer.css";
import "@mantine/notifications/styles.css";
import "@mantine/dates/styles.css";
+import "mantine-contextmenu/styles.css";
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/scrollbar";
@@ -14,6 +15,7 @@ import {
} from "@mantine/core";
import { theme } from "@/theme";
import "@/app/global.css";
+import { ContextMenuProvider } from "mantine-contextmenu";
import { ModalsProvider } from "@mantine/modals";
import { Notifications } from "@mantine/notifications";
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
@@ -63,40 +65,42 @@ export default function RootLayout({ children }: Props) {
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+