feat: cancel deal bill, date range in timetable

This commit is contained in:
2024-08-09 05:23:55 +03:00
parent 75860395b2
commit 0a971819dc
11 changed files with 309 additions and 35 deletions

View File

@@ -27,6 +27,8 @@ export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema';
export type { BillPaymentStatus } from './models/BillPaymentStatus'; export type { BillPaymentStatus } from './models/BillPaymentStatus';
export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest'; export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest';
export type { Body_upload_product_image } from './models/Body_upload_product_image'; 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 { ClientCreateRequest } from './models/ClientCreateRequest';
export type { ClientCreateResponse } from './models/ClientCreateResponse'; export type { ClientCreateResponse } from './models/ClientCreateResponse';
export type { ClientDeleteRequest } from './models/ClientDeleteRequest'; export type { ClientDeleteRequest } from './models/ClientDeleteRequest';

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CancelDealBillRequest = {
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 CancelDealBillResponse = {
ok: boolean;
message: string;
};

View File

@@ -3,6 +3,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { BillStatusUpdateRequest } from '../models/BillStatusUpdateRequest'; 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 { CreateDealBillRequest } from '../models/CreateDealBillRequest';
import type { CreateDealBillResponse } from '../models/CreateDealBillResponse'; import type { CreateDealBillResponse } from '../models/CreateDealBillResponse';
import type { GetDealBillById } from '../models/GetDealBillById'; 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<CancelDealBillResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/billing/cancel-deal-bill',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Get Deal Bill By Id * Get Deal Bill By Id
* @returns GetDealBillById Successful Response * @returns GetDealBillById Successful Response

View File

@@ -1,35 +1,75 @@
import {useMemo} from "react"; import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table"; import {MRT_ColumnDef} from "mantine-react-table";
import {getDatesInMonth, getDayOfWeek} from "../../../../../shared/lib/date.ts"; import {getDayOfWeek} from "../../../../../shared/lib/date.ts";
import {Box, Flex, NumberInput} from "@mantine/core"; import {Box, Flex, NumberInput, rem} from "@mantine/core";
import {isNumber} from "lodash"; import {isNumber, last} from "lodash";
import {processSelectedCells} from "../../../../../shared/lib/interpolateCells.ts";
import {TimeTrackingData} from "../../../../../client";
import dayjs from "dayjs";
export type EmployeeData = { export type EmployeeData = {
name: string; name: string;
userId: number; userId: number;
totalAmount: number; totalAmount: number;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
data?: TimeTrackingData[],
[key: string]: number | string; [key: string]: number | string;
}; };
type Props = { type Props = {
month: Date; month: Date;
data: EmployeeData[]; 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 useWorkTableColumns = ({
const totalAmount = useMemo(() => data.reduce((acc, value) => acc + value.totalAmount, 0), [data]); 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<MRT_ColumnDef<EmployeeData>[]>(() => [ return useMemo<MRT_ColumnDef<EmployeeData>[]>(() => [
{ {
accessorKey: "name", accessorKey: "name",
header: "ФИО" header: "ФИО"
}, },
...getDatesInMonth(month).map(date => ({ ...range.map(date => ({
size: 80, size: 80,
accessorKey: date.date().toString(), accessorKey: date.date().toString(),
header: date.date().toString(), header: date.date().toString(),
enableSorting: false, enableSorting: false,
enableColumnActions: false, enableColumnActions: false,
Header: ( Header: (
<Flex <Flex
align={"center"} align={"center"}
@@ -44,6 +84,19 @@ const useWorkTableColumns = ({month, onUpdate, data}: Props) => {
), ),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @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}) => { Cell: ({cell, row}) => {
return ( return (
<Flex direction={"column"}> <Flex direction={"column"}>
@@ -71,9 +124,13 @@ const useWorkTableColumns = ({month, onUpdate, data}: Props) => {
{ {
accessorKey: "totalAmount", accessorKey: "totalAmount",
header: "Итоговая сумма заработка", header: "Итоговая сумма заработка",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error Cell: ({row}) => {
Cell: ({cell}) => cell.renderValue().toLocaleString("ru-RU"), return (row.original.data || []).reduce((acc, value) => {
acc += value.amount;
return acc;
}, 0);
},
Footer: ( Footer: (
<Flex> <Flex>
Всего: {totalAmount.toLocaleString("ru-RU")} Всего: {totalAmount.toLocaleString("ru-RU")}
@@ -82,7 +139,7 @@ const useWorkTableColumns = ({month, onUpdate, data}: Props) => {
}, },
], [month]); ], [month, selectedCells, selectedBoundaries, totalAmount]);
} }
export default useWorkTableColumns; export default useWorkTableColumns;

View File

@@ -1,10 +1,16 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {TimeTrackingRecord, TimeTrackingService} from "../../../../../client"; 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 useWorkTableState = () => {
const [month, setMonth] = useState<Date>(new Date(new Date().getFullYear(), new Date().getMonth(), 1)); const [month, setMonth] = useState<Date>(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
const [trackingRecords, setTrackingRecords] = useState<TimeTrackingRecord[]>([]); const [trackingRecords, setTrackingRecords] = useState<TimeTrackingRecord[]>([]);
const [dateBoundaries, setDateBoundaries] = useState(getDateBoundaries(month));
const refetch = async () => { const refetch = async () => {
return TimeTrackingService.getTimeTrackingRecords({ return TimeTrackingService.getTimeTrackingRecords({
requestBody: { requestBody: {
@@ -13,16 +19,20 @@ const useWorkTableState = () => {
} }
}).then((response) => setTrackingRecords(response.records)); }).then((response) => setTrackingRecords(response.records));
} }
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
refetch().then(_ => { refetch().then(_ => {
setDateBoundaries(getDateBoundaries(month));
}); });
}, [month]) }, [month])
return { return {
month, month,
setMonth, setMonth,
refetch, refetch,
trackingRecords trackingRecords,
dateBoundaries
} }
} }

View File

@@ -1,34 +1,58 @@
import {ActionIcon, Flex, MultiSelect, rem, Tooltip} from "@mantine/core"; 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 useWorkTableState from "../hooks/useWorkTableState.tsx";
import {BaseTable} from "../../../../../components/BaseTable/BaseTable.tsx"; import {BaseTable} from "../../../../../components/BaseTable/BaseTable.tsx";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import useWorkTableColumns, {EmployeeData} from "../hooks/useWorkTableColumns.tsx"; import useWorkTableColumns, {EmployeeData} from "../hooks/useWorkTableColumns.tsx";
import {TimeTrackingRecord, TimeTrackingService, UserSchema} from "../../../../../client"; 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 useUsersList from "../../../hooks/useUsersList.tsx";
import {notifications} from "../../../../../shared/lib/notifications.ts"; import {notifications} from "../../../../../shared/lib/notifications.ts";
import {PaySchemeType} from "../../../../../shared/enums/PaySchemeType.ts"; import {PaySchemeType} from "../../../../../shared/enums/PaySchemeType.ts";
import {IconEyeOff} from "@tabler/icons-react"; import {IconEyeOff} from "@tabler/icons-react";
import {MRT_TableOptions} from "mantine-react-table"; import {MRT_TableOptions} from "mantine-react-table";
import {difference, omit} from "lodash";
const WorkTimeTable = () => { const WorkTimeTable = () => {
const [data, setData] = useState<EmployeeData[]>([]); const [data, setData] = useState<EmployeeData[]>([]);
const { const {
dateBoundaries,
month, month,
setMonth, setMonth,
trackingRecords, trackingRecords,
refetch refetch
} = useWorkTableState(); } = useWorkTableState();
const [hiddenUsers, setHiddenUsers] = useState<UserSchema[]>([]); const [hiddenUsers, setHiddenUsers] = useState<UserSchema[]>([]);
const [selectedBoundaries, setSelectedBoundaries] = useState<[Date | null, Date | null]>([null, null]);
const users = useUsersList().objects.filter(user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY); 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[] => { const transformTrackingRecordsToData = (trackingRecords: TimeTrackingRecord[]): EmployeeData[] => {
if (!month) return []; 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 existingUserIds = trackingRecords.map(tr => tr.user.id);
const firstResult = trackingRecords.map((record) => ({ const firstResult = trackingRecords.map((record) => ({
name: `${record.user.firstName} ${record.user.secondName}`, name: `${record.user.firstName} ${record.user.secondName}`,
userId: record.user.id, userId: record.user.id,
totalAmount: record.totalAmount, totalAmount: record.totalAmount,
data: record.data,
...Object.fromEntries(getDatesInMonth(month).reduce((acc, day) => { ...Object.fromEntries(getDatesInMonth(month).reduce((acc, day) => {
return acc.set(day.date().toString(), 0); return acc.set(day.date().toString(), 0);
}, new Map<string, number>)), }, new Map<string, number>)),
@@ -45,7 +69,23 @@ const WorkTimeTable = () => {
}, new Map<string, number>)), }, new Map<string, number>)),
})) }))
const hiddenUserIds = hiddenUsers.map(user => user.id); 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) => { const optimisticUpdate = (date: Date, userId: number, value: number) => {
@@ -56,6 +96,8 @@ const WorkTimeTable = () => {
record[date.getDate()] = value; record[date.getDate()] = value;
return record; return record;
})) }))
TimeTrackingService.updateTimeTrackingRecord({ TimeTrackingService.updateTimeTrackingRecord({
requestBody: { requestBody: {
date: dateWithoutTimezone(date), date: dateWithoutTimezone(date),
@@ -71,11 +113,21 @@ const WorkTimeTable = () => {
}) })
} }
const columns = useWorkTableColumns({ const columns = useWorkTableColumns({
month, data, onUpdate: optimisticUpdate month,
selectedBoundaries,
data,
onUpdate: optimisticUpdate,
selectedCells: [],
setSelectedCells: () => {
},
range
}); });
useEffect(() => { useEffect(() => {
setData(transformTrackingRecordsToData(trackingRecords)); setData(transformTrackingRecordsToData(trackingRecords));
}, [trackingRecords, hiddenUsers]) }, [trackingRecords, hiddenUsers, selectedBoundaries]);
useEffect(() => {
setSelectedBoundaries([null, null]);
}, [month])
return ( return (
<Flex <Flex
direction={"column"} direction={"column"}
@@ -83,9 +135,11 @@ const WorkTimeTable = () => {
gap={rem(10)} gap={rem(10)}
> >
<Flex <Flex
justify={"flex-end"} align={"center"}
justify={"space-between"}
gap={rem(10)} gap={rem(10)}
> >
<MultiSelect <MultiSelect
data={users.map(user => ({ data={users.map(user => ({
label: `${user.firstName} ${user.secondName}`, label: `${user.firstName} ${user.secondName}`,
@@ -95,6 +149,23 @@ const WorkTimeTable = () => {
value={hiddenUsers.map(user => user.id.toString())} value={hiddenUsers.map(user => user.id.toString())}
placeholder={hiddenUsers.length > 0 ? "" : "Скрытые пользователи"} placeholder={hiddenUsers.length > 0 ? "" : "Скрытые пользователи"}
/> />
<Flex gap={rem(10)}>
<DatePickerInput
styles={{
input: {
textAlign: "center"
}
}}
miw={rem(80)}
valueFormat={"DD"}
type={"range"}
minDate={dateBoundaries[0]}
maxDate={dateBoundaries[1]}
value={selectedBoundaries}
onChange={setSelectedBoundaries}
placeholder={"Даты"}
/>
<MonthPickerInput <MonthPickerInput
allowDeselect={false} allowDeselect={false}
onChange={(event) => event && setMonth(event)} onChange={(event) => event && setMonth(event)}
@@ -102,6 +173,8 @@ const WorkTimeTable = () => {
placeholder={"Выберите месяц"} placeholder={"Выберите месяц"}
/> />
</Flex> </Flex>
</Flex>
<Flex> <Flex>
<BaseTable <BaseTable
data={data} data={data}
@@ -125,7 +198,7 @@ const WorkTimeTable = () => {
</Tooltip> </Tooltip>
</Flex> </Flex>
) ),
} as MRT_TableOptions<EmployeeData>} } as MRT_TableOptions<EmployeeData>}
/> />
</Flex> </Flex>

View File

@@ -156,6 +156,32 @@ const ProductAndServiceTab: FC = () => {
} }
}); });
} }
const onCancelBillClick = () => {
if (!dealState.deal) return;
const dealId = dealState.deal.id;
modals.openConfirmModal({
withCloseButton: false,
children:
<Text style={{textAlign: "justify"}}>
Вы уверены что хотите отозвать заявку на оплату?
</Text>,
onConfirm: () => {
BillingService.cancelDealBill({
requestBody: {
dealId
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
await dealState.refetch();
})
},
labels: {
confirm: "Отозвать",
cancel: "Отмена"
},
});
}
return ( return (
<div <div
className={ className={
@@ -209,11 +235,19 @@ const ProductAndServiceTab: FC = () => {
</div> </div>
<Divider my={rem(15)}/> <Divider my={rem(15)}/>
<div className={styles['deal-container-buttons']}> <div className={styles['deal-container-buttons']}>
{isLocked ? <Button
onClick={onCancelBillClick}
color={"red"}
>
Отозвать счет
</Button> :
<Button <Button
disabled={isLocked} disabled={isLocked}
onClick={onCreateBillClick} onClick={onCreateBillClick}
variant={"default"} variant={"default"}
fullWidth>Выставить счет</Button> fullWidth>Выставить счет</Button>
}
</div> </div>
</Flex> </Flex>

View File

@@ -23,9 +23,8 @@ const ServicesKitsTable: FC<Props> = ({items, onDelete, onChange}) => {
} }
}) })
} }
const onDeleteClick = (kit: GetServiceKitSchema) => { const onDeleteClick = () => {
if (!onDelete) return; if (!onDelete) return;
console.log(kit)
} }
return ( return (
<BaseTable <BaseTable
@@ -46,7 +45,7 @@ const ServicesKitsTable: FC<Props> = ({items, onDelete, onChange}) => {
</Tooltip> </Tooltip>
<Tooltip label="Удалить"> <Tooltip label="Удалить">
<ActionIcon onClick={() => { <ActionIcon onClick={() => {
if (onDelete) onDeleteClick(row.original); if (onDelete) onDeleteClick();
}} variant={"default"}> }} variant={"default"}>
<IconTrash/> <IconTrash/>
</ActionIcon> </ActionIcon>

View File

@@ -43,4 +43,17 @@ export const getDayOfWeek = (day: number): string => {
} }
return ""; 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); export const isWeekend = (day: number) => (day === 6) || (day === 0);

View File

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