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 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 payrollScheme = paymentRecord.payrollScheme;
|
||||
if (payrollScheme.key === PaySchemeType.HOURLY) {
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"час",
|
||||
"часа",
|
||||
"часов"
|
||||
);
|
||||
} else if (payrollScheme.key === PaySchemeType.DAILY) {
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"день",
|
||||
"дня",
|
||||
"дней"
|
||||
);
|
||||
if (payrollScheme.key === PaySchemeType.DAILY) {
|
||||
return getPluralDays(paymentRecord.workUnits);
|
||||
} else if (payrollScheme.key === PaySchemeType.MONTHLY) {
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"месяц",
|
||||
"месяца",
|
||||
"месяцев"
|
||||
);
|
||||
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(
|
||||
(acc, [key, value]) => {
|
||||
if (isNaN(parseInt(key)) || !isNumber(value))
|
||||
return formatValue(
|
||||
Object.entries(row.original).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (isNaN(parseInt(key)) || !isNumber(value))
|
||||
return acc;
|
||||
acc += 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) => {
|
||||
acc += value.amount;
|
||||
return acc;
|
||||
}, 0);
|
||||
return Math.floor(
|
||||
(row.original.data || []).reduce((acc, value) => {
|
||||
acc += value.amount;
|
||||
return acc;
|
||||
}, 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