feat: time tracking in minutes
This commit is contained in:
		@@ -3,36 +3,67 @@ import { MRT_ColumnDef } from "mantine-react-table";
 | 
				
			|||||||
import { PaymentRecordGetSchema } from "../../../../client";
 | 
					import { PaymentRecordGetSchema } from "../../../../client";
 | 
				
			||||||
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
 | 
					import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
 | 
				
			||||||
import { getPluralForm } from "../../../../shared/lib/utils.ts";
 | 
					import { getPluralForm } from "../../../../shared/lib/utils.ts";
 | 
				
			||||||
import { formatDate } from "../../../../types/utils.ts";
 | 
					import { floatHoursToHoursAndMinutes, formatDate } from "../../../../types/utils.ts";
 | 
				
			||||||
import { isEqual } from "lodash";
 | 
					import { isEqual } from "lodash";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const usePaymentRecordsTableColumns = () => {
 | 
					export const usePaymentRecordsTableColumns = () => {
 | 
				
			||||||
 | 
					    const getPluralMonths = (months: number) => {
 | 
				
			||||||
 | 
					        return getPluralForm(
 | 
				
			||||||
 | 
					            months,
 | 
				
			||||||
 | 
					            "месяц",
 | 
				
			||||||
 | 
					            "месяца",
 | 
				
			||||||
 | 
					            "месяцев",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const getPluralDays = (days: number) => {
 | 
				
			||||||
 | 
					        return getPluralForm(
 | 
				
			||||||
 | 
					            days,
 | 
				
			||||||
 | 
					            "день",
 | 
				
			||||||
 | 
					            "дня",
 | 
				
			||||||
 | 
					            "дней",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const getPluralHours = (hours: number) => {
 | 
				
			||||||
 | 
					        return getPluralForm(
 | 
				
			||||||
 | 
					            hours,
 | 
				
			||||||
 | 
					            "час",
 | 
				
			||||||
 | 
					            "часа",
 | 
				
			||||||
 | 
					            "часов",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const getPluralMinutes = (minutes: number) => {
 | 
				
			||||||
 | 
					        return getPluralForm(
 | 
				
			||||||
 | 
					            minutes,
 | 
				
			||||||
 | 
					            "минута",
 | 
				
			||||||
 | 
					            "минуты",
 | 
				
			||||||
 | 
					            "минут",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const getWorkUnitsText = (paymentRecord: PaymentRecordGetSchema) => {
 | 
					    const getWorkUnitsText = (paymentRecord: PaymentRecordGetSchema) => {
 | 
				
			||||||
        const payrollScheme = paymentRecord.payrollScheme;
 | 
					        const payrollScheme = paymentRecord.payrollScheme;
 | 
				
			||||||
        if (payrollScheme.key === PaySchemeType.HOURLY) {
 | 
					        if (payrollScheme.key === PaySchemeType.DAILY) {
 | 
				
			||||||
            return getPluralForm(
 | 
					            return getPluralDays(paymentRecord.workUnits);
 | 
				
			||||||
                paymentRecord.workUnits,
 | 
					 | 
				
			||||||
                "час",
 | 
					 | 
				
			||||||
                "часа",
 | 
					 | 
				
			||||||
                "часов"
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        } else if (payrollScheme.key === PaySchemeType.DAILY) {
 | 
					 | 
				
			||||||
            return getPluralForm(
 | 
					 | 
				
			||||||
                paymentRecord.workUnits,
 | 
					 | 
				
			||||||
                "день",
 | 
					 | 
				
			||||||
                "дня",
 | 
					 | 
				
			||||||
                "дней"
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        } else if (payrollScheme.key === PaySchemeType.MONTHLY) {
 | 
					        } else if (payrollScheme.key === PaySchemeType.MONTHLY) {
 | 
				
			||||||
            return getPluralForm(
 | 
					            return getPluralMonths(paymentRecord.workUnits);
 | 
				
			||||||
                paymentRecord.workUnits,
 | 
					 | 
				
			||||||
                "месяц",
 | 
					 | 
				
			||||||
                "месяца",
 | 
					 | 
				
			||||||
                "месяцев"
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return "";
 | 
					        return "";
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getWorkUnits = (paymentRecord: PaymentRecordGetSchema) => {
 | 
				
			||||||
 | 
					        const payrollScheme = paymentRecord.payrollScheme;
 | 
				
			||||||
 | 
					        if (payrollScheme.key === PaySchemeType.HOURLY) {
 | 
				
			||||||
 | 
					            const [hours, minutes] = floatHoursToHoursAndMinutes(paymentRecord.workUnits);
 | 
				
			||||||
 | 
					            const hoursPlural = getPluralHours(hours);
 | 
				
			||||||
 | 
					            if (minutes === 0) {
 | 
				
			||||||
 | 
					                return `${hours} ${hoursPlural}`;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const minutesPlural = getPluralMinutes(minutes);
 | 
				
			||||||
 | 
					            return `${hours} ${hoursPlural} ${minutes} ${minutesPlural}`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return `${paymentRecord.workUnits} ${getWorkUnitsText(paymentRecord)}`;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
    const getDateRangesText = (paymentRecord: PaymentRecordGetSchema) => {
 | 
					    const getDateRangesText = (paymentRecord: PaymentRecordGetSchema) => {
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            paymentRecord.endDate &&
 | 
					            paymentRecord.endDate &&
 | 
				
			||||||
@@ -62,18 +93,17 @@ export const usePaymentRecordsTableColumns = () => {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                header: "Количество",
 | 
					                header: "Количество",
 | 
				
			||||||
                Cell: ({ row }) =>
 | 
					                Cell: ({ row }) => getWorkUnits(row.original),
 | 
				
			||||||
                    `${row.original.workUnits} ${getWorkUnitsText(row.original)}`,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                header: "Сумма начисления",
 | 
					                header: "Сумма начисления",
 | 
				
			||||||
                Cell: ({ row }) => row.original.amount.toLocaleString("ru-RU"),
 | 
					                Cell: ({ row }) => Math.round(row.original.amount).toLocaleString("ru-RU"),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                header: "Временной промежуток",
 | 
					                header: "Временной промежуток",
 | 
				
			||||||
                Cell: ({ row }) => getDateRangesText(row.original),
 | 
					                Cell: ({ row }) => getDateRangesText(row.original),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        []
 | 
					        [],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
import { useMemo } from "react";
 | 
					import { useMemo } from "react";
 | 
				
			||||||
import { MRT_ColumnDef } from "mantine-react-table";
 | 
					import { MRT_ColumnDef } from "mantine-react-table";
 | 
				
			||||||
import { getDayOfWeek } from "../../../../../shared/lib/date.ts";
 | 
					import { getDayOfWeek } from "../../../../../shared/lib/date.ts";
 | 
				
			||||||
import { Box, Flex, NumberInput, rem, Text } from "@mantine/core";
 | 
					import { Box, Flex, Input, rem, Text } from "@mantine/core";
 | 
				
			||||||
import { isNumber, last } from "lodash";
 | 
					import { isNumber, last } from "lodash";
 | 
				
			||||||
import { processSelectedCells } from "../../../../../shared/lib/interpolateCells.ts";
 | 
					import { processSelectedCells } from "../../../../../shared/lib/interpolateCells.ts";
 | 
				
			||||||
import { TimeTrackingData } from "../../../../../client";
 | 
					import { TimeTrackingData } from "../../../../../client";
 | 
				
			||||||
import dayjs from "dayjs";
 | 
					import dayjs from "dayjs";
 | 
				
			||||||
 | 
					import { IMaskInput } from "react-imask";
 | 
				
			||||||
 | 
					import { floatHoursToHoursAndMinutes } from "../../../../../types/utils.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type EmployeeData = {
 | 
					export type EmployeeData = {
 | 
				
			||||||
    name: string;
 | 
					    name: string;
 | 
				
			||||||
@@ -20,7 +22,7 @@ export type EmployeeData = {
 | 
				
			|||||||
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: string) => void;
 | 
				
			||||||
    selectedCells: string[];
 | 
					    selectedCells: string[];
 | 
				
			||||||
    setSelectedCells: (cells: string[]) => void;
 | 
					    setSelectedCells: (cells: string[]) => void;
 | 
				
			||||||
    selectedBoundaries: [Date | null, Date | null];
 | 
					    selectedBoundaries: [Date | null, Date | null];
 | 
				
			||||||
@@ -62,6 +64,13 @@ const useWorkTableColumns = ({
 | 
				
			|||||||
        return {};
 | 
					        return {};
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const formatValue = (value: number) => {
 | 
				
			||||||
 | 
					        const [hours, minutes] = floatHoursToHoursAndMinutes(value);
 | 
				
			||||||
 | 
					        const hoursStr = String(hours).padStart(2, "0");
 | 
				
			||||||
 | 
					        const minutesStr = String(minutes).padStart(2, "0");
 | 
				
			||||||
 | 
					        return `${hoursStr}:${minutesStr}`;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return useMemo<MRT_ColumnDef<EmployeeData>[]>(
 | 
					    return useMemo<MRT_ColumnDef<EmployeeData>[]>(
 | 
				
			||||||
        () => [
 | 
					        () => [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -76,7 +85,7 @@ const useWorkTableColumns = ({
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ...range.map(date => ({
 | 
					            ...range.map(date => ({
 | 
				
			||||||
                size: 80,
 | 
					                size: 100,
 | 
				
			||||||
                accessorKey: date.date().toString(),
 | 
					                accessorKey: date.date().toString(),
 | 
				
			||||||
                header: date.date().toString(),
 | 
					                header: date.date().toString(),
 | 
				
			||||||
                enableSorting: false,
 | 
					                enableSorting: false,
 | 
				
			||||||
@@ -112,35 +121,38 @@ const useWorkTableColumns = ({
 | 
				
			|||||||
                Cell: ({ cell, row }) => {
 | 
					                Cell: ({ cell, row }) => {
 | 
				
			||||||
                    return (
 | 
					                    return (
 | 
				
			||||||
                        <Flex direction={"column"}>
 | 
					                        <Flex direction={"column"}>
 | 
				
			||||||
                            <NumberInput
 | 
					                            <Input
 | 
				
			||||||
 | 
					                                component={IMaskInput}
 | 
				
			||||||
 | 
					                                mask="00:00"
 | 
				
			||||||
                                // key={row.original.name + date.date().toString()}
 | 
					                                // key={row.original.name + date.date().toString()}
 | 
				
			||||||
                                onChange={event =>
 | 
					                                onChange={event =>
 | 
				
			||||||
                                    isNumber(event) &&
 | 
					                                    /^\d\d:\d\d$/.test(event.currentTarget.value) &&
 | 
				
			||||||
                                    onUpdate(
 | 
					                                    onUpdate(
 | 
				
			||||||
                                        date.toDate(),
 | 
					                                        date.toDate(),
 | 
				
			||||||
                                        row.original.userId,
 | 
					                                        row.original.userId,
 | 
				
			||||||
                                        event
 | 
					                                        event.currentTarget.value,
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                                styles={{ input: { textAlign: "center" } }}
 | 
					                                styles={{ input: { textAlign: "center" } }}
 | 
				
			||||||
                                hideControls
 | 
					                                value={formatValue(cell.renderValue() as number)}
 | 
				
			||||||
                                value={cell.renderValue()}
 | 
					 | 
				
			||||||
                            />
 | 
					                            />
 | 
				
			||||||
                        </Flex>
 | 
					                        </Flex>
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            })),
 | 
					            })),
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                header: "Всего часов",
 | 
					                header: "Всего времени",
 | 
				
			||||||
                Cell: ({ row }) => {
 | 
					                Cell: ({ row }) => {
 | 
				
			||||||
                    return Object.entries(row.original).reduce(
 | 
					                    return formatValue(
 | 
				
			||||||
                        (acc, [key, value]) => {
 | 
					                        Object.entries(row.original).reduce(
 | 
				
			||||||
                            if (isNaN(parseInt(key)) || !isNumber(value))
 | 
					                            (acc, [key, value]) => {
 | 
				
			||||||
 | 
					                                if (isNaN(parseInt(key)) || !isNumber(value))
 | 
				
			||||||
 | 
					                                    return acc;
 | 
				
			||||||
 | 
					                                acc += value;
 | 
				
			||||||
                                return acc;
 | 
					                                return acc;
 | 
				
			||||||
                            acc += value;
 | 
					                            },
 | 
				
			||||||
                            return acc;
 | 
					                            0,
 | 
				
			||||||
                        },
 | 
					                        ),
 | 
				
			||||||
                        0
 | 
					 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@@ -149,17 +161,19 @@ const useWorkTableColumns = ({
 | 
				
			|||||||
                header: "Итоговая сумма заработка",
 | 
					                header: "Итоговая сумма заработка",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Cell: ({ row }) => {
 | 
					                Cell: ({ row }) => {
 | 
				
			||||||
                    return (row.original.data || []).reduce((acc, value) => {
 | 
					                    return Math.floor(
 | 
				
			||||||
                        acc += value.amount;
 | 
					                        (row.original.data || []).reduce((acc, value) => {
 | 
				
			||||||
                        return acc;
 | 
					                            acc += value.amount;
 | 
				
			||||||
                    }, 0);
 | 
					                            return acc;
 | 
				
			||||||
 | 
					                        }, 0),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                Footer: (
 | 
					                Footer: (
 | 
				
			||||||
                    <Flex>Всего: {totalAmount.toLocaleString("ru-RU")}</Flex>
 | 
					                    <Flex>Всего: {Math.floor(totalAmount).toLocaleString("ru-RU")}</Flex>
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        [month, selectedCells, selectedBoundaries, totalAmount]
 | 
					        [month, selectedCells, selectedBoundaries, totalAmount],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ 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";
 | 
					import { difference, omit } from "lodash";
 | 
				
			||||||
 | 
					import { strTimeToFloatHours } from "../../../../../types/utils.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const WorkTimeTable = () => {
 | 
					const WorkTimeTable = () => {
 | 
				
			||||||
    const [data, setData] = useState<EmployeeData[]>([]);
 | 
					    const [data, setData] = useState<EmployeeData[]>([]);
 | 
				
			||||||
@@ -121,9 +122,16 @@ const WorkTimeTable = () => {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const optimisticUpdate = (date: Date, userId: number, value: number) => {
 | 
					    const optimisticUpdate = (date: Date, userId: number, value: string) => {
 | 
				
			||||||
        const user = users.find(user => user.id === userId);
 | 
					        const user = users.find(user => user.id === userId);
 | 
				
			||||||
        if (!user) return;
 | 
					        if (!user) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const hours = strTimeToFloatHours(value);
 | 
				
			||||||
 | 
					        console.log(hours);
 | 
				
			||||||
 | 
					        if (hours === -1) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        setData(prevState =>
 | 
					        setData(prevState =>
 | 
				
			||||||
            prevState.map(record => {
 | 
					            prevState.map(record => {
 | 
				
			||||||
                if (record.userId !== userId) return record;
 | 
					                if (record.userId !== userId) return record;
 | 
				
			||||||
@@ -135,7 +143,7 @@ const WorkTimeTable = () => {
 | 
				
			|||||||
        TimeTrackingService.updateTimeTrackingRecord({
 | 
					        TimeTrackingService.updateTimeTrackingRecord({
 | 
				
			||||||
            requestBody: {
 | 
					            requestBody: {
 | 
				
			||||||
                date: dateWithoutTimezone(date),
 | 
					                date: dateWithoutTimezone(date),
 | 
				
			||||||
                hours: value,
 | 
					                hours,
 | 
				
			||||||
                userId: user.id,
 | 
					                userId: user.id,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        }).then(async ({ ok, message }) => {
 | 
					        }).then(async ({ ok, message }) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,3 +42,18 @@ export function ObjectStateToTableProps<T extends MRT_RowData>(
 | 
				
			|||||||
        onCreate: state.onCreate,
 | 
					        onCreate: state.onCreate,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const floatHoursToHoursAndMinutes = (hours: number): number[] => {
 | 
				
			||||||
 | 
					    const resHours = Math.floor(hours);
 | 
				
			||||||
 | 
					    const minutes = Math.round((hours - resHours) * 60);
 | 
				
			||||||
 | 
					    return [resHours, minutes];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const strTimeToFloatHours = (time: string): number => {
 | 
				
			||||||
 | 
					    const values = time.split(":");
 | 
				
			||||||
 | 
					    if (values.length !== 2) return -1;
 | 
				
			||||||
 | 
					    const hours = parseInt(values[0]);
 | 
				
			||||||
 | 
					    const minutes = parseInt(values[1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return hours + minutes / 60;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user