Compare commits
11 Commits
pragmatic-
...
8cc11bca67
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cc11bca67 | |||
| e44691d118 | |||
| 3a1d8e23e3 | |||
| 9023b07c65 | |||
| 159d6948c7 | |||
| f90b335ee1 | |||
| 5e59d54011 | |||
| f117605ea3 | |||
| 30bc7bbee4 | |||
| daa9d12983 | |||
| 0fe41656f8 |
@ -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",
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { RefObject, useMemo, useRef } from "react";
|
||||
import { IconTag } from "@tabler/icons-react";
|
||||
import { SimpleGrid, Stack } from "@mantine/core";
|
||||
import Action from "@/app/actions/components/Action/Action";
|
||||
import mobileButtonsData from "@/app/actions/data/mobileButtonsData";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||
import BuiltInLinkData from "@/types/BuiltInLinkData";
|
||||
|
||||
const PageBody = () => {
|
||||
const { selectedProject, setSelectedProjectId, projects, modulesSet } =
|
||||
@ -20,6 +22,14 @@ const PageBody = () => {
|
||||
[modulesSet]
|
||||
);
|
||||
|
||||
const commonActionsData: RefObject<BuiltInLinkData[]> = useRef([
|
||||
{
|
||||
icon: IconTag,
|
||||
label: "Теги",
|
||||
href: "/tags",
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<PageBlock fullScreenMobile>
|
||||
<Stack p={"xs"}>
|
||||
@ -33,7 +43,10 @@ const PageBody = () => {
|
||||
<SimpleGrid
|
||||
type={"container"}
|
||||
cols={2}>
|
||||
{filteredMobileButtonsData.map((data, index) => (
|
||||
{[
|
||||
...commonActionsData.current,
|
||||
...filteredMobileButtonsData,
|
||||
].map((data, index) => (
|
||||
<Action
|
||||
linkData={data}
|
||||
key={index}
|
||||
|
||||
41
src/app/deals/components/mobile/GroupMenu/GroupMenu.tsx
Normal file
41
src/app/deals/components/mobile/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";
|
||||
|
||||
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;
|
||||
@ -1,11 +1,13 @@
|
||||
.create-button {
|
||||
cursor: pointer;
|
||||
min-height: max-content;
|
||||
|
||||
border: 1px dashed;
|
||||
@mixin light {
|
||||
background-color: var(--color-light-white-blue);
|
||||
border-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,47 @@
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
border: 1px dashed;
|
||||
@mixin light {
|
||||
background-color: var(--color-light-white-blue);
|
||||
border-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@mixin light {
|
||||
background-color: var(--color-light-aqua);
|
||||
border-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,21 +1,33 @@
|
||||
import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core";
|
||||
import { IconCategoryPlus } from "@tabler/icons-react";
|
||||
import classNames from "classnames";
|
||||
import { useContextMenu } from "mantine-contextmenu";
|
||||
import { Box, Card, Group, Stack, Text } from "@mantine/core";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import { ModuleNames } from "@/modules/modules";
|
||||
import styles from "./DealCard.module.css";
|
||||
import DealTags from "@/components/ui/DealTags/DealTags";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
isInGroup?: boolean;
|
||||
};
|
||||
|
||||
const DealCard = ({ deal }: Props) => {
|
||||
const DealCard = ({ deal, isInGroup = false }: Props) => {
|
||||
const { selectedProject, modulesSet } = useProjectsContext();
|
||||
const { dealsCrud, refetchDeals } = useDealsContext();
|
||||
const { dealsCrud, refetchDeals, groupDealsSelection } = useDealsContext();
|
||||
const { openDrawer } = useDrawersContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const onClick = () => {
|
||||
if (groupDealsSelection.isDealsSelecting) {
|
||||
groupDealsSelection.toggleDeal(deal);
|
||||
return;
|
||||
}
|
||||
|
||||
openDrawer({
|
||||
key: "dealEditorDrawer",
|
||||
props: {
|
||||
@ -28,10 +40,38 @@ const DealCard = ({ deal }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const { showContextMenu } = useContextMenu();
|
||||
|
||||
const dealContextMenu =
|
||||
deal.group || isMobile
|
||||
? []
|
||||
: [
|
||||
{
|
||||
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={styles.container}>
|
||||
className={classNames(
|
||||
getSelectedStyles(),
|
||||
isInGroup ? styles["container-in-group"] : styles.container
|
||||
)}
|
||||
onContextMenu={showContextMenu(dealContextMenu)}>
|
||||
<Group
|
||||
justify={"space-between"}
|
||||
wrap={"nowrap"}
|
||||
@ -61,10 +101,7 @@ const DealCard = ({ deal }: Props) => {
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Group gap={"xs"}>
|
||||
<Pill className={styles["first-tag"]}>Срочно</Pill>
|
||||
<Pill className={styles["second-tag"]}>Бесплатно</Pill>
|
||||
</Group>
|
||||
{!deal.group && <DealTags dealId={deal.id} tags={deal.tags} />}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
import React, { FC, useMemo } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import SortableItem from "@/components/dnd/SortableItem";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
};
|
||||
|
||||
const DealContainer: FC<Props> = ({ deal }) => {
|
||||
const dealBody = useMemo(() => <DealCard deal={deal} />, [deal]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SortableItem
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
id={deal.id}
|
||||
renderItem={() => dealBody}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealContainer;
|
||||
@ -0,0 +1,22 @@
|
||||
|
||||
.group-container {
|
||||
border: 1px dashed;
|
||||
@mixin light {
|
||||
background-color: var(--color-light-white-blue);
|
||||
border-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
.selected-group {
|
||||
border: 2px solid;
|
||||
@mixin light {
|
||||
border-color: dodgerblue;
|
||||
}
|
||||
@mixin dark {
|
||||
border-color: dodgerblue;
|
||||
}
|
||||
}
|
||||
101
src/app/deals/components/shared/DealsGroup/DealsGroup.tsx
Normal file
101
src/app/deals/components/shared/DealsGroup/DealsGroup.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { IconCheckbox, IconTrash } from "@tabler/icons-react";
|
||||
import classNames from "classnames";
|
||||
import { useContextMenu } from "mantine-contextmenu";
|
||||
import { Flex, Stack, TextInput } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import GroupMenu from "@/app/deals/components/mobile/GroupMenu/GroupMenu";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import DealTags from "@/components/ui/DealTags/DealTags";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
import styles from "./DealsGroup.module.css";
|
||||
|
||||
type Props = {
|
||||
group: GroupWithDealsSchema;
|
||||
};
|
||||
|
||||
const DealsGroup: FC<Props> = ({ group }) => {
|
||||
const [groupName, setGroupName] = useState(group.name ?? "");
|
||||
const [debouncedGroupName] = useDebouncedValue(groupName, 600);
|
||||
const {
|
||||
groupsCrud,
|
||||
groupDealsSelection: {
|
||||
startSelectingWithExistingGroup,
|
||||
selectedGroupId,
|
||||
},
|
||||
} = useDealsContext();
|
||||
const { showContextMenu } = useContextMenu();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedGroupName === group.name) return;
|
||||
groupsCrud.onUpdate(group.id, { name: debouncedGroupName });
|
||||
}, [debouncedGroupName]);
|
||||
|
||||
const dealContextMenu = isMobile
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: "delete",
|
||||
onClick: () => groupsCrud.onDelete(group.id),
|
||||
title: "Удалить группу",
|
||||
icon: <IconTrash />,
|
||||
},
|
||||
{
|
||||
key: "startDealsSelecting",
|
||||
onClick: () => startSelectingWithExistingGroup(group),
|
||||
title: "Добавить/удалить сделки",
|
||||
icon: <IconCheckbox />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className={classNames(
|
||||
styles["group-container"],
|
||||
selectedGroupId === group.id && styles["selected-group"]
|
||||
)}
|
||||
gap={"xs"}
|
||||
bdrs={"lg"}
|
||||
p={"xs"}
|
||||
onContextMenu={showContextMenu(dealContextMenu)}>
|
||||
<Flex
|
||||
mx={"xs"}
|
||||
align={"center"}
|
||||
w={"100%"}>
|
||||
<TextInput
|
||||
value={groupName}
|
||||
onChange={e => setGroupName(e.target.value)}
|
||||
variant={"unstyled"}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
flex={1}
|
||||
/>
|
||||
{isMobile && (
|
||||
<GroupMenu
|
||||
startDealsSelecting={() =>
|
||||
startSelectingWithExistingGroup(group)
|
||||
}
|
||||
onDelete={() => groupsCrud.onDelete(group.id)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{group.items.map(deal => (
|
||||
<DealCard
|
||||
deal={deal}
|
||||
isInGroup
|
||||
key={deal.id}
|
||||
/>
|
||||
))}
|
||||
{group.items.length > 0 && (
|
||||
<DealTags
|
||||
groupId={group.id}
|
||||
tags={group.items[0].tags}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealsGroup;
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
||||
import DealsGroup from "@/app/deals/components/shared/DealsGroup/DealsGroup";
|
||||
import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader";
|
||||
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
@ -11,37 +11,36 @@ import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||
|
||||
const Funnel: FC = () => {
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const { deals } = useDealsContext();
|
||||
const { dealsWithoutGroup, groupsWithDeals } = useDealsContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const {
|
||||
sortedStatuses,
|
||||
handleDragStart,
|
||||
handleDragOver,
|
||||
handleDragEnd,
|
||||
activeStatus,
|
||||
activeDeal,
|
||||
swiperRef,
|
||||
} = useDealsAndStatusesDnd();
|
||||
const { sortedStatuses, handleDragOver, handleDragEnd, swiperRef } =
|
||||
useDealsAndStatusesDnd();
|
||||
|
||||
return (
|
||||
<FunnelDnd
|
||||
<FunnelDnd<StatusSchema, DealSchema, GroupWithDealsSchema>
|
||||
containers={sortedStatuses}
|
||||
items={deals}
|
||||
onDragStart={handleDragStart}
|
||||
itemsAndGroups={sortByLexorank([
|
||||
...dealsWithoutGroup,
|
||||
...groupsWithDeals,
|
||||
])}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
swiperRef={swiperRef}
|
||||
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
||||
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
||||
sortByLexorank(
|
||||
items.filter(deal => deal.status.id === status.id)
|
||||
)
|
||||
getItemsByContainer={(status: StatusSchema) =>
|
||||
sortByLexorank([
|
||||
...dealsWithoutGroup.filter(
|
||||
deal => deal.status.id === status.id
|
||||
),
|
||||
...groupsWithDeals.filter(
|
||||
group => group.items[0].status.id === status.id
|
||||
),
|
||||
])
|
||||
}
|
||||
renderContainer={(
|
||||
status: StatusSchema,
|
||||
@ -59,25 +58,28 @@ const Funnel: FC = () => {
|
||||
renderContainerHeader={status => (
|
||||
<StatusColumnHeader
|
||||
status={status}
|
||||
isDragging={activeStatus?.id === status.id}
|
||||
isDragging={false}
|
||||
/>
|
||||
)}
|
||||
renderItem={(deal: DealSchema) => (
|
||||
<DealContainer
|
||||
<DealCard
|
||||
key={deal.id}
|
||||
deal={deal}
|
||||
/>
|
||||
)}
|
||||
activeContainer={activeStatus}
|
||||
activeItem={activeDeal}
|
||||
renderItemOverlay={(deal: DealSchema) => <DealCard deal={deal} />}
|
||||
renderGroup={(group: GroupWithDealsSchema) => (
|
||||
<DealsGroup
|
||||
key={`${group.id}group`}
|
||||
group={group}
|
||||
/>
|
||||
)}
|
||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
renderHeader={() => (
|
||||
<StatusColumnHeader
|
||||
status={status}
|
||||
isDragging={activeStatus?.id === status.id}
|
||||
isDragging
|
||||
/>
|
||||
)}>
|
||||
{children}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
.shadow {
|
||||
@mixin light {
|
||||
box-shadow: var(--light-shadow);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
box-shadow: var(--dark-shadow);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
"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: {
|
||||
finishDealsSelecting,
|
||||
cancelDealsSelecting,
|
||||
isDealsSelecting,
|
||||
},
|
||||
} = useDealsContext();
|
||||
|
||||
return (
|
||||
<Affix position={{ bottom: 35, right: 35 }}>
|
||||
<Transition
|
||||
transition="slide-up"
|
||||
mounted={isDealsSelecting}>
|
||||
{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;
|
||||
@ -2,6 +2,7 @@ import React, { FC } from "react";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
@ -14,6 +15,7 @@ type Props = {
|
||||
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
||||
const { statusesCrud, refetchStatuses } = useStatusesContext();
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const { groupDealsSelection } = useDealsContext();
|
||||
|
||||
const handleSave = (value: string) => {
|
||||
const newValue = value.trim();
|
||||
@ -59,6 +61,9 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
||||
}
|
||||
refetchStatuses={refetchStatuses}
|
||||
onDeleteStatus={statusesCrud.onDelete}
|
||||
startDealsSelecting={
|
||||
groupDealsSelection.startSelecting
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { FC } from "react";
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconDotsVertical,
|
||||
IconEdit,
|
||||
IconExchange,
|
||||
@ -21,6 +22,7 @@ type Props = {
|
||||
onStatusColorChange: (color: string) => void;
|
||||
board: BoardSchema | null;
|
||||
onDeleteStatus: (status: StatusSchema) => void;
|
||||
startDealsSelecting?: () => void;
|
||||
refetchStatuses?: () => void;
|
||||
withChangeOrderButton?: boolean;
|
||||
};
|
||||
@ -31,6 +33,7 @@ const StatusMenu: FC<Props> = ({
|
||||
onStatusColorChange,
|
||||
board,
|
||||
onDeleteStatus,
|
||||
startDealsSelecting,
|
||||
refetchStatuses,
|
||||
withChangeOrderButton = true,
|
||||
}) => {
|
||||
@ -96,6 +99,13 @@ const StatusMenu: FC<Props> = ({
|
||||
label={"Изменить порядок"}
|
||||
/>
|
||||
)}
|
||||
{isMobile && startDealsSelecting && (
|
||||
<DropdownMenuItem
|
||||
onClick={startDealsSelecting}
|
||||
icon={<IconCheckbox />}
|
||||
label={"Создать группу сделок"}
|
||||
/>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@ -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,26 +1,36 @@
|
||||
"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";
|
||||
import { SortingForm } from "@/hooks/utils/useSorting";
|
||||
import { DealSchema, PaginationInfoSchema } from "@/lib/client";
|
||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
|
||||
type DealsContextState = {
|
||||
deals: DealSchema[];
|
||||
setDeals: (deals: DealSchema[]) => void;
|
||||
dealsWithoutGroup: DealSchema[];
|
||||
groupsWithDeals: GroupWithDealsSchema[];
|
||||
refetchDeals: () => void;
|
||||
dealsCrud: DealsCrud;
|
||||
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 = {
|
||||
@ -48,9 +58,20 @@ const useDealsContextState = ({
|
||||
statuses,
|
||||
});
|
||||
|
||||
const groupsCrud = useDealGroupCrud();
|
||||
|
||||
const groupDealsSelection = useGroupDealsSelection({ groupsCrud });
|
||||
|
||||
const { dealsWithoutGroup, groupsWithDeals } =
|
||||
useDealsAndGroups(dealsListObjects);
|
||||
|
||||
return {
|
||||
...dealsListObjects,
|
||||
dealsCrud,
|
||||
groupsCrud,
|
||||
dealsWithoutGroup,
|
||||
groupsWithDeals,
|
||||
groupDealsSelection,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import ProjectEditorDrawer from "@/app/deals/drawers/ProjectEditorDrawer/ProjectEditorDrawer";
|
||||
|
||||
export default ProjectEditorDrawer;
|
||||
@ -2,6 +2,7 @@ import React, { FC } from "react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
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 { BoardSchema, StatusSchema } from "@/lib/client";
|
||||
|
||||
@ -12,6 +13,7 @@ type Props = {
|
||||
|
||||
const StatusMobile: FC<Props> = ({ status, board }) => {
|
||||
const { statusesCrud } = useStatusesMobileContext();
|
||||
const { groupDealsSelection } = useDealsContext();
|
||||
|
||||
const startEditing = () => {
|
||||
modals.openContextModal({
|
||||
@ -40,8 +42,11 @@ const StatusMobile: FC<Props> = ({ status, board }) => {
|
||||
board={board}
|
||||
onDeleteStatus={statusesCrud.onDelete}
|
||||
handleEdit={startEditing}
|
||||
onStatusColorChange={color => statusesCrud.onUpdate(status.id, { color })}
|
||||
onStatusColorChange={color =>
|
||||
statusesCrud.onUpdate(status.id, { color })
|
||||
}
|
||||
withChangeOrderButton={false}
|
||||
startDealsSelecting={groupDealsSelection.startSelecting}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
|
||||
44
src/app/deals/hooks/useDealsAndGroups.ts
Normal file
44
src/app/deals/hooks/useDealsAndGroups.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { useMemo } from "react";
|
||||
import { isNull } from "lodash";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||
|
||||
type Props = {
|
||||
deals: DealSchema[];
|
||||
};
|
||||
|
||||
const useDealsAndGroups = ({ deals }: Props) => {
|
||||
const dealsWithoutGroup: DealSchema[] = useMemo(
|
||||
() => deals.filter(d => isNull(d.group)),
|
||||
[deals]
|
||||
);
|
||||
|
||||
const groupsWithDeals: GroupWithDealsSchema[] = useMemo(() => {
|
||||
const groupsWithDealMap = new Map<number, GroupWithDealsSchema>();
|
||||
|
||||
for (const deal of deals) {
|
||||
if (isNull(deal.group)) continue;
|
||||
|
||||
const groupData = groupsWithDealMap.get(deal.group.id);
|
||||
if (groupData) {
|
||||
groupData.items.push(deal);
|
||||
groupsWithDealMap.set(deal.group.id, groupData);
|
||||
} else {
|
||||
groupsWithDealMap.set(deal.group.id, {
|
||||
...deal.group,
|
||||
items: [deal],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sortByLexorank(groupsWithDealMap.values().toArray());
|
||||
}, [deals]);
|
||||
|
||||
return {
|
||||
dealsWithoutGroup,
|
||||
groupsWithDeals,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDealsAndGroups;
|
||||
@ -1,32 +1,41 @@
|
||||
import { RefObject, useMemo, useRef, useState } from "react";
|
||||
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
||||
import { RefObject, useMemo, useRef } from "react";
|
||||
import { DragOverEvent, Over } from "@dnd-kit/core";
|
||||
import { SwiperRef } from "swiper/swiper-react";
|
||||
import { useDebouncedCallback } from "@mantine/hooks";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
||||
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
||||
import isItemGroup from "@/app/deals/utils/isItemGroup";
|
||||
import {
|
||||
getContainerId,
|
||||
isContainerId,
|
||||
} from "@/components/dnd/FunnelDnd/utils/columnId";
|
||||
import {
|
||||
getGroupId,
|
||||
isGroupId,
|
||||
} from "@/components/dnd/FunnelDnd/utils/groupId";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||
|
||||
type ReturnType = {
|
||||
sortedStatuses: StatusSchema[];
|
||||
handleDragStart: ({ active }: DragStartEvent) => void;
|
||||
handleDragOver: ({ active, over }: DragOverEvent) => void;
|
||||
handleDragEnd: ({ active, over }: DragOverEvent) => void;
|
||||
activeStatus: StatusSchema | null;
|
||||
activeDeal: DealSchema | null;
|
||||
swiperRef: RefObject<SwiperRef | null>;
|
||||
};
|
||||
|
||||
const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
const swiperRef = useRef<SwiperRef>(null);
|
||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
|
||||
const { deals, setDeals, dealsCrud } = useDealsContext();
|
||||
const {
|
||||
deals,
|
||||
dealsWithoutGroup,
|
||||
groupsWithDeals,
|
||||
setDeals,
|
||||
dealsCrud,
|
||||
groupsCrud,
|
||||
} = useDealsContext();
|
||||
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@ -40,17 +49,36 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
const debouncedSetDeals = useDebouncedCallback(setDeals, 200);
|
||||
|
||||
const getStatusByDealId = (dealId: number) => {
|
||||
const deal = deals.find(deal => deal.id === dealId);
|
||||
const deal = dealsWithoutGroup.find(deal => deal.id === dealId);
|
||||
if (!deal) return;
|
||||
return statuses.find(status => status.id === deal.status.id);
|
||||
};
|
||||
|
||||
const getStatusByGroupId = (groupId: number) => {
|
||||
const group = groupsWithDeals.find(group => group.id === groupId);
|
||||
if (!group || group.items.length === 0) return;
|
||||
return statuses.find(status => status.id === group.items[0].status.id);
|
||||
};
|
||||
|
||||
const getStatusById = (statusId: number) => {
|
||||
return statuses.find(status => status.id === statusId);
|
||||
};
|
||||
|
||||
const swipeSliderDuringDrag = (activeId: number, over: Over) => {
|
||||
const activeStatus = getStatusByDealId(activeId);
|
||||
const getStatusDealsAndGroups = (statusId: number) =>
|
||||
sortByLexorank([
|
||||
...dealsWithoutGroup.filter(d => d.status.id === statusId),
|
||||
...groupsWithDeals.filter(
|
||||
g => g.items.length > 0 && g.items[0].status.id === statusId
|
||||
),
|
||||
]);
|
||||
|
||||
const swipeSliderDuringDrag = (activeId: number | string, over: Over) => {
|
||||
let activeStatus: StatusSchema | undefined;
|
||||
if (typeof activeId === "string") {
|
||||
activeStatus = getStatusByGroupId(getGroupId(activeId));
|
||||
} else {
|
||||
activeStatus = getStatusByDealId(Number(activeId));
|
||||
}
|
||||
const swiperActiveStatus =
|
||||
statuses[swiperRef.current?.swiper.activeIndex ?? 0];
|
||||
if (swiperActiveStatus.id !== activeStatus?.id) return;
|
||||
@ -58,11 +86,16 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
const activeStatusLexorank = activeStatus?.lexorank;
|
||||
let overStatusLexorank: string | undefined;
|
||||
|
||||
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||
const overStatusId = getStatusId(over.id);
|
||||
overStatusLexorank = statuses.find(
|
||||
s => s.id === overStatusId
|
||||
)?.lexorank;
|
||||
if (typeof over.id === "string") {
|
||||
if (isContainerId(over.id)) {
|
||||
const overStatusId = getContainerId(over.id);
|
||||
overStatusLexorank = statuses.find(
|
||||
s => s.id === overStatusId
|
||||
)?.lexorank;
|
||||
} else {
|
||||
const overGroupId = getGroupId(over.id);
|
||||
overStatusLexorank = getStatusByGroupId(overGroupId)?.lexorank;
|
||||
}
|
||||
} else {
|
||||
overStatusLexorank = getStatusByDealId(Number(over.id))?.lexorank;
|
||||
}
|
||||
@ -94,15 +127,20 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
if (!over) return;
|
||||
const activeId = active.id as string | number;
|
||||
|
||||
if (isMobile && typeof activeId !== "string") {
|
||||
if (isMobile && (typeof activeId !== "string" || isGroupId(activeId))) {
|
||||
swipeSliderDuringDrag(activeId, over);
|
||||
}
|
||||
|
||||
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||
if (typeof activeId !== "string") {
|
||||
handleDealDragOver(activeId, over);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContainerId(activeId)) {
|
||||
handleColumnDragOver(activeId, over);
|
||||
return;
|
||||
}
|
||||
handleDealDragOver(activeId, over);
|
||||
handleGroupDragOver(activeId, over);
|
||||
};
|
||||
|
||||
const handleDealDragOver = (activeId: string | number, over: Over) => {
|
||||
@ -113,6 +151,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
const { overStatus, newLexorank } = getDropTarget(
|
||||
over.id,
|
||||
activeDealId,
|
||||
undefined,
|
||||
activeStatusId
|
||||
);
|
||||
if (!overStatus) return;
|
||||
@ -130,14 +169,49 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleGroupDragOver = (activeId: string, over: Over) => {
|
||||
const activeGroupId = getGroupId(activeId);
|
||||
const activeStatusId = getStatusByGroupId(activeGroupId)?.id;
|
||||
if (!activeStatusId) return;
|
||||
|
||||
const { overStatus, newLexorank } = getDropTarget(
|
||||
over.id,
|
||||
undefined,
|
||||
activeGroupId,
|
||||
activeStatusId
|
||||
);
|
||||
if (!overStatus) return;
|
||||
|
||||
debouncedSetDeals(
|
||||
deals.map(deal =>
|
||||
deal.group && deal.group.id === activeGroupId
|
||||
? {
|
||||
...deal,
|
||||
status: overStatus,
|
||||
group: {
|
||||
...deal.group,
|
||||
lexorank: newLexorank || deal.group.lexorank,
|
||||
},
|
||||
}
|
||||
: deal
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleColumnDragOver = (activeId: string, over: Over) => {
|
||||
const activeStatusId = getStatusId(activeId);
|
||||
const activeStatusId = getContainerId(activeId);
|
||||
let overStatusId: number;
|
||||
|
||||
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||
overStatusId = getStatusId(over.id);
|
||||
if (typeof over.id === "string") {
|
||||
if (isContainerId(over.id)) {
|
||||
overStatusId = getContainerId(over.id);
|
||||
} else {
|
||||
const status = getStatusByGroupId(getGroupId(over.id));
|
||||
if (!status) return;
|
||||
overStatusId = status.id;
|
||||
}
|
||||
} else {
|
||||
const deal = deals.find(deal => deal.id === over.id);
|
||||
const deal = dealsWithoutGroup.find(deal => deal.id === over.id);
|
||||
if (!deal) return;
|
||||
overStatusId = deal.status.id;
|
||||
}
|
||||
@ -158,17 +232,44 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
|
||||
const getDropTarget = (
|
||||
overId: string | number,
|
||||
activeDealId: number,
|
||||
activeDealId: number | undefined,
|
||||
activeGroupId: number | undefined,
|
||||
activeStatusId: number,
|
||||
isOnDragEnd: boolean = false
|
||||
): { overStatus?: StatusSchema; newLexorank?: string } => {
|
||||
if (typeof overId === "string") {
|
||||
return {
|
||||
overStatus: getStatusById(getStatusId(overId)),
|
||||
newLexorank: undefined,
|
||||
};
|
||||
if (isContainerId(overId)) {
|
||||
return getStatusDropTarget(overId);
|
||||
}
|
||||
if (isGroupId(overId)) {
|
||||
return getGroupDropTarget(
|
||||
overId,
|
||||
activeGroupId,
|
||||
activeStatusId,
|
||||
isOnDragEnd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return getDealDropTarget(
|
||||
Number(overId),
|
||||
activeDealId,
|
||||
activeStatusId,
|
||||
isOnDragEnd
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusDropTarget = (overId: string) => ({
|
||||
overStatus: getStatusById(getContainerId(overId)),
|
||||
newLexorank: undefined,
|
||||
});
|
||||
|
||||
const getDealDropTarget = (
|
||||
overId: number,
|
||||
activeDealId: number | undefined,
|
||||
activeStatusId: number,
|
||||
isOnDragEnd: boolean = false
|
||||
) => {
|
||||
const overDealId = Number(overId);
|
||||
const overStatus = getStatusByDealId(overDealId);
|
||||
|
||||
@ -176,51 +277,93 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
return { overStatus: undefined, newLexorank: undefined };
|
||||
}
|
||||
|
||||
const statusDeals = sortByLexorank(
|
||||
deals.filter(deal => deal.status.id === overStatus.id)
|
||||
const statusItems = getStatusDealsAndGroups(overStatus.id);
|
||||
const overDealIndex = statusItems.findIndex(
|
||||
deal => !isItemGroup(deal) && deal.id === overDealId
|
||||
);
|
||||
const overDealIndex = statusDeals.findIndex(
|
||||
deal => deal.id === overDealId
|
||||
const activeDealIndex = statusItems.findIndex(
|
||||
deal => !isItemGroup(deal) && deal.id === activeDealId
|
||||
);
|
||||
|
||||
if (activeStatusId === overStatus.id) {
|
||||
const newLexorank = getNewRankForSameStatus(
|
||||
statusDeals,
|
||||
statusItems,
|
||||
overDealIndex,
|
||||
activeDealId
|
||||
activeDealIndex
|
||||
);
|
||||
return { overStatus, newLexorank };
|
||||
}
|
||||
|
||||
const newLexorank = getNewRankForAnotherStatus(
|
||||
statusDeals,
|
||||
statusItems,
|
||||
overDealIndex
|
||||
);
|
||||
return { overStatus, newLexorank };
|
||||
};
|
||||
|
||||
const getGroupDropTarget = (
|
||||
overId: string,
|
||||
activeGroupId: number | undefined,
|
||||
activeStatusId: number,
|
||||
isOnDragEnd: boolean = false
|
||||
) => {
|
||||
const overGroupId = getGroupId(overId);
|
||||
const overStatus = getStatusByGroupId(overGroupId);
|
||||
|
||||
if (!overStatus || (!isOnDragEnd && activeGroupId === overGroupId)) {
|
||||
return { overStatus: undefined, newLexorank: undefined };
|
||||
}
|
||||
|
||||
const statusItems = getStatusDealsAndGroups(overStatus.id);
|
||||
const overGroupIndex = statusItems.findIndex(
|
||||
group => isItemGroup(group) && group.id === overGroupId
|
||||
);
|
||||
const activeGroupIndex = statusItems.findIndex(
|
||||
group => isItemGroup(group) && group.id === activeGroupId
|
||||
);
|
||||
|
||||
if (activeStatusId === overStatus.id) {
|
||||
const newLexorank = getNewRankForSameStatus(
|
||||
statusItems,
|
||||
overGroupIndex,
|
||||
activeGroupIndex
|
||||
);
|
||||
return { overStatus, newLexorank };
|
||||
}
|
||||
|
||||
const newLexorank = getNewRankForAnotherStatus(
|
||||
statusItems,
|
||||
overGroupIndex
|
||||
);
|
||||
return { overStatus, newLexorank };
|
||||
};
|
||||
|
||||
const handleDragEnd = ({ active, over }: DragOverEvent) => {
|
||||
setActiveDeal(null);
|
||||
setActiveStatus(null);
|
||||
if (!over) return;
|
||||
|
||||
const activeId: string | number = active.id;
|
||||
|
||||
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||
if (typeof activeId !== "string") {
|
||||
handleDealDragEnd(activeId, over);
|
||||
return;
|
||||
}
|
||||
if (isContainerId(activeId)) {
|
||||
handleStatusColumnDragEnd(activeId, over);
|
||||
return;
|
||||
}
|
||||
handleDealDragEnd(activeId, over);
|
||||
handleGroupDragEnd(activeId, over);
|
||||
};
|
||||
|
||||
const handleStatusColumnDragEnd = (activeId: string, over: Over) => {
|
||||
const activeStatusId = getStatusId(activeId);
|
||||
const activeStatusId = getContainerId(activeId);
|
||||
let overStatusId: number;
|
||||
|
||||
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||
overStatusId = getStatusId(over.id);
|
||||
if (typeof over.id === "string" && isContainerId(over.id)) {
|
||||
overStatusId = getContainerId(over.id);
|
||||
} else {
|
||||
const deal = deals.find(deal => deal.status.id === over.id);
|
||||
const deal = dealsWithoutGroup.find(
|
||||
deal => deal.status.id === over.id
|
||||
);
|
||||
if (!deal) return;
|
||||
overStatusId = deal.status.id;
|
||||
}
|
||||
@ -245,6 +388,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
const { overStatus, newLexorank } = getDropTarget(
|
||||
over.id,
|
||||
activeDealId,
|
||||
undefined,
|
||||
activeStatusId,
|
||||
true
|
||||
);
|
||||
@ -261,30 +405,36 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
dealsCrud.onUpdate(dealId, { statusId, lexorank, name: null });
|
||||
};
|
||||
|
||||
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||
const activeId = active.id as string | number;
|
||||
const handleGroupDragEnd = (activeId: string, over: Over) => {
|
||||
const activeGroupId = getGroupId(activeId);
|
||||
const activeStatusId = getStatusByGroupId(activeGroupId)?.id;
|
||||
if (!activeStatusId) return;
|
||||
|
||||
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||
const statusId = getStatusId(activeId);
|
||||
setActiveStatus(
|
||||
statuses.find(status => status.id === statusId) ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveDeal(
|
||||
deals.find(deal => deal.id === (activeId as number)) ?? null
|
||||
const { overStatus, newLexorank } = getDropTarget(
|
||||
over.id,
|
||||
undefined,
|
||||
activeGroupId,
|
||||
activeStatusId,
|
||||
true
|
||||
);
|
||||
if (!overStatus) return;
|
||||
|
||||
onGroupDragEnd(activeGroupId, overStatus.id, newLexorank);
|
||||
};
|
||||
|
||||
const onGroupDragEnd = (
|
||||
groupId: number,
|
||||
statusId: number,
|
||||
lexorank?: string
|
||||
) => {
|
||||
groupsCrud.onUpdate(groupId, { statusId, lexorank, name: null });
|
||||
};
|
||||
|
||||
return {
|
||||
swiperRef,
|
||||
sortedStatuses,
|
||||
handleDragStart,
|
||||
handleDragOver,
|
||||
handleDragEnd,
|
||||
activeStatus,
|
||||
activeDeal,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,18 +1,24 @@
|
||||
import { LexoRank } from "lexorank";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||
import {
|
||||
BaseDraggable,
|
||||
BaseGroupDraggable,
|
||||
} from "@/components/dnd/types/types";
|
||||
import { getNewLexorank } from "@/utils/lexorank/generation";
|
||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||
|
||||
type NewRankGetters = {
|
||||
type NewRankGetters<
|
||||
TItem extends BaseDraggable,
|
||||
TGroup extends BaseGroupDraggable<TItem>,
|
||||
> = {
|
||||
getNewRankForSameStatus: (
|
||||
statusDeals: DealSchema[],
|
||||
overDealIndex: number,
|
||||
activeDealId: number
|
||||
statusItemsAndGroups: (TItem | TGroup)[],
|
||||
overItemOrGroupIndex: number,
|
||||
activeItemOrGroupIndex: number
|
||||
) => string;
|
||||
getNewRankForAnotherStatus: (
|
||||
statusDeals: DealSchema[],
|
||||
overDealIndex: number
|
||||
statusItemsAndGroups: (TItem | TGroup)[],
|
||||
overItemOrGroupIndex: number
|
||||
) => string;
|
||||
getNewStatusRank: (
|
||||
activeStatusId: number,
|
||||
@ -20,44 +26,46 @@ type NewRankGetters = {
|
||||
) => string | null;
|
||||
};
|
||||
|
||||
const useGetNewRank = (): NewRankGetters => {
|
||||
const useGetNewRank = <
|
||||
TItem extends BaseDraggable,
|
||||
TGroup extends BaseGroupDraggable<TItem>,
|
||||
>(): NewRankGetters<TItem, TGroup> => {
|
||||
const { statuses } = useStatusesContext();
|
||||
|
||||
const getNewRankForSameStatus = (
|
||||
statusDeals: DealSchema[],
|
||||
overDealIndex: number,
|
||||
activeDealId: number
|
||||
statusItemsAndGroups: (TItem | TGroup)[],
|
||||
overItemOrGroupIndex: number,
|
||||
activeItemOrGroupIndex: number
|
||||
): string => {
|
||||
const activeDealIndex = statusDeals.findIndex(
|
||||
deal => deal.id === activeDealId
|
||||
);
|
||||
const [leftIndex, rightIndex] =
|
||||
overDealIndex < activeDealIndex
|
||||
? [overDealIndex - 1, overDealIndex]
|
||||
: [overDealIndex, overDealIndex + 1];
|
||||
overItemOrGroupIndex < activeItemOrGroupIndex
|
||||
? [overItemOrGroupIndex - 1, overItemOrGroupIndex]
|
||||
: [overItemOrGroupIndex, overItemOrGroupIndex + 1];
|
||||
|
||||
const leftLexorank =
|
||||
leftIndex >= 0
|
||||
? LexoRank.parse(statusDeals[leftIndex].lexorank)
|
||||
? LexoRank.parse(statusItemsAndGroups[leftIndex].lexorank)
|
||||
: null;
|
||||
const rightLexorank =
|
||||
rightIndex < statusDeals.length
|
||||
? LexoRank.parse(statusDeals[rightIndex].lexorank)
|
||||
rightIndex < statusItemsAndGroups.length
|
||||
? LexoRank.parse(statusItemsAndGroups[rightIndex].lexorank)
|
||||
: null;
|
||||
|
||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||
};
|
||||
|
||||
const getNewRankForAnotherStatus = (
|
||||
statusDeals: DealSchema[],
|
||||
overDealIndex: number
|
||||
statusItemsAndGroups: (TItem | TGroup)[],
|
||||
overItemOrGroupIndex: number
|
||||
): string => {
|
||||
const leftLexorank =
|
||||
overDealIndex > 0
|
||||
? LexoRank.parse(statusDeals[overDealIndex - 1].lexorank)
|
||||
overItemOrGroupIndex > 0
|
||||
? LexoRank.parse(
|
||||
statusItemsAndGroups[overItemOrGroupIndex - 1].lexorank
|
||||
)
|
||||
: null;
|
||||
const rightLexorank = LexoRank.parse(
|
||||
statusDeals[overDealIndex].lexorank
|
||||
statusItemsAndGroups[overItemOrGroupIndex].lexorank
|
||||
);
|
||||
|
||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||
|
||||
114
src/app/deals/hooks/useGroupDealsSelection.tsx
Normal file
114
src/app/deals/hooks/useGroupDealsSelection.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
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;
|
||||
startSelecting: () => void;
|
||||
selectedDealIds: Set<number>;
|
||||
setSelectedDealIds: Dispatch<SetStateAction<Set<number>>>;
|
||||
toggleDeal: (deal: DealSchema) => 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 = (deal: DealSchema) => {
|
||||
if (selectedBaseDealId === deal.id) return;
|
||||
|
||||
if (selectedDealIds.has(deal.id)) {
|
||||
selectedDealIds.delete(deal.id);
|
||||
} else {
|
||||
if (!selectedBaseDealId && !selectedGroupId) {
|
||||
if (deal.group) return;
|
||||
setSelectedBaseDealId(deal.id);
|
||||
return;
|
||||
}
|
||||
selectedDealIds.add(deal.id);
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
// For editing group
|
||||
const startSelectingWithExistingGroup = (group: GroupWithDealsSchema) => {
|
||||
setSelectedDealIds(new Set(group.items.map(item => item.id)));
|
||||
setSelectedBaseDealId(null);
|
||||
setSelectedGroupId(group.id);
|
||||
setIsDealsSelecting(true);
|
||||
};
|
||||
|
||||
// For creating group on desktop
|
||||
const startSelectingWithDeal = (dealId: number) => {
|
||||
setSelectedDealIds(new Set([dealId]));
|
||||
setSelectedBaseDealId(dealId);
|
||||
setSelectedGroupId(null);
|
||||
setIsDealsSelecting(true);
|
||||
};
|
||||
|
||||
// For creating group on mobile
|
||||
const startSelecting = () => {
|
||||
setSelectedDealIds(new Set());
|
||||
setSelectedBaseDealId(null);
|
||||
setSelectedGroupId(null);
|
||||
setIsDealsSelecting(true);
|
||||
};
|
||||
|
||||
return {
|
||||
isDealsSelecting,
|
||||
selectedBaseDealId,
|
||||
startSelectingWithDeal,
|
||||
selectedGroupId,
|
||||
startSelectingWithExistingGroup,
|
||||
startSelecting,
|
||||
selectedDealIds,
|
||||
setSelectedDealIds,
|
||||
toggleDeal,
|
||||
finishDealsSelecting,
|
||||
cancelDealsSelecting,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGroupDealsSelection;
|
||||
15
src/app/deals/utils/isItemGroup.ts
Normal file
15
src/app/deals/utils/isItemGroup.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {
|
||||
BaseDraggable,
|
||||
BaseGroupDraggable,
|
||||
} from "@/components/dnd/types/types";
|
||||
|
||||
const isItemGroup = <
|
||||
TItem extends BaseDraggable,
|
||||
TGroup extends BaseGroupDraggable<TItem>,
|
||||
>(
|
||||
item: TItem | TGroup
|
||||
): boolean => {
|
||||
return "items" in item;
|
||||
};
|
||||
|
||||
export default isItemGroup;
|
||||
@ -1,6 +0,0 @@
|
||||
const STATUS_POSTFIX = "-status";
|
||||
|
||||
export const isStatusId = (rawId: string) => rawId.endsWith(STATUS_POSTFIX);
|
||||
|
||||
export const getStatusId = (rawId: string) =>
|
||||
Number(rawId.replace(STATUS_POSTFIX, ""));
|
||||
@ -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>
|
||||
|
||||
31
src/app/tags/components/PageBody/PageBody.tsx
Normal file
31
src/app/tags/components/PageBody/PageBody.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { Center, Divider, Stack, Text } from "@mantine/core";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import TagsPageHeader from "@/app/tags/components/TagsPageHeader/TagsPageHeader";
|
||||
import TagsTable from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/components/TagsTable";
|
||||
import { DealTagsContextProvider } from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/contexts/DealTagsContext";
|
||||
|
||||
const PageBody = () => {
|
||||
const { selectedProject } = useProjectsContext();
|
||||
|
||||
if (!selectedProject) {
|
||||
return (
|
||||
<Center>
|
||||
<Text>Проект не найден</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DealTagsContextProvider project={selectedProject}>
|
||||
<Stack gap={"md"}>
|
||||
<TagsPageHeader project={selectedProject} />
|
||||
<Divider />
|
||||
<TagsTable />
|
||||
</Stack>
|
||||
</DealTagsContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageBody;
|
||||
32
src/app/tags/components/TagsPageHeader/TagsPageHeader.tsx
Normal file
32
src/app/tags/components/TagsPageHeader/TagsPageHeader.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { IconChevronLeft, IconPlus } from "@tabler/icons-react";
|
||||
import { Box, Group, Title } from "@mantine/core";
|
||||
import useDealTagActions from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/hooks/useDealTagActions";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
project: ProjectSchema;
|
||||
};
|
||||
|
||||
const TagsPageHeader: FC<Props> = ({ project }) => {
|
||||
const { onCreateClick } = useDealTagActions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
mx={"xs"}
|
||||
mt={"md"}
|
||||
wrap={"nowrap"}
|
||||
justify={"space-between"}>
|
||||
<Link href={"/actions"}>
|
||||
<IconChevronLeft />
|
||||
</Link>
|
||||
<Title order={5}>Теги проекта "{project.name}"</Title>
|
||||
<Box onClick={onCreateClick}>
|
||||
<IconPlus />
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsPageHeader;
|
||||
25
src/app/tags/page.tsx
Normal file
25
src/app/tags/page.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Suspense } from "react";
|
||||
import { Center, Loader } from "@mantine/core";
|
||||
import PageBody from "@/app/tags/components/PageBody/PageBody";
|
||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||
|
||||
/*
|
||||
* Page for mobiles only
|
||||
*/
|
||||
export default async function TagsPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Center h="50vh">
|
||||
<Loader size="lg" />
|
||||
</Center>
|
||||
}>
|
||||
<PageContainer>
|
||||
<PageBlock fullScreenMobile>
|
||||
<PageBody />
|
||||
</PageBlock>
|
||||
</PageContainer>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { Stack } from "@mantine/core";
|
||||
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||
|
||||
type Props<TItem> = {
|
||||
id: string;
|
||||
items: TItem[];
|
||||
renderItem: (item: TItem) => ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const FunnelColumn = <TItem extends BaseDraggable>({
|
||||
id,
|
||||
items,
|
||||
renderItem,
|
||||
children,
|
||||
}: Props<TItem>) => {
|
||||
const { setNodeRef } = useDroppable({ id });
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<SortableContext
|
||||
id={id}
|
||||
items={items}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<Stack
|
||||
gap="xs"
|
||||
ref={setNodeRef}>
|
||||
{items.map(renderItem)}
|
||||
</Stack>
|
||||
</SortableContext>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunnelColumn;
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { ReactNode, RefObject, useMemo } from "react";
|
||||
import React, { ReactNode, RefObject, useMemo, useState } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
@ -16,17 +16,28 @@ import { Swiper, SwiperRef, SwiperSlide } from "swiper/react";
|
||||
import { Box } from "@mantine/core";
|
||||
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||
import FunnelColumn from "@/components/dnd/FunnelDnd/FunnelColumn";
|
||||
import FunnelOverlay from "@/components/dnd/FunnelDnd/FunnelOverlay";
|
||||
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||
import FunnelColumn from "@/components/dnd/FunnelDnd/components/FunnelColumn";
|
||||
import FunnelOverlay from "@/components/dnd/FunnelDnd/components/FunnelOverlay";
|
||||
import {
|
||||
getContainerId,
|
||||
getDndContainerId,
|
||||
isContainerId,
|
||||
} from "@/components/dnd/FunnelDnd/utils/columnId";
|
||||
import {
|
||||
getGroupId,
|
||||
isGroupId,
|
||||
} from "@/components/dnd/FunnelDnd/utils/groupId";
|
||||
import {
|
||||
BaseDraggable,
|
||||
BaseGroupDraggable,
|
||||
} from "@/components/dnd/types/types";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import SortableItem from "../SortableItem";
|
||||
import classes from "./FunnelDnd.module.css";
|
||||
|
||||
type Props<TContainer, TItem> = {
|
||||
type Props<TContainer, TItem, TGroup> = {
|
||||
containers: TContainer[];
|
||||
items: TItem[];
|
||||
onDragStart: (event: DragStartEvent) => void;
|
||||
itemsAndGroups: (TItem | TGroup)[];
|
||||
onDragOver: (event: DragOverEvent) => void;
|
||||
onDragEnd: (event: DragEndEvent) => void;
|
||||
swiperRef: RefObject<SwiperRef | null>;
|
||||
@ -42,11 +53,8 @@ type Props<TContainer, TItem> = {
|
||||
children: ReactNode
|
||||
) => ReactNode;
|
||||
renderItem: (item: TItem) => ReactNode;
|
||||
renderItemOverlay: (item: TItem) => ReactNode;
|
||||
getContainerId: (container: TContainer) => string;
|
||||
getItemsByContainer: (container: TContainer, items: TItem[]) => TItem[];
|
||||
activeContainer: TContainer | null;
|
||||
activeItem: TItem | null;
|
||||
renderGroup: (group: TGroup) => ReactNode;
|
||||
getItemsByContainer: (container: TContainer) => (TItem | TGroup)[];
|
||||
isCreatingContainerEnabled?: boolean;
|
||||
disabledColumns?: boolean;
|
||||
};
|
||||
@ -54,10 +62,10 @@ type Props<TContainer, TItem> = {
|
||||
const FunnelDnd = <
|
||||
TContainer extends BaseDraggable,
|
||||
TItem extends BaseDraggable,
|
||||
TGroup extends BaseGroupDraggable<TItem>,
|
||||
>({
|
||||
containers,
|
||||
items,
|
||||
onDragStart,
|
||||
itemsAndGroups,
|
||||
onDragOver,
|
||||
onDragEnd,
|
||||
swiperRef,
|
||||
@ -65,22 +73,25 @@ const FunnelDnd = <
|
||||
renderContainerHeader,
|
||||
renderContainerOverlay,
|
||||
renderItem,
|
||||
renderItemOverlay,
|
||||
getContainerId,
|
||||
renderGroup,
|
||||
getItemsByContainer,
|
||||
activeContainer,
|
||||
activeItem,
|
||||
isCreatingContainerEnabled = true,
|
||||
disabledColumns = false,
|
||||
}: Props<TContainer, TItem>) => {
|
||||
}: Props<TContainer, TItem, TGroup>) => {
|
||||
const sensors = useDndSensors();
|
||||
const isMobile = useIsMobile();
|
||||
const frequency = useMemo(() => (isMobile ? 1 : undefined), [isMobile]);
|
||||
|
||||
const [activeItem, setActiveItem] = useState<TItem | null>(null);
|
||||
const [activeContainer, setActiveContainer] = useState<TContainer | null>(
|
||||
null
|
||||
);
|
||||
const [activeGroup, setActiveGroup] = useState<TGroup | null>(null);
|
||||
|
||||
const renderContainers = () =>
|
||||
containers.map((container, index) => {
|
||||
const containerItems = getItemsByContainer(container, items);
|
||||
const containerId = getContainerId(container);
|
||||
const containerItems = getItemsByContainer(container);
|
||||
const containerId = getDndContainerId(container.id);
|
||||
return (
|
||||
<SwiperSlide
|
||||
style={{ width: 250 }}
|
||||
@ -94,8 +105,9 @@ const FunnelDnd = <
|
||||
container,
|
||||
<FunnelColumn
|
||||
id={containerId}
|
||||
items={containerItems}
|
||||
itemsAndGroups={containerItems}
|
||||
renderItem={renderItem}
|
||||
renderGroup={renderGroup}
|
||||
/>,
|
||||
renderDraggable!,
|
||||
index
|
||||
@ -156,6 +168,34 @@ const FunnelDnd = <
|
||||
);
|
||||
};
|
||||
|
||||
const onDragStart = ({ active }: DragStartEvent) => {
|
||||
const activeId = active.id as string | number;
|
||||
|
||||
if (typeof activeId !== "string") {
|
||||
const item = (itemsAndGroups.find(
|
||||
item => !("items" in item) && item.id === activeId
|
||||
) ?? null) as TItem | null;
|
||||
setActiveItem(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContainerId(activeId)) {
|
||||
const contId = getContainerId(activeId);
|
||||
setActiveContainer(
|
||||
containers.find(container => container.id === contId) ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGroupId(activeId)) {
|
||||
const groupId = getGroupId(activeId);
|
||||
const group = (itemsAndGroups.find(
|
||||
group => "items" in group && group.id === groupId
|
||||
) ?? null) as TGroup | null;
|
||||
setActiveGroup(group);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
@ -166,30 +206,37 @@ const FunnelDnd = <
|
||||
}}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}>
|
||||
onDragEnd={state => {
|
||||
setActiveContainer(null);
|
||||
setActiveItem(null);
|
||||
setActiveGroup(null);
|
||||
onDragEnd(state);
|
||||
}}>
|
||||
<SortableContext
|
||||
items={containers.map(getContainerId)}
|
||||
items={containers.map(container =>
|
||||
getDndContainerId(container.id)
|
||||
)}
|
||||
strategy={horizontalListSortingStrategy}>
|
||||
{renderBody()}
|
||||
<FunnelOverlay
|
||||
activeContainer={activeContainer}
|
||||
activeItem={activeItem}
|
||||
activeGroup={activeGroup}
|
||||
renderContainer={container => {
|
||||
const containerItems = getItemsByContainer(
|
||||
container,
|
||||
items
|
||||
);
|
||||
const containerId = getContainerId(container);
|
||||
const containerItems = getItemsByContainer(container);
|
||||
const containerId = getDndContainerId(container.id);
|
||||
return renderContainerOverlay(
|
||||
container,
|
||||
<FunnelColumn
|
||||
id={containerId}
|
||||
items={containerItems}
|
||||
itemsAndGroups={containerItems}
|
||||
renderItem={renderItem}
|
||||
renderGroup={renderGroup}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
renderItem={renderItemOverlay}
|
||||
renderItem={renderItem}
|
||||
renderGroup={renderGroup}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
76
src/components/dnd/FunnelDnd/components/FunnelColumn.tsx
Normal file
76
src/components/dnd/FunnelDnd/components/FunnelColumn.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { Stack } from "@mantine/core";
|
||||
import { getDndGroupId } from "@/components/dnd/FunnelDnd/utils/groupId";
|
||||
import SortableItem from "@/components/dnd/SortableItem";
|
||||
import {
|
||||
BaseDraggable,
|
||||
BaseGroupDraggable,
|
||||
} from "@/components/dnd/types/types";
|
||||
import isItemGroup from "@/app/deals/utils/isItemGroup";
|
||||
|
||||
type Props<
|
||||
TItem extends BaseDraggable,
|
||||
TGroup extends BaseGroupDraggable<TItem>,
|
||||
> = {
|
||||
id: string;
|
||||
itemsAndGroups: (TItem | TGroup)[];
|
||||
renderItem: (item: TItem) => ReactNode;
|
||||
renderGroup: (group: TGroup) => ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const FunnelColumn = <
|
||||
TItem extends BaseDraggable,
|
||||
TGroup extends BaseGroupDraggable<TItem>,
|
||||
>({
|
||||
id,
|
||||
itemsAndGroups,
|
||||
renderItem,
|
||||
renderGroup,
|
||||
children,
|
||||
}: Props<TItem, TGroup>) => {
|
||||
const { setNodeRef } = useDroppable({ id });
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<SortableContext
|
||||
id={id}
|
||||
items={itemsAndGroups.map(itemOrGroup =>
|
||||
isItemGroup(itemOrGroup)
|
||||
? getDndGroupId(itemOrGroup.id)
|
||||
: itemOrGroup.id
|
||||
)}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<Stack
|
||||
gap="xs"
|
||||
ref={setNodeRef}>
|
||||
{itemsAndGroups.map(itemOrGroup =>
|
||||
"items" in itemOrGroup ? (
|
||||
<SortableItem
|
||||
key={`${itemOrGroup.id.toString()}g`}
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
id={getDndGroupId(itemOrGroup.id)}
|
||||
renderItem={() => renderGroup(itemOrGroup)}
|
||||
/>
|
||||
) : (
|
||||
<SortableItem
|
||||
key={itemOrGroup.id}
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
id={itemOrGroup.id}
|
||||
renderItem={() => renderItem(itemOrGroup)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</SortableContext>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunnelColumn;
|
||||
@ -2,28 +2,32 @@ import React, { ReactNode } from "react";
|
||||
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
||||
import styles from "@/components/dnd/FunnelDnd/FunnelDnd.module.css";
|
||||
|
||||
type Props<TContainer, TItem> = {
|
||||
type Props<TContainer, TItem, TGroup> = {
|
||||
activeContainer: TContainer | null;
|
||||
activeItem: TItem | null;
|
||||
activeGroup: TGroup | null;
|
||||
renderContainer: (container: TContainer) => ReactNode;
|
||||
renderItem: (item: TItem) => ReactNode;
|
||||
renderGroup: (group: TGroup) => ReactNode;
|
||||
};
|
||||
|
||||
const FunnelOverlay = <TContainer, TItem>({
|
||||
const FunnelOverlay = <TContainer, TItem, TGroup>({
|
||||
activeContainer,
|
||||
activeItem,
|
||||
activeGroup,
|
||||
renderContainer,
|
||||
renderItem,
|
||||
}: Props<TContainer, TItem>) => {
|
||||
renderGroup,
|
||||
}: Props<TContainer, TItem, TGroup>) => {
|
||||
const renderOverlay = () => {
|
||||
if (activeItem) return renderItem(activeItem);
|
||||
if (activeContainer) return renderContainer(activeContainer);
|
||||
if (activeGroup) return renderGroup(activeGroup);
|
||||
};
|
||||
|
||||
return (
|
||||
<DragOverlay dropAnimation={defaultDropAnimation}>
|
||||
<div className={styles.overlay}>
|
||||
{activeItem
|
||||
? renderItem(activeItem)
|
||||
: activeContainer
|
||||
? renderContainer(activeContainer)
|
||||
: null}
|
||||
</div>
|
||||
<div className={styles.overlay}>{renderOverlay()}</div>
|
||||
</DragOverlay>
|
||||
);
|
||||
};
|
||||
9
src/components/dnd/FunnelDnd/utils/columnId.ts
Normal file
9
src/components/dnd/FunnelDnd/utils/columnId.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const CONTAINER_POSTFIX = "-con";
|
||||
|
||||
export const isContainerId = (rawId: string) => rawId.endsWith(CONTAINER_POSTFIX);
|
||||
|
||||
export const getContainerId = (rawId: string) =>
|
||||
Number(rawId.replace(CONTAINER_POSTFIX, ""));
|
||||
|
||||
export const getDndContainerId = (id: number) =>
|
||||
`${id}${CONTAINER_POSTFIX}`;
|
||||
8
src/components/dnd/FunnelDnd/utils/groupId.ts
Normal file
8
src/components/dnd/FunnelDnd/utils/groupId.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const GROUP_POSTFIX = "-gr";
|
||||
|
||||
export const isGroupId = (rawId: string) => rawId.endsWith(GROUP_POSTFIX);
|
||||
|
||||
export const getGroupId = (rawId: string) =>
|
||||
Number(rawId.replace(GROUP_POSTFIX, ""));
|
||||
|
||||
export const getDndGroupId = (id: number) => `${id}${GROUP_POSTFIX}`;
|
||||
@ -1,3 +1,8 @@
|
||||
export type BaseDraggable = {
|
||||
id: number;
|
||||
lexorank: string;
|
||||
};
|
||||
|
||||
export type BaseGroupDraggable<TItem extends BaseDraggable> = BaseDraggable & {
|
||||
items: TItem[];
|
||||
};
|
||||
|
||||
@ -3,7 +3,8 @@ import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||
|
||||
const useProjectActions = () => {
|
||||
const { selectedProject, projectsCrud } = useProjectsContext();
|
||||
const { selectedProject, projectsCrud, refetchProjects } =
|
||||
useProjectsContext();
|
||||
const { openDrawer } = useDrawersContext();
|
||||
|
||||
const onCreateClick = () => {
|
||||
@ -27,6 +28,7 @@ const useProjectActions = () => {
|
||||
onChange: value => projectsCrud.onUpdate(value.id, value),
|
||||
onDelete: projectsCrud.onDelete,
|
||||
},
|
||||
onClose: refetchProjects,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
34
src/components/ui/DealTag/DealTag.tsx
Normal file
34
src/components/ui/DealTag/DealTag.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { lighten, Pill, useMantineColorScheme } from "@mantine/core";
|
||||
import { DealTagSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
tag: Partial<DealTagSchema>;
|
||||
};
|
||||
|
||||
const DealTag = ({ tag }: Props) => {
|
||||
const theme = useMantineColorScheme();
|
||||
const isInherit = tag.tagColor!.backgroundColor === "inherit";
|
||||
|
||||
let color = tag.tagColor!.color;
|
||||
const backgroundColor = tag.tagColor!.backgroundColor;
|
||||
|
||||
if (!(theme.colorScheme === "dark" || isInherit)) {
|
||||
color = lighten(color, 0.95);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pill
|
||||
key={tag.id}
|
||||
style={{
|
||||
opacity: 0.7,
|
||||
color,
|
||||
backgroundColor,
|
||||
border: "1px solid",
|
||||
borderColor: color,
|
||||
}}>
|
||||
{tag.name}
|
||||
</Pill>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealTag;
|
||||
30
src/components/ui/DealTags/DealTags.module.css
Normal file
30
src/components/ui/DealTags/DealTags.module.css
Normal file
@ -0,0 +1,30 @@
|
||||
.add-tag-button {
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-1);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
color: gray;
|
||||
border: 1px gray dashed;
|
||||
border-radius: 50%;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-tag-button:hover {
|
||||
@mixin light {
|
||||
border-color: black;
|
||||
color: black;
|
||||
}
|
||||
@mixin dark {
|
||||
border-color: white;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.add-tag-button-icon {
|
||||
color: inherit !important;
|
||||
}
|
||||
93
src/components/ui/DealTags/DealTags.tsx
Normal file
93
src/components/ui/DealTags/DealTags.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import classNames from "classnames";
|
||||
import { Button, Center, Checkbox, Group, Menu, Stack } from "@mantine/core";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import DealTag from "@/components/ui/DealTag/DealTag";
|
||||
import useDealTags from "@/components/ui/DealTags/hooks/useDealTags";
|
||||
import { DealTagSchema } from "@/lib/client";
|
||||
import styles from "./DealTags.module.css";
|
||||
|
||||
type Props = {
|
||||
dealId?: number;
|
||||
groupId?: number;
|
||||
tags: DealTagSchema[];
|
||||
};
|
||||
|
||||
const DealTags = ({ tags, dealId, groupId }: Props) => {
|
||||
const { selectedProject } = useProjectsContext();
|
||||
const { switchTag } = useDealTags();
|
||||
const tagIdsSet = useMemo(() => new Set(tags.map(t => t.id)), [tags]);
|
||||
|
||||
if (selectedProject?.tags.length === 0) return;
|
||||
|
||||
const onTagClick = (tagId: number, event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
switchTag({ dealId, groupId, tagId });
|
||||
};
|
||||
|
||||
const addTagButton = useMemo(
|
||||
() => (
|
||||
<Menu withArrow>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
onClick={e => e.stopPropagation()}
|
||||
unstyled
|
||||
className={classNames(styles["add-tag-button"])}>
|
||||
<Center>
|
||||
<IconPlus
|
||||
size={"1.2em"}
|
||||
className={classNames(
|
||||
styles["add-tag-button-icon"]
|
||||
)}
|
||||
/>
|
||||
</Center>
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Stack
|
||||
p={"xs"}
|
||||
gap={"sm"}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
{selectedProject?.tags.map(tag => (
|
||||
<Group
|
||||
key={tag.id}
|
||||
wrap={"nowrap"}>
|
||||
<Checkbox
|
||||
checked={tagIdsSet.has(tag.id)}
|
||||
onChange={event =>
|
||||
onTagClick(
|
||||
tag.id,
|
||||
event as unknown as any
|
||||
)
|
||||
}
|
||||
label={tag.name}
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
),
|
||||
[selectedProject?.tags, tags]
|
||||
);
|
||||
|
||||
return (
|
||||
<Group gap={"xs"}>
|
||||
{addTagButton}
|
||||
{selectedProject?.tags.map(
|
||||
tag =>
|
||||
tagIdsSet.has(tag.id) && (
|
||||
<DealTag
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealTags;
|
||||
25
src/components/ui/DealTags/hooks/useDealTags.ts
Normal file
25
src/components/ui/DealTags/hooks/useDealTags.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import { SwitchDealTagRequest } from "@/lib/client";
|
||||
import { switchDealTagMutation } from "@/lib/client/@tanstack/react-query.gen";
|
||||
|
||||
const useDealTags = () => {
|
||||
const { refetchDeals } = useDealsContext();
|
||||
|
||||
const switchTagMutation = useMutation({
|
||||
...switchDealTagMutation(),
|
||||
onSettled: refetchDeals,
|
||||
});
|
||||
|
||||
const switchTag = (data: SwitchDealTagRequest) => {
|
||||
switchTagMutation.mutate({
|
||||
body: data,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
switchTag,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDealTags;
|
||||
@ -2,10 +2,10 @@
|
||||
|
||||
import React, { FC, useState } from "react";
|
||||
import { Drawer } from "@mantine/core";
|
||||
import ProjectEditorBody from "@/app/deals/drawers/ProjectEditorDrawer/components/ProjectEditorBody";
|
||||
import { DrawerProps } from "@/drawers/types";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
import ProjectEditorBody from "@/drawers/common/ProjectEditorDrawer/components/ProjectEditorBody";
|
||||
|
||||
type Props = {
|
||||
value: ProjectSchema;
|
||||
@ -1,10 +1,11 @@
|
||||
import { FC } from "react";
|
||||
import { IconBlocks, IconEdit } from "@tabler/icons-react";
|
||||
import { IconBlocks, IconEdit, IconTags } from "@tabler/icons-react";
|
||||
import { Tabs } from "@mantine/core";
|
||||
import {
|
||||
GeneralTab,
|
||||
ModulesTab,
|
||||
} from "@/app/deals/drawers/ProjectEditorDrawer/tabs";
|
||||
} from "@/drawers/common/ProjectEditorDrawer/tabs";
|
||||
import TagsTab from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/TagsTab";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
import styles from "../ProjectEditorDrawer.module.css";
|
||||
|
||||
@ -30,13 +31,21 @@ const ProjectEditorBody: FC<Props> = props => {
|
||||
leftSection={<IconBlocks />}>
|
||||
Модули
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"tags"}
|
||||
leftSection={<IconTags />}>
|
||||
Теги
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="general">
|
||||
<Tabs.Panel value={"general"}>
|
||||
<GeneralTab {...props} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="modules">
|
||||
<Tabs.Panel value={"modules"}>
|
||||
<ModulesTab {...props} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"tags"}>
|
||||
<TagsTab {...props} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
3
src/drawers/common/ProjectEditorDrawer/index.ts
Normal file
3
src/drawers/common/ProjectEditorDrawer/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import ProjectEditorDrawer from "./ProjectEditorDrawer";
|
||||
|
||||
export default ProjectEditorDrawer;
|
||||
@ -1,8 +1,8 @@
|
||||
import { FC } from "react";
|
||||
import { Stack, TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import Footer from "@/app/deals/drawers/ProjectEditorDrawer/tabs/GeneralTab/components/Footer";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
import Footer from "./components/Footer";
|
||||
|
||||
type Props = {
|
||||
value: ProjectSchema;
|
||||
@ -1,7 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { Button, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import resolveDependencies from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/utils/resolveDependencies";
|
||||
import resolveDependencies from "@/drawers/common/ProjectEditorDrawer/tabs/ModulesTab/utils/resolveDependencies";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
import ModulesTable from "./components/ModulesTable";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { Divider, Stack } from "@mantine/core";
|
||||
import useModulesTableColumns from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/hooks/useModulesTableColumns";
|
||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||
import useModulesTableColumns from "@/drawers/common/ProjectEditorDrawer/tabs/ModulesTab/hooks/useModulesTableColumns";
|
||||
import useBuiltInModulesList from "@/hooks/lists/useBuiltInModulesList";
|
||||
import { BuiltInModuleSchemaOutput } from "@/lib/client";
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import { FC } from "react";
|
||||
import { Flex } from "@mantine/core";
|
||||
import TagsTabHeader from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/components/TagsTabHeader";
|
||||
import TagsTable from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/components/TagsTable";
|
||||
import { DealTagsContextProvider } from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/contexts/DealTagsContext";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
value: ProjectSchema;
|
||||
};
|
||||
|
||||
const TagsTab: FC<Props> = ({ value }) => {
|
||||
return (
|
||||
<Flex
|
||||
h={"100%"}
|
||||
direction={"column"}
|
||||
gap={"xs"}>
|
||||
<DealTagsContextProvider project={value}>
|
||||
<TagsTabHeader />
|
||||
<TagsTable />
|
||||
</DealTagsContextProvider>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsTab;
|
||||
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { Group, SelectProps } from "@mantine/core";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||
import DealTag from "@/components/ui/DealTag/DealTag";
|
||||
import { DealTagColorSchema } from "@/lib/client";
|
||||
import useTagColorList from "../hooks/useTagColorList";
|
||||
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<DealTagColorSchema>,
|
||||
"data" | "getValueFn" | "getLabelFn"
|
||||
>;
|
||||
|
||||
const TagColorInput = (props: Props) => {
|
||||
const { colors } = useTagColorList();
|
||||
const colorsMap = new Map<string, DealTagColorSchema>(
|
||||
colors.map(
|
||||
color =>
|
||||
[color.id.toString(), color] as [string, DealTagColorSchema]
|
||||
)
|
||||
);
|
||||
|
||||
const renderSelectOption: SelectProps["renderOption"] = ({
|
||||
option,
|
||||
checked,
|
||||
}) => {
|
||||
const tag = {
|
||||
id: Number(option.value),
|
||||
name: "Тег-пример",
|
||||
tagColor: colorsMap.get(option.value),
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
flex="1"
|
||||
gap="md">
|
||||
<DealTag tag={tag} />
|
||||
{option.label}
|
||||
{checked && <IconCheck style={{ marginInlineStart: "auto" }} />}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
label={"Цвет"}
|
||||
renderOption={renderSelectOption}
|
||||
data={colors}
|
||||
getValueFn={color => color.id.toString()}
|
||||
getLabelFn={color => color.label}
|
||||
searchable
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagColorInput;
|
||||
@ -0,0 +1,21 @@
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Group } from "@mantine/core";
|
||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||
import useDealTagActions from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/hooks/useDealTagActions";
|
||||
|
||||
const TagsTabHeader = () => {
|
||||
const { onCreateClick } = useDealTagActions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
pt={"xs"}
|
||||
px={"xs"}>
|
||||
<InlineButton onClick={onCreateClick}>
|
||||
<IconPlus />
|
||||
Создать
|
||||
</InlineButton>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsTabHeader;
|
||||
@ -0,0 +1,38 @@
|
||||
import { IconMoodSad } from "@tabler/icons-react";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||
import { useDealTagsContext } from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/contexts/DealTagsContext";
|
||||
import tagsTableColumns from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/hooks/tagsTableColumns";
|
||||
import useDealTagActions from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/hooks/useDealTagActions";
|
||||
|
||||
const TagsTable = () => {
|
||||
const { dealTags, dealTagsCrud } = useDealTagsContext();
|
||||
const { onChangeClick } = useDealTagActions();
|
||||
|
||||
const columns = tagsTableColumns({
|
||||
onDelete: dealTagsCrud.onDelete,
|
||||
onChange: onChangeClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseTable
|
||||
withTableBorder
|
||||
records={dealTags}
|
||||
columns={columns}
|
||||
groups={undefined}
|
||||
style={{
|
||||
marginInline: "var(--mantine-spacing-xs)",
|
||||
minHeight: 200,
|
||||
}}
|
||||
verticalSpacing={"xs"}
|
||||
emptyState={
|
||||
<Group mt={dealTags.length === 0 ? "xl" : 0}>
|
||||
<Text>Нет тегов</Text>
|
||||
<IconMoodSad />
|
||||
</Group>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsTable;
|
||||
@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { DealTagsCrud, useDealTagsCrud } from "@/hooks/cruds/useDealTagsCrud";
|
||||
import useDealTagsList from "@/hooks/lists/useDealTagsList";
|
||||
import { DealTagSchema, ProjectSchema } from "@/lib/client";
|
||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||
|
||||
type DealTagsContextState = {
|
||||
dealTags: DealTagSchema[];
|
||||
refetchDealTags: () => void;
|
||||
dealTagsCrud: DealTagsCrud;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
project: ProjectSchema;
|
||||
};
|
||||
|
||||
const useDealTagsContextState = ({ project }: Props): DealTagsContextState => {
|
||||
const dealTagsList = useDealTagsList({ projectId: project.id });
|
||||
|
||||
const dealTagsCrud = useDealTagsCrud({
|
||||
...dealTagsList,
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
return {
|
||||
dealTags: dealTagsList.dealTags,
|
||||
refetchDealTags: dealTagsList.refetch,
|
||||
dealTagsCrud,
|
||||
};
|
||||
};
|
||||
|
||||
export const [DealTagsContextProvider, useDealTagsContext] = makeContext<
|
||||
DealTagsContextState,
|
||||
Props
|
||||
>(useDealTagsContextState, "DealTags");
|
||||
@ -0,0 +1,46 @@
|
||||
import { useMemo } from "react";
|
||||
import { DataTableColumn } from "mantine-datatable";
|
||||
import { Center } from "@mantine/core";
|
||||
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
||||
import DealTag from "@/components/ui/DealTag/DealTag";
|
||||
import { DealTagSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
onDelete: (tag: DealTagSchema) => void;
|
||||
onChange: (tag: DealTagSchema) => void;
|
||||
};
|
||||
|
||||
const useTagsTableColumns = ({ onDelete, onChange }: Props) => {
|
||||
return useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
accessor: "actions",
|
||||
title: <Center>Действия</Center>,
|
||||
width: "0%",
|
||||
render: tag => (
|
||||
<UpdateDeleteTableActions
|
||||
onDelete={() => onDelete(tag)}
|
||||
onChange={() => onChange(tag)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Название",
|
||||
accessor: "name",
|
||||
},
|
||||
{
|
||||
title: "Цвет",
|
||||
accessor: "tagColor.label",
|
||||
},
|
||||
{
|
||||
title: "Пример",
|
||||
accessor: "tagColor",
|
||||
render: tag => <DealTag tag={tag} />,
|
||||
},
|
||||
] as DataTableColumn<DealTagSchema>[],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
export default useTagsTableColumns;
|
||||
@ -0,0 +1,37 @@
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useDealTagsContext } from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/contexts/DealTagsContext";
|
||||
import { DealTagSchema } from "@/lib/client";
|
||||
|
||||
const useDealTagActions = () => {
|
||||
const { dealTagsCrud } = useDealTagsContext();
|
||||
|
||||
const onChangeClick = (tag: DealTagSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "dealTagModal",
|
||||
innerProps: {
|
||||
entity: tag,
|
||||
onChange: data => dealTagsCrud.onUpdate(tag.id, data),
|
||||
isEditing: true,
|
||||
},
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "dealTagModal",
|
||||
innerProps: {
|
||||
onCreate: dealTagsCrud.onCreate,
|
||||
isEditing: false,
|
||||
},
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onChangeClick,
|
||||
onCreateClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDealTagActions;
|
||||
@ -0,0 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getDealTagColorsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||
|
||||
const useTagColorList = () => {
|
||||
const { data, refetch } = useQuery(getDealTagColorsOptions());
|
||||
|
||||
return {
|
||||
colors: data?.items ?? [],
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTagColorList;
|
||||
@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { Stack, TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import TagColorInput from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/components/TagColorInput";
|
||||
import {
|
||||
CreateDealTagSchema,
|
||||
DealTagSchema,
|
||||
UpdateDealTagSchema,
|
||||
} from "@/lib/client";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
||||
|
||||
type Props = CreateEditFormProps<
|
||||
CreateDealTagSchema,
|
||||
UpdateDealTagSchema,
|
||||
DealTagSchema
|
||||
>;
|
||||
|
||||
const DealTagModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const initialValues: Partial<DealTagSchema> = innerProps.isEditing
|
||||
? innerProps.entity
|
||||
: {
|
||||
name: "",
|
||||
tagColor: undefined,
|
||||
tagColorId: undefined,
|
||||
};
|
||||
|
||||
const form = useForm<Partial<DealTagSchema>>({
|
||||
initialValues,
|
||||
validate: {
|
||||
name: name => !name && "Необходимо указать название тега",
|
||||
tagColor: tagColor => !tagColor && "Необходимо указать цвет тега",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
placeholder={"Введите название тега"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TagColorInput
|
||||
placeholder={"Укажите цвет"}
|
||||
{...form.getInputProps("tagColor")}
|
||||
onChange={tag => {
|
||||
form.setFieldValue("tagColor", tag);
|
||||
form.setFieldValue("tagColorId", tag.id);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealTagModal;
|
||||
@ -1,9 +1,9 @@
|
||||
import ClientMarketplaceDrawer from "@/app/clients/drawers/ClientMarketplacesDrawer";
|
||||
import BoardsMobileEditorDrawer from "@/app/deals/drawers/BoardsMobileEditorDrawer";
|
||||
import DealEditorDrawer from "@/app/deals/drawers/DealEditorDrawer";
|
||||
import ProjectEditorDrawer from "@/app/deals/drawers/ProjectEditorDrawer";
|
||||
import ProjectsMobileEditorDrawer from "@/app/deals/drawers/ProjectsMobileEditorDrawer";
|
||||
import StatusesMobileEditorDrawer from "../app/deals/drawers/StatusesMobileEditorDrawer";
|
||||
import ProjectEditorDrawer from "./common/ProjectEditorDrawer";
|
||||
|
||||
const drawerRegistry = {
|
||||
projectsMobileEditorDrawer: ProjectsMobileEditorDrawer,
|
||||
|
||||
125
src/hooks/cruds/useDealGroupCrud.tsx
Normal file
125
src/hooks/cruds/useDealGroupCrud.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { HttpValidationError, UpdateDealGroupSchema } from "@/lib/client";
|
||||
import {
|
||||
createDealGroupMutation,
|
||||
deleteDealGroupMutation,
|
||||
updateDealGroupMutation,
|
||||
updateDealsInGroupMutation,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
|
||||
export type GroupsCrud = {
|
||||
onUpdate: (groupId: number, group: UpdateDealGroupSchema) => void;
|
||||
onCreate: (mainDealId: number, otherDealIds: number[]) => void;
|
||||
onUpdateDealsInGroup: (groupId: number, dealIds: number[]) => void;
|
||||
onDelete: (groupId: number) => void;
|
||||
};
|
||||
|
||||
const useDealGroupCrud = (): GroupsCrud => {
|
||||
const queryClient = useQueryClient();
|
||||
const key = "getDeals";
|
||||
|
||||
const onError = (
|
||||
error: AxiosError<HttpValidationError>,
|
||||
_: any,
|
||||
context: any
|
||||
) => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
if (context?.previous) {
|
||||
queryClient.setQueryData([key], context.previous);
|
||||
}
|
||||
};
|
||||
|
||||
const onSettled = () => {
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query: { queryKey: any }) =>
|
||||
query.queryKey[0]?._id === key,
|
||||
});
|
||||
};
|
||||
|
||||
const updateMutation = useMutation({
|
||||
...updateDealGroupMutation(),
|
||||
onSettled,
|
||||
onError,
|
||||
});
|
||||
|
||||
const onUpdate = (groupId: number, entity: UpdateDealGroupSchema) => {
|
||||
updateMutation.mutate({
|
||||
path: {
|
||||
pk: groupId,
|
||||
},
|
||||
body: {
|
||||
entity,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createMutation = useMutation({
|
||||
...createDealGroupMutation(),
|
||||
onSettled,
|
||||
onError,
|
||||
});
|
||||
|
||||
const onCreate = (mainDealId: number, otherDealIds: number[]) => {
|
||||
createMutation.mutate({
|
||||
body: {
|
||||
mainDealId,
|
||||
otherDealIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateDealsMutation = useMutation({
|
||||
...updateDealsInGroupMutation(),
|
||||
onSettled,
|
||||
onError,
|
||||
});
|
||||
|
||||
const onUpdateDealsInGroup = (groupId: number, dealIds: number[]) => {
|
||||
updateDealsMutation.mutate({
|
||||
path: {
|
||||
pk: groupId,
|
||||
},
|
||||
body: {
|
||||
dealIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
...deleteDealGroupMutation(),
|
||||
onSettled,
|
||||
onError,
|
||||
});
|
||||
|
||||
const onDelete = (groupId: number) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление группы",
|
||||
children: <Text>Вы уверены, что хотите удалить группу?</Text>,
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
deleteMutation.mutate({
|
||||
path: {
|
||||
pk: groupId,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onUpdate,
|
||||
onCreate,
|
||||
onUpdateDealsInGroup,
|
||||
onDelete,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDealGroupCrud;
|
||||
53
src/hooks/cruds/useDealTagsCrud.tsx
Normal file
53
src/hooks/cruds/useDealTagsCrud.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
||||
import {
|
||||
CreateDealTagSchema,
|
||||
DealTagSchema,
|
||||
UpdateDealTagSchema,
|
||||
} from "@/lib/client";
|
||||
import {
|
||||
createDealTagMutation,
|
||||
deleteDealTagMutation,
|
||||
updateDealTagMutation,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
|
||||
type UseDealTagsOperationsProps = {
|
||||
queryKey: any[];
|
||||
projectId: number;
|
||||
};
|
||||
|
||||
export type DealTagsCrud = {
|
||||
onCreate: (data: CreateDealTagSchema) => void;
|
||||
onUpdate: (dealTagId: number, dealTag: UpdateDealTagSchema) => void;
|
||||
onDelete: (dealTag: DealTagSchema) => void;
|
||||
};
|
||||
|
||||
export const useDealTagsCrud = ({
|
||||
queryKey,
|
||||
projectId,
|
||||
}: UseDealTagsOperationsProps): DealTagsCrud => {
|
||||
return useCrudOperations<
|
||||
DealTagSchema,
|
||||
UpdateDealTagSchema,
|
||||
CreateDealTagSchema
|
||||
>({
|
||||
key: "getDealTags",
|
||||
queryKey,
|
||||
mutations: {
|
||||
create: createDealTagMutation(),
|
||||
update: updateDealTagMutation(),
|
||||
delete: deleteDealTagMutation(),
|
||||
},
|
||||
getCreateEntity: data => ({
|
||||
tagColorId: data.tagColorId!,
|
||||
name: data.name!,
|
||||
projectId,
|
||||
}),
|
||||
getUpdateEntity: (old, update) => ({
|
||||
...old,
|
||||
name: update.name ?? old.name,
|
||||
tagColor: update.tagColor ?? old.tagColor,
|
||||
tagColorId: update.tagColor?.id ?? old.tagColorId,
|
||||
}),
|
||||
getDeleteConfirmTitle: () => "Удаление доски",
|
||||
});
|
||||
};
|
||||
39
src/hooks/lists/useDealTagsList.ts
Normal file
39
src/hooks/lists/useDealTagsList.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { DealTagSchema } from "@/lib/client";
|
||||
import {
|
||||
getDealTagsOptions,
|
||||
getDealTagsQueryKey,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
|
||||
type Props = {
|
||||
projectId: number;
|
||||
};
|
||||
|
||||
const useDealTagsList = ({ projectId }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const options = {
|
||||
path: { projectId },
|
||||
};
|
||||
const { data, refetch } = useQuery(getDealTagsOptions(options));
|
||||
|
||||
const queryKey = getDealTagsQueryKey(options);
|
||||
|
||||
const setDealTags = (dealTags: DealTagSchema[]) => {
|
||||
queryClient.setQueryData(
|
||||
queryKey,
|
||||
(old: { items: DealTagSchema[] }) => ({
|
||||
...old,
|
||||
items: dealTags,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
dealTags: data?.items ?? [],
|
||||
setDealTags,
|
||||
refetch,
|
||||
queryKey,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDealTagsList;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -193,6 +193,30 @@ export const zCreateClientResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* CreateDealGroupRequest
|
||||
*/
|
||||
export const zCreateDealGroupRequest = z.object({
|
||||
mainDealId: z.int(),
|
||||
otherDealIds: z.array(z.int()),
|
||||
});
|
||||
|
||||
/**
|
||||
* DealGroupSchema
|
||||
*/
|
||||
export const zDealGroupSchema = z.object({
|
||||
id: z.int(),
|
||||
name: z.optional(z.union([z.string(), z.null()])),
|
||||
lexorank: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* CreateDealGroupResponse
|
||||
*/
|
||||
export const zCreateDealGroupResponse = z.object({
|
||||
entity: zDealGroupSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* CreateDealProductSchema
|
||||
*/
|
||||
@ -324,6 +348,27 @@ export const zStatusSchema = z.object({
|
||||
color: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DealTagColorSchema
|
||||
*/
|
||||
export const zDealTagColorSchema = z.object({
|
||||
id: z.int(),
|
||||
color: z.string(),
|
||||
backgroundColor: z.string(),
|
||||
label: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DealTagSchema
|
||||
*/
|
||||
export const zDealTagSchema = z.object({
|
||||
name: z.string(),
|
||||
projectId: z.int(),
|
||||
tagColorId: z.int(),
|
||||
id: z.int(),
|
||||
tagColor: zDealTagColorSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* DealSchema
|
||||
*/
|
||||
@ -336,6 +381,8 @@ export const zDealSchema = z.object({
|
||||
createdAt: z.iso.datetime({
|
||||
offset: true,
|
||||
}),
|
||||
group: z.union([zDealGroupSchema, z.null()]),
|
||||
tags: z.array(zDealTagSchema),
|
||||
productsQuantity: z.optional(z.int()).default(0),
|
||||
totalPrice: z.optional(z.number()).default(0),
|
||||
client: z.optional(z.union([zClientSchema, z.null()])),
|
||||
@ -386,6 +433,30 @@ export const zCreateDealServiceResponse = z.object({
|
||||
entity: zDealServiceSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* CreateDealTagSchema
|
||||
*/
|
||||
export const zCreateDealTagSchema = z.object({
|
||||
name: z.string(),
|
||||
projectId: z.int(),
|
||||
tagColorId: z.int(),
|
||||
});
|
||||
|
||||
/**
|
||||
* CreateDealTagRequest
|
||||
*/
|
||||
export const zCreateDealTagRequest = z.object({
|
||||
entity: zCreateDealTagSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* CreateDealTagResponse
|
||||
*/
|
||||
export const zCreateDealTagResponse = z.object({
|
||||
message: z.string(),
|
||||
entity: zDealTagSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* CreateMarketplaceSchema
|
||||
*/
|
||||
@ -501,6 +572,7 @@ export const zProjectSchema = z.object({
|
||||
id: z.int(),
|
||||
name: z.string(),
|
||||
builtInModules: z.array(zBuiltInModuleSchemaOutput),
|
||||
tags: z.array(zDealTagSchema),
|
||||
});
|
||||
|
||||
/**
|
||||
@ -675,6 +747,13 @@ export const zDeleteClientResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DeleteDealGroupResponse
|
||||
*/
|
||||
export const zDeleteDealGroupResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DeleteDealProductResponse
|
||||
*/
|
||||
@ -696,6 +775,13 @@ export const zDeleteDealServiceResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DeleteDealTagResponse
|
||||
*/
|
||||
export const zDeleteDealTagResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DeleteMarketplaceResponse
|
||||
*/
|
||||
@ -815,6 +901,13 @@ export const zGetDealServicesResponse = z.object({
|
||||
items: z.array(zDealServiceSchema),
|
||||
});
|
||||
|
||||
/**
|
||||
* GetDealTagsResponse
|
||||
*/
|
||||
export const zGetDealTagsResponse = z.object({
|
||||
items: z.array(zDealTagSchema),
|
||||
});
|
||||
|
||||
/**
|
||||
* PaginationInfoSchema
|
||||
*/
|
||||
@ -864,6 +957,13 @@ export const zGetProductsResponse = z.object({
|
||||
paginationInfo: zPaginationInfoSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* GetProjectResponse
|
||||
*/
|
||||
export const zGetProjectResponse = z.object({
|
||||
entity: zProjectSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* GetProjectsResponse
|
||||
*/
|
||||
@ -919,6 +1019,13 @@ export const zGetStatusesResponse = z.object({
|
||||
items: z.array(zStatusSchema),
|
||||
});
|
||||
|
||||
/**
|
||||
* GetTagColorsResponse
|
||||
*/
|
||||
export const zGetTagColorsResponse = z.object({
|
||||
items: z.array(zDealTagColorSchema),
|
||||
});
|
||||
|
||||
/**
|
||||
* ValidationError
|
||||
*/
|
||||
@ -962,6 +1069,22 @@ export const zProductServicesDuplicateResponse = z.object({
|
||||
|
||||
export const zSortDir = z.enum(["asc", "desc"]);
|
||||
|
||||
/**
|
||||
* SwitchDealTagRequest
|
||||
*/
|
||||
export const zSwitchDealTagRequest = z.object({
|
||||
tagId: z.int(),
|
||||
dealId: z.optional(z.union([z.int(), z.null()])),
|
||||
groupId: z.optional(z.union([z.int(), z.null()])),
|
||||
});
|
||||
|
||||
/**
|
||||
* SwitchDealTagResponse
|
||||
*/
|
||||
export const zSwitchDealTagResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateBarcodeTemplateSchema
|
||||
*/
|
||||
@ -1034,6 +1157,29 @@ export const zUpdateClientResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealGroupSchema
|
||||
*/
|
||||
export const zUpdateDealGroupSchema = z.object({
|
||||
name: z.optional(z.union([z.string(), z.null()])),
|
||||
lexorank: z.optional(z.union([z.string(), z.null()])),
|
||||
statusId: z.optional(z.union([z.int(), z.null()])),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealGroupRequest
|
||||
*/
|
||||
export const zUpdateDealGroupRequest = z.object({
|
||||
entity: zUpdateDealGroupSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealGroupResponse
|
||||
*/
|
||||
export const zUpdateDealGroupResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealProductSchema
|
||||
*/
|
||||
@ -1104,6 +1250,42 @@ export const zUpdateDealServiceResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealTagSchema
|
||||
*/
|
||||
export const zUpdateDealTagSchema = z.object({
|
||||
name: z.optional(z.union([z.string(), z.null()])),
|
||||
tagColor: z.optional(z.union([zDealTagColorSchema, z.null()])),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealTagRequest
|
||||
*/
|
||||
export const zUpdateDealTagRequest = z.object({
|
||||
entity: zUpdateDealTagSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealTagResponse
|
||||
*/
|
||||
export const zUpdateDealTagResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealsInGroupRequest
|
||||
*/
|
||||
export const zUpdateDealsInGroupRequest = z.object({
|
||||
dealIds: z.array(z.int()),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateDealsInGroupResponse
|
||||
*/
|
||||
export const zUpdateDealsInGroupResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* UpdateMarketplaceSchema
|
||||
*/
|
||||
@ -1412,6 +1594,128 @@ export const zUpdateDealData = z.object({
|
||||
*/
|
||||
export const zUpdateDealResponse2 = zUpdateDealResponse;
|
||||
|
||||
export const zDeleteDealGroupData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zDeleteDealGroupResponse2 = zDeleteDealGroupResponse;
|
||||
|
||||
export const zUpdateDealGroupData = z.object({
|
||||
body: zUpdateDealGroupRequest,
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zUpdateDealGroupResponse2 = zUpdateDealGroupResponse;
|
||||
|
||||
export const zCreateDealGroupData = z.object({
|
||||
body: zCreateDealGroupRequest,
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zCreateDealGroupResponse2 = zCreateDealGroupResponse;
|
||||
|
||||
export const zUpdateDealsInGroupData = z.object({
|
||||
body: zUpdateDealsInGroupRequest,
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zUpdateDealsInGroupResponse2 = zUpdateDealsInGroupResponse;
|
||||
|
||||
export const zGetDealTagsData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
projectId: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetDealTagsResponse2 = zGetDealTagsResponse;
|
||||
|
||||
export const zCreateDealTagData = z.object({
|
||||
body: zCreateDealTagRequest,
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zCreateDealTagResponse2 = zCreateDealTagResponse;
|
||||
|
||||
export const zDeleteDealTagData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zDeleteDealTagResponse2 = zDeleteDealTagResponse;
|
||||
|
||||
export const zUpdateDealTagData = z.object({
|
||||
body: zUpdateDealTagRequest,
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zUpdateDealTagResponse2 = zUpdateDealTagResponse;
|
||||
|
||||
export const zSwitchDealTagData = z.object({
|
||||
body: zSwitchDealTagRequest,
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zSwitchDealTagResponse2 = zSwitchDealTagResponse;
|
||||
|
||||
export const zGetDealTagColorsData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetDealTagColorsResponse = zGetTagColorsResponse;
|
||||
|
||||
export const zGetBuiltInModulesData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.optional(z.never()),
|
||||
@ -1423,117 +1727,6 @@ export const zGetBuiltInModulesData = z.object({
|
||||
*/
|
||||
export const zGetBuiltInModulesResponse = zGetAllBuiltInModulesResponse;
|
||||
|
||||
export const zGetProjectsData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetProjectsResponse2 = zGetProjectsResponse;
|
||||
|
||||
export const zCreateProjectData = z.object({
|
||||
body: zCreateProjectRequest,
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zCreateProjectResponse2 = zCreateProjectResponse;
|
||||
|
||||
export const zDeleteProjectData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zDeleteProjectResponse2 = zDeleteProjectResponse;
|
||||
|
||||
export const zUpdateProjectData = z.object({
|
||||
body: zUpdateProjectRequest,
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zUpdateProjectResponse2 = zUpdateProjectResponse;
|
||||
|
||||
export const zGetStatusesData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
boardId: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetStatusesResponse2 = zGetStatusesResponse;
|
||||
|
||||
export const zCreateStatusData = z.object({
|
||||
body: zCreateStatusRequest,
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zCreateStatusResponse2 = zCreateStatusResponse;
|
||||
|
||||
export const zDeleteStatusData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zDeleteStatusResponse2 = zDeleteStatusResponse;
|
||||
|
||||
export const zUpdateStatusData = z.object({
|
||||
body: zUpdateStatusRequest,
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zUpdateStatusResponse2 = zUpdateStatusResponse;
|
||||
|
||||
export const zGetStatusHistoryData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
dealId: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetStatusHistoryResponse2 = zGetStatusHistoryResponse;
|
||||
|
||||
export const zGetClientsData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.optional(z.never()),
|
||||
@ -2107,3 +2300,127 @@ export const zUpdateServicesKitData = z.object({
|
||||
* Successful Response
|
||||
*/
|
||||
export const zUpdateServicesKitResponse2 = zUpdateServicesKitResponse;
|
||||
|
||||
export const zGetProjectsData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetProjectsResponse2 = zGetProjectsResponse;
|
||||
|
||||
export const zCreateProjectData = z.object({
|
||||
body: zCreateProjectRequest,
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zCreateProjectResponse2 = zCreateProjectResponse;
|
||||
|
||||
export const zDeleteProjectData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zDeleteProjectResponse2 = zDeleteProjectResponse;
|
||||
|
||||
export const zGetProjectData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetProjectResponse2 = zGetProjectResponse;
|
||||
|
||||
export const zUpdateProjectData = z.object({
|
||||
body: zUpdateProjectRequest,
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zUpdateProjectResponse2 = zUpdateProjectResponse;
|
||||
|
||||
export const zGetStatusesData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
boardId: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetStatusesResponse2 = zGetStatusesResponse;
|
||||
|
||||
export const zCreateStatusData = z.object({
|
||||
body: zCreateStatusRequest,
|
||||
path: z.optional(z.never()),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zCreateStatusResponse2 = zCreateStatusResponse;
|
||||
|
||||
export const zDeleteStatusData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zDeleteStatusResponse2 = zDeleteStatusResponse;
|
||||
|
||||
export const zUpdateStatusData = z.object({
|
||||
body: zUpdateStatusRequest,
|
||||
path: z.object({
|
||||
pk: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zUpdateStatusResponse2 = zUpdateStatusResponse;
|
||||
|
||||
export const zGetStatusHistoryData = z.object({
|
||||
body: z.optional(z.never()),
|
||||
path: z.object({
|
||||
dealId: z.int(),
|
||||
}),
|
||||
query: z.optional(z.never()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
export const zGetStatusHistoryResponse2 = zGetStatusHistoryResponse;
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
ProductServiceEditorModal,
|
||||
ServicesKitSelectModal,
|
||||
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals";
|
||||
import DealTagModal from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/modals/DealTagModal";
|
||||
|
||||
export const modals = {
|
||||
enterNameModal: EnterNameModal,
|
||||
@ -40,4 +41,5 @@ export const modals = {
|
||||
printBarcodeModal: PrintBarcodeModal,
|
||||
statusColorPickerModal: ColorPickerModal,
|
||||
marketplaceEditorModal: MarketplaceEditorModal,
|
||||
dealTagModal: DealTagModal,
|
||||
};
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
Image,
|
||||
NumberInput,
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
@ -16,9 +17,9 @@ import {
|
||||
duplicateProductServices,
|
||||
ServicesKitSchema,
|
||||
} from "@/lib/client";
|
||||
import { useFulfillmentBaseContext } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext";
|
||||
import ProductFieldsList from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/components/ProductFieldsList";
|
||||
import ProductViewActions from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/components/ProductViewActions";
|
||||
import { useFulfillmentBaseContext } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext";
|
||||
import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service";
|
||||
import ProductServicesTable from "./components/ProductServicesTable";
|
||||
import styles from "../../../FulfillmentBase.module.css";
|
||||
@ -116,10 +117,10 @@ const ProductView: FC<Props> = ({ dealProduct }) => {
|
||||
)}
|
||||
<Title order={3}>{dealProduct.product.name}</Title>
|
||||
<ProductFieldsList product={dealProduct.product} />
|
||||
{/*<Text>*/}
|
||||
{/* Штрихкоды:*/}
|
||||
{/*{value.product.barcodes.join(", ")}*/}
|
||||
{/*</Text>*/}
|
||||
<Text>
|
||||
Штрихкоды:
|
||||
{dealProduct.product.barcodes.join(", ")}
|
||||
</Text>
|
||||
<NumberInput
|
||||
suffix={" шт."}
|
||||
value={dealProduct.quantity}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { FC } from "react";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { Flex } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||
@ -33,18 +33,28 @@ const ProductViewActions: FC<Props> = ({ dealProduct }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onPrintBarcodeClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "printBarcodeModal",
|
||||
title: "Печать штрихкода",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
product: dealProduct.product,
|
||||
defaultQuantity: dealProduct.quantity,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
mt={"auto"}
|
||||
ml={"auto"}
|
||||
gap={"sm"}>
|
||||
{/*<Tooltip*/}
|
||||
{/* onClick={onPrintBarcodeClick}*/}
|
||||
{/* label="Печать штрихкода">*/}
|
||||
{/* <ActionIcon variant={"default"}>*/}
|
||||
{/* <IconBarcode />*/}
|
||||
{/* </ActionIcon>*/}
|
||||
{/*</Tooltip>*/}
|
||||
<ActionIconWithTip
|
||||
onClick={onPrintBarcodeClick}
|
||||
tipLabel="Печать штрихкода">
|
||||
<IconBarcode />
|
||||
</ActionIconWithTip>
|
||||
<ActionIconWithTip
|
||||
onClick={onProductEditClick}
|
||||
tipLabel="Редактировать товар">
|
||||
|
||||
9
src/types/BuiltInLinkData.ts
Normal file
9
src/types/BuiltInLinkData.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
|
||||
type BuiltInLinkData = {
|
||||
icon: typeof IconPlus;
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export default BuiltInLinkData;
|
||||
7
src/types/GroupWithDealsSchema.ts
Normal file
7
src/types/GroupWithDealsSchema.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { DealGroupSchema, DealSchema } from "@/lib/client";
|
||||
|
||||
type GroupWithDealsSchema = DealGroupSchema & {
|
||||
items: DealSchema[];
|
||||
};
|
||||
|
||||
export default GroupWithDealsSchema;
|
||||
14
yarn.lock
14
yarn.lock
@ -6198,6 +6198,7 @@ __metadata:
|
||||
jest-environment-jsdom: "npm:^30.0.0"
|
||||
lexorank: "npm:^1.0.5"
|
||||
libphonenumber-js: "npm:^1.12.10"
|
||||
mantine-contextmenu: "npm:^8.2.0"
|
||||
mantine-datatable: "npm:^8.2.0"
|
||||
next: "npm:15.4.7"
|
||||
phone: "npm:^3.1.67"
|
||||
@ -10181,6 +10182,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mantine-contextmenu@npm:^8.2.0":
|
||||
version: 8.2.0
|
||||
resolution: "mantine-contextmenu@npm:8.2.0"
|
||||
peerDependencies:
|
||||
"@mantine/core": ">=8.2"
|
||||
"@mantine/hooks": ">=8.2"
|
||||
clsx: ">=2"
|
||||
react: ">=19"
|
||||
react-dom: ">=19"
|
||||
checksum: 10c0/fea66f890eb9baea7e1ebcdcf3732d8522e2322938d7d0de168acffa9abe6ab0efae8838d7af2ae4399d044bca27879775efd0a07d7f6193cf0ebfe250d4b786
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mantine-datatable@npm:^8.2.0":
|
||||
version: 8.2.0
|
||||
resolution: "mantine-datatable@npm:8.2.0"
|
||||
|
||||
Reference in New Issue
Block a user