feat: displaying and sorting groups of deals

This commit is contained in:
2025-10-17 11:52:19 +04:00
parent fc176ec9e4
commit 0fe41656f8
26 changed files with 1310 additions and 224 deletions

View File

@ -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);
}
}

View File

@ -1,11 +1,27 @@
.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-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);
}
}

View File

@ -8,9 +8,10 @@ import styles from "./DealCard.module.css";
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 { openDrawer } = useDrawersContext();
@ -31,7 +32,9 @@ const DealCard = ({ deal }: Props) => {
return (
<Card
onClick={onClick}
className={styles.container}>
className={
isInGroup ? styles["container-in-group"] : styles.container
}>
<Group
justify={"space-between"}
wrap={"nowrap"}

View File

@ -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;

View File

@ -0,0 +1,12 @@
.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);
}
}

View File

@ -0,0 +1,30 @@
import { FC } from "react";
import { Stack, Text } from "@mantine/core";
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
import styles from "./DealsGroup.module.css";
type Props = {
group: GroupWithDealsSchema;
};
const DealsGroup: FC<Props> = ({ group }) => {
return (
<Stack
className={styles["group-container"]}
gap={"xs"}
bdrs={"lg"}
p={"xs"}>
<Text mx={"xs"}>{group.name}</Text>
{group.items.map(deal => (
<DealCard
deal={deal}
isInGroup
key={deal.id}
/>
))}
</Stack>
);
};
export default DealsGroup;

View File

@ -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}

View File

@ -3,18 +3,24 @@
import React 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 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>>;
@ -48,9 +54,17 @@ const useDealsContextState = ({
statuses,
});
const groupsCrud = useDealGroupCrud();
const { dealsWithoutGroup, groupsWithDeals } =
useDealsAndGroups(dealsListObjects);
return {
...dealsListObjects,
dealsCrud,
groupsCrud,
dealsWithoutGroup,
groupsWithDeals,
};
};

View 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;

View File

@ -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,15 +49,29 @@ 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 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, over: Over) => {
const activeStatus = getStatusByDealId(activeId);
const swiperActiveStatus =
@ -58,8 +81,8 @@ const useDealsAndStatusesDnd = (): ReturnType => {
const activeStatusLexorank = activeStatus?.lexorank;
let overStatusLexorank: string | undefined;
if (typeof over.id === "string" && isStatusId(over.id)) {
const overStatusId = getStatusId(over.id);
if (typeof over.id === "string" && isContainerId(over.id)) {
const overStatusId = getContainerId(over.id);
overStatusLexorank = statuses.find(
s => s.id === overStatusId
)?.lexorank;
@ -98,11 +121,16 @@ const useDealsAndStatusesDnd = (): ReturnType => {
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 +141,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
const { overStatus, newLexorank } = getDropTarget(
over.id,
activeDealId,
undefined,
activeStatusId
);
if (!overStatus) return;
@ -130,14 +159,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 +222,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 +267,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 +378,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
const { overStatus, newLexorank } = getDropTarget(
over.id,
activeDealId,
undefined,
activeStatusId,
true
);
@ -261,30 +395,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,
};
};

View File

@ -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();

View 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;

View File

@ -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, ""));

View File

@ -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;

View File

@ -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>

View 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;

View File

@ -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>
);
};

View 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}`;

View 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}`;

View File

@ -1,3 +1,8 @@
export type BaseDraggable = {
id: number;
lexorank: string;
};
export type BaseGroupDraggable<TItem extends BaseDraggable> = BaseDraggable & {
items: TItem[];
};

View File

@ -0,0 +1,69 @@
import { useMutation } from "@tanstack/react-query";
import { UpdateDealGroupSchema } from "@/lib/client";
import {
addDealMutation,
createDealGroupMutation,
removeDealMutation,
updateDealGroupMutation,
} from "@/lib/client/@tanstack/react-query.gen";
export type GroupsCrud = {
onUpdate: (groupId: number, group: UpdateDealGroupSchema) => void;
// onDelete: (group: DealGroupSchema, onSuccess?: () => void) => void;
};
const useDealGroupCrud = (): GroupsCrud => {
const updateMutation = useMutation(updateDealGroupMutation());
const onUpdate = (groupId: number, entity: UpdateDealGroupSchema) => {
updateMutation.mutate({
path: {
pk: groupId,
},
body: {
entity,
},
});
};
const createMutation = useMutation(createDealGroupMutation());
const onCreate = (draggingDealId: number, hoveredDealId: number) => {
createMutation.mutate({
body: {
draggingDealId,
hoveredDealId,
},
});
};
const addDealToGroupMutation = useMutation(addDealMutation());
const onAddDeal = (dealId: number, groupId: number) => {
addDealToGroupMutation.mutate({
body: {
dealId,
groupId,
},
});
};
const removeDealFromGroupMutation = useMutation(removeDealMutation());
const onRemoveDeal = (dealId: number) => {
removeDealFromGroupMutation.mutate({
body: {
dealId,
},
});
};
return {
onUpdate,
// onCreate,
// onAddDeal,
// onRemoveDeal,
};
};
export default useDealGroupCrud;

View File

@ -9,12 +9,14 @@ import {
import type { AxiosError } from "axios";
import { client as _heyApiClient } from "../client.gen";
import {
addDeal,
addKitToDeal,
addKitToDealProduct,
createBarcodeTemplate,
createBoard,
createClient,
createDeal,
createDealGroup,
createDealProduct,
createDealProductService,
createDealService,
@ -59,10 +61,12 @@ import {
getServicesKits,
getStatuses,
getStatusHistory,
removeDeal,
updateBarcodeTemplate,
updateBoard,
updateClient,
updateDeal,
updateDealGroup,
updateDealProduct,
updateDealProductService,
updateDealService,
@ -76,6 +80,9 @@ import {
type Options,
} from "../sdk.gen";
import type {
AddDealData,
AddDealError,
AddDealResponse,
AddKitToDealData,
AddKitToDealError,
AddKitToDealProductData,
@ -93,6 +100,9 @@ import type {
CreateClientResponse2,
CreateDealData,
CreateDealError,
CreateDealGroupData,
CreateDealGroupError,
CreateDealGroupResponse2,
CreateDealProductData,
CreateDealProductError,
CreateDealProductResponse2,
@ -194,6 +204,9 @@ import type {
GetServicesKitsData,
GetStatusesData,
GetStatusHistoryData,
RemoveDealData,
RemoveDealError,
RemoveDealResponse,
UpdateBarcodeTemplateData,
UpdateBarcodeTemplateError,
UpdateBarcodeTemplateResponse2,
@ -205,6 +218,9 @@ import type {
UpdateClientResponse2,
UpdateDealData,
UpdateDealError,
UpdateDealGroupData,
UpdateDealGroupError,
UpdateDealGroupResponse2,
UpdateDealProductData,
UpdateDealProductError,
UpdateDealProductResponse2,
@ -605,6 +621,159 @@ export const updateDealMutation = (
return mutationOptions;
};
/**
* Update Group
*/
export const updateDealGroupMutation = (
options?: Partial<Options<UpdateDealGroupData>>
): UseMutationOptions<
UpdateDealGroupResponse2,
AxiosError<UpdateDealGroupError>,
Options<UpdateDealGroupData>
> => {
const mutationOptions: UseMutationOptions<
UpdateDealGroupResponse2,
AxiosError<UpdateDealGroupError>,
Options<UpdateDealGroupData>
> = {
mutationFn: async localOptions => {
const { data } = await updateDealGroup({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
export const createDealGroupQueryKey = (
options: Options<CreateDealGroupData>
) => createQueryKey("createDealGroup", options);
/**
* Create Group
*/
export const createDealGroupOptions = (
options: Options<CreateDealGroupData>
) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await createDealGroup({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: createDealGroupQueryKey(options),
});
};
/**
* Create Group
*/
export const createDealGroupMutation = (
options?: Partial<Options<CreateDealGroupData>>
): UseMutationOptions<
CreateDealGroupResponse2,
AxiosError<CreateDealGroupError>,
Options<CreateDealGroupData>
> => {
const mutationOptions: UseMutationOptions<
CreateDealGroupResponse2,
AxiosError<CreateDealGroupError>,
Options<CreateDealGroupData>
> = {
mutationFn: async localOptions => {
const { data } = await createDealGroup({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
/**
* Remove Deal
*/
export const removeDealMutation = (
options?: Partial<Options<RemoveDealData>>
): UseMutationOptions<
RemoveDealResponse,
AxiosError<RemoveDealError>,
Options<RemoveDealData>
> => {
const mutationOptions: UseMutationOptions<
RemoveDealResponse,
AxiosError<RemoveDealError>,
Options<RemoveDealData>
> = {
mutationFn: async localOptions => {
const { data } = await removeDeal({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
export const addDealQueryKey = (options: Options<AddDealData>) =>
createQueryKey("addDeal", options);
/**
* Add Deal
*/
export const addDealOptions = (options: Options<AddDealData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await addDeal({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: addDealQueryKey(options),
});
};
/**
* Add Deal
*/
export const addDealMutation = (
options?: Partial<Options<AddDealData>>
): UseMutationOptions<
AddDealResponse,
AxiosError<AddDealError>,
Options<AddDealData>
> => {
const mutationOptions: UseMutationOptions<
AddDealResponse,
AxiosError<AddDealError>,
Options<AddDealData>
> = {
mutationFn: async localOptions => {
const { data } = await addDeal({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
export const getBuiltInModulesQueryKey = (
options?: Options<GetBuiltInModulesData>
) => createQueryKey("getBuiltInModules", options);

View File

@ -3,6 +3,9 @@
import type { Client, Options as ClientOptions, TDataShape } from "./client";
import { client as _heyApiClient } from "./client.gen";
import type {
AddDealData,
AddDealErrors,
AddDealResponses,
AddKitToDealData,
AddKitToDealErrors,
AddKitToDealProductData,
@ -20,6 +23,9 @@ import type {
CreateClientResponses,
CreateDealData,
CreateDealErrors,
CreateDealGroupData,
CreateDealGroupErrors,
CreateDealGroupResponses,
CreateDealProductData,
CreateDealProductErrors,
CreateDealProductResponses,
@ -144,6 +150,9 @@ import type {
GetStatusHistoryData,
GetStatusHistoryErrors,
GetStatusHistoryResponses,
RemoveDealData,
RemoveDealErrors,
RemoveDealResponses,
UpdateBarcodeTemplateData,
UpdateBarcodeTemplateErrors,
UpdateBarcodeTemplateResponses,
@ -155,6 +164,9 @@ import type {
UpdateClientResponses,
UpdateDealData,
UpdateDealErrors,
UpdateDealGroupData,
UpdateDealGroupErrors,
UpdateDealGroupResponses,
UpdateDealProductData,
UpdateDealProductErrors,
UpdateDealProductResponses,
@ -188,6 +200,8 @@ import type {
UpdateStatusResponses,
} from "./types.gen";
import {
zAddDealData,
zAddDealResponse,
zAddKitToDealData,
zAddKitToDealProductData,
zAddKitToDealProductResponse,
@ -199,6 +213,8 @@ import {
zCreateClientData,
zCreateClientResponse2,
zCreateDealData,
zCreateDealGroupData,
zCreateDealGroupResponse2,
zCreateDealProductData,
zCreateDealProductResponse2,
zCreateDealProductServiceData,
@ -288,6 +304,8 @@ import {
zGetStatusesResponse2,
zGetStatusHistoryData,
zGetStatusHistoryResponse2,
zRemoveDealData,
zRemoveDealResponse,
zUpdateBarcodeTemplateData,
zUpdateBarcodeTemplateResponse2,
zUpdateBoardData,
@ -295,6 +313,8 @@ import {
zUpdateClientData,
zUpdateClientResponse2,
zUpdateDealData,
zUpdateDealGroupData,
zUpdateDealGroupResponse2,
zUpdateDealProductData,
zUpdateDealProductResponse2,
zUpdateDealProductServiceData,
@ -535,6 +555,114 @@ export const updateDeal = <ThrowOnError extends boolean = false>(
});
};
/**
* Update Group
*/
export const updateDealGroup = <ThrowOnError extends boolean = false>(
options: Options<UpdateDealGroupData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).patch<
UpdateDealGroupResponses,
UpdateDealGroupErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zUpdateDealGroupData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zUpdateDealGroupResponse2.parseAsync(data);
},
url: "/deal-group/{pk}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Create Group
*/
export const createDealGroup = <ThrowOnError extends boolean = false>(
options: Options<CreateDealGroupData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
CreateDealGroupResponses,
CreateDealGroupErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zCreateDealGroupData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zCreateDealGroupResponse2.parseAsync(data);
},
url: "/deal-group/",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Remove Deal
*/
export const removeDeal = <ThrowOnError extends boolean = false>(
options: Options<RemoveDealData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).delete<
RemoveDealResponses,
RemoveDealErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zRemoveDealData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zRemoveDealResponse.parseAsync(data);
},
url: "/deal-group/deal",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Add Deal
*/
export const addDeal = <ThrowOnError extends boolean = false>(
options: Options<AddDealData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
AddDealResponses,
AddDealErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zAddDealData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zAddDealResponse.parseAsync(data);
},
url: "/deal-group/deal",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Get Built In Modules
*/

View File

@ -1,5 +1,29 @@
// This file is auto-generated by @hey-api/openapi-ts
/**
* AddDealToGroupRequest
*/
export type AddDealToGroupRequest = {
/**
* Dealid
*/
dealId: number;
/**
* Groupid
*/
groupId: number;
};
/**
* AddDealToGroupResponse
*/
export type AddDealToGroupResponse = {
/**
* Message
*/
message: string;
};
/**
* BarcodeTemplateAttributeSchema
*/
@ -347,6 +371,27 @@ export type CreateClientSchema = {
details: ClientDetailsSchema;
};
/**
* CreateDealGroupRequest
*/
export type CreateDealGroupRequest = {
/**
* Draggingdealid
*/
draggingDealId: number;
/**
* Hovereddealid
*/
hoveredDealId: number;
};
/**
* CreateDealGroupResponse
*/
export type CreateDealGroupResponse = {
entity: DealGroupSchema;
};
/**
* CreateDealProductRequest
*/
@ -832,6 +877,24 @@ export type DealAddKitResponse = {
message: string;
};
/**
* DealGroupSchema
*/
export type DealGroupSchema = {
/**
* Id
*/
id: number;
/**
* Name
*/
name?: string | null;
/**
* Lexorank
*/
lexorank: string;
};
/**
* DealProductAddKitRequest
*/
@ -887,6 +950,26 @@ export type DealProductSchema = {
productServices: Array<ProductServiceSchema>;
};
/**
* DealRemoveFromGroupRequest
*/
export type DealRemoveFromGroupRequest = {
/**
* Dealid
*/
dealId: number;
};
/**
* DealRemoveFromGroupResponse
*/
export type DealRemoveFromGroupResponse = {
/**
* Message
*/
message: string;
};
/**
* DealSchema
*/
@ -909,6 +992,7 @@ export type DealSchema = {
* Createdat
*/
createdAt: string;
group: DealGroupSchema | null;
/**
* Productsquantity
*/
@ -1753,6 +1837,41 @@ export type UpdateClientSchema = {
details?: ClientDetailsSchema | null;
};
/**
* UpdateDealGroupRequest
*/
export type UpdateDealGroupRequest = {
entity: UpdateDealGroupSchema;
};
/**
* UpdateDealGroupResponse
*/
export type UpdateDealGroupResponse = {
/**
* Message
*/
message: string;
};
/**
* UpdateDealGroupSchema
*/
export type UpdateDealGroupSchema = {
/**
* Name
*/
name?: string | null;
/**
* Lexorank
*/
lexorank?: string | null;
/**
* Statusid
*/
statusId?: number | null;
};
/**
* UpdateDealProductRequest
*/
@ -2466,6 +2585,115 @@ export type UpdateDealResponses = {
export type UpdateDealResponse2 =
UpdateDealResponses[keyof UpdateDealResponses];
export type UpdateDealGroupData = {
body: UpdateDealGroupRequest;
path: {
/**
* Pk
*/
pk: number;
};
query?: never;
url: "/deal-group/{pk}";
};
export type UpdateDealGroupErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UpdateDealGroupError =
UpdateDealGroupErrors[keyof UpdateDealGroupErrors];
export type UpdateDealGroupResponses = {
/**
* Successful Response
*/
200: UpdateDealGroupResponse;
};
export type UpdateDealGroupResponse2 =
UpdateDealGroupResponses[keyof UpdateDealGroupResponses];
export type CreateDealGroupData = {
body: CreateDealGroupRequest;
path?: never;
query?: never;
url: "/deal-group/";
};
export type CreateDealGroupErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateDealGroupError =
CreateDealGroupErrors[keyof CreateDealGroupErrors];
export type CreateDealGroupResponses = {
/**
* Successful Response
*/
200: CreateDealGroupResponse;
};
export type CreateDealGroupResponse2 =
CreateDealGroupResponses[keyof CreateDealGroupResponses];
export type RemoveDealData = {
body: DealRemoveFromGroupRequest;
path?: never;
query?: never;
url: "/deal-group/deal";
};
export type RemoveDealErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type RemoveDealError = RemoveDealErrors[keyof RemoveDealErrors];
export type RemoveDealResponses = {
/**
* Successful Response
*/
200: DealRemoveFromGroupResponse;
};
export type RemoveDealResponse = RemoveDealResponses[keyof RemoveDealResponses];
export type AddDealData = {
body: AddDealToGroupRequest;
path?: never;
query?: never;
url: "/deal-group/deal";
};
export type AddDealErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type AddDealError = AddDealErrors[keyof AddDealErrors];
export type AddDealResponses = {
/**
* Successful Response
*/
200: AddDealToGroupResponse;
};
export type AddDealResponse = AddDealResponses[keyof AddDealResponses];
export type GetBuiltInModulesData = {
body?: never;
path?: never;

View File

@ -2,6 +2,21 @@
import { z } from "zod";
/**
* AddDealToGroupRequest
*/
export const zAddDealToGroupRequest = z.object({
dealId: z.int(),
groupId: z.int(),
});
/**
* AddDealToGroupResponse
*/
export const zAddDealToGroupResponse = z.object({
message: z.string(),
});
/**
* BarcodeTemplateAttributeSchema
*/
@ -193,6 +208,30 @@ export const zCreateClientResponse = z.object({
message: z.string(),
});
/**
* CreateDealGroupRequest
*/
export const zCreateDealGroupRequest = z.object({
draggingDealId: z.int(),
hoveredDealId: 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
*/
@ -336,6 +375,7 @@ export const zDealSchema = z.object({
createdAt: z.iso.datetime({
offset: true,
}),
group: z.union([zDealGroupSchema, z.null()]),
productsQuantity: z.optional(z.int()).default(0),
totalPrice: z.optional(z.number()).default(0),
client: z.optional(z.union([zClientSchema, z.null()])),
@ -654,6 +694,20 @@ export const zDealProductAddKitResponse = z.object({
message: z.string(),
});
/**
* DealRemoveFromGroupRequest
*/
export const zDealRemoveFromGroupRequest = z.object({
dealId: z.int(),
});
/**
* DealRemoveFromGroupResponse
*/
export const zDealRemoveFromGroupResponse = z.object({
message: z.string(),
});
/**
* DeleteBarcodeTemplateResponse
*/
@ -1034,6 +1088,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
*/
@ -1412,6 +1489,52 @@ export const zUpdateDealData = z.object({
*/
export const zUpdateDealResponse2 = zUpdateDealResponse;
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 zRemoveDealData = z.object({
body: zDealRemoveFromGroupRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zRemoveDealResponse = zDealRemoveFromGroupResponse;
export const zAddDealData = z.object({
body: zAddDealToGroupRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zAddDealResponse = zAddDealToGroupResponse;
export const zGetBuiltInModulesData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),

View File

@ -0,0 +1,7 @@
import { DealGroupSchema, DealSchema } from "@/lib/client";
type GroupWithDealsSchema = DealGroupSchema & {
items: DealSchema[];
};
export default GroupWithDealsSchema;