feat: product barcode images
This commit is contained in:
@ -22,6 +22,7 @@ const ProductsActions: FC = () => {
|
||||
onCreate: productsCrud.onCreate,
|
||||
isEditing: false,
|
||||
clientId: deal.client.id,
|
||||
refetchProducts: dealProductsList.refetch,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -29,6 +29,7 @@ const ProductViewActions: FC<Props> = ({ dealProduct }) => {
|
||||
entity: dealProduct.product,
|
||||
isEditing: true,
|
||||
clientId: dealProduct.product.clientId,
|
||||
refetchProducts: dealProductsList.refetch,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1,182 @@
|
||||
import { FC } from "react";
|
||||
import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||
import { Fieldset, Flex, Group, Loader, rem, Text } from "@mantine/core";
|
||||
import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
|
||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||
import { deleteProductBarcodeImage, uploadProductBarcodeImage } from "@/lib/client";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
import useImageDropzone from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/components/ProductImageDropzone/useImageDropzone";
|
||||
import BaseFormInputProps from "@/utils/baseFormInputProps";
|
||||
|
||||
|
||||
// Barcode image aspects ratio should be equal 58/40
|
||||
const BARCODE_IMAGE_RATIO = 1.45;
|
||||
|
||||
interface RestProps {
|
||||
imageUrlInputProps?: BaseFormInputProps<string>;
|
||||
productId?: number;
|
||||
onUploaded?: () => void;
|
||||
}
|
||||
|
||||
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
|
||||
|
||||
const BarcodeImageDropzone: FC<Props> = ({
|
||||
productId,
|
||||
imageUrlInputProps,
|
||||
onUploaded,
|
||||
}: Props) => {
|
||||
const imageDropzoneProps = useImageDropzone({
|
||||
imageUrlInputProps,
|
||||
});
|
||||
|
||||
const onDeleteBarcodeImage = () => {
|
||||
if (!productId || !imageUrlInputProps) return;
|
||||
const { setIsLoading } = imageDropzoneProps;
|
||||
setIsLoading(true);
|
||||
|
||||
deleteProductBarcodeImage({
|
||||
path: {
|
||||
pk: productId,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
notifications.success({ message: data?.message });
|
||||
imageUrlInputProps.onChange("");
|
||||
setIsLoading(false);
|
||||
onUploaded && onUploaded();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
notifications.error({ message: err.toString() });
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
uploadProductBarcodeImage({
|
||||
path: {
|
||||
pk: productId,
|
||||
},
|
||||
body: {
|
||||
upload_file: file,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
notifications.success({ message: data?.message });
|
||||
setIsLoading(false);
|
||||
|
||||
if (!data?.imageUrl) {
|
||||
setShowDropzone(true);
|
||||
return;
|
||||
}
|
||||
imageUrlInputProps.onChange(data?.imageUrl);
|
||||
setShowDropzone(false);
|
||||
onUploaded && onUploaded();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
notifications.error({ message: err.toString() });
|
||||
setShowDropzone(true);
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getBody = () => {
|
||||
return imageUrlInputProps?.value ? (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<object
|
||||
style={{
|
||||
aspectRatio: BARCODE_IMAGE_RATIO,
|
||||
width: "240px",
|
||||
}}
|
||||
data={imageUrlInputProps?.value}
|
||||
/>
|
||||
) : (
|
||||
<Dropzone
|
||||
accept={["application/pdf"]}
|
||||
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>
|
||||
Перенесите или нажмите чтоб выбрать файл <br />
|
||||
Pdf-файл должен содержать 1 страницу размером 58 х
|
||||
40
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap={"xs"}
|
||||
direction={"column"}>
|
||||
<Fieldset legend={"Штрихкод"}>
|
||||
<Flex justify={"center"}>
|
||||
{imageDropzoneProps.isLoading ? <Loader /> : getBody()}
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
{imageUrlInputProps?.value && (
|
||||
<>
|
||||
<InlineButton
|
||||
onClick={() => imageUrlInputProps?.onChange("")}>
|
||||
Заменить штрихкод
|
||||
</InlineButton>
|
||||
<InlineButton onClick={onDeleteBarcodeImage}>
|
||||
Удалить штрихкод
|
||||
</InlineButton>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodeImageDropzone;
|
||||
@ -9,6 +9,7 @@ import useImageDropzone from "./useImageDropzone";
|
||||
interface RestProps {
|
||||
imageUrlInputProps?: BaseFormInputProps<string>;
|
||||
productId?: number;
|
||||
onUploaded?: () => void;
|
||||
}
|
||||
|
||||
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
|
||||
@ -16,6 +17,7 @@ type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
|
||||
const ProductImageDropzone: FC<Props> = ({
|
||||
imageUrlInputProps,
|
||||
productId,
|
||||
onUploaded,
|
||||
}: Props) => {
|
||||
const imageDropzoneProps = useImageDropzone({
|
||||
imageUrlInputProps,
|
||||
@ -34,7 +36,7 @@ const ProductImageDropzone: FC<Props> = ({
|
||||
|
||||
uploadProductImage({
|
||||
path: {
|
||||
productId,
|
||||
pk: productId,
|
||||
},
|
||||
body: {
|
||||
upload_file: file,
|
||||
@ -50,6 +52,7 @@ const ProductImageDropzone: FC<Props> = ({
|
||||
}
|
||||
imageUrlInputProps?.onChange(data?.imageUrl);
|
||||
setShowDropzone(false);
|
||||
onUploaded && onUploaded();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Fieldset, Flex, Stack, TagsInput, TextInput } from "@mantine/core";
|
||||
import { Flex } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BarcodeTemplateSelect from "@/app/products/components/shared/BarcodeTemplateSelect/BarcodeTemplateSelect";
|
||||
import {
|
||||
BarcodeTemplateSchema,
|
||||
CreateProductSchema,
|
||||
@ -14,11 +13,11 @@ import {
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
||||
import ProductImageDropzone from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductImageDropzone/ProductImageDropzone";
|
||||
import CharacteristicsTab from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/CharacteristicsTab";
|
||||
import ImagesTab from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/ImagesTab";
|
||||
import ProductEditorSegmentedControl, {
|
||||
ProductEditorTab,
|
||||
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/ProductEditorSegmentedControl";
|
||||
import BaseFormInputProps from "@/utils/baseFormInputProps";
|
||||
|
||||
type Props = CreateEditFormProps<
|
||||
CreateProductSchema,
|
||||
@ -26,6 +25,7 @@ type Props = CreateEditFormProps<
|
||||
ProductSchema
|
||||
> & {
|
||||
clientId: number;
|
||||
refetchProducts: () => void;
|
||||
};
|
||||
|
||||
type ProductForm = Partial<
|
||||
@ -64,92 +64,18 @@ const ProductEditorModal = ({
|
||||
initialValues,
|
||||
validate: {
|
||||
name: name =>
|
||||
!name || name.trim() !== ""
|
||||
? null
|
||||
: "Необходимо ввести название товара",
|
||||
(!name || name.trim() === "") &&
|
||||
"Необходимо ввести название товара",
|
||||
barcodeTemplate: barcodeTemplate =>
|
||||
!barcodeTemplate && "Необходимо выбрать шаблон штрихкода",
|
||||
},
|
||||
});
|
||||
|
||||
const characteristicsTab = (
|
||||
<>
|
||||
<Fieldset legend={"Основные характеристики"}>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
placeholder={"Введите название товара"}
|
||||
label={"Название товара"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите артикул"}
|
||||
label={"Артикул"}
|
||||
{...form.getInputProps("article")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите складской артикул"}
|
||||
label={"Складской артикул"}
|
||||
{...form.getInputProps("factoryArticle")}
|
||||
/>
|
||||
<BarcodeTemplateSelect
|
||||
placeholder={"Выберите шаблон штрихкода"}
|
||||
label={"Шаблон штрихкода"}
|
||||
{...form.getInputProps("barcodeTemplate")}
|
||||
onChange={template => {
|
||||
form.setFieldValue("barcodeTemplate", template);
|
||||
form.setFieldValue(
|
||||
"barcodeTemplateId",
|
||||
template?.id
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<TagsInput
|
||||
placeholder={
|
||||
!form.values.barcodes?.length
|
||||
? "Добавьте штрихкоды к товару"
|
||||
: ""
|
||||
}
|
||||
label={"Штрихкоды"}
|
||||
{...form.getInputProps("barcodes")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Дополнительные характеристики"}>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
placeholder={"Введите бренд"}
|
||||
label={"Бренд"}
|
||||
{...form.getInputProps("brand")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите состав"}
|
||||
label={"Состав"}
|
||||
{...form.getInputProps("composition")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите цвет"}
|
||||
label={"Цвет"}
|
||||
{...form.getInputProps("color")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите размер"}
|
||||
label={"Размер"}
|
||||
{...form.getInputProps("size")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите доп. информацию"}
|
||||
label={"Доп. информация"}
|
||||
{...form.getInputProps("additionalInfo")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
</>
|
||||
);
|
||||
|
||||
const imageTab = isEditing && (
|
||||
<ProductImageDropzone
|
||||
imageUrlInputProps={
|
||||
form.getInputProps("imageUrl") as BaseFormInputProps<string>
|
||||
}
|
||||
<ImagesTab
|
||||
form={form}
|
||||
productId={innerProps.entity.id}
|
||||
onUploaded={innerProps.refetchProducts}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -169,9 +95,11 @@ const ProductEditorModal = ({
|
||||
onChange={setEditorTab}
|
||||
/>
|
||||
)}
|
||||
{editorTab === ProductEditorTab.CHARACTERISTICS
|
||||
? characteristicsTab
|
||||
: imageTab}
|
||||
{editorTab === ProductEditorTab.CHARACTERISTICS ? (
|
||||
<CharacteristicsTab form={form} />
|
||||
) : (
|
||||
imageTab
|
||||
)}
|
||||
</Flex>
|
||||
</BaseFormModal>
|
||||
);
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
import { FC } from "react";
|
||||
import { Fieldset, Stack, TagsInput, TextInput } from "@mantine/core";
|
||||
import BarcodeTemplateSelect from "@/app/products/components/shared/BarcodeTemplateSelect/BarcodeTemplateSelect";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { ProductSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturnType<Partial<ProductSchema>>;
|
||||
}
|
||||
|
||||
const CharacteristicsTab: FC<Props> = ({ form }) => (
|
||||
<>
|
||||
<Fieldset legend={"Основные характеристики"}>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
placeholder={"Введите название товара"}
|
||||
label={"Название товара"}
|
||||
{...form.getInputProps("name")}
|
||||
withAsterisk
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите артикул"}
|
||||
label={"Артикул"}
|
||||
{...form.getInputProps("article")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите складской артикул"}
|
||||
label={"Складской артикул"}
|
||||
{...form.getInputProps("factoryArticle")}
|
||||
/>
|
||||
<BarcodeTemplateSelect
|
||||
placeholder={"Выберите шаблон штрихкода"}
|
||||
label={"Шаблон штрихкода"}
|
||||
{...form.getInputProps("barcodeTemplate")}
|
||||
onChange={template => {
|
||||
form.setFieldValue("barcodeTemplate", template);
|
||||
form.setFieldValue(
|
||||
"barcodeTemplateId",
|
||||
template?.id
|
||||
);
|
||||
}}
|
||||
withAsterisk
|
||||
/>
|
||||
<TagsInput
|
||||
placeholder={
|
||||
!form.values.barcodes?.length
|
||||
? "Добавьте штрихкоды к товару"
|
||||
: ""
|
||||
}
|
||||
label={"Штрихкоды"}
|
||||
{...form.getInputProps("barcodes")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Дополнительные характеристики"}>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
placeholder={"Введите бренд"}
|
||||
label={"Бренд"}
|
||||
{...form.getInputProps("brand")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите состав"}
|
||||
label={"Состав"}
|
||||
{...form.getInputProps("composition")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите цвет"}
|
||||
label={"Цвет"}
|
||||
{...form.getInputProps("color")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите размер"}
|
||||
label={"Размер"}
|
||||
{...form.getInputProps("size")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите доп. информацию"}
|
||||
label={"Доп. информация"}
|
||||
{...form.getInputProps("additionalInfo")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
</>
|
||||
)
|
||||
|
||||
export default CharacteristicsTab;
|
||||
@ -0,0 +1,36 @@
|
||||
import { FC } from "react";
|
||||
import { Stack } from "@mantine/core";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { ProductSchema } from "@/lib/client";
|
||||
import BarcodeImageDropzone from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/components/BarcodeImageDropzone/BarcodeImageDropzone";
|
||||
import ProductImageDropzone from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/components/ProductImageDropzone/ProductImageDropzone";
|
||||
import BaseFormInputProps from "@/utils/baseFormInputProps";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturnType<Partial<ProductSchema>>;
|
||||
productId: number;
|
||||
onUploaded: () => void;
|
||||
};
|
||||
|
||||
const ImagesTab: FC<Props> = ({ form, productId, onUploaded }) => (
|
||||
<Stack>
|
||||
<ProductImageDropzone
|
||||
imageUrlInputProps={
|
||||
form.getInputProps("imageUrl") as BaseFormInputProps<string>
|
||||
}
|
||||
productId={productId}
|
||||
onUploaded={onUploaded}
|
||||
/>
|
||||
<BarcodeImageDropzone
|
||||
imageUrlInputProps={
|
||||
form.getInputProps(
|
||||
"barcodeImageUrl"
|
||||
) as BaseFormInputProps<string>
|
||||
}
|
||||
productId={productId}
|
||||
onUploaded={onUploaded}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
export default ImagesTab;
|
||||
Reference in New Issue
Block a user