feat: worktime table with dates range

This commit is contained in:
2025-05-09 16:19:29 +04:00
parent d03ba66ebb
commit 37136b83bc
6 changed files with 67 additions and 126 deletions

View File

@@ -24,7 +24,7 @@ export type CardSummary = {
attributes: Array<CardAttributeSchema>; attributes: Array<CardAttributeSchema>;
shipmentWarehouseId: (number | null); shipmentWarehouseId: (number | null);
shipmentWarehouseName: (string | null); shipmentWarehouseName: (string | null);
billRequests?: Array<CardBillRequestSchema>; billRequests: Array<CardBillRequestSchema>;
group?: (CardGroupSchema | null); group?: (CardGroupSchema | null);
}; };

View File

@@ -3,7 +3,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetTimeTrackingRecordsRequest = { export type GetTimeTrackingRecordsRequest = {
date: string; dateFrom: string;
userIds: Array<number>; dateTo: string;
}; };

View File

@@ -7,7 +7,7 @@ import { processSelectedCells } from "../../../../../shared/lib/interpolateCells
import { TimeTrackingData } from "../../../../../client"; import { TimeTrackingData } from "../../../../../client";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { IMaskInput } from "react-imask"; import { IMaskInput } from "react-imask";
import { floatHoursToHoursAndMinutes } from "../../../../../types/utils.ts"; import { dateToString, floatHoursToHoursAndMinutes } from "../../../../../types/utils.ts";
export type EmployeeData = { export type EmployeeData = {
name: string; name: string;
@@ -20,21 +20,17 @@ export type EmployeeData = {
[key: string]: number | string; [key: string]: number | string;
}; };
type Props = { type Props = {
month: Date;
data: EmployeeData[]; data: EmployeeData[];
onUpdate: (date: Date, userId: number, value: string) => 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];
range: dayjs.Dayjs[]; range: dayjs.Dayjs[];
}; };
const useWorkTableColumns = ({ const useWorkTableColumns = ({
month,
onUpdate, onUpdate,
data, data,
selectedCells, selectedCells,
setSelectedCells, setSelectedCells,
selectedBoundaries,
range, range,
}: Props) => { }: Props) => {
const totalAmount = useMemo(() => { const totalAmount = useMemo(() => {
@@ -48,7 +44,7 @@ const useWorkTableColumns = ({
} }
return acc; return acc;
}, 0); }, 0);
}, [data, month, selectedCells, selectedBoundaries]); }, [data, selectedCells, range]);
const getBorderStyles = (cellId: string) => { const getBorderStyles = (cellId: string) => {
if (selectedCells.length <= 1) return {}; if (selectedCells.length <= 1) return {};
if (selectedCells[0] === cellId) if (selectedCells[0] === cellId)
@@ -86,8 +82,8 @@ const useWorkTableColumns = ({
...range.map(date => ({ ...range.map(date => ({
size: 100, size: 100,
accessorKey: date.date().toString(), accessorKey: dateToString(date.toDate()) ?? "",
header: date.date().toString(), header: dateToString(date.toDate()) ?? "",
enableSorting: false, enableSorting: false,
enableColumnActions: false, enableColumnActions: false,
Header: ( Header: (
@@ -130,7 +126,7 @@ const useWorkTableColumns = ({
row.original.userId, row.original.userId,
value, value,
); );
} };
return ( return (
<Flex direction={"column"}> <Flex direction={"column"}>
@@ -178,7 +174,7 @@ const useWorkTableColumns = ({
), ),
}, },
], ],
[month, selectedCells, selectedBoundaries, totalAmount], [selectedCells, range, totalAmount],
); );
}; };

View File

@@ -1,49 +1,45 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TimeTrackingRecord, TimeTrackingService } from "../../../../../client"; import { TimeTrackingRecord, TimeTrackingService } from "../../../../../client";
import { import { getDefaultEndDate } from "../../WorkShiftsPlanning/utils/utils.tsx";
dateWithoutTimezone, import { dateToString } from "../../../../../types/utils.ts";
getDatesInMonth,
} from "../../../../../shared/lib/date.ts";
import { last } from "lodash";
const getDateBoundaries = (month: Date) => {
return [
getDatesInMonth(month)[0].toDate(),
last(getDatesInMonth(month))?.toDate(),
];
};
const useWorkTableState = () => { const useWorkTableState = () => {
const [month, setMonth] = useState<Date>( const [dateRange, setDateRange] = useState<
new Date(new Date().getFullYear(), new Date().getMonth(), 1) [Date | null, Date | null]
); >([new Date(), getDefaultEndDate()]);
const [trackingRecords, setTrackingRecords] = useState< const [trackingRecords, setTrackingRecords] = useState<
TimeTrackingRecord[] TimeTrackingRecord[]
>([]); >([]);
const [dateBoundaries, setDateBoundaries] = useState(
getDateBoundaries(month)
);
const refetch = async () => { const refetch = () => {
return TimeTrackingService.getTimeTrackingRecords({ const ending = "T00:00:00";
const dateFrom = dateToString(dateRange[0]);
const dateTo = dateToString(dateRange[1]);
if (!(dateFrom && dateTo)) return;
TimeTrackingService
.getTimeTrackingRecords({
requestBody: { requestBody: {
date: dateWithoutTimezone(month), dateFrom: dateFrom + ending,
userIds: [], dateTo: dateTo + ending,
}, },
}).then(response => setTrackingRecords(response.records)); })
.then(response => {
console.log(response.records);
setTrackingRecords(response.records);
})
.catch(err => console.log(err));
}; };
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars refetch();
refetch().then(_ => { }, [dateRange]);
setDateBoundaries(getDateBoundaries(month));
});
}, [month]);
return { return {
month, dateRange,
setMonth, setDateRange,
refetch, refetch,
trackingRecords, trackingRecords,
dateBoundaries,
}; };
}; };

View File

@@ -1,56 +1,44 @@
import { ActionIcon, Flex, MultiSelect, rem, Tooltip } from "@mantine/core"; import { ActionIcon, Flex, MultiSelect, rem, Tooltip } from "@mantine/core";
import { DatePickerInput, MonthPickerInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import useWorkTableState from "../hooks/useWorkTableState.tsx"; import useWorkTableState from "../hooks/useWorkTableState.tsx";
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx"; import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useWorkTableColumns, { EmployeeData } from "../hooks/useWorkTableColumns.tsx"; import useWorkTableColumns, { EmployeeData } from "../hooks/useWorkTableColumns.tsx";
import { TimeTrackingRecord, TimeTrackingService, UserSchema } from "../../../../../client"; import { TimeTrackingRecord, TimeTrackingService, UserSchema } from "../../../../../client";
import { dateWithoutTimezone, getDatesBetween, getDatesInMonth } from "../../../../../shared/lib/date.ts"; import { getDatesBetween } from "../../../../../shared/lib/date.ts";
import useUsersList from "../../../hooks/useUsersList.tsx"; import useUsersList from "../../../hooks/useUsersList.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts"; import { notifications } from "../../../../../shared/lib/notifications.ts";
import { PaySchemeType } from "../../../../../shared/enums/PaySchemeType.ts"; 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"; import { dateToString, strTimeToFloatHours } from "../../../../../types/utils.ts";
import { useListState } from "@mantine/hooks"; import { useListState } from "@mantine/hooks";
const WorkTimeTable = () => { const WorkTimeTable = () => {
const [data, setData] = useState<EmployeeData[]>([]); const [data, setData] = useState<EmployeeData[]>([]);
const { dateBoundaries, month, setMonth, trackingRecords, refetch } = const { dateRange, setDateRange, trackingRecords, refetch } = useWorkTableState();
useWorkTableState();
const [shownUsers, shownUsersHandlers] = useListState<UserSchema>([]); const [shownUsers, shownUsersHandlers] = useListState<UserSchema>([]);
const [selectedBoundaries, setSelectedBoundaries] = useState<
[Date | null, Date | null]
>([null, null]);
const users = useUsersList().objects.filter( const users = useUsersList().objects.filter(
user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY, user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY,
); );
const getRange = () => { const getRange = () => {
const startDate = selectedBoundaries[0]; if (!(dateRange.length === 2 && dateRange[0] && dateRange[1])) return [];
const endDate = selectedBoundaries[1]; const startDate = dateRange[0];
if (startDate && endDate) { const endDate = dateRange[1];
return getDatesBetween(startDate, endDate); return getDatesBetween(startDate, endDate);
} else {
return getDatesInMonth(month);
}
}; };
const range = getRange(); const range = getRange();
const transformTrackingRecordsToData = ( const transformTrackingRecordsToData = (
trackingRecords: TimeTrackingRecord[], trackingRecords: TimeTrackingRecord[],
): EmployeeData[] => { ): EmployeeData[] => {
if (!month) return []; if (!(dateRange.length === 2 && dateRange[0] && dateRange[1])) return [];
const rangeDays = range.map(r => r.date()); const dateFrom = dateRange[0];
const dateTo = dateRange[1];
const dates = getDatesBetween(dateFrom, dateTo);
trackingRecords = trackingRecords.map(tr => ({
...tr,
data: tr.data.filter(d =>
rangeDays.includes(new Date(d.date).getDate()),
),
}));
const existingUserIds = trackingRecords.map(tr => tr.user.id); const existingUserIds = trackingRecords.map(tr => tr.user.id);
const firstResult = trackingRecords.map(record => ({ const firstResult = trackingRecords.map(record => ({
name: `${record.user.firstName} ${record.user.secondName}`, name: `${record.user.firstName} ${record.user.secondName}`,
@@ -59,14 +47,14 @@ const WorkTimeTable = () => {
totalAmount: record.totalAmount, totalAmount: record.totalAmount,
data: record.data, data: record.data,
...Object.fromEntries( ...Object.fromEntries(
getDatesInMonth(month).reduce((acc, day) => { dates.reduce((acc, day) => {
return acc.set(day.date().toString(), 0); return acc.set(dateToString(day.toDate()) ?? "", 0);
}, new Map<string, number>()), }, new Map<string, number>()),
), ),
...Object.fromEntries( ...Object.fromEntries(
record.data.reduce((acc, recordData) => { record.data.reduce((acc, recordData) => {
return acc.set( return acc.set(
new Date(recordData.date).getDate().toString(), recordData.date,
recordData.hours, recordData.hours,
); );
}, new Map<string, number>()), }, new Map<string, number>()),
@@ -80,7 +68,7 @@ const WorkTimeTable = () => {
comment: user.comment, comment: user.comment,
totalAmount: 0, totalAmount: 0,
...Object.fromEntries( ...Object.fromEntries(
getDatesInMonth(month).reduce((acc, day) => { dates.reduce((acc, day) => {
return acc.set(day.date().toString(), 0); return acc.set(day.date().toString(), 0);
}, new Map<string, number>()), }, new Map<string, number>()),
), ),
@@ -93,13 +81,12 @@ const WorkTimeTable = () => {
// @ts-expect-error // @ts-expect-error
.concat(restUsersResult) .concat(restUsersResult)
.filter(r => shownUserIds.includes(r.userId)); .filter(r => shownUserIds.includes(r.userId));
const firstDate = selectedBoundaries[0];
const lastDate = selectedBoundaries[1]; if (dateFrom && dateTo) {
if (firstDate && lastDate) { const allDays = dates.map(d =>
const allDays = getDatesInMonth(month).map(d =>
d.date().toString(), d.date().toString(),
); );
const allowedDays = getDatesBetween(firstDate, lastDate).map(d => const allowedDays = getDatesBetween(dateFrom, dateTo).map(d =>
d.date().toString(), d.date().toString(),
); );
const omitDays = difference(allDays, allowedDays); const omitDays = difference(allDays, allowedDays);
@@ -117,10 +104,7 @@ const WorkTimeTable = () => {
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); const hours = strTimeToFloatHours(value);
console.log(hours);
if (hours === -1) return; if (hours === -1) return;
setData(prevState => setData(prevState =>
@@ -128,12 +112,13 @@ const WorkTimeTable = () => {
if (record.userId !== userId) return record; if (record.userId !== userId) return record;
record[date.getDate()] = value; record[date.getDate()] = value;
return record; return record;
}) }),
); );
const ending = "T00:00:00";
TimeTrackingService.updateTimeTrackingRecord({ TimeTrackingService.updateTimeTrackingRecord({
requestBody: { requestBody: {
date: dateWithoutTimezone(date), date: dateToString(date) + ending,
hours, hours,
userId: user.id, userId: user.id,
}, },
@@ -141,13 +126,11 @@ const WorkTimeTable = () => {
if (!ok) { if (!ok) {
notifications.guess(ok, { message }); notifications.guess(ok, { message });
} }
await refetch(); refetch();
}); });
}; };
const columns = useWorkTableColumns({ const columns = useWorkTableColumns({
month,
selectedBoundaries,
data, data,
onUpdate: optimisticUpdate, onUpdate: optimisticUpdate,
selectedCells: [], selectedCells: [],
@@ -158,11 +141,7 @@ const WorkTimeTable = () => {
useEffect(() => { useEffect(() => {
setData(transformTrackingRecordsToData(trackingRecords)); setData(transformTrackingRecordsToData(trackingRecords));
}, [trackingRecords, shownUsers, selectedBoundaries]); }, [trackingRecords, shownUsers, dateRange]);
useEffect(() => {
setSelectedBoundaries([null, null]);
}, [month]);
return ( return (
<Flex <Flex
@@ -193,25 +172,12 @@ const WorkTimeTable = () => {
/> />
<Flex gap={rem(10)}> <Flex gap={rem(10)}>
<DatePickerInput <DatePickerInput
styles={{ placeholder={"Выберите временной промежуток"}
input: {
textAlign: "center",
},
}}
miw={rem(80)}
valueFormat={"DD"}
type={"range"} type={"range"}
minDate={dateBoundaries[0]} value={dateRange}
maxDate={dateBoundaries[1]} onChange={(value) => {
value={selectedBoundaries} setDateRange(value);
onChange={setSelectedBoundaries} }}
placeholder={"Даты"}
/>
<MonthPickerInput
allowDeselect={false}
onChange={event => event && setMonth(event)}
value={month}
placeholder={"Выберите месяц"}
/> />
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -5,23 +5,6 @@ export const dateWithoutTimezone = (date: Date) => {
return new Date(date.valueOf() - tzoffset).toISOString().slice(0, -1); return new Date(date.valueOf() - tzoffset).toISOString().slice(0, -1);
}; };
export const getDatesInMonth = (inputDate: Date) => {
const month = inputDate.getMonth();
const year = inputDate.getFullYear();
// Create a Day.js object for the first day of the specified month and year
let date = dayjs(new Date(year, month, 1));
const dates = [];
// Iterate through the days of the month
while (date.month() === month) {
dates.push(date);
date = date.add(1, "day");
}
return dates;
};
export const getDayOfWeek = (day: number): string => { export const getDayOfWeek = (day: number): string => {
switch (day) { switch (day) {
case 0: case 0: