create product sides added configure updated
This commit is contained in:
parent
54a88fa9ea
commit
ab8ed33cd2
@ -1,317 +1,657 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import "highlight.js/styles/github.css";
|
||||
import client from "@auth";
|
||||
import { Icon } from "@iconify/react";
|
||||
import MasterLayout from "@/masterLayout/MasterLayout";
|
||||
import Breadcrumb from "@/components/Breadcrumb";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import axios from "axios";
|
||||
import { Baseurl } from "@utils/BaseUrl.utils";
|
||||
import { Baseurl, ImageBase } from "@utils/BaseUrl.utils";
|
||||
import client from "@auth";
|
||||
|
||||
const AddNewProduct = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const category = searchParams.get("category");
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const category = searchParams.get("category");
|
||||
const router = useRouter();
|
||||
|
||||
const [imagePreview, setImagePreview] = useState(null);
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
const [imagePreview, setImagePreview] = useState(null);
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
const [restaruntBranch, setRestaruntBranch] = useState("");
|
||||
const [formData, setFormData] = useState({
|
||||
menuitemname: "",
|
||||
price: 0,
|
||||
is_active: false,
|
||||
is_special: false,
|
||||
availability_time: "",
|
||||
preparation_time: 0,
|
||||
description: "",
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
menuitemname: "",
|
||||
price: 0,
|
||||
is_active: false,
|
||||
is_special: false,
|
||||
availability_time: "",
|
||||
preparation_time: 0,
|
||||
description: "",
|
||||
const [errors, setErrors] = useState({});
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState("");
|
||||
const [sidesCategoryData, setSidesCategoryData] = useState(null);
|
||||
const [sidesDataByCategory, setSidesDataByCategory] = useState({}); // cache per category
|
||||
const [sidesData, setSidesData] = useState(null);
|
||||
|
||||
// selection stored per category
|
||||
const [selectionByCategory, setSelectionByCategory] = useState({}); // { [categoryName]: Set<sideName> }
|
||||
const selectAllRef = useRef(null);
|
||||
|
||||
// derive current category selected set
|
||||
const selectedSides = selectionByCategory[activeCategory] || new Set();
|
||||
|
||||
const isAllSelected =
|
||||
sidesData && sidesData.length > 0 && selectedSides.size === sidesData.length;
|
||||
const isPartialSelected =
|
||||
sidesData &&
|
||||
selectedSides.size > 0 &&
|
||||
selectedSides.size < (sidesData?.length || 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectAllRef.current) {
|
||||
selectAllRef.current.indeterminate = !!isPartialSelected;
|
||||
}
|
||||
}, [isPartialSelected]);
|
||||
|
||||
const toggleSide = (name) => {
|
||||
setSelectionByCategory((prev) => {
|
||||
const prevSet = new Set(prev[activeCategory] || []);
|
||||
if (prevSet.has(name)) prevSet.delete(name);
|
||||
else prevSet.add(name);
|
||||
return { ...prev, [activeCategory]: prevSet };
|
||||
});
|
||||
};
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const toggleSelectAll = () => {
|
||||
if (!sidesData) return;
|
||||
setSelectionByCategory((prev) => {
|
||||
const copy = { ...prev };
|
||||
if (isAllSelected) {
|
||||
copy[activeCategory] = new Set(); // clear
|
||||
} else {
|
||||
copy[activeCategory] = new Set(sidesData.map((s) => s.name));
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: type === "checkbox" ? checked : value,
|
||||
}));
|
||||
useEffect(() => {
|
||||
const restarunt = localStorage.getItem("restaurantbranch");
|
||||
setRestaruntBranch(restarunt);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isLogin = JSON.parse(localStorage.getItem("isLogin"));
|
||||
if (!isLogin) {
|
||||
router.push(`/admin?restaurantbranch=${restaruntBranch}`);
|
||||
}
|
||||
}, [router, restaruntBranch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (restaruntBranch && restaruntBranch !== "") getSidesCategory();
|
||||
}, [restaruntBranch]);
|
||||
|
||||
const getSidesCategory = async () => {
|
||||
try {
|
||||
const res = await client?.get(
|
||||
`/Dine360%20Food%20Sides%20Category?fields=[%22*%22]&limit_page_length=100&filters=[["restaurantbranch","=","${restaruntBranch}"]]`
|
||||
);
|
||||
setSidesCategoryData(res?.data?.data);
|
||||
if (res?.data?.data?.[0]) {
|
||||
const first = res?.data?.data[0]?.name;
|
||||
setActiveCategory(first);
|
||||
await getSideData(first);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching category data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getSideData = async (sidesCategoryName) => {
|
||||
try {
|
||||
const sideRes = await client.get(
|
||||
`/Dine360%20Food%20Sides?fields=[%22*%22]&limit_page_length=100&filters=[["sidecategoryid","=","${sidesCategoryName}"]]`
|
||||
);
|
||||
const data = sideRes?.data?.data || [];
|
||||
setSidesData(data);
|
||||
setSidesDataByCategory((prev) => ({ ...prev, [sidesCategoryName]: data }));
|
||||
// keep existing selectionByCategory for that category (do not clear)
|
||||
} catch (error) {
|
||||
console.error("Error fetching sides data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSidesCategoryClick = async (catname) => {
|
||||
setActiveCategory(catname);
|
||||
await getSideData(catname);
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: type === "checkbox" ? checked : value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files?.[0] ?? null;
|
||||
if (file) {
|
||||
setImageFile(file);
|
||||
setImagePreview(URL.createObjectURL(file));
|
||||
setErrors((prev) => {
|
||||
const { item_image, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImage = () => {
|
||||
setImageFile(null);
|
||||
setImagePreview(null);
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const newErrors = {};
|
||||
if (!formData.menuitemname || formData.menuitemname.trim().length < 3) {
|
||||
newErrors.menuitemname = "Menu Item Name is required (min 3 chars)";
|
||||
}
|
||||
if (!formData.price || Number(formData.price) <= 0) {
|
||||
newErrors.price = "Price must be greater than 0";
|
||||
}
|
||||
if (!formData.availability_time) {
|
||||
newErrors.availability_time = "Availability Time is required";
|
||||
}
|
||||
if (!formData.preparation_time || Number(formData.preparation_time) <= 0) {
|
||||
newErrors.preparation_time = "Preparation Time must be greater than 0";
|
||||
}
|
||||
if (!formData.description || formData.description.trim().length < 5) {
|
||||
newErrors.description = "Description is too short";
|
||||
}
|
||||
if (!imageFile) {
|
||||
newErrors.item_image = "Image is required";
|
||||
}
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const validationErrors = validate();
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
setErrors(validationErrors);
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
|
||||
const body = {
|
||||
menuitemname: formData.menuitemname,
|
||||
price: Number(formData.price),
|
||||
is_active: formData.is_active ? 1 : 0,
|
||||
is_special: formData.is_special ? 1 : 0,
|
||||
availability_time: formData.availability_time,
|
||||
preparation_time: Number(formData.preparation_time),
|
||||
description: formData.description,
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setImageFile(file);
|
||||
setImagePreview(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
try {
|
||||
const formDataToSend = new FormData();
|
||||
formDataToSend.append("endpoint", "Dine360 Menu Category");
|
||||
if (imageFile) {
|
||||
formDataToSend.append("file", imageFile);
|
||||
}
|
||||
formDataToSend.append("fileid", "item_image");
|
||||
formDataToSend.append("childjson", JSON.stringify(body));
|
||||
formDataToSend.append("childkey", "menuitems_child");
|
||||
formDataToSend.append("docname", category ?? "");
|
||||
formDataToSend.append("isimageupdateorcreate", "1");
|
||||
|
||||
const handleRemoveImage = () => {
|
||||
setImageFile(null);
|
||||
setImagePreview(null);
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const newErrors = {};
|
||||
if (!formData.menuitemname || formData.menuitemname.trim().length < 3) {
|
||||
newErrors.menuitemname = "Menu Item Name is required";
|
||||
}
|
||||
if (!formData.price || Number(formData.price) <= 0) {
|
||||
newErrors.price = "Price must be greater than 0";
|
||||
}
|
||||
if (!formData.availability_time) {
|
||||
newErrors.availability_time = "Availability Time is required";
|
||||
}
|
||||
if (!formData.preparation_time || Number(formData.preparation_time) <= 0) {
|
||||
newErrors.preparation_time = "Preparation Time must be greater than 0";
|
||||
}
|
||||
if (!formData.description || formData.description.trim().length < 5) {
|
||||
newErrors.description = "Description is too short";
|
||||
}
|
||||
if (!imageFile) {
|
||||
newErrors.image_item = "Image is required";
|
||||
}
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const validationErrors = validate();
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
setErrors(validationErrors);
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
|
||||
const body =
|
||||
await axios.post(
|
||||
`${Baseurl}/Upload-Image-To-Frappe/parent-child`,
|
||||
formDataToSend,
|
||||
{
|
||||
menuitemname: formData.menuitemname,
|
||||
price: formData.price,
|
||||
is_active: formData.is_active ? 1 : 0,
|
||||
is_special: formData.is_special ? 1 : 0,
|
||||
availability_time: formData.availability_time,
|
||||
preparation_time: formData.preparation_time,
|
||||
description: formData.description,
|
||||
};
|
||||
|
||||
try {
|
||||
// const formDataToSend = new FormData();
|
||||
// formDataToSend.append("endpoint", `Dine360%20Menu%20Category/${category}`);
|
||||
// formDataToSend.append("body", JSON.stringify(body));
|
||||
// formDataToSend.append("file", imageFile); // ✅ use imageFile
|
||||
// formDataToSend.append("fileid", "image_item");
|
||||
|
||||
// const response = await axios.post(`${Baseurl}/Upload-Image-To-Frappe`, formDataToSend, {
|
||||
// headers: {
|
||||
// Authorization: "token 482beca79d9c005:b8778f51fcca82b",
|
||||
// },
|
||||
// });
|
||||
const formDataToSend = new FormData();
|
||||
formDataToSend.append("endpoint", "Dine360 Menu Category");
|
||||
formDataToSend.append("file", imageFile); // ✅ use imageFile
|
||||
formDataToSend.append("fileid", "item_image");
|
||||
formDataToSend.append("childjson", JSON.stringify(body))
|
||||
formDataToSend.append("childkey", "menuitems_child");
|
||||
formDataToSend.append("docname", category);
|
||||
formDataToSend.append("isimageupdateorcreate", 1);
|
||||
console.log(formDataToSend)
|
||||
const response = await axios.post(`${Baseurl}/Upload-Image-To-Frappe/parent-child`, formDataToSend, {
|
||||
headers: {
|
||||
Authorization: "token 482beca79d9c005:b8778f51fcca82b",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("response", response);
|
||||
alert("Form submitted successfully!");
|
||||
router.push("/admin/pos/product-list");
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
headers: {
|
||||
Authorization: "token 482beca79d9c005:b8778f51fcca82b",
|
||||
},
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
return (
|
||||
<MasterLayout>
|
||||
<Breadcrumb title="Create Product" />
|
||||
<div className="container" style={{ marginBottom: "100px" }}>
|
||||
<div className="row gy-4 d-flex justify-content-center">
|
||||
<div className="col-xxl-6 col-xl-6 col-lg-7 col-md-9">
|
||||
<div className="card mt-24 p-lg-3">
|
||||
<div className="card-body">
|
||||
<h6 className="text-xl mb-3">Add New Product</h6>
|
||||
<form onSubmit={handleSubmit} className="d-flex flex-column gap-20">
|
||||
<div className="row">
|
||||
<div className="col-xl-6">
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Menu Item Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="menuitemname"
|
||||
value={formData.menuitemname}
|
||||
onChange={handleChange}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
{errors.menuitemname && (
|
||||
<div className="text-danger small">{errors.menuitemname}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-xl-6">
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">Price ($)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="price"
|
||||
value={formData.price}
|
||||
onChange={handleChange}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
/>
|
||||
{errors.price && (
|
||||
<div className="text-danger small">{errors.price}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
alert("Form submitted successfully!");
|
||||
router.push("/admin/pos/product-list");
|
||||
} catch (error) {
|
||||
console.error("Submission error:", error);
|
||||
alert("Failed to submit. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
<div>
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
rows={4}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
placeholder="Enter description..."
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<div className="text-danger small">{errors.description}</div>
|
||||
)}
|
||||
</div>
|
||||
const handleSaveSides = () => {
|
||||
// aggregate all selected sides across categories
|
||||
const allSelected = [];
|
||||
Object.entries(selectionByCategory).forEach(([cat, setOfNames]) => {
|
||||
const list = sidesDataByCategory[cat] || [];
|
||||
list.forEach((side) => {
|
||||
if (setOfNames.has(side?.name)) {
|
||||
allSelected.push({ ...side, category: cat });
|
||||
}
|
||||
});
|
||||
});
|
||||
console.log("All selected sides (full objects):", allSelected);
|
||||
const names = allSelected.map((s) => s.sidename || s.name);
|
||||
console.log("Selected side names:", names);
|
||||
alert(`Selected sides (${names.length}): ${names.join(", ")}`);
|
||||
};
|
||||
|
||||
<div className="row">
|
||||
<div className="form-check col-6 d-flex justify-content-start gap-2 align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_active"
|
||||
className="form-check-input"
|
||||
checked={formData.is_active}
|
||||
onChange={handleChange}
|
||||
id="is_active"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="is_active">
|
||||
Is Active
|
||||
</label>
|
||||
</div>
|
||||
const resetForm = () => {
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
<div className="form-check col-6 d-flex justify-content-start gap-2 align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_special"
|
||||
className="form-check-input"
|
||||
checked={formData.is_special}
|
||||
onChange={handleChange}
|
||||
id="is_special"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="is_special">
|
||||
Is Special
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-xl-6">
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Availability Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="availability_time"
|
||||
value={formData.availability_time}
|
||||
onChange={handleChange}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
/>
|
||||
{errors.availability_time && (
|
||||
<div className="text-danger small">{errors.availability_time}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-xl-6">
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Preparation Time (minutes)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="preparation_time"
|
||||
value={formData.preparation_time}
|
||||
onChange={handleChange}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
/>
|
||||
{errors.preparation_time && (
|
||||
<div className="text-danger small">{errors.preparation_time}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Menu Item Image
|
||||
</label>
|
||||
<div className="upload-image-wrapper">
|
||||
{imagePreview ? (
|
||||
<div className="uploaded-img position-relative h-160-px w-100 border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveImage}
|
||||
className="uploaded-img__remove position-absolute top-0 end-0 z-1 text-2xxl line-height-1 me-8 mt-8 d-flex"
|
||||
aria-label="Remove uploaded image"
|
||||
>
|
||||
<Icon icon="radix-icons:cross-2" className="text-xl text-danger-600" />
|
||||
</button>
|
||||
<img
|
||||
id="uploaded-img__preview"
|
||||
className="w-100 h-100 object-fit-cover"
|
||||
src={imagePreview}
|
||||
alt="Uploaded"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<label
|
||||
className="upload-file h-160-px w-100 border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50 bg-hover-neutral-200 d-flex align-items-center flex-column justify-content-center gap-1"
|
||||
htmlFor="upload-file"
|
||||
>
|
||||
<Icon icon="solar:camera-outline" className="text-xl text-secondary-light" />
|
||||
<span className="fw-semibold text-secondary-light">Upload</span>
|
||||
<input id="upload-file" type="file" hidden onChange={handleFileChange} />
|
||||
</label>
|
||||
)}
|
||||
{errors.image_item && (
|
||||
<div className="text-danger small">{errors.image_item}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-6">
|
||||
<button
|
||||
className="btn btn-outline-theme radius-8"
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => router.push("/admin/pos/product-list")}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<button type="submit" className="btn btn-bg-theme radius-8" style={{ width: "100%" }}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
return (
|
||||
<MasterLayout>
|
||||
<Breadcrumb title="Create Product" />
|
||||
<div className="container" style={{ marginBottom: "100px" }}>
|
||||
<div className="row gy-4 d-flex justify-content-center">
|
||||
<div className="col-xxl-6 col-xl-6 col-lg-7 col-md-9">
|
||||
<div className="card mt-24 p-lg-3">
|
||||
<div className="card-body">
|
||||
<h6 className="text-xl mb-3">Add New Product</h6>
|
||||
<form
|
||||
className="d-flex flex-column gap-20"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
>
|
||||
{/* ... (form inputs same as before) ... */}
|
||||
<div className="row">
|
||||
<div className="col-xl-6">
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Menu Item Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="menuitemname"
|
||||
value={formData.menuitemname}
|
||||
onChange={handleChange}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
{errors.menuitemname && (
|
||||
<div className="text-danger small">
|
||||
{errors.menuitemname}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-xl-6">
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Price ($)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="price"
|
||||
value={formData.price}
|
||||
onChange={handleChange}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
/>
|
||||
{errors.price && (
|
||||
<div className="text-danger small">{errors.price}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
rows={4}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
placeholder="Enter description..."
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<div className="text-danger small">
|
||||
{errors.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="form-check col-6 d-flex justify-content-start gap-2 align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_active"
|
||||
className="form-check-input"
|
||||
checked={formData.is_active}
|
||||
onChange={handleChange}
|
||||
id="is_active"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="is_active">
|
||||
Is Active
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check col-6 d-flex justify-content-start gap-2 align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_special"
|
||||
className="form-check-input"
|
||||
checked={formData.is_special}
|
||||
onChange={handleChange}
|
||||
id="is_special"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="is_special">
|
||||
Is Special
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-xl-6">
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Availability Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="availability_time"
|
||||
value={formData.availability_time}
|
||||
onChange={handleChange}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
/>
|
||||
{errors.availability_time && (
|
||||
<div className="text-danger small">
|
||||
{errors.availability_time}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-xl-6">
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Preparation Time (minutes)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="preparation_time"
|
||||
value={formData.preparation_time}
|
||||
onChange={handleChange}
|
||||
className="form-control border border-neutral-200 radius-8"
|
||||
/>
|
||||
{errors.preparation_time && (
|
||||
<div className="text-danger small">
|
||||
{errors.preparation_time}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="form-label fw-bold text-neutral-900 mb-0">
|
||||
Menu Item Image
|
||||
</label>
|
||||
<div className="upload-image-wrapper">
|
||||
{imagePreview ? (
|
||||
<div className="uploaded-img position-relative h-160-px w-100 border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveImage}
|
||||
className="uploaded-img__remove position-absolute top-0 end-0 z-1 text-2xxl line-height-1 me-8 mt-8 d-flex"
|
||||
aria-label="Remove uploaded image"
|
||||
>
|
||||
<Icon
|
||||
icon="radix-icons:cross-2"
|
||||
className="text-xl text-danger-600"
|
||||
/>
|
||||
</button>
|
||||
<img
|
||||
id="uploaded-img__preview"
|
||||
className="w-100 h-100 object-fit-cover"
|
||||
src={imagePreview}
|
||||
alt="Uploaded"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<label
|
||||
className="upload-file h-160-px w-100 border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50 bg-hover-neutral-200 d-flex align-items-center flex-column justify-content-center gap-1"
|
||||
htmlFor="upload-file"
|
||||
>
|
||||
<Icon
|
||||
icon="solar:camera-outline"
|
||||
className="text-xl text-secondary-light"
|
||||
/>
|
||||
<span className="fw-semibold text-secondary-light">
|
||||
Upload
|
||||
</span>
|
||||
<input
|
||||
id="upload-file"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
{errors.item_image && (
|
||||
<div className="text-danger small">
|
||||
{errors.item_image}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-theme radius-8"
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Link Sides
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-theme radius-8"
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => router.push("/admin/pos/product-list")}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-bg-theme radius-8"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</MasterLayout>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showModal && (
|
||||
<div
|
||||
className="modal fade show"
|
||||
style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
<div
|
||||
className="modal-dialog modal-xl modal-dialog-centered"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
style={{ backgroundColor: "#f5f6fa" }}
|
||||
>
|
||||
<div className="modal-header border-0 pb-0">
|
||||
<h6 className="modal-title">Link Sides</h6>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
aria-label="Close"
|
||||
onClick={() => setShowModal(false)}
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{/* Select All / Deselect All button */}
|
||||
<div className="d-flex align-items-center mb-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSelectAll}
|
||||
aria-pressed={isAllSelected}
|
||||
className={`btn radius-8 ${
|
||||
isAllSelected ? "btn-bg-theme text-white" : "btn-outline-theme"
|
||||
}`}
|
||||
style={{ display: "inline-flex", alignItems: "center", gap: 6 }}
|
||||
>
|
||||
{isPartialSelected && (
|
||||
<span
|
||||
aria-label="partial"
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#ffc107",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{isAllSelected
|
||||
? "Deselect All"
|
||||
: isPartialSelected
|
||||
? "Select All (partial)"
|
||||
: "Select All"}
|
||||
</span>
|
||||
<small className="text-muted" style={{ marginLeft: 4 }}>
|
||||
({selectedSides.size}/{sidesData?.length || 0})
|
||||
</small>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="row gy-3 gx-3 justify-content-center">
|
||||
<div className="col-xl-3 col-lg-3 col-md-4 mt-3">
|
||||
<div className="d-inline-block" style={{ width: "100%" }}>
|
||||
<div className="card p-0 overflow-hidden radius-12 h-100">
|
||||
<div className="card-body p-24">
|
||||
<ul className="d-flex flex-column gap-2">
|
||||
{sidesCategoryData?.map((sidesCategory) => (
|
||||
<li
|
||||
key={sidesCategory?.name}
|
||||
className={`nav-item border rounded-2 px-3 py-3 bg-border-theme d-flex align-items-center gap-3 position-relative ${
|
||||
activeCategory === sidesCategory.name
|
||||
? "bg-theme text-white"
|
||||
: ""
|
||||
}`}
|
||||
role="presentation"
|
||||
onClick={() =>
|
||||
handleSidesCategoryClick(sidesCategory?.name)
|
||||
}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<img
|
||||
src="/assets/images/menu/menu-icons/all-menu.png"
|
||||
alt="menu"
|
||||
className="w-28-px h-28-px"
|
||||
/>
|
||||
<span className="line-height-1">
|
||||
{sidesCategory?.sidecategoryname}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-xl-9 col-lg-9 col-md-8 mt-3">
|
||||
<div className="row gy-3 gx-3">
|
||||
{sidesData?.map((side) => {
|
||||
const selected = (selectionByCategory[activeCategory] || new Set()).has(
|
||||
side?.name
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="col-xxl-3 col-xl-3 col-lg-4 col-sm-6 col-6 mt-3 cursor-pointer"
|
||||
key={side?.name}
|
||||
onClick={() => toggleSide(side?.name)}
|
||||
>
|
||||
<div
|
||||
className={`card p-3 shadow-2 radius-8 h-100 border position-relative ${
|
||||
selected ? "bg-theme text-white" : "border-white"
|
||||
}`}
|
||||
style={{
|
||||
transition: "background-color .2s, color .2s",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 2,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleSide(side?.name);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
aria-label={`Select ${side?.sidename}`}
|
||||
onChange={() => toggleSide(side?.name)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card-body text-center p-0">
|
||||
<img
|
||||
src={`${ImageBase}/${side.item_image}`}
|
||||
alt={side?.sidename || ""}
|
||||
className="w-50-px h-50-px rounded-circle object-fit-cover"
|
||||
/>
|
||||
<h6 className="text-sm mb-0 mt-3">
|
||||
{side?.sidename}
|
||||
</h6>
|
||||
<span className="text-secondary-light text-sm">
|
||||
${side.price.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* footer actions */}
|
||||
<div className="modal-footer border-0 pt-0">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-bg-theme radius-8"
|
||||
onClick={handleSaveSides}
|
||||
>
|
||||
Save Selected Sides
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</MasterLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNewProduct;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user