From 2cb62a4e0bec9025db89eaa04138d33a6d666f6f Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 4 Dec 2024 20:21:01 +0400 Subject: [PATCH] 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;