time tracking
This commit is contained in:
@@ -107,6 +107,8 @@ export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfR
|
|||||||
export type { GetProductBarcodePdfResponse } from './models/GetProductBarcodePdfResponse';
|
export type { GetProductBarcodePdfResponse } from './models/GetProductBarcodePdfResponse';
|
||||||
export type { GetProductBarcodeRequest } from './models/GetProductBarcodeRequest';
|
export type { GetProductBarcodeRequest } from './models/GetProductBarcodeRequest';
|
||||||
export type { GetProductBarcodeResponse } from './models/GetProductBarcodeResponse';
|
export type { GetProductBarcodeResponse } from './models/GetProductBarcodeResponse';
|
||||||
|
export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest';
|
||||||
|
export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse';
|
||||||
export type { HTTPValidationError } from './models/HTTPValidationError';
|
export type { HTTPValidationError } from './models/HTTPValidationError';
|
||||||
export type { PaginationInfoSchema } from './models/PaginationInfoSchema';
|
export type { PaginationInfoSchema } from './models/PaginationInfoSchema';
|
||||||
export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema';
|
export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema';
|
||||||
@@ -146,8 +148,12 @@ export type { ServiceSchema } from './models/ServiceSchema';
|
|||||||
export type { ServiceUpdateRequest } from './models/ServiceUpdateRequest';
|
export type { ServiceUpdateRequest } from './models/ServiceUpdateRequest';
|
||||||
export type { ServiceUpdateResponse } from './models/ServiceUpdateResponse';
|
export type { ServiceUpdateResponse } from './models/ServiceUpdateResponse';
|
||||||
export type { ShippingWarehouseSchema } from './models/ShippingWarehouseSchema';
|
export type { ShippingWarehouseSchema } from './models/ShippingWarehouseSchema';
|
||||||
|
export type { TimeTrackingData } from './models/TimeTrackingData';
|
||||||
|
export type { TimeTrackingRecord } from './models/TimeTrackingRecord';
|
||||||
export type { UpdatePayRateRequest } from './models/UpdatePayRateRequest';
|
export type { UpdatePayRateRequest } from './models/UpdatePayRateRequest';
|
||||||
export type { UpdatePayRateResponse } from './models/UpdatePayRateResponse';
|
export type { UpdatePayRateResponse } from './models/UpdatePayRateResponse';
|
||||||
|
export type { UpdateTimeTrackingRecordRequest } from './models/UpdateTimeTrackingRecordRequest';
|
||||||
|
export type { UpdateTimeTrackingRecordResponse } from './models/UpdateTimeTrackingRecordResponse';
|
||||||
export type { UpdateUserRequest } from './models/UpdateUserRequest';
|
export type { UpdateUserRequest } from './models/UpdateUserRequest';
|
||||||
export type { UpdateUserResponse } from './models/UpdateUserResponse';
|
export type { UpdateUserResponse } from './models/UpdateUserResponse';
|
||||||
export type { UserSchema } from './models/UserSchema';
|
export type { UserSchema } from './models/UserSchema';
|
||||||
@@ -165,4 +171,5 @@ export { ProductService } from './services/ProductService';
|
|||||||
export { RoleService } from './services/RoleService';
|
export { RoleService } from './services/RoleService';
|
||||||
export { ServiceService } from './services/ServiceService';
|
export { ServiceService } from './services/ServiceService';
|
||||||
export { ShippingWarehouseService } from './services/ShippingWarehouseService';
|
export { ShippingWarehouseService } from './services/ShippingWarehouseService';
|
||||||
|
export { TimeTrackingService } from './services/TimeTrackingService';
|
||||||
export { UserService } from './services/UserService';
|
export { UserService } from './services/UserService';
|
||||||
|
|||||||
9
src/client/models/GetTimeTrackingRecordsRequest.ts
Normal file
9
src/client/models/GetTimeTrackingRecordsRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type GetTimeTrackingRecordsRequest = {
|
||||||
|
date: string;
|
||||||
|
userIds: Array<number>;
|
||||||
|
};
|
||||||
|
|
||||||
9
src/client/models/GetTimeTrackingRecordsResponse.ts
Normal file
9
src/client/models/GetTimeTrackingRecordsResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { TimeTrackingRecord } from './TimeTrackingRecord';
|
||||||
|
export type GetTimeTrackingRecordsResponse = {
|
||||||
|
records: Array<TimeTrackingRecord>;
|
||||||
|
};
|
||||||
|
|
||||||
10
src/client/models/TimeTrackingData.ts
Normal file
10
src/client/models/TimeTrackingData.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type TimeTrackingData = {
|
||||||
|
date: string;
|
||||||
|
hours: number;
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
12
src/client/models/TimeTrackingRecord.ts
Normal file
12
src/client/models/TimeTrackingRecord.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { TimeTrackingData } from './TimeTrackingData';
|
||||||
|
import type { UserSchema } from './UserSchema';
|
||||||
|
export type TimeTrackingRecord = {
|
||||||
|
user: UserSchema;
|
||||||
|
totalAmount: number;
|
||||||
|
data: Array<TimeTrackingData>;
|
||||||
|
};
|
||||||
|
|
||||||
10
src/client/models/UpdateTimeTrackingRecordRequest.ts
Normal file
10
src/client/models/UpdateTimeTrackingRecordRequest.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type UpdateTimeTrackingRecordRequest = {
|
||||||
|
userId: number;
|
||||||
|
date: string;
|
||||||
|
hours: number;
|
||||||
|
};
|
||||||
|
|
||||||
9
src/client/models/UpdateTimeTrackingRecordResponse.ts
Normal file
9
src/client/models/UpdateTimeTrackingRecordResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type UpdateTimeTrackingRecordResponse = {
|
||||||
|
ok: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
53
src/client/services/TimeTrackingService.ts
Normal file
53
src/client/services/TimeTrackingService.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { GetTimeTrackingRecordsRequest } from '../models/GetTimeTrackingRecordsRequest';
|
||||||
|
import type { GetTimeTrackingRecordsResponse } from '../models/GetTimeTrackingRecordsResponse';
|
||||||
|
import type { UpdateTimeTrackingRecordRequest } from '../models/UpdateTimeTrackingRecordRequest';
|
||||||
|
import type { UpdateTimeTrackingRecordResponse } from '../models/UpdateTimeTrackingRecordResponse';
|
||||||
|
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||||
|
import { OpenAPI } from '../core/OpenAPI';
|
||||||
|
import { request as __request } from '../core/request';
|
||||||
|
export class TimeTrackingService {
|
||||||
|
/**
|
||||||
|
* Get Data
|
||||||
|
* @returns GetTimeTrackingRecordsResponse Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static getTimeTrackingRecords({
|
||||||
|
requestBody,
|
||||||
|
}: {
|
||||||
|
requestBody: GetTimeTrackingRecordsRequest,
|
||||||
|
}): CancelablePromise<GetTimeTrackingRecordsResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/time-tracking/get-records',
|
||||||
|
body: requestBody,
|
||||||
|
mediaType: 'application/json',
|
||||||
|
errors: {
|
||||||
|
422: `Validation Error`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get Data
|
||||||
|
* @returns UpdateTimeTrackingRecordResponse Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static updateTimeTrackingRecord({
|
||||||
|
requestBody,
|
||||||
|
}: {
|
||||||
|
requestBody: UpdateTimeTrackingRecordRequest,
|
||||||
|
}): CancelablePromise<UpdateTimeTrackingRecordResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/time-tracking/update-record',
|
||||||
|
body: requestBody,
|
||||||
|
mediaType: 'application/json',
|
||||||
|
errors: {
|
||||||
|
422: `Validation Error`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/components/DebouncedNumberInput/DebouncedNumberInput.tsx
Normal file
34
src/components/DebouncedNumberInput/DebouncedNumberInput.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {NumberInput, NumberInputProps} from "@mantine/core";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {useDebouncedValue} from "@mantine/hooks";
|
||||||
|
import {isNumber, omit} from "lodash";
|
||||||
|
|
||||||
|
|
||||||
|
type Props = NumberInputProps;
|
||||||
|
|
||||||
|
const DebouncedNumberInput = (props: Props) => {
|
||||||
|
const [value, setValue] = useState<number | string>(props.defaultValue || props.value || '');
|
||||||
|
const [debounced] = useDebouncedValue(value, 200);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.onChange) return;
|
||||||
|
props.onChange(debounced);
|
||||||
|
}, [debounced])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isNumber(props.value)) return;
|
||||||
|
setValue(props.value);
|
||||||
|
}, [props.value])
|
||||||
|
|
||||||
|
const restProps = omit(props, ["onChange", "value"])
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default DebouncedNumberInput;
|
||||||
@@ -4,8 +4,8 @@ import styles from './CreateDealButton.module.css';
|
|||||||
import {Text, Transition} from '@mantine/core';
|
import {Text, Transition} from '@mantine/core';
|
||||||
import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
|
import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
|
||||||
import {DealService} from "../../../client";
|
import {DealService} from "../../../client";
|
||||||
import {dateWithoutTimezone} from "../../../shared/lib/utils.ts";
|
|
||||||
import {useQueryClient} from "@tanstack/react-query";
|
import {useQueryClient} from "@tanstack/react-query";
|
||||||
|
import {dateWithoutTimezone} from "../../../shared/lib/date.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import {Button, Flex, rem, Textarea} from "@mantine/core";
|
|||||||
import {DateTimePicker, DateValue} from "@mantine/dates";
|
import {DateTimePicker, DateValue} from "@mantine/dates";
|
||||||
import {useForm} from "@mantine/form";
|
import {useForm} from "@mantine/form";
|
||||||
import {DealSummaryReorderRequest} from "../../client";
|
import {DealSummaryReorderRequest} from "../../client";
|
||||||
import {dateWithoutTimezone} from "../../shared/lib/utils.ts";
|
|
||||||
|
import {dateWithoutTimezone} from "../../shared/lib/date.ts";
|
||||||
|
|
||||||
type Deadline = {
|
type Deadline = {
|
||||||
deadline: DateValue,
|
deadline: DateValue,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import styles from './AdminPage.module.css';
|
import styles from './AdminPage.module.css';
|
||||||
import {Tabs} from "@mantine/core";
|
import {Tabs} from "@mantine/core";
|
||||||
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
|
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 RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx";
|
||||||
import UsersTab from "./tabs/Users/UsersTab.tsx";
|
import UsersTab from "./tabs/Users/UsersTab.tsx";
|
||||||
import {motion} from "framer-motion";
|
import {motion} from "framer-motion";
|
||||||
import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
|
import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
|
||||||
|
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
|
||||||
|
|
||||||
const AdminPage = () => {
|
const AdminPage = () => {
|
||||||
|
|
||||||
@@ -23,15 +24,15 @@ const AdminPage = () => {
|
|||||||
<Tabs.Tab value={"rolesAndPositions"} leftSection={<IconBriefcase/>}>
|
<Tabs.Tab value={"rolesAndPositions"} leftSection={<IconBriefcase/>}>
|
||||||
Должности
|
Должности
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value={"workTimeTable"} leftSection={<IconCalendarUser/>}>
|
||||||
|
Рабочее время
|
||||||
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<Tabs.Panel value={"users"}>
|
<Tabs.Panel value={"users"}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{opacity: 0}}
|
initial={{opacity: 0}}
|
||||||
animate={{opacity: 1}}
|
animate={{opacity: 1}}
|
||||||
transition={{duration: 0.2}}
|
transition={{duration: 0.2}}
|
||||||
|
|
||||||
>
|
>
|
||||||
<UsersTab/>
|
<UsersTab/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -54,6 +55,15 @@ const AdminPage = () => {
|
|||||||
<FinancesTab/>
|
<FinancesTab/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel value={"workTimeTable"}>
|
||||||
|
<motion.div
|
||||||
|
initial={{opacity: 0}}
|
||||||
|
animate={{opacity: 1}}
|
||||||
|
transition={{duration: 0.2}}
|
||||||
|
>
|
||||||
|
<WorkTimeTable/>
|
||||||
|
</motion.div>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</PageBlock>
|
</PageBlock>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {useForm} from "@mantine/form";
|
|||||||
import {Flex, NumberInput, rem} from "@mantine/core";
|
import {Flex, NumberInput, rem} from "@mantine/core";
|
||||||
import {DatePickerInput, MonthPickerInput} from "@mantine/dates";
|
import {DatePickerInput, MonthPickerInput} from "@mantine/dates";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {dateWithoutTimezone} from "../../../../shared/lib/utils.ts";
|
|
||||||
import UserSelect from "../../../../components/Selects/UserSelect/UserSelect.tsx";
|
import UserSelect from "../../../../components/Selects/UserSelect/UserSelect.tsx";
|
||||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
||||||
import {motion} from "framer-motion";
|
import {motion} from "framer-motion";
|
||||||
|
import {dateWithoutTimezone} from "../../../../shared/lib/date.ts";
|
||||||
|
|
||||||
type Props = CreateProps<PaymentRecordCreateSchema>;
|
type Props = CreateProps<PaymentRecordCreateSchema>;
|
||||||
const CreatePaymentRecordModal = ({
|
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;
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import {createLazyFileRoute} from "@tanstack/react-router";
|
import {createLazyFileRoute} from "@tanstack/react-router";
|
||||||
|
import DebouncedNumberInput from "../components/DebouncedNumberInput/DebouncedNumberInput.tsx";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {isNumber} from "lodash";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute('/test')({
|
export const Route = createLazyFileRoute('/test')({
|
||||||
component: TestPage
|
component: TestPage
|
||||||
@@ -6,9 +9,15 @@ export const Route = createLazyFileRoute('/test')({
|
|||||||
|
|
||||||
|
|
||||||
function TestPage() {
|
function TestPage() {
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(`effect: ${value}`)
|
||||||
|
}, [value]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DebouncedNumberInput
|
||||||
|
onChange={(event) => isNumber(event) && setValue(event)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
46
src/shared/lib/date.ts
Normal file
46
src/shared/lib/date.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export const dateWithoutTimezone = (date: Date) => {
|
||||||
|
const tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
|
||||||
|
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 => {
|
||||||
|
switch (day) {
|
||||||
|
case 0:
|
||||||
|
return "Вс";
|
||||||
|
case 1:
|
||||||
|
return "Пн";
|
||||||
|
case 2:
|
||||||
|
return "Вт";
|
||||||
|
case 3:
|
||||||
|
return "Ср";
|
||||||
|
case 4:
|
||||||
|
return "Чт";
|
||||||
|
case 5:
|
||||||
|
return "Пт";
|
||||||
|
case 6:
|
||||||
|
return "Сб";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
export const isWeekend = (day: number) => (day === 6) || (day === 0);
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
import {isNil} from "lodash";
|
import {isNil} from "lodash";
|
||||||
|
|
||||||
export const dateWithoutTimezone = (date: Date) => {
|
|
||||||
const tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
|
|
||||||
const withoutTimezone = new Date(date.valueOf() - tzoffset)
|
|
||||||
.toISOString()
|
|
||||||
.slice(0, -1);
|
|
||||||
return withoutTimezone;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDigitsCount = (num: number): number => {
|
export const getDigitsCount = (num: number): number => {
|
||||||
if (num === 0) return 1;
|
if (num === 0) return 1;
|
||||||
return Math.floor(Math.log10(Math.abs(num))) + 1;
|
return Math.floor(Math.log10(Math.abs(num))) + 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user