feat: work shifts history

This commit is contained in:
2024-11-28 18:01:47 +04:00
parent f41083d2a8
commit 900427275f
14 changed files with 434 additions and 248 deletions

View File

@@ -7,8 +7,6 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI'; export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI';
export type { ActiveWorkShiftSchema } from './models/ActiveWorkShiftSchema';
export type { ActiveWorkShiftsResponse } from './models/ActiveWorkShiftsResponse';
export type { AuthLoginRequest } from './models/AuthLoginRequest'; export type { AuthLoginRequest } from './models/AuthLoginRequest';
export type { AuthLoginResponse } from './models/AuthLoginResponse'; export type { AuthLoginResponse } from './models/AuthLoginResponse';
export type { BarcodeAttributeSchema } from './models/BarcodeAttributeSchema'; export type { BarcodeAttributeSchema } from './models/BarcodeAttributeSchema';
@@ -187,6 +185,7 @@ export type { GetProfitTableDataResponse } from './models/GetProfitTableDataResp
export type { GetServiceKitSchema } from './models/GetServiceKitSchema'; export type { GetServiceKitSchema } from './models/GetServiceKitSchema';
export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest'; export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest';
export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse'; export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse';
export type { GetWorkShiftsResponse } from './models/GetWorkShiftsResponse';
export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema'; export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema';
export type { HTTPValidationError } from './models/HTTPValidationError'; export type { HTTPValidationError } from './models/HTTPValidationError';
export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema';
@@ -275,6 +274,7 @@ export type { UserCreate } from './models/UserCreate';
export type { UserSchema } from './models/UserSchema'; export type { UserSchema } from './models/UserSchema';
export type { UserUpdate } from './models/UserUpdate'; export type { UserUpdate } from './models/UserUpdate';
export type { ValidationError } from './models/ValidationError'; export type { ValidationError } from './models/ValidationError';
export type { WorkShiftSchema } from './models/WorkShiftSchema';
export { AuthService } from './services/AuthService'; export { AuthService } from './services/AuthService';
export { BarcodeService } from './services/BarcodeService'; export { BarcodeService } from './services/BarcodeService';

View File

@@ -1,9 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ActiveWorkShiftSchema } from './ActiveWorkShiftSchema';
export type ActiveWorkShiftsResponse = {
shifts: Array<ActiveWorkShiftSchema>;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PaginationInfoSchema } from './PaginationInfoSchema';
import type { WorkShiftSchema } from './WorkShiftSchema';
export type GetWorkShiftsResponse = {
shifts: Array<WorkShiftSchema>;
paginationInfo: PaginationInfoSchema;
};

View File

@@ -3,9 +3,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { UserSchema } from './UserSchema'; import type { UserSchema } from './UserSchema';
export type ActiveWorkShiftSchema = { export type WorkShiftSchema = {
id: number; id: number;
startedAt: string; startedAt: string;
finishedAt?: (string | null);
hours?: (number | null);
user: UserSchema; user: UserSchema;
}; };

View File

@@ -2,10 +2,10 @@
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { ActiveWorkShiftsResponse } from '../models/ActiveWorkShiftsResponse';
import type { DeleteShiftResponse } from '../models/DeleteShiftResponse'; import type { DeleteShiftResponse } from '../models/DeleteShiftResponse';
import type { FinishShiftByIdResponse } from '../models/FinishShiftByIdResponse'; import type { FinishShiftByIdResponse } from '../models/FinishShiftByIdResponse';
import type { FinishShiftResponse } from '../models/FinishShiftResponse'; import type { FinishShiftResponse } from '../models/FinishShiftResponse';
import type { GetWorkShiftsResponse } from '../models/GetWorkShiftsResponse';
import type { StartShiftResponse } from '../models/StartShiftResponse'; import type { StartShiftResponse } from '../models/StartShiftResponse';
import type { CancelablePromise } from '../core/CancelablePromise'; import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI'; import { OpenAPI } from '../core/OpenAPI';
@@ -96,14 +96,32 @@ export class WorkShiftsService {
}); });
} }
/** /**
* Get Active Shifts * Get Shifts
* @returns ActiveWorkShiftsResponse Successful Response * @returns GetWorkShiftsResponse Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static getActiveShifts(): CancelablePromise<ActiveWorkShiftsResponse> { public static getShifts({
isActive,
page,
itemsPerPage,
}: {
isActive: boolean,
page?: (number | null),
itemsPerPage?: (number | null),
}): CancelablePromise<GetWorkShiftsResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/work-shifts/get-active-shifts', url: '/work-shifts/get-shifts/{is_active}',
path: {
'is_active': isActive,
},
query: {
'page': page,
'items_per_page': itemsPerPage,
},
errors: {
422: `Validation Error`,
},
}); });
} }
/** /**

View File

@@ -1,116 +0,0 @@
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { useActiveShiftsTableColumns } from "./columns.tsx";
import { ActionIcon, Flex, Stack, Text, Title, Tooltip } from "@mantine/core";
import { IconCheck, IconTrash } from "@tabler/icons-react";
import { ActiveWorkShiftSchema, WorkShiftsService } from "../../../../client";
import { modals } from "@mantine/modals";
import { formatDate } from "../../../../types/utils.ts";
import { MRT_TableOptions } from "mantine-react-table";
import { notifications } from "../../../../shared/lib/notifications.ts";
type Props = {
activeShifts: ActiveWorkShiftSchema[];
fetchActiveShifts: () => void;
}
export const ActiveShiftsTable = ({ activeShifts, fetchActiveShifts }: Props) => {
const columns = useActiveShiftsTableColumns();
const onDelete = (workShift: ActiveWorkShiftSchema) => {
WorkShiftsService.deleteWorkShift({
shiftId: workShift.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchActiveShifts();
})
.catch(err => {
console.log(err);
});
};
const onDeleteClick = (workShift: ActiveWorkShiftSchema) => {
modals.openConfirmModal({
title: "Удаление записи о начале смены",
children: (
<Text size="sm">
Вы уверены что хотите удалить запись о начале смены работника{" "}
{workShift.user.firstName} {workShift.user.secondName} от{" "}
{formatDate(workShift.startedAt)}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onDelete(workShift),
});
};
const onShiftFinish = (workShift: ActiveWorkShiftSchema) => {
WorkShiftsService.finishWorkShiftById({
shiftId: workShift.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchActiveShifts();
})
.catch(err => console.log(err));
}
const onShiftFinishClick = (workShift: ActiveWorkShiftSchema) => {
modals.openConfirmModal({
title: "Завершение смены",
children: (
<Text size="sm">
Вы уверены что хотите завершить смену работника{" "}
{workShift.user.firstName} {workShift.user.secondName} от{" "}
{formatDate(workShift.startedAt)}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onShiftFinish(workShift),
});
}
return (
<Stack mx={"xs"}>
<Title order={4} mt={"md"}>
Активные смены
</Title>
<BaseTable
data={activeShifts}
columns={columns}
restProps={
{
enableRowActions: true,
enableSorting: true,
enableColumnActions: false,
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() =>
onDeleteClick(row.original)
}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Завершить смену">
<ActionIcon
onClick={() =>
onShiftFinishClick(row.original)
}
variant={"default"}>
<IconCheck />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<ActiveWorkShiftSchema>
}
/>
</Stack>
);
};

View File

@@ -1,32 +0,0 @@
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { ActiveWorkShiftSchema } from "../../../../client";
export const useActiveShiftsTableColumns = () => {
return useMemo<MRT_ColumnDef<ActiveWorkShiftSchema>[]>(
() => [
{
header: "Начало смены",
accessorKey: "startedAt",
Cell: ({ row }) =>
new Date(row.original.startedAt).toLocaleString("ru-RU"),
},
{
header: "ФИО",
Cell: ({ row }) =>
`${row.original.user.firstName} ${row.original.user.secondName}`,
},
{
header: "Роль",
accessorKey: "user.role.name",
enableSorting: false,
},
{
header: "Должность",
accessorKey: "user.position.name",
enableSorting: false,
}
],
[]
);
};

View File

@@ -0,0 +1,28 @@
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
import { FC } from "react";
export enum ShiftsTableType {
ACTIVE,
HISTORY,
}
type Props = Omit<SegmentedControlProps, "data">;
const data = [
{
label: "Активные смены",
value: ShiftsTableType.ACTIVE.toString(),
},
{
label: "Завершенные смены",
value: ShiftsTableType.HISTORY.toString(),
},
];
export const ShiftsTableSegmentedControl: FC<Props> = props => {
return (
<SegmentedControl
data={data}
{...props}
/>
);
};

View File

@@ -1,91 +1,52 @@
import { Button, Group, Stack } from "@mantine/core"; import { Divider, Flex, Pagination, rem, Skeleton, Stack } from "@mantine/core";
import { ActiveWorkShiftSchema, WorkShiftsService } from "../../../../client"; import { ShiftsTable } from "./components/ShiftsTable.tsx";
import { useEffect, useState } from "react"; import {
import { notifications } from "../../../../shared/lib/notifications.ts"; ShiftsTableSegmentedControl,
import { modals } from "@mantine/modals"; } from "../../components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx";
import { ActiveShiftsTable } from "../../components/ActiveShiftsTable/ActiveShiftsTable.tsx"; import useWorkShiftsTable from "./hooks/useWorkShiftsTable.tsx";
import WorkShiftInput from "./components/WorkShiftInput.tsx";
export const WorkShiftsTab = () => { export const WorkShiftsTab = () => {
let inputType: "StartShift" | "FinishShift" = "StartShift"; const {
const [activeShifts, setActiveShifts] = useState<ActiveWorkShiftSchema[]>([]); shifts,
shiftsTableType,
const fetchActiveShifts = () => { setShiftsTableType,
WorkShiftsService.getActiveShifts() totalPages,
.then(res => { page,
setActiveShifts(res.shifts); setPage,
}) fetchShifts,
.catch(err => console.log(err)); isLoading,
} } = useWorkShiftsTable();
useEffect(() => {
fetchActiveShifts();
}, []);
const onInputFinish = (userIdInput: string) => {
const userId = parseInt(userIdInput);
if (isNaN(userId)) {
notifications.error({ message: "Ошибка, некорректные данные в QR-коде" });
return;
}
if (inputType === "StartShift") {
WorkShiftsService.startShift({
userId: userId!,
})
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
fetchActiveShifts();
})
.catch(err => console.log(err));
return;
}
WorkShiftsService.finishShift({
userId: userId!,
})
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
fetchActiveShifts();
})
.catch(err => console.log(err));
};
const onScanningStart = () => {
modals.openContextModal({
modal: "scanningModal",
innerProps: {
label: "Отсканируйте QR-код",
onScan: onInputFinish,
closeOnScan: true,
},
withCloseButton: false,
});
};
const onShiftStart = () => {
inputType = "StartShift";
onScanningStart();
};
const onShiftFinish = () => {
inputType = "FinishShift";
onScanningStart();
};
return ( return (
<Stack> <Stack gap={0}>
<Group ml={"xs"} mt={"xs"}> <WorkShiftInput fetchShifts={fetchShifts} />
<Button variant={"default"} onClick={onShiftStart}> <Divider />
Начать смену <ShiftsTableSegmentedControl
</Button> value={shiftsTableType.toString()}
<Button variant={"default"} onClick={onShiftFinish}> onChange={event => {
Закончить смену setPage(1);
</Button> setShiftsTableType(parseInt(event));
</Group> }}
<ActiveShiftsTable
activeShifts={activeShifts}
fetchActiveShifts={fetchActiveShifts}
/> />
<Skeleton visible={isLoading}>
<Flex gap={rem(10)} direction={"column"}>
<ShiftsTable
shiftsTableType={shiftsTableType}
shifts={shifts}
fetchShifts={fetchShifts}
/>
{totalPages > 1 && (
<Pagination
style={{ alignSelf: "flex-end" }}
withEdges
onChange={event => setPage(event)}
value={page}
total={totalPages}
/>
)}
</Flex>
</Skeleton>
</Stack> </Stack>
); );
}; };

View File

@@ -0,0 +1,120 @@
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
import { useShiftsTableColumns } from "../hooks/columns.tsx";
import { ActionIcon, Flex, Text, Tooltip } from "@mantine/core";
import { IconCheck, IconTrash } from "@tabler/icons-react";
import { WorkShiftSchema, WorkShiftsService } from "../../../../../client";
import { modals } from "@mantine/modals";
import { formatDate } from "../../../../../types/utils.ts";
import { MRT_TableOptions } from "mantine-react-table";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { ShiftsTableType } from "../../../components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx";
type Props = {
shifts: WorkShiftSchema[];
fetchShifts: () => void;
shiftsTableType: ShiftsTableType;
}
export const ShiftsTable = ({
shifts,
fetchShifts,
shiftsTableType,
}: Props) => {
const isActiveShiftsTable = shiftsTableType === ShiftsTableType.ACTIVE;
const columns = useShiftsTableColumns({ isActiveShiftsTable });
const onDelete = (workShift: WorkShiftSchema) => {
WorkShiftsService.deleteWorkShift({
shiftId: workShift.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchShifts();
})
.catch(err => console.log(err));
};
const onDeleteClick = (workShift: WorkShiftSchema) => {
modals.openConfirmModal({
title: "Удаление смены",
children: (
<Text size="sm">
Вы уверены что хотите удалить смену работника{" "}
{workShift.user.firstName} {workShift.user.secondName} от{" "}
{formatDate(workShift.startedAt)}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onDelete(workShift),
});
};
const onShiftFinish = (workShift: WorkShiftSchema) => {
WorkShiftsService.finishWorkShiftById({
shiftId: workShift.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchShifts();
})
.catch(err => console.log(err));
};
const onShiftFinishClick = (workShift: WorkShiftSchema) => {
modals.openConfirmModal({
title: "Завершение смены",
children: (
<Text size="sm">
Вы уверены что хотите завершить смену работника{" "}
{workShift.user.firstName} {workShift.user.secondName} от{" "}
{formatDate(workShift.startedAt)}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onShiftFinish(workShift),
});
};
return (
<BaseTable
data={shifts}
columns={columns}
restProps={
{
enableRowActions: true,
enableSorting: false,
enableColumnActions: false,
renderRowActions: ({ row }) => {
return (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() =>
onDeleteClick(row.original)
}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
{isActiveShiftsTable && (
<Tooltip label="Завершить смену">
<ActionIcon
onClick={() =>
onShiftFinishClick(row.original)
}
variant={"default"}>
<IconCheck />
</ActionIcon>
</Tooltip>
)}
</Flex>
);
},
} as MRT_TableOptions<WorkShiftSchema>
}
/>
);
};

View File

@@ -0,0 +1,26 @@
import { Button, Group } from "@mantine/core";
import useWorkShiftInput from "../hooks/useWorkShiftInput.tsx";
type Props = {
fetchShifts: () => void;
}
const WorkShiftInput = ({ fetchShifts }: Props) => {
const {
onShiftStart,
onShiftFinish,
} = useWorkShiftInput({ fetchShifts });
return (
<Group ml={"xs"} my={"xs"}>
<Button variant={"default"} onClick={onShiftStart}>
Начать смену
</Button>
<Button variant={"default"} onClick={onShiftFinish}>
Закончить смену
</Button>
</Group>
);
};
export default WorkShiftInput;

View File

@@ -0,0 +1,64 @@
import { useMemo } from "react";
import { MRT_ColumnDef, MRT_Row } from "mantine-react-table";
import { WorkShiftSchema } from "../../../../../client";
type Props = {
isActiveShiftsTable: boolean;
}
export const useShiftsTableColumns = ({ isActiveShiftsTable }: Props) => {
const getWorkedHoursString = (startedAtStr: string, finishedAtStr: string) => {
const finishedAt = new Date(finishedAtStr);
const startedAt = new Date(startedAtStr);
const diff: number = finishedAt.getTime() - startedAt.getTime();
const hours = Math.floor(diff / 3_600_000);
const minutes = Math.round(diff % 3_600_000 / 60_000);
if (hours === 0) {
return `${minutes} мин.`;
}
return `${hours} ч. ${minutes} мин.`;
};
const getColumnsForHistory = () => {
return isActiveShiftsTable ? [] : [
{
header: "Конец смены",
accessorKey: "finishedAt",
Cell: ({ row }: { row: MRT_Row<WorkShiftSchema> }) =>
row.original.finishedAt && new Date(row.original.finishedAt).toLocaleString("ru-RU"),
},
{
header: "Отработано",
Cell: ({ row }: { row: MRT_Row<WorkShiftSchema> }) =>
getWorkedHoursString(row.original.startedAt, row.original.finishedAt ?? ""),
},
];
};
return useMemo<MRT_ColumnDef<WorkShiftSchema>[]>(
() => [
{
header: "ФИО",
Cell: ({ row }: { row: MRT_Row<WorkShiftSchema> }) =>
`${row.original.user.firstName} ${row.original.user.secondName}`,
},
{
header: "Роль",
accessorKey: "user.role.name",
},
{
header: "Должность",
accessorKey: "user.position.name",
},
{
header: "Начало смены",
accessorKey: "startedAt",
Cell: ({ row }) =>
new Date(row.original.startedAt).toLocaleString("ru-RU"),
},
...getColumnsForHistory(),
],
[isActiveShiftsTable],
);
};

View File

@@ -0,0 +1,69 @@
import { WorkShiftsService } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { modals } from "@mantine/modals";
type Props = {
fetchShifts: () => void;
}
const useWorkShiftInput = ({ fetchShifts }: Props) => {
let inputType: "StartShift" | "FinishShift" = "StartShift";
const onInputFinish = (userIdInput: string) => {
const userId = parseInt(userIdInput);
if (isNaN(userId)) {
notifications.error({ message: "Ошибка, некорректные данные в QR-коде" });
return;
}
if (inputType === "StartShift") {
WorkShiftsService.startShift({
userId: userId!,
})
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
fetchShifts();
})
.catch(err => console.log(err));
return;
}
WorkShiftsService.finishShift({
userId: userId!,
})
.then(async ({ ok, message }) => {
notifications.guess(ok, { message });
fetchShifts();
})
.catch(err => console.log(err));
};
const onScanningStart = () => {
modals.openContextModal({
modal: "scanningModal",
innerProps: {
label: "Отсканируйте QR-код",
onScan: onInputFinish,
closeOnScan: true,
},
withCloseButton: false,
});
};
const onShiftStart = () => {
inputType = "StartShift";
onScanningStart();
};
const onShiftFinish = () => {
inputType = "FinishShift";
onScanningStart();
};
return {
onShiftStart,
onShiftFinish,
};
};
export default useWorkShiftInput;

View File

@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import { WorkShiftSchema, WorkShiftsService } from "../../../../../client";
import { ShiftsTableType } from "../../../components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx";
const useWorkShiftsTable = () => {
const [totalPages, setTotalPages] = useState(1);
const [page, setPage] = useState(1);
const [shifts, setShifts] = useState<WorkShiftSchema[]>([]);
const [shiftsTableType, setShiftsTableType] = useState<ShiftsTableType>(ShiftsTableType.ACTIVE);
const [isLoading, setIsLoading] = useState(false);
const fetchShifts = () => {
setIsLoading(true);
WorkShiftsService.getShifts({
isActive: shiftsTableType === ShiftsTableType.ACTIVE,
page,
itemsPerPage: 10,
})
.then(res => {
setShifts(res.shifts);
setTotalPages(res.paginationInfo.totalPages);
})
.catch(err => console.log(err))
.finally(() => setIsLoading(false));
};
useEffect(() => {
fetchShifts();
}, [shiftsTableType, page]);
return {
shifts,
shiftsTableType,
setShiftsTableType,
totalPages,
page,
setPage,
fetchShifts,
isLoading,
};
};
export default useWorkShiftsTable;