feat: creating and updating groups
This commit is contained in:
@ -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;
|
||||
|
||||
@ -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: <IconCategoryPlus />,
|
||||
},
|
||||
];
|
||||
|
||||
const getSelectedStyles = () => {
|
||||
if (groupDealsSelection.selectedBaseDealId === deal.id) {
|
||||
return styles["container-mainly-selected"];
|
||||
}
|
||||
if (groupDealsSelection.selectedDealIds.has(deal.id)) {
|
||||
return styles["container-selected"];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={onClick}
|
||||
className={
|
||||
className={classNames(
|
||||
getSelectedStyles(),
|
||||
isInGroup ? styles["container-in-group"] : styles.container
|
||||
}>
|
||||
)}
|
||||
onContextMenu={showContextMenu(dealContextMenu)}>
|
||||
<Group
|
||||
justify={"space-between"}
|
||||
wrap={"nowrap"}
|
||||
|
||||
@ -10,3 +10,13 @@
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
.selected-group {
|
||||
border: 2px solid;
|
||||
@mixin light {
|
||||
border-color: dodgerblue;
|
||||
}
|
||||
@mixin dark {
|
||||
border-color: dodgerblue;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { FC } from "react";
|
||||
import { Stack, Text } from "@mantine/core";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Flex, Stack, TextInput } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import GroupMenu from "@/app/deals/components/shared/GroupMenu/GroupMenu";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
import styles from "./DealsGroup.module.css";
|
||||
|
||||
@ -9,13 +13,41 @@ type Props = {
|
||||
};
|
||||
|
||||
const DealsGroup: FC<Props> = ({ 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 (
|
||||
<Stack
|
||||
className={styles["group-container"]}
|
||||
className={classNames(
|
||||
styles["group-container"],
|
||||
groupDealsSelection.selectedGroupId === group.id &&
|
||||
styles["selected-group"]
|
||||
)}
|
||||
gap={"xs"}
|
||||
bdrs={"lg"}
|
||||
p={"xs"}>
|
||||
<Text mx={"xs"}>{group.name}</Text>
|
||||
<Flex
|
||||
mx={"xs"}
|
||||
align={"center"}>
|
||||
<TextInput
|
||||
value={groupName}
|
||||
onChange={e => setGroupName(e.target.value)}
|
||||
variant={"unstyled"}
|
||||
/>
|
||||
<GroupMenu
|
||||
group={group}
|
||||
onDelete={groupsCrud.onDelete}
|
||||
onStartDealsSelecting={
|
||||
groupDealsSelection.startSelectingWithExistingGroup
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
{group.items.map(deal => (
|
||||
<DealCard
|
||||
deal={deal}
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
.shadow {
|
||||
@mixin light {
|
||||
box-shadow: var(--light-shadow);
|
||||
}
|
||||
@mixin dark {
|
||||
box-shadow: var(--dark-shadow);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { Affix, Flex, Stack, Title, Transition } from "@mantine/core";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||
import styles from "./GroupDealsSelectionAffix.module.css";
|
||||
|
||||
const GroupDealsSelectionAffix = () => {
|
||||
const {
|
||||
groupDealsSelection: {
|
||||
selectedBaseDealId,
|
||||
selectedGroupId,
|
||||
finishDealsSelecting,
|
||||
cancelDealsSelecting,
|
||||
},
|
||||
} = useDealsContext();
|
||||
|
||||
return (
|
||||
<Affix position={{ bottom: 35, right: 35 }}>
|
||||
<Transition
|
||||
transition="slide-up"
|
||||
mounted={!!(selectedBaseDealId || selectedGroupId)}>
|
||||
{transitionStyles => (
|
||||
<Stack
|
||||
bdrs={"xl"}
|
||||
bd={"1px solid var(--mantine-color-default-border"}
|
||||
className={styles.shadow}
|
||||
p={"md"}
|
||||
gap={"md"}
|
||||
style={transitionStyles}>
|
||||
<Title
|
||||
order={5}
|
||||
ta={"center"}>
|
||||
Выбор сделок для группы
|
||||
</Title>
|
||||
<Flex gap={"xs"}>
|
||||
<InlineButton onClick={cancelDealsSelecting}>
|
||||
Отмена
|
||||
</InlineButton>
|
||||
<InlineButton variant={"filled"} onClick={finishDealsSelecting}>
|
||||
Сохранить
|
||||
</InlineButton>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
</Transition>
|
||||
</Affix>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupDealsSelectionAffix;
|
||||
41
src/app/deals/components/shared/GroupMenu/GroupMenu.tsx
Normal file
41
src/app/deals/components/shared/GroupMenu/GroupMenu.tsx
Normal 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";
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
|
||||
|
||||
type Props = {
|
||||
group: GroupWithDealsSchema;
|
||||
onDelete: (groupId: number) => void;
|
||||
onStartDealsSelecting: (group: GroupWithDealsSchema) => void;
|
||||
};
|
||||
|
||||
const GroupMenu: FC<Props> = ({ group, onDelete, onStartDealsSelecting }) => {
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Box onClick={e => e.stopPropagation()}>
|
||||
<ThemeIcon size={"sm"}>
|
||||
<IconDotsVertical />
|
||||
</ThemeIcon>
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(group.id)}
|
||||
icon={<IconTrash />}
|
||||
label={"Удалить"}
|
||||
/>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onStartDealsSelecting(group)}
|
||||
icon={<IconCheckbox />}
|
||||
label={"Добавить/удалить сделки"}
|
||||
/>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupMenu;
|
||||
@ -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 = () => (
|
||||
<>
|
||||
<MainBlockHeader />
|
||||
<Space h="md" />
|
||||
<Funnel />
|
||||
<GroupDealsSelectionAffix />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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<React.SetStateAction<number>>;
|
||||
setPage: Dispatch<SetStateAction<number>>;
|
||||
dealsFiltersForm: UseFormReturnType<DealsFiltersForm>;
|
||||
isChangedFilters: boolean;
|
||||
sortingForm: UseFormReturnType<SortingForm>;
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
92
src/app/deals/hooks/useGroupDealsSelection.tsx
Normal file
92
src/app/deals/hooks/useGroupDealsSelection.tsx
Normal file
@ -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<number>;
|
||||
setSelectedDealIds: Dispatch<SetStateAction<Set<number>>>;
|
||||
toggleDeal: (dealId: number) => void;
|
||||
finishDealsSelecting: () => void;
|
||||
cancelDealsSelecting: () => void;
|
||||
};
|
||||
|
||||
const useGroupDealsSelection = ({ groupsCrud }: Props): GroupDealsSelection => {
|
||||
const [selectedDealIds, setSelectedDealIds] = useState<Set<number>>(
|
||||
new Set()
|
||||
);
|
||||
const [isDealsSelecting, setIsDealsSelecting] = useState<boolean>(false);
|
||||
const [selectedBaseDealId, setSelectedBaseDealId] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const [selectedGroupId, setSelectedGroupId] = useState<number | null>(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;
|
||||
@ -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) {
|
||||
<MantineProvider
|
||||
theme={theme}
|
||||
defaultColorScheme={"auto"}>
|
||||
<ReactQueryProvider>
|
||||
<ReduxProvider>
|
||||
<ModalsProvider
|
||||
labels={{ confirm: "Да", cancel: "Нет" }}
|
||||
modals={modals}>
|
||||
<DrawersContextProvider>
|
||||
<ProjectsContextProvider>
|
||||
<AppShell
|
||||
layout={"alt"}
|
||||
withBorder={false}
|
||||
navbar={{
|
||||
width: 220,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
desktop: false,
|
||||
mobile: true,
|
||||
},
|
||||
}}>
|
||||
<AppShellNavbarWrapper>
|
||||
<Navbar />
|
||||
</AppShellNavbarWrapper>
|
||||
<AppShellMainWrapper>
|
||||
{children}
|
||||
</AppShellMainWrapper>
|
||||
<AppShellFooterWrapper>
|
||||
<Footer />
|
||||
</AppShellFooterWrapper>
|
||||
</AppShell>
|
||||
</ProjectsContextProvider>
|
||||
</DrawersContextProvider>
|
||||
</ModalsProvider>
|
||||
</ReduxProvider>
|
||||
<Notifications position="bottom-right" />
|
||||
</ReactQueryProvider>
|
||||
<ContextMenuProvider>
|
||||
<ReactQueryProvider>
|
||||
<ReduxProvider>
|
||||
<ModalsProvider
|
||||
labels={{ confirm: "Да", cancel: "Нет" }}
|
||||
modals={modals}>
|
||||
<DrawersContextProvider>
|
||||
<ProjectsContextProvider>
|
||||
<AppShell
|
||||
layout={"alt"}
|
||||
withBorder={false}
|
||||
navbar={{
|
||||
width: 220,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
desktop: false,
|
||||
mobile: true,
|
||||
},
|
||||
}}>
|
||||
<AppShellNavbarWrapper>
|
||||
<Navbar />
|
||||
</AppShellNavbarWrapper>
|
||||
<AppShellMainWrapper>
|
||||
{children}
|
||||
</AppShellMainWrapper>
|
||||
<AppShellFooterWrapper>
|
||||
<Footer />
|
||||
</AppShellFooterWrapper>
|
||||
</AppShell>
|
||||
</ProjectsContextProvider>
|
||||
</DrawersContextProvider>
|
||||
</ModalsProvider>
|
||||
</ReduxProvider>
|
||||
<Notifications position="bottom-right" />
|
||||
</ReactQueryProvider>
|
||||
</ContextMenuProvider>
|
||||
</MantineProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user