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

@ -4,7 +4,10 @@ const path = "src/lib/client/zod.gen.ts";
let content = fs.readFileSync(path, "utf8");
// Replace only for the upload schema
content = content.replace("upload_file: z.string", "upload_file: z.any");
const target = "upload_file: z.string";
while (content.includes(target)) {
content = content.replace(target, "upload_file: z.any");
}
fs.writeFileSync(path, content);
console.log("✅ Fixed zod schema for upload_file");

View File

@ -18,6 +18,7 @@ export type ProductsFiltersForm = {
type ProductsContextState = {
productsFiltersForm: UseFormReturnType<ProductsFiltersForm>;
refetch: () => void;
products: ProductSchema[];
productsCrud: ProductsCrud;
paginationInfo?: PaginationInfoSchema;
@ -37,7 +38,7 @@ const useProductsContextState = (): ProductsContextState => {
500
);
const { products, paginationInfo, queryKey } = useProductsList({
const { products, paginationInfo, queryKey, refetch } = useProductsList({
clientId: productsFiltersForm.values.client?.id,
searchInput: debouncedSearchInput,
page: productsFiltersForm.values.page,
@ -47,6 +48,7 @@ const useProductsContextState = (): ProductsContextState => {
return {
productsFiltersForm,
refetch,
products,
productsCrud,
paginationInfo,

View File

@ -4,7 +4,7 @@ import { ProductSchema } from "@/lib/client";
import { notifications } from "@/lib/notifications";
const useProductsActions = () => {
const { productsCrud, productsFiltersForm } = useProductsContext();
const { productsCrud, productsFiltersForm, refetch } = useProductsContext();
const onCreateClick = () => {
if (!productsFiltersForm.values.client) {
@ -19,6 +19,7 @@ const useProductsActions = () => {
innerProps: {
onCreate: productsCrud.onCreate,
clientId: productsFiltersForm.values.client.id,
refetchProducts: refetch,
isEditing: false,
},
});
@ -33,6 +34,7 @@ const useProductsActions = () => {
onChange: updated => productsCrud.onUpdate(product.id, updated),
clientId: product.clientId,
entity: product,
refetchProducts: refetch,
isEditing: true,
},
});

View File

@ -39,6 +39,7 @@ import {
deleteDealTag,
deleteMarketplace,
deleteProduct,
deleteProductBarcodeImage,
deleteProject,
deleteService,
deleteServiceCategory,
@ -84,6 +85,7 @@ import {
updateServiceCategory,
updateServicesKit,
updateStatus,
uploadProductBarcodeImage,
uploadProductImage,
type Options,
} from "../sdk.gen";
@ -172,6 +174,9 @@ import type {
DeleteMarketplaceData,
DeleteMarketplaceError,
DeleteMarketplaceResponse2,
DeleteProductBarcodeImageData,
DeleteProductBarcodeImageError,
DeleteProductBarcodeImageResponse,
DeleteProductData,
DeleteProductError,
DeleteProductResponse2,
@ -275,6 +280,9 @@ import type {
UpdateStatusData,
UpdateStatusError,
UpdateStatusResponse2,
UploadProductBarcodeImageData,
UploadProductBarcodeImageError,
UploadProductBarcodeImageResponse,
UploadProductImageData,
UploadProductImageError,
UploadProductImageResponse,
@ -2292,6 +2300,84 @@ export const getProductBarcodePdfMutation = (
return mutationOptions;
};
export const uploadProductBarcodeImageQueryKey = (
options: Options<UploadProductBarcodeImageData>
) => createQueryKey("uploadProductBarcodeImage", options);
/**
* Upload Product Barcode Image
*/
export const uploadProductBarcodeImageOptions = (
options: Options<UploadProductBarcodeImageData>
) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await uploadProductBarcodeImage({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: uploadProductBarcodeImageQueryKey(options),
});
};
/**
* Upload Product Barcode Image
*/
export const uploadProductBarcodeImageMutation = (
options?: Partial<Options<UploadProductBarcodeImageData>>
): UseMutationOptions<
UploadProductBarcodeImageResponse,
AxiosError<UploadProductBarcodeImageError>,
Options<UploadProductBarcodeImageData>
> => {
const mutationOptions: UseMutationOptions<
UploadProductBarcodeImageResponse,
AxiosError<UploadProductBarcodeImageError>,
Options<UploadProductBarcodeImageData>
> = {
mutationFn: async localOptions => {
const { data } = await uploadProductBarcodeImage({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
/**
* Delete Product Barcode Image
*/
export const deleteProductBarcodeImageMutation = (
options?: Partial<Options<DeleteProductBarcodeImageData>>
): UseMutationOptions<
DeleteProductBarcodeImageResponse,
AxiosError<DeleteProductBarcodeImageError>,
Options<DeleteProductBarcodeImageData>
> => {
const mutationOptions: UseMutationOptions<
DeleteProductBarcodeImageResponse,
AxiosError<DeleteProductBarcodeImageError>,
Options<DeleteProductBarcodeImageData>
> = {
mutationFn: async localOptions => {
const { data } = await deleteProductBarcodeImage({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
export const getServicesQueryKey = (options?: Options<GetServicesData>) =>
createQueryKey("getServices", options);

View File

@ -92,6 +92,9 @@ import type {
DeleteMarketplaceData,
DeleteMarketplaceErrors,
DeleteMarketplaceResponses,
DeleteProductBarcodeImageData,
DeleteProductBarcodeImageErrors,
DeleteProductBarcodeImageResponses,
DeleteProductData,
DeleteProductErrors,
DeleteProductResponses,
@ -220,6 +223,9 @@ import type {
UpdateStatusData,
UpdateStatusErrors,
UpdateStatusResponses,
UploadProductBarcodeImageData,
UploadProductBarcodeImageErrors,
UploadProductBarcodeImageResponses,
UploadProductImageData,
UploadProductImageErrors,
UploadProductImageResponses,
@ -281,6 +287,8 @@ import {
zDeleteDealTagResponse2,
zDeleteMarketplaceData,
zDeleteMarketplaceResponse2,
zDeleteProductBarcodeImageData,
zDeleteProductBarcodeImageResponse,
zDeleteProductData,
zDeleteProductResponse2,
zDeleteProjectData,
@ -373,6 +381,8 @@ import {
zUpdateServicesKitResponse2,
zUpdateStatusData,
zUpdateStatusResponse2,
zUploadProductBarcodeImageData,
zUploadProductBarcodeImageResponse,
zUploadProductImageData,
zUploadProductImageResponse,
} from "./zod.gen";
@ -1719,7 +1729,7 @@ export const uploadProductImage = <ThrowOnError extends boolean = false>(
responseValidator: async data => {
return await zUploadProductImageResponse.parseAsync(data);
},
url: "/crm/v1/fulfillment-base/product/images/upload/{productId}",
url: "/crm/v1/fulfillment-base/product{pk}/images/upload",
...options,
headers: {
"Content-Type": null,
@ -1755,6 +1765,57 @@ export const getProductBarcodePdf = <ThrowOnError extends boolean = false>(
});
};
/**
* Upload Product Barcode Image
*/
export const uploadProductBarcodeImage = <ThrowOnError extends boolean = false>(
options: Options<UploadProductBarcodeImageData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
UploadProductBarcodeImageResponses,
UploadProductBarcodeImageErrors,
ThrowOnError
>({
...formDataBodySerializer,
requestValidator: async data => {
return await zUploadProductBarcodeImageData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zUploadProductBarcodeImageResponse.parseAsync(data);
},
url: "/crm/v1/fulfillment-base/product{pk}/barcode/image/upload",
...options,
headers: {
"Content-Type": null,
...options.headers,
},
});
};
/**
* Delete Product Barcode Image
*/
export const deleteProductBarcodeImage = <ThrowOnError extends boolean = false>(
options: Options<DeleteProductBarcodeImageData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).delete<
DeleteProductBarcodeImageResponses,
DeleteProductBarcodeImageErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zDeleteProductBarcodeImageData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zDeleteProductBarcodeImageResponse.parseAsync(data);
},
url: "/crm/v1/fulfillment-base/product{pk}/barcode/image",
...options,
});
};
/**
* Get Services
*/

View File

@ -63,6 +63,20 @@ export type BarcodeTemplateSizeSchema = {
height: number;
};
/**
* BarcodeUploadImageResponse
*/
export type BarcodeUploadImageResponse = {
/**
* Message
*/
message: string;
/**
* Imageurl
*/
imageUrl?: string | null;
};
/**
* BaseMarketplaceSchema
*/
@ -103,6 +117,16 @@ export type BoardSchema = {
projectId: number;
};
/**
* Body_upload_product_barcode_image
*/
export type BodyUploadProductBarcodeImage = {
/**
* Upload File
*/
upload_file: Blob | File;
};
/**
* Body_upload_product_image
*/
@ -639,15 +663,7 @@ export type CreateProductSchema = {
/**
* Barcodes
*/
barcodes: Array<string>;
/**
* Imageurl
*/
imageUrl?: string | null;
/**
* Images
*/
images?: Array<ProductImageSchema> | null;
barcodes?: Array<string>;
};
/**
@ -1090,6 +1106,16 @@ export type DealTagSchema = {
tagColor: DealTagColorSchema;
};
/**
* DeleteBarcodeImageResponse
*/
export type DeleteBarcodeImageResponse = {
/**
* Message
*/
message: string;
};
/**
* DeleteBarcodeTemplateResponse
*/
@ -1538,6 +1564,20 @@ export type PaginationInfoSchema = {
totalItems: number;
};
/**
* ProductBarcodeImageSchema
*/
export type ProductBarcodeImageSchema = {
/**
* Productid
*/
productId: number;
/**
* Imageurl
*/
imageUrl: string;
};
/**
* ProductImageSchema
*/
@ -1603,7 +1643,7 @@ export type ProductSchema = {
/**
* Barcodes
*/
barcodes: Array<string>;
barcodes?: Array<string>;
/**
* Imageurl
*/
@ -1612,6 +1652,11 @@ export type ProductSchema = {
* Images
*/
images?: Array<ProductImageSchema> | null;
/**
* Barcodeimageurl
*/
barcodeImageUrl?: string | null;
barcodeImage?: ProductBarcodeImageSchema | null;
/**
* Id
*/
@ -4073,12 +4118,12 @@ export type UploadProductImageData = {
body: BodyUploadProductImage;
path: {
/**
* Productid
* Pk
*/
productId: number;
pk: number;
};
query?: never;
url: "/crm/v1/fulfillment-base/product/images/upload/{productId}";
url: "/crm/v1/fulfillment-base/product{pk}/images/upload";
};
export type UploadProductImageErrors = {
@ -4128,6 +4173,70 @@ export type GetProductBarcodePdfResponses = {
export type GetProductBarcodePdfResponse2 =
GetProductBarcodePdfResponses[keyof GetProductBarcodePdfResponses];
export type UploadProductBarcodeImageData = {
body: BodyUploadProductBarcodeImage;
path: {
/**
* Pk
*/
pk: number;
};
query?: never;
url: "/crm/v1/fulfillment-base/product{pk}/barcode/image/upload";
};
export type UploadProductBarcodeImageErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UploadProductBarcodeImageError =
UploadProductBarcodeImageErrors[keyof UploadProductBarcodeImageErrors];
export type UploadProductBarcodeImageResponses = {
/**
* Successful Response
*/
200: BarcodeUploadImageResponse;
};
export type UploadProductBarcodeImageResponse =
UploadProductBarcodeImageResponses[keyof UploadProductBarcodeImageResponses];
export type DeleteProductBarcodeImageData = {
body?: never;
path: {
/**
* Pk
*/
pk: number;
};
query?: never;
url: "/crm/v1/fulfillment-base/product{pk}/barcode/image";
};
export type DeleteProductBarcodeImageErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DeleteProductBarcodeImageError =
DeleteProductBarcodeImageErrors[keyof DeleteProductBarcodeImageErrors];
export type DeleteProductBarcodeImageResponses = {
/**
* Successful Response
*/
200: DeleteBarcodeImageResponse;
};
export type DeleteProductBarcodeImageResponse =
DeleteProductBarcodeImageResponses[keyof DeleteProductBarcodeImageResponses];
export type GetServicesData = {
body?: never;
path?: never;

View File

@ -32,6 +32,14 @@ export const zBarcodeTemplateSchema = z.object({
id: z.int(),
});
/**
* BarcodeUploadImageResponse
*/
export const zBarcodeUploadImageResponse = z.object({
message: z.string(),
imageUrl: z.optional(z.union([z.string(), z.null()])),
});
/**
* BaseMarketplaceSchema
*/
@ -51,6 +59,13 @@ export const zBoardSchema = z.object({
projectId: z.int(),
});
/**
* Body_upload_product_barcode_image
*/
export const zBodyUploadProductBarcodeImage = z.object({
upload_file: z.any(),
});
/**
* Body_upload_product_image
*/
@ -250,6 +265,14 @@ export const zProductImageSchema = z.object({
imageUrl: z.string(),
});
/**
* ProductBarcodeImageSchema
*/
export const zProductBarcodeImageSchema = z.object({
productId: z.int(),
imageUrl: z.string(),
});
/**
* ProductSchema
*/
@ -264,9 +287,11 @@ export const zProductSchema = z.object({
composition: z.union([z.string(), z.null()]),
size: z.union([z.string(), z.null()]),
additionalInfo: z.union([z.string(), z.null()]),
barcodes: z.array(z.string()),
barcodes: z.optional(z.array(z.string())).default([]),
imageUrl: z.optional(z.union([z.string(), z.null()])),
images: z.optional(z.union([z.array(zProductImageSchema), z.null()])),
barcodeImageUrl: z.optional(z.union([z.string(), z.null()])),
barcodeImage: z.optional(z.union([zProductBarcodeImageSchema, z.null()])),
id: z.int(),
barcodeTemplate: zBarcodeTemplateSchema,
});
@ -526,9 +551,7 @@ export const zCreateProductSchema = z.object({
composition: z.union([z.string(), z.null()]),
size: z.union([z.string(), z.null()]),
additionalInfo: z.union([z.string(), z.null()]),
barcodes: z.array(z.string()),
imageUrl: z.optional(z.union([z.string(), z.null()])),
images: z.optional(z.union([z.array(zProductImageSchema), z.null()])),
barcodes: z.optional(z.array(z.string())).default([]),
});
/**
@ -746,6 +769,13 @@ export const zDealProductAddKitResponse = z.object({
message: z.string(),
});
/**
* DeleteBarcodeImageResponse
*/
export const zDeleteBarcodeImageResponse = z.object({
message: z.string(),
});
/**
* DeleteBarcodeTemplateResponse
*/
@ -2161,7 +2191,7 @@ export const zUpdateProductResponse2 = zUpdateProductResponse;
export const zUploadProductImageData = z.object({
body: zBodyUploadProductImage,
path: z.object({
productId: z.int(),
pk: z.int(),
}),
query: z.optional(z.never()),
});
@ -2182,6 +2212,32 @@ export const zGetProductBarcodePdfData = z.object({
*/
export const zGetProductBarcodePdfResponse2 = zGetProductBarcodePdfResponse;
export const zUploadProductBarcodeImageData = z.object({
body: zBodyUploadProductBarcodeImage,
path: z.object({
pk: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zUploadProductBarcodeImageResponse = zBarcodeUploadImageResponse;
export const zDeleteProductBarcodeImageData = z.object({
body: z.optional(z.never()),
path: z.object({
pk: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zDeleteProductBarcodeImageResponse = zDeleteBarcodeImageResponse;
export const zGetServicesData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),

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;