feat: groups creating on mobile

This commit is contained in:
2025-10-18 10:23:13 +04:00
parent f90b335ee1
commit 159d6948c7
9 changed files with 149 additions and 47 deletions

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

@ -1,10 +1,11 @@
import { IconCategoryPlus } from "@tabler/icons-react"; import { IconCategoryPlus } from "@tabler/icons-react";
import classNames from "classnames"; import classNames from "classnames";
import { useContextMenu } from "mantine-contextmenu"; import { useContextMenu } from "mantine-contextmenu";
import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core"; import { Box, Card, Group, Stack, Text } from "@mantine/core";
import { useDealsContext } from "@/app/deals/contexts/DealsContext"; import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import { useDrawersContext } from "@/drawers/DrawersContext"; import { useDrawersContext } from "@/drawers/DrawersContext";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { DealSchema } from "@/lib/client"; import { DealSchema } from "@/lib/client";
import { ModuleNames } from "@/modules/modules"; import { ModuleNames } from "@/modules/modules";
import styles from "./DealCard.module.css"; import styles from "./DealCard.module.css";
@ -18,11 +19,11 @@ const DealCard = ({ deal, isInGroup = false }: Props) => {
const { selectedProject, modulesSet } = useProjectsContext(); const { selectedProject, modulesSet } = useProjectsContext();
const { dealsCrud, refetchDeals, groupDealsSelection } = useDealsContext(); const { dealsCrud, refetchDeals, groupDealsSelection } = useDealsContext();
const { openDrawer } = useDrawersContext(); const { openDrawer } = useDrawersContext();
const isMobile = useIsMobile();
const onClick = () => { const onClick = () => {
if (groupDealsSelection.isDealsSelecting) { if (groupDealsSelection.isDealsSelecting) {
if (groupDealsSelection.selectedBaseDealId !== deal.id) groupDealsSelection.toggleDeal(deal);
groupDealsSelection.toggleDeal(deal.id);
return; return;
} }
@ -40,17 +41,18 @@ const DealCard = ({ deal, isInGroup = false }: Props) => {
const { showContextMenu } = useContextMenu(); const { showContextMenu } = useContextMenu();
const dealContextMenu = deal.group const dealContextMenu =
? [] deal.group || isMobile
: [ ? []
{ : [
key: "startGroupForming", {
onClick: () => key: "startGroupForming",
groupDealsSelection.startSelectingWithDeal(deal.id), onClick: () =>
title: "Создать группу", groupDealsSelection.startSelectingWithDeal(deal.id),
icon: <IconCategoryPlus />, title: "Создать группу",
}, icon: <IconCategoryPlus />,
]; },
];
const getSelectedStyles = () => { const getSelectedStyles = () => {
if (groupDealsSelection.selectedBaseDealId === deal.id) { if (groupDealsSelection.selectedBaseDealId === deal.id) {
@ -98,10 +100,6 @@ const DealCard = ({ deal, isInGroup = false }: Props) => {
</> </>
)} )}
</Stack> </Stack>
<Group gap={"xs"}>
<Pill className={styles["first-tag"]}>Срочно</Pill>
<Pill className={styles["second-tag"]}>Бесплатно</Pill>
</Group>
</Stack> </Stack>
</Card> </Card>
); );

View File

@ -4,8 +4,10 @@ import classNames from "classnames";
import { useContextMenu } from "mantine-contextmenu"; import { useContextMenu } from "mantine-contextmenu";
import { Flex, Stack, TextInput } from "@mantine/core"; import { Flex, Stack, TextInput } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import GroupMenu from "@/app/deals/components/mobile/GroupMenu/GroupMenu";
import DealCard from "@/app/deals/components/shared/DealCard/DealCard"; import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
import { useDealsContext } from "@/app/deals/contexts/DealsContext"; import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import useIsMobile from "@/hooks/utils/useIsMobile";
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema"; import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
import styles from "./DealsGroup.module.css"; import styles from "./DealsGroup.module.css";
@ -16,36 +18,43 @@ type Props = {
const DealsGroup: FC<Props> = ({ group }) => { const DealsGroup: FC<Props> = ({ group }) => {
const [groupName, setGroupName] = useState(group.name ?? ""); const [groupName, setGroupName] = useState(group.name ?? "");
const [debouncedGroupName] = useDebouncedValue(groupName, 600); const [debouncedGroupName] = useDebouncedValue(groupName, 600);
const { groupsCrud, groupDealsSelection } = useDealsContext(); const {
groupsCrud,
groupDealsSelection: {
startSelectingWithExistingGroup,
selectedGroupId,
},
} = useDealsContext();
const { showContextMenu } = useContextMenu(); const { showContextMenu } = useContextMenu();
const isMobile = useIsMobile();
useEffect(() => { useEffect(() => {
if (debouncedGroupName === group.name) return; if (debouncedGroupName === group.name) return;
groupsCrud.onUpdate(group.id, { name: debouncedGroupName }); groupsCrud.onUpdate(group.id, { name: debouncedGroupName });
}, [debouncedGroupName]); }, [debouncedGroupName]);
const dealContextMenu = [ const dealContextMenu = isMobile
{ ? []
key: "delete", : [
onClick: () => groupsCrud.onDelete(group.id), {
title: "Удалить группу", key: "delete",
icon: <IconTrash />, onClick: () => groupsCrud.onDelete(group.id),
}, title: "Удалить группу",
{ icon: <IconTrash />,
key: "startDealsSelecting", },
onClick: () => {
groupDealsSelection.startSelectingWithExistingGroup(group), key: "startDealsSelecting",
title: "Добавить/удалить сделки", onClick: () => startSelectingWithExistingGroup(group),
icon: <IconCheckbox />, title: "Добавить/удалить сделки",
}, icon: <IconCheckbox />,
]; },
];
return ( return (
<Stack <Stack
className={classNames( className={classNames(
styles["group-container"], styles["group-container"],
groupDealsSelection.selectedGroupId === group.id && selectedGroupId === group.id && styles["selected-group"]
styles["selected-group"]
)} )}
gap={"xs"} gap={"xs"}
bdrs={"lg"} bdrs={"lg"}
@ -53,13 +62,23 @@ const DealsGroup: FC<Props> = ({ group }) => {
onContextMenu={showContextMenu(dealContextMenu)}> onContextMenu={showContextMenu(dealContextMenu)}>
<Flex <Flex
mx={"xs"} mx={"xs"}
align={"center"}> align={"center"}
w={"100%"}>
<TextInput <TextInput
value={groupName} value={groupName}
onChange={e => setGroupName(e.target.value)} onChange={e => setGroupName(e.target.value)}
variant={"unstyled"} variant={"unstyled"}
onKeyDown={e => e.stopPropagation()} onKeyDown={e => e.stopPropagation()}
flex={1}
/> />
{isMobile && (
<GroupMenu
startDealsSelecting={() =>
startSelectingWithExistingGroup(group)
}
onDelete={() => groupsCrud.onDelete(group.id)}
/>
)}
</Flex> </Flex>
{group.items.map(deal => ( {group.items.map(deal => (
<DealCard <DealCard

View File

@ -3,6 +3,7 @@
box-shadow: var(--light-shadow); box-shadow: var(--light-shadow);
} }
@mixin dark { @mixin dark {
background-color: var(--mantine-color-dark-7);
box-shadow: var(--dark-shadow); box-shadow: var(--dark-shadow);
} }
} }

View File

@ -8,10 +8,9 @@ import styles from "./GroupDealsSelectionAffix.module.css";
const GroupDealsSelectionAffix = () => { const GroupDealsSelectionAffix = () => {
const { const {
groupDealsSelection: { groupDealsSelection: {
selectedBaseDealId,
selectedGroupId,
finishDealsSelecting, finishDealsSelecting,
cancelDealsSelecting, cancelDealsSelecting,
isDealsSelecting,
}, },
} = useDealsContext(); } = useDealsContext();
@ -19,7 +18,7 @@ const GroupDealsSelectionAffix = () => {
<Affix position={{ bottom: 35, right: 35 }}> <Affix position={{ bottom: 35, right: 35 }}>
<Transition <Transition
transition="slide-up" transition="slide-up"
mounted={!!(selectedBaseDealId || selectedGroupId)}> mounted={isDealsSelecting}>
{transitionStyles => ( {transitionStyles => (
<Stack <Stack
bdrs={"xl"} bdrs={"xl"}
@ -37,7 +36,9 @@ const GroupDealsSelectionAffix = () => {
<InlineButton onClick={cancelDealsSelecting}> <InlineButton onClick={cancelDealsSelecting}>
Отмена Отмена
</InlineButton> </InlineButton>
<InlineButton variant={"filled"} onClick={finishDealsSelecting}> <InlineButton
variant={"filled"}
onClick={finishDealsSelecting}>
Сохранить Сохранить
</InlineButton> </InlineButton>
</Flex> </Flex>

View File

@ -2,6 +2,7 @@ import React, { FC } from "react";
import { Group, Text } from "@mantine/core"; import { Group, Text } from "@mantine/core";
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu"; import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext"; import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput"; import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
import { StatusSchema } from "@/lib/client"; import { StatusSchema } from "@/lib/client";
@ -14,6 +15,7 @@ type Props = {
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => { const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
const { statusesCrud, refetchStatuses } = useStatusesContext(); const { statusesCrud, refetchStatuses } = useStatusesContext();
const { selectedBoard } = useBoardsContext(); const { selectedBoard } = useBoardsContext();
const { groupDealsSelection } = useDealsContext();
const handleSave = (value: string) => { const handleSave = (value: string) => {
const newValue = value.trim(); const newValue = value.trim();
@ -59,6 +61,9 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
} }
refetchStatuses={refetchStatuses} refetchStatuses={refetchStatuses}
onDeleteStatus={statusesCrud.onDelete} onDeleteStatus={statusesCrud.onDelete}
startDealsSelecting={
groupDealsSelection.startSelecting
}
/> />
</> </>
)} )}

View File

@ -1,5 +1,6 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { import {
IconCheckbox,
IconDotsVertical, IconDotsVertical,
IconEdit, IconEdit,
IconExchange, IconExchange,
@ -21,6 +22,7 @@ type Props = {
onStatusColorChange: (color: string) => void; onStatusColorChange: (color: string) => void;
board: BoardSchema | null; board: BoardSchema | null;
onDeleteStatus: (status: StatusSchema) => void; onDeleteStatus: (status: StatusSchema) => void;
startDealsSelecting?: () => void;
refetchStatuses?: () => void; refetchStatuses?: () => void;
withChangeOrderButton?: boolean; withChangeOrderButton?: boolean;
}; };
@ -31,6 +33,7 @@ const StatusMenu: FC<Props> = ({
onStatusColorChange, onStatusColorChange,
board, board,
onDeleteStatus, onDeleteStatus,
startDealsSelecting,
refetchStatuses, refetchStatuses,
withChangeOrderButton = true, withChangeOrderButton = true,
}) => { }) => {
@ -96,6 +99,13 @@ const StatusMenu: FC<Props> = ({
label={"Изменить порядок"} label={"Изменить порядок"}
/> />
)} )}
{isMobile && startDealsSelecting && (
<DropdownMenuItem
onClick={startDealsSelecting}
icon={<IconCheckbox />}
label={"Создать группу сделок"}
/>
)}
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
); );

View File

@ -2,6 +2,7 @@ import React, { FC } from "react";
import { Box, Group, Text } from "@mantine/core"; import { Box, Group, Text } from "@mantine/core";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu"; import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext"; import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext";
import { BoardSchema, StatusSchema } from "@/lib/client"; import { BoardSchema, StatusSchema } from "@/lib/client";
@ -12,6 +13,7 @@ type Props = {
const StatusMobile: FC<Props> = ({ status, board }) => { const StatusMobile: FC<Props> = ({ status, board }) => {
const { statusesCrud } = useStatusesMobileContext(); const { statusesCrud } = useStatusesMobileContext();
const { groupDealsSelection } = useDealsContext();
const startEditing = () => { const startEditing = () => {
modals.openContextModal({ modals.openContextModal({
@ -40,8 +42,11 @@ const StatusMobile: FC<Props> = ({ status, board }) => {
board={board} board={board}
onDeleteStatus={statusesCrud.onDelete} onDeleteStatus={statusesCrud.onDelete}
handleEdit={startEditing} handleEdit={startEditing}
onStatusColorChange={color => statusesCrud.onUpdate(status.id, { color })} onStatusColorChange={color =>
statusesCrud.onUpdate(status.id, { color })
}
withChangeOrderButton={false} withChangeOrderButton={false}
startDealsSelecting={groupDealsSelection.startSelecting}
/> />
</Group> </Group>
); );

View File

@ -1,5 +1,6 @@
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
import { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud"; import { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud";
import { DealSchema } from "@/lib/client";
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema"; import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
type Props = { type Props = {
@ -12,9 +13,10 @@ export type GroupDealsSelection = {
startSelectingWithDeal: (dealId: number) => void; startSelectingWithDeal: (dealId: number) => void;
selectedGroupId: number | null; selectedGroupId: number | null;
startSelectingWithExistingGroup: (group: GroupWithDealsSchema) => void; startSelectingWithExistingGroup: (group: GroupWithDealsSchema) => void;
startSelecting: () => void;
selectedDealIds: Set<number>; selectedDealIds: Set<number>;
setSelectedDealIds: Dispatch<SetStateAction<Set<number>>>; setSelectedDealIds: Dispatch<SetStateAction<Set<number>>>;
toggleDeal: (dealId: number) => void; toggleDeal: (deal: DealSchema) => void;
finishDealsSelecting: () => void; finishDealsSelecting: () => void;
cancelDealsSelecting: () => void; cancelDealsSelecting: () => void;
}; };
@ -29,11 +31,18 @@ const useGroupDealsSelection = ({ groupsCrud }: Props): GroupDealsSelection => {
); );
const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null); const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null);
const toggleDeal = (dealId: number) => { const toggleDeal = (deal: DealSchema) => {
if (selectedDealIds.has(dealId)) { if (selectedBaseDealId === deal.id) return;
selectedDealIds.delete(dealId);
if (selectedDealIds.has(deal.id)) {
selectedDealIds.delete(deal.id);
} else { } else {
selectedDealIds.add(dealId); if (!selectedBaseDealId && !selectedGroupId) {
if (deal.group) return;
setSelectedBaseDealId(deal.id);
return;
}
selectedDealIds.add(deal.id);
} }
setSelectedDealIds(new Set(selectedDealIds)); setSelectedDealIds(new Set(selectedDealIds));
}; };
@ -63,15 +72,27 @@ const useGroupDealsSelection = ({ groupsCrud }: Props): GroupDealsSelection => {
setIsDealsSelecting(false); setIsDealsSelecting(false);
}; };
// For editing group
const startSelectingWithExistingGroup = (group: GroupWithDealsSchema) => { const startSelectingWithExistingGroup = (group: GroupWithDealsSchema) => {
setSelectedDealIds(new Set(group.items.map(item => item.id))); setSelectedDealIds(new Set(group.items.map(item => item.id)));
setSelectedBaseDealId(null);
setSelectedGroupId(group.id); setSelectedGroupId(group.id);
setIsDealsSelecting(true); setIsDealsSelecting(true);
}; };
// For creating group on desktop
const startSelectingWithDeal = (dealId: number) => { const startSelectingWithDeal = (dealId: number) => {
setSelectedDealIds(new Set([dealId])); setSelectedDealIds(new Set([dealId]));
setSelectedBaseDealId(dealId); setSelectedBaseDealId(dealId);
setSelectedGroupId(null);
setIsDealsSelecting(true);
};
// For creating group on mobile
const startSelecting = () => {
setSelectedDealIds(new Set());
setSelectedBaseDealId(null);
setSelectedGroupId(null);
setIsDealsSelecting(true); setIsDealsSelecting(true);
}; };
@ -81,6 +102,7 @@ const useGroupDealsSelection = ({ groupsCrud }: Props): GroupDealsSelection => {
startSelectingWithDeal, startSelectingWithDeal,
selectedGroupId, selectedGroupId,
startSelectingWithExistingGroup, startSelectingWithExistingGroup,
startSelecting,
selectedDealIds, selectedDealIds,
setSelectedDealIds, setSelectedDealIds,
toggleDeal, toggleDeal,