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