From 0fe41656f887f02cc6be830296f7568dbd75bff0 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Fri, 17 Oct 2025 11:52:19 +0400 Subject: [PATCH] feat: displaying and sorting groups of deals --- .../CreateDealButton.module.css | 4 +- .../shared/DealCard/DealCard.module.css | 16 ++ .../components/shared/DealCard/DealCard.tsx | 7 +- .../shared/DealContainer/DealContainer.tsx | 25 -- .../shared/DealsGroup/DealsGroup.module.css | 12 + .../shared/DealsGroup/DealsGroup.tsx | 30 +++ .../deals/components/shared/Funnel/Funnel.tsx | 54 ++-- src/app/deals/contexts/DealsContext.tsx | 14 + src/app/deals/hooks/useDealsAndGroups.ts | 44 +++ src/app/deals/hooks/useDealsAndStatusesDnd.ts | 250 ++++++++++++++---- src/app/deals/hooks/useGetNewRank.ts | 60 +++-- src/app/deals/utils/isItemGroup.ts | 15 ++ src/app/deals/utils/statusId.ts | 6 - src/components/dnd/FunnelDnd/FunnelColumn.tsx | 42 --- src/components/dnd/FunnelDnd/FunnelDnd.tsx | 109 +++++--- .../dnd/FunnelDnd/components/FunnelColumn.tsx | 76 ++++++ .../{ => components}/FunnelOverlay.tsx | 24 +- .../dnd/FunnelDnd/utils/columnId.ts | 9 + src/components/dnd/FunnelDnd/utils/groupId.ts | 8 + src/components/dnd/types/types.ts | 5 + src/hooks/cruds/useDealGroupCrud.tsx | 69 +++++ src/lib/client/@tanstack/react-query.gen.ts | 169 ++++++++++++ src/lib/client/sdk.gen.ts | 128 +++++++++ src/lib/client/types.gen.ts | 228 ++++++++++++++++ src/lib/client/zod.gen.ts | 123 +++++++++ src/types/GroupWithDealsSchema.ts | 7 + 26 files changed, 1310 insertions(+), 224 deletions(-) delete mode 100644 src/app/deals/components/shared/DealContainer/DealContainer.tsx create mode 100644 src/app/deals/components/shared/DealsGroup/DealsGroup.module.css create mode 100644 src/app/deals/components/shared/DealsGroup/DealsGroup.tsx create mode 100644 src/app/deals/hooks/useDealsAndGroups.ts create mode 100644 src/app/deals/utils/isItemGroup.ts delete mode 100644 src/app/deals/utils/statusId.ts delete mode 100644 src/components/dnd/FunnelDnd/FunnelColumn.tsx create mode 100644 src/components/dnd/FunnelDnd/components/FunnelColumn.tsx rename src/components/dnd/FunnelDnd/{ => components}/FunnelOverlay.tsx (51%) create mode 100644 src/components/dnd/FunnelDnd/utils/columnId.ts create mode 100644 src/components/dnd/FunnelDnd/utils/groupId.ts create mode 100644 src/hooks/cruds/useDealGroupCrud.tsx create mode 100644 src/types/GroupWithDealsSchema.ts diff --git a/src/app/deals/components/shared/CreateDealButton/CreateDealButton.module.css b/src/app/deals/components/shared/CreateDealButton/CreateDealButton.module.css index bf4b467..f896eeb 100644 --- a/src/app/deals/components/shared/CreateDealButton/CreateDealButton.module.css +++ b/src/app/deals/components/shared/CreateDealButton/CreateDealButton.module.css @@ -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); } } diff --git a/src/app/deals/components/shared/DealCard/DealCard.module.css b/src/app/deals/components/shared/DealCard/DealCard.module.css index fef50ef..1a749f6 100644 --- a/src/app/deals/components/shared/DealCard/DealCard.module.css +++ b/src/app/deals/components/shared/DealCard/DealCard.module.css @@ -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); } } diff --git a/src/app/deals/components/shared/DealCard/DealCard.tsx b/src/app/deals/components/shared/DealCard/DealCard.tsx index c840a5c..7c5ecb6 100644 --- a/src/app/deals/components/shared/DealCard/DealCard.tsx +++ b/src/app/deals/components/shared/DealCard/DealCard.tsx @@ -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 ( + className={ + isInGroup ? styles["container-in-group"] : styles.container + }> = ({ deal }) => { - const dealBody = useMemo(() => , [deal]); - - return ( - - dealBody} - /> - - ); -}; - -export default DealContainer; diff --git a/src/app/deals/components/shared/DealsGroup/DealsGroup.module.css b/src/app/deals/components/shared/DealsGroup/DealsGroup.module.css new file mode 100644 index 0000000..c05f58a --- /dev/null +++ b/src/app/deals/components/shared/DealsGroup/DealsGroup.module.css @@ -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); + } +} diff --git a/src/app/deals/components/shared/DealsGroup/DealsGroup.tsx b/src/app/deals/components/shared/DealsGroup/DealsGroup.tsx new file mode 100644 index 0000000..f376650 --- /dev/null +++ b/src/app/deals/components/shared/DealsGroup/DealsGroup.tsx @@ -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 = ({ group }) => { + return ( + + {group.name} + {group.items.map(deal => ( + + ))} + + ); +}; + +export default DealsGroup; diff --git a/src/app/deals/components/shared/Funnel/Funnel.tsx b/src/app/deals/components/shared/Funnel/Funnel.tsx index e98a919..c62ac29 100644 --- a/src/app/deals/components/shared/Funnel/Funnel.tsx +++ b/src/app/deals/components/shared/Funnel/Funnel.tsx @@ -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 ( - 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 => ( )} renderItem={(deal: DealSchema) => ( - )} - activeContainer={activeStatus} - activeItem={activeDeal} - renderItemOverlay={(deal: DealSchema) => } + renderGroup={(group: GroupWithDealsSchema) => ( + + )} renderContainerOverlay={(status: StatusSchema, children) => ( ( )}> {children} diff --git a/src/app/deals/contexts/DealsContext.tsx b/src/app/deals/contexts/DealsContext.tsx index c0f751c..292b2b9 100644 --- a/src/app/deals/contexts/DealsContext.tsx +++ b/src/app/deals/contexts/DealsContext.tsx @@ -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>; @@ -48,9 +54,17 @@ const useDealsContextState = ({ statuses, }); + const groupsCrud = useDealGroupCrud(); + + const { dealsWithoutGroup, groupsWithDeals } = + useDealsAndGroups(dealsListObjects); + return { ...dealsListObjects, dealsCrud, + groupsCrud, + dealsWithoutGroup, + groupsWithDeals, }; }; diff --git a/src/app/deals/hooks/useDealsAndGroups.ts b/src/app/deals/hooks/useDealsAndGroups.ts new file mode 100644 index 0000000..7132f60 --- /dev/null +++ b/src/app/deals/hooks/useDealsAndGroups.ts @@ -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(); + + 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; diff --git a/src/app/deals/hooks/useDealsAndStatusesDnd.ts b/src/app/deals/hooks/useDealsAndStatusesDnd.ts index 332986b..65d1ea9 100644 --- a/src/app/deals/hooks/useDealsAndStatusesDnd.ts +++ b/src/app/deals/hooks/useDealsAndStatusesDnd.ts @@ -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; }; const useDealsAndStatusesDnd = (): ReturnType => { const swiperRef = useRef(null); - const [activeDeal, setActiveDeal] = useState(null); - const [activeStatus, setActiveStatus] = useState(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, }; }; diff --git a/src/app/deals/hooks/useGetNewRank.ts b/src/app/deals/hooks/useGetNewRank.ts index ff39414..923c3a3 100644 --- a/src/app/deals/hooks/useGetNewRank.ts +++ b/src/app/deals/hooks/useGetNewRank.ts @@ -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, +> = { 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, +>(): NewRankGetters => { 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(); diff --git a/src/app/deals/utils/isItemGroup.ts b/src/app/deals/utils/isItemGroup.ts new file mode 100644 index 0000000..dae8285 --- /dev/null +++ b/src/app/deals/utils/isItemGroup.ts @@ -0,0 +1,15 @@ +import { + BaseDraggable, + BaseGroupDraggable, +} from "@/components/dnd/types/types"; + +const isItemGroup = < + TItem extends BaseDraggable, + TGroup extends BaseGroupDraggable, +>( + item: TItem | TGroup +): boolean => { + return "items" in item; +}; + +export default isItemGroup; diff --git a/src/app/deals/utils/statusId.ts b/src/app/deals/utils/statusId.ts deleted file mode 100644 index 8479c16..0000000 --- a/src/app/deals/utils/statusId.ts +++ /dev/null @@ -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, "")); diff --git a/src/components/dnd/FunnelDnd/FunnelColumn.tsx b/src/components/dnd/FunnelDnd/FunnelColumn.tsx deleted file mode 100644 index 53e6052..0000000 --- a/src/components/dnd/FunnelDnd/FunnelColumn.tsx +++ /dev/null @@ -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 = { - id: string; - items: TItem[]; - renderItem: (item: TItem) => ReactNode; - children?: ReactNode; -}; - -const FunnelColumn = ({ - id, - items, - renderItem, - children, -}: Props) => { - const { setNodeRef } = useDroppable({ id }); - - return ( - <> - {children} - - - {items.map(renderItem)} - - - - ); -}; - -export default FunnelColumn; diff --git a/src/components/dnd/FunnelDnd/FunnelDnd.tsx b/src/components/dnd/FunnelDnd/FunnelDnd.tsx index 6359f36..43d1845 100644 --- a/src/components/dnd/FunnelDnd/FunnelDnd.tsx +++ b/src/components/dnd/FunnelDnd/FunnelDnd.tsx @@ -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 = { +type Props = { containers: TContainer[]; - items: TItem[]; - onDragStart: (event: DragStartEvent) => void; + itemsAndGroups: (TItem | TGroup)[]; onDragOver: (event: DragOverEvent) => void; onDragEnd: (event: DragEndEvent) => void; swiperRef: RefObject; @@ -42,11 +53,8 @@ type Props = { 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 = { const FunnelDnd = < TContainer extends BaseDraggable, TItem extends BaseDraggable, + TGroup extends BaseGroupDraggable, >({ 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) => { +}: Props) => { const sensors = useDndSensors(); const isMobile = useIsMobile(); const frequency = useMemo(() => (isMobile ? 1 : undefined), [isMobile]); + const [activeItem, setActiveItem] = useState(null); + const [activeContainer, setActiveContainer] = useState( + null + ); + const [activeGroup, setActiveGroup] = useState(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 ( , 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 ( + onDragEnd={state => { + setActiveContainer(null); + setActiveItem(null); + setActiveGroup(null); + onDragEnd(state); + }}> + getDndContainerId(container.id) + )} strategy={horizontalListSortingStrategy}> {renderBody()} { - const containerItems = getItemsByContainer( - container, - items - ); - const containerId = getContainerId(container); + const containerItems = getItemsByContainer(container); + const containerId = getDndContainerId(container.id); return renderContainerOverlay( container, ); }} - renderItem={renderItemOverlay} + renderItem={renderItem} + renderGroup={renderGroup} /> diff --git a/src/components/dnd/FunnelDnd/components/FunnelColumn.tsx b/src/components/dnd/FunnelDnd/components/FunnelColumn.tsx new file mode 100644 index 0000000..4767965 --- /dev/null +++ b/src/components/dnd/FunnelDnd/components/FunnelColumn.tsx @@ -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, +> = { + id: string; + itemsAndGroups: (TItem | TGroup)[]; + renderItem: (item: TItem) => ReactNode; + renderGroup: (group: TGroup) => ReactNode; + children?: ReactNode; +}; + +const FunnelColumn = < + TItem extends BaseDraggable, + TGroup extends BaseGroupDraggable, +>({ + id, + itemsAndGroups, + renderItem, + renderGroup, + children, +}: Props) => { + const { setNodeRef } = useDroppable({ id }); + + return ( + <> + {children} + + isItemGroup(itemOrGroup) + ? getDndGroupId(itemOrGroup.id) + : itemOrGroup.id + )} + strategy={verticalListSortingStrategy}> + + {itemsAndGroups.map(itemOrGroup => + "items" in itemOrGroup ? ( + renderGroup(itemOrGroup)} + /> + ) : ( + renderItem(itemOrGroup)} + /> + ) + )} + + + + ); +}; + +export default FunnelColumn; diff --git a/src/components/dnd/FunnelDnd/FunnelOverlay.tsx b/src/components/dnd/FunnelDnd/components/FunnelOverlay.tsx similarity index 51% rename from src/components/dnd/FunnelDnd/FunnelOverlay.tsx rename to src/components/dnd/FunnelDnd/components/FunnelOverlay.tsx index 18d175b..e0a4f7a 100644 --- a/src/components/dnd/FunnelDnd/FunnelOverlay.tsx +++ b/src/components/dnd/FunnelDnd/components/FunnelOverlay.tsx @@ -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 = { +type Props = { activeContainer: TContainer | null; activeItem: TItem | null; + activeGroup: TGroup | null; renderContainer: (container: TContainer) => ReactNode; renderItem: (item: TItem) => ReactNode; + renderGroup: (group: TGroup) => ReactNode; }; -const FunnelOverlay = ({ +const FunnelOverlay = ({ activeContainer, activeItem, + activeGroup, renderContainer, renderItem, -}: Props) => { + renderGroup, +}: Props) => { + const renderOverlay = () => { + if (activeItem) return renderItem(activeItem); + if (activeContainer) return renderContainer(activeContainer); + if (activeGroup) return renderGroup(activeGroup); + }; + return ( -
- {activeItem - ? renderItem(activeItem) - : activeContainer - ? renderContainer(activeContainer) - : null} -
+
{renderOverlay()}
); }; diff --git a/src/components/dnd/FunnelDnd/utils/columnId.ts b/src/components/dnd/FunnelDnd/utils/columnId.ts new file mode 100644 index 0000000..809badc --- /dev/null +++ b/src/components/dnd/FunnelDnd/utils/columnId.ts @@ -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}`; diff --git a/src/components/dnd/FunnelDnd/utils/groupId.ts b/src/components/dnd/FunnelDnd/utils/groupId.ts new file mode 100644 index 0000000..9acbc73 --- /dev/null +++ b/src/components/dnd/FunnelDnd/utils/groupId.ts @@ -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}`; diff --git a/src/components/dnd/types/types.ts b/src/components/dnd/types/types.ts index 9122003..7b06b95 100644 --- a/src/components/dnd/types/types.ts +++ b/src/components/dnd/types/types.ts @@ -1,3 +1,8 @@ export type BaseDraggable = { id: number; + lexorank: string; +}; + +export type BaseGroupDraggable = BaseDraggable & { + items: TItem[]; }; diff --git a/src/hooks/cruds/useDealGroupCrud.tsx b/src/hooks/cruds/useDealGroupCrud.tsx new file mode 100644 index 0000000..ca062a0 --- /dev/null +++ b/src/hooks/cruds/useDealGroupCrud.tsx @@ -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; diff --git a/src/lib/client/@tanstack/react-query.gen.ts b/src/lib/client/@tanstack/react-query.gen.ts index 807996b..4d2939f 100644 --- a/src/lib/client/@tanstack/react-query.gen.ts +++ b/src/lib/client/@tanstack/react-query.gen.ts @@ -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> +): UseMutationOptions< + UpdateDealGroupResponse2, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + UpdateDealGroupResponse2, + AxiosError, + Options + > = { + mutationFn: async localOptions => { + const { data } = await updateDealGroup({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +export const createDealGroupQueryKey = ( + options: Options +) => createQueryKey("createDealGroup", options); + +/** + * Create Group + */ +export const createDealGroupOptions = ( + options: Options +) => { + 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> +): UseMutationOptions< + CreateDealGroupResponse2, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + CreateDealGroupResponse2, + AxiosError, + Options + > = { + mutationFn: async localOptions => { + const { data } = await createDealGroup({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * Remove Deal + */ +export const removeDealMutation = ( + options?: Partial> +): UseMutationOptions< + RemoveDealResponse, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + RemoveDealResponse, + AxiosError, + Options + > = { + mutationFn: async localOptions => { + const { data } = await removeDeal({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +export const addDealQueryKey = (options: Options) => + createQueryKey("addDeal", options); + +/** + * Add Deal + */ +export const addDealOptions = (options: Options) => { + 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> +): UseMutationOptions< + AddDealResponse, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + AddDealResponse, + AxiosError, + Options + > = { + mutationFn: async localOptions => { + const { data } = await addDeal({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + export const getBuiltInModulesQueryKey = ( options?: Options ) => createQueryKey("getBuiltInModules", options); diff --git a/src/lib/client/sdk.gen.ts b/src/lib/client/sdk.gen.ts index cb11ef2..c4d745c 100644 --- a/src/lib/client/sdk.gen.ts +++ b/src/lib/client/sdk.gen.ts @@ -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 = ( }); }; +/** + * Update Group + */ +export const updateDealGroup = ( + options: Options +) => { + 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 = ( + options: Options +) => { + 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 = ( + options: Options +) => { + 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 = ( + options: Options +) => { + 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 */ diff --git a/src/lib/client/types.gen.ts b/src/lib/client/types.gen.ts index 7ed56e0..028c583 100644 --- a/src/lib/client/types.gen.ts +++ b/src/lib/client/types.gen.ts @@ -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; }; +/** + * 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; diff --git a/src/lib/client/zod.gen.ts b/src/lib/client/zod.gen.ts index 008eecd..455f141 100644 --- a/src/lib/client/zod.gen.ts +++ b/src/lib/client/zod.gen.ts @@ -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()), diff --git a/src/types/GroupWithDealsSchema.ts b/src/types/GroupWithDealsSchema.ts new file mode 100644 index 0000000..baa2900 --- /dev/null +++ b/src/types/GroupWithDealsSchema.ts @@ -0,0 +1,7 @@ +import { DealGroupSchema, DealSchema } from "@/lib/client"; + +type GroupWithDealsSchema = DealGroupSchema & { + items: DealSchema[]; +}; + +export default GroupWithDealsSchema;