menu, menu category modulefully completed menu item create and update page ui updated

This commit is contained in:
alaguraj 2025-07-23 20:28:19 +05:30
parent 5ba2b7581c
commit a75de8c589
5 changed files with 1673 additions and 186 deletions

View File

@ -0,0 +1,309 @@
"use client";
import { useState } 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";
const AddNewProduct = () => {
const searchParams = useSearchParams();
const category = searchParams.get("category");
const router = useRouter();
const [imagePreview, setImagePreview] = useState(null);
const [imageFile, setImageFile] = useState(null);
const [formData, setFormData] = useState({
menuitemname: "",
price: 0,
is_active: false,
is_special: false,
availability_time: "",
preparation_time: 0,
description: "",
});
const [errors, setErrors] = useState({});
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];
if (file) {
setImageFile(file);
setImagePreview(URL.createObjectURL(file));
}
};
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 = {
menucategoryname: category,
description: formData.description,
is_active: 0,
doctype: "Dine360 Menu Category",
menuitems_child: [
{
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,
},
],
};
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",
},
});
console.log("response", response);
alert("Form submitted successfully!");
router.push("/admin/pos/product-list");
} catch (error) {
console.log("error", error);
}
};
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>
<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" 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>
</div>
</div>
</div>
</div>
</MasterLayout>
);
};
export default AddNewProduct;

View File

@ -0,0 +1,851 @@
"use client";
import React, { Suspense, useEffect, useState } from "react";
import MasterLayout from "@/masterLayout/MasterLayout";
import client from "@auth";
import { Icon } from "@iconify/react";
import PageLoader from "@/components/common-component/PageLoader";
import PageNoData from "@/components/common-component/PageNoData";
import Breadcrumb from "@/components/Breadcrumb";
import Link from "next/link";
import { useRouter } from "next/navigation";
const ProductListInner = () => {
const router = useRouter()
const [menuData, setMenuData] = useState(null);
const [menuItems, setMenuItems] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [activeCategory, setActiveCategory] = useState(null);
const [catMenu, setCatMenu] = useState(null);
const [catMenuActive, setCatMenuActive] = useState(null);
const [menuFieldsModel, setMenuFieldsModel] = useState(false);
const [isMenuFieldEditMode, setIsMenuFieldEditMode] = useState(false);
const [editingMenuId, setEditingMenuId] = useState(null);
const [formErrors, setFormErrors] = useState({});
const [menuFieldDeleteConfirm, setMenuFieldDeleteConfirm] = useState({ show: false, id: null });
const [menuFieldsFormData, setMenuFieldsFormData] = useState({
menuname: "",
description: "",
is_active: 0,
});
const [menuCategoryModel, setMenuCategoryModel] = useState(false);
const [isMenuCategoryEditMode, setIsMenuCategoryEditMode] = useState(false);
const [editingMenuCategoryId, setEditingMenuCategoryId] = useState(null);
const [formMenuCategoryErrors, setFormMenuCategoryErrors] = useState({});
const [menuCategoryDeleteConfirm, setMenuCategoryDeleteConfirm] = useState({ show: false, id: null });
const [menuCategoryFormData, setMenuCategoryFormData] = useState({
menucategoryname: "",
description: "",
is_active: 0,
});
const [menuItemDeleteConfirm, setMenuItemDeleteConfirm] = useState({ show: false, id: null });
const [restaruntBranch, setRestaruntBranch] = useState("")
useEffect(() => {
const restarunt = localStorage.getItem("restaurantbranch")
setRestaruntBranch(restarunt)
}, [])
useEffect(() => {
const isLogin = JSON.parse(localStorage.getItem("isLogin"));
if (!isLogin) {
router.push(`/admin?restaurantbranch=${restaruntBranch}`);
}
}, [router]);
useEffect(() => {
getMenuItem();
}, []);
const getMenuItem = async () => {
try {
const res = await client?.get(`/Dine360 Menu?fields=["*"]&limit_page_length=100`);
setCatMenu(res?.data?.data || []);
setCatMenuActive(res?.data?.data[0]?.name || []);
} catch (error) {
console.error("Error fetching menu list:", error);
}
};
useEffect(() => {
if (catMenu?.length > 0) {
getMenuCategoryItems(catMenu[0]?.menuname);
}
}, [catMenu]);
useEffect(() => {
if (menuData?.length > 0) {
getMenuFoodItems(menuData[0]);
setActiveCategory(menuData[0]?.name);
}
}, [menuData]);
const getMenuCategoryItems = async (menuname) => {
try {
setLoading(true);
setError(null);
const menuRes = await client.get(
`/Dine360%20Menu%20Category%20Link?fields=["*"]&limit_page_length=100&filters=[["menu","=","${menuname}"]]`
);
const menuLinks = menuRes?.data?.data || [];
console.log("menuLinks", menuLinks)
const menuCategoryPromises = menuLinks.map(async (menuItem) => {
const res = await client.get(
`/Dine360%20Menu%20Category?fields=["*"]&limit_page_length=100&filters=[["name","=","${menuItem.menucategory}"]]`
);
return res?.data?.data?.[0] || null;
});
const categories = await Promise.all(menuCategoryPromises);
const validCategories = categories.filter(Boolean);
setMenuData(validCategories);
} catch (error) {
console.error("Error fetching menu categories:", error);
setError(error?.message || "Failed to fetch menu categories");
} finally {
setLoading(false);
}
};
const getMenuFoodItems = async (category) => {
if (!category?.name) return;
try {
const res = await client.get(
`/Dine360%20Menu%20Category/${category.name}?fields=["*"]&limit_page_length=100`
);
setMenuItems(res?.data?.data || []);
} catch (error) {
console.error("Error fetching menu items:", error);
}
};
const handleCatMenuClick = async (menuname) => {
await getMenuCategoryItems(menuname);
setCatMenuActive(menuname);
};
const handleMenuClick = async (menu) => {
try {
const res = await client.get(
`/Dine360%20Menu%20Category/${menu.name}?fields=["*"]&limit_page_length=100`
);
setMenuItems(res?.data?.data || []);
setActiveCategory(menu?.name);
} catch (error) {
console.error("Error fetching menu data:", error);
setError(error?.message || "Failed to fetch menu data");
}
};
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setMenuFieldsFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? (checked ? 1 : 0) : value,
}));
};
const openCreateModal = () => {
setIsMenuFieldEditMode(false);
setEditingMenuId(null);
setMenuFieldsFormData({ menuname: "", description: "", is_active: 0 });
setFormErrors({});
setMenuFieldsModel(true);
};
const openEditModal = (menu) => {
setIsMenuFieldEditMode(true);
setEditingMenuId(menu.name);
setMenuFieldsFormData({
menuname: menu.menuname || "",
description: menu.description || "",
is_active: menu.is_active || 0,
});
setFormErrors({});
setMenuFieldsModel(true);
};
const closeModal = () => {
setMenuFieldsModel(false);
setFormErrors({});
};
const handleFormSubmit = async (e) => {
e.preventDefault();
const errors = {};
if (!menuFieldsFormData.menuname.trim()) errors.menuname = "Menu name is required";
if (!menuFieldsFormData.description.trim()) errors.description = "Description is required";
if (Object.keys(errors).length > 0) {
setFormErrors(errors);
return;
}
const body = {
restaurantbranch: restaruntBranch,
menuname: menuFieldsFormData?.menuname,
description: menuFieldsFormData?.description,
is_active: menuFieldsFormData?.is_active
}
try {
if (isMenuFieldEditMode && editingMenuId) {
await client.put(`/Dine360 Menu/${editingMenuId}`, body);
} else {
await client.post(`/Dine360 Menu`, body);
}
closeModal();
getMenuItem();
} catch (error) {
console.error("Error saving menu:", error);
}
};
const handleMenuFieldDelete = async () => {
try {
await client.delete(`/Dine360 Menu/${menuFieldDeleteConfirm.id}`);
setMenuFieldDeleteConfirm({ show: false, id: null });
getMenuItem();
} catch (error) {
if (
error?.response?.data?.exception?.includes("DuplicateEntryError") ||
error?.response?.data?.message?.includes("Duplicate entry")
) {
alert("Menu Name already exists. Please use a different name.");
} else if (error?.response?.data?.exception?.includes("LinkExistsError") ||
error?.response?.data?.message?.includes("LinkExistsError")) {
alert(" Cannot delete or cancel because Dine360 Menu three is linked with Dine360 Menu Category ");
}
}
};
// MenuCategory
const handleMenuCategoryInputChange = (e) => {
const { name, value, type, checked } = e.target;
setMenuCategoryFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? (checked ? 1 : 0) : value,
}));
};
const openMenuCategoryCreateModal = () => {
setIsMenuCategoryEditMode(false);
setEditingMenuCategoryId(null);
setMenuCategoryFormData({ menucategoryname: "", description: "", is_active: 0 });
setFormMenuCategoryErrors({});
setMenuCategoryModel(true);
};
const openMenuCategoryEditModal = (menu) => {
setIsMenuCategoryEditMode(true);
setEditingMenuCategoryId(menu.name);
setMenuCategoryFormData({
menucategoryname: menu.menucategoryname || "",
description: menu.description || "",
is_active: menu.is_active || 0,
});
setFormMenuCategoryErrors({});
setMenuCategoryModel(true);
};
const closeMenuCategoryModal = () => {
setMenuCategoryModel(false);
setFormMenuCategoryErrors({});
};
const handleMenuCategoryFormSubmit = async (e) => {
e.preventDefault();
const errors = {};
if (!menuCategoryFormData.menucategoryname.trim()) errors.menucategoryname = "Menu Category name is required";
if (!menuCategoryFormData.description.trim()) errors.description = "Description is required";
if (Object.keys(errors).length > 0) {
setFormMenuCategoryErrors(errors);
return;
}
const body = {
restaurantbranch: restaruntBranch,
menucategoryname: menuCategoryFormData?.menucategoryname,
description: menuCategoryFormData?.description,
is_active: menuCategoryFormData?.is_active
}
try {
if (isMenuCategoryEditMode && editingMenuCategoryId) {
await client.put(`/Dine360%20Menu%20Category/${editingMenuCategoryId}`, body);
} else {
const res = await client.post(`/Dine360%20Menu%20Category`, body);
const CategoryLinkBody = {
menu: catMenuActive,
menucategory: res?.data?.data?.name,
restaurantbranch: restaruntBranch,
}
await client.post(`/Dine360%20Menu%20Category%20Link`, CategoryLinkBody)
}
closeMenuCategoryModal();
getMenuCategoryItems(catMenuActive);
} catch (error) {
if (
error?.response?.data?.exc_type?.includes("UniqueValidationError") ||
error?.response?.data?.message?.includes("Duplicate entry")
) {
alert("Menu Category Name already exists. Please use a different name.");
} else if (error?.response?.data?.exception?.includes("LinkExistsError") ||
error?.response?.data?.message?.includes("LinkExistsError")) {
alert("Cannot delete or cancel because Dine360 Menu three is linked with Dine360 Menu Category ");
}
}
};
console.log("catMenuActive", catMenuActive)
const handleMenuCategoryDelete = async () => {
try {
await client.delete(`/Dine360%20Menu%20Category%20Link/${catMenuActive} - ${menuCategoryDeleteConfirm.id}`)
await client.delete(`/Dine360%20Menu%20Category/${menuCategoryDeleteConfirm.id}`);
setMenuCategoryDeleteConfirm({ show: false, id: null });
getMenuItem();
} catch (error) {
if (
error?.response?.data?.exception?.includes("DuplicateEntryError") ||
error?.response?.data?.message?.includes("Duplicate entry")
) {
alert("Menu Name already exists. Please use a different name.");
} else if (error?.response?.data?.exception?.includes("LinkExistsError") ||
error?.response?.data?.message?.includes("LinkExistsError")) {
alert("Cannot delete or cancel because Dine360 Menu three is linked with Dine360 Menu Category ");
}
}
};
const handleMenuItemDelete = async () => {
try {
await client.delete(`/Dine360%20Menu%20Category/${activeCategory}/${menuItemDeleteConfirm.id}`);
setMenuItemDeleteConfirm({ show: false, id: null });
getMenuItem();
} catch (error) {
if (
error?.response?.data?.exception?.includes("DuplicateEntryError") ||
error?.response?.data?.message?.includes("Duplicate entry")
) {
alert("Menu Name already exists. Please use a different name.");
} else if (error?.response?.data?.exception?.includes("LinkExistsError") ||
error?.response?.data?.message?.includes("LinkExistsError")) {
alert("Cannot delete or cancel because Dine360 Menu three is linked with Dine360 Menu Category ");
}
}
};
const renderMenuItem = (menu) => (
<div key={menu.name} className="col-xxl-2 col-md-6 user-grid-card">
<div className="border radius-16 overflow-hidden">
<div className="card cursor-pointer position-relative ">
<div className="card-body text-center p-3">
<img
src={menu.profileImg}
alt=""
className="border br-white border-width-2-px w-100-px h-100-px rounded-circle object-fit-cover"
/>
<div className="p-0">
<h6 className="text-lg mb-1 mt-1">{menu.menuitemname}</h6>
<span className="text-secondary-light text-sm lh-sm mb-1">{menu.parent}</span>
<h6 className="text-md mb-0 mt-1">${menu.price?.toFixed(2)}</h6>
</div>
</div>
<div className="position-absolute top-0 end-0 me-1 mt-1" onClick={(e) => e.stopPropagation()}>
<div className="dropdown">
<button
className="btn px-1 py-1 d-flex align-items-center text-primary-light"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="entypo:dots-three-vertical" className="menu-icon" />
</button>
<ul className="dropdown-menu">
<li>
<Link
href="#"
className="dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900"
onClick={(e) => {
e.preventDefault();
router.push(`/admin/pos/update-product?menuitemname=${menu.name}`)
// handleFloorEdit(menu);
}}
>
<Icon icon="lucide:edit" className="menu-icon" />
Edit
</Link>
</li>
<li>
<Link
href="#"
className="dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900"
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); // optional
setMenuItemDeleteConfirm({ show: true, id: menu.name });
}}
>
<Icon icon="fluent:delete-24-regular" className="menu-icon" />
Delete
</Link>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
return (
<>
<div className="container-fluid" style={{ marginBottom: "100px" }}>
{loading ? (
<PageLoader />
) : (
<div className="row gy-4">
<div className="col-xxl-2">
<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">
<li
className="nav-item border rounded-2 px-3 py-3 d-flex align-items-center gap-3 justify-content-between"
style={{ cursor: "pointer" }}
onClick={openCreateModal}
>
<div className="d-flex align-items-center gap-3 ">
<Icon icon="lucide:plus" className="text-lg w-28-px h-28-px " />
<span className="fw-semibold">New</span>
</div>
</li>
{catMenu.map((menu) => (
<li
key={menu?.name}
className={`nav-item border rounded-2 px-3 py-3 bg-border-theme d-flex align-items-center gap-3 position-relative ${catMenuActive === menu.menuname ? "bg-theme" : ""
}`}
role="presentation"
onClick={() => handleCatMenuClick(menu?.menuname)}
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">{menu?.menuname}</span>
<div className="position-absolute top-0 end-0 me-1 mt-1" onClick={(e) => e.stopPropagation()}>
<div className="dropdown">
<button
className="btn px-1 py-1 d-flex align-items-center text-primary-light"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="entypo:dots-three-vertical" className="menu-icon" />
</button>
<ul className="dropdown-menu">
<li>
<Link
href="#"
className="dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900"
onClick={(e) => {
e.preventDefault();
openEditModal(menu);// optional: prevents bubbling from link
// handleFloorEdit(menu);
}}
>
<Icon icon="lucide:edit" className="menu-icon" />
Edit
</Link>
</li>
<li>
<Link
href="#"
className="dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900"
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); // optional
setMenuFieldDeleteConfirm({ show: true, id: menu.name });
}}
>
<Icon icon="fluent:delete-24-regular" className="menu-icon" />
Delete
</Link>
</li>
</ul>
</div>
</div>
</li>
))}
</ul>
</div>
</div>
</div>
<div className="col-xxl-10">
<div className="card p-0 radius-12 mb-3">
<div className="card-body p-24">
<ul className="d-flex gap-2 mb-0 flex-wrap">
{/* Create New Category Button */}
<li
className="nav-item border rounded-2 px-3 py-1 bg-outline-theme d-flex align-items-center gap-2"
style={{ cursor: "pointer" }}
onClick={openMenuCategoryCreateModal} >
<Icon icon="lucide:plus" className="text-lg w-28-px h-28-px " />
<span className="fw-semibold">New</span>
</li>
{/* Menu Items */}
{menuData.map((menu) => (
<li
key={menu?.name}
className={`nav-item border rounded-2 px-3 py-1 bg-outline-theme position-relative d-flex justify-content-between align-items-center gap-3 ${activeCategory === menu.name ? "bg-theme text-white" : ""}`}
role="presentation"
onClick={() => handleMenuClick(menu)} // Keep this
style={{ cursor: "pointer", minWidth: "150px", maxWidth: "220px" }}
>
{/* Left: Icon and Name */}
<div className="d-flex align-items-center gap-2">
<img
src="/assets/images/menu/menu-icons/all-menu.png"
alt="menu icon"
className="w-28-px h-28-px"
/>
<span className="line-height-1">{menu?.menucategoryname}</span>
</div>
{/* Three Dots Menu (Absolute) */}
<div
className="position-absolute"
style={{ top: "4px", right: "4px", zIndex: 100 }}
onClick={(e) => e.stopPropagation()} // Prevent parent click
>
<div className="dropdown">
<button
className="btn px-1 py-1 d-flex align-items-center text-primary-light"
type="button"
data-bs-toggle="dropdown"
>
<Icon icon="entypo:dots-three-vertical" />
</button>
<ul className="dropdown-menu">
<li>
<Link
className="dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900"
href="#"
onClick={(e) => {
e.preventDefault();
openMenuCategoryEditModal(menu);
}}
>
<Icon icon="lucide:edit" /> Edit
</Link>
</li>
<li>
<Link
className="dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900"
href="#"
onClick={(e) => {
e.preventDefault();
setMenuCategoryDeleteConfirm({ show: true, id: menu.name }); // 👈 use menu
}}
>
<Icon icon="fluent:delete-24-regular" /> Delete
</Link>
</li>
</ul>
</div>
</div>
</li>
))}
</ul>
</div>
</div>
<div className="row gy-4">
<div className="col-xxl-2 col-md-6 user-grid-card">
<div className="position-relative border radius-16 overflow-hidden h-100 w-100">
<div className="card cursor-pointer h-100 w-100 d-flex justify-content-center align-items-center" onClick={() => router.push(`/admin/pos/create-product?category=${activeCategory}`)}>
<div className="card-body text-center p-3 d-flex justify-content-center align-items-center">
<div>
<Icon icon="lucide:plus" className="text-lg w-20-px h-20-px " />
<h6 className="text-md mb-0 mt-1">Create</h6>
</div>
</div>
</div>
</div>
</div>
{menuItems?.menuitems_child?.map(renderMenuItem)}
</div>
</div>
</div>
)}
</div>
{/* Modal */}
{menuFieldsModel && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header border-0 pb-0">
<h6 className="modal-title text-lg">{isMenuFieldEditMode ? "Edit Menu" : "Create Menu"}</h6>
<button type="button" className="btn-close" onClick={closeModal}></button>
</div>
<div className="modal-body">
<form onSubmit={handleFormSubmit}>
<div className="mb-3">
<label className="form-label">Menu Name</label>
<input
type="text"
name="menuname"
className={`form-control ${formErrors.menuname ? "is-invalid" : ""}`}
value={menuFieldsFormData.menuname}
onChange={handleInputChange}
/>
{formErrors.menuname && (
<div className="invalid-feedback">{formErrors.menuname}</div>
)}
</div>
<div className="mb-3">
<label className="form-label">Description</label>
<textarea
name="description"
className={`form-control ${formErrors.description ? "is-invalid" : ""}`}
value={menuFieldsFormData.description}
onChange={handleInputChange}
rows="3"
></textarea>
{formErrors.description && (
<div className="invalid-feedback">{formErrors.description}</div>
)}
</div>
<div className="form-check mb-3 d-flex justify-content-start align-items-center gap-2">
<input
type="checkbox"
className="form-check-input"
id="is_active"
name="is_active"
checked={menuFieldsFormData.is_active === 1}
onChange={handleInputChange}
/>
<label className="form-check-label" htmlFor="is_active">
Active
</label>
</div>
<div className="d-flex justify-content-end">
<button type="submit" className="btn btn-bg-theme">
{isMenuFieldEditMode ? "Update" : "Submit"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{menuFieldDeleteConfirm.show && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-body">
<div className="d-flex justify-content-between mb-1">
<h6 className="text-lg mb-0">Confirm Delete</h6>
<button
type="button"
className="btn-close"
onClick={() => setMenuFieldDeleteConfirm({ show: false, id: null })}
></button>
</div>
<p className="m-0">Are you sure you want to delete this Menu?</p>
<div className="d-flex justify-content-end gap-2 mt-1 ">
<button
className="btn btn-outline-danger px-14 py-6 text-sm"
onClick={() => setMenuFieldDeleteConfirm({ show: false, id: null })}
>
Cancel
</button>
<button className="btn btn-danger px-14 py-6 text-sm" onClick={handleMenuFieldDelete}>
Delete
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* Menu category create & Upadte */}
{menuCategoryModel && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header border-0 pb-0">
<h6 className="modal-title text-lg">{isMenuCategoryEditMode ? "Edit Menu Category" : "Create Menu Category"}</h6>
<button type="button" className="btn-close" onClick={closeMenuCategoryModal}></button>
</div>
<div className="modal-body">
<form onSubmit={handleMenuCategoryFormSubmit}>
<div className="mb-3">
<label className="form-label">Menu Category Name</label>
<input
type="text"
name="menucategoryname"
className={`form-control ${formMenuCategoryErrors.menucategoryname ? "is-invalid" : ""}`}
value={menuCategoryFormData.menucategoryname}
onChange={handleMenuCategoryInputChange}
/>
{formMenuCategoryErrors.menucategoryname && (
<div className="invalid-feedback">{formMenuCategoryErrors.menucategoryname}</div>
)}
</div>
<div className="mb-3">
<label className="form-label">Description</label>
<textarea
name="description"
className={`form-control ${formMenuCategoryErrors.description ? "is-invalid" : ""}`}
value={menuCategoryFormData.description}
onChange={handleMenuCategoryInputChange}
rows="3"
></textarea>
{formMenuCategoryErrors.description && (
<div className="invalid-feedback">{formMenuCategoryErrors.description}</div>
)}
</div>
<div className="form-check mb-3 d-flex justify-content-start align-items-center gap-2">
<input
type="checkbox"
className="form-check-input"
id="is_active"
name="is_active"
checked={menuCategoryFormData.is_active === 1}
onChange={handleMenuCategoryInputChange}
/>
<label className="form-check-label" htmlFor="is_active">
Active
</label>
</div>
<div className="d-flex justify-content-end">
<button type="submit" className="btn btn-bg-theme">
{isMenuCategoryEditMode ? "Update" : "Submit"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{menuCategoryDeleteConfirm.show && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-body">
<div className="d-flex justify-content-between mb-1">
<h6 className="text-lg mb-0">Confirm Delete</h6>
<button
type="button"
className="btn-close"
onClick={() => setMenuCategoryDeleteConfirm({ show: false, id: null })}
></button>
</div>
<p className="m-0">Are you sure you want to delete this Menu?</p>
<div className="d-flex justify-content-end gap-2 mt-1 ">
<button
className="btn btn-outline-danger px-14 py-6 text-sm"
onClick={() => setMenuCategoryDeleteConfirm({ show: false, id: null })}
>
Cancel
</button>
<button className="btn btn-danger px-14 py-6 text-sm" onClick={handleMenuCategoryDelete}>
Delete
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{menuItemDeleteConfirm.show && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-body">
<div className="d-flex justify-content-between mb-1">
<h6 className="text-lg mb-0">Confirm Delete</h6>
<button
type="button"
className="btn-close"
onClick={() => setMenuItemDeleteConfirm({ show: false, id: null })}
></button>
</div>
<p className="m-0">Are you sure you want to delete this Menu?</p>
<div className="d-flex justify-content-end gap-2 mt-1 ">
<button
className="btn btn-outline-danger px-14 py-6 text-sm"
onClick={() => setMenuItemDeleteConfirm({ show: false, id: null })}
>
Cancel
</button>
<button className="btn btn-danger px-14 py-6 text-sm" onClick={handleMenuItemDelete}>
Delete
</button>
</div>
</div>
</div>
</div>
</div>
)}
</>
);
};
const ProductList = () => (
<MasterLayout>
<Breadcrumb title="Menu Items" />
<Suspense fallback={<PageLoader />}>
<ProductListInner />
</Suspense>
</MasterLayout>
);
export default ProductList;

View File

@ -0,0 +1,326 @@
"use client";
import { useState } 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 { useSearchParams } from "next/navigation";
const UpdateProduct = () => {
const searchParams = useSearchParams()
const category = searchParams.get('category')
console.log("category", category)
const [imagePreview, setImagePreview] = useState(null);
const [imageFile, setImageFile] = useState(null);
const [formData, setFormData] = useState({
menuitemname: "",
price: 0,
is_active: false,
is_special: false,
availability_time: "",
preparation_time: 0,
description: "",
});
const [errors, setErrors] = useState({});
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];
if (file) {
setImageFile(file);
setImagePreview(URL.createObjectURL(file));
}
};
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 data = new FormData();
data.append("menuitemname", formData.menuitemname);
data.append("price", formData.price.toString());
data.append("is_active", formData.is_active ? "1" : "0");
data.append("is_special", formData.is_special ? "1" : "0");
data.append("availability_time", formData.availability_time);
data.append("preparation_time", formData.preparation_time.toString());
data.append("description", formData.description);
if (imageFile) data.append("image_item", imageFile);
// for (let pair of data.entries()) {
// console.log(pair[0], pair[1]);
// }
const body = {
menucategoryname: category,
description: formData.description,
is_active: 0,
doctype: "Dine360 Menu Category",
menuitems_child: [
{
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,
}
]
}
try {
const res = await client.post(`/Dine360%20Menu%20Category/${category}`, body);
console.log('res', res)
alert("Form submitted successfully!");
}
catch (error) {
console.log("error", error)
}
};
return (
<MasterLayout>
{/* Breadcrumb */}
<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'
htmlFor='title'
>
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">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">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">
{/* Is Active */}
<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>
{/* Is Special */}
<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">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">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'>
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'
></Icon>
</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'
>
<iconify-icon
icon='solar:camera-outline'
className='text-xl text-secondary-light'
></iconify-icon>
<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 type='submit' className='btn btn-outline-theme radius-8' style={{ width: "100%" }}>
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>
</div>
</div>
</div>
</MasterLayout>
);
};
export default UpdateProduct;

View File

@ -10,6 +10,7 @@ import PageNoData from "@/components/common-component/PageNoData";
import Breadcrumb from "@/components/Breadcrumb"; import Breadcrumb from "@/components/Breadcrumb";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { Baseurl, ImageBase } from "@utils/BaseUrl.utils"; import { Baseurl, ImageBase } from "@utils/BaseUrl.utils";
import axios from "axios";
const SidesPageInner = () => { const SidesPageInner = () => {
const router = useRouter(); const router = useRouter();
@ -109,17 +110,36 @@ const SidesPageInner = () => {
// Step 3: Update // Step 3: Update
await client.put(`/Dine360%20Food%20Sides/${itemId}`, body); await client.put(`/Dine360%20Food%20Sides/${itemId}`, body);
} else { } else {
// Step 1: Create without image // Step 1: Create new item with image
const res = await client.post(`/Dine360%20Food%20Sides`, body); const formDataToSend = new FormData();
itemId = res?.data?.data?.name; formDataToSend.append("endpoint", "Dine360 Food Sides");
formDataToSend.append("body", JSON.stringify(body));
// Step 2: Upload image // Append image file (should be File object, not base64)
if (formData.item_image) { formDataToSend.append("file", formData.item_image); // `imageFile` should be a real File from <input type="file" />
const uploadPath = await uploadImage(formData.item_image, "Dine360 Food Sides", itemId); formDataToSend.append("fileid", "item_image");
// Step 3: Update with image const response = await axios.post(`${Baseurl}/Upload-Image-To-Frappe`, formDataToSend, {
await client.put(`/Dine360%20Food%20Sides/${itemId}`, { item_image: uploadPath }); headers: {
} "Authorization": "token 482beca79d9c005:b8778f51fcca82b",
},
});
// Handle response if needed
console.log("Upload success:", response.data);
// // Step 1: Create without image
// const res = await client.post(`/Dine360%20Food%20Sides`, body);
// itemId = res?.data?.data?.name;
// // Step 2: Upload image
// if (formData.item_image) {
// const uploadPath = await uploadImage(formData.item_image, "Dine360 Food Sides", itemId);
// // Step 3: Update with image
// await client.put(`/Dine360%20Food%20Sides/${itemId}`, { item_image: uploadPath });
// }
} }
getSideData(); getSideData();
@ -226,53 +246,53 @@ const SidesPageInner = () => {
<div className={`card p-3 shadow-2 radius-8 h-100 border border-white position-relative`}> <div className={`card p-3 shadow-2 radius-8 h-100 border border-white position-relative`}>
<div className="position-absolute top-0 end-0 me-1 mt-1 d-flex gap-2"> <div className="position-absolute top-0 end-0 me-1 mt-1 d-flex gap-2">
<div className="dropdown"> <div className="dropdown">
<div className='dropdown'> <div className='dropdown'>
<button <button
className='btn px-1 py-1 d-flex align-items-center text-primary-light' className='btn px-1 py-1 d-flex align-items-center text-primary-light'
type='button' type='button'
data-bs-toggle='dropdown' data-bs-toggle='dropdown'
aria-expanded='false' aria-expanded='false'
> >
<Icon icon='entypo:dots-three-vertical' className='menu-icon' /> <Icon icon='entypo:dots-three-vertical' className='menu-icon' />
</button> </button>
<ul className='dropdown-menu'> <ul className='dropdown-menu'>
<li> <li>
<Link <Link
href="#" href="#"
className='dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900' className='dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900'
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleEdit(room); handleEdit(room);
}} }}
> <Icon icon='lucide:edit' className='menu-icon' /> > <Icon icon='lucide:edit' className='menu-icon' />
Edit Edit
</Link> </Link>
</li> </li>
<li> <li>
<Link <Link
href="#" href="#"
className='dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900' className='dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900'
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setDeleteConfirm({ show: true, id: room.name }); setDeleteConfirm({ show: true, id: room.name });
}} }}
> >
<Icon <Icon
icon='fluent:delete-24-regular' icon='fluent:delete-24-regular'
className='menu-icon' className='menu-icon'
/> />
Delete Delete
</Link> </Link>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div className="card-body text-center p-3"> <div className="card-body text-center p-3">
<img <img
src={`${ImageBase}/api${room.item_image}`} src={`${ImageBase}/${room.item_image}`}
alt="" alt=""
className="border br-white border-width-2-px w-100-px h-100-px rounded-circle object-fit-cover" className="border br-white border-width-2-px w-100-px h-100-px rounded-circle object-fit-cover"
/> />
@ -325,31 +345,41 @@ const SidesPageInner = () => {
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Item Image</label> <label className="form-label">Item Image</label>
<div className="d-flex align-items-center gap-3"> <div className="d-flex align-items-center gap-3">
<input type="file" accept="image/*" className={`form-control ${errors.item_image ? "is-invalid" : ""}`} style={{ maxWidth: "350px" }} onChange={(e) => { <input type="file" accept="image/*" className={`form-control ${errors.item_image ? "is-invalid" : ""}`}
const file = e.target.files[0]; style={{ maxWidth: "350px" }}
if (!file) return; onChange={(e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith("image/")) { if (!file.type.startsWith("image/")) {
setErrors(prev => ({ ...prev, item_image: "Only image files are allowed." })); setErrors(prev => ({ ...prev, item_image: "Only image files are allowed." }));
return; return;
} }
if (file.size / (1024 * 1024) > 1) { if (file.size / (1024 * 1024) > 1) {
setErrors(prev => ({ ...prev, item_image: "Max size 1MB" })); setErrors(prev => ({ ...prev, item_image: "Max size 1MB" }));
return; return;
} }
const reader = new FileReader(); setFormData(prev => ({ ...prev, item_image: file })); // Save raw File
reader.onloadend = () => {
setFormData(prev => ({ ...prev, item_image: reader.result }));
setErrors(prev => ({ ...prev, item_image: null })); setErrors(prev => ({ ...prev, item_image: null }));
}; }}
reader.readAsDataURL(file); />
}} />
{formData.item_image && ( {formData.item_image && (
<div style={{ position: "relative", width: "60px", height: "60px" }}> <div style={{ position: "relative", width: "60px", height: "60px" }}>
<img src={formData.item_image} alt="preview" className="img-thumbnail" style={{ objectFit: "cover" }} /> <img
<button type="button" className="btn btn-sm btn-danger position-absolute top-0 end-0 p-1 pt-0 pb-0" onClick={() => setFormData(prev => ({ ...prev, item_image: null }))}>×</button> src={URL.createObjectURL(formData.item_image)}
alt="preview"
className="img-thumbnail"
style={{ objectFit: "cover" }}
/>
<button
type="button"
className="btn btn-sm btn-danger position-absolute top-0 end-0 p-1 pt-0 pb-0"
onClick={() => setFormData(prev => ({ ...prev, item_image: null }))}
>
×
</button>
</div> </div>
)} )}
</div> </div>

View File

@ -8,96 +8,74 @@ import { useRouter, useSearchParams } from "next/navigation";
const DayReservationTableEnableInner = () => { const DayReservationTableEnableInner = () => {
const router = useRouter(); const router = useRouter();
// const day_name = params.name;
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const day = searchParams.get('day'); const day = searchParams.get('day');
const day_name = searchParams.get('name'); const day_name = searchParams.get('name');
const [tables, setTables] = useState([]); const [tables, setTables] = useState([]);
const [showModal, setShowModal] = useState(false);
const [selectedTable, setSelectedTable] = useState(null);
const [error, setError] = useState('');
const [tableConfigs, setTableConfigs] = useState([]); const [tableConfigs, setTableConfigs] = useState([]);
const [selectedTable, setSelectedTable] = useState(null);
const [showModal, setShowModal] = useState(false);
const [formData, setFormData] = useState({ enabled: false, start_time: '', end_time: '', interval: '' });
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [restaurantBranch, setRestaurantBranch] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState({ useEffect(() => {
enabled: false, const branch = localStorage.getItem('restaurantbranch');
start_time: '', setRestaurantBranch(branch);
end_time: '', }, []);
interval: ''
});
const [restaruntBranch, setRestaruntBranch] = useState("")
useEffect(() => { useEffect(() => {
const restarunt = localStorage.getItem("restaurantbranch") const isLogin = JSON.parse(localStorage.getItem('isLogin'));
setRestaruntBranch(restarunt)
}, [])
useEffect(() => {
const isLogin = JSON.parse(localStorage.getItem("isLogin"));
if (!isLogin) { if (!isLogin) {
router.push(`/admin?restaurantbranch=${restaruntBranch}`); router.push(`/admin?restaurantbranch=${restaurantBranch}`);
} }
}, [router]); }, [router, restaurantBranch]);
useEffect(() => { useEffect(() => {
if (restaruntBranch && restaruntBranch !== "") if (restaurantBranch) getTableData();
getTableData(); }, [restaurantBranch]);
}, [restaruntBranch]);
const getTableData = async () => { const getTableData = async () => {
try { try {
setLoading(true) setLoading(true);
const [tableRes, enableTableRes] = await Promise.all([ const [tableRes, configRes] = await Promise.all([
// client.get(`/Dine360%20Table?fields=["*"]`), client.get(`/Dine360%20Table?fields=["*"]&filters=[["restaurantbranch","=","${restaurantBranch}"]]`),
client.get(`/Dine360%20Table?fields=["*"]&filters=[["restaurantbranch","=","${restaruntBranch}"]]`),
client.get(`/Dine360%20Days/${day_name}?fields=["*"]`) client.get(`/Dine360%20Days/${day_name}?fields=["*"]`)
]); ]);
const allTables = tableRes.data.data || []; const allTables = tableRes.data.data || [];
const enabledTables = enableTableRes.data.data?.table_configuration || []; const savedConfigs = configRes.data.data?.table_configuration || [];
setTableConfigs(enabledTables); setTableConfigs(savedConfigs);
const enabledNames = new Set(enabledTables.map((t) => t.table)); const enabledNames = new Set(savedConfigs.filter(t => t.enabled).map(t => t.table));
const mergedTables = allTables.map((table) => ({ const merged = allTables.map(table => ({
...table, ...table,
enabled: enabledNames.has(table.name) enabled: enabledNames.has(table.name)
})); }));
setTables(mergedTables); setTables(merged);
setLoading(false) setLoading(false);
} catch (error) { } catch (error) {
console.error("Error fetching table or enabled list:", error); console.error('Error loading table data:', error);
setLoading(false) setLoading(false);
} }
}; };
console.log("tableConfigs", tableConfigs)
console.log("tables", tables)
const formatTime = (timeStr) => {
if (!timeStr) return '';
const parts = timeStr.split(':');
return parts.length >= 2 ? `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}` : '';
};
const handleTableClick = (table) => { const handleTableClick = (table) => {
setSelectedTable(table); setSelectedTable(table);
setError('');
const existingConfig = tableConfigs.find(cfg => cfg.table === table.name); const existing = tableConfigs.find(cfg => cfg.table === table.name);
if (existingConfig) {
if (existing && existing.enabled) {
setFormData({ setFormData({
enabled: true, enabled: true,
start_time: formatTime(existingConfig.start_time), start_time: existing.start_time?.slice(0, 5) || '',
end_time: formatTime(existingConfig.end_time), end_time: existing.end_time?.slice(0, 5) || '',
interval: existingConfig.interval?.toString() || '' interval: existing.interval?.toString() || ''
}); });
} else { } else {
setFormData({ setFormData({
@ -109,72 +87,59 @@ const DayReservationTableEnableInner = () => {
} }
setShowModal(true); setShowModal(true);
setErrors({});
}; };
const handleChange = (e) => { const handleChange = (e) => {
const { name, value, type, checked } = e.target; const { name, value, type, checked } = e.target;
setFormData((prev) => ({ setFormData(prev => ({
...prev, ...prev,
[name]: type === 'checkbox' ? checked : value [name]: type === 'checkbox' ? checked : value
})); }));
}; };
const handleSubmit = async (e) => { const handleSubmit = () => {
e.preventDefault(); const errs = {};
if (formData.enabled) {
if (!formData.start_time) errs.start_time = 'Start time required';
if (!formData.end_time) errs.end_time = 'End time required';
if (!formData.interval || isNaN(formData.interval) || formData.interval <= 0) {
errs.interval = 'Positive interval required';
}
}
const newErrors = {}; if (Object.keys(errs).length > 0) {
setErrors(errs);
if (!formData.enabled) {
// Remove any config for this table when disabling
setTableConfigs(prev => prev.filter(cfg => cfg.table !== selectedTable?.name));
setShowModal(false);
return; return;
} }
if (!formData.start_time) { const updatedConfig = formData.enabled
newErrors.start_time = 'Start time is required.'; ? {
} day,
if (!formData.end_time) { table: selectedTable.name,
newErrors.end_time = 'End time is required.'; enabled: 1,
} start_time: `${formData.start_time}:00`,
if (!formData.interval) { end_time: `${formData.end_time}:00`,
newErrors.interval = 'Interval is required.'; interval: Number(formData.interval)
} else if (isNaN(formData.interval) || formData.interval <= 0) { }
newErrors.interval = 'Interval must be a positive number.'; : {
} day,
table: selectedTable.name,
enabled: 0,
start_time: null,
end_time: null,
interval: null
};
setErrors(newErrors); setTableConfigs(prev => {
if (Object.keys(newErrors).length > 0) return; const others = prev.filter(cfg => cfg.table !== selectedTable.name);
return [...others, updatedConfig];
const formatTime = (timeStr) => {
if (!timeStr) return '';
const [hour, minute] = timeStr.split(':');
const hh = hour.padStart(2, '0');
const mm = minute.padStart(2, '0');
return `${hh}:${mm}:00`;
};
const newConfig = {
day,
table: selectedTable?.name,
enabled: 1,
start_time: formatTime(formData.start_time),
end_time: formatTime(formData.end_time),
interval: Number(formData.interval)
};
// Replace or add only enabled configs
setTableConfigs(prevConfigs => {
const filtered = prevConfigs.filter(cfg => cfg.table !== newConfig.table);
return [...filtered, newConfig];
}); });
setShowModal(false); setShowModal(false);
setErrors({});
setFormData({ enabled: false, start_time: '', end_time: '', interval: '' });
}; };
const handleModelClose = () => { const handleModelClose = () => {
setShowModal(false); setShowModal(false);
setErrors({}); setErrors({});
@ -182,34 +147,40 @@ const DayReservationTableEnableInner = () => {
}; };
const handleSaveAllChanges = async () => { const handleSaveAllChanges = async () => {
if (tableConfigs.length === 0) { const confirm = window.confirm('Are you sure you want to save all changes?');
alert("No changes to save."); if (!confirm) return;
return;
}
const confirmed = window.confirm("Are you sure you want to save all changes?"); const fullConfigs = tables.map(table => {
if (!confirmed) return; const cfg = tableConfigs.find(c => c.table === table.name);
return cfg
const payload = { ? cfg
name: day_name, : {
table_configuration: tableConfigs day,
}; table: table.name,
enabled: 0,
start_time: null,
end_time: null,
interval: null
};
});
try { try {
await client.put(`/Dine360%20Days/${day_name}`, payload, { await client.put(`/Dine360%20Days/${day_name}`, {
name: day_name,
table_configuration: fullConfigs
}, {
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}); });
alert("All table configurations saved successfully!"); alert('Saved successfully!');
setTableConfigs([]); setTableConfigs([]);
getTableData(); getTableData();
} catch (error) { } catch (error) {
console.error("Error saving all changes:", error); console.error('Save failed:', error);
alert("Failed to save configurations."); alert('Failed to save changes.');
} }
}; };
const sortedTables = [...tables].sort((a, b) => a.tablename.localeCompare(b.tablename)); const sortedTables = [...tables].sort((a, b) => a.tablename.localeCompare(b.tablename));
console.log("sortedTables", sortedTables)
return ( return (
<> <>