نسخ من RaghadAlkhous/RestaurantDash
Initial commit - restaurant dashboard
هذا الالتزام موجود في:
206
src/components/Home/Meal/Meal.js
Normal file
206
src/components/Home/Meal/Meal.js
Normal file
@@ -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;
|
||||
330
src/components/Home/Meal/contect/AccountSettings.js
Normal file
330
src/components/Home/Meal/contect/AccountSettings.js
Normal file
@@ -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;
|
||||
102
src/components/Home/Meal/contect/AddCategory.js
Normal file
102
src/components/Home/Meal/contect/AddCategory.js
Normal file
@@ -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;
|
||||
251
src/components/Home/Meal/contect/AddMeal.js
Normal file
251
src/components/Home/Meal/contect/AddMeal.js
Normal file
@@ -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;
|
||||
131
src/components/Home/Meal/contect/AllMeals.js
Normal file
131
src/components/Home/Meal/contect/AllMeals.js
Normal file
@@ -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;
|
||||
76
src/components/Home/Meal/contect/CategoryScrollList.js
Normal file
76
src/components/Home/Meal/contect/CategoryScrollList.js
Normal file
@@ -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;
|
||||
138
src/components/Home/Meal/contect/MealCard.js
Normal file
138
src/components/Home/Meal/contect/MealCard.js
Normal file
@@ -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;
|
||||
202
src/components/Home/Meal/contect/MealsByCateg.js
Normal file
202
src/components/Home/Meal/contect/MealsByCateg.js
Normal file
@@ -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;
|
||||
551
src/components/Home/Meal/contect/ProductDetail.js
Normal file
551
src/components/Home/Meal/contect/ProductDetail.js
Normal file
@@ -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;
|
||||
المرجع في مشكلة جديدة
حظر مستخدم