Untitled
unknown
plain_text
9 months ago
31 kB
6
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 '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;
Editor is loading...
Leave a Comment