Untitled
const AddProduct = () => { const { replace } = useRouter(); const { store, refetchStore: refetch } = useLayoutContext(); const [addGroupModal, setAddGroupModal] = useState(false); const [createProduct] = useCreateProductMutation(); const type = [{ name: "Type", code: "type" }]; const { show, handleCloseDialog, handleShowDialog } = useQueryString(); const [selectedGroup, setSelectedGroup] = useState<string>(""); const [selected, setSelected] = useState<string>("no"); const editorHeader = editorHeaderTemplate(); const [text, setText] = useState(""); const radioOptions = [ { label: "Yes", value: "yes" }, { label: "No", value: "no" }, ]; const initialValues = { productType: ProductTypeEnum.Physical, productName: "", productDescription: "", imageUrls: [""], groupId: 0, publishStatus: 1, productInventory: { quantity: 0, minimumInventoryCount: 0, }, orderQuantity: { minimumOrderQuantity: 0, maximumOrderQuantity: 0, }, variants: [ { variantName: "", imageUrls: [""], variantOptions: [ { option: "", additionalPrice: 0, }, ], }, ], pricing: { costPrice: 0, price: 0, strikethroughPrice: 0, }, storeId: store?.id || "", }; const validationSchema = object().shape({ productType: number().required("Product type is required"), productName: string().required("Product name is required"), productDescription: string().required("Product description is required"), imageUrls: array() .of(string().required("Image URL is required")) .min(1, "At least one image URL is required"), groupId: number().required("Group ID is required"), publishStatus: number() .oneOf([0, 1], "Invalid publish status") .required("Publish status is required"), productInventory: object().shape({ quantity: number() .min(0, "Quantity must be at least 0") .required("Quantity is required"), minimumInventoryCount: number() .min(0, "Minimum inventory count must be at least 0") .required("Minimum inventory count is required"), }), orderQuantity: object().shape({ minimumOrderQuantity: number() .min(1, "Minimum order quantity must be at least 1") .required("Minimum order quantity is required"), maximumOrderQuantity: number() .min( ref("minimumOrderQuantity"), "Maximum order quantity must be greater than or equal to the minimum order quantity" ) .required("Maximum order quantity is required"), }), variants: array() .of( object().shape({ variantName: string().required("Variant name is required"), imageUrls: array() .of(string().required("Image URL is required")) .min(1, "At least one image URL is required"), variantOptions: array() .of( object().shape({ option: string().required("Option is required"), additionalPrice: number() .min(0, "Additional price must be at least 0") .required("Additional price is required"), }) ) .min(1, "At least one variant option is required"), }) ) .min(1, "At least one variant is required"), pricing: object().shape({ costPrice: number() .min(0, "Cost price must be at least 0") .required("Cost price is required"), price: number() .min(0, "Price must be at least 0") .required("Price is required"), strikethroughPrice: number() .min(0, "Strikethrough price must be at least 0") .notRequired(), }), storeId: string() .uuid("Invalid store ID format") .required("Store ID is required"), }); const handleSubmit = async ( values: ICreateProductRequestBody, actions: FormikHelpers<ICreateProductRequestBody> ) => { try { actions.setSubmitting(true); if (!store?.id) { throw new Error("Store ID is required"); } const payload = { productType: Number(values.productType) as ProductTypeEnum, productName: values.productName, productDescription: values.productDescription, imageUrls: values.imageUrls, groupId: values.groupId, publishStatus: values.publishStatus, productInventory: { quantity: values.productInventory.quantity, minimumInventoryCount: values.productInventory.minimumInventoryCount, }, orderQuantity: { minimumOrderQuantity: values.orderQuantity.minimumOrderQuantity, maximumOrderQuantity: values.orderQuantity.maximumOrderQuantity, }, variants: values.variants.map((variant) => ({ variantName: variant.variantName, imageUrls: variant.imageUrls, variantOptions: variant.variantOptions.map((option) => ({ option: option.option, additionalPrice: option.additionalPrice, })), })), pricing: { costPrice: values.pricing.costPrice, price: values.pricing.price, strikethroughPrice: values.pricing.strikethroughPrice, }, storeId: store?.id, }; const response = await createProduct(payload).unwrap(); if (response) { toast.success("Product created successfully"); } else { toast.error("Failed to create product group"); } refetch(); replace("/products"); } catch (err: unknown) { const errorMessage = getErrorMessage(err); toast.error(errorMessage); } actions.setSubmitting(false); }; return ( <> <AddVariation show={show === "add-variant"} onHide={handleCloseDialog} refetch={refetch} /> <Formik enableReinitialize initialValues={initialValues} validationSchema={validationSchema} onSubmit={handleSubmit}> {(props) => ( <Form onSubmit={props.handleSubmit} className="space-y-6"> <FormTopActions btnType="submit" title="Create Product" disabled={!props.isValid || !props.dirty} loading={props.isSubmitting} /> <SectionWrapper showInner> <div className="mx-auto max-w-[644px] space-y-5 px-4 pb-12 pt-8"> <div className="space-y-[16px]"> <p className="border-b border-grey-100 bg-grey-25 px-[4px] py-[8px] text-base font-[600] text-grey-900"> Product Type </p> <FormSelect filter={false} name="productType" hideTemplate placeholder="Select product type" options={[ { label: "Physical Product", value: ProductTypeEnum.Physical.toString(), }, { label: "Digital Product", value: ProductTypeEnum.Digital.toString(), }, ]} value={String(props.values.productType)} onChange={(value) => props.setFieldValue("productType", value) } message={props.errors.productType} /> </div> <div className="space-y-[16px]"> <p className="border-b border-grey-100 bg-grey-25 px-[4px] py-[8px] text-base font-[600] text-grey-900"> General Information </p> <FormInput label="Product Name" placeholder="Enter Name" {...props.getFieldProps("productName")} touched={props.touched.productName} message={props.errors.productName} /> <div className="relative space-y-2"> <label htmlFor="description" className="form-control-label"> Product Description </label> <Editor placeholder="Enter your store description" value={text} onTextChange={(e: EditorTextChangeEvent) => { setText(e.htmlValue || ""); props.setFieldValue( "productDescription", e.htmlValue || "" ); }} className="form-editor h-full rounded-lg border-grey-100" headerTemplate={editorHeader} /> <ErrorMessage name="productDescription" component="span" className="text-xs text-red-500" /> </div> {props.values.productType === ProductTypeEnum.Digital && ( <div className="space-y-[12px]"> <p className="text-sm font-[500] text-grey-500"> Do you want the product to be accessible on Shopppar ? </p> <RadioButtonInput options={radioOptions} name="addOption" // selectedValue={''} // onChange={''} className="mt-4" /> </div> )} </div> {/* Product Media */} <MediaUpload title="Product Media" {...props.getFieldProps("imageUrls")} message={props.errors.imageUrls} touched={props.touched.imageUrls} /> <div className="space-y-[16px]"> <div className="flex items-center gap-[5px] border-b border-grey-100 bg-grey-25 px-[4px] py-[8px]"> <p className="text-base font-[600] text-grey-900"> Product Group </p> <IoIosInformationCircleOutline size={20} /> </div> <div> <FormSelect filter={false} hideTemplate label="Group (optional)" placeholder="Select product type" name="Group" options={[ { label: "T-shirts", value: "tshirts" }, { label: "Shoes", value: "shoes" }, { label: "Perfumes", value: "perfumes" }, ]} value={selectedGroup} onChange={(value) => setSelectedGroup(value as string)} /> <p className="text-xs font-[500] text-grey-300"> This helps to group similar products on store…e.g T-shirts, Shoes, Perfumes.” </p> </div> <div className="inline-flex"> <Button label="Add New Group" height={40} icon={<FiPlus size={20} />} onPress={() => setAddGroupModal(true)} className="" /> </div> <div className="mt-[24px] space-y-[16px]"> <p className="border-b border-grey-100 bg-grey-25 px-[4px] py-[8px] text-base font-[600] text-grey-900"> Variations </p> <div className="space-y-[12px]"> <p className="text-sm font-[500] text-grey-500"> Does this product have variations (options)? e.g different tiers, packages, add-ons, sizes or colors </p> <RadioButtonInput options={radioOptions} name="addOption" selectedValue={selected} onChange={setSelected} className="mt-4" /> </div> {selected === "yes" && ( <div className=""> <div className="inline-flex"> <Button label="Add Variant" height={40} icon={<FiPlus size={20} />} onPress={handleShowDialog.bind(null, "add-variant")} className="" /> </div> <div className="mt-[24px] w-[644px] rounded-[12px] bg-grey-25 p-[18px]"> <div className="flex items-center justify-between border-b border-gray-200 pb-2"> <div className="w-32 flex-shrink-0"> <FormInput label="Variant Name" placeholder="Enter Name" {...props.getFieldProps( "variants.[0].variantName" )} touched={ props.touched.variants?.[0]?.variantName } /> <ErrorMessage name="variants.[0].variantName" component="span" className="text-xs text-red-500" /> </div> <div className="flex gap-3"> <button className="rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50"> <FiEdit3 size={20} className="text-gray-400 hover:text-gray-600" /> </button> <button className="rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50"> <FaRegTrashAlt size={20} className="text-gray-400 hover:text-gray-600" /> </button> </div> </div> <MediaUpload title="Variant Media" {...props.getFieldProps("variants.0.imageUrls")} touched={props.touched.variants?.[0]?.imageUrls} /> <ErrorMessage name="variants.0.imageUrls" component="span" className="text-xs text-red-500" /> <div className="flex-grow pt-4"> <div className="fle flex items-center gap-[16px]"> <div className="h-24"> <FormInput label="Option" placeholder="Enter option" {...props.getFieldProps( "variants[0].variantOptions[0].option" )} touched={ props.touched.variants?.[0] ?.variantOptions?.[0]?.option } /> <ErrorMessage name="variants[0].variantOptions[0].option" component="span" className="text-xs text-red-500" /> </div> <div className="h-24"> <FormInput name="Additional Price" keyfilter={"pnum"} inputMode="decimal" label="Additional Price" placeholder="₦" value={formatAmount( props.values.variants[0].variantOptions[0] .additionalPrice )} onChange={(e) => handleFormattedAmountChange( e, "variants[0].variantOptions[0].additionalPrice", props.setFieldValue ) } // {...props.getFieldProps( // "variants[0].variantOptions[0].additionalPrice" // )} touched={ props.touched.variants?.[0] ?.variantOptions?.[0]?.additionalPrice } /> <ErrorMessage name="variants[0].variantOptions[0].additionalPrice" component="span" className="text-xs text-red-500" /> </div> <button className="rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50"> <FaRegTrashAlt size={20} className="text-gray-400 hover:text-gray-600" /> </button> </div> </div> </div> </div> )} </div> </div> <div className="my-[24px] space-y-[16px]"> <p className="border-b border-grey-100 bg-grey-25 px-[4px] py-[8px] text-base font-[600] text-grey-900"> Pricing </p> <div className="flex items-center justify-between gap-[20px]"> <div className="flex-grow"> <FormInput name={"costPrice"} value={formatAmount(props.values.pricing.costPrice)} keyfilter={"pnum"} inputMode="decimal" onChange={(e) => handleFormattedAmountChange( e, "pricing.costPrice", props.setFieldValue ) } label="Cost Price" placeholder="₦" touched={props.touched.pricing?.costPrice} message={props.errors.pricing?.costPrice} /> </div> <div className="flex-grow"> <FormInput name={"Product Price"} label="Product Price" placeholder="₦" keyfilter={"pnum"} inputMode="decimal" onChange={(e) => handleFormattedAmountChange( e, "pricing.price", props.setFieldValue ) } value={formatAmount(props.values.pricing.price)} touched={props.touched.pricing?.price} message={props.errors.pricing?.price} /> </div> <div className="flex-grow"> <FormInput name={"DiscountPrice"} placeholder="₦" keyfilter={"pnum"} inputMode="decimal" label="Discount price (optional)" onChange={(e) => handleFormattedAmountChange( e, "pricing.strikethroughPrice", props.setFieldValue ) } value={formatAmount( props.values.pricing.strikethroughPrice )} touched={props.touched.pricing?.strikethroughPrice} message={props.errors.pricing?.strikethroughPrice} /> </div> </div> </div> {props.values.productType === ProductTypeEnum.Physical && ( <> <div className="space-y-[16px]"> <div className="flex items-center gap-[5px] border-b border-grey-100 bg-grey-25 px-[4px] py-[8px]"> <p className="text-base font-[600] text-grey-900"> Product Inventory </p> <IoIosInformationCircleOutline size={20} /> </div> <div className="grid grid-cols-3 items-start gap-[20px]"> <div className="flex-grow"> <FormInput label="Quantity" inputMode="decimal" placeholder="Type quantity" keyfilter={"pnum"} value={formatAmount( props.values.productInventory.quantity )} onChange={(e) => handleFormattedAmountChange( e, "productInventory.quantity", props.setFieldValue ) } touched={props.touched.productInventory?.quantity} message={props.errors.productInventory?.quantity} name="productInventory.quantity" /> </div> <div className="flex-grow"> <FormInput keyfilter={"pnum"} inputMode="decimal" label="Minimum Inventory Level" placeholder="Type Level" name="Minimum Inventory Level" value={formatAmount( props.values.productInventory .minimumInventoryCount )} onChange={(e) => handleFormattedAmountChange( e, "productInventory.minimumInventoryCount", props.setFieldValue ) } touched={ props.touched.productInventory ?.minimumInventoryCount } message={ props.errors.productInventory ?.minimumInventoryCount } /> </div> <div className="flex-grow"> <p className="py-[5px] text-sm font-[500] text-grey-400"> Weight (optional) </p> <DropDownTemplate placeholder="Kg" options={type} showIcon={false} paddingLeft="pl-[0px]" /> </div> </div> </div> <div className="my-[24px] space-y-[16px]"> <div className="flex items-center gap-[5px] border-b border-grey-100 bg-grey-25 px-[4px] py-[8px]"> <p className="text-base font-[600] text-grey-900"> Order Quantity </p> <IoIosInformationCircleOutline size={20} /> </div> <div className="grid grid-cols-2 items-start gap-[20px]"> <div className="flex-grow"> <FormInput keyfilter={"pnum"} label="Minimum Order Quantity" placeholder="Type quantity" name="Minimum Order Quantity" value={formatAmount( props.values.orderQuantity?.minimumOrderQuantity )} onChange={(e) => handleFormattedAmountChange( e, "orderQuantity.minimumOrderQuantity", props.setFieldValue ) } touched={ props.touched.orderQuantity?.minimumOrderQuantity } message={ props.errors.orderQuantity?.minimumOrderQuantity } /> </div> <div className="flex-grow"> <FormInput keyfilter={"pnum"} label="Maximum Order Quantity" placeholder="Type quantity" name="Maximum Order Quantity" onChange={(e) => handleFormattedAmountChange( e, "orderQuantity.maximumOrderQuantity", props.setFieldValue ) } value={formatAmount( props.values.orderQuantity?.maximumOrderQuantity )} touched={ props.touched.orderQuantity?.maximumOrderQuantity } message={ props.errors.orderQuantity?.maximumOrderQuantity } /> </div> </div> </div> </> )} <div className="my-[24px] space-y-[16px]"> <div className="border-b border-grey-100 bg-grey-25 px-[4px] py-[8px]"> <p className="text-base font-[600] text-grey-900"> Product Status </p> </div> <div className=""> <p className="text-sm font-[400] text-grey-400"> Select 'Save as Draft' to save your product but making it unavailable to customers. or choose 'Publish' to make your product visible to customers and available for purchase." </p> </div> <div className="my-[24px] space-y-[16px]"> <div className="space-y-[16px]"> <div className="flex items-center gap-[10px]"> <Field type="checkbox" name="publishStatus" checked={props.values.publishStatus === 0} onChange={() => props.setFieldValue("publishStatus", 0) } className="mr-2 mt-1 bg-grey-0 *:h-4 *:w-4 checked:bg-primary-300" /> <p className="cursor-pointer text-base font-[500] text-grey-900"> Save as draft </p> </div> <div className="flex items-center gap-[10px]"> <Field type="checkbox" name="publishStatus" checked={props.values.publishStatus === 1} onChange={() => props.setFieldValue("publishStatus", 1) } className="mr-2 mt-1 bg-grey-0 *:h-4 *:w-4 checked:bg-primary-300" /> <p className="cursor-pointer text-base font-[500] text-grey-900"> Publish Product </p> </div> <ErrorMessage name="publishStatus" component="span" className="text-xs text-red-500" /> </div> </div> </div> </div> </SectionWrapper> </Form> )} </Formik> <div className=""> {addGroupModal && ( <SidebarLayout visible={addGroupModal} setVisible={setAddGroupModal} content={ <> <AddGroup /> </> } headerText="Create Product Group" /> )} </div> </> ); }; export default AddProduct;
Leave a Comment