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"); let content = fs.readFileSync(path, "utf8");
// Replace only for the upload schema // 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); fs.writeFileSync(path, content);
console.log("✅ Fixed zod schema for upload_file"); console.log("✅ Fixed zod schema for upload_file");

View File

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

View File

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

View File

@ -39,6 +39,7 @@ import {
deleteDealTag, deleteDealTag,
deleteMarketplace, deleteMarketplace,
deleteProduct, deleteProduct,
deleteProductBarcodeImage,
deleteProject, deleteProject,
deleteService, deleteService,
deleteServiceCategory, deleteServiceCategory,
@ -84,6 +85,7 @@ import {
updateServiceCategory, updateServiceCategory,
updateServicesKit, updateServicesKit,
updateStatus, updateStatus,
uploadProductBarcodeImage,
uploadProductImage, uploadProductImage,
type Options, type Options,
} from "../sdk.gen"; } from "../sdk.gen";
@ -172,6 +174,9 @@ import type {
DeleteMarketplaceData, DeleteMarketplaceData,
DeleteMarketplaceError, DeleteMarketplaceError,
DeleteMarketplaceResponse2, DeleteMarketplaceResponse2,
DeleteProductBarcodeImageData,
DeleteProductBarcodeImageError,
DeleteProductBarcodeImageResponse,
DeleteProductData, DeleteProductData,
DeleteProductError, DeleteProductError,
DeleteProductResponse2, DeleteProductResponse2,
@ -275,6 +280,9 @@ import type {
UpdateStatusData, UpdateStatusData,
UpdateStatusError, UpdateStatusError,
UpdateStatusResponse2, UpdateStatusResponse2,
UploadProductBarcodeImageData,
UploadProductBarcodeImageError,
UploadProductBarcodeImageResponse,
UploadProductImageData, UploadProductImageData,
UploadProductImageError, UploadProductImageError,
UploadProductImageResponse, UploadProductImageResponse,
@ -2292,6 +2300,84 @@ export const getProductBarcodePdfMutation = (
return mutationOptions; 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>) => export const getServicesQueryKey = (options?: Options<GetServicesData>) =>
createQueryKey("getServices", options); createQueryKey("getServices", options);

View File

@ -92,6 +92,9 @@ import type {
DeleteMarketplaceData, DeleteMarketplaceData,
DeleteMarketplaceErrors, DeleteMarketplaceErrors,
DeleteMarketplaceResponses, DeleteMarketplaceResponses,
DeleteProductBarcodeImageData,
DeleteProductBarcodeImageErrors,
DeleteProductBarcodeImageResponses,
DeleteProductData, DeleteProductData,
DeleteProductErrors, DeleteProductErrors,
DeleteProductResponses, DeleteProductResponses,
@ -220,6 +223,9 @@ import type {
UpdateStatusData, UpdateStatusData,
UpdateStatusErrors, UpdateStatusErrors,
UpdateStatusResponses, UpdateStatusResponses,
UploadProductBarcodeImageData,
UploadProductBarcodeImageErrors,
UploadProductBarcodeImageResponses,
UploadProductImageData, UploadProductImageData,
UploadProductImageErrors, UploadProductImageErrors,
UploadProductImageResponses, UploadProductImageResponses,
@ -281,6 +287,8 @@ import {
zDeleteDealTagResponse2, zDeleteDealTagResponse2,
zDeleteMarketplaceData, zDeleteMarketplaceData,
zDeleteMarketplaceResponse2, zDeleteMarketplaceResponse2,
zDeleteProductBarcodeImageData,
zDeleteProductBarcodeImageResponse,
zDeleteProductData, zDeleteProductData,
zDeleteProductResponse2, zDeleteProductResponse2,
zDeleteProjectData, zDeleteProjectData,
@ -373,6 +381,8 @@ import {
zUpdateServicesKitResponse2, zUpdateServicesKitResponse2,
zUpdateStatusData, zUpdateStatusData,
zUpdateStatusResponse2, zUpdateStatusResponse2,
zUploadProductBarcodeImageData,
zUploadProductBarcodeImageResponse,
zUploadProductImageData, zUploadProductImageData,
zUploadProductImageResponse, zUploadProductImageResponse,
} from "./zod.gen"; } from "./zod.gen";
@ -1719,7 +1729,7 @@ export const uploadProductImage = <ThrowOnError extends boolean = false>(
responseValidator: async data => { responseValidator: async data => {
return await zUploadProductImageResponse.parseAsync(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, ...options,
headers: { headers: {
"Content-Type": null, "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 * Get Services
*/ */

View File

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

View File

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

View File

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

View File

@ -29,6 +29,7 @@ const ProductViewActions: FC<Props> = ({ dealProduct }) => {
entity: dealProduct.product, entity: dealProduct.product,
isEditing: true, isEditing: true,
clientId: dealProduct.product.clientId, 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 { interface RestProps {
imageUrlInputProps?: BaseFormInputProps<string>; imageUrlInputProps?: BaseFormInputProps<string>;
productId?: number; productId?: number;
onUploaded?: () => void;
} }
type Props = Omit<DropzoneProps, "onDrop"> & RestProps; type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
@ -16,6 +17,7 @@ type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
const ProductImageDropzone: FC<Props> = ({ const ProductImageDropzone: FC<Props> = ({
imageUrlInputProps, imageUrlInputProps,
productId, productId,
onUploaded,
}: Props) => { }: Props) => {
const imageDropzoneProps = useImageDropzone({ const imageDropzoneProps = useImageDropzone({
imageUrlInputProps, imageUrlInputProps,
@ -34,7 +36,7 @@ const ProductImageDropzone: FC<Props> = ({
uploadProductImage({ uploadProductImage({
path: { path: {
productId, pk: productId,
}, },
body: { body: {
upload_file: file, upload_file: file,
@ -50,6 +52,7 @@ const ProductImageDropzone: FC<Props> = ({
} }
imageUrlInputProps?.onChange(data?.imageUrl); imageUrlInputProps?.onChange(data?.imageUrl);
setShowDropzone(false); setShowDropzone(false);
onUploaded && onUploaded();
}) })
.catch(err => { .catch(err => {
console.log(err); console.log(err);

View File

@ -1,10 +1,9 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Fieldset, Flex, Stack, TagsInput, TextInput } from "@mantine/core"; import { Flex } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals"; import { ContextModalProps } from "@mantine/modals";
import BarcodeTemplateSelect from "@/app/products/components/shared/BarcodeTemplateSelect/BarcodeTemplateSelect";
import { import {
BarcodeTemplateSchema, BarcodeTemplateSchema,
CreateProductSchema, CreateProductSchema,
@ -14,11 +13,11 @@ import {
import BaseFormModal, { import BaseFormModal, {
CreateEditFormProps, CreateEditFormProps,
} from "@/modals/base/BaseFormModal/BaseFormModal"; } 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, { import ProductEditorSegmentedControl, {
ProductEditorTab, ProductEditorTab,
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/ProductEditorSegmentedControl"; } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/ProductEditorSegmentedControl";
import BaseFormInputProps from "@/utils/baseFormInputProps";
type Props = CreateEditFormProps< type Props = CreateEditFormProps<
CreateProductSchema, CreateProductSchema,
@ -26,6 +25,7 @@ type Props = CreateEditFormProps<
ProductSchema ProductSchema
> & { > & {
clientId: number; clientId: number;
refetchProducts: () => void;
}; };
type ProductForm = Partial< type ProductForm = Partial<
@ -64,92 +64,18 @@ const ProductEditorModal = ({
initialValues, initialValues,
validate: { validate: {
name: name => name: name =>
!name || name.trim() !== "" (!name || name.trim() === "") &&
? null "Необходимо ввести название товара",
: "Необходимо ввести название товара", 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 && ( const imageTab = isEditing && (
<ProductImageDropzone <ImagesTab
imageUrlInputProps={ form={form}
form.getInputProps("imageUrl") as BaseFormInputProps<string>
}
productId={innerProps.entity.id} productId={innerProps.entity.id}
onUploaded={innerProps.refetchProducts}
/> />
); );
@ -169,9 +95,11 @@ const ProductEditorModal = ({
onChange={setEditorTab} onChange={setEditorTab}
/> />
)} )}
{editorTab === ProductEditorTab.CHARACTERISTICS {editorTab === ProductEditorTab.CHARACTERISTICS ? (
? characteristicsTab <CharacteristicsTab form={form} />
: imageTab} ) : (
imageTab
)}
</Flex> </Flex>
</BaseFormModal> </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;