-
-
-
+
-
-);
+
+
+
+
+ );
};
-export default CategoryFormModal;
\ No newline at end of file
+export default CategoryFormModal;
diff --git a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/CategorySidebar.jsx b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/CategorySidebar.jsx
index 31bb9b91..677f7738 100644
--- a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/CategorySidebar.jsx
+++ b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/CategorySidebar.jsx
@@ -1,121 +1,247 @@
-import { useState, useEffect } from 'react';
-import { ChevronRight, RefreshCw, FolderPlus } from 'lucide-react';
+import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
+import { ChevronRight, Edit2, FolderPlus, RefreshCw, Trash2 } from 'lucide-react';
+import toast from 'react-hot-toast';
import api from "@services/common/api";
import AdminSkeleton from '../../AdminSkeleton';
import { useAdminTheme } from '@context/AdminThemeContext';
-// ============================================
-// SKELETON (Private)
-// ============================================
-const CategorySkeleton = () => {
- const { isDark } = useAdminTheme();
- return (
-
- {[1, 2, 3, 4].map(i => (
-
- ))}
-
- );
+const getCategoryList = (responseData) => {
+ if (Array.isArray(responseData)) return responseData;
+ return responseData?.data?.content || responseData?.content || [];
+};
+
+const getRelationCount = (category) => {
+ const foodCount = Array.isArray(category.foodIds)
+ ? category.foodIds.length
+ : category.foodIds?.size || 0;
+ const menuCount = Array.isArray(category.menuIds)
+ ? category.menuIds.length
+ : category.menuIds?.size || 0;
+
+ return foodCount + menuCount;
+};
+
+const getErrorMessage = (err, fallback) =>
+ err.response?.data?.message || err.response?.data?.error || err.message || fallback;
+
+const isValidCategoryId = (categoryId) => {
+ const normalizedId = String(categoryId ?? '').trim();
+ const isNumericId = /^\d+$/.test(normalizedId);
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(normalizedId);
+
+ return isNumericId || isUuid;
};
-// ============================================
-// MAIN COMPONENT
-// ============================================
-const CategorySidebar = ({ selectedCategory, onCategoryChange, onAddCategory, setAllCategories }) => {
+const getSafeCategoryId = (categoryId) => {
+ const normalizedId = String(categoryId ?? '').trim();
+
+ if (!isValidCategoryId(normalizedId)) {
+ throw new Error('Invalid category selected');
+ }
+
+ return encodeURIComponent(normalizedId);
+};
+
+const CategorySidebar = forwardRef(({
+ selectedCategory,
+ onCategoryChange,
+ onAddCategory,
+ onEditCategory,
+ setAllCategories,
+ onCategoryDeleted
+}, ref) => {
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [deletingId, setDeletingId] = useState(null);
const { isDark } = useAdminTheme();
- // ------------------------------------
- // FALLBACK DATA
- // ------------------------------------
- const fallbackCategories = [
- { categoryId: 'all', name: 'All Items', count: 24, emoji: '🍽️', foodIds:[] },
- { categoryId: 'main-course', name: 'Main Course', count: 12, emoji: '🥘', foodIds:[] },
- { categoryId: 'starters', name: 'Starters', count: 8, emoji: '🍛', foodIds:[] },
- { categoryId: 'drinks', name: 'Drinks', count: 4, emoji: '🥛', foodIds:[] },
- { categoryId: 'desserts', name: 'Desserts', count: 0, emoji: '🍰', foodIds:[] },
- ];
-
- // ------------------------------------
- // FETCH
- // ------------------------------------
useEffect(() => {
fetchCategories();
}, []);
+ useImperativeHandle(ref, () => ({
+ refreshCategories() {
+ fetchCategories();
+ },
+ refreshCategoryCounts() {
+ fetchCategories();
+ }
+ }));
+
const fetchCategories = async () => {
try {
setLoading(true);
- var response = null;
- response = await api.get(`/secure/api/v1/categories/getallcategories?page=0&size=10&sortBy=name&sortDirection=asc`);
- const data = [
- { categoryId: 'all', name: 'All Items', foodIds: [] },
- ...response.data.data.content
- ];
-
- setCategories(data);
- setAllCategories(response.data.data.content);
+ setError('');
+
+ const response = await api.get('/secure/api/v1/categories/activecategories', {
+ params: { page: 0, size: 100, sort: 'name' }
+ });
+
+ const categoryList = getCategoryList(response.data);
+ const visibleCategories = categoryList.filter((category) => !category.isDelete);
+ setCategories(visibleCategories);
+ setAllCategories(visibleCategories);
} catch (err) {
- console.error('Failed to fetch categories:', err);
- setCategories(fallbackCategories);
- setAllCategories(fallbackCategories);
+ console.error('Failed to fetch categories:', err.response?.data || err);
+ const message = getErrorMessage(err, 'Failed to load categories');
+ setError(message);
+ setCategories([]);
+ setAllCategories([]);
} finally {
setLoading(false);
}
};
- // ------------------------------------
- // RENDER
- // ------------------------------------
+ const handleDelete = async (category) => {
+ let safeCategoryId;
+
+ try {
+ safeCategoryId = getSafeCategoryId(category.categoryId);
+ } catch (err) {
+ console.error('Invalid category id:', category.categoryId);
+ toast.error('Invalid category selected');
+ return;
+ }
+
+ const relationCount = getRelationCount(category);
+ const relationText = relationCount > 0
+ ? ` It is linked to ${relationCount} item${relationCount === 1 ? '' : 's'}.`
+ : '';
+
+ if (!window.confirm(`Delete "${category.name}"?${relationText}`)) return;
+
+ try {
+ setDeletingId(category.categoryId);
+ await api.delete(`/secure/api/v1/categories/delete/${safeCategoryId}`);
+ toast.success('Category deleted successfully');
+
+ setCategories((prev) => prev.filter((item) => item.categoryId !== category.categoryId));
+ setAllCategories((prev) => prev.filter((item) => item.categoryId !== category.categoryId));
+
+ if (selectedCategory === category.categoryId) {
+ onCategoryChange('all');
+ }
+
+ onCategoryDeleted();
+ } catch (err) {
+ console.error('Failed to delete category:', err.response?.data || err);
+ toast.error(getErrorMessage(err, 'Failed to delete category'));
+ } finally {
+ setDeletingId(null);
+ }
+ };
+
+ const sidebarItems = [
+ {
+ categoryId: 'all',
+ name: 'All Items',
+ foodIds: categories.flatMap((category) => category.foodIds || []),
+ menuIds: [],
+ isSystem: true,
+ },
+ ...categories,
+ ];
+
return (
-
+
- {/* Header */}
-
Categories
+
+
Categories
+ {!loading && (
+
+ {categories.length} active
+
+ )}
+
- {/* Categories List */}
{loading ? (
+ ) : error ? (
+
+
{error}
+
+
) : (
- {categories.map((cat) => (
-
);
-};
+});
-export default CategorySidebar;
\ No newline at end of file
+export default CategorySidebar;
diff --git a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemFormModal.jsx b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemFormModal.jsx
index ec410b39..ae314822 100644
--- a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemFormModal.jsx
+++ b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemFormModal.jsx
@@ -10,7 +10,7 @@ const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gi
const getErrorMessage = (err, fallback) =>
err.response?.data?.message || err.response?.data?.error || fallback;
-const MenuFormModal = ({ isOpen, onClose, editingItem, allCategories }) => {
+const MenuFormModal = ({ isOpen, onClose, onSaved, editingItem, allCategories }) => {
const [categories, setCategories] = useState([]);
const [formData, setFormData] = useState({
name: '',
@@ -157,7 +157,11 @@ const MenuFormModal = ({ isOpen, onClose, editingItem, allCategories }) => {
toast.success('Food item created successfully');
}
- onClose();
+ if (onSaved) {
+ onSaved();
+ } else {
+ onClose();
+ }
} catch (err) {
console.error("Failed to save item:", err.response?.data || err);
const message = getErrorMessage(err, 'Failed to save food item');
diff --git a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemsGrid.jsx b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemsGrid.jsx
index 43b307ab..26f653f7 100644
--- a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemsGrid.jsx
+++ b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemsGrid.jsx
@@ -11,7 +11,7 @@ const getPageContent = (data) => {
return data?.content || data?.data?.content || [];
};
-const MenuItemsGrid = forwardRef(({ selectedCategory, onEditItem }, ref) => {
+const MenuItemsGrid = forwardRef(({ selectedCategory, onEditItem, onFoodItemsChanged }, ref) => {
const [menuItems, setMenuItems] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
@@ -108,6 +108,7 @@ const MenuItemsGrid = forwardRef(({ selectedCategory, onEditItem }, ref) => {
...prev,
totalElements: Math.max(prev.totalElements - 1, 0),
}));
+ onFoodItemsChanged?.();
};
const goToPreviousPage = () => {
diff --git a/RestroHub-FrontEnd/src/services/public/ApiService.js b/RestroHub-FrontEnd/src/services/public/ApiService.js
index 9d3c4623..2ec5bd96 100644
--- a/RestroHub-FrontEnd/src/services/public/ApiService.js
+++ b/RestroHub-FrontEnd/src/services/public/ApiService.js
@@ -60,6 +60,7 @@ try {
console.error("Failed to parse response:", err);
throw new Error("Invalid server response");
}
+};
const ApiService = {
// ============================================