feat: deal group and stuff
This commit is contained in:
@@ -3,11 +3,12 @@ import styles from "./Board.module.css";
|
||||
import { Divider, Text, Title } from "@mantine/core";
|
||||
import { Draggable, Droppable } from "@hello-pangea/dnd";
|
||||
import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx";
|
||||
import { DealSummary } from "../../../client";
|
||||
import { DealGroupSchema, DealSummary } from "../../../client";
|
||||
import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx";
|
||||
import classNames from "classnames";
|
||||
import { getPluralForm } from "../../../shared/lib/utils.ts";
|
||||
import { sum } from "lodash";
|
||||
import { groupBy, has, sum, uniq } from "lodash";
|
||||
import { DealGroupView } from "../DealGroupView/DealGroupView.tsx";
|
||||
|
||||
type Props = {
|
||||
droppableId: string;
|
||||
@@ -16,24 +17,127 @@ type Props = {
|
||||
summaries: DealSummary[];
|
||||
color: string;
|
||||
};
|
||||
type GroupWithDeals = { group: DealGroupSchema, deals: DealSummary[] }
|
||||
|
||||
export const Board: FC<Props> = ({
|
||||
droppableId,
|
||||
title,
|
||||
summaries,
|
||||
color,
|
||||
withCreateButton = false,
|
||||
}) => {
|
||||
droppableId,
|
||||
title,
|
||||
summaries,
|
||||
color,
|
||||
withCreateButton = false,
|
||||
}) => {
|
||||
const getDealsText = () => {
|
||||
const pluralForm = getPluralForm(
|
||||
summaries.length,
|
||||
"сделка",
|
||||
"сделки",
|
||||
"сделок"
|
||||
"сделок",
|
||||
);
|
||||
return `${summaries.length} ${pluralForm}: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}₽`;
|
||||
};
|
||||
const isGroup = (obj: GroupWithDeals | DealSummary): obj is GroupWithDeals => {
|
||||
return has(obj, "deals");
|
||||
};
|
||||
|
||||
const getDateFromDealOrGroup = (obj: GroupWithDeals | DealSummary) => {
|
||||
// if is group get group with the delivery earliest date
|
||||
if (isGroup(obj)) {
|
||||
const dates = obj.deals.map(d => d.deliveryDate || d.createdAt).filter(Boolean);
|
||||
if (dates.length === 0) return null;
|
||||
return new Date(Math.min(...dates.map(d => {
|
||||
return new Date(d).getTime();
|
||||
})));
|
||||
}
|
||||
// if is deal get deal delivery date
|
||||
return new Date(obj.deliveryDate || obj.createdAt);
|
||||
};
|
||||
const getDealGroups = (): GroupWithDeals[] => {
|
||||
const groups = uniq<DealGroupSchema>(summaries.filter(s => s.group).map(summary => summary.group) as DealGroupSchema[]);
|
||||
if (groups.length === 0) return [];
|
||||
const groupedSummaries = groupBy(summaries, "group.id");
|
||||
const groupDict = groups.reduce((acc, group) => {
|
||||
acc[group.id] = group;
|
||||
return acc;
|
||||
}
|
||||
, {} as { [key: number]: DealGroupSchema });
|
||||
return Object.entries(groupedSummaries).reduce((acc, [groupId, deals]) => {
|
||||
if (!groupId) return acc;
|
||||
const group = groupDict[parseInt(groupId)];
|
||||
if (!group) return acc;
|
||||
acc.push({
|
||||
group,
|
||||
deals,
|
||||
});
|
||||
return acc;
|
||||
}, [] as { group: DealGroupSchema; deals: DealSummary[] }[]);
|
||||
};
|
||||
const getDealsAndGroups = (): (GroupWithDeals | DealSummary)[] => {
|
||||
const groups = getDealGroups();
|
||||
const deals = summaries.filter(s => !s.group);
|
||||
const data = [...groups, ...deals];
|
||||
return data.sort((a, b) => {
|
||||
const aDate = getDateFromDealOrGroup(a);
|
||||
const bDate = getDateFromDealOrGroup(b);
|
||||
if (!aDate || !bDate) return 0;
|
||||
return aDate.getTime() - bDate.getTime();
|
||||
}).map((obj, index) => {
|
||||
if (isGroup(obj)) {
|
||||
obj.deals[0].rank = index;
|
||||
return obj;
|
||||
}
|
||||
obj.rank = index;
|
||||
return obj;
|
||||
}) as (GroupWithDeals | DealSummary)[];
|
||||
};
|
||||
|
||||
const renderDeal = (deal: DealSummary) => {
|
||||
return (<Draggable
|
||||
|
||||
draggableId={deal.id.toString()}
|
||||
index={deal.rank}
|
||||
key={deal.id}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}>
|
||||
<div
|
||||
style={{
|
||||
transition: "transform 0.1s ease-in-out",
|
||||
transform: snapshot.combineWith ? "scale(0.85)" : "none",
|
||||
color: snapshot.combineWith ? "red" : "black",
|
||||
}}
|
||||
>
|
||||
<DealSummaryCard
|
||||
dealSummary={deal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</Draggable>);
|
||||
};
|
||||
const renderGroup = (obj: GroupWithDeals) => {
|
||||
const { deals, group } = obj;
|
||||
return (<Draggable
|
||||
draggableId={"group-" + group.id}
|
||||
index={deals[0].rank}
|
||||
key={"group-" + group.id}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<DealGroupView
|
||||
deals={deals}
|
||||
group={group}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>);
|
||||
};
|
||||
return (
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["header"]}>
|
||||
@@ -45,38 +149,29 @@ export const Board: FC<Props> = ({
|
||||
color={color}
|
||||
/>
|
||||
</div>
|
||||
<Droppable droppableId={droppableId}>
|
||||
<Droppable
|
||||
isCombineEnabled
|
||||
droppableId={droppableId}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
className={classNames(
|
||||
styles["items-list"],
|
||||
snapshot.isDraggingOver &&
|
||||
!snapshot.draggingFromThisWith &&
|
||||
styles["items-list-drag-over"]
|
||||
!snapshot.draggingFromThisWith &&
|
||||
styles["items-list-drag-over"],
|
||||
)}
|
||||
{...provided.droppableProps}>
|
||||
{withCreateButton && (
|
||||
<CreateDealButton onClick={() => {}} />
|
||||
<CreateDealButton onClick={() => {
|
||||
}} />
|
||||
)}
|
||||
{summaries.map(summary => (
|
||||
<Draggable
|
||||
draggableId={summary.id.toString()}
|
||||
index={summary.rank}
|
||||
key={summary.id}>
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}>
|
||||
<DealSummaryCard
|
||||
dealSummary={summary}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
{getDealsAndGroups().map(obj => {
|
||||
if (isGroup(obj)) {
|
||||
return renderGroup(obj);
|
||||
}
|
||||
return renderDeal(obj);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
@@ -86,3 +181,4 @@ export const Board: FC<Props> = ({
|
||||
};
|
||||
|
||||
export default Board;
|
||||
|
||||
|
||||
88
src/components/Dnd/DealGroupView/DealGroupView.tsx
Normal file
88
src/components/Dnd/DealGroupView/DealGroupView.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { DealGroupSchema, DealService, DealSummary } from "../../../client";
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx";
|
||||
import { Flex, rem, Text, TextInput, useMantineColorScheme } from "@mantine/core";
|
||||
import { IconGripHorizontal } from "@tabler/icons-react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { notifications } from "../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
deals: DealSummary[];
|
||||
group: DealGroupSchema;
|
||||
}
|
||||
|
||||
export const DealGroupView: FC<Props> = ({ deals, group }) => {
|
||||
const theme = useMantineColorScheme();
|
||||
const [name, setName] = useState<string>(group.name || "");
|
||||
const [debouncedName] = useDebouncedValue(name, 200);
|
||||
const totalPrice = useMemo(() => deals.reduce((acc, deal) => acc + deal.totalPrice, 0), [deals]);
|
||||
const totalProducts = useMemo(() => deals.reduce((acc, deal) => acc + deal.totalProducts, 0), [deals]);
|
||||
const updateName = () => {
|
||||
if (debouncedName === group.name) return;
|
||||
DealService.updateDealGroup({
|
||||
requestBody: {
|
||||
data: {
|
||||
...group,
|
||||
name: debouncedName,
|
||||
},
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.ok) return;
|
||||
setName(group.name || "");
|
||||
notifications.guess(response.ok, { message: response.message });
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateName();
|
||||
}, [debouncedName]);
|
||||
return (
|
||||
<Flex
|
||||
style={{
|
||||
border: "dashed var(--item-border-size) var(--mantine-color-default-border)",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
|
||||
p={rem(5)}
|
||||
py={rem(10)}
|
||||
bg={theme.colorScheme === "dark" ? "var(--mantine-color-dark-5)" : "var(--mantine-color-gray-1)"}
|
||||
gap={rem(10)}
|
||||
direction={"column"}>
|
||||
<Flex
|
||||
justify={"space-between"}
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
px={rem(10)}
|
||||
>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={event => setName(event.currentTarget.value)}
|
||||
variant={"unstyled"}
|
||||
/>
|
||||
<IconGripHorizontal />
|
||||
</Flex>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
{deals.map(deal => (
|
||||
<DealSummaryCard
|
||||
color={theme.colorScheme === "dark" ? "var(--mantine-color-dark-6)" : "var(--mantine-color-gray-2)"}
|
||||
key={deal.id}
|
||||
dealSummary={deal}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex
|
||||
p={rem(10)}
|
||||
direction={"column"}
|
||||
bg={theme.colorScheme === "dark" ? "var(--mantine-color-dark-6)" : "var(--mantine-color-gray-2)"}
|
||||
style={{ borderRadius: "0.5rem" }}
|
||||
>
|
||||
<Text
|
||||
c={"gray.6"}
|
||||
size={"xs"}>Сумма: {totalPrice.toLocaleString("ru-RU")} руб.</Text>
|
||||
<Text
|
||||
c={"gray.6"}
|
||||
size={"xs"}>Всего товаров: {totalProducts.toLocaleString("ru-RU")} шт.</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -13,20 +13,27 @@ import {
|
||||
Popover,
|
||||
rem,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDealPageContext } from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { DealStatus } from "../../../shared/enums/DealStatus.ts";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { IconCheck, IconLayoutGridRemove, IconTrash } from "@tabler/icons-react";
|
||||
import { useContextMenu } from "mantine-contextmenu";
|
||||
import useDealSummaryState from "./useDealSummaryState.tsx";
|
||||
|
||||
type Props = {
|
||||
dealSummary: DealSummary;
|
||||
color?: string
|
||||
};
|
||||
|
||||
const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
|
||||
const DealSummaryCard: FC<Props> = ({ dealSummary, color }) => {
|
||||
const { showContextMenu } = useContextMenu();
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
const { onDelete, onComplete, onDeleteFromGroup } = useDealSummaryState();
|
||||
|
||||
const onDealSummaryClick = () => {
|
||||
DealService.getDealById({ dealId: dealSummary.id }).then(deal => {
|
||||
setSelectedDeal(deal);
|
||||
@@ -63,12 +70,36 @@ const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
|
||||
<Indicator
|
||||
position={"top-end"}
|
||||
withBorder
|
||||
size={15}
|
||||
processing
|
||||
{...getIndicatorProps()}
|
||||
>
|
||||
|
||||
<div
|
||||
onContextMenu={showContextMenu([
|
||||
...dealSummary.group ? [{
|
||||
key: "removeFromGroup",
|
||||
onClick: () => onDeleteFromGroup(dealSummary),
|
||||
title: "Убрать из группы",
|
||||
icon: <IconLayoutGridRemove />,
|
||||
}] : [],
|
||||
{
|
||||
key: "complete",
|
||||
onClick: () => onComplete(dealSummary),
|
||||
title: "Завершить",
|
||||
icon: <IconCheck />,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
onClick: () => onDelete(dealSummary),
|
||||
title: "Удалить",
|
||||
icon: <IconTrash />,
|
||||
},
|
||||
])}
|
||||
onClick={() => onDealSummaryClick()}
|
||||
className={styles["container"]}>
|
||||
className={styles["container"]}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
<Flex direction={"column"} flex={1} gap={rem(3)}>
|
||||
<Flex justify={"space-between"}>
|
||||
|
||||
@@ -81,7 +112,9 @@ const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Text c={"blue.5"} size={"sm"}>
|
||||
<Text
|
||||
c={"blue.5"}
|
||||
size={"sm"}>
|
||||
{dealSummary.name}
|
||||
</Text>
|
||||
<Flex
|
||||
@@ -109,14 +142,14 @@ const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
|
||||
<Flex direction={"column"}>
|
||||
{dealSummary.deliveryDate && (
|
||||
<Text
|
||||
c={"gray.6"}
|
||||
c={"blue.5"}
|
||||
size={"sm"}>
|
||||
Доставка: {(new Date(dealSummary.deliveryDate)).toLocaleDateString("ru-RU")}
|
||||
</Text>
|
||||
)}
|
||||
{dealSummary.receivingSlotDate && (
|
||||
<Text
|
||||
c={"gray.6"}
|
||||
c={"blue.5"}
|
||||
size={"sm"}>
|
||||
Слот: {(new Date(dealSummary.receivingSlotDate)).toLocaleDateString("ru-RU")}
|
||||
</Text>
|
||||
@@ -163,8 +196,11 @@ const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
|
||||
</CopyButton>
|
||||
{dealSummary.billRequest?.paid && (
|
||||
<Tooltip label={"Оплачен"}>
|
||||
<ThemeIcon variant={"transparent"}>
|
||||
<IconCheck />
|
||||
</ThemeIcon>
|
||||
|
||||
|
||||
<IconCheck />
|
||||
</Tooltip>
|
||||
|
||||
)}
|
||||
|
||||
101
src/components/Dnd/DealSummaryCard/useDealSummaryState.tsx
Normal file
101
src/components/Dnd/DealSummaryCard/useDealSummaryState.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { DealService, DealSummary } from "../../../client";
|
||||
import { useDealPageContext } from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { Text } from "@mantine/core";
|
||||
import { notifications } from "../../../shared/lib/notifications.ts";
|
||||
|
||||
const useDealSummaryState = () => {
|
||||
const { refetchDeals } = useDealPageContext();
|
||||
|
||||
const recalculate = async (dealId: number) => {
|
||||
return DealService.recalculateDealPrice({
|
||||
requestBody: {
|
||||
dealId: dealId,
|
||||
},
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = (summary: DealSummary) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление сделки",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены что хотите удалить сделку "{summary.name}"?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
DealService.deleteDeal({
|
||||
requestBody: {
|
||||
dealId: summary.id,
|
||||
},
|
||||
}).then(async (response) => {
|
||||
notifications.guess(response.ok, { message: response.message });
|
||||
if (response.ok) await refetchDeals();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onComplete = (summary: DealSummary) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление сделки",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены что хотите завершить сделку "{summary.name}"?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "green" },
|
||||
onConfirm: () => {
|
||||
DealService.completeDeal({
|
||||
requestBody: {
|
||||
dealId: summary.id,
|
||||
},
|
||||
}).then(async (response) => {
|
||||
notifications.guess(response.ok, { message: response.message });
|
||||
if (response.ok) await refetchDeals();
|
||||
|
||||
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteFromGroup = (summary: DealSummary) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление сделки",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены что хотите удалить сделку "{summary.name}" из группы?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
DealService.removeDealFromGroup({
|
||||
requestBody: {
|
||||
dealId: summary.id,
|
||||
},
|
||||
}).then(async (response) => {
|
||||
notifications.guess(response.ok, { message: response.message });
|
||||
if (response.ok) await refetchDeals();
|
||||
await recalculate(summary.id);
|
||||
if (response.ok) await refetchDeals();
|
||||
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onDelete,
|
||||
onComplete,
|
||||
onDeleteFromGroup,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDealSummaryState;
|
||||
Reference in New Issue
Block a user