Untitled
unknown
plain_text
2 months ago
34 kB
4
Indexable
const ProductForm = () => { const { id } = useParams(); const { replace } = useRouter(); const { store } = useLayoutContext(); const [createProduct] = useCreateProductMutation(); const [updateProduct] = useUpdateProductMutation(); const { data: productGroups, refetch: refetchProductGroups } = useGetProductGroupsQuery( { storeId: store?.id ?? "", paginateResponse: "false" }, { refetchOnMountOrArgChange: true, skip: !store?.id, } ); const { data: productById } = useGetProductByIdQuery({ productId: id as string, storeId: store?.id ?? "", }); const selectProductGroups = useMemo( () => productGroups?.result?.map((group) => ({ label: group.groupName, value: group.id.toString(), })) || [], [productGroups?.result] ); const { show, handleCloseDialog, handleShowDialog } = useQueryString(); const [selected, setSelected] = useState<string>("no"); const editorHeader = editorHeaderTemplate(); const radioOptions = [ { label: "Yes", value: "yes" }, { label: "No", value: "no" }, ]; const { selectedFiles, setSelectedFiles, featuredUrl, setFeaturedUrl, fileUrls, loading, } = useAzureStorage(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const initialValues: any = useMemo(() => { if (productById) { return { productId: productById?.id || "", productType: productById?.productType || "", productName: productById?.productName || "", imageUrls: productById.images || [""], productDescription: productById?.productDescription || "", publishStatus: productById?.publishStatus || 1, costPrice: productById?.costPrice || 0, price: productById?.price || 0, strikethroughPrice: productById?.strikethroughPrice || 0, variants: productById?.productVariants?.map((variant) => ({ variantName: variant?.variantName || "", imageUrls: variant?.imageUrls || [""], variantOptions: variant?.variantOptions?.map((option) => ({ option: option?.option || "", additionalPrice: option?.additionalPrice || 0, })) || [], })) || [], onlyShooper: "no", weightType: "kg", groupId: 0, quantity: productById?.inventory || 0, minimumInventoryCount: productById?.minimumInventoryCount || 0, minimumOrderQuantity: productById?.minimumOrderQuantity || 0, maximumOrderQuantity: productById?.maximumOrderQuantity || 0, storeId: store?.id || "", }; } return { 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 || "", onlyShooper: "no", weightType: "kg", }; }, [productById, 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"), 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().notRequired(), minimumInventoryCount: number().notRequired(), }), orderQuantity: object().shape({ minimumOrderQuantity: number().notRequired(), maximumOrderQuantity: number().notRequired(), }), variants: array() .of( object().shape({ variantName: string(), // imageUrls: array().of(string()).notRequired(), variantOptions: array().of( object().shape({ option: string(), additionalPrice: number(), }) ), }) ) .notRequired(), 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().notRequired(), }), storeId: string(), onlyShooper: string().oneOf(["yes", "no"]).notRequired(), weightType: string().oneOf(["kg", "gram"]).notRequired(), }); 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: fileUrls.slice(0, 3).map((file) => file.url), groupId: Number(values.groupId), publishStatus: values.publishStatus, productInventory: { quantity: values.productInventory.quantity, minimumInventoryCount: values.productInventory.minimumInventoryCount, }, orderQuantity: { minimumOrderQuantity: values.orderQuantity.minimumOrderQuantity, maximumOrderQuantity: values.orderQuantity.maximumOrderQuantity, }, variants: selected === "yes" ? values.variants.map((variant) => ({ variantName: variant.variantName, imageUrls: fileUrls.slice(0, 3).map((file) => file.url), 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, onlyShooper: values.onlyShooper, weightType: values.weightType, }; const res = id ? await createProduct(payload).unwrap() : await updateProduct(payload).unwrap(); toast.success( typeof res?.data === "string" ? res?.data : `Products ${id ? "updated" : "created"} successfully` ); setSelectedFiles([]); replace("/products"); } catch (err: unknown) { const errorMessage = getErrorMessage(err); toast.error(errorMessage); } actions.setSubmitting(false); }; const formik = useFormik({ initialValues, validationSchema, onSubmit: handleSubmit, }); return ( <> <AddGroup show={show === "add-product-group"} onHide={handleCloseDialog} dataId={""} refetch={refetchProductGroups} /> <AddVariation formik={formik} show={show === "add-variant"} onHide={handleCloseDialog} /> <Formik enableReinitialize initialValues={initialValues} validationSchema={validationSchema} onSubmit={handleSubmit}> {(props) => { return ( <Form onSubmit={props.handleSubmit} className="space-y-6"> <FormTopActions btnType="submit" title={`${id ? "Edit" : "Create"} Product`} disabled={!props.isValid} 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) } touched={props.touched.productType} 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={props.values.productDescription} onTextChange={(e: EditorTextChangeEvent) => { 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> {Number(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 Shooppar? </p> <RadioButtonInput options={radioOptions} name="onlyShooper" selectedValue={props.values.onlyShooper} onChange={(value) => props.setFieldValue("onlyShooper", value) } className="mt-4" /> </div> )} </div> {/* Product Media */} <MediaUpload text="Images (Recommended dimension 930px x 1136px)" loading={loading} title="Product Media" setSelectedFiles={setSelectedFiles} selectedFiles={selectedFiles} desc="Maximum of {maxFiles} images ({maxSize}MB) // File format: PNG and JPEG" /> <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={selectProductGroups} value={String(props.values.groupId)} onChange={(value) => props.setFieldValue("groupId", 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={handleShowDialog.bind( null, "add-product-group" )} className="" /> </div> {/* PRICING */} <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> <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-40 flex-shrink-0"> <FormInput className="w-full" label="Variant Name" placeholder="Enter Name" {...props.getFieldProps( "variants.[0].variantName" )} touched={ ( props.touched .variants as FormikTouched<IVariant>[] )?.[0]?.variantName } /> </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 text="Images (Recommended dimension 930px x 1136px)" title="Variant Images" loading={loading} desc="Maximum of {maxFiles} images ({maxSize}MB) // File format: PNG and JPEG" selectedFiles={featuredUrl} setSelectedFiles={setFeaturedUrl} /> <div className="flex-grow pt-5"> <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 } /> </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 ) } touched={ props.touched.variants?.[0] ?.variantOptions?.[0]?.additionalPrice } /> </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> {Number(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"> <FormSelect readOnly filter={false} hideTemplate label="Weight (optional)" placeholder="Kg" name="weight" value={props.values.weightType} onChange={(value) => props.setFieldValue("weightType", value) } options={[ { label: "Kg", value: "kg", }, { label: "Gram", value: "gram", }, ]} /> </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> </> ); }; export default ProductForm;
Editor is loading...
Leave a Comment