feat: time tracking in minutes

This commit is contained in:
2024-11-22 21:43:07 +04:00
parent 20616d9e81
commit de55f9fa1c
4 changed files with 116 additions and 49 deletions

View File

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

View File

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

View File

@@ -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 }) => {

View File

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