From 39f746f43547916b6e8fc863625541667cf07d73 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 20 Nov 2024 13:09:55 +0400 Subject: [PATCH] feat: work shifts by QR codes --- src/client/index.ts | 7 + src/client/models/ActiveWorkShiftSchema.ts | 11 ++ src/client/models/ActiveWorkShiftsResponse.ts | 9 ++ src/client/models/DeleteShiftResponse.ts | 9 ++ src/client/models/FinishShiftByIdResponse.ts | 9 ++ src/client/models/FinishShiftResponse.ts | 9 ++ src/client/models/StartShiftResponse.ts | 9 ++ src/client/services/WorkShiftsService.ts | 130 ++++++++++++++++++ src/modals/ScanningModal/ScanningModal.tsx | 38 +++++ src/modals/modals.ts | 2 + src/pages/AdminPage/AdminPage.tsx | 16 ++- .../ActiveShiftsTable/ActiveShiftsTable.tsx | 115 ++++++++++++++++ .../components/ActiveShiftsTable/columns.tsx | 29 ++++ .../components/UsersTable/UsersTable.tsx | 18 ++- .../tabs/WorkShifts/WorkShiftsTab.tsx | 92 +++++++++++++ 15 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 src/client/models/ActiveWorkShiftSchema.ts create mode 100644 src/client/models/ActiveWorkShiftsResponse.ts create mode 100644 src/client/models/DeleteShiftResponse.ts create mode 100644 src/client/models/FinishShiftByIdResponse.ts create mode 100644 src/client/models/FinishShiftResponse.ts create mode 100644 src/client/models/StartShiftResponse.ts create mode 100644 src/client/services/WorkShiftsService.ts create mode 100644 src/modals/ScanningModal/ScanningModal.tsx create mode 100644 src/pages/AdminPage/components/ActiveShiftsTable/ActiveShiftsTable.tsx create mode 100644 src/pages/AdminPage/components/ActiveShiftsTable/columns.tsx create mode 100644 src/pages/AdminPage/tabs/WorkShifts/WorkShiftsTab.tsx diff --git a/src/client/index.ts b/src/client/index.ts index 6610a7a..26d067b 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -7,6 +7,8 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise'; export { OpenAPI } 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 { AuthLoginResponse } from './models/AuthLoginResponse'; export type { BarcodeAttributeSchema } from './models/BarcodeAttributeSchema'; @@ -140,8 +142,11 @@ export type { DeletePositionRequest } from './models/DeletePositionRequest'; export type { DeletePositionResponse } from './models/DeletePositionResponse'; export type { DeletePriceCategoryRequest } from './models/DeletePriceCategoryRequest'; export type { DeletePriceCategoryResponse } from './models/DeletePriceCategoryResponse'; +export type { DeleteShiftResponse } from './models/DeleteShiftResponse'; export type { DeleteShippingWarehouseRequest } from './models/DeleteShippingWarehouseRequest'; export type { DeleteShippingWarehouseResponse } from './models/DeleteShippingWarehouseResponse'; +export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse'; +export type { FinishShiftResponse } from './models/FinishShiftResponse'; export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBarcodeTemplateAttributesResponse'; export type { GetAllBarcodeTemplateSizesResponse } from './models/GetAllBarcodeTemplateSizesResponse'; export type { GetAllBarcodeTemplatesResponse } from './models/GetAllBarcodeTemplatesResponse'; @@ -225,6 +230,7 @@ export type { ServiceUpdateCategoryResponse } from './models/ServiceUpdateCatego export type { ServiceUpdateRequest } from './models/ServiceUpdateRequest'; export type { ServiceUpdateResponse } from './models/ServiceUpdateResponse'; export type { ShippingWarehouseSchema } from './models/ShippingWarehouseSchema'; +export type { StartShiftResponse } from './models/StartShiftResponse'; export type { SynchronizeMarketplaceRequest } from './models/SynchronizeMarketplaceRequest'; export type { TaskInfoResponse } from './models/TaskInfoResponse'; export type { TimeTrackingData } from './models/TimeTrackingData'; @@ -264,3 +270,4 @@ export { ShippingWarehouseService } from './services/ShippingWarehouseService'; export { TaskService } from './services/TaskService'; export { TimeTrackingService } from './services/TimeTrackingService'; export { UserService } from './services/UserService'; +export { WorkShiftsService } from './services/WorkShiftsService'; diff --git a/src/client/models/ActiveWorkShiftSchema.ts b/src/client/models/ActiveWorkShiftSchema.ts new file mode 100644 index 0000000..59d3047 --- /dev/null +++ b/src/client/models/ActiveWorkShiftSchema.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { UserSchema } from './UserSchema'; +export type ActiveWorkShiftSchema = { + id: number; + startedAt: string; + user: UserSchema; +}; + diff --git a/src/client/models/ActiveWorkShiftsResponse.ts b/src/client/models/ActiveWorkShiftsResponse.ts new file mode 100644 index 0000000..37dfb85 --- /dev/null +++ b/src/client/models/ActiveWorkShiftsResponse.ts @@ -0,0 +1,9 @@ +/* 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; +}; + diff --git a/src/client/models/DeleteShiftResponse.ts b/src/client/models/DeleteShiftResponse.ts new file mode 100644 index 0000000..4ec6744 --- /dev/null +++ b/src/client/models/DeleteShiftResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type DeleteShiftResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/FinishShiftByIdResponse.ts b/src/client/models/FinishShiftByIdResponse.ts new file mode 100644 index 0000000..38032ae --- /dev/null +++ b/src/client/models/FinishShiftByIdResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type FinishShiftByIdResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/FinishShiftResponse.ts b/src/client/models/FinishShiftResponse.ts new file mode 100644 index 0000000..2463e65 --- /dev/null +++ b/src/client/models/FinishShiftResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type FinishShiftResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/StartShiftResponse.ts b/src/client/models/StartShiftResponse.ts new file mode 100644 index 0000000..7492313 --- /dev/null +++ b/src/client/models/StartShiftResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type StartShiftResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/services/WorkShiftsService.ts b/src/client/services/WorkShiftsService.ts new file mode 100644 index 0000000..5be255a --- /dev/null +++ b/src/client/services/WorkShiftsService.ts @@ -0,0 +1,130 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ActiveWorkShiftsResponse } from '../models/ActiveWorkShiftsResponse'; +import type { DeleteShiftResponse } from '../models/DeleteShiftResponse'; +import type { FinishShiftByIdResponse } from '../models/FinishShiftByIdResponse'; +import type { FinishShiftResponse } from '../models/FinishShiftResponse'; +import type { StartShiftResponse } from '../models/StartShiftResponse'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class WorkShiftsService { + /** + * Generate Qr Code + * @returns any Successful Response + * @throws ApiError + */ + public static generateQrCode({ + userId, + }: { + userId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/work-shifts/generate-qr-code/{user_id}', + path: { + 'user_id': userId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Start Shift + * @returns StartShiftResponse Successful Response + * @throws ApiError + */ + public static startShift({ + userId, + }: { + userId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/work-shifts/start-shift/{user_id}', + path: { + 'user_id': userId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Finish Shift + * @returns FinishShiftResponse Successful Response + * @throws ApiError + */ + public static finishShift({ + userId, + }: { + userId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/work-shifts/finish-shift/{user_id}', + path: { + 'user_id': userId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Finish Work Shift By Id + * @returns FinishShiftByIdResponse Successful Response + * @throws ApiError + */ + public static finishWorkShiftById({ + shiftId, + }: { + shiftId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/work-shifts/finish-shift-by-id/{shift_id}', + path: { + 'shift_id': shiftId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Active Shifts + * @returns ActiveWorkShiftsResponse Successful Response + * @throws ApiError + */ + public static getActiveShifts(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/work-shifts/get-active-shifts', + }); + } + /** + * Delete Work Shift + * @returns DeleteShiftResponse Successful Response + * @throws ApiError + */ + public static deleteWorkShift({ + shiftId, + }: { + shiftId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/work-shifts/delete-shift/{shift_id}', + path: { + 'shift_id': shiftId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } +} diff --git a/src/modals/ScanningModal/ScanningModal.tsx b/src/modals/ScanningModal/ScanningModal.tsx new file mode 100644 index 0000000..ff6e8b3 --- /dev/null +++ b/src/modals/ScanningModal/ScanningModal.tsx @@ -0,0 +1,38 @@ +import { ContextModalProps } from "@mantine/modals"; +import { Text } from "@mantine/core"; +import { useWindowEvent } from "@mantine/hooks"; +import { useState } from "react"; + +type Props = { + label: string; + onScan: (value: string) => void; + closeOnScan?: boolean; +}; + +const ScanningModal = ({ + context, + id, + innerProps, + }: ContextModalProps) => { + const [inputValues, setInputValues] = useState(""); + + const { label, onScan, closeOnScan } = innerProps; + + useWindowEvent("keydown", (event) => { + event.preventDefault(); + console.log(inputValues); + setInputValues(prevState => prevState + event.key); + if (event.key === "Enter") { + onScan(inputValues); + if (closeOnScan) { + context.closeContextModal(id); + } + } + }); + + return ( + {label} + ); +}; + +export default ScanningModal; diff --git a/src/modals/modals.ts b/src/modals/modals.ts index cdcda29..82a945e 100644 --- a/src/modals/modals.ts +++ b/src/modals/modals.ts @@ -21,6 +21,7 @@ import SelectDealProductsModal from "../pages/LeadsPage/modals/SelectDealProduct import ShippingWarehouseForm from "../pages/ShippingWarehousesPage/modals/ShippingWarehouseForm.tsx"; import MarketplaceFormModal from "../pages/MarketplacesPage/modals/MarketplaceFormModal/MarketplaceFormModal.tsx"; import ServicePriceCategoryForm from "../pages/ServicesPage/modals/ServicePriceCategoryForm.tsx"; +import ScanningModal from "./ScanningModal/ScanningModal.tsx"; export const modals = { enterDeadline: EnterDeadlineModal, @@ -46,4 +47,5 @@ export const modals = { shippingWarehouseForm: ShippingWarehouseForm, marketplaceFormModal: MarketplaceFormModal, servicePriceCategoryForm: ServicePriceCategoryForm, + scanningModal: ScanningModal, }; diff --git a/src/pages/AdminPage/AdminPage.tsx b/src/pages/AdminPage/AdminPage.tsx index e30e985..9a044fa 100644 --- a/src/pages/AdminPage/AdminPage.tsx +++ b/src/pages/AdminPage/AdminPage.tsx @@ -4,7 +4,7 @@ import PageBlock from "../../components/PageBlock/PageBlock.tsx"; import { IconBriefcase, IconCalendarUser, - IconCurrencyDollar, + IconCurrencyDollar, IconQrcode, IconUser, } from "@tabler/icons-react"; import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx"; @@ -12,6 +12,7 @@ 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"; +import { WorkShiftsTab } from "./tabs/WorkShifts/WorkShiftsTab.tsx"; const AdminPage = () => { return ( @@ -42,6 +43,11 @@ const AdminPage = () => { leftSection={}> Рабочее время + }> + Смены + { + + + + + diff --git a/src/pages/AdminPage/components/ActiveShiftsTable/ActiveShiftsTable.tsx b/src/pages/AdminPage/components/ActiveShiftsTable/ActiveShiftsTable.tsx new file mode 100644 index 0000000..a5b2482 --- /dev/null +++ b/src/pages/AdminPage/components/ActiveShiftsTable/ActiveShiftsTable.tsx @@ -0,0 +1,115 @@ +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: ( + + Вы уверены что хотите удалить запись о начале смены работника{" "} + {workShift.user.firstName} {workShift.user.secondName} от{" "} + {formatDate(workShift.startedAt)} + + ), + 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: ( + + Вы уверены что хотите завершить смену работника{" "} + {workShift.user.firstName} {workShift.user.secondName} от{" "} + {formatDate(workShift.startedAt)} + + ), + labels: { confirm: "Да", cancel: "Нет" }, + confirmProps: { color: "red" }, + onConfirm: () => onShiftFinish(workShift), + }); + } + + return ( + + + Активные смены + + ( + + + + onDeleteClick(row.original) + } + variant={"default"}> + + + + + + onShiftFinishClick(row.original) + } + variant={"default"}> + + + + + ), + } as MRT_TableOptions + } + /> + + ); +}; \ No newline at end of file diff --git a/src/pages/AdminPage/components/ActiveShiftsTable/columns.tsx b/src/pages/AdminPage/components/ActiveShiftsTable/columns.tsx new file mode 100644 index 0000000..8531613 --- /dev/null +++ b/src/pages/AdminPage/components/ActiveShiftsTable/columns.tsx @@ -0,0 +1,29 @@ +import { useMemo } from "react"; +import { MRT_ColumnDef } from "mantine-react-table"; +import { ActiveWorkShiftSchema } from "../../../../client"; + +export const useActiveShiftsTableColumns = () => { + return useMemo[]>( + () => [ + { + header: "Начало смены", + 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", + }, + { + header: "Должность", + accessorKey: "user.position.name", + } + ], + [] + ); +}; diff --git a/src/pages/AdminPage/components/UsersTable/UsersTable.tsx b/src/pages/AdminPage/components/UsersTable/UsersTable.tsx index f7cb259..3dba49b 100644 --- a/src/pages/AdminPage/components/UsersTable/UsersTable.tsx +++ b/src/pages/AdminPage/components/UsersTable/UsersTable.tsx @@ -4,7 +4,7 @@ import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx"; import { FC } from "react"; import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core"; import { useUsersTableColumns } from "./columns.tsx"; -import { IconEdit, IconTrash } from "@tabler/icons-react"; +import { IconEdit, IconQrcode, IconTrash } from "@tabler/icons-react"; import { modals } from "@mantine/modals"; import { MRT_TableOptions } from "mantine-react-table"; @@ -53,6 +53,13 @@ const UsersTable: FC = ({ items, onChange, onDelete, onCreate }) => { size: "md", }); }; + const onGenerateQrClick = (user: UserSchema) => { + const pdfWindow = window.open( + `${import.meta.env.VITE_API_URL}/work-shifts/generate-qr-code/${user.id}`, + ); + if (!pdfWindow) return; + pdfWindow.print(); + }; return ( = ({ items, onChange, onDelete, onCreate }) => { + { + onGenerateQrClick(row.original); + }} + label="QR-код"> + + + + ), } as MRT_TableOptions diff --git a/src/pages/AdminPage/tabs/WorkShifts/WorkShiftsTab.tsx b/src/pages/AdminPage/tabs/WorkShifts/WorkShiftsTab.tsx new file mode 100644 index 0000000..75ab8a6 --- /dev/null +++ b/src/pages/AdminPage/tabs/WorkShifts/WorkShiftsTab.tsx @@ -0,0 +1,92 @@ +import { Button, Group, Stack } from "@mantine/core"; +import { ActiveWorkShiftSchema, WorkShiftsService } from "../../../../client"; +import { useEffect, useState } from "react"; +import { notifications } from "../../../../shared/lib/notifications.ts"; +import { modals } from "@mantine/modals"; +import { ActiveShiftsTable } from "../../components/ActiveShiftsTable/ActiveShiftsTable.tsx"; + +export const WorkShiftsTab = () => { + let inputType: "StartShift" | "FinishShift" = "StartShift"; + const [activeShifts, setActiveShifts] = useState([]); + + const fetchActiveShifts = () => { + WorkShiftsService.getActiveShifts() + .then(res => { + setActiveShifts(res.shifts); + }) + .catch(err => console.log(err)); + } + + 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 ( + + + + + + + + ); +}; +