Untitled

 avatar
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 &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>
    </>
  );
};

export default ProductForm;
Editor is loading...
Leave a Comment