diff --git a/package.json b/package.json index e9ef079..beef21f 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "globals": "^15.8.0", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", + "mantine-contextmenu": "^7.12.2", "mantine-form-zod-resolver": "^1.1.0", "mantine-react-table": "^2.0.0-beta.5", "phone": "^3.1.49", diff --git a/src/client/index.ts b/src/client/index.ts index 9c6db4a..16fcf4f 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -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'; diff --git a/src/client/models/DealAddToGroupRequest.ts b/src/client/models/DealAddToGroupRequest.ts new file mode 100644 index 0000000..e817931 --- /dev/null +++ b/src/client/models/DealAddToGroupRequest.ts @@ -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; +}; + diff --git a/src/client/models/DealAddToGroupResponse.ts b/src/client/models/DealAddToGroupResponse.ts new file mode 100644 index 0000000..e38365f --- /dev/null +++ b/src/client/models/DealAddToGroupResponse.ts @@ -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; +}; + diff --git a/src/client/models/DealCreateGroupRequest.ts b/src/client/models/DealCreateGroupRequest.ts new file mode 100644 index 0000000..5a2ec1d --- /dev/null +++ b/src/client/models/DealCreateGroupRequest.ts @@ -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; +}; + diff --git a/src/client/models/DealCreateGroupResponse.ts b/src/client/models/DealCreateGroupResponse.ts new file mode 100644 index 0000000..17393df --- /dev/null +++ b/src/client/models/DealCreateGroupResponse.ts @@ -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; +}; + diff --git a/src/client/models/DealGroupChangeStatusRequest.ts b/src/client/models/DealGroupChangeStatusRequest.ts new file mode 100644 index 0000000..64a78b0 --- /dev/null +++ b/src/client/models/DealGroupChangeStatusRequest.ts @@ -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; +}; + diff --git a/src/client/models/DealGroupChangeStatusResponse.ts b/src/client/models/DealGroupChangeStatusResponse.ts new file mode 100644 index 0000000..89be5a8 --- /dev/null +++ b/src/client/models/DealGroupChangeStatusResponse.ts @@ -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; +}; + diff --git a/src/client/models/DealGroupSchema.ts b/src/client/models/DealGroupSchema.ts new file mode 100644 index 0000000..49b9c43 --- /dev/null +++ b/src/client/models/DealGroupSchema.ts @@ -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; +}; + diff --git a/src/client/models/DealGroupUpdateRequest.ts b/src/client/models/DealGroupUpdateRequest.ts new file mode 100644 index 0000000..d017140 --- /dev/null +++ b/src/client/models/DealGroupUpdateRequest.ts @@ -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; +}; + diff --git a/src/client/models/DealGroupUpdateResponse.ts b/src/client/models/DealGroupUpdateResponse.ts new file mode 100644 index 0000000..5d2bced --- /dev/null +++ b/src/client/models/DealGroupUpdateResponse.ts @@ -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; +}; + diff --git a/src/client/models/DealRemoveFromGroupRequest.ts b/src/client/models/DealRemoveFromGroupRequest.ts new file mode 100644 index 0000000..139a2e8 --- /dev/null +++ b/src/client/models/DealRemoveFromGroupRequest.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type DealRemoveFromGroupRequest = { + dealId: number; +}; + diff --git a/src/client/models/DealRemoveFromGroupResponse.ts b/src/client/models/DealRemoveFromGroupResponse.ts new file mode 100644 index 0000000..8f772d7 --- /dev/null +++ b/src/client/models/DealRemoveFromGroupResponse.ts @@ -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; +}; + diff --git a/src/client/models/DealSummary.ts b/src/client/models/DealSummary.ts index e417810..a5e75a5 100644 --- a/src/client/models/DealSummary.ts +++ b/src/client/models/DealSummary.ts @@ -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); }; diff --git a/src/client/services/DealService.ts b/src/client/services/DealService.ts index 5502716..a7bd266 100644 --- a/src/client/services/DealService.ts +++ b/src/client/services/DealService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + return __request(OpenAPI, { + method: 'POST', + url: '/deal/group/change-status', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } } diff --git a/src/components/Dnd/Board/Board.tsx b/src/components/Dnd/Board/Board.tsx index 6a16390..5d84ff5 100644 --- a/src/components/Dnd/Board/Board.tsx +++ b/src/components/Dnd/Board/Board.tsx @@ -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 = ({ - 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(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 ( + {(provided, snapshot) => ( +
+
+ +
+ +
+ )} +
); + }; + const renderGroup = (obj: GroupWithDeals) => { + const { deals, group } = obj; + return ( + {(provided) => ( +
+ +
+ )} +
); + }; return (
@@ -45,38 +149,29 @@ export const Board: FC = ({ color={color} />
- + {(provided, snapshot) => (
{withCreateButton && ( - {}} /> + { + }} /> )} - {summaries.map(summary => ( - - {provided => ( -
- -
- )} -
- ))} - + {getDealsAndGroups().map(obj => { + if (isGroup(obj)) { + return renderGroup(obj); + } + return renderDeal(obj); + })} {provided.placeholder}
)} @@ -86,3 +181,4 @@ export const Board: FC = ({ }; export default Board; + diff --git a/src/components/Dnd/DealGroupView/DealGroupView.tsx b/src/components/Dnd/DealGroupView/DealGroupView.tsx new file mode 100644 index 0000000..9705341 --- /dev/null +++ b/src/components/Dnd/DealGroupView/DealGroupView.tsx @@ -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 = ({ deals, group }) => { + const theme = useMantineColorScheme(); + const [name, setName] = useState(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 ( + + + setName(event.currentTarget.value)} + variant={"unstyled"} + /> + + + + {deals.map(deal => ( + + ))} + + + Сумма: {totalPrice.toLocaleString("ru-RU")} руб. + Всего товаров: {totalProducts.toLocaleString("ru-RU")} шт. + + + ); +}; \ No newline at end of file diff --git a/src/components/Dnd/DealSummaryCard/DealSummaryCard.tsx b/src/components/Dnd/DealSummaryCard/DealSummaryCard.tsx index a31fe3a..5e9b45f 100644 --- a/src/components/Dnd/DealSummaryCard/DealSummaryCard.tsx +++ b/src/components/Dnd/DealSummaryCard/DealSummaryCard.tsx @@ -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 = ({ dealSummary }) => { +const DealSummaryCard: FC = ({ 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 = ({ dealSummary }) => {
onDeleteFromGroup(dealSummary), + title: "Убрать из группы", + icon: , + }] : [], + { + key: "complete", + onClick: () => onComplete(dealSummary), + title: "Завершить", + icon: , + }, + { + key: "delete", + onClick: () => onDelete(dealSummary), + title: "Удалить", + icon: , + }, + ])} onClick={() => onDealSummaryClick()} - className={styles["container"]}> + className={styles["container"]} + style={{ backgroundColor: color }} + > @@ -81,7 +112,9 @@ const DealSummaryCard: FC = ({ dealSummary }) => { - + {dealSummary.name} = ({ dealSummary }) => { {dealSummary.deliveryDate && ( Доставка: {(new Date(dealSummary.deliveryDate)).toLocaleDateString("ru-RU")} )} {dealSummary.receivingSlotDate && ( Слот: {(new Date(dealSummary.receivingSlotDate)).toLocaleDateString("ru-RU")} @@ -163,8 +196,11 @@ const DealSummaryCard: FC = ({ dealSummary }) => { {dealSummary.billRequest?.paid && ( + + + + - )} diff --git a/src/components/Dnd/DealSummaryCard/useDealSummaryState.tsx b/src/components/Dnd/DealSummaryCard/useDealSummaryState.tsx new file mode 100644 index 0000000..730025c --- /dev/null +++ b/src/components/Dnd/DealSummaryCard/useDealSummaryState.tsx @@ -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: ( + + Вы уверены что хотите удалить сделку "{summary.name}"? + + ), + 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: ( + + Вы уверены что хотите завершить сделку "{summary.name}"? + + ), + 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: ( + + Вы уверены что хотите удалить сделку "{summary.name}" из группы? + + ), + 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; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index aa06bd2..0493dad 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( - - - - - - - - + + + + + + + + + + + - + , ); diff --git a/src/pages/LeadsPage/components/DealServicesTable/columns.tsx b/src/pages/LeadsPage/components/DealServicesTable/columns.tsx index 8533657..560bb22 100644 --- a/src/pages/LeadsPage/components/DealServicesTable/columns.tsx +++ b/src/pages/LeadsPage/components/DealServicesTable/columns.tsx @@ -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", diff --git a/src/pages/LeadsPage/contexts/DealPageContext.tsx b/src/pages/LeadsPage/contexts/DealPageContext.tsx index c9789a7..6ab133c 100644 --- a/src/pages/LeadsPage/contexts/DealPageContext.tsx +++ b/src/pages/LeadsPage/contexts/DealPageContext.tsx @@ -4,30 +4,40 @@ import { DealSchema } from "../../../client"; type DealPageContextState = { selectedDeal?: DealSchema; setSelectedDeal: (deal: DealSchema | undefined) => void; + refetchDeals: () => Promise; }; const DealPageContext = createContext( - undefined + undefined, ); -const useDealPageContextState = () => { + +type DealPageContextStateProps = { + refetchDeals: () => Promise; +} + +const useDealPageContextState = (props: DealPageContextStateProps) => { + const { refetchDeals } = props; const [selectedDeal, setSelectedDeal] = useState( - undefined + undefined, ); return { selectedDeal, setSelectedDeal, + refetchDeals, }; }; type DealPageContextProviderProps = { children: React.ReactNode; + refetchDeals: () => Promise; }; export const DealPageContextProvider: FC = ({ - children, -}) => { - const state = useDealPageContextState(); + children, + refetchDeals, + }) => { + const state = useDealPageContextState({ refetchDeals }); return ( {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; diff --git a/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/DealServicesTable/DealServicesTable.tsx b/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/DealServicesTable/DealServicesTable.tsx index 7abab4f..d5d103e 100644 --- a/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/DealServicesTable/DealServicesTable.tsx +++ b/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/DealServicesTable/DealServicesTable.tsx @@ -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 = ({ 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 = ({ }; 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 = ({ }; 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 = ({ }; const onEmployeesChange = (items: UserSchema[]) => { if (!currentService || !onChange) return; - onChange({ + debouncedOnChange({ ...currentService, employees: items, }); diff --git a/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/ProductServicesTable/columns.tsx b/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/ProductServicesTable/columns.tsx index d146c0b..925b850 100644 --- a/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/ProductServicesTable/columns.tsx +++ b/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/ProductServicesTable/columns.tsx @@ -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[]>( () => [ @@ -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: "Цена", diff --git a/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/ProductView/ProductView.tsx b/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/ProductView/ProductView.tsx index ac38b8b..0aa5b1a 100644 --- a/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/ProductView/ProductView.tsx +++ b/src/pages/LeadsPage/tabs/ProductAndServiceTab/components/ProductView/ProductView.tsx @@ -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 = ({ 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 = ({ const onQuantityChange = (quantity: number) => { if (!onChange) return; - onChange({ + debouncedOnChange({ ...product, quantity, }); diff --git a/src/pages/LeadsPage/tabs/ProductAndServiceTab/hooks/useProductAndServiceTabState.tsx b/src/pages/LeadsPage/tabs/ProductAndServiceTab/hooks/useProductAndServiceTabState.tsx index bae0b8c..b0e6dd6 100644 --- a/src/pages/LeadsPage/tabs/ProductAndServiceTab/hooks/useProductAndServiceTabState.tsx +++ b/src/pages/LeadsPage/tabs/ProductAndServiceTab/hooks/useProductAndServiceTabState.tsx @@ -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, diff --git a/src/pages/LeadsPage/ui/LeadsPage.tsx b/src/pages/LeadsPage/ui/LeadsPage.tsx index f93c336..212b5e7 100644 --- a/src/pages/LeadsPage/ui/LeadsPage.tsx +++ b/src/pages/LeadsPage/ui/LeadsPage.tsx @@ -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", }}> - + { + await refetch(); + }}>