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