From 0a971819dce5264ac6639d4cf4f42314dfc158d8 Mon Sep 17 00:00:00 2001 From: fakz9 Date: Fri, 9 Aug 2024 05:23:55 +0300 Subject: [PATCH] feat: cancel deal bill, date range in timetable --- src/client/index.ts | 2 + src/client/models/CancelDealBillRequest.ts | 8 ++ src/client/models/CancelDealBillResponse.ts | 9 ++ src/client/services/BillingService.ts | 22 +++++ .../hooks/useWorkTableColumns.tsx | 81 ++++++++++++--- .../WorkTimeTable/hooks/useWorkTableState.tsx | 14 ++- .../tabs/WorkTimeTable/ui/WorkTimeTable.tsx | 99 ++++++++++++++++--- .../ProductAndServiceTab.tsx | 44 ++++++++- .../ServicesKitsTable/ServicesKitsTable.tsx | 5 +- src/shared/lib/date.ts | 13 +++ src/shared/lib/interpolateCells.ts | 47 +++++++++ 11 files changed, 309 insertions(+), 35 deletions(-) create mode 100644 src/client/models/CancelDealBillRequest.ts create mode 100644 src/client/models/CancelDealBillResponse.ts create mode 100644 src/shared/lib/interpolateCells.ts diff --git a/src/client/index.ts b/src/client/index.ts index 658b1e1..59a9a2b 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -27,6 +27,8 @@ export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema'; export type { BillPaymentStatus } from './models/BillPaymentStatus'; export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest'; export type { Body_upload_product_image } from './models/Body_upload_product_image'; +export type { CancelDealBillRequest } from './models/CancelDealBillRequest'; +export type { CancelDealBillResponse } from './models/CancelDealBillResponse'; export type { ClientCreateRequest } from './models/ClientCreateRequest'; export type { ClientCreateResponse } from './models/ClientCreateResponse'; export type { ClientDeleteRequest } from './models/ClientDeleteRequest'; diff --git a/src/client/models/CancelDealBillRequest.ts b/src/client/models/CancelDealBillRequest.ts new file mode 100644 index 0000000..9f9e51a --- /dev/null +++ b/src/client/models/CancelDealBillRequest.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CancelDealBillRequest = { + dealId: number; +}; + diff --git a/src/client/models/CancelDealBillResponse.ts b/src/client/models/CancelDealBillResponse.ts new file mode 100644 index 0000000..7399779 --- /dev/null +++ b/src/client/models/CancelDealBillResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CancelDealBillResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/services/BillingService.ts b/src/client/services/BillingService.ts index 91d1e5c..0d23090 100644 --- a/src/client/services/BillingService.ts +++ b/src/client/services/BillingService.ts @@ -3,6 +3,8 @@ /* tslint:disable */ /* eslint-disable */ import type { BillStatusUpdateRequest } from '../models/BillStatusUpdateRequest'; +import type { CancelDealBillRequest } from '../models/CancelDealBillRequest'; +import type { CancelDealBillResponse } from '../models/CancelDealBillResponse'; import type { CreateDealBillRequest } from '../models/CreateDealBillRequest'; import type { CreateDealBillResponse } from '../models/CreateDealBillResponse'; import type { GetDealBillById } from '../models/GetDealBillById'; @@ -50,6 +52,26 @@ export class BillingService { }, }); } + /** + * Create Deal Bill + * @returns CancelDealBillResponse Successful Response + * @throws ApiError + */ + public static cancelDealBill({ + requestBody, + }: { + requestBody: CancelDealBillRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/billing/cancel-deal-bill', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } /** * Get Deal Bill By Id * @returns GetDealBillById Successful Response diff --git a/src/pages/AdminPage/tabs/WorkTimeTable/hooks/useWorkTableColumns.tsx b/src/pages/AdminPage/tabs/WorkTimeTable/hooks/useWorkTableColumns.tsx index 2d3ed5d..2c03b32 100644 --- a/src/pages/AdminPage/tabs/WorkTimeTable/hooks/useWorkTableColumns.tsx +++ b/src/pages/AdminPage/tabs/WorkTimeTable/hooks/useWorkTableColumns.tsx @@ -1,35 +1,75 @@ import {useMemo} from "react"; import {MRT_ColumnDef} from "mantine-react-table"; -import {getDatesInMonth, getDayOfWeek} from "../../../../../shared/lib/date.ts"; -import {Box, Flex, NumberInput} from "@mantine/core"; -import {isNumber} from "lodash"; +import {getDayOfWeek} from "../../../../../shared/lib/date.ts"; +import {Box, Flex, NumberInput, rem} from "@mantine/core"; +import {isNumber, last} from "lodash"; +import {processSelectedCells} from "../../../../../shared/lib/interpolateCells.ts"; +import {TimeTrackingData} from "../../../../../client"; +import dayjs from "dayjs"; export type EmployeeData = { name: string; userId: number; totalAmount: number; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + data?: TimeTrackingData[], [key: string]: number | string; }; type Props = { month: Date; data: EmployeeData[]; - onUpdate: (date: Date, userId: number, value: number) => void + onUpdate: (date: Date, userId: number, value: number) => void, + selectedCells: string[]; + setSelectedCells: (cells: string[]) => void + selectedBoundaries: [Date | null, Date | null]; + range: dayjs.Dayjs[]; } -const useWorkTableColumns = ({month, onUpdate, data}: Props) => { - const totalAmount = useMemo(() => data.reduce((acc, value) => acc + value.totalAmount, 0), [data]); +const useWorkTableColumns = ({ + month, + onUpdate, + data, + selectedCells, + setSelectedCells, + selectedBoundaries, + range + }: Props) => { + const totalAmount = useMemo(() => { + return data.reduce((acc, value) => { + if (value.data) { + const sum = value.data.reduce((innerAcc, item) => innerAcc + item.amount, 0); + return acc + sum; + } + return acc; + }, 0); + }, [data, month, selectedCells, selectedBoundaries]); + const getBorderStyles = (cellId: string) => { + if (selectedCells.length <= 1) return {} + if (selectedCells[0] === cellId) + return { + borderTopLeftRadius: rem(20), + borderBottomLeftRadius: rem(20), + } + else if (last(selectedCells) === cellId) + return { + borderTopRightRadius: rem(20), + borderBottomRightRadius: rem(20), + } + return {} + } + return useMemo[]>(() => [ { accessorKey: "name", header: "ФИО" }, - ...getDatesInMonth(month).map(date => ({ + ...range.map(date => ({ size: 80, accessorKey: date.date().toString(), header: date.date().toString(), enableSorting: false, enableColumnActions: false, - Header: ( { ), // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error + mantineTableBodyCellProps: ({cell}) => ({ + style: selectedCells.includes(cell.id) ? { + backgroundColor: "var(--mantine-primary-color-filled)", + ...getBorderStyles(cell.id) + } : {}, + onClick: () => { + const result = processSelectedCells(selectedCells, cell.id); + setSelectedCells(result); + }, + + }), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Cell: ({cell, row}) => { return ( @@ -71,9 +124,13 @@ const useWorkTableColumns = ({month, onUpdate, data}: Props) => { { accessorKey: "totalAmount", header: "Итоговая сумма заработка", - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - Cell: ({cell}) => cell.renderValue().toLocaleString("ru-RU"), + + Cell: ({row}) => { + return (row.original.data || []).reduce((acc, value) => { + acc += value.amount; + return acc; + }, 0); + }, Footer: ( Всего: {totalAmount.toLocaleString("ru-RU")} @@ -82,7 +139,7 @@ const useWorkTableColumns = ({month, onUpdate, data}: Props) => { }, - ], [month]); + ], [month, selectedCells, selectedBoundaries, totalAmount]); } export default useWorkTableColumns; \ No newline at end of file diff --git a/src/pages/AdminPage/tabs/WorkTimeTable/hooks/useWorkTableState.tsx b/src/pages/AdminPage/tabs/WorkTimeTable/hooks/useWorkTableState.tsx index 97a8032..3c25733 100644 --- a/src/pages/AdminPage/tabs/WorkTimeTable/hooks/useWorkTableState.tsx +++ b/src/pages/AdminPage/tabs/WorkTimeTable/hooks/useWorkTableState.tsx @@ -1,10 +1,16 @@ import {useEffect, useState} from "react"; import {TimeTrackingRecord, TimeTrackingService} from "../../../../../client"; -import {dateWithoutTimezone} from "../../../../../shared/lib/date.ts"; +import {dateWithoutTimezone, getDatesInMonth} from "../../../../../shared/lib/date.ts"; +import {last} from "lodash"; +const getDateBoundaries = (month: Date) => { + return [getDatesInMonth(month)[0].toDate(), last(getDatesInMonth(month))?.toDate()] +} const useWorkTableState = () => { const [month, setMonth] = useState(new Date(new Date().getFullYear(), new Date().getMonth(), 1)); const [trackingRecords, setTrackingRecords] = useState([]); + const [dateBoundaries, setDateBoundaries] = useState(getDateBoundaries(month)); + const refetch = async () => { return TimeTrackingService.getTimeTrackingRecords({ requestBody: { @@ -13,16 +19,20 @@ const useWorkTableState = () => { } }).then((response) => setTrackingRecords(response.records)); } + useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars refetch().then(_ => { + setDateBoundaries(getDateBoundaries(month)); }); + }, [month]) return { month, setMonth, refetch, - trackingRecords + trackingRecords, + dateBoundaries } } diff --git a/src/pages/AdminPage/tabs/WorkTimeTable/ui/WorkTimeTable.tsx b/src/pages/AdminPage/tabs/WorkTimeTable/ui/WorkTimeTable.tsx index 135d274..a273343 100644 --- a/src/pages/AdminPage/tabs/WorkTimeTable/ui/WorkTimeTable.tsx +++ b/src/pages/AdminPage/tabs/WorkTimeTable/ui/WorkTimeTable.tsx @@ -1,34 +1,58 @@ import {ActionIcon, Flex, MultiSelect, rem, Tooltip} from "@mantine/core"; -import {MonthPickerInput} from "@mantine/dates"; +import {DatePickerInput, MonthPickerInput} from "@mantine/dates"; import useWorkTableState from "../hooks/useWorkTableState.tsx"; import {BaseTable} from "../../../../../components/BaseTable/BaseTable.tsx"; import {useEffect, useState} from "react"; import useWorkTableColumns, {EmployeeData} from "../hooks/useWorkTableColumns.tsx"; import {TimeTrackingRecord, TimeTrackingService, UserSchema} from "../../../../../client"; -import {dateWithoutTimezone, getDatesInMonth} from "../../../../../shared/lib/date.ts"; +import {dateWithoutTimezone, getDatesBetween, getDatesInMonth} from "../../../../../shared/lib/date.ts"; import useUsersList from "../../../hooks/useUsersList.tsx"; import {notifications} from "../../../../../shared/lib/notifications.ts"; import {PaySchemeType} from "../../../../../shared/enums/PaySchemeType.ts"; import {IconEyeOff} from "@tabler/icons-react"; import {MRT_TableOptions} from "mantine-react-table"; +import {difference, omit} from "lodash"; + const WorkTimeTable = () => { const [data, setData] = useState([]); const { + dateBoundaries, month, setMonth, trackingRecords, refetch } = useWorkTableState(); const [hiddenUsers, setHiddenUsers] = useState([]); + const [selectedBoundaries, setSelectedBoundaries] = useState<[Date | null, Date | null]>([null, null]); + const users = useUsersList().objects.filter(user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY); + + const getRange = () => { + const startDate = selectedBoundaries[0]; + const endDate = selectedBoundaries[1]; + if (startDate && endDate) { + return getDatesBetween(startDate, endDate); + } else { + return getDatesInMonth(month); + } + } + const range = getRange(); + const transformTrackingRecordsToData = (trackingRecords: TimeTrackingRecord[]): EmployeeData[] => { if (!month) return []; + const rangeDays = range.map(r => r.date()); + + trackingRecords = trackingRecords.map(tr => ({ + ...tr, + data: tr.data.filter(d => rangeDays.includes((new Date(d.date)).getDate())) + })); const existingUserIds = trackingRecords.map(tr => tr.user.id); const firstResult = trackingRecords.map((record) => ({ name: `${record.user.firstName} ${record.user.secondName}`, userId: record.user.id, totalAmount: record.totalAmount, + data: record.data, ...Object.fromEntries(getDatesInMonth(month).reduce((acc, day) => { return acc.set(day.date().toString(), 0); }, new Map)), @@ -45,7 +69,23 @@ const WorkTimeTable = () => { }, new Map)), })) const hiddenUserIds = hiddenUsers.map(user => user.id); - return (firstResult.concat(restUsersResult) as unknown as EmployeeData[]).filter(r => !hiddenUserIds.includes(r.userId)); + + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const result = (firstResult.concat(restUsersResult)).filter(r => !hiddenUserIds.includes(r.userId)); + const firstDate = selectedBoundaries[0]; + const lastDate = selectedBoundaries[1]; + if (firstDate && lastDate) { + const allDays = getDatesInMonth(month).map(d => d.date().toString()); + const allowedDays = getDatesBetween(firstDate, lastDate).map(d => d.date().toString()); + const omitDays = difference(allDays, allowedDays); + + return result.map(r => { + return omit(r, omitDays); + }) as unknown as EmployeeData[]; + } + return result as unknown as EmployeeData[]; } const optimisticUpdate = (date: Date, userId: number, value: number) => { @@ -56,6 +96,8 @@ const WorkTimeTable = () => { record[date.getDate()] = value; return record; })) + + TimeTrackingService.updateTimeTrackingRecord({ requestBody: { date: dateWithoutTimezone(date), @@ -71,11 +113,21 @@ const WorkTimeTable = () => { }) } const columns = useWorkTableColumns({ - month, data, onUpdate: optimisticUpdate + month, + selectedBoundaries, + data, + onUpdate: optimisticUpdate, + selectedCells: [], + setSelectedCells: () => { + }, + range }); useEffect(() => { setData(transformTrackingRecordsToData(trackingRecords)); - }, [trackingRecords, hiddenUsers]) + }, [trackingRecords, hiddenUsers, selectedBoundaries]); + useEffect(() => { + setSelectedBoundaries([null, null]); + }, [month]) return ( { gap={rem(10)} > + ({ label: `${user.firstName} ${user.secondName}`, @@ -95,12 +149,31 @@ const WorkTimeTable = () => { value={hiddenUsers.map(user => user.id.toString())} placeholder={hiddenUsers.length > 0 ? "" : "Скрытые пользователи"} /> - event && setMonth(event)} - value={month} - placeholder={"Выберите месяц"} - /> + + + + event && setMonth(event)} + value={month} + placeholder={"Выберите месяц"} + /> + + { - ) + ), } as MRT_TableOptions} /> diff --git a/src/pages/LeadsPage/tabs/ProductAndServiceTab/ProductAndServiceTab.tsx b/src/pages/LeadsPage/tabs/ProductAndServiceTab/ProductAndServiceTab.tsx index 2f53b65..a1c3a61 100644 --- a/src/pages/LeadsPage/tabs/ProductAndServiceTab/ProductAndServiceTab.tsx +++ b/src/pages/LeadsPage/tabs/ProductAndServiceTab/ProductAndServiceTab.tsx @@ -156,6 +156,32 @@ const ProductAndServiceTab: FC = () => { } }); } + const onCancelBillClick = () => { + if (!dealState.deal) return; + const dealId = dealState.deal.id; + modals.openConfirmModal({ + withCloseButton: false, + children: + + Вы уверены что хотите отозвать заявку на оплату? + , + onConfirm: () => { + BillingService.cancelDealBill({ + requestBody: { + dealId + } + }).then(async ({ok, message}) => { + notifications.guess(ok, {message}); + await dealState.refetch(); + }) + }, + labels: { + confirm: "Отозвать", + cancel: "Отмена" + }, + + }); + } return (
{
- + {isLocked ? : + + } +
diff --git a/src/pages/ServicesPage/components/ServicesKitsTable/ServicesKitsTable.tsx b/src/pages/ServicesPage/components/ServicesKitsTable/ServicesKitsTable.tsx index 1970429..1bb8dff 100644 --- a/src/pages/ServicesPage/components/ServicesKitsTable/ServicesKitsTable.tsx +++ b/src/pages/ServicesPage/components/ServicesKitsTable/ServicesKitsTable.tsx @@ -23,9 +23,8 @@ const ServicesKitsTable: FC = ({items, onDelete, onChange}) => { } }) } - const onDeleteClick = (kit: GetServiceKitSchema) => { + const onDeleteClick = () => { if (!onDelete) return; - console.log(kit) } return ( = ({items, onDelete, onChange}) => { { - if (onDelete) onDeleteClick(row.original); + if (onDelete) onDeleteClick(); }} variant={"default"}> diff --git a/src/shared/lib/date.ts b/src/shared/lib/date.ts index 070b4c9..7dc59f1 100644 --- a/src/shared/lib/date.ts +++ b/src/shared/lib/date.ts @@ -43,4 +43,17 @@ export const getDayOfWeek = (day: number): string => { } return ""; } + +export function getDatesBetween(startDate: Date, endDate: Date): dayjs.Dayjs[] { + const dates: dayjs.Dayjs[] = []; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + dates.push(dayjs(new Date(currentDate))); + currentDate.setDate(currentDate.getDate() + 1); + } + + return dates; +} + export const isWeekend = (day: number) => (day === 6) || (day === 0); \ No newline at end of file diff --git a/src/shared/lib/interpolateCells.ts b/src/shared/lib/interpolateCells.ts new file mode 100644 index 0000000..2bc7902 --- /dev/null +++ b/src/shared/lib/interpolateCells.ts @@ -0,0 +1,47 @@ +import {maxBy, minBy} from "lodash"; + +type Cell = { row: number, col: number }; + +function parseCell(cell: string): Cell { + const [row, col] = cell.split('_').map(Number); + return {row, col}; +} + +function stringifyCell(cell: Cell): string { + return `${cell.row}_${cell.col}`; +} + +function intropolate(cells: Cell[]) { + const interpolatedCells: Cell[] = []; + for (let i = 0; i < cells.length - 1; i++) { + const current = cells[i]; + const next = cells[i + 1]; + interpolatedCells.push(current); + if (current.row === next.row) { + for (let col = current.col + 1; col < next.col; col++) { + interpolatedCells.push({row: current.row, col}); + } + } + } + interpolatedCells.push(cells[cells.length - 1]); + return interpolatedCells; +} + +export function processSelectedCells(prevCells: string[], newSelection: string): string[] { + const newCell = parseCell(newSelection); + let cells = intropolate([...prevCells, newSelection].map(parseCell).sort((a, b) => a.col - b.col)); + const maxCell = maxBy(cells, cell => cell.col); + const minCell = minBy(cells, cell => cell.col); + const indexOfNewCell = cells.findIndex(cell => (cell.col === newCell.col && cell.row === newCell.row)); + + if (minCell && maxCell) { + if (newCell.col > minCell.col && newCell.col < maxCell.col) { + if (indexOfNewCell >= (cells.length / 2)) + cells = cells.slice(0, indexOfNewCell); + else + cells = cells.slice(indexOfNewCell); + } + } + + return cells.map(stringifyCell); +} \ No newline at end of file