Untitled

 avatar
unknown
plain_text
20 days ago
31 kB
4
Indexable

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 &apos;Save as Draft&apos; to save your product but
                      making it unavailable to customers. or choose
                      &apos;Publish&apos; to make your product visible to
                      customers and available for purchase.&quot;
                    </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