Merge remote-tracking branch 'origin/master'

This commit is contained in:
2024-12-08 23:23:50 +03:00
35 changed files with 921 additions and 311 deletions

View File

@@ -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';
@@ -30,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';
@@ -151,6 +150,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';
@@ -187,12 +188,14 @@ 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';
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';
@@ -246,6 +249,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';
@@ -271,10 +276,13 @@ 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';
export type { ValidationError } from './models/ValidationError';
export type { WorkShiftRowSchema } from './models/WorkShiftRowSchema';
export type { WorkShiftSchema } from './models/WorkShiftSchema';
export { AuthService } from './services/AuthService';
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,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;
};

View File

@@ -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;
};

View File

@@ -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;
};

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 { WorkShiftRowSchema } from './WorkShiftRowSchema';
export type GetWorkShiftsResponse = {
shifts: Array<WorkShiftRowSchema>;
paginationInfo: PaginationInfoSchema;
};

View File

@@ -3,7 +3,7 @@
/* tslint:disable */
/* eslint-disable */
export type PaginationInfoSchema = {
totalPages?: number;
totalItems?: number;
totalPages: number;
totalItems: number;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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);
};

View File

@@ -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<PassportImageSchema> | null);
positionKey?: (string | null);
};

View File

@@ -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<PassportImageSchema> | null);
id: number;
role: RoleSchema;
position?: (PositionSchema | null);

View File

@@ -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<PassportImageSchema> | null);
id: number;
positionKey?: (string | null);
};

View File

@@ -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);
};

View File

@@ -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);
isPaused?: (boolean | null);
user: UserSchema;
};

View File

@@ -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<UploadPassportImageResponse> {
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`,
},
});
}
}

View File

@@ -2,10 +2,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ActiveWorkShiftsResponse } from '../models/ActiveWorkShiftsResponse';
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';
@@ -96,14 +100,32 @@ export class WorkShiftsService {
});
}
/**
* Get Active Shifts
* @returns ActiveWorkShiftsResponse Successful Response
* Get Shifts
* @returns GetWorkShiftsResponse Successful Response
* @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, {
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`,
},
});
}
/**
@@ -127,4 +149,88 @@ export class WorkShiftsService {
},
});
}
/**
* Start Pause By Shift Id
* @returns StartPauseByShiftIdResponse Successful Response
* @throws ApiError
*/
public static startPauseByShiftId({
shiftId,
}: {
shiftId: number,
}): CancelablePromise<StartPauseByShiftIdResponse> {
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<StartPauseByUserIdResponse> {
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<FinishPauseByShiftIdResponse> {
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<FinishPauseByUserIdResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/work-shifts/pause/finish/for-user/{shift_id}',
query: {
'user_id': userId,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -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<string>;
productId?: number;
imageDropzone: UseImageDropzone;
onDrop: (files: FileWithPath[]) => void;
}
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
const ImageDropzone: FC<Props> = (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 ? (
<Image src={props.imageUrlInputProps.value} />
return imageUrlInputProps?.value && !showDropzone ? (
<Image src={imageUrlInputProps.value} />
) : (
<Dropzone
{...restProps}
@@ -87,7 +44,7 @@ const ImageDropzone: FC<Props> = (props: Props) => {
"image/heic",
]}
multiple={false}
onDrop={onDrop}>
onDrop={props.onDrop}>
<Group
justify="center"
gap="xl"

View File

@@ -0,0 +1,26 @@
import { useState } from "react";
import { BaseFormInputProps } from "../types/utils.ts";
type Props = {
imageUrlInputProps?: BaseFormInputProps<string>;
}
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;

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,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<string>;
userId?: number;
}
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
const ProductImageDropzone: FC<Props> = ({ 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 (
<ImageDropzone onDrop={onDrop} imageDropzone={imageDropzoneProps} />
);
};
export default ProductImageDropzone;

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

@@ -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<UserSchema>;
const UserFormModal = ({
@@ -107,6 +109,10 @@ const UserFormModal = ({
{...form.getInputProps("phoneNumber")}
/>
</Input.Wrapper>
</Stack>
</Fieldset>
<Fieldset legend={"Паспортные данные"}>
<Stack>
<Input.Wrapper
label={"Серия и номер паспорта"}
error={form.getInputProps("passportData").error}>
@@ -117,6 +123,18 @@ const UserFormModal = ({
{...form.getInputProps("passportData")}
/>
</Input.Wrapper>
{
isEditing && (
<PassportImageDropzone
imageUrlInputProps={
form.getInputProps(
"passportImageUrl",
) as BaseFormInputProps<string>
}
userId={innerProps?.element.id}
/>
)
}
</Stack>
</Fieldset>
<Fieldset legend={"Роль и должность"}>

View File

@@ -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<ActiveWorkShiftSchema[]>([]);
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 (
<Stack>
<Group ml={"xs"} mt={"xs"}>
<Button variant={"default"} onClick={onShiftStart}>
Начать смену
</Button>
<Button variant={"default"} onClick={onShiftFinish}>
Закончить смену
</Button>
</Group>
<ActiveShiftsTable
activeShifts={activeShifts}
fetchActiveShifts={fetchActiveShifts}
<Stack gap={0}>
<WorkShiftInput fetchShifts={fetchShifts} />
<Divider />
<ShiftsTableSegmentedControl
value={shiftsTableType.toString()}
onChange={event => {
setPage(1);
setShiftsTableType(parseInt(event));
}}
/>
<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>
);
};

View File

@@ -0,0 +1,175 @@
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
import { useShiftsTableColumns } from "../hooks/columns.tsx";
import { ActionIcon, Flex, Text, Tooltip } from "@mantine/core";
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_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: WorkShiftRowSchema[];
fetchShifts: () => void;
shiftsTableType: ShiftsTableType;
}
export const ShiftsTable = ({
shifts,
fetchShifts,
shiftsTableType,
}: Props) => {
const isActiveShiftsTable = shiftsTableType === ShiftsTableType.ACTIVE;
const columns = useShiftsTableColumns({ isActiveShiftsTable });
const onDelete = (workShiftRow: WorkShiftRowSchema) => {
WorkShiftsService.deleteWorkShift({
shiftId: workShiftRow.workShift.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchShifts();
})
.catch(err => console.log(err));
};
const onDeleteClick = (workShiftRow: WorkShiftRowSchema) => {
modals.openConfirmModal({
title: "Удаление смены",
children: (
<Text size="sm">
Вы уверены что хотите удалить смену работника{" "}
{workShiftRow.workShift.user.firstName} {workShiftRow.workShift.user.secondName} от{" "}
{formatDate(workShiftRow.workShift.startedAt)}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onDelete(workShiftRow),
});
};
const onShiftFinish = (workShiftRow: WorkShiftRowSchema) => {
WorkShiftsService.finishWorkShiftById({
shiftId: workShiftRow.workShift.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchShifts();
})
.catch(err => console.log(err));
};
const onShiftFinishClick = (workShiftRow: WorkShiftRowSchema) => {
modals.openConfirmModal({
title: "Завершение смены",
children: (
<Text size="sm">
Вы уверены что хотите завершить смену работника{" "}
{workShiftRow.workShift.user.firstName} {workShiftRow.workShift.user.secondName} от{" "}
{formatDate(workShiftRow.workShift.startedAt)}
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
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 (
<Tooltip label={label}>
<ActionIcon
onClick={func}
variant={"default"}>
{icon}
</ActionIcon>
</Tooltip>
);
};
const getRowActions = (row: MRT_Row<WorkShiftRowSchema>) => {
const actions = [
getAction("Удалить", () => onDeleteClick(row.original), <IconTrash />),
];
if (isActiveShiftsTable) {
actions.push(
getAction(
"Завершить смену",
() => onShiftFinishClick(row.original),
<IconCheck />,
),
);
if (row.original.workShift.isPaused) {
actions.push(
getAction(
"Продолжить смену",
() => onShiftResumeClick(row.original),
<IconPlayerPlay />,
),
);
} else {
actions.push(
getAction(
"Поставить смену на паузу",
() => onShiftPauseClick(row.original),
<IconPlayerPause />,
),
);
}
}
return actions;
};
return (
<BaseTable
data={shifts}
columns={columns}
restProps={
{
enableRowActions: true,
enableSorting: false,
enableColumnActions: false,
renderRowActions: ({ row }) => {
return (
<Flex gap="md">
{...getRowActions(row)}
</Flex>
);
},
} as MRT_TableOptions<WorkShiftRowSchema>
}
/>
);
};

View File

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

View File

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

View File

@@ -0,0 +1,81 @@
import { WorkShiftsService } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { modals } from "@mantine/modals";
type Props = {
fetchShifts: () => void;
}
enum InputType {
START_SHIFT,
FINISH_SHIFT,
RESUME_SHIFT,
PAUSE_SHIFT,
}
const useWorkShiftInput = ({ fetchShifts }: Props) => {
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);
if (isNaN(userId)) {
notifications.error({ message: "Ошибка, некорректные данные в QR-коде" });
return;
}
workShiftMethods[inputType]({ 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 = InputType.START_SHIFT;
onScanningStart();
};
const onShiftFinish = () => {
inputType = InputType.FINISH_SHIFT;
onScanningStart();
};
const onShiftResume = () => {
inputType = InputType.RESUME_SHIFT;
onScanningStart();
};
const onShiftPause = () => {
inputType = InputType.PAUSE_SHIFT;
onScanningStart();
};
return {
onShiftStart,
onShiftFinish,
onShiftResume,
onShiftPause,
};
};
export default useWorkShiftInput;

View File

@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
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<WorkShiftRowSchema[]>([]);
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;

View File

@@ -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<string>;
productId?: number;
}
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
const ProductImageDropzone: FC<Props> = ({ 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 (
<ImageDropzone onDrop={onDrop} imageDropzone={imageDropzoneProps} />
);
};
export default ProductImageDropzone;

View File

@@ -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 && (
// <Fieldset legend={"Изображение"}>
<>
<ImageDropzone
<ProductImageDropzone
imageUrlInputProps={
form.getInputProps(
"imageUrl",

View File

@@ -0,0 +1,12 @@
import { Dispatch, SetStateAction } from "react";
import { BaseFormInputProps } from "./utils.ts";
type UseImageDropzone = {
showDropzone: boolean;
setShowDropzone: Dispatch<SetStateAction<boolean>>;
isLoading: boolean;
setIsLoading: Dispatch<SetStateAction<boolean>>;
imageUrlInputProps?: BaseFormInputProps<string>;
}
export default UseImageDropzone;

View File

@@ -45,7 +45,7 @@ export function ObjectStateToTableProps<T extends MRT_RowData>(
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];
};