1
0

Initial commit - restaurant dashboard

هذا الالتزام موجود في:
RaghadMAlkous
2025-09-04 01:17:15 +03:00
الأصل 13891b47fd
التزام 7b2f8840cb
136 ملفات معدلة مع 16638 إضافات و6033 حذوفات

عرض الملف

@@ -0,0 +1,206 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery, Typography } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import AccountSettings from './contect/AccountSettings';
import MealsByCateg from './contect/MealsByCateg';
import AllMeals from './contect/AllMeals';
import ProductDetail from './contect/ProductDetail';
import authService from '../../../services/authService';
import { useRestaurant } from '../../../contexts/RestaurantContext';
const drawerWidth = 230;
const Meal = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const { restaurantId } = useRestaurant();
const [showAllPopular, setShowAllPopular] = useState(false);
const [categories, setCategories] = useState([]);
const [selectedCategoryId, setSelectedCategoryId] = useState(null);
const [categoryName, setCategoryName] = useState('');
const [selectedProduct, setSelectedProduct] = useState(null);
const [meals, setMeals] = useState([]);
const [loadingMeals, setLoadingMeals] = useState(false);
// جلب الفئات عند التحميل
useEffect(() => {
const fetchCategories = async () => {
if (!restaurantId) return;
const result = await authService.getCategoriesByRestaurant(restaurantId);
if (result.success) setCategories(result.data);
else setCategories([]);
};
fetchCategories();
}, [restaurantId]);
// تحديث اسم الفئة عند تغيير selectedCategoryId أو categories
useEffect(() => {
if (selectedCategoryId && categories.length > 0) {
const selected = categories.find(c => c.id === selectedCategoryId);
setCategoryName(selected ? selected.name : '');
} else {
setCategoryName('');
}
}, [selectedCategoryId, categories]);
// جلب الوجبات عند اختيار الفئة
useEffect(() => {
const fetchMeals = async () => {
if (selectedCategoryId && restaurantId) {
setLoadingMeals(true);
try {
const result = await authService.getMealsByCategory(restaurantId, selectedCategoryId);
if (result.success) {
const selectedCategory = categories.find(c => c.id === selectedCategoryId);
const mealsWithImages = result.data.map(meal => ({
id: meal.id,
name: meal.name,
price: meal.price,
unit: meal.unit || '/pcs',
image: meal.photo || '/images/default-product.png',
category: selectedCategory ? selectedCategory.name : '', // اسم الفئة
category_id: meal.category_id,
restaurant_id: meal.restaurant_id,
description: meal.description || [],
additions: meal.additions || []
}));
setMeals(mealsWithImages);
} else setMeals([]);
} catch (err) {
console.error(err);
setMeals([]);
} finally {
setLoadingMeals(false);
}
} else setMeals([]);
};
fetchMeals();
}, [selectedCategoryId, restaurantId, categories]);
const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen);
const toggleShowAllPopular = () => setShowAllPopular(prev => !prev);
return (
<Box sx={{ display: 'flex', height: '100vh', backgroundColor: '#F6F6F6', overflow: 'hidden' }}>
<Sidebar
open={sidebarOpen}
onClose={handleDrawerToggle}
isMobile={isMobile}
drawerWidth={drawerWidth}
/>
<Box
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
width: '10%',
marginLeft: { xs: 0, sm: sidebarOpen ? `${drawerWidth}px` : 0, md: 0 },
transition: theme.transitions.create(['width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}
>
<KitchPlusAppBar
onDrawerToggle={handleDrawerToggle}
sidebarOpen={sidebarOpen}
isMobile={isMobile}
/>
<Box
sx={{
flexGrow: 1,
width: sidebarOpen ? 'calc(100% - 40px)' : '100%',
pt: { xs: 2, sm: 3 },
overflowY: 'auto',
pl: { xs: 1, sm: 2 },
pr: { xs: 1, sm: 2 },
md: 10,
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
}}
>
{selectedProduct ? (
<ProductDetail
product={selectedProduct}
onBack={() => setSelectedProduct(null)}
/>
) : (
<>
<AccountSettings
selectedCategory={selectedCategoryId}
setSelectedCategory={setSelectedCategoryId}
restaurantId={restaurantId}
/>
{selectedCategoryId ? (
<Box sx={{ mt: 3, md: 10, }}>
<MealsByCateg
categoryId={selectedCategoryId}
restaurantId={restaurantId}
categoryName={categoryName}
onBack={() => setSelectedCategoryId(null)}
onProductClick={(meal) => {
const category = categories.find(c => c.id === meal.category_id);
setSelectedProduct({
...meal,
category: category ? category.name : '',
category_id: meal.category_id,
restaurant_id: restaurantId
});
}}
/>
</Box>
) : !showAllPopular ? (
<Box
sx={{
mt: 3,
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: 'stretch',
gap: 2,
}}
>
<Box
sx={{
flexBasis: { xs: '100%', md: '100%' },
display: 'flex',
flexDirection: 'column',
pb: { xs: 4, md: 4 },
pr: { xs: 1, md: 0 },
mb: 5,
}}
>
<AllMeals
restaurantId={restaurantId}
categories={categories}
showAll={showAllPopular}
onToggleShowAll={toggleShowAllPopular}
onProductClick={(meal) => {
const category = categories.find(c => c.id === meal.category_id);
setSelectedProduct({
...meal,
category: category ? category.name : '',
category_id: meal.category_id,
restaurant_id: restaurantId
});
}}
/>
</Box>
</Box>
) : null}
</>
)}
</Box>
</Box>
</Box>
);
};
export default Meal;

عرض الملف

@@ -0,0 +1,330 @@
import React, { useState, useEffect, useRef } from 'react';
import {
Box,
Typography,
Button,
IconButton,
Menu,
MenuItem,
Modal,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
useTheme,
useMediaQuery,
Skeleton,
} from '@mui/material';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import CategoryScrollList from './CategoryScrollList';
import AddCategory from './AddCategory';
import authService from '../../../../services/authService';
const AccountSettings = ({ selectedCategory, setSelectedCategory, restaurantId }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true); // <-- حالة التحميل
const [selectedCategoryForEdit, setSelectedCategoryForEdit] = useState(null);
const [contextMenu, setContextMenu] = useState(null);
const [openAddModal, setOpenAddModal] = useState(false);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const scrollRef = useRef();
// تحميل الفئات عند فتح الصفحة
useEffect(() => {
const fetchCategories = async () => {
setLoading(true); // بدء التحميل
const result = await authService.getCategoriesByRestaurant(restaurantId);
if (result.success) setCategories(result.data);
else console.error(result.message);
setLoading(false); // انتهاء التحميل
};
if (restaurantId) fetchCategories();
}, [restaurantId]);
useEffect(() => {
if (selectedCategory) localStorage.setItem('selectedCategoryId', selectedCategory);
}, [selectedCategory]);
const scroll = (offset) => {
if (scrollRef.current) scrollRef.current.scrollLeft += offset;
};
const handleAddCategory = async (payload) => {
try {
const response = await authService.addCategory(payload);
setCategories((prev) => [...prev, response.category]);
setOpenAddModal(false);
} catch (error) {
console.error('خطأ أثناء الإضافة:', error);
}
};
const handleUpdateCategory = async (payload) => {
try {
const response = await authService.updateCategory(selectedCategoryForEdit.id, payload);
setCategories((prev) =>
prev.map((cat) => (cat.id === selectedCategoryForEdit.id ? response.category : cat))
);
setOpenAddModal(false);
} catch (error) {
console.error('خطأ أثناء التعديل:', error);
}
};
const handleDeleteCategory = async () => {
try {
await authService.deleteCategory(selectedCategoryForEdit.id);
setCategories((prev) =>
prev.filter((cat) => cat.id !== selectedCategoryForEdit.id)
);
setConfirmDeleteOpen(false);
setSelectedCategoryForEdit(null);
if (selectedCategory === selectedCategoryForEdit?.id) {
setSelectedCategory(null);
localStorage.removeItem('selectedCategoryId');
}
} catch (error) {
console.error('خطأ أثناء الحذف:', error);
}
};
useEffect(() => {
if (!openAddModal) setSelectedCategoryForEdit(null);
}, [openAddModal]);
return (
<Box
sx={{
pl: { xs: 2, sm: 3 },
pr: { xs: 2, sm: 3 },
pb: { xs: 2, sm: 3 },
pt: { xs: 2, sm: 1.5 },
backgroundColor: '#FFFFFF',
maxWidth: { xs: '90%', md: '100%' },
borderRadius: 2,
maxHeight: { xs: '293px', sm: '30%', md: 140 },
}}
>
{/* رأس القسم */}
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center' },
mb: 3,
pl: 1,
pr: { xs: 1, sm: 0 },
flexDirection: { xs: 'column', sm: 'row' },
}}
>
<Typography
variant="h6"
sx={{
fontWeight: '600',
fontSize: { xs: '20px', sm: '22px', md: '24px' },
color: '#121212',
}}
>
Categories
</Typography>
<Box
sx={{
display: 'flex',
gap: { xs: 1, sm: 2 },
flexWrap: { xs: 'wrap', sm: 'nowrap' },
width: { xs: '100%', sm: 'auto' },
justifyContent: { xs: 'space-between', sm: 'flex-end' },
}}
>
<Button
variant="contained"
sx={{
textTransform: 'none',
color: '#fff',
backgroundColor: theme.palette.primary.main,
boxShadow: 'none',
borderRadius: '8px',
height: '40px',
width: { xs: '100%', sm: '130px', md: '150px' },
fontSize: { xs: '12px', sm: '14px', md: '16px' },
fontWeight: 700,
minWidth: 'unset',
}}
onClick={() => {
setSelectedCategoryForEdit(null);
setOpenAddModal(true);
}}
>
Add Category
</Button>
<IconButton
onClick={() => scroll(-200)}
sx={{
backgroundColor: theme.palette.primary.main,
color: '#fff',
width: 40,
height: 40,
'&:hover': { backgroundColor: '#ffddbfff' },
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<IconButton
onClick={() => scroll(200)}
sx={{
backgroundColor: theme.palette.primary.main,
color: '#fff',
width: 40,
height: 40,
'&:hover': { backgroundColor: '#ffddbfff' },
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
</Box>
</Box>
{/* قائمة الفئات */}
{/* قائمة الفئات */}
<Box
sx={{
width: { xs: '90%', sm: '95%', md: '100%' },
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
mb: 3,
pl: 1,
mt: 5,
pr: { xs: 1, sm: 4 },
flexDirection: 'row',
overflowX: 'auto',
gap: 2,
}}
ref={scrollRef}
>
{loading ? (
// Skeleton Loader
Array.from({ length: 5 }).map((_, index) => (
<Skeleton
key={index}
variant="rectangular"
width={120}
height={60}
sx={{ borderRadius: 2 }}
/>
))
) : categories.length === 0 ? (
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '60px',
}}
>
<Typography variant="body1" color="text.secondary">
No categories available
</Typography>
</Box>
) : (
<CategoryScrollList
categories={categories}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
setSelectedCategoryForEdit={setSelectedCategoryForEdit}
setContextMenu={setContextMenu}
scrollRef={scrollRef}
/>
)}
</Box>
{/* مودال الإضافة / التعديل */}
<Modal
open={openAddModal}
onClose={() => setOpenAddModal(false)}
aria-labelledby="add-category-modal"
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 2,
}}
>
<Box
sx={{
backgroundColor: '#fff',
borderRadius: 2,
boxShadow: 24,
p: 2,
maxWidth: '90vw',
width: 400,
}}
>
<AddCategory
onAdd={selectedCategoryForEdit ? handleUpdateCategory : handleAddCategory}
editingCategory={selectedCategoryForEdit}
/>
</Box>
</Modal>
{/* قائمة السياق */}
<Menu
open={contextMenu !== null}
onClose={() => setContextMenu(null)}
anchorReference="anchorPosition"
anchorPosition={
contextMenu !== null
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
: undefined
}
sx={{ zIndex: 2000 }}
>
<MenuItem
onClick={() => {
setOpenAddModal(true);
setContextMenu(null);
}}
>
Edit Category
</MenuItem>
<MenuItem
onClick={() => {
setConfirmDeleteOpen(true);
setContextMenu(null);
}}
sx={{ color: 'red' }}
>
Delete Category
</MenuItem>
</Menu>
{/* تأكيد الحذف */}
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<Typography>Are you sure you want to delete this category?</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
<Button onClick={handleDeleteCategory} color="error" variant="contained">
Delete
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default AccountSettings;

عرض الملف

@@ -0,0 +1,102 @@
import React, { useState, useEffect } from 'react';
import { Box, Button, TextField, Typography } from '@mui/material';
import { useRestaurant } from '../../../../contexts/RestaurantContext';
const AddCategory = ({ onAdd, editingCategory }) => {
const { restaurantId } = useRestaurant();
const [productName, setProductName] = useState('');
const [description, setDescription] = useState('');
const [price, setPrice] = useState('');
const [discount, setDiscount] = useState('');
const [productImage, setProductImage] = useState(null);
useEffect(() => {
if (editingCategory) {
setProductName(editingCategory.name || '');
setDescription(editingCategory.description || '');
setPrice(editingCategory.price || '');
setDiscount(editingCategory.discount || '');
setProductImage(editingCategory.icon || null);
}
}, [editingCategory]);
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => setProductImage(reader.result);
reader.readAsDataURL(file);
}
};
const handleSubmit = () => {
if (!productName) {
alert("Please enter category name");
return;
}
if (!restaurantId) {
alert("Restaurant ID not set");
return;
}
const payload = {
name: productName,
restaurant_id: restaurantId,
description,
price,
discount,
icon: productImage,
};
// فقط أرسل البيانات للمكون الأب
if (onAdd) onAdd(payload);
};
return (
<Box p={2} maxWidth={400} mx="auto">
<Typography variant="body2" color="black" sx={{ fontWeight: 600, fontSize: '24px' }}>
Category Name
</Typography>
<TextField
fullWidth
placeholder="Enter category name"
value={productName}
onChange={(e) => setProductName(e.target.value)}
margin="normal"
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: '#FF914D',
boxShadow: '0 0 0 2px rgba(255,145,77,0.2)',
},
},
}}
/>
<Button
variant="contained"
fullWidth
sx={{
mt: 2,
backgroundColor: '#FF914D',
'&:hover': { backgroundColor: '#e57f3c' },
borderRadius: 2,
color: '#fff',
textTransform: 'none',
fontWeight: 600,
fontSize: '16px',
}}
onClick={handleSubmit}
>
{editingCategory ? 'Update Category' : 'Add Category'}
</Button>
</Box>
);
};
export default AddCategory;

عرض الملف

@@ -0,0 +1,251 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Typography,
Box,
Button,
IconButton,
InputAdornment,
} from '@mui/material';
import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined';
import DeleteIcon from '@mui/icons-material/Delete';
import authService from '../../../../services/authService';
import AddIcon from '@mui/icons-material/Add';
const MAX_FILE_SIZE_MB = 2;
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/jpg'];
const AddMeal = ({ open, onClose, onAdd, selectedCategory, restaurantId }) => {
const [mealName, setMealName] = useState('');
const [description, setDescription] = useState('');
const [additions, setAdditions] = useState(['']);
const [price, setPrice] = useState('');
const [mealImage, setMealImage] = useState(null);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (!open) {
setMealName('');
setDescription('');
setAdditions(['']);
setPrice('');
setMealImage(null);
setErrors({});
}
}, [open]);
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (!file) return;
if (!ALLOWED_TYPES.includes(file.type)) {
setErrors(prev => ({ ...prev, image: 'Allowed types: jpg, jpeg, png' }));
return;
}
if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
setErrors(prev => ({ ...prev, image: `Image must be smaller than ${MAX_FILE_SIZE_MB} MB` }));
return;
}
setMealImage(file);
setErrors(prev => ({ ...prev, image: '' }));
};
const handleAddAddition = () => setAdditions([...additions, '']);
const handleRemoveAddition = (index) => setAdditions(additions.filter((_, i) => i !== index));
const handleChangeAddition = (index, value) => {
const newAdditions = [...additions];
newAdditions[index] = value;
setAdditions(newAdditions);
};
const validate = () => {
const newErrors = {};
if (!mealName.trim()) newErrors.name = 'Meal name is required';
if (!description.trim()) newErrors.description = 'Description is required';
if (!price || isNaN(price) || Number(price) <= 0) newErrors.price = 'Valid price is required';
if (!mealImage) newErrors.image = 'Meal image is required';
if (!selectedCategory) newErrors.category = 'Category must be selected';
additions.forEach((add, i) => {
if (!add.trim()) newErrors[`addition_${i}`] = 'Cannot be empty';
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validate()) return;
setIsSubmitting(true);
const mealData = {
name: mealName.trim(),
category_id: selectedCategory,
price,
restaurant_id: restaurantId,
photo: mealImage,
description: [description.trim()],
additions: additions.filter(a => a.trim() !== ''),
};
try {
const result = await authService.addMeal(mealData);
// عرض رسالة Snackbar أو alert
alert(result.message);
// تمرير الوجبة الجديدة للمكون الأب لتحديث القائمة
if (result.data && onAdd) {
const newMeal = {
id: result.data.id,
name: result.data.name,
price: result.data.price,
description: result.data.description || [],
additions: result.data.additions || [],
unit: result.data.unit || "/pcs",
photo: result.data.photo || "/images/default-product.png",
category_id: result.data.category_id || selectedCategory,
};
onAdd(newMeal);
}
// إغلاق المودال بعد الإضافة
onClose();
} catch (error) {
console.error('Add meal error:', error);
alert(error.message || "Something went wrong");
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600, fontSize: '20px' }}>Add Meal</DialogTitle>
<DialogContent
dividers
sx={{
overflow: 'auto',
'&::-webkit-scrollbar': {
display: 'none',
},
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
{/* Meal Image */}
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Meal Image</Typography>
<Box
component="label"
htmlFor="upload-image"
sx={{
border: '2px dashed #ccc',
borderRadius: '10px',
height: '150px',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
color: '#969BA7',
backgroundColor: '#FAFAFA',
fontWeight: 500,
fontSize: '18px',
textAlign: 'center',
mb: 2,
'&:hover': { borderColor: '#FF914D', backgroundColor: '#fffaf5' }
}}
>
{mealImage ? (
<Box sx={{ position: 'relative', width: '100%', height: '100%', borderRadius: 2, overflow: 'hidden' }}>
<img src={mealImage instanceof File ? URL.createObjectURL(mealImage) : mealImage} alt="Preview" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<Button onClick={() => setMealImage(null)} sx={{ position: 'absolute', top: 4, right: 4, minWidth: 0, padding: '2px 6px', fontSize: 12 }}>Remove</Button>
</Box>
) : (
<>
Upload Meal Picture
<AddAPhotoOutlinedIcon sx={{ fontSize: 32, color: '#9e9e9e', mt: 1 }} />
</>
)}
<input id="upload-image" type="file" accept="image/*" hidden onChange={handleImageUpload} />
</Box>
{errors.image && <Typography color="error" sx={{ mb: 1 }}>{errors.image}</Typography>}
{/* Meal Name */}
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Meal Name</Typography>
<TextField fullWidth placeholder="Meal Name" value={mealName} onChange={e => setMealName(e.target.value)} error={!!errors.name} helperText={errors.name} sx={{ mb: 2 }} />
{/* Description */}
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Description</Typography>
<TextField fullWidth placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} error={!!errors.description} helperText={errors.description} sx={{ mb: 2 }} />
{/* Additions */}
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Additions</Typography>
{additions.map((add, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<TextField
fullWidth
placeholder={`Addition ${i + 1}`}
value={add}
onChange={e => handleChangeAddition(i, e.target.value)}
error={!!errors[`addition_${i}`]}
helperText={errors[`addition_${i}`]}
/>
<IconButton onClick={() => handleRemoveAddition(i)} color="error"><DeleteIcon /></IconButton>
</Box>
))}
<Button variant="text" startIcon={<AddIcon />} onClick={handleAddAddition} sx={{ mb: 2 ,textTransform:'none'}}>Add Another</Button>
{/* Price */}
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Price</Typography>
<TextField fullWidth type="number" placeholder="Price" value={price} onChange={e => setPrice(e.target.value)} error={!!errors.price} helperText={errors.price} sx={{ mb: 2 }} />
</DialogContent>
<DialogActions sx={{ pr: 3, pb: 2 }}>
<Button
onClick={onClose}
sx={{
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
width: '135px',
textTransform: 'none',
}}
variant="outlined"
size="small"
>
Cancel
</Button>
<Button
onClick={handleSubmit}
sx={{
color: 'white',
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
width: '135px',
textTransform: 'none',
}}
variant="contained"
size="small"
>
Add Meal
</Button>
</DialogActions>
</Dialog>
);
};
export default AddMeal;

عرض الملف

@@ -0,0 +1,131 @@
import React, { useState, useEffect } from "react";
import { Box, Typography, Skeleton, useTheme } from "@mui/material";
import MealCard from "./MealCard";
import authService from "../../../../services/authService";
import SimplePagination from "../../SimplePagination";
const AllMeals = ({ restaurantId, categories = [], onProductClick }) => {
const theme = useTheme();
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [pagination, setPagination] = useState(null);
const itemsPerPage = 6;
useEffect(() => {
if (!restaurantId) return;
const fetchMeals = async () => {
setLoading(true);
setError(null);
try {
const result = await authService.getAllMealsByRestaurant(restaurantId, page);
if (result.success) {
const mealsWithImages = result.data.map(meal => {
const category = categories.find(c => c.id === meal.category_id);
return {
id: meal.id,
name: meal.name,
price: meal.price,
description: meal.description || [],
additions: meal.additions || [],
unit: meal.unit || "/pcs",
photo: meal.photo || meal.photo_url || "/images/default-product.png",
category_id: meal.category_id,
category: category ? category.name : null,
};
});
setProducts(mealsWithImages);
setPagination(result.pagination);
} else {
setProducts([]);
setPagination(null);
}
} catch (err) {
console.error(err);
setError("Failed to fetch meals.");
} finally {
setLoading(false);
}
};
fetchMeals();
}, [restaurantId, page, categories]);
return (
<Box
sx={{
mb: 0,
mr: { xs: 2, sm: 2, md: 0 },
pl: { xs: 2, sm: 3 },
pr: { xs: 2, sm: 3 },
pb: { xs: 2, sm: 3 },
pt: { xs: 2, sm: 1.5 },
backgroundColor: "#FFFFFF",
borderRadius: "10px",
padding: "20px",
display: "flex",
flexDirection: "column",
gap: "20px",
overflowY: "auto",
position: "relative",
}}
>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Typography variant="h6" fontWeight={600}>All Meals</Typography>
</Box>
{loading ? (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
{Array.from({ length: itemsPerPage }).map((_, index) => (
<Box key={index} sx={{ width: "calc(33.33% - 16px)" }}>
<Skeleton variant="rectangular" width="100%" height={180} sx={{ borderRadius: 2 }} />
<Skeleton variant="text" width="80%" sx={{ mt: 1 }} />
<Skeleton variant="text" width="40%" />
</Box>
))}
</Box>
) : error ? (
<Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}>
<Typography variant="body1" color="error">{error}</Typography>
</Box>
) : products.length === 0 ? (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "150px" }}>
<Typography variant="body1" color="text.secondary">There is no meal available</Typography>
</Box>
) : (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
{products.map(product => (
<Box key={product.id} onClick={() => onProductClick(product)} >
<MealCard
meal={product}
onClick={() => onProductClick(product)}
/>
</Box>
))}
</Box>
)}
{/* Pagination Footer */}
{pagination && !loading && (
<Box display="flex" justifyContent="space-between" alignItems="center" mt={2} sx={{ borderTop: "1px solid #f0f0f0", pt: 2 }}>
<Typography variant="body2" color="text.secondary">
Showing {(page - 1) * itemsPerPage + 1} to {Math.min(page * itemsPerPage, products.length)} of {pagination.total} meals
</Typography>
<SimplePagination
currentPage={page}
pageCount={pagination.last_page}
onChange={(newPage) => setPage(newPage)}
/>
</Box>
)}
</Box>
);
};
export default AllMeals;

عرض الملف

@@ -0,0 +1,76 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
const CategoryScrollList = ({
categories,
selectedCategory,
setSelectedCategory,
setSelectedCategoryForEdit,
setContextMenu,
scrollRef,
}) => {
const theme = useTheme();
return (
<Box
ref={scrollRef}
sx={{
width: '100%',
maxWidth: '100%',
overflowX: 'auto',
scrollBehavior: 'smooth',
pl: 1,
pr: { xs: 1, sm: 0 },
mb: 3,
mx: 'auto',
display: 'flex',
flexDirection: 'row',
gap: 2,
alignItems: 'center',
'&::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
}}
>
{categories.map((cat) => {
const isSelected = selectedCategory === cat.id;
return (
<Box
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
onContextMenu={(e) => {
e.preventDefault();
setSelectedCategoryForEdit(cat);
setContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6 });
}}
sx={{
p: 1,
borderRadius: '12px',
backgroundColor: isSelected ? theme.palette.primary.main : '#F9F9FC',
border: isSelected ? `0px solid ${theme.palette.primary.main}` : '0px solid #ddd',
textAlign: 'center',
minWidth: '70px',
cursor: 'pointer',
flexShrink: 0,
transition: '0.3s',
'&:hover': {
backgroundColor: isSelected ? theme.palette.primary.dark : '#EDEDED',
},
}}
>
<Typography
variant="body2"
fontWeight={500}
sx={{ color: isSelected ? '#fff' : '#121212' }}
>
{cat.name}
</Typography>
</Box>
);
})}
</Box>
);
};
export default CategoryScrollList;

عرض الملف

@@ -0,0 +1,138 @@
import React from "react";
import {
Box,
Typography,
Card,
CardContent,
CardMedia,
IconButton,
Chip,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
const MealCard = ({ meal, onClick, onAdd }) => {
const { name, price, unit, photo, image, description, additions } = meal;
return (
<Card
onClick={onClick}
sx={{
width: { xs: "160px", md: "160px" },
height: 250,
borderRadius: "20px",
p: "15px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#FCFCFC",
boxShadow: "none",
border: "1px solid #f0f0f0",
cursor: "pointer",
transition: "transform 0.2s ease-in-out",
"&:hover": { transform: "scale(1.02)" },
}}
>
{/* صورة الوجبة */}
<CardMedia
component="img"
image={photo || "/images/default-product.png"}
alt={name}
sx={{
width: "120px",
height: "120px",
objectFit: "cover",
borderRadius: "12px",
// mb: 1,
pointerEvents: "none",
}}
/>
{/* تفاصيل الوجبة */}
<CardContent sx={{ p: 0, width: "100%", textAlign: "center" }}>
<Typography variant="body1" fontWeight={600} fontSize={16} noWrap>
{name}
</Typography>
{/* السعر */}
<Typography
variant="body2"
fontWeight={600}
sx={{ color: "#FF914D", fontSize: "15px", mt: 0.5 }}
>
${price}{" "}
<Typography
component="span"
variant="caption"
color="text.secondary"
>
{unit || "/pcs"}
</Typography>
</Typography>
{/* الوصف (أول عنصر فقط للتصغير) */}
{description && description.length > 0 && (
<Typography
variant="caption"
color="text.secondary"
noWrap
sx={{ display: "block", mt: 0.5 }}
>
{description[0]}
</Typography>
)}
{/* الإضافات (أول 2 فقط) */}
{/* {additions && additions.length > 0 && (
<Box
sx={{
display: "flex",
justifyContent: "center",
flexWrap: "wrap",
gap: 0.5,
mt: 1,
}}
>
{additions.slice(0, 2).map((add, i) => (
<Chip
key={i}
label={add}
size="small"
sx={{
fontSize: "10px",
height: 18,
borderRadius: "8px",
}}
/>
))}
</Box>
)} */}
{/* زر الإضافة */}
{/* <IconButton
color="primary"
size="small"
onClick={(e) => {
e.stopPropagation();
if (onAdd) onAdd(meal);
}}
sx={{
backgroundColor: "#FF8551",
color: "#fff",
mt: 1,
width: 28,
height: 28,
"&:hover": { backgroundColor: "#ff7043" },
}}
>
<AddIcon fontSize="small" />
</IconButton> */}
</CardContent>
</Card>
);
};
export default MealCard;

عرض الملف

@@ -0,0 +1,202 @@
import React, { useState, useEffect, useCallback } from "react";
import { Box, Typography, Skeleton, useTheme, Button } from "@mui/material";
import MealCard from "./MealCard";
import authService from "../../../../services/authService";
import AddMeal from "./AddMeal";
import SimplePagination from "../../SimplePagination";
const MealsByCateg = ({
restaurantId,
categoryId,
onProductClick,
onAddMeal,
categoryName,
onBack,
}) => {
const theme = useTheme();
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [openModal, setOpenModal] = useState(false);
const [page, setPage] = useState(1);
const [pagination, setPagination] = useState(null);
const itemsPerPage = 6;
// استخدام useCallback لمنع إنشاء الدالة عند كل إعادة render
const fetchMeals = useCallback(async () => {
if (!restaurantId || !categoryId) return;
setLoading(true);
setError(null);
try {
const result = await authService.getMealsByCategory(
restaurantId,
categoryId,
page
);
if (result.success) {
const mealsWithImages = result.data.map((meal) => ({
id: meal.id,
name: meal.name,
price: meal.price,
description: meal.description || [],
additions: meal.additions || [],
unit: meal.unit || "/pcs",
photo: meal.photo || "/images/default-product.png",
category_id: meal.category_id || categoryId,
}));
setProducts(mealsWithImages);
setPagination(result.pagination);
} else {
setProducts([]);
setPagination(null);
}
} catch (err) {
console.error(err);
setError("Failed to fetch meals.");
setProducts([]);
setPagination(null);
} finally {
setLoading(false);
}
}, [restaurantId, categoryId, page]);
// جلب البيانات عند تغيير restaurantId أو categoryId أو page
useEffect(() => {
fetchMeals();
}, [fetchMeals]);
// إضافة وجبة جديدة وتحديث القائمة فورًا
// في MealsByCateg
const handleAddProduct = async (meal) => {
setOpenModal(false); // اغلاق مودال الإضافة
setPage(1); // العودة للصفحة الأولى
const result = await fetchMeals(); // جلب الوجبات بعد الإضافة
if (onAddMeal) onAddMeal(meal); // تمرير الوجبة للمكون الأب
};
return (
<Box
sx={{
mb: 2,
pl: { xs: 2, sm: 3 },
pr: { xs: 2, sm: 3 },
pb: { xs: 2, sm: 3 },
pt: { xs: 2, sm: 1.5 },
maxWidth: { xs: "90%", md: "100%" },
backgroundColor: "#FFFFFF",
borderRadius: "10px",
padding: "20px",
display: "flex",
flexDirection: "column",
gap: "20px",
overflowY: "auto",
}}
>
{/* العنوان + أزرار */}
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Typography variant="h6" fontWeight={600}>
{categoryName || "Meals"}
</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Button
onClick={() => setOpenModal(true)}
sx={{
textTransform: "none",
color: "#fff",
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
height: "40px",
width: { xs: "100%", sm: "130px", md: "150px" },
fontWeight: 700,
}}
>
Add Product
</Button>
<Button variant="text" color="primary" sx={{ ml: 1 }} onClick={onBack}>
Back
</Button>
</Box>
</Box>
{/* حالة التحميل أو الخطأ */}
{loading ? (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
{Array.from({ length: itemsPerPage }).map((_, index) => (
<Box key={index} sx={{ flex: "1 1 calc(33.33% - 16px)" }}>
<Skeleton variant="rectangular" width="100%" height={180} sx={{ borderRadius: 2 }} />
<Skeleton variant="text" width="80%" sx={{ mt: 1 }} />
<Skeleton variant="text" width="40%" />
</Box>
))}
</Box>
) : error ? (
<Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}>
<Typography variant="body1" color="error">{error}</Typography>
</Box>
) : products.length === 0 ? (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "150px" }}>
<Typography variant="body1" color="text.secondary">
There is no meal available
</Typography>
</Box>
) : (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
{products.map((product) => (
<Box
key={product.id}
onClick={() =>
onProductClick({
...product,
category: categoryName,
category_id: categoryId,
})
}
>
<MealCard
meal={product}
onClick={() =>
onProductClick({
...product,
category: categoryName,
category_id: categoryId,
})
}
onAdd={onAddMeal}
/>
</Box>
))}
</Box>
)}
{/* Pagination Footer */}
{pagination && !loading && pagination.last_page > 1 && (
<Box display="flex" justifyContent="space-between" alignItems="center" mt={2} sx={{ borderTop: "1px solid #f0f0f0", pt: 2 }}>
<Typography variant="body2" color="text.secondary">
Showing {(page - 1) * itemsPerPage + 1} to{" "}
{Math.min(page * itemsPerPage, pagination.total)} of {pagination.total} meals
</Typography>
<SimplePagination
currentPage={page}
pageCount={pagination.last_page}
onChange={(newPage) => setPage(newPage)}
/>
</Box>
)}
{/* Add Meal Modal */}
<AddMeal
open={openModal}
onClose={() => setOpenModal(false)}
onAdd={handleAddProduct}
selectedCategory={categoryId}
restaurantId={restaurantId}
/>
</Box>
);
};
export default MealsByCateg;

عرض الملف

@@ -0,0 +1,551 @@
import React, { useState, useEffect } from "react";
import {
Box,
Typography,
Button,
TextField,
Chip,
LinearProgress,
useTheme,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormControl,
Select,
MenuItem,
FormHelperText,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import ClearIcon from "@mui/icons-material/Clear";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import authService from "../../../../services/authService";
const ProductDetail = ({ product, onBack }) => {
const theme = useTheme();
const [isEditing, setIsEditing] = useState(false);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [formData, setFormData] = useState({
name: "",
category_id: "",
restaurant_id: "",
description: [],
additions: [],
price: "",
photoFile: null,
});
const [newDescription, setNewDescription] = useState("");
const [newAddition, setNewAddition] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [errors, setErrors] = useState({});
const [categories, setCategories] = useState([]);
useEffect(() => {
if (product) {
setFormData({
name: product.name || "",
category_id: product.category_id || "",
restaurant_id: product.restaurant_id || "",
description: product.description || [],
additions: product.additions || [],
price: product.price || "",
photoFile: null,
});
}
const fetchCategories = async () => {
try {
const result = await authService.getCategoriesByRestaurant(product?.restaurant_id);
if (result.success) setCategories(result.data);
} catch (error) {
console.error("Error fetching categories:", error);
}
};
if (product?.restaurant_id) fetchCategories();
}, [product]);
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) newErrors.name = "Name is required";
if (!formData.category_id) newErrors.category_id = "Category is required";
if (!formData.price || isNaN(formData.price) || formData.price <= 0) {
newErrors.price = "Valid price is required";
}
if (formData.description.length === 0) newErrors.description = "At least one description is required";
if (formData.additions.length === 0) newErrors.additions = "At least one addition is required";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: "" }));
};
const handleFileChange = (e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
setErrors((prev) => ({ ...prev, photoFile: "Please select an image file" }));
return;
}
if (file.size > 5 * 1024 * 1024) {
setErrors((prev) => ({ ...prev, photoFile: "File size should be less than 5MB" }));
return;
}
setFormData((prev) => ({ ...prev, photoFile: file }));
setErrors((prev) => ({ ...prev, photoFile: "" }));
};
const handleAddDescription = () => {
if (newDescription.trim() !== "") {
setFormData((prev) => ({
...prev,
description: [...prev.description, newDescription.trim()],
}));
setNewDescription("");
setErrors((prev) => ({ ...prev, description: "" }));
}
};
const handleRemoveDescription = (idx) => {
const newDesc = formData.description.filter((_, i) => i !== idx);
setFormData((prev) => ({ ...prev, description: newDesc }));
if (newDesc.length === 0) setErrors((prev) => ({ ...prev, description: "At least one description is required" }));
};
const handleAddAddition = () => {
if (newAddition.trim() !== "") {
setFormData((prev) => ({
...prev,
additions: [...prev.additions, newAddition.trim()],
}));
setNewAddition("");
setErrors((prev) => ({ ...prev, additions: "" }));
}
};
const handleRemoveAddition = (idx) => {
const newAdds = formData.additions.filter((_, i) => i !== idx);
setFormData((prev) => ({ ...prev, additions: newAdds }));
if (newAdds.length === 0) setErrors((prev) => ({ ...prev, additions: "At least one addition is required" }));
};
const handleEditDescription = (idx, value) => {
const newDesc = [...formData.description];
newDesc[idx] = value;
setFormData((prev) => ({ ...prev, description: newDesc }));
};
const handleEditAddition = (idx, value) => {
const newAdds = [...formData.additions];
newAdds[idx] = value;
setFormData((prev) => ({ ...prev, additions: newAdds }));
};
const handleSave = async () => {
if (!validateForm()) return;
try {
setIsSaving(true);
const updateData = {
name: formData.name.trim(),
category_id: formData.category_id,
restaurant_id: formData.restaurant_id,
description: formData.description,
additions: formData.additions,
price: formData.price,
photo: formData.photoFile ? [formData.photoFile] : [],
};
const res = await authService.updateMeal(product.id, updateData);
// ✅ حدّث state بآخر نسخة من السيرفر
setFormData({
name: res.meal.name || "",
category_id: res.meal.category_id || "",
restaurant_id: res.meal.restaurant_id || "",
description: res.meal.description || [],
additions: res.meal.additions || [],
price: res.meal.price || "",
photoFile: null,
});
setIsEditing(false);
alert("Meal updated successfully!");
// ✅ إغلاق المكون بعد النجاح
if (onBack) {
onBack();
}
} catch (err) {
console.error(err);
alert("Error updating meal: " + (err.message || "Something went wrong"));
} finally {
setIsSaving(false);
}
};
const handleDeleteMeal = async () => {
try {
setIsSaving(true);
await authService.deleteMeal(product.id, product.restaurant_id, product.category_id);
onBack();
// alert("Meal deleted successfully!");
} catch (err) {
console.error(err);
alert("Error deleting meal: " + (err.message || "Something went wrong"));
} finally {
setIsSaving(false);
setConfirmDeleteOpen(false);
}
};
return (
<Box sx={{ display: "flex", backgroundColor: "#fff", borderRadius: "8px", p: 3, mx: "auto", gap: 3 }}>
{/* صندوق الصورة */}
<Box sx={{ width: "50%", display: "flex", flexDirection: "column", gap: 2 }}>
<Box sx={{ position: "relative", width: "100%", height: 450 }}>
<IconButton
onClick={onBack}
sx={{
position: "absolute",
top: 0,
left: 0,
zIndex: 102,
backgroundColor: "#fff",
color: theme.palette.primary.main,
"&:hover": { backgroundColor: "#f0f0f0" },
}}
>
<ArrowBackIcon />
</IconButton>
<Box
component="img"
src={formData.photoFile ? URL.createObjectURL(formData.photoFile) : product.image || product.photo}
alt={product.name}
sx={{
width: "80%",
height: "80%",
objectFit: "cover",
borderRadius: "8px",
p: 5,
border: errors.photoFile ? "2px solid red" : "none"
}}
/>
</Box>
{isEditing && (
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Button
component="label"
variant="outlined"
startIcon={<CloudUploadIcon />}
sx={{
textTransform: 'none',
borderColor: errors.photoFile ? 'red' : theme.palette.primary.main,
color: errors.photoFile ? 'red' : theme.palette.primary.main
}}
>
Change Image
<input
type="file"
hidden
accept="image/*"
onChange={handleFileChange}
/>
</Button>
{errors.photoFile && <FormHelperText error>{errors.photoFile}</FormHelperText>}
{formData.photoFile && <Typography variant="caption" color="textSecondary">Selected: {formData.photoFile.name}</Typography>}
</Box>
)}
</Box>
{/* التفاصيل */}
<Box sx={{ width: "50%", display: "flex", flexDirection: "column", gap: 2 }}>
{/* الاسم */}
<Box>
{isEditing ? (
<TextField
fullWidth
placeholder="Meal Name"
name="name"
value={formData.name}
onChange={handleInputChange}
error={!!errors.name}
helperText={errors.name}
size="small"
/>
) : (
<Typography variant="h5" sx={{mt:1}} fontWeight={700}>{product.name}</Typography>
)}
</Box>
{/* الفئة */}
<Box>
{isEditing ? (
<FormControl fullWidth error={!!errors.category_id} size="small">
<Select
name="category_id"
value={formData.category_id || ""}
onChange={handleInputChange}
displayEmpty
>
{!formData.category_id && (
<MenuItem value="">
<em>Select Category</em>
</MenuItem>
)}
{categories.map((category) => (
<MenuItem key={category.id} value={category.id}>
{category.name}
</MenuItem>
))}
</Select>
{errors.category_id && (
<FormHelperText>{errors.category_id}</FormHelperText>
)}
</FormControl>
) : (
<Typography variant="body2" sx={{ color: "#777" }}>
{product.category ||
categories.find((c) => c.id === (product.category_id || formData.category_id))?.name ||
`Category #${product.category_id || formData.category_id}`}
</Typography>
)}
</Box>
{/* السعر */}
<Box>
<Typography variant="body2" fontWeight={500} fontSize={18} sx={{ mb: 1 }}>Price</Typography>
{isEditing ? (
<TextField
fullWidth
placeholder="0.00"
name="price"
type="number"
value={formData.price}
onChange={handleInputChange}
error={!!errors.price}
helperText={errors.price}
inputProps={{ min: 0, step: 0.01 }}
size="small"
/>
) : (
<Typography variant="h6" sx={{ color: theme.palette.primary.main, fontWeight: 700 }}>
${Number(product.price).toFixed(2)} {product.unit || "/pcs"}
</Typography>
)}
</Box>
{/* الوصف */}
<Box>
<Typography variant="body2" fontWeight={500} fontSize={18} sx={{ mb: 1 }}>
Description {isEditing && "*"}
</Typography>
{isEditing ? (
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{formData.description.map((desc, idx) => (
<Box key={idx} sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<TextField
fullWidth
value={desc}
onChange={(e) => handleEditDescription(idx, e.target.value)}
placeholder={`Description ${idx + 1}`}
size="small"
/>
</Box>
))}
{errors.description && <FormHelperText error>{errors.description}</FormHelperText>}
</Box>
) : (
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
{formData.description.map((desc, idx) => <Typography key={idx} variant="body2"> {desc}</Typography>)}
</Box>
)}
</Box>
{/* الإضافات */}
<Box>
<Typography variant="body2" fontWeight={500} fontSize={18} sx={{ mb: 1 }}>
Additions {isEditing && "*"}
</Typography>
{isEditing ? (
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{formData.additions.map((add, idx) => (
<Box key={idx} sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<TextField
fullWidth
value={add}
onChange={(e) => handleEditAddition(idx, e.target.value)}
placeholder={`Addition ${idx + 1}`}
size="small"
/>
<IconButton color="error" onClick={() => handleRemoveAddition(idx)} size="small">
<ClearIcon fontSize="small" />
</IconButton>
</Box>
))}
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<TextField
fullWidth
placeholder="Add new addition"
value={newAddition}
onChange={(e) => setNewAddition(e.target.value)}
size="small"
/>
<IconButton color="primary" onClick={handleAddAddition} disabled={!newAddition.trim()}>
<AddIcon />
</IconButton>
</Box>
{errors.additions && <FormHelperText error>{errors.additions}</FormHelperText>}
</Box>
) : (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
{formData.additions.map((add, idx) => <Chip key={idx} label={add} size="small" variant="outlined" />)}
</Box>
)}
</Box>
{/* شريط تقدم */}
<LinearProgress
variant="determinate"
value={100}
sx={{
mt: 2,
height: 4,
borderRadius: 2,
backgroundColor: "#f0f0f0",
"& .MuiLinearProgress-bar": { backgroundColor: theme.palette.primary.main },
}}
/>
<Box sx={{backgroundColor:'#fff2e0ff' , height:100 , width:500 , borderRadius: '8px',alignItems:'center' }}>
{/* أزرار التحكم */}
<Box sx={{ display: "flex", gap: 2, p:4}}>
{isEditing ? (
<>
<Button
variant="contained"
sx={{
flex: 1,
textTransform: 'none',
backgroundColor: theme.palette.primary.main,
borderRadius: '8px',
height: '45px',
color: '#fff'
}}
onClick={handleSave}
disabled={isSaving}
>
{isSaving ? "Saving..." : "Save Changes"}
</Button>
<Button
variant="outlined"
sx={{
flex: 1,
textTransform: 'none',
borderColor: "gray",
color: "gray",
borderRadius: '8px',
height: '45px',
backgroundColor:'#fff'
}}
onClick={() => {
setIsEditing(false);
setFormData({
name: product.name || "",
category_id: product.category_id || "",
restaurant_id: product.restaurant_id || "",
description: product.description || [],
additions: product.additions || [],
price: product.price || "",
photoFile: null,
});
setErrors({});
}}
disabled={isSaving}
>
Cancel
</Button>
</>
) : (
<>
<Button
variant="outlined"
sx={{
flex: 1,
textTransform: 'none',
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
borderRadius: '8px',
height: '45px',
backgroundColor:'#fff'
}}
onClick={() => setIsEditing(true)}
>
Edit Meal
</Button>
<Button
variant="outlined"
sx={{
flex: 1,
textTransform: 'none',
borderColor: "error.main",
color: "error.main",
borderRadius: '8px',
height: '45px',
backgroundColor:'#fff'
}}
onClick={() => setConfirmDeleteOpen(true)}
>
Delete Meal
</Button>
</>
)}
</Box>
</Box>
</Box>
{/* مودال تأكيد الحذف */}
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete "{product.name}"? This action cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteMeal}
color="error"
variant="contained"
disabled={isSaving}
>
{isSaving ? "Deleting..." : "Delete"}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default ProductDetail;