feat: product barcode images

This commit is contained in:
2025-10-21 11:10:27 +04:00
parent 82f08b4f83
commit 4d5723bc72
15 changed files with 668 additions and 111 deletions

View File

@ -22,6 +22,7 @@ const ProductsActions: FC = () => {
onCreate: productsCrud.onCreate,
isEditing: false,
clientId: deal.client.id,
refetchProducts: dealProductsList.refetch,
},
});
};

View File

@ -29,6 +29,7 @@ const ProductViewActions: FC<Props> = ({ dealProduct }) => {
entity: dealProduct.product,
isEditing: true,
clientId: dealProduct.product.clientId,
refetchProducts: dealProductsList.refetch,
},
});
};

View File

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

View File

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

View File

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

View File

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

View File

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