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 { 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';

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 */
/* 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<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
* @returns GetDealBillById Successful Response

View File

@@ -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<MRT_ColumnDef<EmployeeData>[]>(() => [
{
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: (
<Flex
align={"center"}
@@ -44,6 +84,19 @@ const useWorkTableColumns = ({month, onUpdate, data}: Props) => {
),
// 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 (
<Flex direction={"column"}>
@@ -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: (
<Flex>
Всего: {totalAmount.toLocaleString("ru-RU")}
@@ -82,7 +139,7 @@ const useWorkTableColumns = ({month, onUpdate, data}: Props) => {
},
], [month]);
], [month, selectedCells, selectedBoundaries, totalAmount]);
}
export default useWorkTableColumns;

View File

@@ -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<Date>(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
const [trackingRecords, setTrackingRecords] = useState<TimeTrackingRecord[]>([]);
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
}
}

View File

@@ -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<EmployeeData[]>([]);
const {
dateBoundaries,
month,
setMonth,
trackingRecords,
refetch
} = useWorkTableState();
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 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<string, number>)),
@@ -45,7 +69,23 @@ const WorkTimeTable = () => {
}, new Map<string, number>)),
}))
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 (
<Flex
direction={"column"}
@@ -83,9 +135,11 @@ const WorkTimeTable = () => {
gap={rem(10)}
>
<Flex
justify={"flex-end"}
align={"center"}
justify={"space-between"}
gap={rem(10)}
>
<MultiSelect
data={users.map(user => ({
label: `${user.firstName} ${user.secondName}`,
@@ -95,12 +149,31 @@ const WorkTimeTable = () => {
value={hiddenUsers.map(user => user.id.toString())}
placeholder={hiddenUsers.length > 0 ? "" : "Скрытые пользователи"}
/>
<MonthPickerInput
allowDeselect={false}
onChange={(event) => event && setMonth(event)}
value={month}
placeholder={"Выберите месяц"}
/>
<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
allowDeselect={false}
onChange={(event) => event && setMonth(event)}
value={month}
placeholder={"Выберите месяц"}
/>
</Flex>
</Flex>
<Flex>
<BaseTable
@@ -125,7 +198,7 @@ const WorkTimeTable = () => {
</Tooltip>
</Flex>
)
),
} as MRT_TableOptions<EmployeeData>}
/>
</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 (
<div
className={
@@ -209,11 +235,19 @@ const ProductAndServiceTab: FC = () => {
</div>
<Divider my={rem(15)}/>
<div className={styles['deal-container-buttons']}>
<Button
disabled={isLocked}
onClick={onCreateBillClick}
variant={"default"}
fullWidth>Выставить счет</Button>
{isLocked ? <Button
onClick={onCancelBillClick}
color={"red"}
>
Отозвать счет
</Button> :
<Button
disabled={isLocked}
onClick={onCreateBillClick}
variant={"default"}
fullWidth>Выставить счет</Button>
}
</div>
</Flex>

View File

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

View File

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

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