diff --git a/fix-zod.ts b/fix-zod.ts new file mode 100644 index 0000000..e886928 --- /dev/null +++ b/fix-zod.ts @@ -0,0 +1,10 @@ +import * as fs from "fs"; + +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"); + +fs.writeFileSync(path, content); +console.log("✅ Fixed zod schema for upload_file"); diff --git a/package.json b/package.json index 6c60339..b35b78d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client", + "generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client & sudo npx tsc ./fix-zod.ts && mv -f ./fix-zod.js ./fix-zod.cjs && sudo node ./fix-zod.cjs", "generate-modules": "sudo npx tsc ./src/modules/modulesFileGen/modulesFileGen.ts && mv -f ./src/modules/modulesFileGen/modulesFileGen.js ./src/modules/modulesFileGen/modulesFileGen.cjs && sudo node ./src/modules/modulesFileGen/modulesFileGen.cjs" }, "dependencies": { diff --git a/src/app/services/modals/ServicesKitEditorModal.tsx b/src/app/services/modals/ServicesKitEditorModal.tsx index dbadd42..9f89dc6 100644 --- a/src/app/services/modals/ServicesKitEditorModal.tsx +++ b/src/app/services/modals/ServicesKitEditorModal.tsx @@ -47,7 +47,7 @@ const ServicesKitEditorModal = ({ {...form.getInputProps("name")} /> { form.setFieldValue("serviceType", Number(tab)); form.setFieldValue("services", []); diff --git a/src/lib/client/@tanstack/react-query.gen.ts b/src/lib/client/@tanstack/react-query.gen.ts index 2717d96..1a0f813 100644 --- a/src/lib/client/@tanstack/react-query.gen.ts +++ b/src/lib/client/@tanstack/react-query.gen.ts @@ -60,7 +60,6 @@ import { getMarketplaces, getProductBarcodePdf, getProducts, - getProject, getProjects, getServiceCategories, getServices, @@ -85,6 +84,7 @@ import { updateServiceCategory, updateServicesKit, updateStatus, + uploadProductImage, type Options, } from "../sdk.gen"; import type { @@ -215,7 +215,6 @@ import type { GetProductsData, GetProductsError, GetProductsResponse2, - GetProjectData, GetProjectsData, GetServiceCategoriesData, GetServicesData, @@ -276,6 +275,9 @@ import type { UpdateStatusData, UpdateStatusError, UpdateStatusResponse2, + UploadProductImageData, + UploadProductImageError, + UploadProductImageResponse, } from "../types.gen"; export type QueryKey = [ @@ -2188,6 +2190,57 @@ export const updateProductMutation = ( return mutationOptions; }; +export const uploadProductImageQueryKey = ( + options: Options +) => createQueryKey("uploadProductImage", options); + +/** + * Upload Product Image + */ +export const uploadProductImageOptions = ( + options: Options +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await uploadProductImage({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: uploadProductImageQueryKey(options), + }); +}; + +/** + * Upload Product Image + */ +export const uploadProductImageMutation = ( + options?: Partial> +): UseMutationOptions< + UploadProductImageResponse, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + UploadProductImageResponse, + AxiosError, + Options + > = { + mutationFn: async localOptions => { + const { data } = await uploadProductImage({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + export const getProductBarcodePdfQueryKey = ( options: Options ) => createQueryKey("getProductBarcodePdf", options); @@ -2716,27 +2769,6 @@ export const deleteProjectMutation = ( return mutationOptions; }; -export const getProjectQueryKey = (options: Options) => - createQueryKey("getProject", options); - -/** - * Get Project - */ -export const getProjectOptions = (options: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getProject({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getProjectQueryKey(options), - }); -}; - /** * Update Project */ diff --git a/src/lib/client/sdk.gen.ts b/src/lib/client/sdk.gen.ts index 7d71299..f8a54db 100644 --- a/src/lib/client/sdk.gen.ts +++ b/src/lib/client/sdk.gen.ts @@ -1,6 +1,11 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from "./client"; +import { + formDataBodySerializer, + type Client, + type Options as ClientOptions, + type TDataShape, +} from "./client"; import { client as _heyApiClient } from "./client.gen"; import type { AddKitToDealData, @@ -147,9 +152,6 @@ import type { GetProductsData, GetProductsErrors, GetProductsResponses, - GetProjectData, - GetProjectErrors, - GetProjectResponses, GetProjectsData, GetProjectsResponses, GetServiceCategoriesData, @@ -218,6 +220,9 @@ import type { UpdateStatusData, UpdateStatusErrors, UpdateStatusResponses, + UploadProductImageData, + UploadProductImageErrors, + UploadProductImageResponses, } from "./types.gen"; import { zAddKitToDealData, @@ -320,8 +325,6 @@ import { zGetProductBarcodePdfResponse2, zGetProductsData, zGetProductsResponse2, - zGetProjectData, - zGetProjectResponse2, zGetProjectsData, zGetProjectsResponse2, zGetServiceCategoriesData, @@ -370,6 +373,8 @@ import { zUpdateServicesKitResponse2, zUpdateStatusData, zUpdateStatusResponse2, + zUploadProductImageData, + zUploadProductImageResponse, } from "./zod.gen"; export type Options< @@ -1695,6 +1700,34 @@ export const updateProduct = ( }); }; +/** + * Upload Product Image + */ +export const uploadProductImage = ( + options: Options +) => { + return (options.client ?? _heyApiClient).post< + UploadProductImageResponses, + UploadProductImageErrors, + ThrowOnError + >({ + ...formDataBodySerializer, + requestValidator: async data => { + return await zUploadProductImageData.parseAsync(data); + }, + responseType: "json", + responseValidator: async data => { + return await zUploadProductImageResponse.parseAsync(data); + }, + url: "/crm/v1/fulfillment-base/product/images/upload/{productId}", + ...options, + headers: { + "Content-Type": null, + ...options.headers, + }, + }); +}; + /** * Get Product Barcode Pdf */ @@ -2095,29 +2128,6 @@ export const deleteProject = ( }); }; -/** - * Get Project - */ -export const getProject = ( - options: Options -) => { - return (options.client ?? _heyApiClient).get< - GetProjectResponses, - GetProjectErrors, - ThrowOnError - >({ - requestValidator: async data => { - return await zGetProjectData.parseAsync(data); - }, - responseType: "json", - responseValidator: async data => { - return await zGetProjectResponse2.parseAsync(data); - }, - url: "/crm/v1/project/{pk}", - ...options, - }); -}; - /** * Update Project */ diff --git a/src/lib/client/types.gen.ts b/src/lib/client/types.gen.ts index 12cbee3..a492b0a 100644 --- a/src/lib/client/types.gen.ts +++ b/src/lib/client/types.gen.ts @@ -103,6 +103,16 @@ export type BoardSchema = { projectId: number; }; +/** + * Body_upload_product_image + */ +export type BodyUploadProductImage = { + /** + * Upload File + */ + upload_file: Blob | File; +}; + /** * BuiltInModuleSchema */ @@ -630,6 +640,14 @@ export type CreateProductSchema = { * Barcodes */ barcodes: Array; + /** + * Imageurl + */ + imageUrl?: string | null; + /** + * Images + */ + images?: Array | null; }; /** @@ -1400,13 +1418,6 @@ export type GetProductsResponse = { paginationInfo: PaginationInfoSchema; }; -/** - * GetProjectResponse - */ -export type GetProjectResponse = { - entity: ProjectSchema; -}; - /** * GetProjectsResponse */ @@ -1593,6 +1604,14 @@ export type ProductSchema = { * Barcodes */ barcodes: Array; + /** + * Imageurl + */ + imageUrl?: string | null; + /** + * Images + */ + images?: Array | null; /** * Id */ @@ -1655,6 +1674,20 @@ export type ProductServicesDuplicateResponse = { message: string; }; +/** + * ProductUploadImageResponse + */ +export type ProductUploadImageResponse = { + /** + * Message + */ + message: string; + /** + * Imageurl + */ + imageUrl?: string | null; +}; + /** * ProjectSchema */ @@ -4036,6 +4069,38 @@ export type UpdateProductResponses = { export type UpdateProductResponse2 = UpdateProductResponses[keyof UpdateProductResponses]; +export type UploadProductImageData = { + body: BodyUploadProductImage; + path: { + /** + * Productid + */ + productId: number; + }; + query?: never; + url: "/crm/v1/fulfillment-base/product/images/upload/{productId}"; +}; + +export type UploadProductImageErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UploadProductImageError = + UploadProductImageErrors[keyof UploadProductImageErrors]; + +export type UploadProductImageResponses = { + /** + * Successful Response + */ + 200: ProductUploadImageResponse; +}; + +export type UploadProductImageResponse = + UploadProductImageResponses[keyof UploadProductImageResponses]; + export type GetProductBarcodePdfData = { body: GetProductBarcodePdfRequest; path?: never; @@ -4458,37 +4523,6 @@ export type DeleteProjectResponses = { export type DeleteProjectResponse2 = DeleteProjectResponses[keyof DeleteProjectResponses]; -export type GetProjectData = { - body?: never; - path: { - /** - * Pk - */ - pk: number; - }; - query?: never; - url: "/crm/v1/project/{pk}"; -}; - -export type GetProjectErrors = { - /** - * Validation Error - */ - 422: HttpValidationError; -}; - -export type GetProjectError = GetProjectErrors[keyof GetProjectErrors]; - -export type GetProjectResponses = { - /** - * Successful Response - */ - 200: GetProjectResponse; -}; - -export type GetProjectResponse2 = - GetProjectResponses[keyof GetProjectResponses]; - export type UpdateProjectData = { body: UpdateProjectRequest; path: { diff --git a/src/lib/client/zod.gen.ts b/src/lib/client/zod.gen.ts index c9777fe..a222183 100644 --- a/src/lib/client/zod.gen.ts +++ b/src/lib/client/zod.gen.ts @@ -51,6 +51,13 @@ export const zBoardSchema = z.object({ projectId: z.int(), }); +/** + * Body_upload_product_image + */ +export const zBodyUploadProductImage = z.object({ + upload_file: z.any(), +}); + /** * BuiltInModuleTabSchema */ @@ -234,6 +241,15 @@ export const zCreateDealProductRequest = z.object({ entity: zCreateDealProductSchema, }); +/** + * ProductImageSchema + */ +export const zProductImageSchema = z.object({ + id: z.int(), + productId: z.int(), + imageUrl: z.string(), +}); + /** * ProductSchema */ @@ -249,6 +265,8 @@ export const zProductSchema = z.object({ 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()])), id: z.int(), barcodeTemplate: zBarcodeTemplateSchema, }); @@ -509,6 +527,8 @@ export const zCreateProductSchema = z.object({ 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()])), }); /** @@ -957,13 +977,6 @@ export const zGetProductsResponse = z.object({ paginationInfo: zPaginationInfoSchema, }); -/** - * GetProjectResponse - */ -export const zGetProjectResponse = z.object({ - entity: zProjectSchema, -}); - /** * GetProjectsResponse */ @@ -1042,15 +1055,6 @@ export const zHttpValidationError = z.object({ detail: z.optional(z.array(zValidationError)), }); -/** - * ProductImageSchema - */ -export const zProductImageSchema = z.object({ - id: z.int(), - productId: z.int(), - imageUrl: z.string(), -}); - /** * ProductServicesDuplicateRequest */ @@ -1067,6 +1071,14 @@ export const zProductServicesDuplicateResponse = z.object({ message: z.string(), }); +/** + * ProductUploadImageResponse + */ +export const zProductUploadImageResponse = z.object({ + message: z.string(), + imageUrl: z.optional(z.union([z.string(), z.null()])), +}); + export const zSortDir = z.enum(["asc", "desc"]); /** @@ -2146,6 +2158,19 @@ export const zUpdateProductData = z.object({ */ export const zUpdateProductResponse2 = zUpdateProductResponse; +export const zUploadProductImageData = z.object({ + body: zBodyUploadProductImage, + path: z.object({ + productId: z.int(), + }), + query: z.optional(z.never()), +}); + +/** + * Successful Response + */ +export const zUploadProductImageResponse = zProductUploadImageResponse; + export const zGetProductBarcodePdfData = z.object({ body: zGetProductBarcodePdfRequest, path: z.optional(z.never()), @@ -2336,19 +2361,6 @@ export const zDeleteProjectData = z.object({ */ export const zDeleteProjectResponse2 = zDeleteProjectResponse; -export const zGetProjectData = z.object({ - body: z.optional(z.never()), - path: z.object({ - pk: z.int(), - }), - query: z.optional(z.never()), -}); - -/** - * Successful Response - */ -export const zGetProjectResponse2 = zGetProjectResponse; - export const zUpdateProjectData = z.object({ body: zUpdateProjectRequest, path: z.object({ diff --git a/src/modals/base/BaseFormModal/BaseFormModal.tsx b/src/modals/base/BaseFormModal/BaseFormModal.tsx index 859979f..fd29326 100644 --- a/src/modals/base/BaseFormModal/BaseFormModal.tsx +++ b/src/modals/base/BaseFormModal/BaseFormModal.tsx @@ -25,14 +25,16 @@ export type BaseFormProps = { onClose: () => void; closeOnSubmit?: boolean; children: ReactNode; + actionsEnabled?: boolean; }; type Props = BaseFormProps & CreateEditFormProps; -const BaseFormModal = ( - props: Props -) => { +const BaseFormModal = ({ + actionsEnabled = true, + ...props +}: Props) => { const { closeOnSubmit = false } = props; const onSubmit = (values: Partial) => { @@ -54,7 +56,7 @@ const BaseFormModal = ( gap={"xs"} direction={"column"}> {props.children} - + {actionsEnabled && } ); diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductImageDropzone/ProductImageDropzone.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductImageDropzone/ProductImageDropzone.tsx index 31ab76b..c5d5b9b 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductImageDropzone/ProductImageDropzone.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductImageDropzone/ProductImageDropzone.tsx @@ -1,6 +1,7 @@ import { FC } from "react"; import { DropzoneProps, FileWithPath } from "@mantine/dropzone"; import ImageDropzone from "@/components/ui/ImageDropzone/ImageDropzone"; +import { uploadProductImage } from "@/lib/client"; import { notifications } from "@/lib/notifications"; import BaseFormInputProps from "@/utils/baseFormInputProps"; import useImageDropzone from "./useImageDropzone"; @@ -31,30 +32,31 @@ const ProductImageDropzone: FC = ({ setIsLoading(true); - // TODO SEND REQUEST + uploadProductImage({ + path: { + productId, + }, + body: { + upload_file: file, + }, + }) + .then(({ data }) => { + notifications.success({ message: data?.message }); + setIsLoading(false); - // ProductService.uploadProductImage({ - // productId, - // formData: { - // upload_file: file, - // }, - // }) - // .then(({ ok, message, imageUrl }) => { - // notifications.guess(ok, { message }); - // setIsLoading(false); - // - // if (!ok || !imageUrl) { - // setShowDropzone(true); - // return; - // } - // imageUrlInputProps?.onChange(imageUrl); - // setShowDropzone(false); - // }) - // .catch(error => { - // notifications.error({ message: error.toString() }); - // setShowDropzone(true); - // setIsLoading(false); - // }); + if (!data?.imageUrl) { + setShowDropzone(true); + return; + } + imageUrlInputProps?.onChange(data?.imageUrl); + setShowDropzone(false); + }) + .catch(err => { + console.log(err); + notifications.error({ message: err.toString() }); + setShowDropzone(true); + setIsLoading(false); + }); }; return ( diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/ProductView.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/ProductView.tsx index 2e1ebf5..799d0c0 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/ProductView.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/ProductView.tsx @@ -107,12 +107,12 @@ const ProductView: FC = ({ dealProduct }) => { - {!dealProduct.product && ( + {dealProduct.product?.imageUrl && ( )} {dealProduct.product.name} diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/ProductEditorModal.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/ProductEditorModal.tsx index 0cdf4d9..87eccf8 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/ProductEditorModal.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/ProductEditorModal.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from "react"; import { Fieldset, Flex, Stack, TagsInput, TextInput } from "@mantine/core"; import { useForm } from "@mantine/form"; import { ContextModalProps } from "@mantine/modals"; @@ -14,6 +15,9 @@ import BaseFormModal, { CreateEditFormProps, } from "@/modals/base/BaseFormModal/BaseFormModal"; import ProductImageDropzone from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductImageDropzone/ProductImageDropzone"; +import ProductEditorSegmentedControl, { + ProductEditorTab, +} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/ProductEditorSegmentedControl"; import BaseFormInputProps from "@/utils/baseFormInputProps"; type Props = CreateEditFormProps< @@ -35,6 +39,9 @@ const ProductEditorModal = ({ id, innerProps, }: ContextModalProps) => { + const [editorTab, setEditorTab] = useState( + ProductEditorTab.CHARACTERISTICS + ); const isEditing = "entity" in innerProps; const initialValues: ProductForm = isEditing @@ -63,94 +70,108 @@ const ProductEditorModal = ({ }, }); + const characteristicsTab = ( + <> +
+ + + + + { + form.setFieldValue("barcodeTemplate", template); + form.setFieldValue( + "barcodeTemplateId", + template?.id + ); + }} + /> + + +
+
+ + + + + + + +
+ + ); + + const imageTab = isEditing && ( + + } + productId={innerProps.entity.id} + /> + ); + return ( context.closeContextModal(id)}> -
- - - - - { - form.setFieldValue("barcodeTemplate", template); - form.setFieldValue( - "barcodeTemplateId", - template?.id - ); - }} - /> - - -
-
- - - - - - - -
{isEditing && ( - - } - productId={innerProps.entity.id} + )} + {editorTab === ProductEditorTab.CHARACTERISTICS + ? characteristicsTab + : imageTab}
); diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/ProductEditorSegmentedControl.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/ProductEditorSegmentedControl.tsx new file mode 100644 index 0000000..aef8b03 --- /dev/null +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/components/ProductEditorSegmentedControl.tsx @@ -0,0 +1,31 @@ +import { FC } from "react"; +import BaseSegmentedControl, { + BaseSegmentedControlProps, +} from "@/components/ui/BaseSegmentedControl/BaseSegmentedControl"; + +export enum ProductEditorTab { + CHARACTERISTICS, + IMAGES, +} + +type Props = Omit, "data">; + +const data = [ + { + label: "Характеристики", + value: ProductEditorTab.CHARACTERISTICS, + }, + { + label: "Изображение", + value: ProductEditorTab.IMAGES, + }, +]; + +const ProductEditorSegmentedControl: FC = props => ( + +); + +export default ProductEditorSegmentedControl;