diff --git a/admin-client/components/AddProduct/AddProduct.html b/admin-client/components/AddProduct/AddProduct.html deleted file mode 100644 index afc91e9..0000000 --- a/admin-client/components/AddProduct/AddProduct.html +++ /dev/null @@ -1,39 +0,0 @@ -
-
-

Add New Product

-
- - -
-
- -
-
-
- - -
- -
- - -
- -
-
- - -
-
-
diff --git a/admin-client/components/AddProduct/AddProduct.jsx b/admin-client/components/AddProduct/AddProduct.jsx index 781e9aa..c0a951d 100644 --- a/admin-client/components/AddProduct/AddProduct.jsx +++ b/admin-client/components/AddProduct/AddProduct.jsx @@ -1,249 +1,365 @@ "use client"; -import { PRODUCT_VISIBILITY, SIZES } from "@/constants"; -import { useState } from "react"; -import axios from "../../utils/axiosInstance"; +import { ProductVisibility } from "@/constants"; +import useToast from "@/hooks/useToast"; +import { + useCreateProductService, + useUploadProductImagesService, +} from "@/services/api/services/product"; +import HTTP_CODES from "@/services/api/types/http-codes"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import * as yup from "yup"; +import InputError from "../Input/InputError"; +import InputText from "../Input/InputText"; import CategoryInput from "./CategoryInput"; import ColorPickerFromImage from "./ColorPickerFromImage"; +import SizesInput from "./SizesInput"; import TagsInput from "./TagsInput"; +const schema = yup.object().shape({ + title: yup.string().required("Title is required"), + description: yup.string().required("Description is required"), + buyPrice: yup.number().required("Buy Price is required"), + sellPrice: yup.number().required("Sell Price is required"), + category: yup.object().shape({ + id: yup.string().required("Category is required"), + }), + tags: yup.array().of(yup.string()), + quantity: yup.number().required("Quantity is required"), + discount: yup.number().required("Discount is required"), + sizes: yup.array().min(1, "Please select at least one size"), + visibility: yup.string().required("Visibility is required"), + productInfo: yup + .array() + .min(1, "Please upload one image") + .of( + yup.object().shape({ + color: yup.string().required("Click on image to pick color"), + colorWiseQuantity: yup + .number() + .required("Color Wise Quantity is required"), + colorSizeWiseQuantity: yup + .object() + .test( + "sizes-exist", + "All sizes must exist in colorSizeWiseQuantity", + (value, context) => { + const sizes = context.parent.sizes || []; + if (!value || typeof value !== "object") return false; + return sizes.every((size) => Object.keys(value).includes(size)); + } + ), + colorName: yup.string().required("Color Name is required"), + image: yup.mixed().required("Image is required"), + }) + ), +}); + const AddProductForm = () => { - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); - const [buyPrice, setBuyPrice] = useState(""); - const [sellPrice, setSellPrice] = useState(""); - const [category, setCategory] = useState(""); - const [quantity, setQuantity] = useState(""); - const [discount, setDiscount] = useState(""); - const [tags, setTags] = useState([]); - const [sizes, setSizes] = useState([]); - const [error, setError] = useState(""); - const [visibility, setVisibility] = useState(PRODUCT_VISIBILITY[0]); - const [productInfo, setProductInfo] = useState([]); - - const handleSelectSize = (e) => { - const value = e.target.value; - - if (sizes.includes(value)) { - setSizes(sizes.filter((size) => size !== value)); - } else { - setSizes([...sizes, value]); - } - }; - - console.log({ - title, - description, - buyPrice, - sellPrice, - category, - quantity, - discount, - tags, - sizes, - productInfo, + const fetchUploadImages = useUploadProductImagesService(); + const fetchCreateProduct = useCreateProductService(); + const showToast = useToast(); + const methods = useForm({ + resolver: yupResolver(schema), + defaultValues: { + title: "", + description: "", + buyPrice: null, + sellPrice: null, + category: null, + tags: [], + quantity: null, + discount: null, + sizes: [], //array of sizes. [{id: 1, name: xs}] + visibility: ProductVisibility.HIDDEN, + productInfo: [], + }, }); - const handleSubmit = (e) => { - e.preventDefault(); - console.log("submitting"); - const formData = new FormData(); - try { - formData.append("title", title); - formData.append("description", description); - formData.append("buyPrice", buyPrice); - formData.append("sellPrice", sellPrice); - formData.append("categoryId", category.id); - formData.append("quantity", quantity); - formData.append("discount", discount); - formData.append("sizes", JSON.stringify(sizes)); - formData.append("visibility", visibility); - - productInfo.forEach((info, index) => { - formData.append(`productInfo[${index}][color]`, info.color); - formData.append( - `productInfo[${index}][colorWiseQuantity]`, - info.colorWiseQuantity - ); - formData.append( - `productInfo[${index}][colorSizeWiseQuantity]`, - JSON.stringify(info.colorSizeWiseQuantity) - ); - formData.append(`productInfo[${index}][colorName]`, info.colorName); - if (info.image) { - formData.append(`images`, info.image); - } - }); - - axios - .post("/products", formData) - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); - } catch (error) { - console.log(error); + const { + register, + handleSubmit, + setValue, + getValues, + control, + reset, + formState: { errors }, + } = methods; + + const sizes = methods.watch("sizes"); + + console.log({ errors }); + + const onSubmit = handleSubmit(async (formData) => { + console.log({ formData }); + // cnsole the errors if any + // const errors = await methods.trigger(); + // if (errors?.error?.length > 0) { + // if(errors.)showToast(errors.error.color.message, "error"); + // return; + // } + + // validate quantity and size wise quantity + const invalidProductInfo = formData.productInfo.find((info) => { + const sizeWiseQuantity = Object.values(info.colorSizeWiseQuantity); + const totalSizeWiseQuantity = sizeWiseQuantity.reduce( + (acc, qty) => acc + qty, + 0 + ); + console.log({ totalSizeWiseQuantity, sizeWiseQuantity }); + return info.colorWiseQuantity < totalSizeWiseQuantity; + }); + + if (invalidProductInfo) { + showToast( + "Color wise quantity must be greater than or equal to total size wise quantity", + "error" + ); + return; + } + + const productImages = formData.productInfo.map((info) => info.image); + + const imageData = new FormData(); + productImages.forEach((img) => { + imageData.append("images", img); + }); + + // upload images + + const { status, data: imageResData } = await fetchUploadImages(imageData); + if (status !== HTTP_CODES.OK) { + showToast("Failed to upload images", "error"); + return; } - }; + + // create product + const productData = { + ...formData, + images: imageResData.map((img) => ({ id: img.id })), + sizes: formData.sizes.map((s) => ({ + id: s.id, + })), + category: { + id: Number(formData.category.id), + }, + productInfo: formData.productInfo.map((info) => ({ + colorCode: info.color, + colorWiseQuantity: info.colorWiseQuantity, + colorSizeWiseQuantity: info.colorSizeWiseQuantity, + colorName: info.colorName, + })), + }; + + const { status: createStatus, data: createData } = await fetchCreateProduct( + productData + ); + if (createStatus !== HTTP_CODES.CREATED) { + console.log({ createData, createStatus }); + showToast("Failed to create product", "error"); + return; + } + + showToast("Product created successfully", "success"); + reset(); + }); return ( -
-
- - setTitle(e.target.value)} - required - className="input text-text input-bordered w-full h-10 focus:outline-1 focus:outline-offset-1 bg-secondary focus:bg-white dark:focus:bg-secondary" - placeholder="Product Title" - /> -
- -
- - -
- -
-
- - setBuyPrice(e.target.value)} - required - id="price" - className="input text-text input-bordered w-full h-10 focus:outline-1 focus:outline-offset-1 bg-secondary focus:bg-white dark:focus:bg-secondary" - placeholder="৳ 0.00" + + +
+
-
-
-
-
- - setDiscount(e.target.value)} - required - id="discount" - className="input text-text input-bordered w-full h-10 focus:outline-1 focus:outline-offset-1 bg-secondary focus:bg-white dark:focus:bg-secondary" - placeholder="0 %" - /> +
+
+ +
+
+ +
- {/* quantity */} -
- - setQuantity(e.target.value)} - required - placeholder="Quantity" - className="input text-text input-bordered w-full h-10 focus:outline-1 focus:outline-offset-1 bg-secondary focus:bg-white dark:focus:bg-secondary" - /> + +
+
+ +
+ {/* quantity */} +
+ +
-
- -
-
-

Size:

-
- {SIZES.map((size, i) => ( -
- + ( +
+ - + + {fieldState.error ? fieldState.error.message : ""} + +
+ )} + /> + {/* */} + ( +
+ + + + {fieldState.error ? fieldState.error.message : ""} +
- ))} + )} + /> +
+ + {/* Visivility and tags inputs */} +
+
+ + ( + + )} + />
+ ( +
+ field.onChange(updatedTags)} + /> + + + {fieldState.error ? fieldState.error.message : ""} + +
+ )} + /> + {/* */}
- -
-
-
- - + +
+ { + return ( + <> + + {fieldState.error && ( + {fieldState.error.message} + )} + + ); + }} + /> +
+
+
- -
- -
- -
-
- -
- + + ); }; diff --git a/admin-client/components/AddProduct/CategoryInput.jsx b/admin-client/components/AddProduct/CategoryInput.jsx index 90c7427..1033a03 100644 --- a/admin-client/components/AddProduct/CategoryInput.jsx +++ b/admin-client/components/AddProduct/CategoryInput.jsx @@ -1,64 +1,59 @@ -import { useState } from "react"; -const categories = [ - { - id: 1, - name: "Men", - subCategory: [ - { - id: 143, - name: "Shirt", - subCategory: [ - { - id: 1, - name: "Full Sleeve", - subCategory: [ - { id: 42, name: "Formal" }, - { id: 43, name: "Casual" }, - ], - }, - { - id: 2, - name: "Half Sleeve", - subCategory: [ - { id: 42, name: "Formal" }, - { id: 43, name: "Casual" }, - ], - }, - ], - }, - ], - }, - { - id: 2, - name: "Women", - subCategory: [ - { - id: 143, - name: "Shirt", - subCategory: [ - { - id: 1, - name: "Full Sleeve", - subCategory: [ - { id: 42, name: "Formal" }, - { id: 43, name: "Casual" }, - ], - }, - { - id: 2, - name: "Half Sleeve", - subCategory: [ - { id: 42, name: "Formal" }, - { id: 43, name: "Casual" }, - ], - }, - ], - }, - ], - }, -]; -export default function CategoryInput({ category, setCategory }) { +import { useGetCategoriesService } from "@/services/api/services/categories"; +import HTTP_CODES from "@/services/api/types/http-codes"; +import { useEffect, useMemo, useState } from "react"; + +const groupCategoriesByParent = (categories) => { + const result = []; + + // First, create a map of categories by their IDs + const categoriesMap = new Map(); + categories.forEach((category) => { + categoriesMap.set(category.id, { ...category, subCategory: [] }); + }); + + // Step 2: Populate the result with the top-level categories + categories.forEach((category) => { + if (category.isVisibleInMenu) { + result.push(categoriesMap.get(category.id)); + } + + // If it has a parentCategory, push it into the parent's subCategory + if (category.parentCategory) { + const parent = categoriesMap.get(category.parentCategory.id); + if (parent) { + parent.subCategory.push(categoriesMap.get(category.id)); + } + } + }); + + return result; +}; +export default function CategoryInput({ setValue, getValues, name }) { + const [categories, setCategories] = useState([]); + const fetchCategories = useGetCategoriesService(); const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + const fetchData = async () => { + const { status, data } = await fetchCategories(); + if (status === HTTP_CODES.OK) { + setCategories(data); + } + }; + fetchData(); + }, []); + + const handleCategorySelect = (category) => { + setValue(name, category, { shouldValidate: true }); + setIsOpen(false); // Close dropdown after selection + }; + + const groupedCategories = useMemo( + () => groupCategoriesByParent(categories), + [categories] + ); + + const category = getValues(name) || null; const renderCategoryOptions = (categories, level = 0) => { return categories.map((category, i) => { const categoryName = category.name; @@ -95,10 +90,7 @@ export default function CategoryInput({ category, setCategory }) { ); }); }; - const handleCategorySelect = (category) => { - setCategory({ id: category.id, name: category.name }); - setIsOpen(false); // Close dropdown after selection - }; + return (
{isOpen && (
    - {renderCategoryOptions(categories)} + {renderCategoryOptions(groupedCategories)}
)}
diff --git a/admin-client/components/AddProduct/ColorPickerFromImage.jsx b/admin-client/components/AddProduct/ColorPickerFromImage.jsx index 4dee86f..e815ef6 100644 --- a/admin-client/components/AddProduct/ColorPickerFromImage.jsx +++ b/admin-client/components/AddProduct/ColorPickerFromImage.jsx @@ -1,8 +1,15 @@ import Image from "next/image"; import { useRef } from "react"; +import InputError from "../Input/InputError"; import SizeWiseQuantity from "./SizeWiseQuantity"; -const ColorPickerFromImage = ({ productInfo, setProductInfo, sizes }) => { +const ColorPickerFromImage = ({ + productInfo, + setProductInfo, + sizes, + name, + errors, +}) => { const canvasRef = useRef([]); const imgRefs = useRef([]); const imgContainerRef = useRef(); @@ -18,13 +25,15 @@ const ColorPickerFromImage = ({ productInfo, setProductInfo, sizes }) => { reader.onloadend = () => { const newImage = reader.result; - setProductInfo((prevProductInfo) => [ - ...prevProductInfo, + setProductInfo(name, [ + ...productInfo, { image: file, color: "", - colorWiseQuantity: "", - colorSizeWiseQuantity: Object.fromEntries(sizes?.map((s) => [s, 0])), + colorWiseQuantity: null, + colorSizeWiseQuantity: Object.fromEntries( + sizes?.map((s) => [s.name, 0]) + ), previewImage: newImage, }, ]); // setting image preview @@ -69,7 +78,7 @@ const ColorPickerFromImage = ({ productInfo, setProductInfo, sizes }) => { } return info; }); - setProductInfo(updatedProductInfo); + setProductInfo(name, updatedProductInfo); }; // handle size wise quantity @@ -81,19 +90,19 @@ const ColorPickerFromImage = ({ productInfo, setProductInfo, sizes }) => { ...info, colorSizeWiseQuantity: { ...info.colorSizeWiseQuantity, - [size]: value, + [size?.name?.toLowerCase()]: Number(value), }, }; } return info; }); - setProductInfo(updatedProductInfo); + setProductInfo(name, updatedProductInfo); }; // delete image from product info const deleteImage = (index) => { const updatedProductInfo = productInfo.filter((_, i) => i !== index); - setProductInfo(updatedProductInfo); + setProductInfo(name, updatedProductInfo); }; // handle color name @@ -108,7 +117,7 @@ const ColorPickerFromImage = ({ productInfo, setProductInfo, sizes }) => { } return info; }); - setProductInfo(updatedProductInfo); + setProductInfo(name, updatedProductInfo); }; const handleColorWiseQuantity = (e, index) => { @@ -117,12 +126,12 @@ const ColorPickerFromImage = ({ productInfo, setProductInfo, sizes }) => { if (i === index) { return { ...info, - colorWiseQuantity: value, + colorWiseQuantity: Number(value), }; } return info; }); - setProductInfo(updatedProductInfo); + setProductInfo(name, updatedProductInfo); }; return ( @@ -131,7 +140,7 @@ const ColorPickerFromImage = ({ productInfo, setProductInfo, sizes }) => { type="file" accept="image/*" onChange={handleImageChange} - required={productInfo?.length === 0} + // required={productInfo?.length === 0} />
@@ -164,52 +173,80 @@ const ColorPickerFromImage = ({ productInfo, setProductInfo, sizes }) => { )} {/* product image and color wise product info */}
-
-

Click on IMAGE to pick Color:

-
+
+
+

Click on IMAGE to pick Color:

+
+
+ + {errors?.[index]?.color?.message && + errors[index]?.color?.message} +
-
- - handleColorName(e, index)} - /> +
+
+ + handleColorName(e, index)} + /> +
+ + + {errors?.[index]?.colorName?.message && + errors[index]?.colorName?.message} +
-
- - handleColorWiseQuantity(e, index)} - /> +
+
+ + handleColorWiseQuantity(e, index)} + /> +
+ + + {errors?.[index]?.colorWiseQuantity?.message && + errors[index]?.colorWiseQuantity?.message} +
{sizes?.length > 0 && ( - - handleSizeWiseQuantity(e, index, size) - } - sizes={sizes} - /> + <> + + handleSizeWiseQuantity(e, index, size) + } + sizes={sizes} + /> + + {errors?.[index]?.colorSizeWiseQuantity?.message && + errors[index]?.colorSizeWiseQuantity?.message} + + )}
diff --git a/admin-client/components/AddProduct/SizeWiseQuantity.jsx b/admin-client/components/AddProduct/SizeWiseQuantity.jsx index ee607df..ebb8375 100644 --- a/admin-client/components/AddProduct/SizeWiseQuantity.jsx +++ b/admin-client/components/AddProduct/SizeWiseQuantity.jsx @@ -1,29 +1,29 @@ +"use client"; export default function SizeWiseQuantity({ info, onHandleSizeWiseQuantity, - sizes, }) { return (
{sizes?.map((s, i) => { return (
onHandleSizeWiseQuantity(e, s)} - value={info.colorSizeWiseQuantity[s]} - required + value={info.colorSizeWiseQuantity[s?.name]} />
diff --git a/admin-client/components/AddProduct/SizesInput.jsx b/admin-client/components/AddProduct/SizesInput.jsx new file mode 100644 index 0000000..b78cc7c --- /dev/null +++ b/admin-client/components/AddProduct/SizesInput.jsx @@ -0,0 +1,73 @@ +import { SIZES } from "@/constants"; + +export default function SizesInput({ setValue, getValues, name: fieldName }) { + const handleSelectSize = ({ name, id }) => { + const currentSizes = getValues(fieldName) || []; + const isSelected = currentSizes.some((size) => size.id === id); + + const updatedSizes = isSelected + ? currentSizes.filter((size) => size.id !== id) // Remove size + : [...currentSizes, { name, id }]; // Add size + + setValue(fieldName, updatedSizes, { shouldValidate: true }); + }; + + const selectedSizes = getValues(fieldName) || []; + + return ( +
+

Size:

+
+ {Object.entries(SIZES).map(([name, id]) => ( +
+ handleSelectSize({ name, id })} + /> + +
+ ))} +
+
+ + //
+ //

Size:

+ //
+ // {Object.entries(SIZES).map(([key, id], i) => ( + //
+ // handleSelectSize({ key, id })} + // /> + // + //
+ // ))} + //
+ //
+ ); +} diff --git a/admin-client/components/AddProduct/TagsInput.jsx b/admin-client/components/AddProduct/TagsInput.jsx index 9c7ce7c..85c48f9 100644 --- a/admin-client/components/AddProduct/TagsInput.jsx +++ b/admin-client/components/AddProduct/TagsInput.jsx @@ -41,7 +41,7 @@ export default function TagsInput({ tags, onSetTags }) { onSetTags(tags.filter((_, index) => index !== indexToRemove)); }; return ( -
+
@@ -79,7 +79,7 @@ export default function TagsInput({ tags, onSetTags }) { onChange={handleTag} id="tags" name="tags" - className=" min-w-[1ch] -outline-offset-2 overflow-hidden min-h-10 h-10" + className=" min-w-[3ch] -outline-offset-2 overflow-hidden min-h-10 h-10" style={{ width: `${typingInputWidth}ch` }} placeholder="Type here" onKeyDown={handleKeyDown} diff --git a/admin-client/components/Input/InputError.jsx b/admin-client/components/Input/InputError.jsx new file mode 100644 index 0000000..b4506a6 --- /dev/null +++ b/admin-client/components/Input/InputError.jsx @@ -0,0 +1,3 @@ +export default function InputError(props) { + return
{props.children}
; +} diff --git a/admin-client/components/Input/InputText.jsx b/admin-client/components/Input/InputText.jsx index 2b09988..a1bf9b0 100644 --- a/admin-client/components/Input/InputText.jsx +++ b/admin-client/components/Input/InputText.jsx @@ -10,6 +10,7 @@ function InputText({ labelTitle, labelStyle, containerStyle, + inputStyle, }) { return ( diff --git a/admin-client/constants/index.js b/admin-client/constants/index.js index 6f13ab0..98e124a 100644 --- a/admin-client/constants/index.js +++ b/admin-client/constants/index.js @@ -1,5 +1,16 @@ -export const SIZES = ["XS", "S", "M", "L", "XL", "2XL", "3XL"]; -export const PRODUCT_VISIBILITY = ["Hidden", "Published"]; +export const SIZES = { + xs: 1, + s: 2, + m: 3, + l: 4, + xl: 5, + xxl: 6, + xxxl: 7, +}; +export const ProductVisibility = { + VISIBLE: "VISIBLE", + HIDDEN: "HIDDEN", +}; export const ROLES = { ADMIN: 1, USER: 2, diff --git a/admin-client/services/api/services/categories.js b/admin-client/services/api/services/categories.js new file mode 100644 index 0000000..657791a --- /dev/null +++ b/admin-client/services/api/services/categories.js @@ -0,0 +1,17 @@ +import { useCallback } from "react"; +import { API_URL } from "../config"; +import useFetchBase from "../use-fetch-base"; +import wrapperFetchJsonResponse from "../wrapper-fetch-json-response"; + +export function useGetCategoriesService() { + const fetchBase = useFetchBase(); + + return useCallback( + async (data) => { + return fetchBase(`${API_URL}/v1/categories`, { + method: "GET", + }).then(wrapperFetchJsonResponse); + }, + [fetchBase] + ); +} diff --git a/admin-client/services/api/services/product.js b/admin-client/services/api/services/product.js index e69de29..c94f3b0 100644 --- a/admin-client/services/api/services/product.js +++ b/admin-client/services/api/services/product.js @@ -0,0 +1,78 @@ +import { useCallback } from "react"; +import { API_URL } from "../config"; +import useFetch from "../use-fetch"; +import wrapperFetchJsonResponse from "../wrapper-fetch-json-response"; + +export function useUploadProductImagesService() { + const fetch = useFetch(); + + return useCallback( + async (data, reqConfig) => { + return fetch(`${API_URL}/v1/products/image/add`, { + method: "POST", + "Content-Type": "multipart/form-data", + body: data, + ...reqConfig, + }).then(wrapperFetchJsonResponse); + }, + [fetch] + ); +} + +export function useGetProductService() { + const fetch = useFetch(); + + return useCallback( + async (reqConfig) => { + return fetch(`${API_URL}/v1/products`, { + method: "GET", + ...reqConfig, + }).then(wrapperFetchJsonResponse); + }, + [fetch] + ); +} + +export function useGetProductByIdService() { + const fetch = useFetch(); + + return useCallback( + async (id, reqConfig) => { + return fetch(`${API_URL}/v1/products/${id}`, { + method: "GET", + ...reqConfig, + }).then(wrapperFetchJsonResponse); + }, + [fetch] + ); +} + +export function useCreateProductService() { + const fetch = useFetch(); + + return useCallback( + async (data, reqConfig) => { + return fetch(`${API_URL}/v1/products`, { + method: "POST", + body: JSON.stringify(data), + ...reqConfig, + }).then(wrapperFetchJsonResponse); + }, + [fetch] + ); +} + +export function useUpdateProductService() { + const fetch = useFetch(); + + return useCallback( + async (id, data, reqConfig) => { + return fetch(`${API_URL}/v1/products/${id}`, { + method: "PUT", + body: JSON.stringify(data), + ...reqConfig, + }).then(wrapperFetchJsonResponse); + }, + [fetch] + ); +} diff --git a/server/src/products/domain/product.ts b/server/src/products/domain/product.ts index 9735418..3f1dce8 100644 --- a/server/src/products/domain/product.ts +++ b/server/src/products/domain/product.ts @@ -1,5 +1,6 @@ import { Expose } from 'class-transformer'; import { Category } from 'src/categories/domain/category'; +import { ProductVisibility } from '../product-visibility.enum'; export class Size { id: number; name?: string; @@ -41,5 +42,5 @@ export class Product { productInfo: ProductInfo[]; @Expose({ groups: ['admin'] }) - visibility: 'Hidden' | 'Visible'; + visibility: ProductVisibility; } diff --git a/server/src/products/dto/create-product.dto.ts b/server/src/products/dto/create-product.dto.ts index 8e13f90..3995b41 100644 --- a/server/src/products/dto/create-product.dto.ts +++ b/server/src/products/dto/create-product.dto.ts @@ -9,6 +9,7 @@ import { IsString, ValidateNested, } from 'class-validator'; +import { ProductVisibility } from '../product-visibility.enum'; class SizeDto { @IsNumber() @@ -74,8 +75,8 @@ export class CreateProductDto { @ValidateNested({ each: true }) images: ImageDto[]; - @IsEnum(['Hidden', 'Visible']) - visibility: 'Hidden' | 'Visible'; + @IsEnum(ProductVisibility) + visibility: ProductVisibility; @IsArray() @Type(() => ProductInfoDto) diff --git a/server/src/products/infrastructure/entities/product.entity.ts b/server/src/products/infrastructure/entities/product.entity.ts index 45520e1..17d11b1 100644 --- a/server/src/products/infrastructure/entities/product.entity.ts +++ b/server/src/products/infrastructure/entities/product.entity.ts @@ -9,16 +9,17 @@ import { OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, -} from 'typeorm'; +} from "typeorm"; -import { CategoryEntity } from 'src/categories/infrastructure/entities/category.entity'; -import { ProductSizeEntity } from 'src/product-sizes/infrastructure/entities/product-size.entity'; -import { EntityHelper } from 'src/utils/entity-helper'; -import { ProductColorEntity } from './product-color.entity'; -import { ProductImageEntity } from './product-image.entity'; +import { CategoryEntity } from "src/categories/infrastructure/entities/category.entity"; +import { ProductSizeEntity } from "src/product-sizes/infrastructure/entities/product-size.entity"; +import { ProductVisibility } from "src/products/product-visibility.enum"; +import { EntityHelper } from "src/utils/entity-helper"; +import { ProductColorEntity } from "./product-color.entity"; +import { ProductImageEntity } from "./product-image.entity"; @Entity({ - name: 'product', + name: "product", }) export class ProductEntity extends EntityHelper { @PrimaryGeneratedColumn() @@ -27,13 +28,13 @@ export class ProductEntity extends EntityHelper { @Column() title: string; - @Column({ type: 'text' }) + @Column({ type: "text" }) description: string; - @Column({ type: 'float' }) + @Column({ type: "float" }) buyPrice: number; - @Column({ type: 'float' }) + @Column({ type: "float" }) sellPrice: number; @ManyToOne(() => CategoryEntity, (category) => category.products, { @@ -44,11 +45,11 @@ export class ProductEntity extends EntityHelper { @Column() quantity: number; - @Column({ type: 'decimal', precision: 5, scale: 2 }) + @Column({ type: "decimal", precision: 5, scale: 2 }) discount: number; - @Column({ type: 'enum', enum: ['Hidden', 'Visible'] }) - visibility: 'Hidden' | 'Visible'; + @Column({ type: "enum", enum: ProductVisibility }) + visibility: ProductVisibility; @OneToMany(() => ProductColorEntity, (productColor) => productColor.product, { cascade: true, diff --git a/server/src/products/infrastructure/mappers/product.mapper.ts b/server/src/products/infrastructure/mappers/product.mapper.ts index b3edb7f..cd9a2ea 100644 --- a/server/src/products/infrastructure/mappers/product.mapper.ts +++ b/server/src/products/infrastructure/mappers/product.mapper.ts @@ -2,6 +2,7 @@ import { Category } from 'src/categories/domain/category'; import { CategoryEntity } from 'src/categories/infrastructure/entities/category.entity'; import { ProductSizeEntity } from 'src/product-sizes/infrastructure/entities/product-size.entity'; import { Image, Product, ProductInfo, Size } from 'src/products/domain/product'; +import { ProductVisibility } from 'src/products/product-visibility.enum'; import { ProductColorEntity } from '../entities/product-color.entity'; import { ProductImageEntity } from '../entities/product-image.entity'; import { ProductEntity } from '../entities/product.entity'; @@ -60,9 +61,11 @@ export class ProductMapper { product.sizes = domainSizes; product.images = domainImages; product.productInfo = domainProductInfo; + product.visibility = ProductVisibility[raw.visibility]; return product; } + static toPersistence(product: Product): ProductEntity { let category: CategoryEntity | undefined = undefined; if (product.category && product.category.id) { @@ -116,6 +119,7 @@ export class ProductMapper { productEntity.sizes = sizes; productEntity.images = images; productEntity.productColors = productColors; + console.log({ visi: product.visibility }); productEntity.visibility = product.visibility; return productEntity; diff --git a/server/src/products/infrastructure/repositories/product.repository.impl.ts b/server/src/products/infrastructure/repositories/product.repository.impl.ts index 6079f93..1af41bb 100644 --- a/server/src/products/infrastructure/repositories/product.repository.impl.ts +++ b/server/src/products/infrastructure/repositories/product.repository.impl.ts @@ -30,6 +30,8 @@ export class ProductRepositoryImpl implements ProductRepository { async create(data: Product): Promise { const persistenceModel = ProductMapper.toPersistence(data); + console.log({ fromPer: persistenceModel.visibility }); + const entities = await this.productRepository.save( this.productRepository.create(persistenceModel), ); diff --git a/server/src/products/products.controller.ts b/server/src/products/products.controller.ts index 3bd2dae..5019f58 100644 --- a/server/src/products/products.controller.ts +++ b/server/src/products/products.controller.ts @@ -72,6 +72,7 @@ export class ProductsController { @Post() @HttpCode(HttpStatus.CREATED) async createProduct(@Body() data: CreateProductDto): Promise { + console.log(data.visibility); return this.productsService.create(data); } diff --git a/server/src/products/products.service.ts b/server/src/products/products.service.ts index de47f43..f9a42b8 100644 --- a/server/src/products/products.service.ts +++ b/server/src/products/products.service.ts @@ -132,7 +132,7 @@ export class ProductsService { } }); } - + console.log({ formService: clonedPayload.visibility }); return await this.productsRepo.create(clonedPayload); // // Save product sizes