time tracking
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import styles from './AdminPage.module.css';
|
||||
import {Tabs} from "@mantine/core";
|
||||
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
|
||||
import {IconBriefcase, IconCurrencyDollar, IconUser} from "@tabler/icons-react";
|
||||
import {IconBriefcase, IconCalendarUser, IconCurrencyDollar, IconUser} from "@tabler/icons-react";
|
||||
import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx";
|
||||
import UsersTab from "./tabs/Users/UsersTab.tsx";
|
||||
import {motion} from "framer-motion";
|
||||
import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
|
||||
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
|
||||
|
||||
const AdminPage = () => {
|
||||
|
||||
@@ -23,15 +24,15 @@ const AdminPage = () => {
|
||||
<Tabs.Tab value={"rolesAndPositions"} leftSection={<IconBriefcase/>}>
|
||||
Должности
|
||||
</Tabs.Tab>
|
||||
|
||||
<Tabs.Tab value={"workTimeTable"} leftSection={<IconCalendarUser/>}>
|
||||
Рабочее время
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value={"users"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
|
||||
>
|
||||
<UsersTab/>
|
||||
</motion.div>
|
||||
@@ -54,6 +55,15 @@ const AdminPage = () => {
|
||||
<FinancesTab/>
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"workTimeTable"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<WorkTimeTable/>
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
|
||||
</Tabs>
|
||||
</PageBlock>
|
||||
|
||||
@@ -5,10 +5,10 @@ import {useForm} from "@mantine/form";
|
||||
import {Flex, NumberInput, rem} from "@mantine/core";
|
||||
import {DatePickerInput, MonthPickerInput} from "@mantine/dates";
|
||||
import {useEffect, useState} from "react";
|
||||
import {dateWithoutTimezone} from "../../../../shared/lib/utils.ts";
|
||||
import UserSelect from "../../../../components/Selects/UserSelect/UserSelect.tsx";
|
||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import {motion} from "framer-motion";
|
||||
import {dateWithoutTimezone} from "../../../../shared/lib/date.ts";
|
||||
|
||||
type Props = CreateProps<PaymentRecordCreateSchema>;
|
||||
const CreatePaymentRecordModal = ({
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {getDatesInMonth, getDayOfWeek} from "../../../../../shared/lib/date.ts";
|
||||
import {Box, Flex, NumberInput} from "@mantine/core";
|
||||
import {isNumber} from "lodash";
|
||||
|
||||
export type EmployeeData = {
|
||||
name: string;
|
||||
userId: number;
|
||||
totalAmount: number;
|
||||
[key: string]: number | string;
|
||||
};
|
||||
type Props = {
|
||||
month: Date;
|
||||
data: EmployeeData[];
|
||||
onUpdate: (date: Date, userId: number, value: number) => void
|
||||
}
|
||||
const useWorkTableColumns = ({month, onUpdate, data}: Props) => {
|
||||
const totalAmount = useMemo(() => data.reduce((acc, value) => acc + value.totalAmount, 0), [data]);
|
||||
return useMemo<MRT_ColumnDef<EmployeeData>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "ФИО"
|
||||
},
|
||||
|
||||
...getDatesInMonth(month).map(date => ({
|
||||
size: 80,
|
||||
accessorKey: date.date().toString(),
|
||||
header: date.date().toString(),
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
|
||||
Header: (
|
||||
<Flex
|
||||
align={"center"}
|
||||
direction={"column"}>
|
||||
<Box>
|
||||
{date.date()}
|
||||
</Box>
|
||||
<Box>
|
||||
{getDayOfWeek(date.day())}
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
Cell: ({cell, row}) => {
|
||||
return (
|
||||
<Flex direction={"column"}>
|
||||
<NumberInput
|
||||
// key={row.original.name + date.date().toString()}
|
||||
onChange={event => isNumber(event) && onUpdate(date.toDate(), row.original.userId, event)}
|
||||
styles={{input: {textAlign: "center"}}}
|
||||
hideControls
|
||||
value={cell.renderValue()}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
})),
|
||||
{
|
||||
header: "Всего часов",
|
||||
Cell: ({row}) => {
|
||||
const v = Object.entries(row.original).reduce((acc, [key, value]) => {
|
||||
if (isNaN(parseInt(key)) || !isNumber(value)) return acc;
|
||||
console.log("dobro")
|
||||
acc += value;
|
||||
return acc;
|
||||
}, 0);
|
||||
console.log(`V: ${v}`)
|
||||
return v;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "totalAmount",
|
||||
header: "Итоговая сумма заработка",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
Cell: ({cell}) => cell.renderValue().toLocaleString("ru-RU"),
|
||||
Footer: (
|
||||
<Flex>
|
||||
Всего: {totalAmount.toLocaleString("ru-RU")}
|
||||
</Flex>
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
], [month]);
|
||||
}
|
||||
|
||||
export default useWorkTableColumns;
|
||||
@@ -0,0 +1,29 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {TimeTrackingRecord, TimeTrackingService} from "../../../../../client";
|
||||
import {dateWithoutTimezone} from "../../../../../shared/lib/date.ts";
|
||||
|
||||
const useWorkTableState = () => {
|
||||
const [month, setMonth] = useState<Date>(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||||
const [trackingRecords, setTrackingRecords] = useState<TimeTrackingRecord[]>([]);
|
||||
const refetch = async () => {
|
||||
return TimeTrackingService.getTimeTrackingRecords({
|
||||
requestBody: {
|
||||
date: dateWithoutTimezone(month),
|
||||
userIds: []
|
||||
}
|
||||
}).then((response) => setTrackingRecords(response.records));
|
||||
}
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
refetch().then(_ => {
|
||||
});
|
||||
}, [month])
|
||||
return {
|
||||
month,
|
||||
setMonth,
|
||||
refetch,
|
||||
trackingRecords
|
||||
}
|
||||
}
|
||||
|
||||
export default useWorkTableState;
|
||||
136
src/pages/AdminPage/tabs/WorkTimeTable/ui/WorkTimeTable.tsx
Normal file
136
src/pages/AdminPage/tabs/WorkTimeTable/ui/WorkTimeTable.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import {ActionIcon, Flex, MultiSelect, rem, Tooltip} from "@mantine/core";
|
||||
import {MonthPickerInput} from "@mantine/dates";
|
||||
import useWorkTableState from "../hooks/useWorkTableState.tsx";
|
||||
import {BaseTable} from "../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import useWorkTableColumns, {EmployeeData} from "../hooks/useWorkTableColumns.tsx";
|
||||
import {TimeTrackingRecord, TimeTrackingService, UserSchema} from "../../../../../client";
|
||||
import {dateWithoutTimezone, getDatesInMonth} from "../../../../../shared/lib/date.ts";
|
||||
import useUsersList from "../../../hooks/useUsersList.tsx";
|
||||
import {notifications} from "../../../../../shared/lib/notifications.ts";
|
||||
import {PaySchemeType} from "../../../../../shared/enums/PaySchemeType.ts";
|
||||
import {IconEyeOff} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
|
||||
const WorkTimeTable = () => {
|
||||
const [data, setData] = useState<EmployeeData[]>([]);
|
||||
const {
|
||||
month,
|
||||
setMonth,
|
||||
trackingRecords,
|
||||
refetch
|
||||
} = useWorkTableState();
|
||||
const [hiddenUsers, setHiddenUsers] = useState<UserSchema[]>([]);
|
||||
const users = useUsersList().objects.filter(user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY);
|
||||
const transformTrackingRecordsToData = (trackingRecords: TimeTrackingRecord[]): EmployeeData[] => {
|
||||
if (!month) return [];
|
||||
const existingUserIds = trackingRecords.map(tr => tr.user.id);
|
||||
const firstResult = trackingRecords.map((record) => ({
|
||||
name: `${record.user.firstName} ${record.user.secondName}`,
|
||||
userId: record.user.id,
|
||||
totalAmount: record.totalAmount,
|
||||
...Object.fromEntries(getDatesInMonth(month).reduce((acc, day) => {
|
||||
return acc.set(day.date().toString(), 0);
|
||||
}, new Map<string, number>)),
|
||||
...Object.fromEntries(record.data.reduce((acc, recordData) => {
|
||||
return acc.set((new Date(recordData.date).getDate().toString()), recordData.hours);
|
||||
}, new Map<string, number>))
|
||||
}));
|
||||
const restUsersResult = users.filter(user => !existingUserIds.includes(user.id)).map(user => ({
|
||||
name: `${user.firstName} ${user.secondName}`,
|
||||
userId: user.id,
|
||||
totalAmount: 0,
|
||||
...Object.fromEntries(getDatesInMonth(month).reduce((acc, day) => {
|
||||
return acc.set(day.date().toString(), 0);
|
||||
}, new Map<string, number>)),
|
||||
}))
|
||||
const hiddenUserIds = hiddenUsers.map(user => user.id);
|
||||
return (firstResult.concat(restUsersResult) as unknown as EmployeeData[]).filter(r => !hiddenUserIds.includes(r.userId));
|
||||
}
|
||||
|
||||
const optimisticUpdate = (date: Date, userId: number, value: number) => {
|
||||
const user = users.find(user => user.id === userId);
|
||||
if (!user) return;
|
||||
setData(prevState => prevState.map(record => {
|
||||
if (record.userId !== userId) return record;
|
||||
record[date.getDate()] = value;
|
||||
return record;
|
||||
}))
|
||||
TimeTrackingService.updateTimeTrackingRecord({
|
||||
requestBody: {
|
||||
date: dateWithoutTimezone(date),
|
||||
hours: value,
|
||||
userId: user.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
if (!ok) {
|
||||
notifications.guess(ok, {message});
|
||||
}
|
||||
await refetch();
|
||||
|
||||
})
|
||||
}
|
||||
const columns = useWorkTableColumns({
|
||||
month, data, onUpdate: optimisticUpdate
|
||||
});
|
||||
useEffect(() => {
|
||||
setData(transformTrackingRecordsToData(trackingRecords));
|
||||
}, [trackingRecords, hiddenUsers])
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
p={rem(10)}
|
||||
gap={rem(10)}
|
||||
>
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
<MultiSelect
|
||||
data={users.map(user => ({
|
||||
label: `${user.firstName} ${user.secondName}`,
|
||||
value: user.id.toString()
|
||||
}))}
|
||||
onChange={(event) => setHiddenUsers(users.filter(user => event.includes(user.id.toString())))}
|
||||
value={hiddenUsers.map(user => user.id.toString())}
|
||||
placeholder={hiddenUsers.length > 0 ? "" : "Скрытые пользователи"}
|
||||
/>
|
||||
<MonthPickerInput
|
||||
allowDeselect={false}
|
||||
onChange={(event) => event && setMonth(event)}
|
||||
value={month}
|
||||
placeholder={"Выберите месяц"}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<BaseTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableColumnActions: false,
|
||||
enableSorting: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Скрыть">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
const user = users.find(user => user.id === row.original.userId);
|
||||
if (!user) return;
|
||||
setHiddenUsers(prevState => [...prevState, user]);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconEyeOff/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<EmployeeData>}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkTimeTable;
|
||||
Reference in New Issue
Block a user