feat: deal group and stuff

This commit is contained in:
2024-11-08 17:05:17 +03:00
parent a4f4d467e9
commit f5ead70051
27 changed files with 728 additions and 112 deletions

View File

@@ -71,11 +71,15 @@ export type { DealAddServiceRequest } from './models/DealAddServiceRequest';
export type { DealAddServiceResponse } from './models/DealAddServiceResponse';
export type { DealAddServicesRequest } from './models/DealAddServicesRequest';
export type { DealAddServicesResponse } from './models/DealAddServicesResponse';
export type { DealAddToGroupRequest } from './models/DealAddToGroupRequest';
export type { DealAddToGroupResponse } from './models/DealAddToGroupResponse';
export type { DealBillRequestSchema } from './models/DealBillRequestSchema';
export type { DealChangeStatusRequest } from './models/DealChangeStatusRequest';
export type { DealChangeStatusResponse } from './models/DealChangeStatusResponse';
export type { DealCompleteRequest } from './models/DealCompleteRequest';
export type { DealCompleteResponse } from './models/DealCompleteResponse';
export type { DealCreateGroupRequest } from './models/DealCreateGroupRequest';
export type { DealCreateGroupResponse } from './models/DealCreateGroupResponse';
export type { DealCreateGuestUrlRequest } from './models/DealCreateGuestUrlRequest';
export type { DealCreateGuestUrlResponse } from './models/DealCreateGuestUrlResponse';
export type { DealCreateRequest } from './models/DealCreateRequest';
@@ -91,6 +95,11 @@ export type { DealDeleteServicesRequest } from './models/DealDeleteServicesReque
export type { DealDeleteServicesResponse } from './models/DealDeleteServicesResponse';
export type { DealGeneralInfoSchema } from './models/DealGeneralInfoSchema';
export type { DealGetAllResponse } from './models/DealGetAllResponse';
export type { DealGroupChangeStatusRequest } from './models/DealGroupChangeStatusRequest';
export type { DealGroupChangeStatusResponse } from './models/DealGroupChangeStatusResponse';
export type { DealGroupSchema } from './models/DealGroupSchema';
export type { DealGroupUpdateRequest } from './models/DealGroupUpdateRequest';
export type { DealGroupUpdateResponse } from './models/DealGroupUpdateResponse';
export type { DealPrefillRequest } from './models/DealPrefillRequest';
export type { DealPrefillResponse } from './models/DealPrefillResponse';
export type { DealProductAddKitRequest } from './models/DealProductAddKitRequest';
@@ -101,6 +110,8 @@ export type { DealQuickCreateRequest } from './models/DealQuickCreateRequest';
export type { DealQuickCreateResponse } from './models/DealQuickCreateResponse';
export type { DealRecalculatePriceRequest } from './models/DealRecalculatePriceRequest';
export type { DealRecalculatePriceResponse } from './models/DealRecalculatePriceResponse';
export type { DealRemoveFromGroupRequest } from './models/DealRemoveFromGroupRequest';
export type { DealRemoveFromGroupResponse } from './models/DealRemoveFromGroupResponse';
export type { DealSchema } from './models/DealSchema';
export type { DealServiceSchema } from './models/DealServiceSchema';
export type { DealServicesCopyRequest } from './models/DealServicesCopyRequest';

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealAddToGroupRequest = {
dealId: number;
groupId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealAddToGroupResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealCreateGroupRequest = {
draggingDealId: number;
hoveredDealId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealCreateGroupResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealGroupChangeStatusRequest = {
groupId: number;
newStatus: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealGroupChangeStatusResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealGroupSchema = {
id: number;
name?: (string | null);
lexorank: string;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { DealGroupSchema } from './DealGroupSchema';
export type DealGroupUpdateRequest = {
data: DealGroupSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealGroupUpdateResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealRemoveFromGroupRequest = {
dealId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealRemoveFromGroupResponse = {
ok: boolean;
message: string;
};

View File

@@ -4,13 +4,14 @@
/* eslint-disable */
import type { BaseMarketplaceSchema } from './BaseMarketplaceSchema';
import type { DealBillRequestSchema } from './DealBillRequestSchema';
import type { DealGroupSchema } from './DealGroupSchema';
export type DealSummary = {
id: number;
name: string;
clientName: string;
changedAt: string;
createdAt: string;
deadline: string;
deadline?: (string | null);
status: number;
totalPrice: number;
rank: number;
@@ -21,5 +22,6 @@ export type DealSummary = {
deliveryDate?: (string | null);
receivingSlotDate?: (string | null);
billRequest?: (DealBillRequestSchema | null);
group?: (DealGroupSchema | null);
};

View File

@@ -10,10 +10,14 @@ import type { DealAddServiceRequest } from '../models/DealAddServiceRequest';
import type { DealAddServiceResponse } from '../models/DealAddServiceResponse';
import type { DealAddServicesRequest } from '../models/DealAddServicesRequest';
import type { DealAddServicesResponse } from '../models/DealAddServicesResponse';
import type { DealAddToGroupRequest } from '../models/DealAddToGroupRequest';
import type { DealAddToGroupResponse } from '../models/DealAddToGroupResponse';
import type { DealChangeStatusRequest } from '../models/DealChangeStatusRequest';
import type { DealChangeStatusResponse } from '../models/DealChangeStatusResponse';
import type { DealCompleteRequest } from '../models/DealCompleteRequest';
import type { DealCompleteResponse } from '../models/DealCompleteResponse';
import type { DealCreateGroupRequest } from '../models/DealCreateGroupRequest';
import type { DealCreateGroupResponse } from '../models/DealCreateGroupResponse';
import type { DealCreateGuestUrlRequest } from '../models/DealCreateGuestUrlRequest';
import type { DealCreateGuestUrlResponse } from '../models/DealCreateGuestUrlResponse';
import type { DealCreateRequest } from '../models/DealCreateRequest';
@@ -28,6 +32,10 @@ import type { DealDeleteServiceResponse } from '../models/DealDeleteServiceRespo
import type { DealDeleteServicesRequest } from '../models/DealDeleteServicesRequest';
import type { DealDeleteServicesResponse } from '../models/DealDeleteServicesResponse';
import type { DealGetAllResponse } from '../models/DealGetAllResponse';
import type { DealGroupChangeStatusRequest } from '../models/DealGroupChangeStatusRequest';
import type { DealGroupChangeStatusResponse } from '../models/DealGroupChangeStatusResponse';
import type { DealGroupUpdateRequest } from '../models/DealGroupUpdateRequest';
import type { DealGroupUpdateResponse } from '../models/DealGroupUpdateResponse';
import type { DealPrefillRequest } from '../models/DealPrefillRequest';
import type { DealPrefillResponse } from '../models/DealPrefillResponse';
import type { DealProductAddKitRequest } from '../models/DealProductAddKitRequest';
@@ -36,6 +44,8 @@ import type { DealQuickCreateRequest } from '../models/DealQuickCreateRequest';
import type { DealQuickCreateResponse } from '../models/DealQuickCreateResponse';
import type { DealRecalculatePriceRequest } from '../models/DealRecalculatePriceRequest';
import type { DealRecalculatePriceResponse } from '../models/DealRecalculatePriceResponse';
import type { DealRemoveFromGroupRequest } from '../models/DealRemoveFromGroupRequest';
import type { DealRemoveFromGroupResponse } from '../models/DealRemoveFromGroupResponse';
import type { DealSchema } from '../models/DealSchema';
import type { DealServicesCopyRequest } from '../models/DealServicesCopyRequest';
import type { DealServicesCopyResponse } from '../models/DealServicesCopyResponse';
@@ -652,4 +662,104 @@ export class DealService {
},
});
}
/**
* Add To Group
* @returns DealAddToGroupResponse Successful Response
* @throws ApiError
*/
public static addDealToGroup({
requestBody,
}: {
requestBody: DealAddToGroupRequest,
}): CancelablePromise<DealAddToGroupResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/add-to-group',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Create Group
* @returns DealCreateGroupResponse Successful Response
* @throws ApiError
*/
public static createDealGroup({
requestBody,
}: {
requestBody: DealCreateGroupRequest,
}): CancelablePromise<DealCreateGroupResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/create-group',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Remove From Group
* @returns DealRemoveFromGroupResponse Successful Response
* @throws ApiError
*/
public static removeDealFromGroup({
requestBody,
}: {
requestBody: DealRemoveFromGroupRequest,
}): CancelablePromise<DealRemoveFromGroupResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/remove-from-group',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Update Group
* @returns DealGroupUpdateResponse Successful Response
* @throws ApiError
*/
public static updateDealGroup({
requestBody,
}: {
requestBody: DealGroupUpdateRequest,
}): CancelablePromise<DealGroupUpdateResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/group/update',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Change Group Status
* @returns DealGroupChangeStatusResponse Successful Response
* @throws ApiError
*/
public static changeDealGroupStatus({
requestBody,
}: {
requestBody: DealGroupChangeStatusRequest,
}): CancelablePromise<DealGroupChangeStatusResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/group/change-status',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}

View File

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

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

View File

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

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

View File

@@ -1,5 +1,5 @@
import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
import { MantineProvider } from "@mantine/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
@@ -10,6 +10,8 @@ import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css";
import "@mantine/dates/styles.css";
import "mantine-react-table/styles.css";
import "mantine-contextmenu/styles.css";
import "dayjs/locale/ru";
@@ -20,6 +22,7 @@ import { OpenAPI } from "./client";
import { DatesProvider } from "@mantine/dates";
import { modals } from "./modals/modals.ts";
import TasksProvider from "./providers/TasksProvider/TasksProvider.tsx";
import { ContextMenuProvider } from "mantine-contextmenu";
// Configuring router
const router = createRouter({ routeTree });
@@ -42,22 +45,25 @@ const queryClient = new QueryClient();
OpenAPI.BASE = import.meta.env.VITE_API_URL;
OpenAPI.TOKEN = JSON.parse(localStorage.getItem("authState") || "{}")[
"accessToken"
];
];
ReactDOM.createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MantineProvider defaultColorScheme={"dark"}>
<ModalsProvider
labels={{ confirm: "Да", cancel: "Нет" }}
modals={modals}>
<DatesProvider settings={{ locale: "ru" }}>
<TasksProvider>
<RouterProvider router={router} />
<Notifications />
</TasksProvider>
</DatesProvider>
</ModalsProvider>
<ContextMenuProvider>
<ModalsProvider
labels={{ confirm: "Да", cancel: "Нет" }}
modals={modals}>
<DatesProvider settings={{ locale: "ru" }}>
<TasksProvider>
<RouterProvider router={router} />
<Notifications />
</TasksProvider>
</DatesProvider>
</ModalsProvider>
</ContextMenuProvider>
</MantineProvider>
</QueryClientProvider>
</Provider>
</Provider>,
);

View File

@@ -3,7 +3,6 @@ import { useMemo } from "react";
import { DealServiceSchema } from "../../../../client";
type Props = {
// onChange: (service: DealServiceSchema, quantity: number) => void;
data: DealServiceSchema[];
};
@@ -31,11 +30,6 @@ export const useDealServicesTableColumns = (props: Props) => {
accessorKey: "price",
header: "Цена",
},
{
enableGrouping: false,
accessorKey: "service.cost",
header: "Себестоимость",
},
{
enableGrouping: false,
accessorKey: "quantity",

View File

@@ -4,30 +4,40 @@ import { DealSchema } from "../../../client";
type DealPageContextState = {
selectedDeal?: DealSchema;
setSelectedDeal: (deal: DealSchema | undefined) => void;
refetchDeals: () => Promise<void>;
};
const DealPageContext = createContext<DealPageContextState | undefined>(
undefined
undefined,
);
const useDealPageContextState = () => {
type DealPageContextStateProps = {
refetchDeals: () => Promise<void>;
}
const useDealPageContextState = (props: DealPageContextStateProps) => {
const { refetchDeals } = props;
const [selectedDeal, setSelectedDeal] = useState<DealSchema | undefined>(
undefined
undefined,
);
return {
selectedDeal,
setSelectedDeal,
refetchDeals,
};
};
type DealPageContextProviderProps = {
children: React.ReactNode;
refetchDeals: () => Promise<void>;
};
export const DealPageContextProvider: FC<DealPageContextProviderProps> = ({
children,
}) => {
const state = useDealPageContextState();
children,
refetchDeals,
}) => {
const state = useDealPageContextState({ refetchDeals });
return (
<DealPageContext.Provider value={state}>
{children}
@@ -39,7 +49,7 @@ export const useDealPageContext = () => {
const context = useContext(DealPageContext);
if (!context) {
throw new Error(
"useDealPageContext must be used within a DealPageContextProvider"
"useDealPageContext must be used within a DealPageContextProvider",
);
}
return context;

View File

@@ -11,6 +11,7 @@ import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts";
import useDealProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
import LockCheckbox from "../../../../../../components/LockCheckbox/LockCheckbox.tsx";
import { useDebouncedCallback } from "@mantine/hooks";
type RestProps = {
onKitAdd?: (kit: GetServiceKitSchema) => void;
@@ -23,6 +24,10 @@ const DealServicesTable: FC<Props> = ({
onChange,
onKitAdd,
}) => {
const debouncedOnChange = useDebouncedCallback(async (item: DealServiceSchema) => {
if (!onChange) return;
onChange(item);
}, 200);
const authState = useSelector((state: RootState) => state.auth);
const { dealState } = useDealProductAndServiceTabState();
@@ -39,7 +44,6 @@ const DealServicesTable: FC<Props> = ({
};
const onCreateClick = () => {
if (!onCreate) return;
console.log("228");
const serviceIds = items.map(service => service.service.id);
modals.openContextModal({
modal: "addDealService",
@@ -53,21 +57,21 @@ const DealServicesTable: FC<Props> = ({
};
const onQuantityChange = (item: DealServiceSchema, quantity: number) => {
if (!onChange) return;
onChange({
debouncedOnChange({
...item,
quantity,
});
};
const onPriceChange = (item: DealServiceSchema, price: number) => {
if (!onChange) return;
onChange({
debouncedOnChange({
...item,
price,
});
};
const onLockChange = (item: DealServiceSchema, isLocked: boolean) => {
if (!onChange) return;
onChange({
debouncedOnChange({
...item,
isFixedPrice: isLocked,
});
@@ -91,7 +95,7 @@ const DealServicesTable: FC<Props> = ({
};
const onEmployeesChange = (items: UserSchema[]) => {
if (!currentService || !onChange) return;
onChange({
debouncedOnChange({
...currentService,
employees: items,
});

View File

@@ -15,14 +15,7 @@ const useProductServicesTableColumns = (props: Props) => {
() => data.reduce((acc, row) => acc + row.price * quantity, 0),
[data, quantity]
);
const totalCost = useMemo(
() =>
data.reduce(
(acc, row) => acc + (row.service.cost || 0) * quantity,
0
),
[data, quantity]
);
const hideGuestColumns = ["service.cost"];
return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(
() => [
@@ -30,17 +23,6 @@ const useProductServicesTableColumns = (props: Props) => {
accessorKey: "service.name",
header: "Услуга",
},
{
enableHiding: true,
accessorKey: "service.cost",
header: "Себестоимость",
Footer: () => (
<>
Итоговая себестоимость: {totalCost.toLocaleString("ru")}
</>
),
},
{
accessorKey: "price",
header: "Цена",

View File

@@ -6,23 +6,14 @@ import {
ProductSchema,
} from "../../../../../../client";
import styles from "./ProductView.module.css";
import {
ActionIcon,
Box,
Flex,
Image,
NumberInput,
rem,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { ActionIcon, Box, Flex, Image, NumberInput, rem, Text, Title, Tooltip } from "@mantine/core";
import ProductServicesTable from "../ProductServicesTable/ProductServicesTable.tsx";
import { isNil, isNumber } from "lodash";
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts";
import useDealProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
import { useDebouncedCallback } from "@mantine/hooks";
type Props = {
product: DealProductSchema;
@@ -52,6 +43,10 @@ const ProductView: FC<Props> = ({
onProductEdit,
}) => {
const { dealState } = useDealProductAndServiceTabState();
const debouncedOnChange = useDebouncedCallback(async (item: DealProductSchema) => {
if (!onChange) return;
onChange(item);
}, 200);
const isLocked = Boolean(dealState.deal?.billRequest);
const onDeleteClick = () => {
if (!onDelete) return;
@@ -87,7 +82,7 @@ const ProductView: FC<Props> = ({
const onQuantityChange = (quantity: number) => {
if (!onChange) return;
onChange({
debouncedOnChange({
...product,
quantity,
});

View File

@@ -1,9 +1,5 @@
import { CRUDTableProps } from "../../../../../types/CRUDTable.tsx";
import {
DealProductSchema,
DealService,
DealServiceSchema,
} from "../../../../../client";
import { DealProductSchema, DealService, DealServiceSchema } from "../../../../../client";
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts";
@@ -11,23 +7,29 @@ const useDealState = () => {
const { selectedDeal, setSelectedDeal } = useDealPageContext();
const recalculate = async () => {
return DealService.recalculateDealPrice({
requestBody: {
dealId: selectedDeal?.id || -1,
},
});
};
const refetch = async () => {
const refetchDeal = async () => {
if (!selectedDeal) return;
const { ok, message } = await recalculate();
if (!ok) notifications.guess(ok, { message });
return DealService.getDealById({ dealId: selectedDeal.id }).then(
deal => {
async deal => {
setSelectedDeal(deal);
},
);
};
const refetch = async () => {
if (!selectedDeal) return;
await refetchDeal();
const { ok, message } = await recalculate();
if (!ok) notifications.guess(ok, { message });
await refetchDeal();
};
return {
deal: selectedDeal,
refetch,

View File

@@ -44,6 +44,16 @@ export const LeadsPage: FC = () => {
setSummaries(summariesRaw);
}, [summariesRaw]);
const recalculate = async (dealId: number) => {
return DealService.recalculateDealPrice({
requestBody: {
dealId: dealId,
},
}).then(({ ok, message }) => {
notifications.guess(ok, { message });
});
};
const onDelete = (dealId: number) => {
const summary = summaries.find(summary => summary.id == dealId);
if (!summary) return;
@@ -94,12 +104,79 @@ export const LeadsPage: FC = () => {
},
});
};
const onCombine = async (result: DropResult) => {
if (!result.combine) return;
const destination = result.combine.draggableId;
const source = result.draggableId;
if (!destination || !source) return;
const sourceId = parseInt(source);
if (destination.includes("group")) {
const groupId = parseInt(destination.split("-")[1]);
DealService.addDealToGroup({
requestBody: {
dealId: sourceId,
groupId: groupId,
},
}).then(async response => {
if (!response.ok) {
notifications.error({ message: response.message });
return;
}
await refetch();
await recalculate(sourceId);
await refetch();
});
} else {
const destinationId = parseInt(destination);
// creating new group
DealService.createDealGroup({
requestBody: {
draggingDealId: sourceId,
hoveredDealId: destinationId,
},
}).then(async response => {
if (!response.ok) {
notifications.error({ message: response.message });
return;
}
await refetch();
await recalculate(sourceId);
await refetch();
});
return;
}
};
const moveGroup = async (result: DropResult) => {
const groupId = parseInt(result.draggableId.split("-")[1]);
const destination = result.destination?.droppableId;
if (!destination) return;
const status = getDealStatusByName(destination);
DealService.changeDealGroupStatus({
requestBody: {
groupId: groupId,
newStatus: status,
},
}).then(async response => {
if (!response.ok) {
notifications.error({ message: response.message });
return;
}
await refetch();
});
};
const onDragEnd = async (result: DropResult) => {
if (result.combine) {
return onCombine(result);
}
setIsDragEnded(true);
// If there is no changes
if (!result.destination || result.destination == result.source) return;
// Checking for valid dealId
if (result.draggableId.includes("group")) {
return moveGroup(result);
}
const dealId = parseInt(result.draggableId);
if (isNaN(dealId)) return;
@@ -317,7 +394,9 @@ export const LeadsPage: FC = () => {
backgroundColor: "transparent",
boxShadow: "none",
}}>
<DealPageContextProvider>
<DealPageContextProvider refetchDeals={async () => {
await refetch();
}}>
<PrefillDealContextProvider>
<PageBlock style={{ flex: 0 }}>
<Flex