feat: CRUD for product barcode images

This commit is contained in:
2024-11-01 17:25:06 +04:00
parent bc0c767a15
commit a3fdefb774
5 changed files with 310 additions and 26 deletions

View File

@@ -25,8 +25,10 @@ export type { BaseEnumListSchema } from './models/BaseEnumListSchema';
export type { BaseEnumSchema } from './models/BaseEnumSchema';
export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema';
export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouseSchema';
export type { BillPaymentInfo } from './models/BillPaymentInfo';
export type { BillPaymentStatus } from './models/BillPaymentStatus';
export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest';
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';
export type { CancelDealBillResponse } from './models/CancelDealBillResponse';
@@ -173,16 +175,19 @@ export type { ProductAddBarcodeRequest } from './models/ProductAddBarcodeRequest
export type { ProductAddBarcodeResponse } from './models/ProductAddBarcodeResponse';
export type { ProductCreateRequest } from './models/ProductCreateRequest';
export type { ProductCreateResponse } from './models/ProductCreateResponse';
export type { ProductDeleteBarcodeImageResponse } from './models/ProductDeleteBarcodeImageResponse';
export type { ProductDeleteRequest } from './models/ProductDeleteRequest';
export type { ProductDeleteResponse } from './models/ProductDeleteResponse';
export type { ProductExistsBarcodeResponse } from './models/ProductExistsBarcodeResponse';
export type { ProductGenerateBarcodeRequest } from './models/ProductGenerateBarcodeRequest';
export type { ProductGenerateBarcodeResponse } from './models/ProductGenerateBarcodeResponse';
export type { ProductGetBarcodeImageResponse } from './models/ProductGetBarcodeImageResponse';
export type { ProductGetResponse } from './models/ProductGetResponse';
export type { ProductImageSchema } from './models/ProductImageSchema';
export type { ProductSchema } from './models/ProductSchema';
export type { ProductUpdateRequest } from './models/ProductUpdateRequest';
export type { ProductUpdateResponse } from './models/ProductUpdateResponse';
export type { ProductUploadBarcodeImageResponse } from './models/ProductUploadBarcodeImageResponse';
export type { ProductUploadImageResponse } from './models/ProductUploadImageResponse';
export type { RoleSchema } from './models/RoleSchema';
export type { ServiceCategoryPriceSchema } from './models/ServiceCategoryPriceSchema';

View File

@@ -2,11 +2,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BillPaymentInfo } from './BillPaymentInfo';
import type { BillPaymentStatus } from './BillPaymentStatus';
import type { NotificationChannel } from './NotificationChannel';
export type BillStatusUpdateRequest = {
listenerTransactionId: number;
channel: NotificationChannel;
info: BillPaymentStatus;
info: (BillPaymentInfo | BillPaymentStatus);
};

View File

@@ -2,6 +2,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Body_upload_product_barcode_image } from '../models/Body_upload_product_barcode_image';
import type { Body_upload_product_image } from '../models/Body_upload_product_image';
import type { GetProductBarcodePdfRequest } from '../models/GetProductBarcodePdfRequest';
import type { GetProductBarcodePdfResponse } from '../models/GetProductBarcodePdfResponse';
@@ -11,15 +12,18 @@ import type { ProductAddBarcodeRequest } from '../models/ProductAddBarcodeReques
import type { ProductAddBarcodeResponse } from '../models/ProductAddBarcodeResponse';
import type { ProductCreateRequest } from '../models/ProductCreateRequest';
import type { ProductCreateResponse } from '../models/ProductCreateResponse';
import type { ProductDeleteBarcodeImageResponse } from '../models/ProductDeleteBarcodeImageResponse';
import type { ProductDeleteRequest } from '../models/ProductDeleteRequest';
import type { ProductDeleteResponse } from '../models/ProductDeleteResponse';
import type { ProductExistsBarcodeResponse } from '../models/ProductExistsBarcodeResponse';
import type { ProductGenerateBarcodeRequest } from '../models/ProductGenerateBarcodeRequest';
import type { ProductGenerateBarcodeResponse } from '../models/ProductGenerateBarcodeResponse';
import type { ProductGetBarcodeImageResponse } from '../models/ProductGetBarcodeImageResponse';
import type { ProductGetResponse } from '../models/ProductGetResponse';
import type { ProductSchema } from '../models/ProductSchema';
import type { ProductUpdateRequest } from '../models/ProductUpdateRequest';
import type { ProductUpdateResponse } from '../models/ProductUpdateResponse';
import type { ProductUploadBarcodeImageResponse } from '../models/ProductUploadBarcodeImageResponse';
import type { ProductUploadImageResponse } from '../models/ProductUploadImageResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
@@ -265,4 +269,71 @@ export class ProductService {
},
});
}
/**
* Upload Product Barcode Image
* @returns ProductUploadBarcodeImageResponse Successful Response
* @throws ApiError
*/
public static uploadProductBarcodeImage({
productId,
formData,
}: {
productId: number,
formData: Body_upload_product_barcode_image,
}): CancelablePromise<ProductUploadBarcodeImageResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/product/barcode/upload-image/{product_id}',
path: {
'product_id': productId,
},
formData: formData,
mediaType: 'multipart/form-data',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Product Barcode Image
* @returns ProductDeleteBarcodeImageResponse Successful Response
* @throws ApiError
*/
public static deleteProductBarcodeImage({
productId,
}: {
productId: number,
}): CancelablePromise<ProductDeleteBarcodeImageResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/product/barcode/delete-image/{product_id}',
path: {
'product_id': productId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Product Barcode Image
* @returns ProductGetBarcodeImageResponse Successful Response
* @throws ApiError
*/
public static getProductBarcodeImage({
productId,
}: {
productId: number,
}): CancelablePromise<ProductGetBarcodeImageResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/product/barcode/image/{product_id}',
path: {
'product_id': productId,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -0,0 +1,208 @@
import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
import { FC, useEffect, useState } from "react";
import { Button, Fieldset, Flex, Group, Loader, rem, Text } from "@mantine/core";
import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { omit } from "lodash";
import { notifications } from "../../shared/lib/notifications.ts";
import { ProductService } from "../../client";
// Barcode image aspects ratio should be equal 58/40
const BARCODE_IMAGE_RATIO = 1.45;
interface RestProps {
productId?: number;
}
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
const BarcodeImageDropzone: FC<Props> = (props: Props) => {
const [isLoading, setIsLoading] = useState(false);
const restProps = omit(props, [
"productId",
]);
const [barcodeImageUrl, setBarcodeImageUrl] = useState<string>();
useEffect(() => {
getBarcodeImage();
}, []);
const getBarcodeImage = () => {
if (!props.productId) return;
ProductService.getProductBarcodeImage({
productId: props.productId,
})
.then(res => {
if (res.barcodeImageUrl) {
setBarcodeImageUrl(res.barcodeImageUrl);
}
});
};
const showIncorrectImageSizeError = () => {
notifications.error({
message: "Изображение должно быть размером 58 х 40.",
});
};
const uploadImage = (productId: number, file: File) => {
ProductService.uploadProductBarcodeImage({
productId: productId,
formData: {
upload_file: file,
},
})
.then(({ ok, message, barcodeImageUrl }) => {
notifications.guess(ok, { message });
setIsLoading(false);
if (ok && barcodeImageUrl) {
setBarcodeImageUrl(barcodeImageUrl);
}
})
.catch(error => {
notifications.error({ message: error.toString() });
setIsLoading(false);
});
};
const uploadImageWrapper = (productId: number, file: File) => {
const reader = new FileReader();
reader.onloadend = () => {
const img = new Image();
img.src = reader.result as string;
img.onload = () => {
const ratio = img.width / img.height;
if (Math.abs(ratio - BARCODE_IMAGE_RATIO) > 0.01) {
showIncorrectImageSizeError();
setIsLoading(false);
return;
}
uploadImage(productId, file);
};
};
reader.readAsDataURL(file);
};
const onDrop = (files: FileWithPath[]) => {
if (!props.productId) return;
if (files.length > 1) {
notifications.error({ message: "Прикрепите одно изображение" });
return;
}
const file = files[0];
setIsLoading(true);
uploadImageWrapper(props.productId, file);
};
const deleteImage = () => {
if (!props.productId) return;
ProductService.deleteProductBarcodeImage({ productId: props.productId })
.then(({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
setBarcodeImageUrl("");
})
.catch(error => {
notifications.error({ message: error.toString() });
});
};
const getBody = () => {
return barcodeImageUrl ? (
<object style={{
aspectRatio: BARCODE_IMAGE_RATIO,
width: "240px",
}} data={barcodeImageUrl} />
) : (
<Dropzone
{...restProps}
accept={[
"image/png",
"image/jpeg",
"image/bmp",
"image/tiff",
"image/x-icon",
"image/webp",
"image/svg+xml",
"image/heic",
]}
multiple={false}
onDrop={onDrop}>
<Group
justify="center"
gap="xl"
style={{ pointerEvents: "none" }}>
<Dropzone.Accept>
<IconUpload
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-blue-6)",
}}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-red-6)",
}}
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-dimmed)",
}}
stroke={1.5}
/>
</Dropzone.Idle>
<div style={{ textAlign: "center" }}>
<Text
size="xl"
inline>
Перенесите изображение или нажмите чтоб выбрать файл
Изображение должно быть 58 х 40
</Text>
</div>
</Group>
</Dropzone>
);
};
return (
<Flex
gap={rem(10)}
direction={"column"}>
<Fieldset legend={"Штрихкод"}>
<Flex justify={"center"}>
{isLoading ? <Loader /> : getBody()}
</Flex>
</Fieldset>
{barcodeImageUrl && (
<>
<Button
onClick={() => setBarcodeImageUrl("")}
variant={"default"}>
Заменить штрихкод
</Button>
<Button
onClick={() => deleteImage()}
variant={"default"}>
Удалить штрихкод
</Button>
</>
)}
</Flex>
);
};
export default BarcodeImageDropzone;

View File

@@ -1,18 +1,12 @@
import { ContextModalProps } from "@mantine/modals";
import {
Button,
Fieldset,
Flex,
rem,
TagsInput,
TextInput,
} from "@mantine/core";
import { Button, Fieldset, Flex, rem, TagsInput, TextInput } from "@mantine/core";
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";
type CreateProps = {
clientId: number;
@@ -122,14 +116,19 @@ const CreateProductModal = ({
{
isEditProps && (
// <Fieldset legend={"Изображение"}>
<>
<ImageDropzone
imageUrlInputProps={
form.getInputProps(
"imageUrl"
"imageUrl",
) as BaseFormInputProps<string>
}
productId={innerProps.product.id}
/>
<BarcodeImageDropzone
productId={innerProps.product.id}
/>
</>
)
// </Fieldset>
}