From 30bc7bbee4921796e8e9f24d07f50f2d1c50f62a Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Fri, 17 Oct 2025 19:47:47 +0400 Subject: [PATCH] feat: creating and updating groups --- package.json | 1 + .../shared/DealCard/DealCard.module.css | 20 +++ .../components/shared/DealCard/DealCard.tsx | 40 ++++- .../shared/DealsGroup/DealsGroup.module.css | 10 ++ .../shared/DealsGroup/DealsGroup.tsx | 40 ++++- .../GroupDealsSelectionAffix.module.css | 8 + .../GroupDealsSelectionAffix.tsx | 51 ++++++ .../components/shared/GroupMenu/GroupMenu.tsx | 41 +++++ .../components/shared/views/BoardView.tsx | 2 + src/app/deals/contexts/DealsContext.tsx | 11 +- .../deals/hooks/useGroupDealsSelection.tsx | 92 ++++++++++ src/app/layout.tsx | 72 ++++---- src/hooks/cruds/useDealGroupCrud.tsx | 102 ++++++++--- src/lib/client/@tanstack/react-query.gen.ts | 105 +++++------ src/lib/client/sdk.gen.ts | 86 +++++---- src/lib/client/types.gen.ts | 164 +++++++++--------- src/lib/client/zod.gen.ts | 88 +++++----- yarn.lock | 14 ++ 18 files changed, 657 insertions(+), 290 deletions(-) create mode 100644 src/app/deals/components/shared/GroupDealsSelectionAffix/GroupDealsSelectionAffix.module.css create mode 100644 src/app/deals/components/shared/GroupDealsSelectionAffix/GroupDealsSelectionAffix.tsx create mode 100644 src/app/deals/components/shared/GroupMenu/GroupMenu.tsx create mode 100644 src/app/deals/hooks/useGroupDealsSelection.tsx 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 ( + + + e.stopPropagation()}> + + + + + + + onDelete(group.id)} + icon={} + label={"Удалить"} + /> + onStartDealsSelecting(group)} + icon={} + label={"Добавить/удалить сделки"} + /> + + + ); +}; + +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} - - -