From f41083d2a820449220bdef8ebde7656c2c116431 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 27 Nov 2024 17:02:15 +0400 Subject: [PATCH 1/5] fix: fix pagination info schema --- src/client/models/PaginationInfoSchema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/models/PaginationInfoSchema.ts b/src/client/models/PaginationInfoSchema.ts index fe40eb4..9fb7e01 100644 --- a/src/client/models/PaginationInfoSchema.ts +++ b/src/client/models/PaginationInfoSchema.ts @@ -3,7 +3,7 @@ /* tslint:disable */ /* eslint-disable */ export type PaginationInfoSchema = { - totalPages?: number; - totalItems?: number; + totalPages: number; + totalItems: number; }; From 900427275fb088092ee0c6c699f3d51e8892967c Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Thu, 28 Nov 2024 18:01:47 +0400 Subject: [PATCH 2/5] feat: work shifts history --- src/client/index.ts | 4 +- src/client/models/ActiveWorkShiftsResponse.ts | 9 -- src/client/models/GetWorkShiftsResponse.ts | 11 ++ ...eWorkShiftSchema.ts => WorkShiftSchema.ts} | 4 +- src/client/services/WorkShiftsService.ts | 28 +++- .../ActiveShiftsTable/ActiveShiftsTable.tsx | 116 ---------------- .../components/ActiveShiftsTable/columns.tsx | 32 ----- .../ShiftsTableSegmentedControl.tsx | 28 ++++ .../tabs/WorkShifts/WorkShiftsTab.tsx | 127 ++++++------------ .../WorkShifts/components/ShiftsTable.tsx | 120 +++++++++++++++++ .../WorkShifts/components/WorkShiftInput.tsx | 26 ++++ .../tabs/WorkShifts/hooks/columns.tsx | 64 +++++++++ .../WorkShifts/hooks/useWorkShiftInput.tsx | 69 ++++++++++ .../WorkShifts/hooks/useWorkShiftsTable.tsx | 44 ++++++ 14 files changed, 434 insertions(+), 248 deletions(-) delete mode 100644 src/client/models/ActiveWorkShiftsResponse.ts create mode 100644 src/client/models/GetWorkShiftsResponse.ts rename src/client/models/{ActiveWorkShiftSchema.ts => WorkShiftSchema.ts} (72%) delete mode 100644 src/pages/AdminPage/components/ActiveShiftsTable/ActiveShiftsTable.tsx delete mode 100644 src/pages/AdminPage/components/ActiveShiftsTable/columns.tsx create mode 100644 src/pages/AdminPage/components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx create mode 100644 src/pages/AdminPage/tabs/WorkShifts/components/ShiftsTable.tsx create mode 100644 src/pages/AdminPage/tabs/WorkShifts/components/WorkShiftInput.tsx create mode 100644 src/pages/AdminPage/tabs/WorkShifts/hooks/columns.tsx create mode 100644 src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftInput.tsx create mode 100644 src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx diff --git a/src/client/index.ts b/src/client/index.ts index d5bdf04..c313dca 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -7,8 +7,6 @@ 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'; @@ -187,6 +185,7 @@ export type { GetProfitTableDataResponse } from './models/GetProfitTableDataResp export type { GetServiceKitSchema } from './models/GetServiceKitSchema'; export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest'; export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse'; +export type { GetWorkShiftsResponse } from './models/GetWorkShiftsResponse'; export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema'; export type { HTTPValidationError } from './models/HTTPValidationError'; export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; @@ -275,6 +274,7 @@ export type { UserCreate } from './models/UserCreate'; export type { UserSchema } from './models/UserSchema'; export type { UserUpdate } from './models/UserUpdate'; export type { ValidationError } from './models/ValidationError'; +export type { WorkShiftSchema } from './models/WorkShiftSchema'; export { AuthService } from './services/AuthService'; export { BarcodeService } from './services/BarcodeService'; diff --git a/src/client/models/ActiveWorkShiftsResponse.ts b/src/client/models/ActiveWorkShiftsResponse.ts deleted file mode 100644 index 37dfb85..0000000 --- a/src/client/models/ActiveWorkShiftsResponse.ts +++ /dev/null @@ -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; -}; - diff --git a/src/client/models/GetWorkShiftsResponse.ts b/src/client/models/GetWorkShiftsResponse.ts new file mode 100644 index 0000000..d557b26 --- /dev/null +++ b/src/client/models/GetWorkShiftsResponse.ts @@ -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; + paginationInfo: PaginationInfoSchema; +}; + diff --git a/src/client/models/ActiveWorkShiftSchema.ts b/src/client/models/WorkShiftSchema.ts similarity index 72% rename from src/client/models/ActiveWorkShiftSchema.ts rename to src/client/models/WorkShiftSchema.ts index 59d3047..40ef15e 100644 --- a/src/client/models/ActiveWorkShiftSchema.ts +++ b/src/client/models/WorkShiftSchema.ts @@ -3,9 +3,11 @@ /* tslint:disable */ /* eslint-disable */ import type { UserSchema } from './UserSchema'; -export type ActiveWorkShiftSchema = { +export type WorkShiftSchema = { id: number; startedAt: string; + finishedAt?: (string | null); + hours?: (number | null); user: UserSchema; }; diff --git a/src/client/services/WorkShiftsService.ts b/src/client/services/WorkShiftsService.ts index 5be255a..6b12bd7 100644 --- a/src/client/services/WorkShiftsService.ts +++ b/src/client/services/WorkShiftsService.ts @@ -2,10 +2,10 @@ /* 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 { GetWorkShiftsResponse } from '../models/GetWorkShiftsResponse'; import type { StartShiftResponse } from '../models/StartShiftResponse'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; @@ -96,14 +96,32 @@ export class WorkShiftsService { }); } /** - * Get Active Shifts - * @returns ActiveWorkShiftsResponse Successful Response + * Get Shifts + * @returns GetWorkShiftsResponse Successful Response * @throws ApiError */ - public static getActiveShifts(): CancelablePromise { + public static getShifts({ + isActive, + page, + itemsPerPage, + }: { + isActive: boolean, + page?: (number | null), + itemsPerPage?: (number | null), + }): CancelablePromise { return __request(OpenAPI, { 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`, + }, }); } /** diff --git a/src/pages/AdminPage/components/ActiveShiftsTable/ActiveShiftsTable.tsx b/src/pages/AdminPage/components/ActiveShiftsTable/ActiveShiftsTable.tsx deleted file mode 100644 index f921bc2..0000000 --- a/src/pages/AdminPage/components/ActiveShiftsTable/ActiveShiftsTable.tsx +++ /dev/null @@ -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: ( - - Вы уверены что хотите удалить запись о начале смены работника{" "} - {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 deleted file mode 100644 index e3b9253..0000000 --- a/src/pages/AdminPage/components/ActiveShiftsTable/columns.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useMemo } from "react"; -import { MRT_ColumnDef } from "mantine-react-table"; -import { ActiveWorkShiftSchema } from "../../../../client"; - -export const useActiveShiftsTableColumns = () => { - return useMemo[]>( - () => [ - { - 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, - } - ], - [] - ); -}; diff --git a/src/pages/AdminPage/components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx b/src/pages/AdminPage/components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx new file mode 100644 index 0000000..73fd8a0 --- /dev/null +++ b/src/pages/AdminPage/components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx @@ -0,0 +1,28 @@ +import { SegmentedControl, SegmentedControlProps } from "@mantine/core"; +import { FC } from "react"; + +export enum ShiftsTableType { + ACTIVE, + HISTORY, +} + +type Props = Omit; +const data = [ + { + label: "Активные смены", + value: ShiftsTableType.ACTIVE.toString(), + }, + { + label: "Завершенные смены", + value: ShiftsTableType.HISTORY.toString(), + }, +]; + +export const ShiftsTableSegmentedControl: FC = props => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/pages/AdminPage/tabs/WorkShifts/WorkShiftsTab.tsx b/src/pages/AdminPage/tabs/WorkShifts/WorkShiftsTab.tsx index 75ab8a6..0d9a46f 100644 --- a/src/pages/AdminPage/tabs/WorkShifts/WorkShiftsTab.tsx +++ b/src/pages/AdminPage/tabs/WorkShifts/WorkShiftsTab.tsx @@ -1,91 +1,52 @@ -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"; +import { Divider, Flex, Pagination, rem, Skeleton, Stack } from "@mantine/core"; +import { ShiftsTable } from "./components/ShiftsTable.tsx"; +import { + ShiftsTableSegmentedControl, +} from "../../components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx"; +import useWorkShiftsTable from "./hooks/useWorkShiftsTable.tsx"; +import WorkShiftInput from "./components/WorkShiftInput.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(); - }; + const { + shifts, + shiftsTableType, + setShiftsTableType, + totalPages, + page, + setPage, + fetchShifts, + isLoading, + } = useWorkShiftsTable(); return ( - - - - - - + + + { + setPage(1); + setShiftsTableType(parseInt(event)); + }} /> + + + + {totalPages > 1 && ( + setPage(event)} + value={page} + total={totalPages} + /> + )} + + ); }; diff --git a/src/pages/AdminPage/tabs/WorkShifts/components/ShiftsTable.tsx b/src/pages/AdminPage/tabs/WorkShifts/components/ShiftsTable.tsx new file mode 100644 index 0000000..5cb48a1 --- /dev/null +++ b/src/pages/AdminPage/tabs/WorkShifts/components/ShiftsTable.tsx @@ -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: ( + + Вы уверены что хотите удалить смену работника{" "} + {workShift.user.firstName} {workShift.user.secondName} от{" "} + {formatDate(workShift.startedAt)} + + ), + 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: ( + + Вы уверены что хотите завершить смену работника{" "} + {workShift.user.firstName} {workShift.user.secondName} от{" "} + {formatDate(workShift.startedAt)} + + ), + labels: { confirm: "Да", cancel: "Нет" }, + confirmProps: { color: "red" }, + onConfirm: () => onShiftFinish(workShift), + }); + }; + + return ( + { + return ( + + + + onDeleteClick(row.original) + } + variant={"default"}> + + + + {isActiveShiftsTable && ( + + + onShiftFinishClick(row.original) + } + variant={"default"}> + + + + )} + + ); + }, + } as MRT_TableOptions + } + /> + ); +}; \ No newline at end of file diff --git a/src/pages/AdminPage/tabs/WorkShifts/components/WorkShiftInput.tsx b/src/pages/AdminPage/tabs/WorkShifts/components/WorkShiftInput.tsx new file mode 100644 index 0000000..4e3ba1a --- /dev/null +++ b/src/pages/AdminPage/tabs/WorkShifts/components/WorkShiftInput.tsx @@ -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 ( + + + + + ); +}; + +export default WorkShiftInput; diff --git a/src/pages/AdminPage/tabs/WorkShifts/hooks/columns.tsx b/src/pages/AdminPage/tabs/WorkShifts/hooks/columns.tsx new file mode 100644 index 0000000..39dce21 --- /dev/null +++ b/src/pages/AdminPage/tabs/WorkShifts/hooks/columns.tsx @@ -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 }) => + row.original.finishedAt && new Date(row.original.finishedAt).toLocaleString("ru-RU"), + }, + { + header: "Отработано", + Cell: ({ row }: { row: MRT_Row }) => + getWorkedHoursString(row.original.startedAt, row.original.finishedAt ?? ""), + }, + ]; + }; + + return useMemo[]>( + () => [ + { + header: "ФИО", + Cell: ({ row }: { row: MRT_Row }) => + `${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], + ); +}; diff --git a/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftInput.tsx b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftInput.tsx new file mode 100644 index 0000000..bf0c8fe --- /dev/null +++ b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftInput.tsx @@ -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; diff --git a/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx new file mode 100644 index 0000000..8b71f75 --- /dev/null +++ b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx @@ -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([]); + const [shiftsTableType, setShiftsTableType] = useState(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; From 1795cacc5bba9aef7834c19fc0f5f05ac9eaf2bc Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 4 Dec 2024 11:02:32 +0400 Subject: [PATCH 3/5] feat: work shift pauses --- src/client/index.ts | 5 + .../models/FinishPauseByShiftIdResponse.ts | 9 ++ .../models/FinishPauseByUserIdResponse.ts | 9 ++ src/client/models/GetWorkShiftsResponse.ts | 4 +- .../models/StartPauseByShiftIdResponse.ts | 9 ++ .../models/StartPauseByUserIdResponse.ts | 9 ++ src/client/models/WorkShiftRowSchema.ts | 11 ++ src/client/models/WorkShiftSchema.ts | 2 +- src/client/services/WorkShiftsService.ts | 88 ++++++++++++ .../WorkShifts/components/ShiftsTable.tsx | 129 +++++++++++++----- .../WorkShifts/components/WorkShiftInput.tsx | 8 ++ .../tabs/WorkShifts/hooks/columns.tsx | 49 ++++--- .../WorkShifts/hooks/useWorkShiftInput.tsx | 48 ++++--- .../WorkShifts/hooks/useWorkShiftsTable.tsx | 5 +- src/types/utils.ts | 2 +- 15 files changed, 306 insertions(+), 81 deletions(-) create mode 100644 src/client/models/FinishPauseByShiftIdResponse.ts create mode 100644 src/client/models/FinishPauseByUserIdResponse.ts create mode 100644 src/client/models/StartPauseByShiftIdResponse.ts create mode 100644 src/client/models/StartPauseByUserIdResponse.ts create mode 100644 src/client/models/WorkShiftRowSchema.ts diff --git a/src/client/index.ts b/src/client/index.ts index c313dca..1eb435d 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -149,6 +149,8 @@ export type { DeleteShippingWarehouseRequest } from './models/DeleteShippingWare export type { DeleteShippingWarehouseResponse } from './models/DeleteShippingWarehouseResponse'; export type { ExpenseSchemaBase } from './models/ExpenseSchemaBase'; export type { ExpenseTagSchema } from './models/ExpenseTagSchema'; +export type { FinishPauseByShiftIdResponse } from './models/FinishPauseByShiftIdResponse'; +export type { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse'; export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse'; export type { FinishShiftResponse } from './models/FinishShiftResponse'; export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBarcodeTemplateAttributesResponse'; @@ -245,6 +247,8 @@ 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 { StartPauseByShiftIdResponse } from './models/StartPauseByShiftIdResponse'; +export type { StartPauseByUserIdResponse } from './models/StartPauseByUserIdResponse'; export type { StartShiftResponse } from './models/StartShiftResponse'; export type { SynchronizeMarketplaceRequest } from './models/SynchronizeMarketplaceRequest'; export type { TaskInfoResponse } from './models/TaskInfoResponse'; @@ -274,6 +278,7 @@ export type { UserCreate } from './models/UserCreate'; export type { UserSchema } from './models/UserSchema'; export type { UserUpdate } from './models/UserUpdate'; export type { ValidationError } from './models/ValidationError'; +export type { WorkShiftRowSchema } from './models/WorkShiftRowSchema'; export type { WorkShiftSchema } from './models/WorkShiftSchema'; export { AuthService } from './services/AuthService'; diff --git a/src/client/models/FinishPauseByShiftIdResponse.ts b/src/client/models/FinishPauseByShiftIdResponse.ts new file mode 100644 index 0000000..ed8d6ee --- /dev/null +++ b/src/client/models/FinishPauseByShiftIdResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type FinishPauseByShiftIdResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/FinishPauseByUserIdResponse.ts b/src/client/models/FinishPauseByUserIdResponse.ts new file mode 100644 index 0000000..035abd3 --- /dev/null +++ b/src/client/models/FinishPauseByUserIdResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type FinishPauseByUserIdResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/GetWorkShiftsResponse.ts b/src/client/models/GetWorkShiftsResponse.ts index d557b26..b6299f9 100644 --- a/src/client/models/GetWorkShiftsResponse.ts +++ b/src/client/models/GetWorkShiftsResponse.ts @@ -3,9 +3,9 @@ /* tslint:disable */ /* eslint-disable */ import type { PaginationInfoSchema } from './PaginationInfoSchema'; -import type { WorkShiftSchema } from './WorkShiftSchema'; +import type { WorkShiftRowSchema } from './WorkShiftRowSchema'; export type GetWorkShiftsResponse = { - shifts: Array; + shifts: Array; paginationInfo: PaginationInfoSchema; }; diff --git a/src/client/models/StartPauseByShiftIdResponse.ts b/src/client/models/StartPauseByShiftIdResponse.ts new file mode 100644 index 0000000..d2ddd85 --- /dev/null +++ b/src/client/models/StartPauseByShiftIdResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type StartPauseByShiftIdResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/StartPauseByUserIdResponse.ts b/src/client/models/StartPauseByUserIdResponse.ts new file mode 100644 index 0000000..4b6b3ec --- /dev/null +++ b/src/client/models/StartPauseByUserIdResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type StartPauseByUserIdResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/WorkShiftRowSchema.ts b/src/client/models/WorkShiftRowSchema.ts new file mode 100644 index 0000000..b7f805a --- /dev/null +++ b/src/client/models/WorkShiftRowSchema.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { WorkShiftSchema } from './WorkShiftSchema'; +export type WorkShiftRowSchema = { + workShift: WorkShiftSchema; + totalHours?: (number | null); + pauseHours?: (number | null); +}; + diff --git a/src/client/models/WorkShiftSchema.ts b/src/client/models/WorkShiftSchema.ts index 40ef15e..4ca4c97 100644 --- a/src/client/models/WorkShiftSchema.ts +++ b/src/client/models/WorkShiftSchema.ts @@ -7,7 +7,7 @@ export type WorkShiftSchema = { id: number; startedAt: string; finishedAt?: (string | null); - hours?: (number | null); + isPaused?: (boolean | null); user: UserSchema; }; diff --git a/src/client/services/WorkShiftsService.ts b/src/client/services/WorkShiftsService.ts index 6b12bd7..b32dedc 100644 --- a/src/client/services/WorkShiftsService.ts +++ b/src/client/services/WorkShiftsService.ts @@ -3,9 +3,13 @@ /* tslint:disable */ /* eslint-disable */ import type { DeleteShiftResponse } from '../models/DeleteShiftResponse'; +import type { FinishPauseByShiftIdResponse } from '../models/FinishPauseByShiftIdResponse'; +import type { FinishPauseByUserIdResponse } from '../models/FinishPauseByUserIdResponse'; import type { FinishShiftByIdResponse } from '../models/FinishShiftByIdResponse'; import type { FinishShiftResponse } from '../models/FinishShiftResponse'; import type { GetWorkShiftsResponse } from '../models/GetWorkShiftsResponse'; +import type { StartPauseByShiftIdResponse } from '../models/StartPauseByShiftIdResponse'; +import type { StartPauseByUserIdResponse } from '../models/StartPauseByUserIdResponse'; import type { StartShiftResponse } from '../models/StartShiftResponse'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; @@ -145,4 +149,88 @@ export class WorkShiftsService { }, }); } + /** + * Start Pause By Shift Id + * @returns StartPauseByShiftIdResponse Successful Response + * @throws ApiError + */ + public static startPauseByShiftId({ + shiftId, + }: { + shiftId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/work-shifts/pause/start/{shift_id}', + path: { + 'shift_id': shiftId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Start Pause By User Id + * @returns StartPauseByUserIdResponse Successful Response + * @throws ApiError + */ + public static startPauseByUserId({ + userId, + }: { + userId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/work-shifts/pause/start/for-user/{user_id}', + path: { + 'user_id': userId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Finish Pause By Shift Id + * @returns FinishPauseByShiftIdResponse Successful Response + * @throws ApiError + */ + public static finishPauseByShiftId({ + shiftId, + }: { + shiftId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/work-shifts/pause/finish/{shift_id}', + path: { + 'shift_id': shiftId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Finish Pause By User Id + * @returns FinishPauseByUserIdResponse Successful Response + * @throws ApiError + */ + public static finishPauseByUserId({ + userId, + }: { + userId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/work-shifts/pause/finish/for-user/{shift_id}', + query: { + 'user_id': userId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } } diff --git a/src/pages/AdminPage/tabs/WorkShifts/components/ShiftsTable.tsx b/src/pages/AdminPage/tabs/WorkShifts/components/ShiftsTable.tsx index 5cb48a1..31e1d70 100644 --- a/src/pages/AdminPage/tabs/WorkShifts/components/ShiftsTable.tsx +++ b/src/pages/AdminPage/tabs/WorkShifts/components/ShiftsTable.tsx @@ -1,17 +1,18 @@ 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 { IconCheck, IconPlayerPause, IconPlayerPlay, IconTrash } from "@tabler/icons-react"; +import { WorkShiftRowSchema, WorkShiftsService } from "../../../../../client"; import { modals } from "@mantine/modals"; import { formatDate } from "../../../../../types/utils.ts"; -import { MRT_TableOptions } from "mantine-react-table"; +import { MRT_Row, MRT_TableOptions } from "mantine-react-table"; import { notifications } from "../../../../../shared/lib/notifications.ts"; import { ShiftsTableType } from "../../../components/ShiftsTableSegmentedControl/ShiftsTableSegmentedControl.tsx"; +import { ReactNode } from "react"; type Props = { - shifts: WorkShiftSchema[]; + shifts: WorkShiftRowSchema[]; fetchShifts: () => void; shiftsTableType: ShiftsTableType; } @@ -24,9 +25,9 @@ export const ShiftsTable = ({ const isActiveShiftsTable = shiftsTableType === ShiftsTableType.ACTIVE; const columns = useShiftsTableColumns({ isActiveShiftsTable }); - const onDelete = (workShift: WorkShiftSchema) => { + const onDelete = (workShiftRow: WorkShiftRowSchema) => { WorkShiftsService.deleteWorkShift({ - shiftId: workShift.id, + shiftId: workShiftRow.workShift.id, }) .then(({ ok, message }) => { notifications.guess(ok, { message }); @@ -35,25 +36,25 @@ export const ShiftsTable = ({ .catch(err => console.log(err)); }; - const onDeleteClick = (workShift: WorkShiftSchema) => { + const onDeleteClick = (workShiftRow: WorkShiftRowSchema) => { modals.openConfirmModal({ title: "Удаление смены", children: ( Вы уверены что хотите удалить смену работника{" "} - {workShift.user.firstName} {workShift.user.secondName} от{" "} - {formatDate(workShift.startedAt)} + {workShiftRow.workShift.user.firstName} {workShiftRow.workShift.user.secondName} от{" "} + {formatDate(workShiftRow.workShift.startedAt)} ), labels: { confirm: "Да", cancel: "Нет" }, confirmProps: { color: "red" }, - onConfirm: () => onDelete(workShift), + onConfirm: () => onDelete(workShiftRow), }); }; - const onShiftFinish = (workShift: WorkShiftSchema) => { + const onShiftFinish = (workShiftRow: WorkShiftRowSchema) => { WorkShiftsService.finishWorkShiftById({ - shiftId: workShift.id, + shiftId: workShiftRow.workShift.id, }) .then(({ ok, message }) => { notifications.guess(ok, { message }); @@ -62,22 +63,95 @@ export const ShiftsTable = ({ .catch(err => console.log(err)); }; - const onShiftFinishClick = (workShift: WorkShiftSchema) => { + const onShiftFinishClick = (workShiftRow: WorkShiftRowSchema) => { modals.openConfirmModal({ title: "Завершение смены", children: ( Вы уверены что хотите завершить смену работника{" "} - {workShift.user.firstName} {workShift.user.secondName} от{" "} - {formatDate(workShift.startedAt)} + {workShiftRow.workShift.user.firstName} {workShiftRow.workShift.user.secondName} от{" "} + {formatDate(workShiftRow.workShift.startedAt)} ), labels: { confirm: "Да", cancel: "Нет" }, confirmProps: { color: "red" }, - onConfirm: () => onShiftFinish(workShift), + onConfirm: () => onShiftFinish(workShiftRow), }); }; + const onShiftPauseClick = (workShiftRow: WorkShiftRowSchema) => { + WorkShiftsService.startPauseByShiftId({ + shiftId: workShiftRow.workShift.id, + }) + .then(({ ok, message }) => { + notifications.guess(ok, { message }); + fetchShifts(); + }) + .catch(err => console.log(err)); + }; + + const onShiftResumeClick = (workShiftRow: WorkShiftRowSchema) => { + WorkShiftsService.finishPauseByShiftId({ + shiftId: workShiftRow.workShift.id, + }) + .then(({ ok, message }) => { + notifications.guess(ok, { message }); + fetchShifts(); + }) + .catch(err => console.log(err)); + }; + + const getAction = ( + label: string, + func: () => void, + icon: ReactNode, + ) => { + return ( + + + {icon} + + + ); + }; + + const getRowActions = (row: MRT_Row) => { + const actions = [ + getAction("Удалить", () => onDeleteClick(row.original), ), + ]; + + if (isActiveShiftsTable) { + actions.push( + getAction( + "Завершить смену", + () => onShiftFinishClick(row.original), + , + ), + ); + if (row.original.workShift.isPaused) { + actions.push( + getAction( + "Продолжить смену", + () => onShiftResumeClick(row.original), + , + ), + ); + } else { + actions.push( + getAction( + "Поставить смену на паузу", + () => onShiftPauseClick(row.original), + , + ), + ); + } + } + + return actions; + }; + return ( { return ( - - - onDeleteClick(row.original) - } - variant={"default"}> - - - - {isActiveShiftsTable && ( - - - onShiftFinishClick(row.original) - } - variant={"default"}> - - - - )} + {...getRowActions(row)} ); }, - } as MRT_TableOptions + } as MRT_TableOptions } /> ); diff --git a/src/pages/AdminPage/tabs/WorkShifts/components/WorkShiftInput.tsx b/src/pages/AdminPage/tabs/WorkShifts/components/WorkShiftInput.tsx index 4e3ba1a..c40fc01 100644 --- a/src/pages/AdminPage/tabs/WorkShifts/components/WorkShiftInput.tsx +++ b/src/pages/AdminPage/tabs/WorkShifts/components/WorkShiftInput.tsx @@ -9,6 +9,8 @@ const WorkShiftInput = ({ fetchShifts }: Props) => { const { onShiftStart, onShiftFinish, + onShiftResume, + onShiftPause, } = useWorkShiftInput({ fetchShifts }); return ( @@ -19,6 +21,12 @@ const WorkShiftInput = ({ fetchShifts }: Props) => { + + ); }; diff --git a/src/pages/AdminPage/tabs/WorkShifts/hooks/columns.tsx b/src/pages/AdminPage/tabs/WorkShifts/hooks/columns.tsx index 39dce21..31f615d 100644 --- a/src/pages/AdminPage/tabs/WorkShifts/hooks/columns.tsx +++ b/src/pages/AdminPage/tabs/WorkShifts/hooks/columns.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { MRT_ColumnDef, MRT_Row } from "mantine-react-table"; -import { WorkShiftSchema } from "../../../../../client"; +import { WorkShiftRowSchema } from "../../../../../client"; type Props = { @@ -8,12 +8,9 @@ type Props = { } 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); + const getWorkedHoursString = (seconds: number) => { + const hours = Math.floor(seconds / 3_600); + const minutes = Math.floor(seconds % 3_600 / 60); if (hours === 0) { return `${minutes} мин.`; } @@ -24,38 +21,50 @@ export const useShiftsTableColumns = ({ isActiveShiftsTable }: Props) => { return isActiveShiftsTable ? [] : [ { header: "Конец смены", - accessorKey: "finishedAt", - Cell: ({ row }: { row: MRT_Row }) => - row.original.finishedAt && new Date(row.original.finishedAt).toLocaleString("ru-RU"), + accessorKey: "workShift.finishedAt", + Cell: ({ row }: { row: MRT_Row }) => + row.original.workShift.finishedAt && new Date(row.original.workShift.finishedAt).toLocaleString("ru-RU"), + }, + { + header: "Длительность смены", + accessorKey: "totalHours", + Cell: ({ row }: { row: MRT_Row }) => + getWorkedHoursString(row.original.totalHours ?? 0), + }, + { + header: "Перерывы", + accessorKey: "pauseHours", + Cell: ({ row }: { row: MRT_Row }) => + getWorkedHoursString(row.original.pauseHours ?? 0), }, { header: "Отработано", - Cell: ({ row }: { row: MRT_Row }) => - getWorkedHoursString(row.original.startedAt, row.original.finishedAt ?? ""), + Cell: ({ row }: { row: MRT_Row }) => + getWorkedHoursString((row.original.totalHours ?? 0) - (row.original.pauseHours ?? 0)), }, - ]; + ] as MRT_ColumnDef[]; }; - return useMemo[]>( + return useMemo[]>( () => [ { header: "ФИО", - Cell: ({ row }: { row: MRT_Row }) => - `${row.original.user.firstName} ${row.original.user.secondName}`, + Cell: ({ row }: { row: MRT_Row }) => + `${row.original.workShift.user.firstName} ${row.original.workShift.user.secondName}`, }, { header: "Роль", - accessorKey: "user.role.name", + accessorKey: "workShift.user.role.name", }, { header: "Должность", - accessorKey: "user.position.name", + accessorKey: "workShift.user.position.name", }, { header: "Начало смены", - accessorKey: "startedAt", + accessorKey: "workShift.startedAt", Cell: ({ row }) => - new Date(row.original.startedAt).toLocaleString("ru-RU"), + new Date(row.original.workShift.startedAt).toLocaleString("ru-RU"), }, ...getColumnsForHistory(), ], diff --git a/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftInput.tsx b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftInput.tsx index bf0c8fe..3dfeb89 100644 --- a/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftInput.tsx +++ b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftInput.tsx @@ -6,8 +6,22 @@ type Props = { fetchShifts: () => void; } +enum InputType { + START_SHIFT, + FINISH_SHIFT, + RESUME_SHIFT, + PAUSE_SHIFT, +} + const useWorkShiftInput = ({ fetchShifts }: Props) => { - let inputType: "StartShift" | "FinishShift" = "StartShift"; + let inputType: InputType = InputType.START_SHIFT; + + const workShiftMethods = { + [InputType.START_SHIFT]: WorkShiftsService.startShift, + [InputType.FINISH_SHIFT]: WorkShiftsService.finishShift, + [InputType.RESUME_SHIFT]: WorkShiftsService.finishPauseByUserId, + [InputType.PAUSE_SHIFT]: WorkShiftsService.startPauseByUserId, + }; const onInputFinish = (userIdInput: string) => { const userId = parseInt(userIdInput); @@ -16,21 +30,7 @@ const useWorkShiftInput = ({ fetchShifts }: Props) => { 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!, - }) + workShiftMethods[inputType]({ userId }) .then(async ({ ok, message }) => { notifications.guess(ok, { message }); fetchShifts(); @@ -51,18 +51,30 @@ const useWorkShiftInput = ({ fetchShifts }: Props) => { }; const onShiftStart = () => { - inputType = "StartShift"; + inputType = InputType.START_SHIFT; onScanningStart(); }; const onShiftFinish = () => { - inputType = "FinishShift"; + inputType = InputType.FINISH_SHIFT; + onScanningStart(); + }; + + const onShiftResume = () => { + inputType = InputType.RESUME_SHIFT; + onScanningStart(); + }; + + const onShiftPause = () => { + inputType = InputType.PAUSE_SHIFT; onScanningStart(); }; return { onShiftStart, onShiftFinish, + onShiftResume, + onShiftPause, }; }; diff --git a/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx index 8b71f75..ae3edd0 100644 --- a/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx +++ b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from "react"; -import { WorkShiftSchema, WorkShiftsService } from "../../../../../client"; +import { WorkShiftRowSchema, 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([]); + const [shifts, setShifts] = useState([]); const [shiftsTableType, setShiftsTableType] = useState(ShiftsTableType.ACTIVE); const [isLoading, setIsLoading] = useState(false); @@ -18,6 +18,7 @@ const useWorkShiftsTable = () => { itemsPerPage: 10, }) .then(res => { + console.log(res.shifts); setShifts(res.shifts); setTotalPages(res.paginationInfo.totalPages); }) diff --git a/src/types/utils.ts b/src/types/utils.ts index b917ac1..d30f9c8 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -45,7 +45,7 @@ export function ObjectStateToTableProps( export const floatHoursToHoursAndMinutes = (hours: number): number[] => { const resHours = Math.floor(hours); - const minutes = Math.round((hours - resHours) * 60); + const minutes = Math.floor((hours - resHours) * 60); return [resHours, minutes]; }; From 2cb62a4e0bec9025db89eaa04138d33a6d666f6f Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 4 Dec 2024 20:21:01 +0400 Subject: [PATCH 4/5] feat: passport images for user --- src/client/index.ts | 3 + .../models/Body_upload_passport_image.ts | 8 ++ src/client/models/PassportImageSchema.ts | 10 +++ .../models/UploadPassportImageResponse.ts | 10 +++ src/client/models/UserCreate.ts | 3 + src/client/models/UserSchema.ts | 3 + src/client/models/UserUpdate.ts | 3 + src/client/services/UserService.ts | 27 +++++++ .../ImageDropzone/ImageDropzone.tsx | 73 ++++--------------- src/hooks/useImageDropzone.tsx | 26 +++++++ .../PassportImageDropzone.tsx | 60 +++++++++++++++ .../modals/UserFormModal/UserFormModal.tsx | 18 +++++ .../ProductImageDropzone.tsx | 60 +++++++++++++++ .../CreateProductModal/CreateProductModal.tsx | 4 +- src/types/UseImageDropzone.tsx | 12 +++ 15 files changed, 260 insertions(+), 60 deletions(-) create mode 100644 src/client/models/Body_upload_passport_image.ts create mode 100644 src/client/models/PassportImageSchema.ts create mode 100644 src/client/models/UploadPassportImageResponse.ts create mode 100644 src/hooks/useImageDropzone.tsx create mode 100644 src/pages/AdminPage/components/PassportImageDropzone/PassportImageDropzone.tsx create mode 100644 src/pages/ProductsPage/components/ProductImageDropzone/ProductImageDropzone.tsx create mode 100644 src/types/UseImageDropzone.tsx diff --git a/src/client/index.ts b/src/client/index.ts index 1eb435d..bab211d 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -28,6 +28,7 @@ export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema'; export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouseSchema'; export type { BillPaymentStatus } from './models/BillPaymentStatus'; export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest'; +export type { Body_upload_passport_image } from './models/Body_upload_passport_image'; export type { Body_upload_product_barcode_image } from './models/Body_upload_product_barcode_image'; export type { Body_upload_product_image } from './models/Body_upload_product_image'; export type { CancelDealBillRequest } from './models/CancelDealBillRequest'; @@ -194,6 +195,7 @@ export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; export type { MarketplaceSchema } from './models/MarketplaceSchema'; export type { NotificationChannel } from './models/NotificationChannel'; export type { PaginationInfoSchema } from './models/PaginationInfoSchema'; +export type { PassportImageSchema } from './models/PassportImageSchema'; export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema'; export type { PaymentRecordGetSchema } from './models/PaymentRecordGetSchema'; export type { PayRateSchema } from './models/PayRateSchema'; @@ -274,6 +276,7 @@ export type { UpdateTimeTrackingRecordRequest } from './models/UpdateTimeTrackin export type { UpdateTimeTrackingRecordResponse } from './models/UpdateTimeTrackingRecordResponse'; export type { UpdateUserRequest } from './models/UpdateUserRequest'; export type { UpdateUserResponse } from './models/UpdateUserResponse'; +export type { UploadPassportImageResponse } from './models/UploadPassportImageResponse'; export type { UserCreate } from './models/UserCreate'; export type { UserSchema } from './models/UserSchema'; export type { UserUpdate } from './models/UserUpdate'; diff --git a/src/client/models/Body_upload_passport_image.ts b/src/client/models/Body_upload_passport_image.ts new file mode 100644 index 0000000..39425dd --- /dev/null +++ b/src/client/models/Body_upload_passport_image.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Body_upload_passport_image = { + upload_file: Blob; +}; + diff --git a/src/client/models/PassportImageSchema.ts b/src/client/models/PassportImageSchema.ts new file mode 100644 index 0000000..998a1db --- /dev/null +++ b/src/client/models/PassportImageSchema.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type PassportImageSchema = { + id: number; + userId: number; + imageUrl: string; +}; + diff --git a/src/client/models/UploadPassportImageResponse.ts b/src/client/models/UploadPassportImageResponse.ts new file mode 100644 index 0000000..7c45a3b --- /dev/null +++ b/src/client/models/UploadPassportImageResponse.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type UploadPassportImageResponse = { + ok: boolean; + message: string; + imageUrl?: (string | null); +}; + diff --git a/src/client/models/UserCreate.ts b/src/client/models/UserCreate.ts index c101c25..99180e9 100644 --- a/src/client/models/UserCreate.ts +++ b/src/client/models/UserCreate.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { PassportImageSchema } from './PassportImageSchema'; import type { PayRateSchema } from './PayRateSchema'; export type UserCreate = { telegramId: number; @@ -16,6 +17,8 @@ export type UserCreate = { isDeleted: boolean; roleKey: string; payRate?: (PayRateSchema | null); + passportImageUrl?: (string | null); + passportImages?: (Array | null); positionKey?: (string | null); }; diff --git a/src/client/models/UserSchema.ts b/src/client/models/UserSchema.ts index 7753120..425f963 100644 --- a/src/client/models/UserSchema.ts +++ b/src/client/models/UserSchema.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { PassportImageSchema } from './PassportImageSchema'; import type { PayRateSchema } from './PayRateSchema'; import type { PositionSchema } from './PositionSchema'; import type { RoleSchema } from './RoleSchema'; @@ -18,6 +19,8 @@ export type UserSchema = { isDeleted: boolean; roleKey: string; payRate?: (PayRateSchema | null); + passportImageUrl?: (string | null); + passportImages?: (Array | null); id: number; role: RoleSchema; position?: (PositionSchema | null); diff --git a/src/client/models/UserUpdate.ts b/src/client/models/UserUpdate.ts index 4c66f90..89941bf 100644 --- a/src/client/models/UserUpdate.ts +++ b/src/client/models/UserUpdate.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { PassportImageSchema } from './PassportImageSchema'; import type { PayRateSchema } from './PayRateSchema'; export type UserUpdate = { telegramId: number; @@ -16,6 +17,8 @@ export type UserUpdate = { isDeleted: boolean; roleKey: string; payRate?: (PayRateSchema | null); + passportImageUrl?: (string | null); + passportImages?: (Array | null); id: number; positionKey?: (string | null); }; diff --git a/src/client/services/UserService.ts b/src/client/services/UserService.ts index 3339e4d..d42d134 100644 --- a/src/client/services/UserService.ts +++ b/src/client/services/UserService.ts @@ -2,12 +2,14 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { Body_upload_passport_image } from '../models/Body_upload_passport_image'; import type { CreateUserRequest } from '../models/CreateUserRequest'; import type { CreateUserResponse } from '../models/CreateUserResponse'; import type { GetAllUsersResponse } from '../models/GetAllUsersResponse'; import type { GetManagersResponse } from '../models/GetManagersResponse'; import type { UpdateUserRequest } from '../models/UpdateUserRequest'; import type { UpdateUserResponse } from '../models/UpdateUserResponse'; +import type { UploadPassportImageResponse } from '../models/UploadPassportImageResponse'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; @@ -74,4 +76,29 @@ export class UserService { url: '/user/get-managers', }); } + /** + * Upload Passport Image + * @returns UploadPassportImageResponse Successful Response + * @throws ApiError + */ + public static uploadPassportImage({ + userId, + formData, + }: { + userId: number, + formData: Body_upload_passport_image, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/user/passport-images/upload/{user_id}', + path: { + 'user_id': userId, + }, + formData: formData, + mediaType: 'multipart/form-data', + errors: { + 422: `Validation Error`, + }, + }); + } } diff --git a/src/components/ImageDropzone/ImageDropzone.tsx b/src/components/ImageDropzone/ImageDropzone.tsx index 3f34ce2..f6825e4 100644 --- a/src/components/ImageDropzone/ImageDropzone.tsx +++ b/src/components/ImageDropzone/ImageDropzone.tsx @@ -1,77 +1,34 @@ import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone"; -import { FC, useState } from "react"; -import { - Button, - Fieldset, - Flex, - Group, - Image, - Loader, - rem, - Text, -} from "@mantine/core"; +import { FC } from "react"; +import { Button, Fieldset, Flex, Group, Image, Loader, rem, Text } from "@mantine/core"; import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import { omit } from "lodash"; -import { BaseFormInputProps } from "../../types/utils.ts"; -import { notifications } from "../../shared/lib/notifications.ts"; -import { ProductService } from "../../client"; +import UseImageDropzone from "../../types/UseImageDropzone.tsx"; interface RestProps { - imageUrlInputProps?: BaseFormInputProps; - productId?: number; + imageDropzone: UseImageDropzone; + onDrop: (files: FileWithPath[]) => void; } type Props = Omit & RestProps; const ImageDropzone: FC = (props: Props) => { - const [showDropzone, setShowDropzone] = useState( - !( - typeof props.imageUrlInputProps?.value === "string" && - props.imageUrlInputProps.value.trim() !== "" - ) - ); - const [isLoading, setIsLoading] = useState(false); + const { + showDropzone, + setShowDropzone, + isLoading, + imageUrlInputProps, + } = props.imageDropzone; + const restProps = omit(props, [ "imageUrl", "productId", "imageUrlInputProps", ]); - const onDrop = (files: FileWithPath[]) => { - if (!props.productId || !props.imageUrlInputProps) return; - if (files.length > 1) { - notifications.error({ message: "Прикрепите одно изображение" }); - return; - } - const file = files[0]; - // TODO check if file is image - setIsLoading(true); - ProductService.uploadProductImage({ - productId: props.productId, - formData: { - upload_file: file, - }, - }) - .then(({ ok, message, imageUrl }) => { - notifications.guess(ok, { message }); - setIsLoading(false); - if (!ok || !imageUrl) { - setShowDropzone(true); - - return; - } - props.imageUrlInputProps?.onChange(imageUrl); - setShowDropzone(false); - }) - .catch(error => { - notifications.error({ message: error.toString() }); - setShowDropzone(true); - setIsLoading(false); - }); - }; const getBody = () => { - return props.imageUrlInputProps?.value && !showDropzone ? ( - + return imageUrlInputProps?.value && !showDropzone ? ( + ) : ( = (props: Props) => { "image/heic", ]} multiple={false} - onDrop={onDrop}> + onDrop={props.onDrop}> ; +} + +const useImageDropzone = ({ imageUrlInputProps }: Props) => { + const [showDropzone, setShowDropzone] = useState( + !( + typeof imageUrlInputProps?.value === "string" && + imageUrlInputProps.value.trim() !== "" + ), + ); + const [isLoading, setIsLoading] = useState(false); + + return { + showDropzone, + setShowDropzone, + isLoading, + setIsLoading, + imageUrlInputProps, + }; +}; + +export default useImageDropzone; diff --git a/src/pages/AdminPage/components/PassportImageDropzone/PassportImageDropzone.tsx b/src/pages/AdminPage/components/PassportImageDropzone/PassportImageDropzone.tsx new file mode 100644 index 0000000..925a1fa --- /dev/null +++ b/src/pages/AdminPage/components/PassportImageDropzone/PassportImageDropzone.tsx @@ -0,0 +1,60 @@ +import { DropzoneProps, FileWithPath } from "@mantine/dropzone"; +import { FC } from "react"; +import { notifications } from "../../../../shared/lib/notifications.ts"; +import { UserService } from "../../../../client"; +import { BaseFormInputProps } from "../../../../types/utils.ts"; +import useImageDropzone from "../../../../hooks/useImageDropzone.tsx"; +import ImageDropzone from "../../../../components/ImageDropzone/ImageDropzone.tsx"; + +interface RestProps { + imageUrlInputProps?: BaseFormInputProps; + userId?: number; +} + +type Props = Omit & RestProps; + +const ProductImageDropzone: FC = ({ imageUrlInputProps, userId }: Props) => { + const imageDropzoneProps = useImageDropzone({ + imageUrlInputProps, + }); + + const onDrop = (files: FileWithPath[]) => { + if (!userId || !imageUrlInputProps) return; + if (files.length > 1) { + notifications.error({ message: "Прикрепите одно изображение" }); + return; + } + const { setIsLoading, setShowDropzone } = imageDropzoneProps; + const file = files[0]; + + setIsLoading(true); + UserService.uploadPassportImage({ + userId: userId, + formData: { + upload_file: file, + }, + }) + .then(({ ok, message, imageUrl }) => { + notifications.guess(ok, { message }); + setIsLoading(false); + + if (!ok || !imageUrl) { + setShowDropzone(true); + return; + } + imageUrlInputProps?.onChange(imageUrl); + setShowDropzone(false); + }) + .catch(error => { + notifications.error({ message: error.toString() }); + setShowDropzone(true); + setIsLoading(false); + }); + }; + + return ( + + ); +}; + +export default ProductImageDropzone; diff --git a/src/pages/AdminPage/modals/UserFormModal/UserFormModal.tsx b/src/pages/AdminPage/modals/UserFormModal/UserFormModal.tsx index e64b213..974d264 100644 --- a/src/pages/AdminPage/modals/UserFormModal/UserFormModal.tsx +++ b/src/pages/AdminPage/modals/UserFormModal/UserFormModal.tsx @@ -10,6 +10,8 @@ import { capitalize } from "lodash"; import { IMaskInput } from "react-imask"; import phone from "phone"; import PayRateSelect from "../../../../components/Selects/PayRateSelect/PayRateSelect.tsx"; +import { BaseFormInputProps } from "../../../../types/utils.ts"; +import PassportImageDropzone from "../../components/PassportImageDropzone/PassportImageDropzone.tsx"; type Props = CreateEditFormProps; const UserFormModal = ({ @@ -107,6 +109,10 @@ const UserFormModal = ({ {...form.getInputProps("phoneNumber")} /> + + +
+ @@ -117,6 +123,18 @@ const UserFormModal = ({ {...form.getInputProps("passportData")} /> + { + isEditing && ( + + } + userId={innerProps?.element.id} + /> + ) + }
diff --git a/src/pages/ProductsPage/components/ProductImageDropzone/ProductImageDropzone.tsx b/src/pages/ProductsPage/components/ProductImageDropzone/ProductImageDropzone.tsx new file mode 100644 index 0000000..ce55631 --- /dev/null +++ b/src/pages/ProductsPage/components/ProductImageDropzone/ProductImageDropzone.tsx @@ -0,0 +1,60 @@ +import { DropzoneProps, FileWithPath } from "@mantine/dropzone"; +import { FC } from "react"; +import { notifications } from "../../../../shared/lib/notifications.ts"; +import { ProductService } from "../../../../client"; +import { BaseFormInputProps } from "../../../../types/utils.ts"; +import useImageDropzone from "../../../../hooks/useImageDropzone.tsx"; +import ImageDropzone from "../../../../components/ImageDropzone/ImageDropzone.tsx"; + +interface RestProps { + imageUrlInputProps?: BaseFormInputProps; + productId?: number; +} + +type Props = Omit & RestProps; + +const ProductImageDropzone: FC = ({ imageUrlInputProps, productId }: Props) => { + const imageDropzoneProps = useImageDropzone({ + imageUrlInputProps, + }); + + const onDrop = (files: FileWithPath[]) => { + if (!productId || !imageUrlInputProps) return; + if (files.length > 1) { + notifications.error({ message: "Прикрепите одно изображение" }); + return; + } + const { setIsLoading, setShowDropzone } = imageDropzoneProps; + const file = files[0]; + + setIsLoading(true); + ProductService.uploadProductImage({ + productId, + formData: { + upload_file: file, + }, + }) + .then(({ ok, message, imageUrl }) => { + notifications.guess(ok, { message }); + setIsLoading(false); + + if (!ok || !imageUrl) { + setShowDropzone(true); + return; + } + imageUrlInputProps?.onChange(imageUrl); + setShowDropzone(false); + }) + .catch(error => { + notifications.error({ message: error.toString() }); + setShowDropzone(true); + setIsLoading(false); + }); + }; + + return ( + + ); +}; + +export default ProductImageDropzone; diff --git a/src/pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx b/src/pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx index a04b359..95b495f 100644 --- a/src/pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx +++ b/src/pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx @@ -4,9 +4,9 @@ import { useForm } from "@mantine/form"; import { BaseProduct, CreateProductRequest } from "../../types.ts"; import { ProductSchema } from "../../../../client"; import BarcodeTemplateSelect from "../../../../components/Selects/BarcodeTemplateSelect/BarcodeTemplateSelect.tsx"; -import ImageDropzone from "../../../../components/ImageDropzone/ImageDropzone.tsx"; import { BaseFormInputProps } from "../../../../types/utils.ts"; import BarcodeImageDropzone from "../../../../components/BarcodeImageDropzone/BarcodeImageDropzone.tsx"; +import ProductImageDropzone from "../../components/ProductImageDropzone/ProductImageDropzone.tsx"; type CreateProps = { clientId: number; @@ -117,7 +117,7 @@ const CreateProductModal = ({ isEditProps && ( //
<> - >; + isLoading: boolean; + setIsLoading: Dispatch>; + imageUrlInputProps?: BaseFormInputProps; +} + +export default UseImageDropzone; From e92fcb5eb83ff58de09167081e7c385f1344397a Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 4 Dec 2024 21:09:36 +0400 Subject: [PATCH 5/5] fix: removed console log in work shifts table --- src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx index ae3edd0..8dfda0b 100644 --- a/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx +++ b/src/pages/AdminPage/tabs/WorkShifts/hooks/useWorkShiftsTable.tsx @@ -18,7 +18,6 @@ const useWorkShiftsTable = () => { itemsPerPage: 10, }) .then(res => { - console.log(res.shifts); setShifts(res.shifts); setTotalPages(res.paginationInfo.totalPages); })