initial commit

This commit is contained in:
RaghadAlkhous
2026-05-06 15:08:57 +03:00
commit 1ccca905f3
215 changed files with 61375 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
.App {
margin: 0;
padding: 0;
text-align: center;
}
body,
html,
#root {
margin: 0;
padding: 0;
height: 100%;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* CUSTOM FONTS */
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-Thin.ttf');
font-weight: 100;
}
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-ExtraLight.ttf');
font-weight: 200;
}
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-Light.ttf');
font-weight: 300;
}
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-SemiBold.ttf');
font-weight: 600;
}
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-Bold.ttf');
font-weight: 700;
}
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-ExtraBold.ttf');
font-weight: 800;
}
@font-face {
font-family: INTER;
src: url('/public/fonts/inter/Inter_18pt-Black.ttf');
font-weight: 900;
}
@font-face {
font-family: PlusJakartaSans;
src: url('/public/fonts/PlusJakartaSans/PlusJakartaSans-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: PlusJakartaSans;
src: url('/public/fonts/PlusJakartaSans/PlusJakartaSans-SemiBold.ttf');
font-weight: 600;
}
@font-face {
font-family: PlusJakartaSans;
src: url('/public/fonts/PlusJakartaSans/PlusJakartaSans-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: Hind Siliguri;
src: url('/public/fonts/Hind Siliguri/HindSiliguri-Bold.ttf');
font-weight: 700;
}
@font-face {
font-family: Hind Siliguri;
src: url('/public/fonts/Hind Siliguri/HindSiliguri-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: Hind Siliguri;
src: url('/public/fonts/Hind Siliguri/HindSiliguri-SemiBold.ttf');
font-weight: 600;
}
@font-face {
font-family: Hind Siliguri;
src: url('/public/fonts/Hind Siliguri/HindSiliguri-Regular.ttf');
font-weight: 400;
}
/* === CUSTOM FONTS === */
+62
View File
@@ -0,0 +1,62 @@
import React from 'react';
import './App.css';
import theme from './theme';
import { ThemeProvider } from '@mui/material/styles';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { GoogleOAuthProvider } from '@react-oauth/google';
import PrivateRoute from '../src/components/Routes/PrivateRoute';
// الصفحات العامة
import LoginForm from './components/Authentication/SignUp_In/LoginForm';
import RegisterForm from './components/Authentication/SignUp_In/RegisterForm';
import ForgetPassword from './components/Authentication/ForgetPassword/ForgetFormMain';
import PublicRoute from './components/Routes/PublicRoute';
import { TermsPage, PrivacyPage } from './components/Authentication/Legal/TermsPrivacyComponents';
import { RestaurantProvider } from './contexts/RestaurantContext';
import ProtectedRoutes from './components/Routes/ProtectedRoutes';
import { SnackbarProvider } from "./contexts/SnackbarContext";
import { UserProvider } from './contexts/UserContext';
function App() {
return (
<ThemeProvider theme={theme}>
<GoogleOAuthProvider clientId="443509481732-4i5ne811hr90rn75q3fbqevqi9eu3lvk.apps.googleusercontent.com">
<Router>
<UserProvider>
{/* ✅ لفّ كل المشروع بـ SnackbarProvider */}
<SnackbarProvider>
<Routes>
{/* التوجيه التلقائي للصفحة الرئيسية */}
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/login" element={<PublicRoute><LoginForm /></PublicRoute>} />
<Route path="/register" element={<PublicRoute><RegisterForm /></PublicRoute>} />
<Route path="/forget" element={<PublicRoute><ForgetPassword /></PublicRoute>} />
{/* روابط الشروط والخصوصية */}
<Route path="/terms" element={<TermsPage />} />
<Route path="/privacy" element={<PrivacyPage />} />
<Route
path="/*"
element={
<RestaurantProvider>
<ProtectedRoutes />
</RestaurantProvider>
}
/>
</Routes>
</SnackbarProvider>
</UserProvider>
</Router>
</GoogleOAuthProvider>
</ThemeProvider>
);
}
export default App;
@@ -0,0 +1,132 @@
import React from 'react';
import { Box, TextField, Typography, Button, useTheme } from '@mui/material';
import { useNavigate } from 'react-router-dom';
const Congratulations = ({ emailOrPhone, setEmailOrPhone }) => {
const navigate = useNavigate();
const theme = useTheme();
return (
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
// overflow: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
mt: { xs: 4, sm: 8, md: 2 } // هامش علوي متجاوب
}}
>
<Box
sx={{
marginTop: '90px',
position: 'relative',
width: 132,
height: 132,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box
sx={{
position: 'absolute',
width: 139,
height: 139,
borderRadius: '50%',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 0,
backgroundColor: '#FAFAFA'
}}
/>
<Box
sx={{
width: 120,
height: 120,
backgroundColor: '#E9EFFF',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
<img
src="/images/hand.png"
style={{ width: 60, height: 60, objectFit: 'contain' }}
/>
</Box>
</Box>
<Typography
variant="h4"
fontWeight={700}
marginTop={3}
marginBottom={2}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem'
}
}}
>
Congratulations!
</Typography>
<Typography
variant="h4"
fontWeight={400}
width='390px'
marginLeft='20px'
sx={{
color: '#5F6868',
fontSize: {
xs: '0.8rem',
md: '1.1rem'
}
}}
>
Your password has been successfully updated. Thank you for choosing our service
</Typography>
{/* زر التنقل إلى صفحة تسجيل الدخول */}
<Button
variant="contained"
fullWidth
onClick={() => navigate('/login')}
sx={{
marginTop: '20px',
color: 'white',
backgroundColor: theme.palette.primary.main,
borderRadius: '30px',
textTransform: 'none',
fontWeight: 500,
fontSize: '16px',
height: '52px',
padding: '12px',
maxWidth: '390px',
px: 2,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
transition: 'all 0.4s ease'
}
}}
>
Login
</Button>
</Box>
);
};
export default Congratulations;
@@ -0,0 +1,226 @@
import React, { useState } from 'react';
import { Box, Button, TextField, Typography, useTheme, Snackbar, Alert } from '@mui/material';
import authService from '../../../services/authService';
const EmailInputSection = ({ emailOrPhone, setEmailOrPhone, onStepComplete }) => {
const theme = useTheme();
const [loading, setLoading] = useState(false);
// حالات Snackbar
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [snackbarSeverity, setSnackbarSeverity] = useState('success');
const [inputError, setInputError] = useState('');
const handleCloseSnackbar = (event, reason) => {
if (reason === 'clickaway') return;
setSnackbarOpen(false);
};
const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleSend = async () => {
setInputError(''); // مسح الأخطاء السابقة
if (!emailOrPhone) {
setInputError("Please enter your email.");
return;
}
if (!validateEmail(emailOrPhone)) {
setInputError("Please enter a valid email address.");
return;
}
setLoading(true);
const response = await authService.resetPassword(emailOrPhone);
setLoading(false);
if (response.success !== false) {
setSnackbarMessage("A reset code has been sent to your email.");
setSnackbarSeverity('success');
setSnackbarOpen(true);
setTimeout(() => {
if (onStepComplete) onStepComplete();
}, 5000);
} else {
setInputError('');
let errorMessage = "";
if (response.errors) {
const firstError = Object.values(response.errors)[0];
errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
} else if (response.message) {
errorMessage = response.message;
} else {
errorMessage = "Failed to send reset code.";
}
if (
errorMessage.toLowerCase().includes("not found") ||
errorMessage.toLowerCase().includes("not registered") ||
errorMessage.toLowerCase().includes("doesn't exist") ||
errorMessage.toLowerCase().includes("email")
) {
setInputError("This email is not registered.");
} else if (errorMessage.toLowerCase().includes("invalid credentials")) {
setInputError("Email or password is incorrect.");
} else {
setSnackbarMessage(errorMessage);
setSnackbarSeverity('error');
setSnackbarOpen(true);
}
}
};
return (
<Box sx={{ width: '100%' }}>
<Box>
<Box
component="img"
src="/image.png"
alt="logo"
sx={{
width: { xs: '5vh', sm: '7vh' },
maxWidth: { xs: '40px', sm: '80px' },
height: 'auto',
objectFit: 'contain',
overflow: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
}}
/>
</Box>
<Typography
variant="h4"
fontWeight={700}
sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}
>
Forget Password
</Typography>
<Typography variant="body1" color="text.secondary" mb={2}>
Enter your email to reset it and regain access to your account.
</Typography>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Email
</Typography>
<TextField
placeholder="Enter your email"
type="email"
variant="outlined"
fullWidth
value={emailOrPhone}
onChange={(e) => setEmailOrPhone(e.target.value)}
error={!!inputError}
helperText={inputError}
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)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
},
}}
/>
</Box>
<Button
variant="contained"
fullWidth
onClick={handleSend}
disabled={loading}
sx={{
color: 'white',
backgroundColor: theme.palette.primary.main,
borderRadius: '30px',
textTransform: 'none',
fontWeight: 600,
fontSize: '16px',
height: '48px',
'&:hover': {
backgroundColor: theme.palette.primary.hover,
transition: 'all 0.4s ease',
},
}}
>
{loading ? "Sending..." : "Send"}
</Button>
{/*
<Typography
textAlign="center"
fontWeight={500}
sx={{ fontSize: '16px', width: '100%', mx: 'auto', color: '#969BA7', mt: 2 }}
>
By log in, I agree to the&nbsp;
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>
Terms of Service
</a>{' '}
and&nbsp;
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>
Privacy Policy
</a>
</Typography> */}
{/* Terms */}
<Typography
// variant="caption"
textAlign="center"
fontWeight={500}
sx={{ fontSize: '16px', width: '100%', mx: 'auto', color: '#969BA7', mt: 2 }}
>
By logging in, I agree to the{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/terms', 'TermsOfService')}
>
Terms of Service
</Box>{' '}
and{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
>
Privacy Policy
</Box>.
</Typography>
<Snackbar
open={snackbarOpen}
autoHideDuration={4000}
onClose={handleCloseSnackbar}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={handleCloseSnackbar}
severity={snackbarSeverity}
sx={{ width: { xs: '40%', sm: '60%', md: '100%' }, color: 'white', fontSize: '16px', fontWeight: '500', backgroundColor: '#e57f3f', borderRadius: 6, mb: 6 }}
variant="filled"
>
{snackbarMessage}
</Alert>
</Snackbar>
</Box>
);
};
export default EmailInputSection;
@@ -0,0 +1,118 @@
import React, { useState } from 'react';
import { Box, Stack, useTheme } from '@mui/material';
import Side from './Side';
import EmailInputSection from './EmailInputSection';
import OtpVerification from './OtpVerification';
import NewPassword from './NewPassword';
import Congratulations from './Congratulations';
const ForgetForm = () => {
const [emailOrPhone, setEmailOrPhone] = useState('');
const [currentSlide, setCurrentSlide] = useState(0);
const [stepCompleted, setStepCompleted] = useState(false);
const theme = useTheme();
const slides = [
{ component: EmailInputSection, title: "Email Verification" },
{ component: OtpVerification, title: "OTP Verification" },
{ component: NewPassword, title: "New Password" },
{ component: Congratulations, title: "Congratulations" }
];
const handleNext = () => {
if (currentSlide < slides.length - 1) {
setCurrentSlide(prev => prev + 1);
setStepCompleted(false);
}
};
const handleBack = () => {
if (currentSlide > 0) {
setCurrentSlide(prev => prev - 1);
setStepCompleted(true);
}
};
const CurrentComponent = slides[currentSlide].component;
return (
<Box sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
minHeight: '100vh',
backgroundColor: '#fff' ,
'&::-webkit-scrollbar': { display: 'none' },
}}>
<Box sx={{ width: { sm: '40%', xs: '0%' }, display: { xs: 'none', sm: 'block' } }}>
<Side
currentStepIndex={currentSlide}
onNext={handleNext}
onBack={handleBack}
stepCompleted={stepCompleted}
/>
</Box>
<Box
sx={{
width: { sm: '60%' },
minHeight: { xs: '100vh', sm: '130vh', md: '90vh' },
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
paddingTop: { xs: 2, sm: 4 },
paddingLeft: { xs: 2, sm: 4 },
backgroundColor: '#fff',
}}
>
<Box sx={{ width: '100%', maxWidth: 686, pr: { xs: 3, sm: 2, md: 1 }, pt: { xs: 2, sm: 4, md: 1 } }}>
<Stack spacing={2} alignItems="flex-start">
<CurrentComponent
emailOrPhone={emailOrPhone}
setEmailOrPhone={setEmailOrPhone}
onStepComplete={() => {
setStepCompleted(true);
setCurrentSlide(prev => prev + 1); // هنا تنقل مباشرة للصفحة التالية
}}
onBack={handleBack}
/>
</Stack>
</Box>
<Box
sx={{
position: 'absolute',
bottom: { xs: '5%', sm: '5%', md: '2%' },
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
gap: 1,
width: '80%',
maxWidth: 600,
justifyContent: 'center',
height: { xs: 3, sm: 5, md: 6 },
}}
>
{slides.map((_, index) => (
<Box
key={index}
sx={{
width: '100vh',
height: 5,
bgcolor: index === currentSlide ? theme.palette.primary.main : 'rgba(0, 0, 0, 0.2)',
borderRadius: 2,
cursor: 'pointer',
transition: 'background-color 0.3s',
}}
/>
))}
</Box>
</Box>
</Box>
);
};
export default ForgetForm;
@@ -0,0 +1,222 @@
import React, { useState } from 'react';
import { Box, Button, TextField, Typography, IconButton, InputAdornment } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import { VisibilityOutlined } from '@mui/icons-material';
import authService from '../../../services/authService';
const NewPassword = ({ emailOrPhone, onStepComplete }) => {
const theme = useTheme();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});
const handleTogglePassword = () => setShowPassword(prev => !prev);
const handleToggleConfirmPassword = () => setShowConfirmPassword(prev => !prev);
const handleSend = async () => {
setErrors({});
if (!password || !passwordConfirm) {
alert("Please fill in both password fields.");
return;
}
if (password !== passwordConfirm) {
alert("Passwords do not match.");
return;
}
setLoading(true);
try {
const response = await authService.updatePassword({
email: emailOrPhone,
password,
confirmPassword: passwordConfirm
});
setLoading(false);
if (response.success) {
alert("Password updated successfully.");
if (onStepComplete) onStepComplete();
} else {
alert(response.message || "Failed to update password.");
}
} catch (error) {
setLoading(false);
if (error.response && error.response.status === 422) {
setErrors(error.response.data.errors || {});
} else {
alert("Error: " + (error.message || "Unknown error"));
}
}
};
return (
<>
<Box>
<Box
component="img"
src="/image.png"
alt="logo"
sx={{
width: { xs: '5vh', sm: '7vh' },
maxWidth: { xs: '40px', sm: '80px' },
height: 'auto',
objectFit: 'contain'
}}
/>
</Box>
<Typography
variant="h4"
fontWeight={700}
sx={{
fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' }
}}
>
Create a New Password
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
Enter your new password to regain access to your account.
</Typography>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Password
</Typography>
<TextField
type={showPassword ? 'text' : 'password'}
placeholder="Enter your password"
fullWidth
variant="outlined"
autoComplete="new-password"
value={password}
onChange={e => setPassword(e.target.value)}
error={!!errors.password}
helperText={errors.password && errors.password.join(' ')}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& input::-ms-reveal, & input::-ms-clear': { display: 'none' },
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleTogglePassword} edge="end">
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
</IconButton>
</InputAdornment>
)
}}
/>
</Box>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Confirm Password
</Typography>
<TextField
type={showConfirmPassword ? 'text' : 'password'}
placeholder="Confirm your password"
fullWidth
variant="outlined"
autoComplete="new-password"
value={passwordConfirm}
onChange={e => setPasswordConfirm(e.target.value)}
error={!!errors.password_confirmation}
helperText={errors.password_confirmation && errors.password_confirmation.join(' ')}
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)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
},
'& input::-ms-reveal, & input::-ms-clear': { display: 'none' },
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleToggleConfirmPassword} edge="end">
{showConfirmPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
</IconButton>
</InputAdornment>
)
}}
/>
</Box>
<Button
variant="contained"
fullWidth
onClick={handleSend}
disabled={loading}
sx={{
color: 'white',
backgroundColor: theme.palette.primary.main,
borderRadius: '30px',
textTransform: 'none',
fontWeight: 600,
fontSize: '16px',
height: '48px',
'&:hover': {
backgroundColor: theme.palette.primary.hover,
transition: 'all 0.4s ease'
}
}}
>
{loading ? "Sending..." : "Send"}
</Button>
{/* Terms */}
<Typography
// variant="caption"
textAlign="center"
fontWeight={500}
sx={{ fontSize: '16px', width: '100%', mx: 'auto', color: '#969BA7', mt: 2 }}
>
By logging in, I agree to the{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/terms', 'TermsOfService')}
>
Terms of Service
</Box>{' '}
and{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
>
Privacy Policy
</Box>.
</Typography>
</>
);
};
export default NewPassword;
@@ -0,0 +1,241 @@
import React, { useState, useRef } from 'react';
import {
Box, TextField, Typography, Button, Stack, useTheme,
Snackbar, Alert
} from '@mui/material';
import authService from '../../../services/authService';
const OtpVerification = ({ emailOrPhone, onStepComplete }) => {
const [otp, setOtp] = useState(Array(6).fill(''));
const inputRefs = useRef(Array(6).fill().map(() => React.createRef()));
const [loading, setLoading] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [snackbarSeverity, setSnackbarSeverity] = useState('success');
const theme = useTheme();
const handleSnackbarClose = (event, reason) => {
if (reason === 'clickaway') return;
setSnackbarOpen(false);
};
const handleSend = async () => {
const otpCode = otp.join('');
if (otpCode.length < 6) {
setSnackbarMessage("Please enter the complete OTP code.");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
setLoading(true);
try {
const response = await authService.verifyCode(emailOrPhone, otpCode);
setLoading(false);
if (response.success !== false) {
setSnackbarMessage("OTP verified successfully.");
setSnackbarSeverity("success");
setSnackbarOpen(true);
if (onStepComplete) onStepComplete(); // الانتقال للخطوة التالية
} else {
setSnackbarMessage(response.message || "Invalid OTP code.");
setSnackbarSeverity("error");
setSnackbarOpen(true);
}
} catch (error) {
setLoading(false);
setSnackbarMessage("Failed to verify OTP. Please try again.");
setSnackbarSeverity("error");
setSnackbarOpen(true);
}
};
const handleOtpChange = (e, index) => {
const value = e.target.value;
if (!/^[0-9]?$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
if (value && index < otp.length - 1) {
inputRefs.current[index + 1].current.focus();
}
};
const handleKeyDown = (e, index) => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
inputRefs.current[index - 1].current.focus();
}
};
return (
<>
<Box>
<Box
component="img"
src="/image.png"
alt="logo"
sx={{
width: { xs: '5vh', sm: '7vh' },
maxWidth: { xs: '40px', sm: '80px' },
height: 'auto',
objectFit: 'contain'
}}
/>
</Box>
<Typography
variant="h4"
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem'
}
}}
>
Enter OTP Verification
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
Kindly enter the OTP code sent to your registered email/phone for account verification.
</Typography>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
<Stack
direction="row"
spacing={{ xs: 0, sm: 1, md: 1.5 }}
justifyContent={{ xs: 'center', sm: 'center', md: 'flex-start' }}
flexWrap="wrap"
sx={{
width: '100%',
px: { xs: 0, sm: '0.5rem', md: '1.5rem' },
gap: { xs: '1.5rem', sm: '1.5rem', md: '1rem' }
}}
>
{otp.map((digit, index) => (
<TextField
key={index}
inputRef={inputRefs.current[index]}
value={digit}
onChange={(e) => handleOtpChange(e, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
inputProps={{
maxLength: 1,
style: {
textAlign: 'center',
fontSize: '20px',
},
}}
sx={{
width: { xs: '3rem', sm: '3.25rem', md: '4rem' },
height: { xs: '3rem', sm: '3.25rem', md: '4rem' },
'& .MuiOutlinedInput-root': {
borderRadius: '0.5rem',
height: '100%',
'& fieldset': {
borderColor: '#ccc',
},
'&:hover fieldset': {
borderColor: theme.palette.primary.main,
},
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
},
},
'& .MuiInputBase-input': {
color: theme.palette.primary.main,
textAlign: 'center',
fontSize: '20px',
padding: 0,
height: '100%',
},
}}
/>
))}
</Stack>
</Box>
<Button
variant="contained"
fullWidth
onClick={handleSend}
disabled={loading}
sx={{
color: 'white',
backgroundColor: theme.palette.primary.main,
borderRadius: '30px',
textTransform: 'none',
fontWeight: 600,
fontSize: '16px',
width: '95%',
height: '48px',
mx: 'auto',
display: 'block',
'&:hover': {
backgroundColor: theme.palette.primary.hover,
transition: 'all 0.4s ease'
}
}}
>
{loading ? "Sending..." : "Send"}
</Button>
{/* Terms */}
<Typography
// variant="caption"
textAlign="center"
fontWeight={500}
sx={{ fontSize: '16px', width: '100%', mx: 'auto', color: '#969BA7', mt: 2 }}
>
By logging in, I agree to the{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/terms', 'TermsOfService')}
>
Terms of Service
</Box>{' '}
and{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
>
Privacy Policy
</Box>.
</Typography>
<Snackbar
open={snackbarOpen}
autoHideDuration={4000}
onClose={handleSnackbarClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={handleSnackbarClose}
severity={snackbarSeverity}
sx={{
width: '100%',
fontSize: '16px',
fontWeight: '500',
backgroundColor: theme.palette.primary.main,
borderRadius: 6,
mb: 6
}}
variant="filled"
>
{snackbarMessage}
</Alert>
</Snackbar>
</>
);
};
export default OtpVerification;
@@ -0,0 +1,185 @@
import React from 'react';
import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
const steps = [
{
title: 'Forget Password',
description: 'Provide an email to change your password',
icon: '/images/icons/key.png',
},
{
title: 'Verify your email',
description: 'Enter your otp verification',
icon: '/images/icons/email.png',
},
{
title: 'Create a new password',
description: 'Create your a new password',
icon: '/images/icons/pen.png',
},
{
title: 'Welcome to KitchPlus app',
description: 'Get up and running in 3 minutes',
icon: '/images/icons/rocket.png',
},
];
const Side = ({ currentStepIndex, onNext, onBack }) => {
const theme = useTheme();
return (
<Box
sx={{
height: { xs: '100vh', sm: '130vh', md: '87.5vh' },
backgroundColor: '#FAFAFA',
px: { xs: 2, sm: 4 },
py: 5,
display: { xs: 'none', sm: 'block' },
position: 'relative',
}}
>
<Typography variant="h6" fontWeight={700} mb={2}>
Step for change your password
</Typography>
<Typography variant="body2" color="text.secondary" mb={4}>
Instructions for secure password modification. Follow simple steps for password change.
</Typography>
<Stack spacing={6} position="relative" sx={{ ml: {xs:-1,md:2.1} }}>
{steps.map((step, index) => (
<Box key={index} display="flex" alignItems="flex-start" gap={2} position="relative">
{index !== steps.length - 1 && (
<Box
sx={{
position: 'absolute',
left: '3.8vh',
top: '6vh',
width: '2px',
height: 'calc(100% + 24px)',
backgroundColor: '#e0e0e0',
zIndex: 0,
}}
/>
)}
<Box
sx={{
position: 'relative',
width: 50,
height: 53,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box
sx={{
position: 'absolute',
width: 60,
height: 63,
borderRadius: '50%',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 0,
backgroundImage:
index === currentStepIndex
? 'linear-gradient(0deg, #FF914D, #F3F3F3 )'
: 'none',
backgroundColor: index === currentStepIndex ? 'transparent' : '#F3F3F3',
}}
/>
<Box
sx={{
width: 50,
height: 53,
backgroundColor: '#FFFFFF',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
<img
src={step.icon}
alt={step.title}
style={{ width: 24, height: 24, objectFit: 'contain' }}
/>
</Box>
</Box>
<Box >
<Typography variant="subtitle1" sx={{ fontSize: '16px', fontWeight: 600 }}>
{step.title}
</Typography>
<Typography variant="body2" sx={{ color: '#A0A0A0', fontSize: '12px', fontWeight: 500 }}>
{step.description}
</Typography>
</Box>
</Box>
))}
</Stack>
{/* أزرار التنقل */}
<Box
sx={{
position: 'absolute',
bottom: {sm:31 , md:2},
left: '50%',
transform: 'translateX(-50%)',
width: '90%',
maxWidth: 600,
display: 'flex',
justifyContent: 'space-between',
px: 2,
}}
>
{/* Back */}
<Button
variant="text"
startIcon={<ArrowBackIosIcon sx={{ fontSize: '16px' }} />}
onClick={onBack}
disabled={currentStepIndex === 0}
sx={{
color: currentStepIndex === 0 ? '#C2C2C2' : '#6B7283',
minWidth: '100px',
fontWeight: 400,
fontSize: '16px',
textTransform: 'none',
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.primary.main
}
}}
>
Back
</Button>
{/* Next */}
<Button
variant="text"
endIcon={<ArrowForwardIosIcon sx={{ fontSize: '16px' }} />}
onClick={onNext}
disabled={currentStepIndex === steps.length - 1}
sx={{
color: currentStepIndex === steps.length - 1 ? '#C2C2C2' : '#6B7283',
minWidth: '100px',
fontWeight: 400,
fontSize: '16px',
textTransform: 'none',
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.primary.main,
transition: 'all 0.4s ease'
}
}}
>
Next
</Button>
</Box>
</Box>
);
};
export default Side;
@@ -0,0 +1,192 @@
import React, { useEffect } from 'react';
import { Button, Typography, Container, Box, Paper } from '@mui/material';
/*
File: TermsPrivacyComponents.jsx
Purpose: Two simple standalone pages (Terms and Privacy) and an example LoginCard
which opens them in a new tab/window and listens for the "agree" message.
How it works:
- The LoginCard opens a new tab using window.open(...).
- The opened page (TermsPage / PrivacyPage) will send a postMessage back to the opener
when the user clicks "Agree & Confirm", then attempt to close itself.
- The LoginCard listens for that postMessage and can react (e.g. show a toast,
update state, or navigate). This keeps the original page in control of what
happens after the user agreed.
Note: Some browsers may block window.close() on tabs not opened by window.open.
Because LoginCard uses window.open, the close() call should succeed. If the
page is opened directly (no opener) the Agree button will fall back to
navigating back (history.back()).
*/
/***********************
* Terms / Privacy Pages
***********************/
function sendAgreeToOpener(kind = 'terms') {
try {
if (window.opener && !window.opener.closed) {
// Structured message so opener can identify the response
window.opener.postMessage({ type: 'AGREE_CONFIRM', page: kind }, '*');
// Try to close this tab/window (works when opened by window.open)
window.close();
} else {
// No opener (user opened directly) — go back in history as a fallback
if (window.history && window.history.length > 1) {
window.history.back();
} else {
// Last fallback: navigate to the site's root
window.location.href = '/';
}
}
} catch (e) {
console.error('Failed to send message to opener or close window', e);
// fallback navigation
window.location.href = '/';
}
}
export function TermsPage() {
React.useEffect(() => {
document.title = "Resturant-Terms of Service";
}, []);
return (
<Container maxWidth="md" sx={{ py: 6 }}>
<Paper elevation={3} sx={{ p: 4 }}>
<Typography variant="h4" gutterBottom>
Terms of Service
</Typography>
<Typography variant="body1" paragraph>
Welcome to our Terms of Service. Please read carefully. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Integer posuere erat a ante.
</Typography>
<Box textAlign="center" sx={{ mt: 4 , color:'white'}}>
<Button variant="contained" onClick={() => sendAgreeToOpener('terms')} sx={{color:'white' , textTransform:'none'}}>
Agree & Confirm
</Button>
</Box>
</Paper>
</Container>
);
}
export function PrivacyPage() {
React.useEffect(() => {
document.title = "Resturant-Privacy Policy";
}, []);
return (
<Container maxWidth="md" sx={{ py: 6 }}>
<Paper elevation={3} sx={{ p: 4 }}>
<Typography variant="h4" gutterBottom>
Privacy Policy
</Typography>
<Typography variant="body1" paragraph>
This Privacy Policy explains how we collect and use personal information. Lorem ipsum dolor
sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore.
</Typography>
<Box textAlign="center" sx={{ mt: 4 ,color:'white'}}>
<Button variant="contained" onClick={() => sendAgreeToOpener('privacy')} sx={{color:'white' , textTransform:'none'}}>
Agree & Confirm
</Button>
</Box>
</Paper>
</Container>
);
}
/***********************
* Example LoginCard
***********************/
export default function LoginCard() {
useEffect(() => {
// Listen to messages from the terms/privacy pages.
const handler = (event) => {
// IMPORTANT: In production, check event.origin to be sure the message comes from
// a trusted origin. For demo purposes we accept any origin.
const data = event.data || {};
if (data && data.type === 'AGREE_CONFIRM') {
// data.page === 'terms' or 'privacy'
const page = data.page || 'unknown';
// Do whatever you need when user agreed: update state, show toast, close dialog, etc.
// Example: simple alert (replace with MUI Snackbar or app-level handler)
alert(`User agreed to ${page === 'terms' ? 'Terms of Service' : 'Privacy Policy'}`);
}
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, []);
// Helper that opens a new tab (ensures opener exists)
function openInNewTab(path, name) {
// If your app uses router paths (e.g. /terms), build a full URL. Here we create
// a URL relative to current origin so the opened page belongs to the same origin
const url = new URL(path, window.location.href).toString();
// Use window.open (not anchor target="_blank") so the new tab has window.opener
window.open(url, name, 'noopener=false');
}
return (
<Paper elevation={2} sx={{ p: 4, maxWidth: 420, margin: '40px auto' }}>
<Typography variant="h6" textAlign="center" gutterBottom>
{/* Login title */}
Sign in
</Typography>
{/* Your existing Terms text (you provided earlier) */}
<Typography
variant="caption"
sx={{ fontSize: '14px', textAlign: 'center', color: '#969BA7', pt: 5 }}
>
By logging in, I agree to the{' '}
<Box component="span" sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }} onClick={() => openInNewTab('/terms', 'TermsOfService')}>
Terms of Service
</Box>{' '}
and{' '}
<Box component="span" sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }} onClick={() => openInNewTab('/privacy', 'PrivacyPolicy')}>
Privacy Policy
</Box>
.
</Typography>
{/* The rest of your login UI goes here (inputs, submit, etc.) */}
</Paper>
);
}
/*
Usage notes:
- If you're using a router (react-router), create routes for /terms and /privacy
that render <TermsPage /> and <PrivacyPage /> respectively.
Example (react-router v6):
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoginCard, { TermsPage, PrivacyPage } from './TermsPrivacyComponents';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<LoginCard />} />
<Route path="/terms" element={<TermsPage />} />
<Route path="/privacy" element={<PrivacyPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Security reminder: validate event.origin in the message handler in production.
*/
@@ -0,0 +1,428 @@
import React, { useState } from 'react';
import { TextField, Button, Typography, Stack, Box, IconButton, InputAdornment } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import { VisibilityOutlined } from '@mui/icons-material';
import SidePanel from './SidePanel';
import { useNavigate, Link } from 'react-router-dom';
import authService from '../../../services/authService';
import { GoogleLogin, useGoogleLogin } from '@react-oauth/google';
import { jwtDecode } from "jwt-decode";
const LoginForm = () => {
const navigate = useNavigate();
const theme = useTheme();
const [showPassword, setShowPassword] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleTogglePassword = () => {
setShowPassword(prev => !prev);
};
const handleLogin = async () => {
setLoading(true);
setError('');
const result = await authService.login(email, password);
if (!result.success) {
// 🔹 أولًا: نتحقق إن كانت الرسالة تشير إلى أن الحساب غير موجود
if (result.message && result.message.includes('Account not found')) {
setError('This account is not registered.');
}
// 🔹 ثانيًا: نتحقق من "Invalid credentials"
else if (result.message && result.message.includes('Invalid credentials')) {
setError('Email or password is incorrect.');
}
// 🔹 ثالثًا: نعرض أول خطأ موجود في كائن errors
else if (result.errors) {
const firstError = Object.values(result.errors)[0][0];
setError(firstError);
}
// 🔹 أخيرًا: fallback لأي رسالة أخرى
else {
setError(result.message || 'Login failed');
}
setLoading(false);
return;
}
localStorage.setItem('token', result.data.token);
localStorage.setItem('refresh_token', result.data.refresh_token);
setLoading(false);
navigate('/restaurant', { replace: true });
};
const handleGoogleSuccess = async (tokenResponse) => {
try {
const res = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
headers: {
Authorization: `Bearer ${tokenResponse.access_token}`,
},
});
const userInfo = await res.json();
console.log('Google User Info:', userInfo);
if (userInfo.email) {
setEmail(userInfo.email);
}
setPassword('');
localStorage.setItem("google_user", JSON.stringify(userInfo));
} catch (e) {
console.error("Google login failed:", e);
setError("Google login failed");
}
};
const handleGoogleError = () => {
setError("Google login failed");
};
const loginWithGoogle = useGoogleLogin({
onSuccess: handleGoogleSuccess,
onError: handleGoogleError,
});
return (
<Box
bgcolor={"background.default"}
color={"text.primary"}
display="flex"
sx={{
width: '100vw',
height: '100vh',
overflow: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}
>
<SidePanel />
<Box
sx={{
marginLeft: { xs: 0, sm: '50%' },
width: { xs: '100%', sm: '50%', md: '70%' },
}}
>
<Box
flex={1}
paddingTop={4}
paddingBottom={4}
paddingLeft={2}
paddingRight={2}
sx={{
backgroundColor: '#FFFFFF',
minHeight: '10vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'sticky',
}}
>
<Box
sx={{
width: {
xs: '120%',
sm: '100%',
md: '150%',
lg: '150%'
},
maxWidth: '600px'
}}
>
<Stack spacing={2}>
{/* الشعار */}
<Box>
<img
// src="/image.png"
// alt="logo"
// style={{
// width: '4vw',
// maxWidth: '80px',
// height: 'auto',
// objectFit: 'contain'
// }}
/>
</Box>
{/* عنوان تسجيل الدخول */}
<Typography variant="h4" fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
Login
</Typography>
<Typography variant="body2" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
Enter your username and password to access your account securely. Welcome back to our service!
</Typography>
{/* إدخال البريد */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Email
</Typography>
<TextField
placeholder="Enter your email"
type="email"
variant="outlined"
fullWidth
value={email}
onChange={e => setEmail(e.target.value)}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
{/* إدخال كلمة السر */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Password
</Typography>
<TextField
type={showPassword ? 'text' : 'password'}
placeholder="Enter your password"
fullWidth
variant="outlined"
autoComplete="new-password"
value={password}
onChange={e => setPassword(e.target.value)}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
},
'& input::-ms-reveal, & input::-ms-clear': {
display: 'none',
},
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleTogglePassword} edge="end">
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
</IconButton>
</InputAdornment>
)
}}
/>
</Box>
{/* عرض الخطأ */}
{error && (
<Typography color="error" textAlign="center">
{error}
</Typography>
)}
{/* زر تسجيل الدخول */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleLogin}
disabled={loading}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover
}
}}
>
{loading ? 'Loading...' : 'Log in'}
</Button>
</Box>
{/* Divider */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 2 }}>
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
<Typography variant="body1" sx={{ fontWeight: 500, fontSize: '16px', color: 'black', fontFamily: 'PlusJakartaSans' }}>
Or
</Typography>
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
</Box>
{/* <Button
variant="outlined"
fullWidth
onClick={loginWithGoogle}
sx={{
fontWeight: 500,
fontSize: '16px',
borderRadius: '50px',
height: '50px',
textTransform: 'none',
gap: 1,
borderColor: '#E6E6E6',
color: 'black',
'&:hover': {
borderColor: 'black',
backgroundColor: 'transparent'
},
fontFamily: 'PlusJakartaSans'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<img
src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg"
alt="Google"
style={{ width: 25, height: 25 }}
/>
</Box>
Login with Google
</Button> */}
{/* Facebook Button */}
{/* <Button
variant="outlined"
fullWidth
sx={{
fontWeight: 500,
fontSize: '16px',
borderRadius: '50px',
height: '50px',
textTransform: 'none',
gap: 1,
borderColor: '#E6E6E6',
color: 'black',
'&:hover': {
borderColor: 'black',
backgroundColor: 'transparent'
}
, fontFamily: 'PlusJakartaSans'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<img
src="https://upload.wikimedia.org/wikipedia/commons/0/05/Facebook_Logo_%282019%29.png"
alt="Facebook"
style={{ width: 25, height: 25 }}
/>
</Box>
Login with Facebook
</Button> */}
{/* Register Link */}
<Typography
variant="body2"
sx={{
fontSize: '16px',
textAlign: 'center',
color: '#969BA7',
pt: 2
}}
>
Don’t have an account?{' '}
<Link
to="/register"
style={{
color: '#2261FF',
textDecoration: 'none'
}}
>
Register
</Link>
</Typography>
{/* ForgetPassword Link */}
<Typography
variant="body2"
sx={{
fontSize: '14px',
textAlign: 'center',
color: '#969BA7',
pt: 3
}}
>
Did you forget the password?{' '}
<Link
to="/forget"
style={{
color: '#2261FF',
textDecoration: 'none'
}}
>
Forget Password
</Link>
</Typography>
{/* Terms */}
<Typography
variant="caption"
sx={{
fontSize: '14px',
textAlign: 'center',
color: '#969BA7',
pt: 5
}}
>
By logging in, I agree to the{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/terms', 'TermsOfService')}
>
Terms of Service
</Box>{' '}
and{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
>
Privacy Policy
</Box>.
</Typography>
</Stack>
</Box>
</Box>
</Box>
</Box>
);
};
export default LoginForm;
@@ -0,0 +1,501 @@
import React, { useState ,useContext } from 'react';
import { TextField, Button, Typography, Stack, Box, IconButton, InputAdornment } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import { VisibilityOutlined } from '@mui/icons-material';
import SidePanel from './SidePanel';
import { Link, useNavigate } from 'react-router-dom';
import authService from '../../../services/authService';
import { UserContext } from '../../../contexts/UserContext';
// import { useGoogleRegister } from '@react-oauth/google';
const generateStrongPassword = () => {
const length = 12;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
let password = "";
for (let i = 0, n = charset.length; i < length; ++i) {
password += charset.charAt(Math.floor(Math.random() * n));
}
return password;
};
const Register = () => {
const theme = useTheme();
const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [snackbarOpen, setSnackbarOpen] = useState(false);
const { setUser } = useContext(UserContext);
// تفعيل/تعطيل إظهار كلمة السر
const handleTogglePassword = () => {
setShowPassword((prev) => !prev);
};
const handleToggleConfirmPassword = () => {
setShowConfirmPassword((prev) => !prev);
};
const handleRegister = async () => {
setLoading(true);
setError('');
setSuccessMessage('');
const result = await authService.register(email, password, confirmPassword);
if (!result.success) {
if (result.errors) {
const firstError = Object.values(result.errors)[0][0];
setError(firstError);
} else if (result.message && result.message.includes('Duplicate entry')) {
setError('The email has already been taken.');
} else {
setError(result.message || 'Registration failed');
}
setLoading(false);
return;
}
// تسجيل ناجح
localStorage.setItem('token', result.data.token);
localStorage.setItem('refresh_token', result.data.refresh_token);
// ✅ تحديث الـ User Context مباشرة بعد التسجيل
setUser({
email: email,
token: result.data.token,
refresh_token: result.data.refresh_token,
adminData: result.data.Admin || null,
});
setTimeout(() => {
navigate('/restaurant', { replace: true });
setLoading(false);
}, 500);
};
const handleGoogleSuccess = async (tokenResponse) => {
try {
const res = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
headers: {
Authorization: `Bearer ${tokenResponse.access_token}`,
},
});
const userInfo = await res.json();
console.log('Google User Info:', userInfo);
if (userInfo.email) {
const generatedPassword = generateStrongPassword(); // كلمة مرور مقترحة
setEmail(userInfo.email);
setPassword(generatedPassword);
setConfirmPassword(generatedPassword);
setError('');
setTimeout(() => {
setSuccessMessage('');
}, 5000);
} else {
setError('Google Register failed: No email found');
}
} catch (e) {
console.error('Google Register failed:', e);
setError('Google Register failed');
}
};
const handleGoogleError = () => {
setError('Google Register failed');
};
const handleSnackbarClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setSnackbarOpen(false);
};
// // تهيئة تسجيل الدخول عبر جوجل باستخدام useGoogleRegister
// const RegisterWithGoogle = useGoogleRegister({
// onSuccess: handleGoogleSuccess,
// onError: handleGoogleError,
// });
return (
<Box
bgcolor={'background.default'}
color={'text.primary'}
display="flex"
sx={{
width: '100vw',
height: '100vh',
overflow: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}
>
<SidePanel />
<Box
sx={{
marginLeft: { xs: 0, sm: '50%' },
width: { xs: '100%', sm: '50%' },
}}
>
<Box
flex={1}
paddingTop={4}
paddingBottom={4}
paddingLeft={2}
paddingRight={2}
sx={{
backgroundColor: '#FFFFFF',
minHeight: '10vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'sticky',
}}
>
<Box
sx={{
width: {
xs: '120%',
sm: '100%',
md: '120%',
lg: '120%',
},
maxWidth: '600px',
}}
>
<Stack spacing={2}>
{/* logo */}
<Box>
<img
src="/image.png"
alt="logo"
style={{
width: '4vw',
maxWidth: '80px',
height: 'auto',
objectFit: 'contain',
}}
/>
</Box>
<Typography
variant="h4"
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem',
},
}}
>
Register
</Typography>
<Typography variant="body2" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
Please fill out the registration form with accurate information to create your account successfully.
</Typography>
{/* حقل الايميل */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Email
</Typography>
<TextField
placeholder="Enter your email"
type="email"
variant="outlined"
fullWidth
value={email}
onChange={(e) => setEmail(e.target.value)}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
},
}}
/>
</Box>
{/* حقل كلمة المرور */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Password
</Typography>
<TextField
type={showPassword ? 'text' : 'password'}
placeholder="Enter your password"
fullWidth
variant="outlined"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
},
'& input::-ms-reveal, & input::-ms-clear': {
display: 'none',
},
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleTogglePassword} edge="end">
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
{/* حقل التاكيد لكلمة المرور */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Confirm Password
</Typography>
<TextField
type={showConfirmPassword ? 'text' : 'password'}
placeholder="Confirm your password"
fullWidth
variant="outlined"
autoComplete="new-password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
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)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
},
'& input::-ms-reveal, & input::-ms-clear': {
display: 'none',
},
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleToggleConfirmPassword} edge="end">
{showConfirmPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
{/* Error & Success Message */}
{error && (
<Typography color="error" textAlign="center">
{error}
</Typography>
)}
{successMessage && (
<Typography color="green" textAlign="center">
{successMessage}
</Typography>
)}
{/* زر التسجيل */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleRegister}
disabled={loading}
sx={{
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
},
}}
>
{loading ? 'Loading...' : 'Continue'}
</Button>
</Box>
{/* Divider */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 2 }}>
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
<Typography variant="body1" sx={{ fontWeight: 500, fontSize: '16px', color: 'black' }}>
Or
</Typography>
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
</Box>
{/* زر التسجيل عبر جوجل */}
{/* <Button
variant="outlined"
fullWidth
// onClick={() => RegisterWithGoogle()}
sx={{
fontWeight: 500,
fontSize: '16px',
borderRadius: '50px',
height: '50px',
textTransform: 'none',
gap: 1,
borderColor: '#E6E6E6',
color: 'black',
'&:hover': {
borderColor: 'black',
backgroundColor: 'transparent',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<img
src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg"
alt="Google"
style={{ width: 25, height: 25 }}
/>
</Box>
Register with Google
</Button> */}
{/* زر فيسبوك - فقط للعرض (لم تُضاف وظيفة) */}
{/* <Button
variant="outlined"
fullWidth
sx={{
fontWeight: 500,
fontSize: '16px',
borderRadius: '50px',
height: '50px',
textTransform: 'none',
gap: 1,
borderColor: '#E6E6E6',
color: 'black',
'&:hover': {
borderColor: 'black',
backgroundColor: 'transparent',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<img
src="https://upload.wikimedia.org/wikipedia/commons/0/05/Facebook_Logo_%282019%29.png"
alt="Facebook"
style={{ width: 25, height: 25 }}
/>
</Box>
Register with Facebook
</Button> */}
<Typography
variant="body2"
sx={{
fontSize: '16px',
textAlign: 'center',
color: '#969BA7',
pt: 3,
}}
>
Already have an account?{' '}
<Link
to="/login"
style={{
color: '#2261FF',
textDecoration: 'none',
}}
>
Login
</Link>
</Typography>
<Typography
variant="caption"
sx={{
fontSize: '14px',
textAlign: 'center',
color: '#969BA7',
pt: 2
}}
>
By logging in, I agree to the{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/terms', 'TermsOfService')}
>
Terms of Service
</Box>{' '}
and{' '}
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
>
Privacy Policy
</Box>.
</Typography>
</Stack>
</Box>
</Box>
</Box>
</Box>
);
};
export default Register;
@@ -0,0 +1,191 @@
import React, { useState, useEffect } from 'react';
import {
Button,
Typography,
Stack,
Box
} from '@mui/material';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
const SidePanel = ({ setMode, mode }) => {
const [currentSlide, setCurrentSlide] = useState(0);
const slides = [
{
image: '/images/waitress1.png',
title: "Welcome to our cutting-edge postal application,",
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
},
{
image: '/images/waitress2.png',
title: "Second Slide Title",
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
},
{
image: '/images/waitress3.png',
title: "Third Slide Title",
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
},
{
image: '/images/waitress4.png',
title: "Fourth Slide Title",
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
}
];
useEffect(() => {
const interval = setInterval(() => {
setCurrentSlide((prev) => (prev + 1) % slides.length);
}, 5000);
return () => clearInterval(interval);
}, [slides.length]);
return (
<Box
sx={{
display: { xs: 'none', sm: 'block' },
width: { sm: '50%' },
position: 'fixed',
height: '100vh',
top: 0,
left: 0,
zIndex: 10,
}}
>
<Box
flex={1}
padding={2}
sx={{
display: { xs: "none", sm: "block" },
position: 'sticky',
height: 'calc(102vh - 40px)',
}}
>
<Stack spacing={2} sx={{ width: '100%', height: '100%' }}>
<Box
sx={{
width: '100%',
height: '100vh',
position: 'relative',
overflow: 'hidden',
borderRadius: '20px',
}}
>
{/* الصورة الحالية */}
<img
src={slides[currentSlide].image}
alt={`Slide ${currentSlide + 1}`}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
display: 'block'
}}
/>
{/* مربع النص في الأسفل */}
<Box
sx={{
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
height: {
md: '37vh',
// xs: '5vh',
sm: '42vh',
lg: '31vh', // من 1200px إلى أقل من 1536px
},
bgcolor: 'rgba(0, 0, 0, 0.2)',
color: 'white',
borderBottomLeftRadius: '20px',
borderBottomRightRadius: '20px',
p: 3,
boxSizing: 'border-box',
boxShadow: '0px -40px 20px rgba(0, 0, 0, 0.2)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
}}
>
<Button
variant="contained"
size="small"
onClick={() => window.open('https://landing.velocrea.com/', '_blank')}
sx={{
mb: 1,
bgcolor: 'rgba(255, 255, 255, 0.24)',
color: 'white',
textTransform: 'none',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
padding: '6px 20px',
borderRadius: '50px',
fontSize: '16px',
fontWeight: 500,
marginBottom: '20px'
}}
>
KITCHPLUS
<ArrowForwardIcon sx={{ ml: 1, transform: 'rotate(-45deg)' }} />
</Button>
<Typography
sx={{
wordBreak: 'break-word',
fontSize: '120%',
fontWeight: 400,
}}
>
{slides[currentSlide].description}
</Typography>
</Box>
{/* مؤشرات الشرائح */}
<Box
sx={{
position: 'absolute',
bottom: '1vh', // جعل المؤشرات تحت النص
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
gap: 1,
width: '100%',
justifyContent: 'center', // تأكد من أن المؤشرات تأخذ عرض الصورة بالكامل
}}
>
{slides.map((_, index) => (
<Box
key={index}
sx={{
width: '20%',
height: 5,
bgcolor: index === currentSlide ? 'white' : 'rgba(255, 255, 255, 0.5)',
borderRadius: 2,
cursor: 'pointer',
transition: 'background-color 0.3s',
}}
onClick={() => setCurrentSlide(index)}
/>
))}
</Box>
</Box>
</Stack>
<Box position="fixed">
</Box>
</Box>
</Box>
)
}
export default SidePanel;
@@ -0,0 +1,104 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import AnalyticsContect from './AnalyticsContect';
const drawerWidth = 230;
const AnalyticsPage = () => {
const [timeFrame, setTimeFrame] = useState('month');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [hasProducts, setHasProducts] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
useEffect(() => {
const checkProducts = async () => {
setIsLoading(true);
const productsExist = await checkIfProductsExist();
setHasProducts(productsExist);
setIsLoading(false);
};
checkProducts();
}, []);
const checkIfProductsExist = async () => {
return new Promise((resolve) => setTimeout(() => resolve(true), 1500));
};
useEffect(() => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
}, [theme.breakpoints.values.md]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
const handleDrawerToggle = () => {
setSidebarOpen(!sidebarOpen);
};
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: { xs: '100%', sm: sidebarOpen ? `calc(100% - ${drawerWidth}px)` : '100%' },
marginLeft: 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% - 20px)' : '100%',
pt: { xs: 0.5, sm: 1 },
mt: { xs: 1, sm: 0 },
overflowY: 'auto',
pl: { xs: 0, sm: 1 },
pr: { xs: 1, sm: 1 },
pb: { xs: 1, sm: 2 },
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
transition: theme.transitions.create(['margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}>
<AnalyticsContect />
</Box>
</Box>
</Box>
);
};
export default AnalyticsPage;
@@ -0,0 +1,245 @@
import React, { useState, useEffect } from 'react';
import { useRestaurant } from '../../../contexts/RestaurantContext';
import StatisticsCard from './StatisticsCard';
import TablesManager from './TablesManager';
import {
Box,
useTheme,
useMediaQuery,
Button,
ButtonGroup,
TextField
} from '@mui/material';
import authService from '../../../services/authService';
import dayjs from 'dayjs';
const AnalyticsPage = () => {
const { restaurantId } = useRestaurant();
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [chartData, setChartData] = useState([]);
const handleTimeFrameChange = (newTimeFrame) => {
setTimeFrame(newTimeFrame);
};
const handleCustomDateChange = (event) => {
setCustomDate(event.target.value);
};
const fetchStatistics = async () => {
if (!restaurantId) return;
let period = 'daily';
let labels = [];
let mappedData = [];
switch (timeFrame) {
case '24h': // يومي
period = 'daily';
labels = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`);
try {
const response = await authService.getReservationStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label || item.date || '',
reservations_total: item.reservations_total || 0,
number_of_people_total: item.number_of_people_total || 0
}));
} else {
mappedData = labels.map(label => ({
label,
reservations_total: 0,
number_of_people_total: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
reservations_total: 0,
number_of_people_total: 0
}));
}
break;
case '30d': // شهري
period = 'monthly';
labels = Array.from({ length: 30 }, (_, i) => dayjs().startOf('month').add(i, 'day').format('MM-DD'));
try {
const response = await authService.getReservationStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label ? dayjs(item.label).format('MM-DD') : item.date ? dayjs(item.date).format('MM-DD') : '',
reservations_total: item.reservations_total || 0,
number_of_people_total: item.number_of_people_total || 0
}));
} else {
mappedData = labels.map(label => ({
label,
reservations_total: 0,
number_of_people_total: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
reservations_total: 0,
number_of_people_total: 0
}));
}
break;
case '12m': // سنوي
default:
period = 'yearly';
labels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
try {
const response = await authService.getReservationStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label || item.date || '',
reservations_total: item.reservations_total || 0,
number_of_people_total: item.number_of_people_total || 0
}));
} else {
mappedData = labels.map(label => ({
label,
reservations_total: 0,
number_of_people_total: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
reservations_total: 0,
number_of_people_total: 0
}));
}
break;
}
setChartData(mappedData);
};
useEffect(() => {
fetchStatistics();
}, [timeFrame, restaurantId, customDate]);
return (
<Box p={{ xs: 1, sm: 2 }} maxWidth="100%" overflowX="hidden">
{/* Tables Manager */}
<Box mb={1.5}>
{restaurantId && <TablesManager restaurantId={restaurantId} />}
</Box>
{/* Header Buttons */}
<Box
display="flex"
flexDirection={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems="center"
gap={2}
mb={1.5}
>
<ButtonGroup
size="small"
variant="outlined"
sx={{
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: 'inset 0 0 0 1px #e0e0e0',
'& .MuiButton-root': {
textTransform: 'none',
fontSize: '13px',
padding: '6px 12px',
border: 'none',
borderRadius: '8px',
color: '#555',
'&:hover': { backgroundColor: '#f5f5f5' }
},
'& .MuiButton-contained': {
backgroundColor: 'rgba(255, 117, 34, 0.08)',
color: '#ff5722',
fontWeight: 'bold'
}
}}
>
{[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => (
<Button
key={value}
variant={timeFrame === value ? 'contained' : 'text'}
onClick={() => handleTimeFrameChange(value)}
>
{label}
</Button>
))}
</ButtonGroup>
{/* Date Picker + Today Button */}
<Box
display="flex"
gap={1}
alignItems="center"
sx={{
backgroundColor: theme.palette.background.paper,
borderRadius: 2,
p: '4px 8px',
boxShadow: 'inset 0 0 0 1px #e0e0e0'
}}
>
<TextField
type="date"
value={customDate}
onChange={handleCustomDateChange}
size="small"
sx={{
width: isMobile ? '100%' : 150,
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
}}
/>
<Button
variant="contained"
size="small"
onClick={() => setCustomDate(dayjs().format('YYYY-MM-DD'))}
sx={{
backgroundColor: 'rgba(255, 117, 34, 0.08)',
color: '#ff5722',
textTransform: 'none',
boxShadow: 'none',
fontSize: isMobile ? 12 : 13,
'&:hover': { backgroundColor: 'rgba(255, 117, 34, 0.15)' }
}}
>
Today
</Button>
</Box>
</Box>
{/* StatisticsCard */}
<Box>
<StatisticsCard
title="Analytics Overview"
subtitle="Reservation Statistics"
data={chartData}
dataKeys={[
{ key: 'reservations_total', name: 'Reservations', color: '#4CAF50' },
{ key: 'number_of_people_total', name: 'People', color: '#9C27B0' },
]}
xDataKey="label"
valueFormatter={(value) => value}
timeFrame={timeFrame}
onTimeFrameChange={handleTimeFrameChange}
/>
</Box>
</Box>
);
};
export default AnalyticsPage;
@@ -0,0 +1,124 @@
import React from 'react';
import {
Box,
Typography,
Paper,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Chip,
IconButton,
useTheme,
useMediaQuery
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
const SalesByLocation = ({ data }) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<Paper sx={{
p: { xs: 1, sm: 2 },
borderRadius: '12px',
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.1)',
height: '95%',
display: 'flex',
flexDirection: 'column'
}}>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: { xs: 0.5, sm: 1 }
}}>
<Box>
<Typography sx={{
color: '#1A1C21',
fontWeight: 500,
fontSize: { xs: '16px', sm: '18px' },
lineHeight: 1.2
}}>
Sales by Location
</Typography>
<Typography sx={{
color: '#667085',
fontWeight: 500,
fontSize: { xs: '12px', sm: '14px' },
lineHeight: 1.2
}}>
Sales performance by location
</Typography>
</Box>
<IconButton size={isSmallScreen ? 'small' : 'medium'}>
<MoreVertIcon fontSize={isSmallScreen ? 'small' : 'medium'} />
</IconButton>
</Box>
<List dense sx={{
flexGrow: 1,
overflowY: 'auto',
'&::-webkit-scrollbar': {
display: 'none'
}
}}>
{data.map((item, i) => (
<ListItem key={i} disableGutters sx={{ py: { xs: 0.5, sm: 1 } }}>
<ListItemText
primary={
<Typography
sx={{
fontSize: { xs: '13px', sm: '14px' },
fontWeight: 400
}}
>
{item.country}
</Typography>
}
secondary={
<Typography
sx={{
fontSize: { xs: '11px', sm: '12px' },
color: theme.palette.text.secondary
}}
>
{item.sales} Sales
</Typography>
}
sx={{ my: 0 }}
/>
<ListItemSecondaryAction sx={{
display: 'flex',
alignItems: 'center',
gap: { xs: 0.5, sm: 1 }
}}>
<Typography
variant="body2"
sx={{
fontSize: { xs: '12px', sm: '14px' },
fontWeight: isSmallScreen ? 500 : 400
}}
>
${item.amount.toLocaleString()}
</Typography>
<Chip
label={`${item.change > 0 ? '+' : ''}${item.change}%`}
color={item.change > 0 ? 'success' : item.change < 0 ? 'error' : 'default'}
size={isSmallScreen ? 'small' : 'medium'}
sx={{
fontSize: { xs: '11px', sm: '12px' },
height: { xs: '24px', sm: '28px' },
'& .MuiChip-label': {
px: { xs: 0.5, sm: 1 }
}
}}
/>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</Paper>
);
};
export default SalesByLocation;
@@ -0,0 +1,225 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
Box,
IconButton,
Paper,
Typography,
useTheme,
useMediaQuery,
Menu,
MenuItem,
TextField,
Button
} from '@mui/material';
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
CartesianGrid
} from 'recharts';
// import MoreVertIcon from '@mui/icons-material/MoreVert';
const StatisticsCard = ({
title = "Statistics",
subtitle = "Delivery Times",
data = [],
dataKeys = [
{ key: 'revenue', name: 'Revenue', color: '#E46A11' },
{ key: 'sales', name: 'Sales', color: '#0182FC' }
],
xDataKey = 'month',
valueFormatter = (value) => value,
timeFrame = 'month',
onTimeFrameChange
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const [selectedDate, setSelectedDate] = useState('');
const [anchorEl, setAnchorEl] = useState(null);
const handleMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleDateChange = (event) => {
setSelectedDate(event.target.value);
if (onTimeFrameChange) onTimeFrameChange(timeFrame, event.target.value);
handleMenuClose();
};
const handleTodayClick = () => {
const today = new Date().toISOString().split('T')[0];
setSelectedDate(today);
if (onTimeFrameChange) onTimeFrameChange(timeFrame, today);
handleMenuClose();
};
// حساب العلامات الديناميكية للمحور الشاقولي
const ticksArray = (() => {
if (!data || data.length === 0) return [];
const maxValue = Math.max(
...data.flatMap(d => dataKeys.map(k => d[k.key] || 0))
);
const desiredTicks = 8; // عدد العلامات المطلوب
const step = Math.ceil(maxValue / desiredTicks) || 1;
const arr = [];
for (let i = 0; i <= maxValue; i += step) {
arr.push(i);
}
return arr;
})();
return (
<Box sx={{ borderRadius: 2, width: { sm: '100%', md: '167vh' } }}>
<Paper sx={{
p: { xs: 1.5, sm: 2 },
mb: { xs: 2, sm: 2 },
boxShadow: '0px 4px 5px rgba(0, 0, 0, 0.1)',
borderRadius: 2,
position: 'relative'
}}>
<IconButton
aria-label="more-actions"
sx={{
position: 'absolute',
top: { xs: 4, sm: 8 },
right: { xs: 4, sm: 8 },
color: '#667085'
}}
onClick={handleMenuOpen}
>
{/* <MoreVertIcon fontSize={isMobile ? 'small' : 'medium'} /> */}
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuItem>
<TextField
type="date"
value={selectedDate}
onChange={handleDateChange}
size="small"
sx={{ width: 150 }}
/>
</MenuItem>
<MenuItem>
<Button variant="outlined" size="small" onClick={handleTodayClick}>
Today
</Button>
</MenuItem>
</Menu>
{/* Header */}
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
flexWrap: 'wrap',
gap: 1
}}>
<Box>
<Typography variant="h6" sx={{ fontSize: { xs: '15px', sm: '18px' }, fontWeight: 500 }}>
{title}
</Typography>
<Typography sx={{ fontSize: { xs: '11px', sm: '14px' }, fontWeight: 500, color: '#667085' }}>
{subtitle}
</Typography>
</Box>
</Box>
{/* Chart */}
<ResponsiveContainer width="100%" height={isMobile ? 350 : isTablet ? 390 : 395}>
<AreaChart data={data} margin={{
top: isMobile ? -30 : -45,
right: isMobile ? 15 : 30,
left: isMobile ? 10 : 20,
bottom: isMobile ? 10 : 20
}}>
<defs>
{dataKeys.map(({ key, color }) => (
<linearGradient key={key} id={`color-${key}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
))}
</defs>
<CartesianGrid stroke="#eee" strokeDasharray="0 0" vertical={false} />
<XAxis
dataKey={xDataKey}
axisLine={false}
tickLine={false}
tickMargin={isMobile ? 8 : 15}
tick={{
fontSize: isMobile ? 11 : 12,
angle: -25,
textAnchor: 'end'
}}
interval={0}
/>
<YAxis
ticks={ticksArray}
tickFormatter={valueFormatter}
axisLine={false}
tickLine={false}
tickMargin={isMobile ? 8 : 15}
tick={{ fontSize: isMobile ? 11 : 12 }}
/>
<Tooltip formatter={valueFormatter} />
<Legend
verticalAlign="top"
height={isMobile ? 30 : 36}
iconType="circle"
iconSize={isMobile ? 8 : 10}
wrapperStyle={{ paddingBottom: isMobile ? '15px' : '20px' }}
/>
{dataKeys.map(({ key, name, color }) => (
<Area
key={key}
name={name}
type="monotone"
dataKey={key}
stroke={color}
strokeWidth={isMobile ? 2 : 3}
fill={`url(#color-${key})`}
/>
))}
</AreaChart>
</ResponsiveContainer>
</Paper>
</Box>
);
};
StatisticsCard.propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
data: PropTypes.arrayOf(PropTypes.object),
dataKeys: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
color: PropTypes.string.isRequired
})),
xDataKey: PropTypes.string,
valueFormatter: PropTypes.func,
timeFrame: PropTypes.oneOf(['day', 'week', 'month', 'year']),
onTimeFrameChange: PropTypes.func
};
export default StatisticsCard;
@@ -0,0 +1,456 @@
import React, { useState, useEffect } from "react";
import {
useMediaQuery,
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Skeleton,
Button,
TextField,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Menu,
Chip,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import AddIcon from "@mui/icons-material/Add";
import SaveIcon from "@mui/icons-material/Save";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import TuneIcon from "@mui/icons-material/Tune";
import authService from "../../../services/authService";
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
const theme = useTheme();
const handlePrev = () => currentPage > 1 && onChange(currentPage - 1);
const handleNext = () => currentPage < pageCount && onChange(currentPage + 1);
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<IconButton
size="small"
onClick={handlePrev}
disabled={currentPage <= 1}
sx={{
borderRadius: "8px",
backgroundColor: "#FFECE0",
// "&:hover": { backgroundColor: "#FFD6B5" },
color: theme.palette.primary.main,
// "&.Mui-disabled": { color: "#ccc", backgroundColor: "#FFF5E6" },
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<Box
sx={{
width: 32,
height: 32,
borderRadius: "8px",
backgroundColor: theme.palette.primary.main,
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 600,
fontSize: 14,
userSelect: "none",
}}
>
{currentPage}
</Box>
<IconButton
size="small"
onClick={handleNext}
disabled={currentPage >= pageCount}
sx={{
borderRadius: "8px",
backgroundColor: "#FFECE0",
// "&:hover": { backgroundColor: "#FFD6B5" },
color: theme.palette.primary.main,
// "&.Mui-disabled": { color: "#ccc", backgroundColor: "#FFF5E6" },
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
</Box>
);
};
const statusOptions = ["available", "unavailable"];
const TablesManager = ({ restaurantId }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const itemsPerPage = 4;
const [rows, setRows] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);
const [addingRow, setAddingRow] = useState(false);
const [newRow, setNewRow] = useState({
restaurant_id: restaurantId,
table_number: "",
capacity: "",
status: "available",
});
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [rowToDelete, setRowToDelete] = useState(null);
const [isSaving, setIsSaving] = useState(false);
// فلتر
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
const [statusFilter, setStatusFilter] = useState("all");
const fetchTables = async () => {
setLoading(true);
try {
const result = await authService.getTablesByRestaurant(restaurantId);
if (result.success) setRows(result.data);
} catch (err) {
console.error("❌ Error fetching tables:", err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (restaurantId) fetchTables();
}, [restaurantId]);
const handleSaveRow = async () => {
if (!newRow.table_number || !newRow.capacity) return;
try {
const result = await authService.addTable(newRow);
if (result.success) {
setRows((prev) => [result.data, ...prev]);
setAddingRow(false);
setNewRow({
restaurant_id: restaurantId,
table_number: "",
capacity: "",
status: "available",
});
}
} catch (err) {
console.error("❌ Error adding table:", err);
}
};
const handleDeleteRow = (row) => {
setRowToDelete(row);
setConfirmDeleteOpen(true);
};
const handleConfirmDelete = async () => {
if (!rowToDelete) return;
setIsSaving(true);
try {
const result = await authService.deleteTable(rowToDelete.id);
if (result.success) setRows((prev) => prev.filter((r) => r.id !== rowToDelete.id));
} catch (err) {
console.error(err);
} finally {
setIsSaving(false);
setConfirmDeleteOpen(false);
}
};
const handleFilterClick = (event) => setFilterAnchorEl(event.currentTarget);
const handleFilterClose = () => setFilterAnchorEl(null);
const handleFilterSelect = (value) => {
setStatusFilter(value);
setFilterAnchorEl(null);
};
const filteredRows =
(statusFilter === "all" ? rows : rows.filter((r) => r.status === statusFilter))
.sort((a, b) => parseInt(a.table_number) - parseInt(b.table_number));
const pageCount = Math.ceil(filteredRows.length / itemsPerPage);
const paginatedRows = filteredRows.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const getStatusChipProps = (status) => {
switch (status?.toLowerCase()) {
case "available":
return {
label: "Available",
sx: {
backgroundColor: "#E7F4EE",
color: "#0D894F",
fontWeight: 600,
fontSize: "13px",
minWidth: 90,
},
};
case "unavailable":
return {
label: "Unavailable",
sx: {
backgroundColor: "#fde8e8ff",
color: "#e41111ff",
fontWeight: 600,
fontSize: "13px",
minWidth: 100,
},
};
default:
return {
label: status,
sx: {
backgroundColor: "#FDF1E8",
color: "#E46A11",
fontWeight: 600,
fontSize: "13px",
minWidth: 80,
},
};
}
};
return (
<Box
sx={{
width: { xs: "100%", sm: "95.5%" },
pt: { xs: 2, sm: 2 },
pl: { xs: 2, sm: 3 },
pr: { xs: 2, sm: 3 },
pb: { xs: 2, sm: 2 },
// p: { xs: 1.5, sm: 2 },
// mb: { xs: 2, sm: 2 },
backgroundColor: "white",
borderRadius: 2,
}}
>
{/* Header */}
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", sm: "row" },
justifyContent: "space-between",
alignItems: { xs: "stretch", sm: "center" },
mb: 2,
gap: { xs: 1, sm: 0 },
}}
>
<Typography
variant="h5"
sx={{
fontWeight: 600,
fontSize: { xs: "18px", sm: "20px" },
textAlign: { xs: "center", sm: "left" },
}}
>
Tables
</Typography>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
{/* فلتر */}
<Button
variant="outlined"
sx={{
textTransform: "none",
color: "#667085",
borderColor: "#e0e0e0",
borderRadius: "8px",
height: "40px",
width: { xs: "120px", sm: "120px", md: "99px" },
fontSize: { xs: "0", sm: "13px", md: "14px" },
fontWeight: 600,
minWidth: "unset",
}}
startIcon={<TuneIcon fontSize={isMobile ? "small" : "small"} />}
onClick={handleFilterClick}
>
{isMobile ? "" : "Filters"}
</Button>
<Button
variant={addingRow ? "outlined" : "contained"}
color="primary"
startIcon={<AddIcon />}
onClick={() => setAddingRow(!addingRow)}
sx={{
color: addingRow ? "primary.main" : "white",
borderRadius: "8px",
fontWeight: 600,
fontSize: "14px",
height: "40px",
width: { xs: "100%", sm: "135px" },
textTransform: "none",
}}
>
{addingRow ? "Cancel" : "Add"}
</Button>
</Box>
</Box>
{/* Menu الفلاتر */}
<Menu anchorEl={filterAnchorEl} open={Boolean(filterAnchorEl)} onClose={handleFilterClose}>
{["all", "available", "unavailable"].map((status) => (
<MenuItem
key={status}
selected={statusFilter === status}
onClick={() => handleFilterSelect(status)}
sx={{
color: statusFilter === status ? "#FF914D" : "#4F5867",
fontWeight: statusFilter === status ? 700 : 500,
}}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</MenuItem>
))}
</Menu>
{/* جدول */}
<TableContainer
component={Paper}
sx={{
boxShadow: "none",
border: "1px solid #e0e0e0",
borderRadius: 2,
minWidth: 320,
height: {xs:"310px",md:"349px"},
}}
>
<Table
sx={{ minWidth: { sm: 550, md: 650 }, tableLayout: "auto" }}
aria-label="tables"
size={isMobile ? "small" : "medium"}
>
<TableHead sx={{ backgroundColor: "#f5f5f5" }}>
<TableRow>
<TableCell sx={{ pl: { md: 7 } }}>Table Number</TableCell>
<TableCell>Capacity</TableCell>
<TableCell>Status</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading
? Array.from({ length: itemsPerPage }).map((_, idx) => (
<TableRow key={`skeleton-${idx}`}>
<TableCell ><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
</TableRow>
))
: (
<>
{addingRow && (
<TableRow>
<TableCell >
<TextField
size="small"
placeholder="Table Number"
value={newRow.table_number}
onChange={(e) => setNewRow({ ...newRow, table_number: e.target.value })}
/>
</TableCell>
<TableCell>
<TextField
size="small"
type="number"
placeholder="Capacity"
value={newRow.capacity}
onChange={(e) => setNewRow({ ...newRow, capacity: e.target.value })}
/>
</TableCell>
<TableCell>
<Chip
label="Available"
sx={{
backgroundColor: "#E7F4EE",
color: "#0D894F",
fontWeight: 600,
fontSize: "13px",
minWidth: 90,
}}
/>
</TableCell>
<TableCell>
<IconButton color="success" onClick={handleSaveRow}>
<SaveIcon />
</IconButton>
</TableCell>
</TableRow>
)}
{paginatedRows.map((row) => (
<TableRow key={row.id} >
<TableCell sx={{ pl: { md: 9 } ,color:'#689dffff'}}>{row.table_number}</TableCell>
<TableCell>{row.capacity}</TableCell>
<TableCell>
<Chip {...getStatusChipProps(row.status)} />
</TableCell>
<TableCell>
<IconButton color="error" onClick={() => handleDeleteRow(row)}>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination Footer */}
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
pt={2}
>
<Typography variant="body2" color="text.secondary">
Showing {(currentPage - 1) * itemsPerPage + 1} -{" "}
{Math.min(currentPage * itemsPerPage, filteredRows.length)} of {filteredRows.length}
</Typography>
<SimplePagination currentPage={currentPage} pageCount={pageCount} onChange={setCurrentPage} />
</Box>
{/* Dialog الحذف */}
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete table "{rowToDelete?.table_number}"?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
<Button
onClick={handleConfirmDelete}
color="error"
variant="contained"
disabled={isSaving}
>
{isSaving ? "Deleting..." : "Delete"}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default TablesManager;
@@ -0,0 +1,22 @@
import React from 'react';
import { Button, Stack } from '@mui/material';
const options = ['All Time', '12 Months', '30 Days', '7 Days', '24 Hour'];
const TimeFrameSelector = ({ selected, onChange }) => (
<Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', mb: 2 }}>
{options.map(option => (
<Button
key={option}
size="small"
variant={selected === option ? 'contained' : 'outlined'}
onClick={() => onChange(option)}
sx={{ textTransform: 'none' }}
>
{option}
</Button>
))}
</Stack>
);
export default TimeFrameSelector;
+126
View File
@@ -0,0 +1,126 @@
// src/components/AppBar/KitchPlusAppBar.jsx
import React, { useEffect, useState, useContext } from 'react';
import {
AppBar,
Toolbar,
Typography,
Box,
IconButton,
Divider,
Avatar,
useTheme,
useMediaQuery,
} from '@mui/material';
import { useLocation, useNavigate } from 'react-router-dom';
import MenuIcon from '@mui/icons-material/Menu';
import HomeIcon from '@mui/icons-material/Home';
import { useRestaurant } from '../../contexts/RestaurantContext';
import authService from '../../services/authService';
import { UserContext } from '../../contexts/UserContext'; // ✅ استدعاء UserContext
const KitchPlusAppBar = ({ onDrawerToggle, sidebarOpen, isMobile }) => {
const location = useLocation();
const navigate = useNavigate();
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const isMediumScreen = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const { restaurantId } = useRestaurant();
const { user } = useContext(UserContext); // ✅ استخدم الـ Context بدلاً من localStorage
const [restaurantLogo, setRestaurantLogo] = useState('/images/default-restaurant.png');
useEffect(() => {
const fetchRestaurantLogo = async () => {
if (!restaurantId) return;
const res = await authService.getRestaurantById(restaurantId);
if (res.success && res.data) {
setRestaurantLogo(res.data.image_url);
}
};
fetchRestaurantLogo();
}, [restaurantId]);
return (
<AppBar
sx={{
height: { xs: 56, sm: 64, md: 66 },
backgroundColor: '#F6F6F6',
color: 'black',
boxShadow: 'none',
borderBottom: '1px solid #e0e0e0',
position: 'sticky',
top: 0,
zIndex: theme.zIndex.appBar,
}}
>
<Toolbar
sx={{
px: { xs: 2, sm: 3, md: '24px' },
minHeight: { xs: '56px !important', sm: '64px !important' },
display: 'flex',
justifyContent: 'space-between',
}}
>
{/* Left side */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
mr: 2,
}}
>
{(isMobile || isMediumScreen) && (
<IconButton color="inherit" edge="start" onClick={onDrawerToggle}>
<MenuIcon />
</IconButton>
)}
<Typography variant="h6" sx={{ fontWeight: '500' }}>
Welcome to KitchPlus
</Typography>
</Box>
{/* Right side */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.8, sm: 1, md: 1.5 } }}>
{location.pathname === '/dashboard' && <Divider orientation="vertical" flexItem sx={{ height: 40 }} />}
<IconButton
color="#667085"
size={isSmallScreen ? 'small' : 'medium'}
onClick={() => navigate('/restaurant')}
>
<HomeIcon fontSize={isSmallScreen ? 'medium' : 'medium'} />
</IconButton>
<Divider orientation="vertical" flexItem sx={{ height: 40 }} />
<Avatar
alt="Restaurant Logo"
src={restaurantLogo}
sx={{ width: { xs: 28, sm: 32, md: 40 }, height: { xs: 28, sm: 32, md: 40 } }}
/>
{!isSmallScreen && (
<Typography
variant="body1"
sx={{
ml: 1,
color: '#61677F',
fontSize: { xs: 12, sm: 13, md: 14 },
whiteSpace: 'nowrap',
overflow: 'visible',
textOverflow: 'clip',
maxWidth: 200,
}}
>
{user?.email || 'Admin@gmail.com'}
</Typography>
)}
</Box>
</Toolbar>
</AppBar>
);
};
export default KitchPlusAppBar;
+74
View File
@@ -0,0 +1,74 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import Orders from './contect/Orders';
import authService from '../../../services/authService';
const drawerWidth = 230;
const Cart = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
useEffect(() => {
const handleResize = () => {
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
useEffect(() => {
const admin = authService.getAdminData();
// console.log('Admin Info:', admin);
const adminId = authService.getAdminId();
// console.log('Admin ID:', adminId);
}, []);
const admin = authService.getAdminData();
const adminId = authService.getAdminId();
const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen);
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: '100%',
transition: theme.transitions.create(['width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
}}
>
<KitchPlusAppBar onDrawerToggle={handleDrawerToggle} sidebarOpen={sidebarOpen} isMobile={isMobile} />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 6,
width: { xs: '90%', sm: '95%', md: '96%' },
pt: { xs: 2, sm: 3 },
pl: { xs: 2, sm: 3 },
pb: { xs: 2, sm: 4 },
pr: { xs: 2, sm: 3 },
}}
>
<Orders adminId={adminId} />
</Box>
</Box>
</Box>
);
};
export default Cart;
@@ -0,0 +1,223 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Paper,
List,
ListItem,
ListItemText,
Button,
TextField,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
CircularProgress
} from '@mui/material';
import authService from '../../../../services/authService';
import { useSnackbar } from "../../../../contexts/SnackbarContext";
const CartDetails = ({ cart, onClose, onUpdated, onDeleted }) => {
const [editMode, setEditMode] = useState(false);
const [totalPrice, setTotalPrice] = useState(cart?.attributes?.total_price || "");
const [cartItems, setCartItems] = useState(cart.relationships?.cartItems || cart.relationships?.cart_items || []);
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const { showSnackbar } = useSnackbar();
if (!cart) return null;
const handleSaveAll = async () => {
setLoading(true);
const payload = {
data: {
type: "cart",
attributes: {
totalPrice: Number(totalPrice),
},
relationships: {
cartItems: cartItems.map(item => ({
attributes: {
quantity: Number(item.attributes.quantity),
},
relationships: {
product: {
data: {
id: item.relationships?.supplier_product?.id ||
item.relationships?.product?.data?.id
}
}
}
})),
},
},
};
try {
const result = await authService.updateCart(cart.id, payload);
if (result.success) {
onUpdated(result.data);
setEditMode(false);
} else {
// alert(result.message || 'Failed to update cart');
showSnackbar(result.message || "Failed to update cart", "error");
}
} catch (error) {
// alert('An error occurred while updating the cart');
showSnackbar("An error occurred while updating the cart", "error");
} finally {
setLoading(false);
}
};
const handleDeleteCart = async () => {
setDeleteLoading(true);
try {
const result = await authService.deleteCart(cart.id);
if (result.success) {
onDeleted(cart.id);
onClose();
} else {
// alert(result.message || 'Failed to delete cart');
showSnackbar(result.message || "Failed to update cart", "error");
}
} catch (error) {
// alert('An error occurred while deleting the cart');
showSnackbar("An error occurred while deleting the cart", "error");
} finally {
setDeleteLoading(false);
setOpenDeleteDialog(false);
}
};
const handleChangeQuantity = (itemId, newQuantity) => {
setCartItems(prev =>
prev.map(item =>
item.id === itemId
? { ...item, attributes: { ...item.attributes, quantity: newQuantity } }
: item
)
);
};
return (
<>
<Paper sx={{ p: 3, mt: 2, borderRadius: 2, border: '1px solid #e0e0e0' }}>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
Cart #{cart.id} Details
</Typography>
{editMode ? (
<>
<TextField
label="Total Price"
type="number"
value={totalPrice}
onChange={(e) => setTotalPrice(e.target.value)}
fullWidth
sx={{ mb: 2 }}
/>
<Typography sx={{ mt: 2, fontWeight: 500 }}>Items:</Typography>
<List>
{cartItems.map(item => (
<ListItem key={item.id} sx={{ pl: 0 }}>
<ListItemText
primary={`Item ID: ${item.id} | Product: ${item.relationships?.supplier_product?.id ||
item.relationships?.product?.data?.id
}`}
secondary={
<TextField
type="number"
size="small"
label="Quantity"
value={item.attributes.quantity}
onChange={(e) => handleChangeQuantity(item.id, Number(e.target.value))}
sx={{ width: '120px' }}
/>
}
/>
</ListItem>
))}
</List>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button variant="contained" onClick={handleSaveAll} disabled={loading}>
{loading ? "Saving..." : "Save All"}
</Button>
<Button variant="outlined" onClick={() => setEditMode(false)}>
Cancel
</Button>
</Box>
</>
) : (
<>
<Typography>Total Price: {cart.attributes.total_price || cart.attributes.totalPrice}</Typography>
<Typography>
Created At: {new Date(cart.attributes.createdAt).toLocaleString()}
</Typography>
<Typography sx={{ mt: 2, fontWeight: 500 }}>Items:</Typography>
<List>
{cartItems.map(item => (
<ListItem key={item.id} sx={{ pl: 0 }}>
<ListItemText
primary={`Item ID: ${item.id}`}
secondary={`Quantity: ${item.attributes.quantity} | Product: ${item.relationships?.supplier_product?.id ||
item.relationships?.product?.data?.id
}`}
/>
</ListItem>
))}
</List>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button variant="contained" onClick={() => setEditMode(true)}>
Edit
</Button>
<Button
variant="contained"
color="error"
onClick={() => setOpenDeleteDialog(true)}
sx={{ ml: 'auto' }}
>
Delete Cart
</Button>
<Button variant="outlined" onClick={onClose}>
Back to Orders
</Button>
</Box>
</>
)}
</Paper>
{/* Delete Confirmation Dialog */}
<Dialog
open={openDeleteDialog}
onClose={() => setOpenDeleteDialog(false)}
>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure you want to delete this cart? This action cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDeleteDialog(false)} disabled={deleteLoading}>
Cancel
</Button>
<Button
onClick={handleDeleteCart}
color="error"
variant="contained"
disabled={deleteLoading}
>
{deleteLoading ? <CircularProgress size={24} /> : 'Delete'}
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default CartDetails;
@@ -0,0 +1,170 @@
import React, { useContext, useState, useEffect } from "react";
import { Box, Typography, Button, LinearProgress, IconButton } from "@mui/material";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { CartContext } from "../../../../contexts/CartContextR";
import { useSnackbar } from "../../../../contexts/SnackbarContext";
const CartView = ({ onClose, onCartCreated, adminId }) => {
const { cart, clearCart, createNewCart } = useContext(CartContext);
const [loading, setLoading] = useState(false);
const { showSnackbar } = useSnackbar();
useEffect(() => {
console.log('Admin ID from props in:CartView', adminId);
}, [adminId]);
const totalPrice = cart.reduce((sum, item) => sum + (item.totalPrice || 0), 0);
const handleSendCart = async () => {
if (!cart.length) return;
setLoading(true);
try {
// تحقق من أن adminId موجود ضمن قائمة صالحة (يمكنك تعديلها حسب بياناتك)
const validAdminIds = [1, 2]; // IDs موجودة في DB
if (!validAdminIds.includes(adminId)) {
console.error("Invalid admin ID");
showSnackbar("Selected admin is not valid.", "error");
setLoading(false);
return;
}
// تحقق من أن جميع المنتجات موجودة في قاعدة البيانات
const validProductIds = [1, 2, 3, 4]; // IDs المنتجات الموجودة
for (let item of cart) {
if (!validProductIds.includes(item.id)) {
console.error(`Invalid product ID: ${item.id}`);
// alert(`Product with ID ${item.id} does not exist.`);
showSnackbar(`Product with ID ${item.id} does not exist.`, "error");
setLoading(false);
return;
}
}
// تجهيز البيانات حسب شكل الـ backend
const cartData = {
data: {
type: "cart",
attributes: {
totalPrice: cart.reduce((sum, item) => sum + (item.totalPrice || 0), 0),
},
relationships: {
admin: {
data: { id: adminId },
},
cartItems: cart.map(item => ({
attributes: { quantity: item.quantity },
relationships: { product: { data: { id: item.id } } },
})),
},
},
};
const newCart = await createNewCart(cartData);
if (newCart && newCart.success) {
onCartCreated(newCart.data);
clearCart();
onClose();
showSnackbar("Cart sent successfully!", "success");
} else {
console.error("Failed to create cart:", newCart.message);
}
} catch (error) {
// console.error("Error sending cart:", error);
showSnackbar("Failed to create cart.", "error");
} finally {
setLoading(false);
}
};
if (loading) return <LinearProgress />;
return (
<Box
sx={{
width: { xs: "100%", sm: "93.5%" },
p: { xs: 2, sm: 3 },
backgroundColor: "white",
borderRadius: 2,
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
{/* السهم للعودة */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<IconButton onClick={onClose} size="small" sx={{ mr: 1 }}>
<ArrowBackIcon fontSize="small" />
</IconButton>
<Typography variant="h5" sx={{ fontWeight: 600, fontSize: { xs: "18px", sm: "20px" } }}>
Current Cart
</Typography>
</Box>
{cart.length === 0 ? (
<Typography sx={{ mt: 2, textAlign: "center" }}>No items in cart.</Typography>
) : (
<>
{cart.map(item => (
<Box
key={item.id}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
p: 2,
borderRadius: 1,
border: "1px solid #e0e0e0",
backgroundColor: "#fafafa",
flexWrap: "wrap",
gap: 1,
}}
>
<Box>
<Typography fontWeight={600}>{item.name}</Typography>
<Typography variant="body2">Unit: {item.unit}</Typography>
</Box>
<Box sx={{ textAlign: { xs: "left", sm: "right" } }}>
<Typography variant="body2">Quantity: {item.quantity}</Typography>
<Typography variant="body2">Total: ${item.totalPrice}</Typography>
</Box>
</Box>
))}
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Total Price: ${totalPrice.toFixed(2)}
</Typography>
</Box>
</>
)}
{cart.length > 0 && (
<Box sx={{ display: "flex", gap: 1, mt: 2, flexWrap: { xs: "wrap", sm: "nowrap" }, justifyContent: { xs: "center", sm: "flex-start" } }}>
<Button
variant="contained"
color="primary"
onClick={handleSendCart}
sx={{ color: 'white', borderRadius: '8px', fontWeight: 600, fontSize: '14px', height: '40px', width: { xs: '100%', sm: '200px' }, textTransform: 'none', minWidth: { xs: 'unset', sm: '200px' } }}
>
Send Cart to Server
</Button>
<Button
variant="outlined"
onClick={clearCart}
sx={{ borderRadius: '8px', fontWeight: 600, fontSize: '14px', height: '40px', width: { xs: '100%', sm: '150px' }, textTransform: 'none', minWidth: { xs: 'unset', sm: '150px' } }}
>
Clear Cart
</Button>
</Box>
)}
</Box>
);
};
export default CartView;
+389
View File
@@ -0,0 +1,389 @@
import React, { useState, useEffect } from 'react';
import {
useMediaQuery,
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Skeleton,
CircularProgress,
Button
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import authService from '../../../../services/authService';
import { useRestaurant } from "../../../../contexts/RestaurantContext";
import CartDetails from './CartDetails';
import CartView from './CartView';
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
const theme = useTheme();
const handlePrev = () => { if (currentPage > 1) onChange(currentPage - 1); };
const handleNext = () => { if (currentPage < pageCount) onChange(currentPage + 1); };
const { restaurantId } = useRestaurant();
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
onClick={handlePrev}
disabled={currentPage <= 1}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
backgroundColor: theme.palette.primary.main,
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
fontSize: 14,
userSelect: 'none',
boxShadow: `0 0 0 1px ${theme.palette.primary.main}`,
}}
>
{currentPage}
</Box>
<IconButton
size="small"
onClick={handleNext}
disabled={currentPage >= pageCount}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
</Box>
);
};
const Orders = ({ adminId }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { restaurantId } = useRestaurant();
const [ordersData, setOrdersData] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedCart, setSelectedCart] = useState(null);
const [cartLoading, setCartLoading] = useState(false);
const [showCartView, setShowCartView] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 6;
// ────────────── Fetch Orders ──────────────
useEffect(() => {
const fetchOrders = async () => {
setLoading(true);
try {
const result = await authService.getCart();
if (result.success) {
console.log(Array.isArray(result.data) ? result.data : []);
setOrdersData(Array.isArray(result.data) ? result.data : []);
} else {
console.error(result.message);
setOrdersData([]);
}
} catch (error) {
console.error('Failed to fetch orders:', error);
setOrdersData([]);
}
setLoading(false);
};
fetchOrders();
}, []);
// ────────────── Row Click ──────────────
const handleRowClick = async (cartId) => {
setCartLoading(true);
try {
const result = await authService.getCartById(cartId);
if (result.success) setSelectedCart(result.data);
else console.error(result.message);
} catch (error) {
console.error('Failed to fetch cart details:', error);
}
setCartLoading(false);
};
const handleCartUpdated = (updatedCart) => {
setOrdersData(prev =>
prev.map(cart => cart.id === updatedCart.id ? updatedCart : cart)
);
setSelectedCart(null);
};
const handleCloseCartDetails = () => { setSelectedCart(null); };
const handleAddOrder = (newOrder) => {
setOrdersData(prev => [newOrder, ...prev]);
};
const pageCount = Math.ceil(ordersData.length / itemsPerPage);
const paginatedOrders = ordersData.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
if (cartLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
);
}
if (selectedCart) {
return (
<CartDetails
cart={selectedCart}
onClose={handleCloseCartDetails}
onUpdated={handleCartUpdated}
/>
);
}
if (showCartView) {
return (
<CartView
onClose={() => setShowCartView(false)}
onSend={handleAddOrder}
onCartCreated={(newCart) => {
setOrdersData(prev => [newCart, ...prev]);
setShowCartView(false);
}}
adminId={adminId}
/>
);
}
// ────────────── Main Orders Table ──────────────
return (
<Box
sx={{
width: { xs: '100%', sm: '93.5%' },
p: { xs: 2, sm: 3 },
backgroundColor: 'white',
borderRadius: 2,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' },
mb: 2,
gap: { xs: 1, sm: 0 },
}}
>
<Typography
variant="h5"
sx={{
fontWeight: 600,
fontSize: { xs: '18px', sm: '20px' },
mb: { xs: 1, sm: 0 },
textAlign: { xs: 'center', sm: 'left' },
}}
>
Cart
</Typography>
<Button
onClick={() => setShowCartView(true)}
sx={{
color: 'white',
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
width: { xs: '100%', sm: '135px' },
textTransform: 'none',
minWidth: { xs: 'unset', sm: '135px' },
}}
variant="contained"
size="small"
>
Current Cart
</Button>
</Box>
<TableContainer
component={Paper}
sx={{
boxShadow: 'none',
border: '1px solid #e0e0e0',
borderRadius: 2,
minWidth: 320,
height: '380px',
}}
>
<Table
sx={{ minWidth: { sm: 550, md: 650 }, tableLayout: 'auto' }}
aria-label="orders table"
size={isMobile ? 'small' : 'medium'}
>
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Cart ID</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Total Price</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Created At</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Items Count</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading
? Array.from({ length: itemsPerPage }).map((_, idx) => (
<TableRow key={`skeleton-${idx}`}>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
</TableRow>
))
: (
<>
{paginatedOrders.map(order => {
const attributes = order?.attributes || {};
const relationships = order?.relationships || {};
const cartItems = relationships?.cartItems || relationships?.cart_items || [];
return (
<TableRow
key={order?.id}
hover
sx={{
cursor: 'pointer',
transition: 'background-color 0.3s',
'&:hover': {
backgroundColor: '#ffe4d0ff !important',
'& td': { color: '#000000ff' },
},
}}
onClick={() => handleRowClick(order.id)}
>
<TableCell>{order?.id || '--'}</TableCell>
<TableCell>{attributes?.totalPrice ?? attributes?.total_price ?? '0.00'}</TableCell>
<TableCell>{attributes?.createdAt ? new Date(attributes.createdAt).toLocaleString() : '--'}</TableCell>
<TableCell>{cartItems.length}</TableCell>
<TableCell>
<Button
variant="contained"
color="primary"
size="small"
onClick={ async (e) => {
e.stopPropagation();
const cart = order;
if (!cart) return;
const orderData = {
data: {
type: "order",
attributes: {
totalPrice: parseFloat(cart.attributes.total_price) || 0,
},
relationships: {
restaurant: {
data: {
id: restaurantId,
},
},
orderItems: (cart.relationships.cart_items || []).map((item) => ({
attributes: {
quantity: item.attributes.quantity,
},
relationships: {
product: {
data: {
id: item.relationships.supplier_product.id,
},
},
},
})),
},
},
};
authService.confirmOrder(orderData);
const result =await authService.confirmOrder(orderData);
if (result.success) {
setOrdersData(prev => prev.filter(c => c.id !== cart.id));
} else {
console.error(result.message);
}
}}>
Confirm
</Button>
</TableCell>
</TableRow>
);
})}
{paginatedOrders.length < itemsPerPage &&
Array.from({ length: itemsPerPage - paginatedOrders.length }).map((_, idx) => (
<TableRow key={`empty-${idx}`} sx={{ height: 53 }}>
<TableCell colSpan={4} sx={{ borderBottom: 'none' }} />
</TableRow>
))
}
</>
)}
</TableBody>
</Table>
</TableContainer>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
pt={{ xs: 1, sm: 2 }}
sx={{
position: 'sticky',
bottom: 0,
backgroundColor: theme.palette.background.paper,
borderTop: '1px solid #f0f0f0',
}}
>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '11px', sm: '14px' }, whiteSpace: 'nowrap' }}
>
Showing {(currentPage - 1) * itemsPerPage + 1} - {Math.min(currentPage * itemsPerPage, ordersData.length)} of {ordersData.length}
</Typography>
<SimplePagination currentPage={currentPage} pageCount={pageCount} onChange={setCurrentPage} />
</Box>
</Box>
);
};
export default Orders;
+103
View File
@@ -0,0 +1,103 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery, Typography } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
const drawerWidth = 230;
const Cashier = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [hasProducts, setHasProducts] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
useEffect(() => {
const checkProducts = async () => {
const productsExist = await checkIfProductsExist();
setHasProducts(productsExist);
};
checkProducts();
}, []);
const checkIfProductsExist = async () => {
return false;
};
useEffect(() => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
}, [theme.breakpoints.values.md]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
const handleDrawerToggle = () => {
setSidebarOpen(!sidebarOpen);
};
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: {
xs: '100%',
sm: '100%',
md: '100%'
},
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}
/>
<Typography>Cashier</Typography>
</Box>
</Box>
);
};
export default Cashier;
@@ -0,0 +1,136 @@
import React from 'react';
import {
AppBar,
Toolbar,
Box,
ListItemIcon,
ListItemText,
Typography,
useTheme,
useMediaQuery,
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import LogoutIcon from '@mui/icons-material/Logout';
import authService from '../../../services/authService';
const KitchPlusAppBar = ({ onDrawerToggle, sidebarOpen, isMobile }) => {
const theme = useTheme();
const navigate = useNavigate();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleLogout = async () => {
try {
const result = await authService.logout();
if (result.success) {
localStorage.removeItem('token');
navigate('/login');
} else {
console.error('Logout failed:', result.message);
}
} catch (error) {
console.error('Logout error:', error);
}
};
return (
<AppBar
sx={{
height: { xs: 56, sm: 64, md: 66 },
backgroundColor: '#ffffff',
color: 'black',
boxShadow: 'none',
borderBottom: '1px solid #e0e0e0',
position: 'sticky',
top: 0,
zIndex: theme.zIndex.appBar,
}}
>
<Toolbar
sx={{
px: { xs: 2, sm: 3, md: '24px' },
minHeight: { xs: '56px !important', sm: '64px !important' },
display: 'flex',
justifyContent: 'space-between',
}}
>
{/* Left: Logo */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
component="img"
src="/image.png"
alt="logo"
sx={{
width: 40,
height: 40,
objectFit: 'contain',
mr: 1.5,
}}
/>
<Typography
variant="h6"
sx={{
fontWeight: 400,
fontSize: '1.25rem',
color: 'text.primary',
}}
>
KITCH
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 400,
fontSize: '1.25rem',
color: 'primary.main',
ml: 0.5,
}}
>
PLUS
</Typography>
</Box>
{/* Right: Log Out Button */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.8, sm: 1, md: 1.5 } }}>
<Box
onClick={handleLogout}
sx={{
display: 'flex',
alignItems: 'center',
borderRadius: '5px',
px: 2,
py: 1,
cursor: 'pointer',
transition: 'background-color 0.2s',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: '#fffcf9d5',
},
}}
>
<ListItemIcon
sx={{
color: 'divider',
minWidth: 36,
}}
>
<LogoutIcon />
</ListItemIcon>
<ListItemText
primary="Log Out"
primaryTypographyProps={{
sx: {
color: 'divider',
fontSize: '1rem',
},
}}
/>
</Box>
</Box>
</Toolbar>
</AppBar>
);
};
export default KitchPlusAppBar;
@@ -0,0 +1,228 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery, Snackbar, Alert } from '@mui/material';
import AppBar from './AppBar';
import CloudKitchenProject from '../CreateYourRestaurant/contcet/CloudKitchenProject';
import OperationalDetails from '../CreateYourRestaurant/contcet/OperationalDetails';
import RequiredEquipments from '../CreateYourRestaurant/contcet/RequiredEquipments';
import VisualIdentity from '../CreateYourRestaurant/contcet/VisualIdentity';
import Budget from '../CreateYourRestaurant/contcet/Budget';
import AdditionalNotes from '../CreateYourRestaurant/contcet/AdditionalNotes';
import SideProfile from './SideProfile';
import authService from '../../../services/authService';
const { createNewRestaurant } = authService;
const drawerWidth = 230;
const CreateRestaurant = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState({});
const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' });
const updateFormData = (newData) => {
setFormData(prev => {
const updated = { ...prev, ...newData };
console.log('Updated formData:', updated);
return updated;
});
};
const handleCloseSnackbar = (event, reason) => {
if (reason === 'clickaway') return;
setSnackbar(prev => ({ ...prev, open: false }));
};
const handleSubmit = async () => {
const result = await createNewRestaurant(formData);
if (result.success) {
setSnackbar({ open: true, message: 'Restaurant created successfully!', severity: 'success' });
} else {
setSnackbar({ open: true, message: `Error: ${result.message}`, severity: 'error' });
}
console.log("Form Data being submitted:", formData);
};
const steps = [
<CloudKitchenProject
key="step-0"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<OperationalDetails
key="step-1"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<RequiredEquipments
key="step-2"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<VisualIdentity
key="step-3"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<Budget
key="step-4"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<AdditionalNotes
key="step-5"
formData={formData}
updateFormData={updateFormData}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
onSubmit={handleSubmit}
/>
];
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
const handleDrawerToggle = () => {
setSidebarOpen(!sidebarOpen);
};
return (
<Box
sx={{
display: 'flex',
height: '100vh',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}
>
<Box
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
width: { xs: '100%', sm: '100%', md: '100%' },
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,
}),
}}
>
<AppBar
onDrawerToggle={handleDrawerToggle}
sidebarOpen={sidebarOpen}
isMobile={isMobile}
/>
<Box>
<Box
sx={{
display: 'flex',
height: '100vh',
}}
>
{/* Sidebar profile for desktop */}
<Box
sx={{
height: '100vh',
ml: 3,
mb: 2,
width: { md: '30%' },
display: { xs: 'none', sm: 'none', md: 'block' },
overflowY: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}
>
<Box
sx={{
minHeight: '100%',
pb: 15,
pt: 3,
}}
>
<SideProfile
currentStepIndex={currentStep}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>
</Box>
</Box>
{/* Step content */}
<Box
sx={{
ml: { xs: 2, md: 3 },
flexGrow: 1,
height: '100vh',
pr: { sm: 2, md: 1 },
pt: 3,
mb: { sm: 20 },
width: { md: '60%' },
display: { xs: 'block', sm: 'block', md: 'block' },
overflowY: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}
>
<Box sx={{ minHeight: '100%', pb: 18 }}>
{steps[currentStep]}
</Box>
</Box>
</Box>
</Box>
</Box>
<Snackbar
open={snackbar.open}
autoHideDuration={4000}
onClose={handleCloseSnackbar}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert onClose={handleCloseSnackbar} severity={snackbar.severity}
sx={{
width: { xs: '40%', sm: '60%', md: '100%' },
color: 'white',
fontSize: '16px',
fontWeight: '500',
backgroundColor: '#e57f3f',
borderRadius: 6, mb: 6
}}
>
{snackbar.message}
</Alert>
</Snackbar>
</Box>
);
};
export default CreateRestaurant;
@@ -0,0 +1,160 @@
import React from 'react';
import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
const steps = [
{ title: 'Cloud Kitchen Project', icon: '/images/createProfile/BasicInf.png' },
{ title: 'OperationalDetails', icon: '/images/icons/rocket.png' },
{ title: 'Required Equipments', icon: '/images/createProfile/equipment.png'},
{ title: 'Visual Identity', icon: '/images/createProfile/Vector.png' },
{ title: 'Budget & Expansion', icon: '/images/createProfile/Expansion.png' },
{ title: 'Additional Notes & Concerns', icon: '/images/createProfile/Group.png' },
{ title: 'Submit & Confirmation', icon: '/images/createProfile/Confirmation.png' },
];
const SideProfile = ({ currentStepIndex = 0, onBack }) => {
const theme = useTheme();
return (
<Box
sx={{
height: '100%',
backgroundColor: '#FFFFFF',
px: 3,
py: 4,
display: { xs: 'none', md: 'block' },
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
}}
>
{/* العنوان الرئيسي */}
<Typography fontSize="18px" fontWeight={600} mb={1.2}>
Create Your Restaurant
</Typography>
<Typography variant="body2" color="text.secondary" mb={3}>
Complete this process to register your restaurant on our amazing food platform.
</Typography>
{/* الخطوات */}
<Stack spacing={3} sx={{ ml: 1, position: 'relative', pb: 12 }}>
{steps.map((step, index) => (
<Box key={index} position="relative">
{/* الخط الرأسي بين الدوائر */}
{index !== steps.length - 1 && (
<Box
sx={{
position: 'absolute',
top: '28px',
left: '23px',
width: '2px',
height: 'calc(100% - 5px)',
backgroundColor: '#E0E0E0',
zIndex: 0,
}}
/>
)}
{/* محتوى كل خطوة */}
<Box display="flex" alignItems="center" gap={2}>
{/* الدائرة بالأيقونة */}
<Box
sx={{
position: 'relative',
width: 45,
height: 45,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
{/* الدائرة الخلفية - تظهر دائمًا */}
<Box
sx={{
position: 'absolute',
backgroundImage:
index <= currentStepIndex
? 'linear-gradient(0deg, #FF914D, #F3F3F3)'
: 'linear-gradient(0deg, #DFDFDF, #F0F0F0)',
width: 48,
height: 48,
borderRadius: '50%',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 0,
}}
/>
<Box
sx={{
width: 40,
height: 40,
borderRadius: '50%',
backgroundColor: '#FFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
<img
src={step.icon}
alt={step.title}
style={{
width: 20,
height: 20,
marginLeft: step.title === 'Required Equipments' ? 7 : 0,
objectFit: 'contain',
}}
/>
</Box>
</Box>
{/* العنوان */}
<Typography
sx={{
fontSize: '16px',
fontWeight: 600,
color: '#000',
}}
>
{step.title}
</Typography>
</Box>
</Box>
))}
</Stack>
{/* زر الرجوع */}
<Box
sx={{
position: 'absolute',
bottom: 16,
right: 24,
}}
>
<Button
variant="text"
startIcon={<ArrowBackIosIcon sx={{ fontSize: 20 }} />}
onClick={onBack}
disabled={currentStepIndex === 0}
sx={{
textTransform: 'none',
fontSize: '20px',
color: currentStepIndex === 0 ? '#C2C2C2' : '#6B7283',
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Box>
);
};
export default SideProfile;
@@ -0,0 +1,257 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery, Snackbar, Alert } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import CloudKitchenProject from './contcet/CloudKitchenProject';
import OperationalDetails from './contcet/OperationalDetails';
import RequiredEquipments from './contcet/RequiredEquipments';
import VisualIdentity from './contcet/VisualIdentity';
import Budget from './contcet/Budget';
import AdditionalSupport from './contcet/AdditionalSupport';
import AdditionalNotes from './contcet/AdditionalNotes';
import SideProfile from './SideProfile';
import authService from '../../../services/authService';
const { createNewRestaurant } = authService;
const drawerWidth = 230;
const CreateRestaurant = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [hasProducts, setHasProducts] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
// إدارة بيانات النموذج لجميع الخطوات (مشاركة البيانات بين الخطوات)
const [formData, setFormData] = useState({
});
// دالة لتحديث بيانات النموذج بشكل مرن (دمج الجديد مع القديم)
const updateFormData = (newData) => {
setFormData(prev => ({
...prev,
...newData,
}));
};
// دالة لإغلاق السناك بار
const handleSnackbarClose = (event, reason) => {
if (reason === 'clickaway') return;
setSnackbarOpen(false);
};
const handleSubmit = async () => {
try {
await createNewRestaurant(formData);
setSnackbarOpen(true);
} catch (error) {
console.error(error);
}
};
const steps = [
<CloudKitchenProject
key="step-0"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<OperationalDetails
key="step-1"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<RequiredEquipments
key="step-2"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<VisualIdentity
key="step-3"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<Budget
key="step-4"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>,
<AdditionalNotes
key="step-5"
formData={formData}
updateFormData={updateFormData}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
onSubmit={handleSubmit}
/>
];
// useEffect(() => {
// const checkProducts = async () => {
// const productsExist = await checkIfProductsExist();
// setHasProducts(productsExist);
// };
// checkProducts();
// }, []);
// const checkIfProductsExist = async () => {
// return false;
// };
useEffect(() => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
}, [theme.breakpoints.values.md]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
const handleDrawerToggle = () => {
setSidebarOpen(!sidebarOpen);
};
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: { xs: '100%', sm: '100%', md: '100%' },
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>
<Box
sx={{
display: 'flex',
height: '100vh',
}}
>
<Box
sx={{
height: '100vh',
ml: 3,
mb: 2,
width: { md: '30%' },
display: { xs: 'none', sm: 'none', md: 'block' },
overflowY: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}
>
<Box
sx={{
minHeight: '100%',
pb: 15,
pt: 3,
}}
>
<SideProfile
currentStepIndex={currentStep}
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
/>
</Box>
</Box>
<Box
sx={{
ml: { xs: 2, md: 3 },
flexGrow: 1,
height: '100vh',
pr: { sm: 2, md: 1 },
pt: 3,
mb: { sm: 20 },
width: { md: '60%' },
display: { xs: 'block', sm: 'block', md: 'block' },
overflowY: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}
>
<Box
sx={{
minHeight: '100%',
pb: 18,
}}
>
{steps[currentStep]}
</Box>
</Box>
</Box>
</Box>
</Box>
<Snackbar
open={snackbarOpen}
autoHideDuration={4000}
onClose={handleSnackbarClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert onClose={handleSnackbarClose} severity="success" sx={{ width: '100%' }}>
عملية إنشاء المطعم تمت بنجاح
</Alert>
</Snackbar>
</Box>
);
};
export default CreateRestaurant;
@@ -0,0 +1,161 @@
import React from 'react';
import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
const steps = [
{ title: 'Cloud Kitchen Project', icon: '/images/createProfile/BasicInf.png' },
{ title: 'OperationalDetails', icon: '/images/icons/rocket.png' },
{ title: 'Required Equipments', icon: '/images/createProfile/equipment.png'},
{ title: 'Visual Identity', icon: '/images/createProfile/Vector.png' },
{ title: 'Budget & Expansion', icon: '/images/createProfile/Expansion.png' },
{ title: 'Additional Notes & Concerns', icon: '/images/createProfile/Group.png' },
{ title: 'Submit & Confirmation', icon: '/images/createProfile/Confirmation.png' },
];
const SideProfile = ({ currentStepIndex = 0, onBack }) => {
const theme = useTheme();
return (
<Box
sx={{
height: '100%',
backgroundColor: '#FFFFFF',
px: 3,
py: 4,
display: { xs: 'none', md: 'block' },
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
}}
>
{/* العنوان الرئيسي */}
<Typography fontSize="18px" fontWeight={600} mb={1.2}>
Create Your Restaurant
</Typography>
<Typography variant="body2" color="text.secondary" mb={3}>
Complete this process to register your restaurant on our amazing food platform.
</Typography>
{/* الخطوات */}
<Stack spacing={3} sx={{ ml: 1, position: 'relative', pb: 12 }}>
{steps.map((step, index) => (
<Box key={index} position="relative">
{/* الخط الرأسي بين الدوائر */}
{index !== steps.length - 1 && (
<Box
sx={{
position: 'absolute',
top: '28px',
left: '23px',
width: '2px',
height: 'calc(100% - 5px)',
backgroundColor: '#E0E0E0',
zIndex: 0,
}}
/>
)}
{/* محتوى كل خطوة */}
<Box display="flex" alignItems="center" gap={2}>
{/* الدائرة بالأيقونة */}
<Box
sx={{
position: 'relative',
width: 45,
height: 45,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
{/* الدائرة الخلفية - تظهر دائمًا */}
<Box
sx={{
position: 'absolute',
backgroundImage:
index <= currentStepIndex
? 'linear-gradient(0deg, #FF914D, #F3F3F3)'
: 'linear-gradient(0deg, #DFDFDF, #F0F0F0)',
width: 48,
height: 48,
borderRadius: '50%',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 0,
}}
/>
{/* الدائرة الأمامية بالأيقونة */}
<Box
sx={{
width: 40,
height: 40,
borderRadius: '50%',
backgroundColor: '#FFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
<img
src={step.icon}
alt={step.title}
style={{
width: 20,
height: 20,
marginLeft: step.title === 'Required Equipments' ? 7 : 0,
objectFit: 'contain',
}}
/>
</Box>
</Box>
{/* العنوان */}
<Typography
sx={{
fontSize: '16px',
fontWeight: 600,
color: '#000',
}}
>
{step.title}
</Typography>
</Box>
</Box>
))}
</Stack>
{/* زر الرجوع */}
<Box
sx={{
position: 'absolute',
bottom: 16,
right: 24,
}}
>
<Button
variant="text"
startIcon={<ArrowBackIosIcon sx={{ fontSize: 20 }} />}
onClick={onBack}
disabled={currentStepIndex === 0}
sx={{
textTransform: 'none',
fontSize: '20px',
color: currentStepIndex === 0 ? '#C2C2C2' : '#6B7283',
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Box>
);
};
export default SideProfile;
@@ -0,0 +1,171 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Stack,
Button,
useTheme,
TextField
} from '@mui/material';
import ConfirmationDialog from './ConfirmationDialog';
import { useLocation, useNavigate } from 'react-router-dom';
const AdditionalNotes = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData, onSubmit }) => {
const theme = useTheme();
const [openModal, setOpenModal] = useState(false);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const handleOpenModal = () => setOpenModal(true);
const handleCloseModal = () => setOpenModal(false);
const handleNotesChange = (e) => {
updateFormData({ need_help: e.target.value });
};
const handleConfirmSubmit = async () => {
setLoading(true);
try {
if (onSubmit) {
await onSubmit();
}
setOpenModal(false);
// التوجيه فقط إذا كانت الصفحة هي /create-restaurant
if (location.pathname === '/create-restaurant') {
navigate('/restaurant');
}
// إذا كانت من رابط آخر مثل /create-kitchen لا نفعل شيئًا
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 11 },
pb: 10,
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={2.5}>
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
Additional Notes
</Typography>
<Box sx={{ width: '70%' }}>
<Typography fontSize="16px" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
Enter your basic information to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px' }}>
Notes / Concerns
</Typography>
<TextField
placeholder="Write for us"
variant="outlined"
fullWidth
multiline
minRows={7}
value={formData.need_help || ''}
onChange={handleNotesChange}
sx={{
'& .MuiInputBase-root': {
fontWeight: 500,
fontSize: '15px',
alignItems: 'start',
},
'& textarea::placeholder': {
color: '#969BA7',
},
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
}
}}
/>
</Box>
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleOpenModal}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover
}
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
}
}}
>
Back
</Button>
</Box>
</Stack>
<ConfirmationDialog
open={openModal}
onClose={handleCloseModal}
onConfirm={handleConfirmSubmit}
loading={loading}
title="Confirm Submission"
description="Are you sure you want to proceed to the next step?"
/>
</Box>
);
};
export default AdditionalNotes;
@@ -0,0 +1,175 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Stack,
Button,
useTheme,
FormGroup,
FormControlLabel,
Checkbox,
} from '@mui/material';
const AdditionalSupport = ({ currentStepIndex = 0, onNext, onBack }) => {
const theme = useTheme();
const [selectedOptions, setSelectedOptions] = useState([]);
const handleCheckboxChange = (option) => {
setSelectedOptions((prev) =>
prev.includes(option)
? prev.filter((o) => o !== option)
: [...prev, option]
);
};
const options = ['Supplier Assistance', 'Staff Training'];
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 12 },
pb: 4,
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={2.5}>
<Typography
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem',
},
}}
>
Additional Support
</Typography>
<Box sx={{ width: '70%' }}>
<Typography
fontSize="16px"
color="text.secondary"
fontWeight={500}
sx={{ pb: 1 }}
>
Enter your basic information to proceed to registration of your own
restaurant on this platform
</Typography>
</Box>
{/* Checkboxes for Additional Support Options */}
<Box sx={{ mt: 2 }}>
<Typography
variant="body2"
color="black"
sx={{ fontWeight: '500', fontSize: '16px', mb: 1 }}
>
Select Additional Support Options
</Typography>
<FormGroup
row
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1.5,
}}
>
{options.map((option) => {
const isChecked = selectedOptions.includes(option);
return (
<FormControlLabel
key={option}
control={
<Checkbox
checked={isChecked}
onChange={() => handleCheckboxChange(option)}
sx={{
transform: 'scale(1.5)',
color: '#F0EDED',
'&.Mui-checked': {
color: '#FF914D',
},
}}
/>
}
label={
<Typography
sx={{
color: isChecked ? '#e57f3f' : '#969BA7',
fontWeight: 500,
fontSize: '16px',
}}
>
{option}
</Typography>
}
sx={{
m: 0,
width: { xs: '100%', sm: '48%', md: '40%' },
}}
/>
);
})}
</FormGroup>
</Box>
{/* Buttons */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={onNext}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
},
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default AdditionalSupport;
@@ -0,0 +1,150 @@
import React from 'react';
import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material';
import { useNavigate } from 'react-router-dom';
const Budget = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => {
const theme = useTheme();
const navigate = useNavigate();
const handleNext = () => {
if (onNext) onNext();
else navigate('/dashboard');
};
const handleInputChange = (field) => (e) => {
updateFormData({ [field]: e.target.value });
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 12 },
pb: { xs: 20, sm: 12, md: 3 },
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
width: { xs: '85%', sm: '90%' },
position: 'relative',
}}
>
<Stack spacing={2.5}>
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
Budget & Expansion
</Typography>
<Box sx={{ width: '70%' }}>
<Typography
fontSize="16px"
color="text.secondary"
fontWeight={500}
sx={{ pb: 1 }}
>
Enter your basic information to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
{/* Estimated Budget Input */}
<InputField
label="Estimated Budget"
placeholder="$4200"
theme={theme}
value={formData.estimated_budget || ''}
onChange={handleInputChange('estimated_budget')}
/>
{/* Expansion Plans Through Cloud Kitchen (No of Branches) Input */}
<InputField
label="Expansion Plans Through Cloud Kitchen (No of Branches)"
placeholder="200"
theme={theme}
value={formData.expansion_branches || ''}
onChange={handleInputChange('expansion_branches')}
/>
{/* Next Button */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleNext}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
},
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
const InputField = ({ label, placeholder, theme, value, onChange }) => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px', color: 'black' }}>
{label}
</Typography>
<TextField
placeholder={placeholder}
variant="outlined"
fullWidth
value={value}
onChange={onChange}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255,145,77,0.1)',
}
}}
/>
</Box>
);
export default Budget;
@@ -0,0 +1,438 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Stack,
Button,
useTheme,
TextField,
Radio,
RadioGroup,
FormControlLabel,
MenuItem,
FormControl,
FormHelperText
} from '@mui/material';
import authService from '../../../../services/authService'; // تأكد من المسار الصحيح
const CloudKitchenProject = ({
currentStepIndex = 0,
onNext,
formData,
updateFormData,
}) => {
const theme = useTheme();
const [cuisineTypes, setCuisineTypes] = useState([]);
const [errors, setErrors] = useState({});
//cuisineTypes
useEffect(() => {
const fetchCuisineTypes = async () => {
const data = await authService.cuisineTypes();
setCuisineTypes(data);
};
fetchCuisineTypes();
}, []);
useEffect(() => {
if (formData.menu_status === undefined) {
updateFormData({ menu_status: false });
}
if (formData.is_existing_brand === undefined) {
updateFormData({ is_existing_brand: false });
}
}, []);
const handleChange = (e) => {
const { name, value } = e.target;
if (name === 'is_existing_brand' || name === 'menu_status') {
updateFormData({ [name]: value === 'yes' });
} else {
updateFormData({ [name]: value });
}
};
const validate = () => {
let tempErrors = {};
if (!formData.name || formData.name.trim() === '') {
tempErrors.name = 'Restaurant Name is required';
}
if (!formData.cuisine_type_id) {
tempErrors.cuisine_type_id = 'Cuisine Type is required';
}
if (typeof formData.is_existing_brand !== 'boolean') {
tempErrors.is_existing_brand = 'Existing Brand selection is required';
}
if (!formData.location || formData.location.trim() === '') {
tempErrors.location = 'Location is required';
}
if (typeof formData.menu_status !== 'boolean') {
tempErrors.menu_status = 'Menu Status selection is required';
}
setErrors(tempErrors);
return Object.keys(tempErrors).length === 0;
};
const handleNext = () => {
if (validate()) {
onNext();
}
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: '92.5%' },
backgroundColor: '#FFFFFF',
px: 4,
pt: 5,
pb: 11,
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={2.5}>
<Typography
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem'
}
}}
>
Cloud Kitchen Project
</Typography>
<Box sx={{ width: '80%' }}>
<Typography
fontSize="16px"
color="text.secondary"
fontWeight={500}
sx={{ pb: 1 }}
>
Enter your basic information to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
{/* Restaurant Name */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Restaurant Name
</Typography>
<TextField
name="name"
placeholder="Al-Baik Foods"
variant="outlined"
fullWidth
value={formData.name || ''}
onChange={handleChange}
error={!!errors.name}
helperText={errors.name}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
{/* Cuisine Type */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Cuisine Type
</Typography>
<TextField
select
name="cuisine_type_id"
placeholder="Select Cuisine Type"
variant="outlined"
fullWidth
value={formData.cuisine_type_id || ''}
onChange={handleChange}
error={!!errors.cuisine_type_id}
helperText={errors.cuisine_type_id}
sx={{
'& .MuiSelect-select': { fontWeight: 500, fontSize: '15px' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
>
{cuisineTypes.length > 0 ? (
cuisineTypes.map((type) => (
<MenuItem key={type.id} value={type.id}>
{type.name}
</MenuItem>
))
) : (
<MenuItem disabled>No Cuisine Types Found</MenuItem>
)}
</TextField>
</Box>
{/* Existing Brand */}
<Box>
<Typography
variant="body2"
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
>
Existing Brand
</Typography>
<FormControl error={!!errors.is_existing_brand}>
<RadioGroup
row
name="is_existing_brand"
value={formData.is_existing_brand ? 'yes' : 'no'}
onChange={handleChange}
>
<FormControlLabel
value="yes"
control={
<Radio
sx={{
color: 'rgba(150, 155, 167, 0.6)',
transform: 'scale(0.85)',
'&.Mui-checked': {
color: theme.palette.primary.main
}
}}
/>
}
label="Yes"
/>
<FormControlLabel
value="no"
control={
<Radio
sx={{
color: 'rgba(150, 155, 167, 0.6)',
transform: 'scale(0.85)',
'&.Mui-checked': {
color: theme.palette.primary.main
}
}}
/>
}
label="No"
/>
</RadioGroup>
{errors.is_existing_brand && (
<FormHelperText>{errors.is_existing_brand}</FormHelperText>
)}
</FormControl>
</Box>
{/* Further Brand Details */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px' }}>
Further Brand Details
</Typography>
<TextField
name="brand_details"
placeholder="We need Pizza Machine"
variant="outlined"
fullWidth
multiline
minRows={3}
value={formData.brand_details || ''}
onChange={handleChange}
error={!!errors.brand_details}
helperText={errors.brand_details}
sx={{
'& .MuiInputBase-root': {
fontWeight: 500,
fontSize: '15px',
alignItems: 'start',
},
'& textarea::placeholder': {
color: '#969BA7',
},
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
}
}}
/>
</Box>
{/* Age Group */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Age Group
</Typography>
<TextField
name="age_group"
placeholder="15 - 75"
variant="outlined"
fullWidth
value={formData.age_group || ''}
onChange={handleChange}
error={!!errors.age_group}
helperText={errors.age_group}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
{/* Location */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Location
</Typography>
<TextField
name="location"
placeholder="Street 123, Jordan"
variant="outlined"
fullWidth
value={formData.location || ''}
onChange={handleChange}
error={!!errors.location}
helperText={errors.location}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
{/* Menu Status */}
<Box>
<Typography
variant="body2"
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
>
Menu Status
</Typography>
<FormControl error={!!errors.menu_status}>
<RadioGroup
row
name="menu_status"
value={formData.menu_status ? 'yes' : 'no'}
onChange={handleChange}
>
<FormControlLabel
value="yes"
control={
<Radio
sx={{
color: 'rgba(150, 155, 167, 0.6)',
transform: 'scale(0.85)',
'&.Mui-checked': {
color: theme.palette.primary.main
}
}}
/>
}
label="Yes"
/>
<FormControlLabel
value="no"
control={
<Radio
sx={{
color: 'rgba(150, 155, 167, 0.6)',
transform: 'scale(0.85)',
'&.Mui-checked': {
color: theme.palette.primary.main
}
}}
/>
}
label="No"
/>
</RadioGroup>
{errors.menu_status && (
<FormHelperText>{errors.menu_status}</FormHelperText>
)}
</FormControl>
</Box>
{/* زر Next */}
<Box sx={{ pt: 2.8 }}>
<Button
variant="contained"
fullWidth
onClick={handleNext}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
},
}}
>
Next
</Button>
</Box>
</Stack>
</Box>
);
};
export default CloudKitchenProject;
@@ -0,0 +1,182 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Button,
Box,
Typography,
useTheme,
CircularProgress
} from '@mui/material';
const ConfirmationDialog = ({ open, onClose, onConfirm, loading }) => {
const theme = useTheme();
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
width: '525px',
height: '350px',
maxWidth: '100%',
maxHeight: '100%',
borderRadius: '12px',
overflow: 'hidden'
}
}}
>
<Box sx={{
p: 3,
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
mb: 3,
ml: -5,
}}
>
{/* الدوائر */}
<Box sx={{
position: 'relative',
width: 100,
height: 100,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{/* الدائرة الخلفية */}
<Box sx={{
position: 'absolute',
backgroundColor: '#F5F8FF',
width: '100%',
height: '100%',
borderRadius: '50%',
zIndex: 0,
}} />
<Box sx={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: '#E9EFFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}>
<img
src='/images/createProfile/Successful.png'
style={{
width: 40,
height: 40,
objectFit: 'contain',
}}
alt="Success icon"
/>
</Box>
</Box>
<DialogTitle sx={{ p: 0 }}>
<Typography
variant="h4"
component="div"
sx={{
fontWeight: 600,
fontSize: '28px',
lineHeight: '1.2'
}}
>
Register successful!
</Typography>
</DialogTitle>
</Box>
<DialogContent sx={{ p: 0 }}>
<DialogContentText sx={{
textAlign: 'center',
mb: 0,
fontSize: '16px',
fontWeight: 500,
color: theme.palette.text.secondary
}}>
Congratulations! you well register your restaurant successfully on our platform
</DialogContentText>
</DialogContent>
<DialogActions
sx={{
p: 0,
px: 3,
pt: 2,
flexDirection: 'column',
gap: 2,
}}
>
<Button
variant="contained"
fullWidth
onClick={onClose}
sx={{
fontWeight: 600,
fontSize: '16px',
height: '48px',
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.dark,
},
}}
>
Censel
</Button>
{/* الزر الأول - Filled */}
<Button
variant="contained"
fullWidth
onClick={onConfirm}
disabled={loading}
sx={{
fontWeight: 600,
fontSize: '16px',
height: '48px',
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.dark,
},
}}
>
{loading ? (
<CircularProgress size={24} sx={{ color: 'white' }} />
) : (
'Submit'
)}
</Button>
</DialogActions>
</Box>
</Dialog>
);
};
export default ConfirmationDialog;
@@ -0,0 +1,292 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Typography,
Stack,
Button,
useTheme,
TextField,
MenuItem,
} from '@mui/material';
import authService from '../../../../services/authService'; // ← عدّل المسار حسب مكانك
const OperationalDetails = ({
currentStepIndex = 0,
onNext,
onBack,
formData,
updateFormData,
}) => {
const theme = useTheme();
const [countries, setCountries] = useState([]);
const [loadingCountries, setLoadingCountries] = useState(false);
const [errors, setErrors] = useState({}); // <-- حالة الأخطاء
useEffect(() => {
const fetchCountries = async () => {
setLoadingCountries(true);
const result = await authService.getCountries();
if (result.success) {
setCountries(result.data);
} else {
console.error(result.message);
}
setLoadingCountries(false);
};
fetchCountries();
}, []);
const handleChange = (e) => {
const { name, value } = e.target;
updateFormData({ [name]: value });
};
// التحقق من صحة الحقول
const validate = () => {
let tempErrors = {};
// التحقق من أن الحقل غير فارغ
if (!formData.staff_members || formData.staff_members.trim() === '') {
tempErrors.staff_members = 'Staff Members is required';
}
// التحقق من أن القيمة رقمية
else if (isNaN(formData.staff_members)) {
tempErrors.staff_members = 'Staff Members must be a number';
}
if (!formData.country_id || formData.country_id === '') {
tempErrors.country_id = 'Country selection is required';
}
setErrors(tempErrors);
return Object.keys(tempErrors).length === 0;
};
const handleNext = () => {
if (validate()) {
onNext();
}
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 14.5 },
pb: { xs: 20, sm: 20, md: 0 },
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={2.5}>
<Typography
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem',
},
}}
>
Operational Details
</Typography>
<Box sx={{ width: '70%' }}>
<Typography
fontSize="16px"
color="text.secondary"
fontWeight={500}
sx={{ pb: 1 }}
>
Enter your basic information to proceed to registration of your
own restaurant on this platform
</Typography>
</Box>
{/* Staff Members Input */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography
variant="body2"
color="black"
sx={{ fontWeight: '500', fontSize: '16px' }}
>
Staff Members
</Typography>
<TextField
name="staff_members"
placeholder="200 (100 Chefs, 100 Cooks)"
variant="outlined"
fullWidth
value={formData.staff_members || ''}
onChange={handleChange}
error={!!errors.staff_members}
helperText={errors.staff_members}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
},
}}
/>
</Box>
{/* Country Selector */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography
variant="body2"
color="black"
sx={{ fontWeight: '500', fontSize: '16px' }}
>
Country
</Typography>
<TextField
select
name="country_id"
fullWidth
value={formData.country_id || ''}
onChange={handleChange}
disabled={loadingCountries}
error={!!errors.country_id}
helperText={errors.country_id}
sx={{
'& .MuiSelect-select': {
fontWeight: 500,
fontSize: '15px',
minHeight: '40px',
display: 'flex',
alignItems: 'center',
},
'& .MuiOutlinedInput-input': { padding: '10.5px 14px' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
},
}}
>
<MenuItem value="" disabled hidden>
{loadingCountries ? 'Loading...' : 'Select your country'}
</MenuItem>
{countries.map((country) => (
<MenuItem key={country.id} value={country.id}>
{country.name}
</MenuItem>
))}
</TextField>
</Box>
{/* Expansion Plan Cities Input */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography
variant="body2"
color="black"
sx={{ fontWeight: '500', fontSize: '16px' }}
>
Expansion Plan Cities
</Typography>
<TextField
name="expansion_plan_cities" // ← ✅ الاسم الصحيح المتوقع من الـ API
placeholder="Layyah Lahore Islamabad"
variant="outlined"
fullWidth
value={formData.expansion_plan_cities || ''}
onChange={handleChange}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
},
}}
/>
</Box>
{/* Next Button */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleNext}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
},
}}
>
Next
</Button>
{/* Back Button */}
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default OperationalDetails;
@@ -0,0 +1,285 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Stack,
Button,
useTheme,
TextField,
FormControlLabel,
RadioGroup,
Radio
} from '@mui/material';
const RequiredEquipments = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => {
const theme = useTheme();
const [errors, setErrors] = useState({});
// تعيين القيمة الافتراضية لـ needSpecializedEquipment إذا غير موجودة
useEffect(() => {
if (!formData.needSpecializedEquipment) {
updateFormData({ needSpecializedEquipment: 'no' });
}
}, [formData.needSpecializedEquipment, updateFormData]);
// التعامل مع تغير القيمة النصية للمعدات العامة
const handleEquipmentChange = (e) => {
updateFormData({ equipment: e.target.value });
};
const handleNeedSpecializedChange = (e) => {
updateFormData({ needSpecializedEquipment: e.target.value });
};
// التعامل مع القيمة النصية للمعدات المتخصصة
const handleSpecializedEquipmentChange = (e) => {
updateFormData({ specialized_equipment: e.target.value });
};
// دالة التحقق من صحة الحقول
const validate = () => {
let tempErrors = {};
// if (!formData.equipment || formData.equipment.trim() === '') {
// tempErrors.equipment = 'Equipment is required';
// }
// if (!formData.needSpecializedEquipment || (formData.needSpecializedEquipment !== 'yes' && formData.needSpecializedEquipment !== 'no')) {
// tempErrors.needSpecializedEquipment = 'Please select Yes or No';
// }
if (formData.needSpecializedEquipment === 'yes' && (!formData.specialized_equipment || formData.specialized_equipment.trim() === '')) {
tempErrors.specialized_equipment = 'Please specify specialized equipments';
}
setErrors(tempErrors);
return Object.keys(tempErrors).length === 0;
};
// دالة معالجة الضغط على زر Next مع تحقق الفالديشن
const handleNext = () => {
if (validate()) {
onNext();
}
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 4.5 },
pb: 10,
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={2.5}>
<Typography
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem'
}
}}
>
Required Equipments
</Typography>
<Box sx={{ width: '70%' }}>
<Typography
fontSize="16px"
color="text.secondary"
fontWeight={500}
sx={{ pb: 1 }}
>
Enter your basic information to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
{/* Equipment Input */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Equipment
</Typography>
<TextField
placeholder="Ovens Refrigerators Pizza Machines |"
variant="outlined"
fullWidth
value={formData.equipment || ''}
onChange={handleEquipmentChange}
error={!!errors.equipment}
helperText={errors.equipment}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
{/* Need Specialized Equipment */}
<Box>
<Typography
variant="body2"
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
>
Need Specialized Equipment
</Typography>
<RadioGroup
row
name="needSpecializedEquipment"
value={formData.needSpecializedEquipment || 'no'}
onChange={handleNeedSpecializedChange}
>
<FormControlLabel
value="yes"
control={
<Radio
sx={{
color: 'rgba(150, 155, 167, 0.6)',
transform: 'scale(0.85)',
'&.Mui-checked': {
color: theme.palette.primary.main
}
}}
/>
}
label="Yes"
/>
<FormControlLabel
value="no"
control={
<Radio
sx={{
pl: 5,
color: 'rgba(150, 155, 167, 0.6)',
transform: 'scale(0.85)',
'&.Mui-checked': {
color: theme.palette.primary.main
}
}}
/>
}
label="No"
/>
</RadioGroup>
{errors.needSpecializedEquipment && (
<Typography color="error" variant="caption" sx={{ mt: 0.5 }}>
{errors.needSpecializedEquipment}
</Typography>
)}
</Box>
{/* Specialized Equipments For Food Preparation */}
{formData.needSpecializedEquipment === 'yes' && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px' }}>
Specialized Equipments For Food Preparation
</Typography>
<TextField
placeholder="Knife Spoon Handle |"
variant="outlined"
fullWidth
multiline
minRows={3}
value={formData.specialized_equipment || ''}
onChange={handleSpecializedEquipmentChange}
error={!!errors.specialized_equipment}
helperText={errors.specialized_equipment
}
sx={{
'& .MuiInputBase-root': {
fontWeight: 500,
fontSize: '15px',
alignItems: 'start',
},
'& textarea::placeholder': {
color: '#969BA7',
},
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
}
}}
/>
</Box>
)}
{/* Buttons */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleNext}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
},
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default RequiredEquipments;
@@ -0,0 +1,256 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Stack,
Button,
useTheme,
RadioGroup,
FormControlLabel,
Radio,
Typography as MuiTypography
} from '@mui/material';
const VisualIdentity = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => {
const theme = useTheme();
const [errors, setErrors] = useState({});
// تأكد من وجود قيمة افتراضية
useEffect(() => {
if (!formData.visualIdentity) {
updateFormData({ visualIdentity: 'no' });
}
}, [formData.visualIdentity, updateFormData]);
const handleVisualIdentityChange = (e) => {
updateFormData({ visualIdentity: e.target.value });
};
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
updateFormData({ logo: file });
}
};
const validate = () => {
let tempErrors = {};
if (!formData.visualIdentity || (formData.visualIdentity !== 'yes' && formData.visualIdentity !== 'no')) {
tempErrors.visualIdentity = 'Please select Yes or No';
}
/*
if (!formData.logo) {
tempErrors.logo = 'Please upload a logo or color scheme image';
}
*/
setErrors(tempErrors);
return Object.keys(tempErrors).length === 0;
};
const handleNext = () => {
if (validate()) {
onNext();
}
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 4.5 },
pb: 10,
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
width: { xs: '85%', sm: '90%' },
mx: 'auto'
}}
>
<Stack spacing={2.5}>
<Typography
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem'
}
}}
>
Visual Identity
</Typography>
<Box sx={{ width: '70%' }}>
<Typography
fontSize="16px"
color="text.secondary"
fontWeight={500}
sx={{ pb: 1 }}
>
Enter your basic information to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
<Box>
<Typography
variant="body2"
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
>
Visual Identity
</Typography>
<RadioGroup
row
name="visualIdentity"
value={formData.visualIdentity || ''}
onChange={handleVisualIdentityChange}
>
<FormControlLabel
value="yes"
control={
<Radio
sx={{
color: 'rgba(150, 155, 167, 0.6)',
transform: 'scale(0.85)',
'&.Mui-checked': {
color: theme.palette.primary.main
}
}}
/>
}
label="Yes"
/>
<FormControlLabel
value="no"
control={
<Radio
sx={{
pl: 5,
color: 'rgba(150, 155, 167, 0.6)',
transform: 'scale(0.85)',
'&.Mui-checked': {
color: theme.palette.primary.main
}
}}
/>
}
label="No"
/>
</RadioGroup>
{errors.visualIdentity && (
<MuiTypography color="error" variant="caption" sx={{ mt: 0.5 }}>
{errors.visualIdentity}
</MuiTypography>
)}
</Box>
{/* Upload Logo / Color Scheme */}
<Box>
<Typography
sx={{
fontWeight: 500,
fontSize: '16px',
color: 'black',
mb: 2,
pl: 1,
}}
>
Upload Logo / Color Scheme If Any
</Typography>
<Box
component="label"
htmlFor="upload-image"
sx={{
border: errors.logo_path ? '2px solid red' : '2px dashed #ccc',
borderRadius: '10px',
height: '150px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
color: '#969BA7',
backgroundColor: '#FAFAFA',
fontWeight: 500,
fontSize: '15px',
textAlign: 'center',
transition: 'border-color 0.3s',
'&:hover': {
borderColor: '#FF914D',
backgroundColor: '#fffaf5',
}
}}
>
{formData.logo ? (formData.logo.name || 'Uploaded Image') : 'Upload Picture'}
<input
id="upload-image"
type="file"
accept="image/*"
hidden
onChange={handleFileChange}
/>
</Box>
{errors.logo && (
<MuiTypography color="error" variant="caption" sx={{ mt: 0.5 }}>
{errors.logo}
</MuiTypography>
)}
</Box>
{/* Buttons */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleNext}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
},
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default VisualIdentity;
@@ -0,0 +1,244 @@
import React, { useState, useEffect } from 'react';
import { useRestaurant } from '../../../contexts/RestaurantContext';
import StatisticsCard from './StatisticsCard';
// import TablesManager from './TablesManager';
import {
Box,
useTheme,
useMediaQuery,
Button,
ButtonGroup,
TextField
} from '@mui/material';
import authService from '../../../services/authService';
import dayjs from 'dayjs';
const AnalyticsPage = () => {
const { restaurantId } = useRestaurant();
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [chartData, setChartData] = useState([]);
const handleTimeFrameChange = (newTimeFrame) => {
setTimeFrame(newTimeFrame);
};
const handleCustomDateChange = (event) => {
setCustomDate(event.target.value);
};
const fetchStatistics = async () => {
if (!restaurantId) return;
let period = 'yearly';
let labels = [];
let mappedData = [];
switch (timeFrame) {
case '24h': // يومي
period = 'daily';
labels = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`);
try {
const response = await authService.getOrderStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label || item.date || '',
orders_total: item.orders_total || 0,
items_total: item.items_total || 0
}));
} else {
mappedData = labels.map(label => ({
label,
orders_total: 0,
items_total: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
orders_total: 0,
items_total: 0
}));
}
break;
case '30d': // شهري
period = 'monthly';
labels = Array.from({ length: 30 }, (_, i) => dayjs().startOf('month').add(i, 'day').format('DD'));
try {
const response = await authService.getOrderStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label ? dayjs(item.label).format('DD') : item.date ? dayjs(item.date).format('DD') : '',
orders_total: item.orders_total || 0,
items_total: item.items_total || 0
}));
} else {
mappedData = labels.map(label => ({
label,
orders_total: 0,
items_total: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
orders_total: 0,
items_total: 0
}));
}
break;
case '12m': // سنوي
default:
period = 'yearly';
labels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
try {
const response = await authService.getOrderStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label || item.date || '',
orders_total: item.orders_total || 0,
items_total: item.items_total || 0
}));
} else {
mappedData = labels.map(label => ({
label,
orders_total: 0,
items_total: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
orders_total: 0,
items_total: 0
}));
}
break;
}
setChartData(mappedData);
};
useEffect(() => {
fetchStatistics();
}, [timeFrame, restaurantId, customDate]);
return (
<Box
pr={{ xs: 1, sm: 2 }}
maxWidth="100%"
overflowX="hidden">
{/* Header Buttons */}
<Box
display="flex"
flexDirection={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems="center"
gap={2}
mb={1.5}
>
<ButtonGroup
size="small"
variant="outlined"
sx={{
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: 'inset 0 0 0 1px #e0e0e0',
'& .MuiButton-root': {
textTransform: 'none',
fontSize: '13px',
padding: '6px 12px',
border: 'none',
borderRadius: '8px',
color: '#555',
'&:hover': { backgroundColor: '#f5f5f5' }
},
'& .MuiButton-contained': {
backgroundColor: 'rgba(255, 117, 34, 0.08)',
color: '#ff5722',
fontWeight: 'bold'
}
}}
>
{[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => (
<Button
key={value}
variant={timeFrame === value ? 'contained' : 'text'}
onClick={() => handleTimeFrameChange(value)}
>
{label}
</Button>
))}
</ButtonGroup>
{/* Date Picker + Today Button */}
<Box
display="flex"
gap={1}
alignItems="center"
sx={{
backgroundColor: theme.palette.background.paper,
borderRadius: 2,
p: '4px 8px',
boxShadow: 'inset 0 0 0 1px #e0e0e0'
}}
>
<TextField
type="date"
value={customDate}
onChange={handleCustomDateChange}
size="small"
sx={{
width: isMobile ? '100%' : 150,
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
}}
/>
<Button
variant="contained"
size="small"
onClick={() => setCustomDate(dayjs().format('YYYY-MM-DD'))}
sx={{
backgroundColor: 'rgba(255, 117, 34, 0.08)',
color: '#ff5722',
textTransform: 'none',
boxShadow: 'none',
fontSize: isMobile ? 12 : 13,
'&:hover': { backgroundColor: 'rgba(255, 117, 34, 0.15)' }
}}
>
Today
</Button>
</Box>
</Box>
{/* StatisticsCard */}
<Box>
<StatisticsCard
title="Analytics Overview"
subtitle="Orders Statistics"
data={chartData}
dataKeys={[
{ key: 'orders_total', name: 'Orders', color: '#E46A11' },
{ key: 'items_total', name: 'Items', color: '#0182FC' },
]}
xDataKey="label"
valueFormatter={(value) => value}
timeFrame={timeFrame}
onTimeFrameChange={handleTimeFrameChange}
height={320}
/>
</Box>
</Box>
);
};
export default AnalyticsPage;
@@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react';
import { useRestaurant } from '../../../contexts/RestaurantContext';
import OrderStatusCard from './OrderStatusCard';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import authService from '../../../services/authService';
import dayjs from 'dayjs';
const AnalyticsPageOccupancy = () => {
const { restaurantId } = useRestaurant();
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [occupancyRate, setOccupancyRate] = useState(0);
const fetchOccupancyRate = async () => {
if (!restaurantId) return;
let period = 'yearly';
switch (timeFrame) {
case '24h':
period = 'daily';
break;
case '30d':
period = 'monthly';
break;
case '12m':
default:
period = 'yearly';
break;
}
try {
const response = await authService.getOccupancyRate(
restaurantId,
period,
customDate
);
if (response.success) {
// occupancy_rate جايه كنص مثل "11.83%" → نحولها لرقم
const rateValue = parseFloat(response.data.replace('%', '').trim());
setOccupancyRate(rateValue);
} else {
setOccupancyRate(0);
}
} catch (error) {
console.error(error);
setOccupancyRate(0);
}
};
useEffect(() => {
fetchOccupancyRate();
}, [timeFrame, restaurantId, customDate]);
return (
<Box pr={{ xs: 1, sm: 2 }} maxWidth="100%" overflowX="hidden">
{/* Occupancy Card */}
<Box>
<OrderStatusCard
title="Occupancy Rate"
period={
timeFrame === '24h'
? 'Today'
: timeFrame === '30d'
? 'This Month'
: 'This Year'
}
progress={occupancyRate} // نسبة الإشغال
description={`Current occupancy rate is ${occupancyRate}% for selected period.`}
timeFrame={timeFrame}
setTimeFrame={setTimeFrame}
customDate={customDate}
setCustomDate={setCustomDate}
/>
</Box>
</Box>
);
};
export default AnalyticsPageOccupancy;
@@ -0,0 +1,241 @@
import React, { useState, useEffect } from 'react';
import { useRestaurant } from '../../../contexts/RestaurantContext';
import StatisticsCard from './StatisticsCard';
// import TablesManager from './TablesManager';
import {
Box,
useTheme,
useMediaQuery,
Button,
ButtonGroup,
TextField
} from '@mui/material';
import authService from '../../../services/authService';
import dayjs from 'dayjs';
const AnalyticsPagebill = () => {
const { restaurantId } = useRestaurant();
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [chartData, setChartData] = useState([]);
const handleTimeFrameChange = (newTimeFrame) => {
setTimeFrame(newTimeFrame);
};
const handleCustomDateChange = (event) => {
setCustomDate(event.target.value);
};
const fetchStatistics = async () => {
if (!restaurantId) return;
let period = 'yearly';
let labels = [];
let mappedData = [];
switch (timeFrame) {
case '24h': // يومي
period = 'daily';
labels = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`);
try {
const response = await authService.getBillStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label || item.date || '',
bills_total: item.bills_total || 0,
total_price: item.total_price || 0
}));
} else {
mappedData = labels.map(label => ({
label,
bills_total: 0,
total_price: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
bills_total: 0,
total_price: 0
}));
}
break;
case '30d': // شهري
period = 'monthly';
labels = Array.from({ length: 30 }, (_, i) => dayjs().startOf('month').add(i, 'day').format('DD'));
try {
const response = await authService.getBillStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label ? dayjs(item.label).format('DD') : item.date ? dayjs(item.date).format('DD') : '',
bills_total: item.bills_total || 0,
total_price: item.total_price || 0
}));
} else {
mappedData = labels.map(label => ({
label,
bills_total: 0,
total_price: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
bills_total: 0,
total_price: 0
}));
}
break;
case '12m': // سنوي
default:
period = 'yearly';
labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
try {
const response = await authService.getBillStatistics(restaurantId, period, customDate);
if (response.success && response.data.length > 0) {
mappedData = response.data.map(item => ({
label: item.label || item.date || '',
bills_total: item.bills_total || 0,
total_price: item.total_price || 0
}));
} else {
mappedData = labels.map(label => ({
label,
bills_total: 0,
total_price: 0
}));
}
} catch (error) {
console.error(error);
mappedData = labels.map(label => ({
label,
bills_total: 0,
total_price: 0
}));
}
break;
}
setChartData(mappedData);
};
useEffect(() => {
fetchStatistics();
}, [timeFrame, restaurantId, customDate]);
return (
<Box p={{ xs: 1, sm: 2 }} maxWidth="100%" overflowX="hidden">
{/* Header Buttons */}
<Box
display="flex"
flexDirection={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems="center"
gap={2}
mb={1}
>
<ButtonGroup
size="small"
variant="outlined"
sx={{
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: 'inset 0 0 0 1px #e0e0e0',
'& .MuiButton-root': {
textTransform: 'none',
fontSize: '13px',
padding: '6px 12px',
border: 'none',
borderRadius: '8px',
color: '#555',
'&:hover': { backgroundColor: '#f5f5f5' }
},
'& .MuiButton-contained': {
backgroundColor: 'rgba(255, 117, 34, 0.08)',
color: '#ff5722',
fontWeight: 'bold'
}
}}
>
{[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => (
<Button
key={value}
variant={timeFrame === value ? 'contained' : 'text'}
onClick={() => handleTimeFrameChange(value)}
>
{label}
</Button>
))}
</ButtonGroup>
{/* Date Picker + Today Button */}
<Box
display="flex"
gap={1}
alignItems="center"
sx={{
backgroundColor: theme.palette.background.paper,
borderRadius: 2,
p: '4px 8px',
boxShadow: 'inset 0 0 0 1px #e0e0e0'
}}
>
<TextField
type="date"
value={customDate}
onChange={handleCustomDateChange}
size="small"
sx={{
width: isMobile ? '100%' : 150,
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
}}
/>
<Button
variant="contained"
size="small"
onClick={() => setCustomDate(dayjs().format('YYYY-MM-DD'))}
sx={{
backgroundColor: 'rgba(255, 117, 34, 0.08)',
color: '#ff5722',
textTransform: 'none',
boxShadow: 'none',
fontSize: isMobile ? 12 : 13,
'&:hover': { backgroundColor: 'rgba(255, 117, 34, 0.15)' }
}}
>
Today
</Button>
</Box>
</Box>
{/* StatisticsCard */}
<Box>
<StatisticsCard
title="Analytics Overview"
subtitle="Bills Statistics"
data={chartData}
dataKeys={[
{ key: 'bills_total', name: 'Bills', color: '#11e4c1ff' },
{ key: 'total_price', name: 'Total price', color: '#fcce01ff' },
]}
xDataKey="label"
valueFormatter={(value) => value}
timeFrame={timeFrame}
onTimeFrameChange={handleTimeFrameChange}
height={420}
/>
</Box>
</Box>
);
};
export default AnalyticsPagebill;
+110
View File
@@ -0,0 +1,110 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery, Skeleton } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import DashbordContect from './DashboardContcet';
import NoProdectDash from './NoProdectDash';
const drawerWidth = 230;
const Dashboard = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [hasProducts, setHasProducts] = useState(false);
// const [isLoading, setIsLoading] = useState(true);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
useEffect(() => {
const checkProducts = async () => {
// setIsLoading(true);
const productsExist = await checkIfProductsExist();
setHasProducts(productsExist);
// setIsLoading(false);
};
checkProducts();
}, []);
const checkIfProductsExist = async () => {
return new Promise((resolve) => setTimeout(() => resolve(true), 1500));
};
useEffect(() => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
}, [theme.breakpoints.values.md]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
const handleDrawerToggle = () => {
setSidebarOpen(!sidebarOpen);
};
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: { xs: '100%', sm: '100%', md: '100%' },
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% - 20px)' : '100%',
// pt: { xs: 0.5, sm: 1 },
// mt: { xs: 1, sm: 2 },
overflowY: 'auto',
pl: { xs: 0, sm: 2 },
pr: { xs: 1, sm: 2 },
pb: { xs: 1, sm: 2 },
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
transition: theme.transitions.create(['margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}>
{/* {isLoading ? (
<>
<Skeleton variant="rectangular" height={50} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" height={200} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" height={300} sx={{ mb: 2 }} />
</>
) : (
hasProducts ?
<DashbordContect />
: <NoProdectDash />
)} */}
<DashbordContect />
</Box>
</Box>
</Box>
);
};
export default Dashboard;
@@ -0,0 +1,287 @@
import React from 'react';
import { Box, Button, Card, CardContent, Stack, Typography, useMediaQuery } from '@mui/material';
import AirportShuttleIcon from '@mui/icons-material/AirportShuttle';
import CampaignIcon from '@mui/icons-material/Campaign';
import Inventory2Icon from '@mui/icons-material/Inventory2';
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import { useTheme } from '@mui/material/styles';
import AddIcon from '@mui/icons-material/Add';
import AnalyticsPage from './AnalyticsPage';
import AnalyticsPageOccupancy from './AnalyticsPageOccupancy';
import AnalyticsPagebill from './AnalyticsPagebill';
const IconCircle = ({ children, bgColor = '#DEDEFA', outerColor = '#EFEFFD' }) => (
<Box
sx={{
position: 'relative',
width: { xs: 40, sm: 45, md: 50 },
height: { xs: 43, sm: 48, md: 53 },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box
sx={{
position: 'absolute',
width: { xs: 32, sm: 36, md: 40 },
height: { xs: 35, sm: 39, md: 43 },
borderRadius: '50%',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: outerColor,
zIndex: 0,
}}
/>
<Box
sx={{
width: { xs: 24, sm: 27, md: 30 },
height: { xs: 27, sm: 30, md: 33 },
backgroundColor: bgColor,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
{children}
</Box>
</Box>
);
const DashboardContect = () => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const isMediumScreen = useMediaQuery(theme.breakpoints.between('sm', 'md'));
return (
<>
{/* Header Section */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
mb: 1,
pl: 1,
pr: { xs: 1, sm: 3 },
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
gap: { xs: 2, sm: 0 }
}}
>
{/* Title */}
<Typography
variant="h6"
sx={{
fontWeight: '500',
fontSize: { xs: '20px', sm: '22px', md: '24px' },
color: '#121212'
}}
>
Dashboard
</Typography>
{/* Buttons */}
{/* <Box sx={{
display: 'flex',
gap: { xs: 1, sm: 2 },
flexWrap: { xs: 'wrap', sm: 'nowrap' },
width: { xs: '100%', sm: 'auto' },
justifyContent: { xs: 'space-between', sm: 'space-between', md: 'flex-end' }
}}>
<Button
variant="contained"
sx={{
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
boxShadow: 'none',
borderRadius: '8px',
height: '40px',
width: { xs: '48%', sm: '130px', md: '156px' },
fontSize: { xs: '12px', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset'
}}
>
<AddIcon sx={{ fontSize: { xs: 16, sm: 18, md: 20 } }} />
{!isSmallScreen && 'Add New Menu'}
</Button>
<Button
variant="contained"
sx={{
textTransform: 'none',
color: 'white',
backgroundColor: '#5F6868',
boxShadow: 'none',
borderRadius: '8px',
height: '40px',
width: { xs: '48%', sm: '150px', md: '208px' },
fontSize: { xs: '12px', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset'
}}
>
{isSmallScreen ? 'Marketing' : 'Start Marketing '}
</Button>
<Button
variant="contained"
sx={{
textTransform: 'none',
color: 'black',
backgroundColor: '#FFFFFF',
boxShadow: 'none',
borderRadius: '8px',
height: '40px',
width: { xs: '100%', sm: '120px', md: '141px' },
fontSize: { xs: '12px', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset'
}}
>
Check Inventory
</Button>
</Box> */}
</Box>
{/* Status Cards */}
{/* <Box
sx={{
display: 'flex',
gap: { xs: 1.5, sm: 2 },
flexWrap: 'wrap',
justifyContent: { xs: 'center', sm: 'flex-start' }
}}
>
<StatusCard
icon={<DeveloperBoardIcon />}
statusText="Active"
statusColor="#0D894F"
iconColor="#057DEC"
innerColor="#DEDEFA"
outerColor="#EFEFFD"
iconSpacing={{ xs: 6, sm: 8, md: 12 }}
extraButton={
<Button
variant="contained"
sx={{
backgroundColor: '#F6F6F6',
color: '#5F6868',
textTransform: 'none',
fontSize: { xs: '0.7rem', sm: '0.8rem', md: '0.85rem' },
borderRadius: '8px',
padding: { xs: '2px 8px', sm: '4px 12px' },
boxShadow: 'none',
fontWeight: 600,
height: { xs: 32, sm: 36, md: 40 },
width: { xs: 70, sm: 80, md: 85 },
whiteSpace: 'nowrap',
}}
>
Register
</Button>
}
/>
<StatusCard
icon={<Inventory2Icon />}
statusText="Active"
statusColor="#0D894F"
iconColor="#34B405"
innerColor="#E1F6DF"
outerColor="#EDFBE9"
iconSpacing={{ xs: 6, sm: 8, md: 12 }}
title="Inventory"
/>
<StatusCard
icon={<CampaignIcon />}
statusText="Pending"
statusColor="#BBB50F"
iconColor="#990463"
innerColor="#FEC8EA"
outerColor="#FFE3F5"
iconSpacing={{ xs: 6, sm: 8, md: 12 }}
title="Marketing"
/>
<StatusCard
icon={<AirportShuttleIcon />}
statusText="Action Need"
statusColor="#EF0A0A"
iconColor="#069797"
outerColor="#DBFBFB"
innerColor="#B5F5F5"
iconSpacing={{ xs: 3, sm: 5, md: 7 }}
title="Delivery"
/>
</Box> */}
{/* OrderStatusCard/ StatisticsCard */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'nowrap',
flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 1, sm: 1, md: 7 }, // gap مرن
mr: { xs: 4, sm: 3, md: 0 },
ml: { xs: 2, sm: 0 },
flexShrink: 0,
}}
>
{/* OrderStatusCard */}
<Box
sx={{
flexGrow: 0,
width: { xs: '100%', sm: '35%', md: '30%' },
minWidth: 20,
ml: { xs: 0, sm: -1 },
flexShrink: 0,
mt:8
}}
>
<AnalyticsPageOccupancy />
</Box>
{/* StatisticsCard */}
<Box
sx={{
flexGrow: 1,
minWidth: 0,
// ml: { xs: 0, sm: 2 },
// backgroundColor:'red'
}}
>
<AnalyticsPage />
</Box>
</Box>
{/* RecentActivity */}
<Box
sx={{
mr: { xs: 4, sm: 3, md: 0 },
ml: { xs: 2, sm: 0 },
}}
>
<AnalyticsPagebill />
</Box>
</>
);
};
export default DashboardContect;
@@ -0,0 +1,101 @@
import React from 'react';
import {
Box,
Button,
Card,
CardContent,
Typography,
useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import AddIcon from '@mui/icons-material/Add';
const NoProdectDash = () => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<>
{/* Header Section */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
mb: 3,
mt:10,
pl: 1,
pr: { xs: 1, sm: 3 },
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
gap: { xs: 2, sm: 0 }
}}
>
</Box>
{/* Empty State Content */}
<Card
sx={{
boxShadow: 'none',
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
backgroundColor: 'transparent'
}}
>
<CardContent>
{/* Image Placeholder */}
<Box
component="img"
src="/images/compeoter.png"
alt="No products available"
sx={{
width: isSmallScreen ? 150 : 200,
height: 'auto',
mb: 1,
opacity: 0.8
}}
/>
{/* Title with colored "Sorry!" */}
<Typography
variant="h5"
sx={{
fontWeight: 400,
fontSize: { xs: '14px', md: '18px' },
mb: 2,
color: '#5F6868',
whiteSpace: 'pre-line' // يسمح بكسر السطر عند المسافات
}}
>
<Box component="span" sx={{ fontWeight: 600, fontSize: '18px' }}>Sorry!</Box>{' '}
There is no campaign available.
Please initiate the {'\n'}creation of a new campaign.
</Typography>
{/* Action Button */}
{/* <Button
variant="contained"
startIcon={<AddIcon />}
sx={{
textTransform: 'none',
px: 4,
py: 1.5,
fontSize: '16px',
fontWeight: 600,
borderRadius: 2,
color:'white'
}}
>
Create New Product
</Button> */}
</CardContent>
</Card>
</>
);
};
export default NoProdectDash;
@@ -0,0 +1,241 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Card,
CardContent,
useTheme,
IconButton,
useMediaQuery,
Menu,
MenuItem,
Button,
ButtonGroup,
TextField
} from '@mui/material';
import {
TrendingUp as TrendingUpIcon,
TrendingDown as TrendingDownIcon,
} from '@mui/icons-material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import {
CircularProgressbar,
buildStyles
} from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
import { ResponsiveContainer } from 'recharts';
import dayjs from 'dayjs';
const OrderStatusCard = ({
title = 'Order Status',
period = 'This Quarter',
progress = 70.5,
description = 'You succeeded in earning $240 today, it’s higher than yesterday',
timeFrame,
setTimeFrame,
customDate,
setCustomDate
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
return (
<Card
sx={{
width: { xs: '96%', sm: '94%', md: 360 },
height: '400',
borderRadius: 2,
boxShadow: '0px 4px 5px rgba(0, 0, 0, 0.1)',
p: 1,
position: 'relative'
}}
>
<ResponsiveContainer width="100%" height={isMobile ? '75vh' : isTablet ? '59vh' : 385}>
{/* زر القائمة العلوية */}
<IconButton
aria-label="more-actions"
onClick={handleMenuOpen}
sx={{
position: 'absolute',
top: 8,
right: 8,
color: '#667085'
}}
>
<MoreVertIcon />
</IconButton>
{/* القائمة المنسدلة */}
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleMenuClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<Box sx={{ p: 2, minWidth: 250 }}>
{/* Header Buttons (Timeframe) */}
<ButtonGroup
size="small"
variant="outlined"
sx={{
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: 'inset 0 0 0 1px #e0e0e0',
mb: 2,
width: '100%',
'& .MuiButton-root': {
textTransform: 'none',
fontSize: '13px',
padding: '6px 12px',
border: 'none',
borderRadius: '8px',
color: '#555',
'&:hover': { backgroundColor: '#f5f5f5' }
},
'& .MuiButton-contained': {
backgroundColor: 'rgba(255, 117, 34, 0.08)',
color: '#ff5722',
fontWeight: 'bold'
}
}}
>
{[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => (
<Button
key={value}
variant={timeFrame === value ? 'contained' : 'text'}
onClick={() => { setTimeFrame(value); handleMenuClose(); }}
>
{label}
</Button>
))}
</ButtonGroup>
{/* Date Picker + Today Button */}
<Box
display="flex"
gap={1}
alignItems="center"
sx={{
backgroundColor: theme.palette.background.paper,
borderRadius: 2,
p: '4px 8px',
boxShadow: 'inset 0 0 0 1px #e0e0e0'
}}
>
<TextField
type="date"
value={customDate}
onChange={(e) => setCustomDate(e.target.value)}
size="small"
sx={{
width: isMobile ? '100%' : 150,
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
}}
/>
<Button
variant="contained"
size="small"
onClick={() => setCustomDate(dayjs().format('YYYY-MM-DD'))}
sx={{
backgroundColor: 'rgba(255, 117, 34, 0.08)',
color: '#ff5722',
textTransform: 'none',
boxShadow: 'none',
fontSize: isMobile ? 12 : 13,
'&:hover': { backgroundColor: 'rgba(255, 117, 34, 0.15)' }
}}
>
Today
</Button>
</Box>
</Box>
</Menu>
{/* المحتوى الأساسي للكارت */}
<CardContent>
<Box sx={{ display: 'flex', flexDirection: 'column', ml: 0 }}>
<Typography variant="h6" sx={{ fontSize: { xs: '16px', sm: '18px' }, fontWeight: 500 }}>
{title}
</Typography>
<Typography sx={{ fontSize: { xs: '12px', sm: '14px' }, fontWeight: 500, color: '#667085', mb: 3 }}>
{period}
</Typography>
</Box>
{/* دائرة التقدم */}
<Box sx={{ height: 120, mb: 2, position: 'relative' }}>
<Box sx={{ position: 'relative', width: '100%', maxWidth: 350, mx: 'auto' }}>
<CircularProgressbar
value={100}
circleRatio={0.5}
strokeWidth={5}
styles={buildStyles({
rotation: 0.75,
strokeLinecap: 'round',
trailColor: '#eee',
pathColor: '#eee',
})}
/>
<Box sx={{ position: 'absolute', top: -1.5, left: 0, width: '100%', height: '100%' }}>
<CircularProgressbar
value={progress}
circleRatio={0.5}
strokeWidth={6}
styles={buildStyles({
rotation: 0.75,
strokeLinecap: 'round',
trailColor: 'transparent',
pathColor: theme.palette.primary.main,
})}
/>
</Box>
{/* النص داخل الدائرة */}
<Box
sx={{
position: 'absolute',
top: '36%',
left: '50%',
transform: 'translate(-50%, -50%)',
fontSize: { xs: '20px', sm: '26px' },
fontWeight: 500,
color: theme.palette.text.primary,
}}
>
{`${progress}%`}
</Box>
</Box>
</Box>
{/* الوصف */}
<Typography
variant="body2"
sx={{
mb: 1,
mt: 10,
color: '#667085',
fontSize: { xs: '14px', sm: '16px' },
}}
>
{description}
</Typography>
</CardContent>
</ResponsiveContainer>
</Card>
);
};
export default OrderStatusCard;
@@ -0,0 +1,104 @@
import React from 'react';
import {
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Chip
} from '@mui/material';
const RecentActivity = () => {
// Sample data
const activities = [
{
id: 1,
order: 'Order #1234 completed',
delivery: 'Delivery en route',
inventory: 'Low stock on item #567'
},
{
id: 2,
order: 'Order #1235 completed',
delivery: 'Delivery delayed',
inventory: 'Low stock on item #890'
},
{
id: 3,
order: 'Order #1236 completed',
delivery: 'Delivery arrived',
inventory: 'Out of stock on item #123'
}
];
return (
<Box sx={{ width: '93.5%', p: 3, backgroundColor: "white", borderRadius: 2, }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, fontSize: '20px', mb: 2 }}>
Recent Activity
</Typography>
<TableContainer component={Paper} sx={{ boxShadow: 'none', border: '1px solid #e0e0e0', borderRadius: 2 }}>
<Table sx={{ minWidth: 650 }} aria-label="recent activity table">
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>New Orders</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Delivery Updates</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Inventory Alerts</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{activities.map((activity) => (
<TableRow key={activity.id}>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', color: '#4F5867', fontSize: '14px', fontWeight: 500 }}>
{activity.order}
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', color: '#4F5867', fontSize: '14px', fontWeight: 500 }}>
{activity.delivery}
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', color: '#4F5867', fontSize: '14px', fontWeight: 500 }}>
{activity.inventory}
</Box>
</TableCell>
<TableCell sx={{ width: '28%' }} >
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
sx={{ color: 'white', borderRadius: '8px', fontWeight: 600, fontSize: '14px', height: '40px', width: '135px', textTransform: 'none' }}
variant="contained"
size="small"
>
Record Inventory
</Button>
<Button
sx={{ fontWeight: 600, fontSize: '14px', borderRadius: '8px', height: '40px', textTransform: 'none' }}
variant="outlined"
size="small"
>
Assign Task
</Button>
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
export default RecentActivity;
@@ -0,0 +1,230 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
Box,
IconButton,
Paper,
Typography,
useTheme,
useMediaQuery,
Menu,
MenuItem,
TextField,
Button
} from '@mui/material';
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
CartesianGrid
} from 'recharts';
// import MoreVertIcon from '@mui/icons-material/MoreVert';
const StatisticsCard = ({
title = "Statistics",
subtitle = "Delivery Times",
data = [],
dataKeys = [
{ key: 'revenue', name: 'Revenue', color: '#E46A11' },
{ key: 'sales', name: 'Sales', color: '#0182FC' }
],
xDataKey = 'month',
valueFormatter = (value) => value,
timeFrame = 'month',
onTimeFrameChange,
height // 👈 جديد
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const [selectedDate, setSelectedDate] = useState('');
const [anchorEl, setAnchorEl] = useState(null);
const handleMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleDateChange = (event) => {
setSelectedDate(event.target.value);
if (onTimeFrameChange) onTimeFrameChange(timeFrame, event.target.value);
handleMenuClose();
};
const handleTodayClick = () => {
const today = new Date().toISOString().split('T')[0];
setSelectedDate(today);
if (onTimeFrameChange) onTimeFrameChange(timeFrame, today);
handleMenuClose();
};
// حساب العلامات الديناميكية للمحور الشاقولي
const ticksArray = (() => {
if (!data || data.length === 0) return [];
const maxValue = Math.max(
...data.flatMap(d => dataKeys.map(k => d[k.key] || 0))
);
const desiredTicks = 8; // عدد العلامات المطلوب
const step = Math.ceil(maxValue / desiredTicks) || 1;
const arr = [];
for (let i = 0; i <= maxValue; i += step) {
arr.push(i);
}
return arr;
})();
return (
<Box sx={{ borderRadius: 2, width: '100%' }}>
<Paper sx={{
p: { xs: 1.5, sm: 2 },
mb: { xs: 2, sm: 2 },
boxShadow: '0px 4px 5px rgba(0, 0, 0, 0.1)',
borderRadius: 2,
position: 'relative'
}}>
<IconButton
aria-label="more-actions"
sx={{
position: 'absolute',
top: { xs: 4, sm: 8 },
right: { xs: 4, sm: 8 },
color: '#667085'
}}
onClick={handleMenuOpen}
>
{/* <MoreVertIcon fontSize={isMobile ? 'small' : 'medium'} /> */}
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuItem>
<TextField
type="date"
value={selectedDate}
onChange={handleDateChange}
size="small"
sx={{ width: 150 }}
/>
</MenuItem>
<MenuItem>
<Button variant="outlined" size="small" onClick={handleTodayClick}>
Today
</Button>
</MenuItem>
</Menu>
{/* Header */}
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
flexWrap: 'wrap',
gap: 1
}}>
<Box>
<Typography variant="h6" sx={{ fontSize: { xs: '15px', sm: '18px' }, fontWeight: 500 }}>
{title}
</Typography>
<Typography sx={{ fontSize: { xs: '11px', sm: '14px' }, fontWeight: 500, color: '#667085' }}>
{subtitle}
</Typography>
</Box>
</Box>
{/* Chart */}
<ResponsiveContainer
width="100%"
height={height || (isMobile ? 350 : isTablet ? 390 : 395)} // 👈 أولوية للـ prop
>
<AreaChart data={data} margin={{
top: isMobile ? -30 : -45,
right: isMobile ? 15 : 30,
left: isMobile ? 10 : 20,
bottom: isMobile ? 10 : 20
}}>
<defs>
{dataKeys.map(({ key, color }) => (
<linearGradient key={key} id={`color-${key}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
))}
</defs>
<CartesianGrid stroke="#eee" strokeDasharray="0 0" vertical={false} />
<XAxis
dataKey={xDataKey}
axisLine={false}
tickLine={false}
tickMargin={isMobile ? 8 : 15}
tick={{
fontSize: isMobile ? 11 : 12,
angle: -25,
textAnchor: 'end'
}}
interval={0}
/>
<YAxis
ticks={ticksArray}
tickFormatter={valueFormatter}
axisLine={false}
tickLine={false}
tickMargin={isMobile ? 8 : 15}
tick={{ fontSize: isMobile ? 11 : 12 }}
/>
<Tooltip formatter={valueFormatter} />
<Legend
verticalAlign="top"
height={isMobile ? 30 : 36}
iconType="circle"
iconSize={isMobile ? 8 : 10}
wrapperStyle={{ paddingBottom: isMobile ? '15px' : '20px' }}
/>
{dataKeys.map(({ key, name, color }) => (
<Area
key={key}
name={name}
type="monotone"
dataKey={key}
stroke={color}
strokeWidth={isMobile ? 2 : 3}
fill={`url(#color-${key})`}
/>
))}
</AreaChart>
</ResponsiveContainer>
</Paper>
</Box>
);
};
StatisticsCard.propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
data: PropTypes.arrayOf(PropTypes.object),
dataKeys: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
color: PropTypes.string.isRequired
})),
xDataKey: PropTypes.string,
valueFormatter: PropTypes.func,
timeFrame: PropTypes.oneOf(['day', 'week', 'month', 'year']),
onTimeFrameChange: PropTypes.func
};
export default StatisticsCard;
+114
View File
@@ -0,0 +1,114 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import Waiter from './contcet/waiter';
import Cooker from './contcet/cooker';
import Accountant from './contcet/Accountant';
import { useRestaurant } from '../../../contexts/RestaurantContext';
const drawerWidth = 230;
const Employ = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isMdUp = useMediaQuery(theme.breakpoints.up('md'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const { restaurantId } = useRestaurant();
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
return (
<Box
sx={{
display: 'flex',
height: '100vh',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}
>
<Sidebar
open={sidebarOpen}
onClose={() => setSidebarOpen(!sidebarOpen)}
isMobile={isMobile}
drawerWidth={drawerWidth}
/>
<Box
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
width: '100%',
marginLeft: {
xs: 0,
sm: sidebarOpen ? `${drawerWidth}px` : 0,
md: 0,
},
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}
>
<KitchPlusAppBar
onDrawerToggle={() => setSidebarOpen(!sidebarOpen)}
sidebarOpen={sidebarOpen}
isMobile={isMobile}
/>
<Box
sx={{
flexGrow: 1,
width: sidebarOpen ? 'calc(100% - 40px)' : '100%',
pt: { xs: 2, md: 3 },
pl: { xs: 1, sm: 3, md: 3 },
pb: { xs: 1, md: 4 },
pr: {
xs: 2,
md: 2,
},
overflowY: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
transition: theme.transitions.create(['margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}
>
{/* المحتوى: Waiter و Cooker */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 4,
width: { xs: '90%', sm: '95%', md: '100%' },
mt: 0,
}}
>
<Waiter restaurantId={restaurantId} />
<Cooker restaurantId={restaurantId} />
<Accountant restaurantId={restaurantId} />
</Box>
</Box>
</Box>
</Box>
);
};
export default Employ;
@@ -0,0 +1,526 @@
import React, { useState, useEffect } from 'react';
import {
useMediaQuery,
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Chip,
Menu,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
import TuneIcon from '@mui/icons-material/Tune';
import AddEmployModal from './AddEmployModal';
import authService from '../../../../services/authService';
import IOSSwitch from './IOSSwitch';
import { useTheme } from '@mui/material/styles';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import EmployeeDetailsModal from './EmployeeDetailsModal';
const Accountant = ({ restaurantId }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [modalOpen, setModalOpen] = useState(false);
const [showAll, setShowAll] = useState(false);
const [switchStates, setSwitchStates] = useState({});
const [shiftFilter, setShiftFilter] = useState('all');
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
const [Accountants, setAccountants] = useState([]);
const [loading, setLoading] = useState(true);
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
const [AccountantDetails, setAccountantDetails] = useState(null);
// حالات الحذف
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [selectedAccountant, setSelectedAccountant] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
const fetchAccountants = async () => {
setLoading(true);
try {
const response = await authService.getAllAccountants(restaurantId);
if (response.accountants) {
setAccountants(response.accountants);
// تهيئة switchStates حسب قيمة active
const initialStates = {};
response.accountants.forEach(accountants => {
initialStates[accountants.id] = accountants.active === 1; // أو Boolean(waiter.active)
});
setSwitchStates(initialStates);
} else {
setAccountants([]);
}
} catch (error) {
console.error("Failed to fetch waiters:", error);
setAccountants([]);
} finally {
setLoading(false);
}
};
fetchAccountants();
}, [restaurantId]);
const handleSwitchChange = async (id, email) => {
try {
const response = await authService.toggleAccountStatus(email, "Accountant");
if (response.success) {
setSwitchStates((prev) => ({
...prev,
[id]: response.data,
}));
// alert(response.message);
} else {
alert("Failed: " + response.message);
}
} catch (error) {
alert("Error: " + error.message);
}
};
const handleOpenModal = () => setModalOpen(true);
const handleCloseModal = () => setModalOpen(false);
const handleConfirm = async (formData) => {
try {
const AccountantData = {
...formData,
restaurant_id: restaurantId,
};
const response = await authService.registerAccountant(AccountantData);
if (response.success) {
alert('Accountant registered successfully!');
setModalOpen(false);
// تحديث الحالة مباشرة بدل إعادة جلب كل البيانات
const newAccountant = response.Accountant || AccountantData;
// تأكد أن الـ API يعيد الـ Accountant المضاف، أو استخدم البيانات المحلية
setAccountants((prev) => [newAccountant, ...prev]);
} else {
if (response.errors) {
const errorsList = Object.entries(response.errors)
.map(([field, msgs]) => {
if (Array.isArray(msgs)) {
return `${field}: ${msgs.join(', ')}`;
} else {
return `${field}: ${msgs}`;
}
})
.join('\n');
alert(`Failed to register Accountant due to validation errors:\n${errorsList}`);
} else {
alert('Failed to register Accountant: ' + response.message);
}
}
} catch (error) {
alert('An error occurred: ' + error.message);
}
};
const getShiftChipProps = (shiftType) => {
switch (shiftType?.toLowerCase()) {
case 'morning':
return {
label: 'Morning',
sx: {
backgroundColor: '#E7F4EE',
color: '#0D894F',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
case 'evening':
return {
label: 'Evening',
sx: {
backgroundColor: '#FDF1E8',
color: '#E46A11',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
default:
return {
label: shiftType,
sx: {
backgroundColor: '#E0E0E0',
color: '#424242',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
}
};
const filteredAccountants =
shiftFilter === 'all'
? Accountants
: Accountants.filter((w) => w.shift_type.toLowerCase() === shiftFilter);
const displayedAccountants = showAll ? filteredAccountants : filteredAccountants.slice(0, 3);
const handleFilterClick = (event) => {
setFilterAnchorEl(event.currentTarget);
};
const handleFilterClose = () => {
setFilterAnchorEl(null);
};
const handleFilterSelect = (value) => {
setShiftFilter(value);
setFilterAnchorEl(null);
};
// فتح مودال الحذف
const handleDeleteClick = (Accountant) => {
setSelectedAccountant(Accountant);
setConfirmDeleteOpen(true);
};
// تأكيد الحذف
const handleDeleteConfirm = async () => {
if (!selectedAccountant) return;
setIsDeleting(true);
try {
const response = await authService.deleteAccountant(selectedAccountant.id);
// هنا تحقق من وجود message بدل success
if (response.message && response.message.toLowerCase().includes("successfully")) {
// alert(response.message);
setAccountants((prev) => prev.filter((w) => w.id !== selectedAccountant.id));
} else {
alert("Failed to delete Accountant: " + (response.message || "Unknown error"));
}
} catch (error) {
alert("An error occurred: " + error.message);
} finally {
setIsDeleting(false);
setConfirmDeleteOpen(false);
setSelectedAccountant(null);
}
};
const handleOpenDetails = async (id) => {
try {
const response = await authService.getAccountantById(id);
if (response.accountant) {
setAccountantDetails(response.accountant);
setDetailsModalOpen(true);
} else {
alert("Failed to fetch Accountant details");
}
} catch (error) {
alert("Error: " + error.message);
}
};
return (
<Box
sx={{
width: { xs: '100%', sm: '93.5%' },
p: { xs: 2, sm: 3 },
backgroundColor: 'white',
borderRadius: 2,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' },
mb: 2,
gap: { xs: 1, sm: 0 },
}}
>
<Typography
variant="h5"
sx={{
fontWeight: 600,
fontSize: { xs: '18px', sm: '20px' },
mb: { xs: 1, sm: 0 },
textAlign: { xs: 'center', sm: 'left' },
}}
>
Accountant
</Typography>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1,
justifyContent: { xs: 'center', sm: 'flex-start' },
width: { xs: '100%', sm: 'auto' },
}}
>
{Accountants.length > 3 && (
<Button
onClick={() => setShowAll(!showAll)}
sx={{
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
textTransform: 'none',
minWidth: 90,
}}
variant="outlined"
size="small"
>
{showAll ? 'See Less' : 'See All'}
</Button>
)}
<Button
variant="outlined"
sx={{
textTransform: 'none',
color: '#667085',
borderColor: '#e0e0e0',
backgroundColor: '#fff',
borderRadius: '8px',
height: '40px',
width: { xs: '120px', sm: '120px', md: '99px' },
fontSize: { xs: '0', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset',
}}
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
onClick={handleFilterClick}
>
{isMobile ? '' : 'Filters'}
</Button>
<Button
onClick={handleOpenModal}
sx={{
color: 'white',
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
width: { xs: '100%', sm: '135px' },
textTransform: 'none',
minWidth: { xs: 'unset', sm: '135px' },
}}
variant="contained"
size="small"
>
Add Accountant
</Button>
</Box>
</Box>
<TableContainer
component={Paper}
sx={{
boxShadow: 'none',
border: '1px solid #e0e0e0',
borderRadius: 2,
minWidth: 320,
}}
>
<Table
sx={{
minWidth: { sm: 550, md: 650 },
tableLayout: 'auto',
}}
aria-label="recent activity table"
size={isMobile ? 'small' : 'medium'}
>
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Name
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Email
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 10 } }}>
Shift Type
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
Loading...
</TableCell>
</TableRow>
) : displayedAccountants.length === 0 ? (
<TableRow>
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
No activities found.
</TableCell>
</TableRow>
) : (
displayedAccountants.map((Accountant) => (
<TableRow key={Accountant.id}>
<TableCell>
<Box
sx={{
display: "flex",
alignItems: "center",
color: "#296adaff",
fontSize: "14px",
fontWeight: 500,
pl: { xs: 1, sm: 6 },
wordBreak: "break-word",
cursor: "pointer",
"&:hover": { color: "#71a5ffff" },
}}
onClick={() => handleOpenDetails(Accountant.id)}
>
{Accountant.name}
</Box>
</TableCell>
<TableCell>
<Box
sx={{
display: 'flex',
alignItems: 'center',
color: '#4F5867',
fontSize: '14px',
fontWeight: 500,
pl: { xs: 1, sm: 3 },
wordBreak: 'break-word',
}}
>
{Accountant.email}
</Box>
</TableCell>
<TableCell>
<Box sx={{ pl: { xs: 1, sm: 8 } }}>
<Chip {...getShiftChipProps(Accountant.shift_type)} />
</Box>
</TableCell>
<TableCell sx={{ width: '28%', pl: { xs: 1, sm: 8 } }}>
<Box sx={{ display: 'flex', gap: 5, alignItems: 'center' }}>
<IOSSwitch
checked={!!switchStates[Accountant.id]}
onChange={() => handleSwitchChange(Accountant.id, Accountant.email)}
inputProps={{ 'aria-label': 'accountant switch' }}
/>
<DeleteForeverIcon
sx={{ cursor: 'pointer', color: '#e53935' }}
onClick={() => handleDeleteClick(Accountant)}
/>
</Box>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<AddEmployModal
open={modalOpen}
onClose={handleCloseModal}
onConfirm={handleConfirm}
title="Add Accountant"
buttonText="Add Accountant"
/>
<Menu
anchorEl={filterAnchorEl}
open={Boolean(filterAnchorEl)}
onClose={handleFilterClose}
PaperProps={{
sx: {
backgroundColor: 'white',
px: 1,
position: 'relative',
width: '120px',
},
}}
>
{['all', 'morning', 'evening'].map((shift) => (
<MenuItem
key={shift}
selected={shiftFilter === shift}
onClick={() => handleFilterSelect(shift)}
sx={{
color: shiftFilter === shift ? '#FF914D' : '#4F5867',
backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent',
'&:hover': {
backgroundColor: '#f0f0f0ff',
transition: 'background-color 150ms ease-in-out',
},
}}
>
{shift.charAt(0).toUpperCase() + shift.slice(1)}
</MenuItem>
))}
</Menu>
{/* مودال تأكيد الحذف */}
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete "{selectedAccountant?.name}"? This action cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteConfirm}
color="error"
variant="contained"
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</DialogActions>
</Dialog>
<EmployeeDetailsModal
open={detailsModalOpen}
onClose={() => setDetailsModalOpen(false)}
employee={AccountantDetails}
type="Accountant"
/>
</Box>
);
};
export default Accountant;
@@ -0,0 +1,330 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Typography,
Box,
Button,
useTheme,
IconButton,
InputAdornment,
MenuItem,
} from '@mui/material';
import VisibilityOutlined from '@mui/icons-material/VisibilityOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
const AddUserModal = ({
open,
onClose,
onConfirm,
title = 'Add User',
buttonText = 'Add User',
}) => {
const theme = useTheme();
const [formValues, setFormValues] = useState({
name: '',
email: '',
password: '',
password_confirmation: '',
shift_type: '',
working_hours: '',
monthly_salary: '',
});
const [errors, setErrors] = useState({});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const handleTogglePassword = () => setShowPassword((prev) => !prev);
const handleToggleConfirmPassword = () => setShowConfirmPassword((prev) => !prev);
const handleChange = (key, value) => {
setFormValues((prev) => ({ ...prev, [key]: value }));
setErrors((prev) => ({ ...prev, [key]: null })); // مسح الخطأ عند التعديل
};
const validate = () => {
let tempErrors = {};
if (!formValues.name.trim()) tempErrors.name = 'Name is required';
if (!formValues.email.trim()) tempErrors.email = 'Email is required';
else {
// Regex بسيط للتحقق من صحة الإيميل
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formValues.email))
tempErrors.email = 'Invalid email format';
}
if (!formValues.password) {
tempErrors.password = 'Password is required';
} else if (formValues.password.length < 6) {
tempErrors.password = 'Password must be at least 6 characters long';
}
if (!formValues.password_confirmation)
tempErrors.password_confirmation = 'Confirm password is required';
if (
formValues.password &&
formValues.password_confirmation &&
formValues.password !== formValues.password_confirmation
)
tempErrors.password_confirmation = 'Passwords do not match';
if (!formValues.shift_type) tempErrors.shift_type = 'Shift type is required';
if (!formValues.working_hours) tempErrors.working_hours = 'Working hours is required';
if (!formValues.monthly_salary) tempErrors.monthly_salary = 'Monthly salary is required';
setErrors(tempErrors);
return Object.keys(tempErrors).length === 0;
};
const handleSubmit = () => {
if (validate()) {
onConfirm(formValues);
// إعادة تعيين الحقول بعد النجاح
setFormValues({
name: '',
email: '',
password: '',
password_confirmation: '',
shift_type: '',
working_hours: '',
monthly_salary: '',
});
setErrors({}); // تصفير الأخطاء أيضًا
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 600, fontSize: '20px' }}>{title}</DialogTitle>
<DialogContent
dividers
sx={{
overflow: 'auto', // يسمح بالتمرير
'&::-webkit-scrollbar': {
display: 'none', // يخفي الـ scrollbar على Webkit (Chrome, Edge, Safari)
},
scrollbarWidth: 'none', // يخفي scrollbar على Firefox
msOverflowStyle: 'none', // يخفي scrollbar على IE 10+
}}
>
{/* Name */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Name
</Typography>
<TextField
placeholder="John Doe"
type="text"
variant="outlined"
fullWidth
value={formValues.name}
onChange={(e) => handleChange('name', e.target.value)}
sx={fieldStyle(theme)}
error={!!errors.name}
helperText={errors.name}
/>
</Box>
{/* Email */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Email
</Typography>
<TextField
placeholder="example@example.com"
type="email"
variant="outlined"
fullWidth
value={formValues.email}
onChange={(e) => handleChange('email', e.target.value)}
sx={fieldStyle(theme)}
error={!!errors.email}
helperText={errors.email}
/>
</Box>
{/* Password */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Password
</Typography>
<TextField
type={showPassword ? 'text' : 'password'}
placeholder="Enter password"
fullWidth
variant="outlined"
autoComplete="new-password"
value={formValues.password}
onChange={(e) => handleChange('password', e.target.value)}
sx={fieldStyle(theme)}
error={!!errors.password}
helperText={errors.password}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleTogglePassword} edge="end">
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
{/* Confirm Password */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Confirm Password
</Typography>
<TextField
type={showConfirmPassword ? 'text' : 'password'}
placeholder="Confirm password"
fullWidth
variant="outlined"
autoComplete="new-password"
value={formValues.password_confirmation}
onChange={(e) => handleChange('password_confirmation', e.target.value)}
sx={fieldStyle(theme)}
error={!!errors.password_confirmation}
helperText={errors.password_confirmation}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleToggleConfirmPassword} edge="end">
{showConfirmPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
{/* Shift Type */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Shift Type
</Typography>
<TextField
select
fullWidth
variant="outlined"
value={formValues.shift_type}
onChange={(e) => handleChange('shift_type', e.target.value)}
sx={fieldStyle(theme)}
error={!!errors.shift_type}
helperText={errors.shift_type}
placeholder="Select shift type"
>
<MenuItem value="morning">Morning</MenuItem>
<MenuItem value="evening">Evening</MenuItem>
</TextField>
</Box>
{/* Working Hours */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Working Hours
</Typography>
<TextField
placeholder="12"
type="number"
variant="outlined"
fullWidth
value={formValues.working_hours}
onChange={(e) => handleChange('working_hours', e.target.value)}
sx={fieldStyle(theme)}
error={!!errors.working_hours}
helperText={errors.working_hours}
/>
</Box>
{/* Monthly Salary */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Monthly Salary
</Typography>
<TextField
placeholder="500"
type="number"
variant="outlined"
fullWidth
value={formValues.monthly_salary}
onChange={(e) => handleChange('monthly_salary', e.target.value)}
sx={fieldStyle(theme)}
error={!!errors.monthly_salary}
helperText={errors.monthly_salary}
/>
</Box>
</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"
>
{buttonText}
</Button>
</DialogActions>
</Dialog>
);
};
const fieldStyle = (theme) => ({
'& input': { fontWeight: 500, fontSize: '15px' },
'& label': { fontWeight: 600, fontSize: '15px', color: 'black' },
'& .MuiOutlinedInput-root': {
borderRadius: '8px',
},
'& .MuiOutlinedInput-notchedOutline': {
borderColor: theme.palette.grey[400],
},
'& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: theme.palette.primary.main,
},
'& input::-ms-reveal, & input::-ms-clear': {
display: 'none',
},
});
export default AddUserModal;
@@ -0,0 +1,158 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Button,
Box,
Typography,
useTheme,
Divider
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
const ConfirmationDialog = ({ open, onClose, onConfirm }) => {
const theme = useTheme();
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
width: '525px',
height: '300px',
maxWidth: '100%',
maxHeight: '100%',
borderRadius: '12px',
overflow: 'hidden'
}
}}
>
<Box sx={{
p: 3,
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
mb: 3,
ml: -5,
}}
>
{/* الدوائر */}
<Box sx={{
position: 'relative',
width: 100,
height: 100,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{/* الدائرة الخلفية */}
<Box sx={{
position: 'absolute',
backgroundColor: '#F5F8FF',
width: '100%',
height: '100%',
borderRadius: '50%',
zIndex: 0,
}} />
{/* الدائرة الأمامية مع الأيقونة */}
<Box sx={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: '#E9EFFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}>
<img
src='/images/createProfile/Successful.png'
style={{
width: 40,
height: 40,
objectFit: 'contain',
}}
alt="Success icon"
/>
</Box>
</Box>
{/* العنوان بجانب الدائرة */}
<DialogTitle sx={{ p: 0 }}>
<Typography
variant="h4"
component="div"
sx={{
fontWeight: 600,
fontSize: '28px',
lineHeight: '1.2'
}}
>
Register successful!
</Typography>
</DialogTitle>
</Box>
<DialogContent sx={{ p: 0 }}>
<DialogContentText sx={{
textAlign: 'center',
mb: 0,
fontSize: '16px',
fontWeight: 500,
color: theme.palette.text.secondary
}}>
Congratulations! you have registered your restaurant successfully on our platform
</DialogContentText>
</DialogContent>
<DialogActions
sx={{
p: 0,
px: 3,
pt: 2,
flexDirection: 'column',
gap: 2,
}}
>
{/* الزر الأول - Filled */}
<Button
variant="contained"
fullWidth
onClick={onClose}
sx={{
fontWeight: 600,
fontSize: '16px',
height: '48px',
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.dark,
},
}}
>
Done
</Button>
</DialogActions>
</Box>
</Dialog>
);
};
export default ConfirmationDialog;
@@ -0,0 +1,66 @@
import React from "react";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Button,
Box,
} from "@mui/material";
const EmployeeDetailsModal = ({ open, onClose, employee, type }) => {
if (!employee) return null;
// دالة لتحويل الحالة الرقمية إلى نصية
const getStatusText = (active) => (active ? "Active" : "Inactive");
// عنوان المودال حسب نوع الموظف
const getTitle = () => {
switch (type?.toLowerCase()) {
case "waiter":
return "Waiter Details";
case "cooker":
return "Cooker Details";
case "accountant":
return "Accountant Details";
default:
return "Employee Details";
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{getTitle()}</DialogTitle>
<DialogContent dividers>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
{employee.name && <Typography><b>Name:</b> {employee.name}</Typography>}
{employee.email && <Typography><b>Email:</b> {employee.email}</Typography>}
{employee.shift_type && <Typography><b>Shift:</b> {employee.shift_type}</Typography>}
{employee.working_hours !== undefined && (
<Typography><b>Working Hours:</b> {employee.working_hours}</Typography>
)}
{employee.monthly_salary !== undefined && (
<Typography><b>Monthly Salary:</b> {employee.monthly_salary}</Typography>
)}
{employee.active !== undefined && (
<Typography><b>Status:</b> {getStatusText(employee.active)}</Typography>
)}
{employee.code && <Typography><b>Code:</b> {employee.code}</Typography>}
{employee.expires_at && <Typography><b>Expires At:</b> {employee.expires_at}</Typography>}
</Box>
</DialogContent>
<DialogActions>
<Button
onClick={onClose}
variant="outlined"
sx={{ textTransform: "none", minWidth: { md: 120 } }}
>
Close
</Button>
</DialogActions>
</Dialog>
);
};
export default EmployeeDetailsModal;
@@ -0,0 +1,57 @@
import { styled } from '@mui/material/styles';
import Switch from '@mui/material/Switch';
const IOSSwitch = styled((props) => (
<Switch focusVisibleClassName=".Mui-focusVisible" disableRipple {...props} />
))(({ theme }) => ({
width: 42,
height: 26,
padding: 0,
'& .MuiSwitch-switchBase': {
padding: 0,
margin: 2,
transitionDuration: '300ms',
'&.Mui-checked': {
transform: 'translateX(16px)',
color: '#fff',
'& + .MuiSwitch-track': {
backgroundColor: theme.palette.primary.main,
opacity: 1,
border: 0,
},
'&.Mui-disabled + .MuiSwitch-track': {
opacity: 0.5,
},
},
'&.Mui-focusVisible .MuiSwitch-thumb': {
color: theme.palette.primary.main,
border: '6px solid #fff',
},
'&.Mui-disabled .MuiSwitch-thumb': {
color:
theme.palette.mode === 'light'
? theme.palette.grey[100]
: theme.palette.grey[600],
},
'&.Mui-disabled + .MuiSwitch-track': {
opacity: theme.palette.mode === 'light' ? 0.7 : 0.3,
},
},
'& .MuiSwitch-thumb': {
boxSizing: 'border-box',
width: 22,
height: 22,
borderRadius: 11,
},
'& .MuiSwitch-track': {
borderRadius: 26 / 2,
backgroundColor:
theme.palette.mode === 'light' ? '#E9E9EA' : 'rgba(255,255,255,0.35)',
opacity: 1,
transition: theme.transitions.create(['background-color'], {
duration: 500,
}),
},
}));
export default IOSSwitch;
@@ -0,0 +1,526 @@
import React, { useState, useEffect } from 'react';
import {
useMediaQuery,
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Chip,
Menu,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
import TuneIcon from '@mui/icons-material/Tune';
import AddEmployModal from './AddEmployModal';
import authService from '../../../../services/authService';
import IOSSwitch from './IOSSwitch';
import { useTheme } from '@mui/material/styles';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import EmployeeDetailsModal from './EmployeeDetailsModal';
const Cooker = ({ restaurantId }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [modalOpen, setModalOpen] = useState(false);
const [showAll, setShowAll] = useState(false);
const [switchStates, setSwitchStates] = useState({});
const [shiftFilter, setShiftFilter] = useState('all');
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
const [Cookers, setCookers] = useState([]);
const [loading, setLoading] = useState(true);
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
const [CookerDetails, setCookerDetails] = useState(null);
// حالات الحذف
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [selectedCooker, setSelectedCooker] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
// استدعاء API
useEffect(() => {
const fetchCookers = async () => {
setLoading(true);
try {
const response = await authService.getAllCookers(restaurantId);
if (response.cookers) {
setCookers(response.cookers);
const initialStates = {};
response.cookers.forEach(cookers => {
initialStates[cookers.id] = cookers.active === 1; // أو Boolean(waiter.active)
});
setSwitchStates(initialStates);
} else {
setCookers([]);
}
} catch (error) {
console.error("Failed to fetch Cookers:", error);
setCookers([]);
} finally {
setLoading(false);
}
};
fetchCookers();
}, [restaurantId]);
const handleSwitchChange = async (id, email) => {
try {
const response = await authService.toggleAccountStatus(email, "Cooker");
if (response.success) {
setSwitchStates((prev) => ({
...prev,
[id]: response.data,
}));
// alert(response.message);
} else {
alert("Failed: " + response.message);
}
} catch (error) {
alert("Error: " + error.message);
}
};
const handleOpenModal = () => setModalOpen(true);
const handleCloseModal = () => setModalOpen(false);
const handleConfirm = async (formData) => {
try {
const CookerData = {
...formData,
restaurant_id: restaurantId,
};
const response = await authService.registerCooker(CookerData);
if (response.success) {
alert('Cooker registered successfully!');
setModalOpen(false);
// تحديث الحالة مباشرة بدل إعادة جلب كل البيانات
const newCooker = response.Cooker || CookerData;
// تأكد أن الـ API يعيد الـ Cooker المضاف، أو استخدم البيانات المحلية
setCookers((prev) => [newCooker, ...prev]);
} else {
if (response.errors) {
const errorsList = Object.entries(response.errors)
.map(([field, msgs]) => {
if (Array.isArray(msgs)) {
return `${field}: ${msgs.join(', ')}`;
} else {
return `${field}: ${msgs}`;
}
})
.join('\n');
alert(`Failed to register Cooker due to validation errors:\n${errorsList}`);
} else {
alert('Failed to register Cooker: ' + response.message);
}
}
} catch (error) {
alert('An error occurred: ' + error.message);
}
};
const getShiftChipProps = (shiftType) => {
switch (shiftType?.toLowerCase()) {
case 'morning':
return {
label: 'Morning',
sx: {
backgroundColor: '#E7F4EE',
color: '#0D894F',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
case 'evening':
return {
label: 'Evening',
sx: {
backgroundColor: '#FDF1E8',
color: '#E46A11',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
default:
return {
label: shiftType,
sx: {
backgroundColor: '#E0E0E0',
color: '#424242',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
}
};
const filteredCookers =
shiftFilter === 'all'
? Cookers
: Cookers.filter((w) => w.shift_type.toLowerCase() === shiftFilter);
const displayedCookers = showAll ? filteredCookers : filteredCookers.slice(0, 3);
const handleFilterClick = (event) => {
setFilterAnchorEl(event.currentTarget);
};
const handleFilterClose = () => {
setFilterAnchorEl(null);
};
const handleFilterSelect = (value) => {
setShiftFilter(value);
setFilterAnchorEl(null);
};
// فتح مودال الحذف
const handleDeleteClick = (Cooker) => {
setSelectedCooker(Cooker);
setConfirmDeleteOpen(true);
};
// تأكيد الحذف
const handleDeleteConfirm = async () => {
if (!selectedCooker) return;
setIsDeleting(true);
try {
const response = await authService.deleteCooker(selectedCooker.id);
// هنا تحقق من وجود message بدل success
if (response.message && response.message.toLowerCase().includes("successfully")) {
// alert(response.message);
setCookers((prev) => prev.filter((w) => w.id !== selectedCooker.id));
} else {
alert("Failed to delete Cooker: " + (response.message || "Unknown error"));
}
} catch (error) {
alert("An error occurred: " + error.message);
} finally {
setIsDeleting(false);
setConfirmDeleteOpen(false);
setSelectedCooker(null);
}
};
const handleOpenDetails = async (id) => {
try {
const response = await authService.getCookerById(id);
if (response.cooker) {
setCookerDetails(response.cooker);
setDetailsModalOpen(true);
} else {
alert("Failed to fetch Cooker details");
}
} catch (error) {
alert("Error: " + error.message);
}
};
return (
<Box
sx={{
width: { xs: '100%', sm: '93.5%' },
p: { xs: 2, sm: 3 },
backgroundColor: 'white',
borderRadius: 2,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' },
mb: 2,
gap: { xs: 1, sm: 0 },
}}
>
<Typography
variant="h5"
sx={{
fontWeight: 600,
fontSize: { xs: '18px', sm: '20px' },
mb: { xs: 1, sm: 0 },
textAlign: { xs: 'center', sm: 'left' },
}}
>
Cooker
</Typography>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1,
justifyContent: { xs: 'center', sm: 'flex-start' },
width: { xs: '100%', sm: 'auto' },
}}
>
{Cookers.length > 3 && (
<Button
onClick={() => setShowAll(!showAll)}
sx={{
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
textTransform: 'none',
minWidth: 90,
}}
variant="outlined"
size="small"
>
{showAll ? 'See Less' : 'See All'}
</Button>
)}
<Button
variant="outlined"
sx={{
textTransform: 'none',
color: '#667085',
borderColor: '#e0e0e0',
backgroundColor: '#fff',
borderRadius: '8px',
height: '40px',
width: { xs: '120px', sm: '120px', md: '99px' },
fontSize: { xs: '0', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset',
}}
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
onClick={handleFilterClick}
>
{isMobile ? '' : 'Filters'}
</Button>
<Button
onClick={handleOpenModal}
sx={{
color: 'white',
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
width: { xs: '100%', sm: '135px' },
textTransform: 'none',
minWidth: { xs: 'unset', sm: '135px' },
}}
variant="contained"
size="small"
>
Add Cooker
</Button>
</Box>
</Box>
<TableContainer
component={Paper}
sx={{
boxShadow: 'none',
border: '1px solid #e0e0e0',
borderRadius: 2,
minWidth: 320,
}}
>
<Table
sx={{
minWidth: { sm: 550, md: 650 },
tableLayout: 'auto',
}}
aria-label="recent activity table"
size={isMobile ? 'small' : 'medium'}
>
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Name
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Email
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 10 } }}>
Shift Type
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
Loading...
</TableCell>
</TableRow>
) : displayedCookers.length === 0 ? (
<TableRow>
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
No activities found.
</TableCell>
</TableRow>
) : (
displayedCookers.map((Cooker) => (
<TableRow key={Cooker.id}>
<TableCell>
<Box
sx={{
display: "flex",
alignItems: "center",
color: "#296adaff",
fontSize: "14px",
fontWeight: 500,
pl: { xs: 1, sm: 6 },
wordBreak: "break-word",
cursor: "pointer",
"&:hover": { color: "#71a5ffff" },
}}
onClick={() => handleOpenDetails(Cooker.id)}
>
{Cooker.name}
</Box>
</TableCell>
<TableCell>
<Box
sx={{
display: 'flex',
alignItems: 'center',
color: '#4F5867',
fontSize: '14px',
fontWeight: 500,
pl: { xs: 1, sm: 3 },
wordBreak: 'break-word',
}}
>
{Cooker.email}
</Box>
</TableCell>
<TableCell>
<Box sx={{ pl: { xs: 1, sm: 8 } }}>
<Chip {...getShiftChipProps(Cooker.shift_type)} />
</Box>
</TableCell>
<TableCell sx={{ width: '28%', pl: { xs: 1, sm: 8 } }}>
<Box sx={{ display: 'flex', gap: 5, alignItems: 'center' }}>
<IOSSwitch
checked={!!switchStates[Cooker.id]}
onChange={() => handleSwitchChange(Cooker.id, Cooker.email)}
inputProps={{ 'aria-label': 'accountant switch' }}
/>
<DeleteForeverIcon
sx={{ cursor: 'pointer', color: '#e53935' }}
onClick={() => handleDeleteClick(Cooker)}
/>
</Box>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<AddEmployModal
open={modalOpen}
onClose={handleCloseModal}
onConfirm={handleConfirm}
title="Add Cooker"
buttonText="Add Cooker"
/>
<Menu
anchorEl={filterAnchorEl}
open={Boolean(filterAnchorEl)}
onClose={handleFilterClose}
PaperProps={{
sx: {
backgroundColor: 'white',
px: 1,
position: 'relative',
width: '120px',
},
}}
>
{['all', 'morning', 'evening'].map((shift) => (
<MenuItem
key={shift}
selected={shiftFilter === shift}
onClick={() => handleFilterSelect(shift)}
sx={{
color: shiftFilter === shift ? '#FF914D' : '#4F5867',
backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent',
'&:hover': {
backgroundColor: '#f0f0f0ff',
transition: 'background-color 150ms ease-in-out',
},
}}
>
{shift.charAt(0).toUpperCase() + shift.slice(1)}
</MenuItem>
))}
</Menu>
{/* مودال تأكيد الحذف */}
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete "{selectedCooker?.name}"? This action cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteConfirm}
color="error"
variant="contained"
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</DialogActions>
</Dialog>
<EmployeeDetailsModal
open={detailsModalOpen}
onClose={() => setDetailsModalOpen(false)}
employee={CookerDetails}
type="Cooker"
/>
</Box>
);
};
export default Cooker;
@@ -0,0 +1,519 @@
import React, { useState, useEffect } from 'react';
import {
useMediaQuery,
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Chip,
Menu,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
import TuneIcon from '@mui/icons-material/Tune';
import AddEmployModal from './AddEmployModal';
import authService from '../../../../services/authService';
import IOSSwitch from './IOSSwitch';
import { useTheme } from '@mui/material/styles';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import EmployeeDetailsModal from './EmployeeDetailsModal';
const Waiter = ({ restaurantId }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [modalOpen, setModalOpen] = useState(false);
const [showAll, setShowAll] = useState(false);
const [switchStates, setSwitchStates] = useState({});
const [shiftFilter, setShiftFilter] = useState('all');
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
const [waiters, setWaiters] = useState([]);
const [loading, setLoading] = useState(true);
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
const [waiterDetails, setWaiterDetails] = useState(null);
// حالات الحذف
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [selectedWaiter, setSelectedWaiter] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
// استدعاء API
useEffect(() => {
const fetchWaiters = async () => {
setLoading(true);
try {
const response = await authService.getAllWaiters(restaurantId);
if (response.waiters) {
setWaiters(response.waiters);
// تهيئة switchStates حسب قيمة active
const initialStates = {};
response.waiters.forEach(waiter => {
initialStates[waiter.id] = waiter.active === 1; // أو Boolean(waiter.active)
});
setSwitchStates(initialStates);
} else {
setWaiters([]);
}
} catch (error) {
console.error("Failed to fetch waiters:", error);
setWaiters([]);
} finally {
setLoading(false);
}
};
fetchWaiters();
}, [restaurantId]);
const handleSwitchChange = async (id, email) => {
try {
const response = await authService.toggleAccountStatus(email, "Waiter");
if (response.success) {
setSwitchStates((prev) => ({
...prev,
[id]: response.data,
}));
// alert(response.message);
} else {
alert("Failed: " + response.message);
}
} catch (error) {
alert("Error: " + error.message);
}
};
const handleOpenModal = () => setModalOpen(true);
const handleCloseModal = () => setModalOpen(false);
const handleConfirm = async (formData) => {
try {
const waiterData = {
...formData,
restaurant_id: restaurantId,
};
const response = await authService.registerWaiter(waiterData);
if (response.success) {
alert('Waiter registered successfully!');
setModalOpen(false);
// إعادة التحميل بعد الإضافة
const updated = await authService.getAllWaiters(restaurantId);
if (updated.waiters) setWaiters(updated.waiters);
} else {
if (response.errors) {
const errorsList = Object.entries(response.errors)
.map(([field, msgs]) => `${field}: ${msgs.join(', ')}`)
.join('\n');
alert(`Failed to register waiter due to validation errors:\n${errorsList}`);
} else {
alert('Failed to register waiter: ' + response.message);
}
}
} catch (error) {
alert('An error occurred: ' + error.message);
}
};
const getShiftChipProps = (shiftType) => {
switch (shiftType?.toLowerCase()) {
case 'morning':
return {
label: 'Morning',
sx: {
backgroundColor: '#E7F4EE',
color: '#0D894F',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
case 'evening':
return {
label: 'Evening',
sx: {
backgroundColor: '#FDF1E8',
color: '#E46A11',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
default:
return {
label: shiftType,
sx: {
backgroundColor: '#E0E0E0',
color: '#424242',
fontWeight: 600,
fontSize: '14px',
minWidth: 80,
textTransform: 'capitalize',
},
};
}
};
const filteredWaiters =
shiftFilter === 'all'
? waiters
: waiters.filter((w) => w.shift_type.toLowerCase() === shiftFilter);
const displayedWaiters = showAll ? filteredWaiters : filteredWaiters.slice(0, 3);
const handleFilterClick = (event) => {
setFilterAnchorEl(event.currentTarget);
};
const handleFilterClose = () => {
setFilterAnchorEl(null);
};
const handleFilterSelect = (value) => {
setShiftFilter(value);
setFilterAnchorEl(null);
};
// فتح مودال الحذف
const handleDeleteClick = (waiter) => {
setSelectedWaiter(waiter);
setConfirmDeleteOpen(true);
};
// تأكيد الحذف
const handleDeleteConfirm = async () => {
if (!selectedWaiter) return;
setIsDeleting(true);
try {
const response = await authService.deleteWaiter(selectedWaiter.id);
// هنا تحقق من وجود message بدل success
if (response.message && response.message.toLowerCase().includes("successfully")) {
// alert(response.message);
setWaiters((prev) => prev.filter((w) => w.id !== selectedWaiter.id));
} else {
alert("Failed to delete waiter: " + (response.message || "Unknown error"));
}
} catch (error) {
alert("An error occurred: " + error.message);
} finally {
setIsDeleting(false);
setConfirmDeleteOpen(false);
setSelectedWaiter(null);
}
};
const handleOpenDetails = async (id) => {
try {
const response = await authService.getWaiterById(id);
if (response.waiter) {
setWaiterDetails(response.waiter);
setDetailsModalOpen(true);
} else {
alert("Failed to fetch waiter details");
}
} catch (error) {
alert("Error: " + error.message);
}
};
return (
<Box
sx={{
width: { xs: '100%', sm: '93.5%' },
p: { xs: 2, sm: 3 },
backgroundColor: 'white',
borderRadius: 2,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' },
mb: 2,
gap: { xs: 1, sm: 0 },
}}
>
<Typography
variant="h5"
sx={{
fontWeight: 600,
fontSize: { xs: '18px', sm: '20px' },
mb: { xs: 1, sm: 0 },
textAlign: { xs: 'center', sm: 'left' },
}}
>
Waiter
</Typography>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1,
justifyContent: { xs: 'center', sm: 'flex-start' },
width: { xs: '100%', sm: 'auto' },
}}
>
{waiters.length > 3 && (
<Button
onClick={() => setShowAll(!showAll)}
sx={{
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
textTransform: 'none',
minWidth: 90,
}}
variant="outlined"
size="small"
>
{showAll ? 'See Less' : 'See All'}
</Button>
)}
<Button
variant="outlined"
sx={{
textTransform: 'none',
color: '#667085',
borderColor: '#e0e0e0',
backgroundColor: '#fff',
borderRadius: '8px',
height: '40px',
width: { xs: '120px', sm: '120px', md: '99px' },
fontSize: { xs: '0', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset',
}}
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
onClick={handleFilterClick}
>
{isMobile ? '' : 'Filters'}
</Button>
<Button
onClick={handleOpenModal}
sx={{
color: 'white',
borderRadius: '8px',
fontWeight: 600,
fontSize: '14px',
height: '40px',
width: { xs: '100%', sm: '135px' },
textTransform: 'none',
minWidth: { xs: 'unset', sm: '135px' },
}}
variant="contained"
size="small"
>
Add Waiter
</Button>
</Box>
</Box>
<TableContainer
component={Paper}
sx={{
boxShadow: 'none',
border: '1px solid #e0e0e0',
borderRadius: 2,
minWidth: 320,
}}
>
<Table
sx={{
minWidth: { sm: 550, md: 650 },
tableLayout: 'auto',
}}
aria-label="recent activity table"
size={isMobile ? 'small' : 'medium'}
>
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Name
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Email
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 10 } }}>
Shift Type
</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
Loading...
</TableCell>
</TableRow>
) : displayedWaiters.length === 0 ? (
<TableRow>
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
No activities found.
</TableCell>
</TableRow>
) : (
displayedWaiters.map((waiter) => (
<TableRow key={waiter.id}>
<TableCell>
<Box
sx={{
display: "flex",
alignItems: "center",
color: "#296adaff",
fontSize: "14px",
fontWeight: 500,
pl: { xs: 1, sm: 6 },
wordBreak: "break-word",
cursor: "pointer",
"&:hover": { color: "#71a5ffff" },
}}
onClick={() => handleOpenDetails(waiter.id)}
>
{waiter.name}
</Box>
</TableCell>
<TableCell>
<Box
sx={{
display: 'flex',
alignItems: 'center',
color: '#4F5867',
fontSize: '14px',
fontWeight: 500,
pl: { xs: 1, sm: 3 },
wordBreak: 'break-word',
}}
>
{waiter.email}
</Box>
</TableCell>
<TableCell>
<Box sx={{ pl: { xs: 1, sm: 8 } }}>
<Chip {...getShiftChipProps(waiter.shift_type)} />
</Box>
</TableCell>
<TableCell sx={{ width: '28%', pl: { xs: 1, sm: 8 } }}>
<Box sx={{ display: 'flex', gap: 5, alignItems: 'center' }}>
<IOSSwitch
checked={!!switchStates[waiter.id]}
onChange={() => handleSwitchChange(waiter.id, waiter.email)}
inputProps={{ 'aria-label': 'accountant switch' }}
/>
<DeleteForeverIcon
sx={{ cursor: 'pointer', color: '#e53935' }}
onClick={() => handleDeleteClick(waiter)}
/>
</Box>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<AddEmployModal
open={modalOpen}
onClose={handleCloseModal}
onConfirm={handleConfirm}
title="Add Waiter"
buttonText="Add Waiter"
/>
<Menu
anchorEl={filterAnchorEl}
open={Boolean(filterAnchorEl)}
onClose={handleFilterClose}
PaperProps={{
sx: {
backgroundColor: 'white',
px: 1,
position: 'relative',
width: '120px',
},
}}
>
{['all', 'morning', 'evening'].map((shift) => (
<MenuItem
key={shift}
selected={shiftFilter === shift}
onClick={() => handleFilterSelect(shift)}
sx={{
color: shiftFilter === shift ? '#FF914D' : '#4F5867',
backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent',
'&:hover': {
backgroundColor: '#f0f0f0ff',
transition: 'background-color 150ms ease-in-out',
},
}}
>
{shift.charAt(0).toUpperCase() + shift.slice(1)}
</MenuItem>
))}
</Menu>
{/* مودال تأكيد الحذف */}
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete "{selectedWaiter?.name}"? This action cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteConfirm}
color="error"
variant="contained"
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</DialogActions>
</Dialog>
<EmployeeDetailsModal
open={detailsModalOpen}
onClose={() => setDetailsModalOpen(false)}
employee={waiterDetails} // بدل waiter
type="waiter"
/>
</Box>
);
};
export default Waiter;
+142
View File
@@ -0,0 +1,142 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import ProductEntry from './contect/ProductEntry';
import TableView from './contect/TableView';
const drawerWidth = 230;
const initialProducts = [
{
product: 'Apple Watch',
sales: 150,
amount: 45000,
price: 299,
status: 'Published',
expiration: '30 Days Left',
},
{
product: 'Samsung Galaxy',
sales: 90,
amount: 36000,
price: 400,
status: 'Low Stock',
expiration: '12 Days Left',
},
{
product: 'Sony Headphones',
sales: 60,
amount: 18000,
price: 299,
status: 'Draft',
expiration: '5 Days Left',
},
];
const Inventory = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const [products, setProducts] = useState(initialProducts);
const [showProductEntry, setShowProductEntry] = useState(true);
const addProduct = (newProduct) => {
const formattedProduct = {
product: newProduct.fullName || 'Unnamed Product',
sales: newProduct.stocksLevel ? Number(newProduct.stocksLevel) : 0,
amount: newProduct.productPrice
? Number(newProduct.productPrice) * (newProduct.stocksLevel || 1)
: 0,
price: newProduct.productPrice ? Number(newProduct.productPrice) : 0,
status: newProduct.consumptionStatus ? 'Published' : 'Draft',
expiration: newProduct.expirationDate
? `${Math.ceil(
(new Date(newProduct.expirationDate) - new Date()) /
(1000 * 60 * 60 * 24)
)} Days Left`
: 'N/A',
};
setProducts((prev) => [...prev, formattedProduct]);
setShowProductEntry(false);
};
const handleAddNewProduct = () => {
setShowProductEntry(true);
};
const handleShowTable = () => {
setShowProductEntry(false);
};
return (
<Box
sx={{
display: 'flex',
height: '100vh',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}
>
<Sidebar
open={sidebarOpen}
onClose={() => setSidebarOpen(!sidebarOpen)}
isMobile={isMobile}
drawerWidth={drawerWidth}
/>
<Box
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
width: '100%',
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={() => setSidebarOpen(!sidebarOpen)}
sidebarOpen={sidebarOpen}
isMobile={isMobile}
/>
<Box
sx={{
flexGrow: 1,
width: sidebarOpen ? 'calc(100% - 40px)' : '100%',
pt: { xs: 2, sm: 3 },
overflowY: 'auto',
pl: { xs: 2, sm: 3 },
pb: { xs: 2, sm: 4 },
pr: { xs: 2, sm: 3 },
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
transition: theme.transitions.create(['margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}
>
{showProductEntry ? (
<ProductEntry onAdd={addProduct} onShowTable={handleShowTable} />
) : (
<TableView data={products} onAddNewProduct={handleAddNewProduct} />
)}
</Box>
</Box>
</Box>
);
};
export default Inventory;
@@ -0,0 +1,41 @@
import React, { useState } from 'react';
import TableView from './TableView';
const initialProducts = [
{
product: 'Apple Watch',
sales: 150,
amount: 45000,
price: 299,
status: 'Published',
expiration: '30 Days Left',
},
{
product: 'Samsung Galaxy',
sales: 90,
amount: 36000,
price: 400,
status: 'Low Stock',
expiration: '12 Days Left',
},
{
product: 'Sony Headphones',
sales: 60,
amount: 18000,
price: 299,
status: 'Draft',
expiration: '5 Days Left',
},
];
const InventoryTablePage = () => {
const [products, setProducts] = useState(initialProducts);
const handleAddNewProduct = () => {
alert('Navigate to Add Product page or open form here');
};
return <TableView data={products} onAddNewProduct={handleAddNewProduct} />;
};
export default InventoryTablePage;
@@ -0,0 +1,101 @@
import React from 'react';
import {
Box,
Button,
Card,
CardContent,
Typography,
useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import AddIcon from '@mui/icons-material/Add';
const NoProdectDash = () => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<>
{/* Header Section */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
mb: 3,
mt:10,
pl: 1,
pr: { xs: 1, sm: 3 },
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
gap: { xs: 2, sm: 0 }
}}
>
</Box>
{/* Empty State Content */}
<Card
sx={{
boxShadow: 'none',
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
backgroundColor: 'transparent'
}}
>
<CardContent>
{/* Image Placeholder */}
<Box
component="img"
src="/images/compeoter.png"
alt="No products available"
sx={{
width: isSmallScreen ? 150 : 200,
height: 'auto',
mb: 1,
opacity: 0.8
}}
/>
{/* Title with colored "Sorry!" */}
<Typography
variant="h5"
sx={{
fontWeight: 400,
fontSize: { xs: '14px', md: '18px' },
mb: 2,
color: '#5F6868',
whiteSpace: 'pre-line' // هذا يسمح بكسر السطر عند المسافات
}}
>
<Box component="span" sx={{ fontWeight: 600, fontSize: '18px' }}>Sorry!</Box>{' '}
There is no campaign available.
Please initiate the {'\n'}creation of a new campaign.
</Typography>
{/* Action Button */}
<Button
variant="contained"
startIcon={<AddIcon />}
sx={{
textTransform: 'none',
px: 4,
py: 1.5,
fontSize: '13px',
fontWeight: 500,
borderRadius: 2,
color:'white',
width:179
}}
>
Add a Product
</Button>
</CardContent>
</Card>
</>
);
};
export default NoProdectDash;
@@ -0,0 +1,434 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
TextField,
Button,
styled,
useTheme,
MenuItem,
Radio,
RadioGroup,
FormControlLabel,
Radio as MuiRadio
} from '@mui/material';
import axios from 'axios';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import TableView from './TableView';
const ProductEntry = ({ onAdd, onShowTable }) => {
const theme = useTheme();
const [profileImage, setProfileImage] = useState(null);
const [fullName, setFullName] = useState('');
const [username, setUsername] = useState('');
const [bio, setBio] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [selectedBranch, setSelectedBranch] = useState('default');
const [branches, setBranches] = useState([
{ label: 'Damascus Branch', value: 'damascus' },
{ label: 'Aleppo Branch', value: 'aleppo' },
{ label: 'Homs Branch', value: 'homs' }
]);
const [expirationDate, setExpirationDate] = useState(null);
const [consumptionStatus, setConsumptionStatus] = useState(false);
const [showTable, setShowTable] = useState(false);
const [products, setProducts] = useState([]);
// حالات جديدة للحفظ
const [stocksLevel, setStocksLevel] = useState('');
const [productCategory, setProductCategory] = useState('default');
const [supplierName, setSupplierName] = useState('');
const [supplierNumber, setSupplierNumber] = useState('');
const [productPrice, setProductPrice] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [productCost, setProductCost] = useState('');
// قائمة فئات المنتجات
const productCategories = [
{ label: 'Food', value: 'food' },
{ label: 'Beverages', value: 'beverages' },
{ label: 'Electronics', value: 'electronics' },
{ label: 'Clothing', value: 'clothing' },
];
const handleShowTable = () => {
setShowTable(true);
};
useEffect(() => {
axios.get('https://mocki.io/v1/0b9d4121-d7e7-42f4-b1d5-b14f82e35b69')
.then((response) => {
setBranches(response.data);
})
.catch((error) => {
console.error('Error fetching branches:', error);
});
const savedData = localStorage.getItem('productEntryData');
if (savedData) {
const data = JSON.parse(savedData);
setFullName(data.fullName || '');
setUsername(data.username || '');
setEmail(data.email || '');
setSelectedBranch(data.selectedBranch || 'default');
setExpirationDate(data.expirationDate ? new Date(data.expirationDate) : null);
setConsumptionStatus(data.consumptionStatus || false);
setStocksLevel(data.stocksLevel || '');
setProductCategory(data.productCategory || 'default');
setSupplierName(data.supplierName || '');
setSupplierNumber(data.supplierNumber || '');
setProductPrice(data.productPrice || '');
setPhoneNumber(data.phoneNumber || '');
}
}, []);
useEffect(() => {
const dataToSave = {
fullName,
username,
email,
selectedBranch,
expirationDate: expirationDate ? expirationDate.toISOString() : null,
consumptionStatus,
stocksLevel,
productCategory,
supplierName,
supplierNumber,
productPrice,
phoneNumber,
};
localStorage.setItem('productEntryData', JSON.stringify(dataToSave));
}, [
fullName,
username,
email,
selectedBranch,
expirationDate,
consumptionStatus,
stocksLevel,
productCategory,
supplierName,
supplierNumber,
productPrice,
phoneNumber,
]);
const handleImageUpload = (event) => {
if (event.target.files && event.target.files[0]) {
setProfileImage(URL.createObjectURL(event.target.files[0]));
}
};
const Input = styled('input')({
display: 'none',
});
const customInputStyle = {
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
},
};
const renderField = (label, value, onChange, type = 'text', placeholder = '') => (
<Box
sx={{
flex: 1,
minWidth: { xs: '100%', sm: '45%' },
display: 'flex',
flexDirection: 'column',
gap: 1,
}}
>
<Typography variant="body2" sx={{ fontWeight: '500', fontSize: '16px' }}>
{label}
</Typography>
<TextField
placeholder={placeholder}
type={type}
value={value}
onChange={onChange}
variant="outlined"
fullWidth
sx={customInputStyle}
/>
</Box>
);
const renderSelectField = (label, value, onChange, options = []) => (
<Box
sx={{
flex: 1,
minWidth: { xs: '100%', sm: '45%' },
display: 'flex',
flexDirection: 'column',
gap: 1,
}}
>
<Typography variant="body2" sx={{ fontWeight: '500', fontSize: '16px' }}>
{label}
</Typography>
<TextField
select
value={value}
onChange={onChange}
fullWidth
sx={customInputStyle}
>
<MenuItem value="default" disabled sx={{ color: '#9e9e9e' }}>
Select {label}...
</MenuItem>
{options.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
</Box>
);
const handleSave = () => {
// احصل على أكبر رقم منتج موجود في القائمة
const maxProductNumber = products.length > 0
? Math.max(...products.map(p => Number(p.username) || 0))
: 0;
const newProductNumber = maxProductNumber + 1;
const newProduct = {
fullName,
username: newProductNumber.toString(), // هنا رقم المنتج يتزاد تلقائياً
email,
selectedBranch,
expirationDate: expirationDate ? expirationDate.toISOString().split('T')[0] : '',
consumptionStatus,
stocksLevel,
productCategory,
supplierName,
supplierNumber,
productPrice,
phoneNumber,
productCost,
};
// أضف المنتج الجديد لقائمة المنتجات
setProducts(prev => [...prev, newProduct]);
// استدعي دالة onAdd إن كانت موجودة
if (onAdd) {
onAdd(newProduct);
}
// إعادة تعيين الحقول (يمكنك إزالة إعادة تعيين username لأنه أصبح تلقائي)
setFullName('');
setEmail('');
setSelectedBranch('default');
setExpirationDate(null);
setConsumptionStatus(false);
setStocksLevel('');
setProductCategory('default');
setSupplierName('');
setSupplierNumber('');
setProductPrice('');
setPhoneNumber('');
setProductCost('');
};
return (
<Box
sx={{
p: { xs: 2, sm: 3 },
backgroundColor: '#FFFFFF',
maxWidth: { xs: '90%', md: '100%' },
borderRadius: 2,
}}
>
{/* الحقول النصية */}
<Box sx={{ mb: 3 }}>
{/* السطر الأول: Product Name + Barcode Maker */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
{renderField('Product Name', fullName, (e) => setFullName(e.target.value), 'text', 'Pizza')}
{renderField('Barcode Maker', email, (e) => setEmail(e.target.value), 'text', '12344')}
</Box>
{/* السطر الثاني: Branch Select + Product Number */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
{renderField('Product Number', username, (e) => setUsername(e.target.value), 'text', '#123')}
{renderSelectField('Branch Select', selectedBranch, (e) => setSelectedBranch(e.target.value), branches)}
</Box>
{/* السطر الثالث: Expiration Date + Consumption Status */}
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
mb: 2,
alignItems: 'center',
}}
>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Box
sx={{
flex: 1,
minWidth: { xs: '100%', sm: '45%' },
display: 'flex',
flexDirection: 'column',
gap: 1,
}}
>
<Typography
variant="body2"
sx={{ fontWeight: '500', fontSize: '16px', mb: 1 }}
>
Expiration Date
</Typography>
<DatePicker
value={expirationDate}
onChange={(newValue) => setExpirationDate(newValue)}
renderInput={(params) => (
<TextField
{...params}
fullWidth
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: '2px !important',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
'& fieldset': {
borderRadius: '20px !important',
},
flex: 1,
minWidth: { xs: '100%', sm: '45%' },
display: 'flex',
flexDirection: 'column',
gap: 1,
}}
/>
)}
/>
</Box>
</LocalizationProvider>
<Box sx={{ flex: 1, minWidth: { xs: '100%', sm: '45%' } }}>
<Typography
variant="body2"
sx={{ fontWeight: '500', fontSize: '16px', mb: 1 }}
>
Consumption Status
</Typography>
<RadioGroup
row
aria-label="consumption-status"
name="row-radio-buttons-group"
value={consumptionStatus ? 'yes' : 'no'}
onChange={(e) => setConsumptionStatus(e.target.value === 'yes')}
>
<FormControlLabel
value="yes"
control={<MuiRadio />}
label="Yes"
/>
<FormControlLabel
value="no"
control={<MuiRadio />}
label="No"
/>
</RadioGroup>
</Box>
</Box>
{/* السطر الرابع: Stocks Level + Product Category */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
{renderField('Stocks Level', stocksLevel, (e) => setStocksLevel(e.target.value), 'number', 'Enter level')}
{renderSelectField('Product Category', productCategory, (e) => setProductCategory(e.target.value), productCategories)}
</Box>
{/* السطر الخامس: Supplier Name + Supplier Number */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
{renderField('Supplier Name', supplierName, (e) => setSupplierName(e.target.value), 'text', 'Supplier XYZ')}
{renderField('Supplier Number', supplierNumber, (e) => setSupplierNumber(e.target.value), 'text', '0900-78601')}
</Box>
{/* السطر السادس: Product Price + Phone Number */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 3 }}>
{renderField('Product Price', productPrice, (e) => setProductPrice(e.target.value), 'number', '$200')}
{renderField('Product Cost', productCost, (e) => setProductCost(e.target.value), 'number', '$230')}
</Box>
{/* زر الحفظ */}
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
mb: 2,
gap:2
}}
>
<Button
variant="contained"
onClick={handleSave}
sx={{
width: '150px',
height: '50px',
backgroundColor: theme.palette.primary.main,
color: 'white',
borderRadius: '10px',
fontWeight: 'bold',
fontSize: '16px',
textTransform: 'none',
'&:hover': {
backgroundColor: theme.palette.primary.dark,
},
}}
>
Save Product
</Button>
<Button
variant="outlined"
onClick={onShowTable}
sx={{
width: '150px',
height: '50px',
borderRadius: '10px',
fontWeight: 'bold',
fontSize: '16px',
textTransform: 'none',
color: theme.palette.primary.main,
borderColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: '#FFECE0',
color: theme.palette.primary.main,
borderColor: '#FFECE0',
},
}}
>
Show Table
</Button>
</Box>
</Box>
</Box>
);
};
export default ProductEntry;
@@ -0,0 +1,498 @@
import React, { useState } from 'react';
import { useTheme } from '@mui/material/styles';
import {
useMediaQuery,
Box,
Typography,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Chip,
Pagination,
Button,
Avatar,
TableContainer,
TextField,
IconButton,
} from '@mui/material';
import AssignmentIcon from '@mui/icons-material/Assignment';
import DriveFileRenameOutlineSharpIcon from '@mui/icons-material/DriveFileRenameOutlineSharp';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import CloseIcon from '@mui/icons-material/Close';
import { green } from '@mui/material/colors';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
import TuneIcon from '@mui/icons-material/Tune';
const TableView = ({ data = [], onAddNewProduct }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const itemsPerPage = 5;
const [currentPage, setCurrentPage] = useState(1);
const [showSearch, setShowSearch] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
// فلترة البيانات بناءً على نص البحث (غير حساس لحالة الأحرف)
const filteredData = data.filter((item) =>
item.product.toLowerCase().includes(searchTerm.toLowerCase())
);
const pageCount = Math.ceil(filteredData.length / itemsPerPage);
const paginatedData = filteredData.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const handleSearchChange = (e) => {
setSearchTerm(e.target.value);
setCurrentPage(1); // العودة للصفحة الأولى عند تغيير البحث
};
const handleExportPDF = () => {
const doc = new jsPDF();
doc.setFontSize(14);
doc.text('Top Selling Products', 14, 20);
const tableColumn = ['No.', 'Product', 'Sales', 'Amount', 'Price', 'Expiration', 'Status'];
const tableRows = data.map((row, i) => ([
i + 1,
row.product,
row.sales,
`$${row.amount}`,
`$${row.price}`,
row.expiration,
row.status
]));
autoTable(doc, {
startY: 30,
head: [tableColumn],
body: tableRows,
styles: { fontSize: 10 },
headStyles: { fillColor: [240, 240, 240] }
});
doc.save('top_selling_products.pdf');
};
const handleExportSpreadsheet = () => {
const wsData = [
['No.', 'Product', 'Sales', 'Amount', 'Price', 'Expiration', 'Status'],
...data.map((row, i) => [
i + 1,
row.product,
row.sales,
`$${row.amount}`,
`$${row.price}`,
row.expiration,
row.status
])
];
const worksheet = XLSX.utils.aoa_to_sheet(wsData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Top Selling");
const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([wbout], { type: 'application/octet-stream' });
saveAs(blob, 'top_selling_products.xlsx');
};
return (
<Paper
sx={{
backgroundColor: '#FFFFFF',
maxWidth: { xs: '90%', md: '100%' },
borderRadius: 2,
height: { xs: 'auto', md: '100vh' },
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
boxShadow: theme.shadows[1],
}}
>
{/* Header */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
mb: 3,
pl: { xs: 1, sm: 3 },
pt: 2,
pr: { xs: 1, sm: 3 },
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
gap: { xs: 2, sm: 0 }
}}
>
<Typography variant="h6" sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
Table View
</Typography>
<Box
display="flex"
gap={1}
flexWrap={{ xs: 'wrap', sm: 'nowrap' }}
width={{ xs: '100%', sm: 'auto' }}
justifyContent={{ xs: 'space-between', sm: 'flex-end' }}
>
{/* زر إضافة منتج جديد */}
<Button
variant="contained"
color="primary"
onClick={onAddNewProduct}
sx={{
textTransform: 'none',
borderRadius: '8px',
height: '40px',
width: { xs: '100%', sm: 'auto' },
fontSize: { xs: '12px', sm: '14px' },
fontWeight: 600,
}}
>
Add New Product
</Button>
{/* زر تصدير جدول (Spreadsheet) */}
<Button
variant="contained"
sx={{
textTransform: 'none',
color: 'white',
backgroundColor: '#5F6868',
boxShadow: 'none',
borderRadius: '8px',
height: '40px',
width: { xs: '48%', sm: '140px', md: '166px' },
fontSize: { xs: '12px', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset',
}}
onClick={handleExportSpreadsheet}
>
{isMobile ? 'Export' : 'Export Spreadsheet'}
</Button>
{/* زر PDF */}
<Button
variant="contained"
sx={{
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
boxShadow: 'none',
borderRadius: '8px',
height: '40px',
width: { xs: '48%', sm: '90px', md: '105px' },
fontSize: { xs: '12px', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset',
}}
onClick={handleExportPDF}
>
Export PDF
</Button>
{/* زر الفلاتر */}
<Button
variant="outlined"
sx={{
textTransform: 'none',
color: '#667085',
borderColor: '#e0e0e0',
backgroundColor: '#fff',
borderRadius: '8px',
height: '40px',
width: { xs: '100%', sm: '120px', md: '99px' },
fontSize: { xs: '12px', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset',
}}
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
>
{isMobile ? '' : 'Filters'}
</Button>
</Box>
</Box>
{/* جدول البيانات */}
<TableContainer
sx={{
flexGrow: 1,
overflowX: 'auto',
maxHeight: { xs: '60vh', md: 'calc(100vh - 160px)' },
'&::-webkit-scrollbar': { height: 4 },
}}
>
<Table
size={isMobile ? 'small' : 'medium'}
sx={{
minWidth: 750,
'& .MuiTableCell-root': {
borderBottom: '1px solid #e0e0e0',
py: { xs: 0.5, sm: 1.5 },
},
'& thead .MuiTableCell-root': {
borderBottom: 'none',
fontWeight: 600,
fontSize: { xs: '11px', sm: '14px' },
px: { xs: 0.5, sm: 2 },
},
}}
>
<TableHead sx={{ backgroundColor: '#F0F1F3' }}>
<TableRow sx={{ height: { xs: '6vh', sm: '10vh' } }}>
<TableCell width="6%">No.</TableCell>
<TableCell width="20%" sx={{ position: 'relative' }}>
<Box
sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'pointer', userSelect: 'none' }}
onClick={() => setShowSearch(prev => !prev)}
>
<Typography>Product Name</Typography>
<ArrowDropDownIcon
fontSize="small"
sx={{
transform: showSearch ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.3s ease',
}}
/>
</Box>
{showSearch && (
<TextField
size="small"
variant="outlined"
placeholder="Search product..."
value={searchTerm}
onChange={handleSearchChange}
sx={{ mt: 1, width: '100%' }}
InputProps={{
endAdornment: searchTerm && (
<IconButton
size="small"
onClick={() => setSearchTerm('')}
aria-label="Clear search"
>
<CloseIcon fontSize="small" />
</IconButton>
),
}}
/>
)}
</TableCell>
{!isMobile && <TableCell width="10%">Barcode</TableCell>}
<TableCell width="10%">Branch</TableCell>
{!isMobile && <TableCell width="10%">Expiration</TableCell>}
{!isMobile && <TableCell width="15%">Stock Quantity </TableCell>}
<TableCell width="15%">Consumption Status</TableCell>
<TableCell width="10%" align="center">
Action
</TableCell>
</TableRow>
</TableHead>
<TableBody
sx={{
'& .MuiTableCell-root': {
px: { xs: 2, sm: 3 },
},
}}
>
{paginatedData.length > 0 ? (
paginatedData.map((row, i) => (
<TableRow key={i} sx={{ height: { xs: '8vh', sm: '13vh' } }}>
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
{(currentPage - 1) * itemsPerPage + i + 1}
</TableCell>
<TableCell>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: { xs: 1, sm: 2 },
}}
>
<Avatar
sx={{
bgcolor: green[50],
width: { xs: 28, sm: 40 },
height: { xs: 28, sm: 40 },
}}
variant="rounded"
>
<AssignmentIcon
fontSize={isMobile ? 'small' : 'medium'}
/>
</Avatar>
<Box
sx={{
fontSize: { xs: '11px', sm: '14px' },
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: { xs: '80px', sm: '200px' },
}}
>
{row.product}
</Box>
</Box>
</TableCell>
{!isMobile && (
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
{row.sales}
</TableCell>
)}
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
${row.amount.toLocaleString()}
</TableCell>
{!isMobile && (
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
${row.price}
</TableCell>
)}
{!isMobile && (
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
{row.expiration}
</TableCell>
)}
<TableCell>
<Chip
label={isMobile ? row.status.substring(0, 3) : row.status}
size={isMobile ? 'small' : 'medium'}
sx={{
fontSize: { xs: '10px', sm: '14px' },
fontWeight: 600,
backgroundColor:
row.status === 'Published'
? '#E7F4EE'
: row.status === 'Low Stock'
? '#FDF1E8'
: row.status === 'Out of Stock'
? '#FFCDD2'
: '#E0E0E0',
color:
row.status === 'Published'
? '#0D894F'
: row.status === 'Low Stock'
? '#E46A11'
: row.status === 'Out of Stock'
? '#C62828'
: '#424242',
minWidth: { xs: '50px', sm: '100px' },
}}
/>
</TableCell>
<TableCell align="center">
<Button
variant="text"
size="small"
onClick={() => console.log('Edit', row)}
sx={{ minWidth: 0, padding: 0 }}
>
<DriveFileRenameOutlineSharpIcon
fontSize={isMobile ? 'small' : 'medium'}
sx={{ color: '#5F6868' }}
/>
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={8} align="center" sx={{ py: 5 }}>
No products found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination Footer */}
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
p={{ xs: 1, sm: 2 }}
sx={{
position: 'sticky',
bottom: 0,
backgroundColor: theme.palette.background.paper,
borderTop: '1px solid #f0f0f0',
zIndex: 1,
}}
>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontSize: { xs: '11px', sm: '14px' },
whiteSpace: 'nowrap',
}}
>
Showing{' '}
{(currentPage - 1) * itemsPerPage + 1} to{' '}
{Math.min(currentPage * itemsPerPage, filteredData.length)} of{' '}
{filteredData.length}
</Typography>
<Pagination
count={pageCount}
page={currentPage}
onChange={(event, value) => setCurrentPage(value)}
size={isMobile ? 'small' : 'medium'}
sx={{
'& .MuiPaginationItem-root': {
fontSize: { xs: '12px', sm: '14px' },
minWidth: { xs: 24, sm: 32 },
height: { xs: 24, sm: 32 },
backgroundColor: '#FFECE0',
color: theme.palette.primary.main,
borderRadius: '8px',
'&.Mui-selected': {
backgroundColor: theme.palette.primary.main,
color: '#fff',
},
'&:hover': {
backgroundColor: '#FFD6B5',
},
},
}}
/>
</Box>
</Paper>
);
};
export default TableView;
+206
View 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;
@@ -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;
+251
View 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;
@@ -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;
+74
View File
@@ -0,0 +1,74 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import Orders from './contect/Orders';
import authService from '../../../services/authService';
const drawerWidth = 230;
const Order = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
useEffect(() => {
const handleResize = () => {
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
useEffect(() => {
const admin = authService.getAdminData();
console.log('Admin Info:', admin);
const adminId = authService.getAdminId();
console.log('Admin ID:', adminId);
}, []);
const admin = authService.getAdminData();
const adminId = authService.getAdminId();
const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen);
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: '100%',
transition: theme.transitions.create(['width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
}}
>
<KitchPlusAppBar onDrawerToggle={handleDrawerToggle} sidebarOpen={sidebarOpen} isMobile={isMobile} />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 6,
width: { xs: '90%', sm: '95%', md: '96%' },
pt: { xs: 2, sm: 3 },
pl: { xs: 2, sm: 3 },
pb: { xs: 2, sm: 4 },
pr: { xs: 2, sm: 3 },
}}
>
<Orders adminId={adminId} />
</Box>
</Box>
</Box>
);
};
export default Order;
@@ -0,0 +1,170 @@
import React, { useContext, useState, useEffect } from "react";
import { Box, Typography, Button, LinearProgress, IconButton } from "@mui/material";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { CartContext } from "../../../../contexts/CartContextR";
import { useSnackbar } from "../../../../contexts/SnackbarContext";
const CartView = ({ onClose, onCartCreated, adminId }) => {
const { cart, clearCart, createNewCart } = useContext(CartContext);
const [loading, setLoading] = useState(false);
const { showSnackbar } = useSnackbar();
useEffect(() => {
console.log('Admin ID from props in:CartView', adminId);
}, [adminId]);
const totalPrice = cart.reduce((sum, item) => sum + (item.totalPrice || 0), 0);
const handleSendCart = async () => {
if (!cart.length) return;
setLoading(true);
try {
// تحقق من أن adminId موجود ضمن قائمة صالحة (يمكنك تعديلها حسب بياناتك)
const validAdminIds = [1, 2]; // IDs موجودة في DB
if (!validAdminIds.includes(adminId)) {
console.error("Invalid admin ID");
showSnackbar("Selected admin is not valid.", "error");
setLoading(false);
return;
}
// تحقق من أن جميع المنتجات موجودة في قاعدة البيانات
const validProductIds = [1, 2, 3, 4]; // IDs المنتجات الموجودة
for (let item of cart) {
if (!validProductIds.includes(item.id)) {
console.error(`Invalid product ID: ${item.id}`);
// alert(`Product with ID ${item.id} does not exist.`);
showSnackbar(`Product with ID ${item.id} does not exist.`, "error");
setLoading(false);
return;
}
}
// تجهيز البيانات حسب شكل الـ backend
const cartData = {
data: {
type: "cart",
attributes: {
totalPrice: cart.reduce((sum, item) => sum + (item.totalPrice || 0), 0),
},
relationships: {
admin: {
data: { id: adminId },
},
cartItems: cart.map(item => ({
attributes: { quantity: item.quantity },
relationships: { product: { data: { id: item.id } } },
})),
},
},
};
const newCart = await createNewCart(cartData);
if (newCart && newCart.success) {
onCartCreated(newCart.data);
clearCart();
onClose();
showSnackbar("Cart sent successfully!", "success");
} else {
console.error("Failed to create cart:", newCart.message);
}
} catch (error) {
// console.error("Error sending cart:", error);
showSnackbar("Failed to create cart.", "error");
} finally {
setLoading(false);
}
};
if (loading) return <LinearProgress />;
return (
<Box
sx={{
width: { xs: "100%", sm: "93.5%" },
p: { xs: 2, sm: 3 },
backgroundColor: "white",
borderRadius: 2,
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
{/* السهم للعودة */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<IconButton onClick={onClose} size="small" sx={{ mr: 1 }}>
<ArrowBackIcon fontSize="small" />
</IconButton>
<Typography variant="h5" sx={{ fontWeight: 600, fontSize: { xs: "18px", sm: "20px" } }}>
Current Cart
</Typography>
</Box>
{cart.length === 0 ? (
<Typography sx={{ mt: 2, textAlign: "center" }}>No items in cart.</Typography>
) : (
<>
{cart.map(item => (
<Box
key={item.id}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
p: 2,
borderRadius: 1,
border: "1px solid #e0e0e0",
backgroundColor: "#fafafa",
flexWrap: "wrap",
gap: 1,
}}
>
<Box>
<Typography fontWeight={600}>{item.name}</Typography>
<Typography variant="body2">Unit: {item.unit}</Typography>
</Box>
<Box sx={{ textAlign: { xs: "left", sm: "right" } }}>
<Typography variant="body2">Quantity: {item.quantity}</Typography>
<Typography variant="body2">Total: ${item.totalPrice}</Typography>
</Box>
</Box>
))}
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Total Price: ${totalPrice.toFixed(2)}
</Typography>
</Box>
</>
)}
{cart.length > 0 && (
<Box sx={{ display: "flex", gap: 1, mt: 2, flexWrap: { xs: "wrap", sm: "nowrap" }, justifyContent: { xs: "center", sm: "flex-start" } }}>
<Button
variant="contained"
color="primary"
onClick={handleSendCart}
sx={{ color: 'white', borderRadius: '8px', fontWeight: 600, fontSize: '14px', height: '40px', width: { xs: '100%', sm: '200px' }, textTransform: 'none', minWidth: { xs: 'unset', sm: '200px' } }}
>
Send Cart to Server
</Button>
<Button
variant="outlined"
onClick={clearCart}
sx={{ borderRadius: '8px', fontWeight: 600, fontSize: '14px', height: '40px', width: { xs: '100%', sm: '150px' }, textTransform: 'none', minWidth: { xs: 'unset', sm: '150px' } }}
>
Clear Cart
</Button>
</Box>
)}
</Box>
);
};
export default CartView;
@@ -0,0 +1,223 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Paper,
List,
ListItem,
ListItemText,
Button,
TextField,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
CircularProgress
} from '@mui/material';
import authService from '../../../../services/authService';
import { useSnackbar } from "../../../../contexts/SnackbarContext";
const CartDetails = ({ cart, onClose, onUpdated, onDeleted }) => {
const [editMode, setEditMode] = useState(false);
const [totalPrice, setTotalPrice] = useState(cart?.attributes?.total_price || "");
const [cartItems, setCartItems] = useState(cart.relationships?.cartItems || cart.relationships?.cart_items || []);
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const { showSnackbar } = useSnackbar();
if (!cart) return null;
const handleSaveAll = async () => {
setLoading(true);
const payload = {
data: {
type: "cart",
attributes: {
totalPrice: Number(totalPrice),
},
relationships: {
cartItems: cartItems.map(item => ({
attributes: {
quantity: Number(item.attributes.quantity),
},
relationships: {
product: {
data: {
id: item.relationships?.supplier_product?.id ||
item.relationships?.product?.data?.id
}
}
}
})),
},
},
};
try {
const result = await authService.updateCart(cart.id, payload);
if (result.success) {
onUpdated(result.data);
setEditMode(false);
} else {
// alert(result.message || 'Failed to update cart');
showSnackbar(result.message || "Failed to update cart", "error");
}
} catch (error) {
// alert('An error occurred while updating the cart');
showSnackbar("An error occurred while updating the cart", "error");
} finally {
setLoading(false);
}
};
const handleDeleteCart = async () => {
setDeleteLoading(true);
try {
const result = await authService.deleteCart(cart.id);
if (result.success) {
onDeleted(cart.id);
onClose();
} else {
// alert(result.message || 'Failed to delete cart');
showSnackbar(result.message || "Failed to update cart", "error");
}
} catch (error) {
// alert('An error occurred while deleting the cart');
showSnackbar("An error occurred while deleting the cart", "error");
} finally {
setDeleteLoading(false);
setOpenDeleteDialog(false);
}
};
const handleChangeQuantity = (itemId, newQuantity) => {
setCartItems(prev =>
prev.map(item =>
item.id === itemId
? { ...item, attributes: { ...item.attributes, quantity: newQuantity } }
: item
)
);
};
return (
<>
<Paper sx={{ p: 3, mt: 2, borderRadius: 2, border: '1px solid #e0e0e0' }}>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
Cart #{cart.id} Details
</Typography>
{editMode ? (
<>
<TextField
label="Total Price"
type="number"
value={totalPrice}
onChange={(e) => setTotalPrice(e.target.value)}
fullWidth
sx={{ mb: 2 }}
/>
<Typography sx={{ mt: 2, fontWeight: 500 }}>Items:</Typography>
<List>
{cartItems.map(item => (
<ListItem key={item.id} sx={{ pl: 0 }}>
<ListItemText
primary={`Item ID: ${item.id} | Product: ${item.relationships?.supplier_product?.id ||
item.relationships?.product?.data?.id
}`}
secondary={
<TextField
type="number"
size="small"
label="Quantity"
value={item.attributes.quantity}
onChange={(e) => handleChangeQuantity(item.id, Number(e.target.value))}
sx={{ width: '120px' }}
/>
}
/>
</ListItem>
))}
</List>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button variant="contained" onClick={handleSaveAll} disabled={loading}>
{loading ? "Saving..." : "Save All"}
</Button>
<Button variant="outlined" onClick={() => setEditMode(false)}>
Cancel
</Button>
</Box>
</>
) : (
<>
<Typography>Total Price: {cart.attributes.total_price || cart.attributes.totalPrice}</Typography>
<Typography>
Created At: {new Date(cart.attributes.createdAt).toLocaleString()}
</Typography>
<Typography sx={{ mt: 2, fontWeight: 500 }}>Items:</Typography>
<List>
{cartItems.map(item => (
<ListItem key={item.id} sx={{ pl: 0 }}>
<ListItemText
primary={`Item ID: ${item.id}`}
secondary={`Quantity: ${item.attributes.quantity} | Product: ${item.relationships?.supplier_product?.id ||
item.relationships?.product?.data?.id
}`}
/>
</ListItem>
))}
</List>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button variant="contained" onClick={() => setEditMode(true)}>
Edit
</Button>
<Button
variant="contained"
color="error"
onClick={() => setOpenDeleteDialog(true)}
sx={{ ml: 'auto' }}
>
Delete Cart
</Button>
<Button variant="outlined" onClick={onClose}>
Back to Orders
</Button>
</Box>
</>
)}
</Paper>
{/* Delete Confirmation Dialog */}
<Dialog
open={openDeleteDialog}
onClose={() => setOpenDeleteDialog(false)}
>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure you want to delete this cart? This action cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDeleteDialog(false)} disabled={deleteLoading}>
Cancel
</Button>
<Button
onClick={handleDeleteCart}
color="error"
variant="contained"
disabled={deleteLoading}
>
{deleteLoading ? <CircularProgress size={24} /> : 'Delete'}
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default CartDetails;
+326
View File
@@ -0,0 +1,326 @@
import React, { useState, useEffect } from 'react';
import {
useMediaQuery,
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Skeleton,
CircularProgress,
Button
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import authService from '../../../../services/authService';
import CartDetails from './OrderDetails';
import CartView from './CartView';
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
const theme = useTheme();
const handlePrev = () => { if (currentPage > 1) onChange(currentPage - 1); };
const handleNext = () => { if (currentPage < pageCount) onChange(currentPage + 1); };
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
onClick={handlePrev}
disabled={currentPage <= 1}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
backgroundColor: theme.palette.primary.main,
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
fontSize: 14,
userSelect: 'none',
boxShadow: `0 0 0 1px ${theme.palette.primary.main}`,
}}
>
{currentPage}
</Box>
<IconButton
size="small"
onClick={handleNext}
disabled={currentPage >= pageCount}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
</Box>
);
};
const Orders = ({ adminId }) => {
useEffect(() => {
console.log('Admin ID from props:', adminId);
}, [adminId]);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [ordersData, setOrdersData] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedCart, setSelectedCart] = useState(null);
const [cartLoading, setCartLoading] = useState(false);
const [showCartView, setShowCartView] = useState(false); // ← التحكم بعرض CartView هنا
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 6;
const [orders, setOrders] = useState([]);
const handleAddOrder = (newOrder) => {
setOrders((prevOrders) => [...prevOrders, newOrder]);
};
useEffect(() => {
const fetchOrders = async () => {
setLoading(true);
try {
const result = await authService.getCart();
if (result.success) {
setOrdersData(Array.isArray(result.data) ? result.data : []);
} else {
console.error(result.message);
setOrdersData([]);
}
} catch (error) {
console.error('Failed to fetch orders:', error);
setOrdersData([]);
}
setLoading(false);
};
fetchOrders();
}, []);
const handleRowClick = async (cartId) => {
setCartLoading(true);
try {
const result = await authService.getCartById(cartId);
if (result.success) setSelectedCart(result.data);
else console.error(result.message);
} catch (error) {
console.error('Failed to fetch cart details:', error);
}
setCartLoading(false);
};
const handleCartUpdated = (updatedCart) => {
setOrdersData(prev =>
prev.map(cart => cart.id === updatedCart.id ? updatedCart : cart)
);
setSelectedCart(null);
};
const handleCloseCartDetails = () => { setSelectedCart(null); };
const pageCount = Math.ceil(ordersData.length / itemsPerPage);
const paginatedOrders = ordersData.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
if (cartLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
);
}
if (selectedCart) {
return (
<CartDetails
cart={selectedCart}
onClose={handleCloseCartDetails}
onUpdated={handleCartUpdated}
/>
);
}
// عرض CartView بدل الطلبات إذا تم الضغط على الزر
// داخل Orders.js
if (showCartView) {
return (
<CartView
onClose={() => setShowCartView(false)}
onSend={handleAddOrder}
onCartCreated={(newCart) => {
setOrdersData(prev => [newCart, ...prev]); // ضف الكارت الجديد مباشرة
setShowCartView(false);
}}
adminId={adminId} // ← أرسل الـ adminId هنا
/>
);
}
return (
<Box
sx={{
width: { xs: '100%', sm: '93.5%' },
p: { xs: 2, sm: 3 },
backgroundColor: 'white',
borderRadius: 2,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' },
mb: 2,
gap: { xs: 1, sm: 0 },
}}
>
<Typography
variant="h5"
sx={{
fontWeight: 600,
fontSize: { xs: '18px', sm: '20px' },
mb: { xs: 1, sm: 0 },
textAlign: { xs: 'center', sm: 'left' },
}}
>
Order
</Typography>
</Box>
{/* جدول الطلبات */}
<TableContainer
component={Paper}
sx={{
boxShadow: 'none',
border: '1px solid #e0e0e0',
borderRadius: 2,
minWidth: 320,
height: '380px',
}}
>
<Table
sx={{ minWidth: { sm: 550, md: 650 }, tableLayout: 'auto' }}
aria-label="orders table"
size={isMobile ? 'small' : 'medium'}
>
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Cart ID</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Total Price</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Created At</TableCell>
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Items Count</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading
? Array.from({ length: itemsPerPage }).map((_, idx) => (
<TableRow key={`skeleton-${idx}`}>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
</TableRow>
))
: (
<>
{paginatedOrders.map(order => {
const attributes = order?.attributes || {};
const relationships = order?.relationships || {};
const cartItems = relationships?.cartItems || relationships?.cart_items || [];
return (
<TableRow
key={order?.id}
hover
sx={{
cursor: 'pointer',
transition: 'background-color 0.3s',
'&:hover': {
backgroundColor: '#ffe4d0ff !important',
'& td': { color: '#000000ff' },
},
}}
onClick={() => handleRowClick(order.id)}
>
<TableCell>{order?.id || '--'}</TableCell>
<TableCell>{attributes?.totalPrice ?? attributes?.total_price ?? '0.00'}</TableCell>
<TableCell>{attributes?.createdAt ? new Date(attributes.createdAt).toLocaleString() : '--'}</TableCell>
<TableCell>{cartItems.length}</TableCell>
</TableRow>
);
})}
{paginatedOrders.length < itemsPerPage &&
Array.from({ length: itemsPerPage - paginatedOrders.length }).map((_, idx) => (
<TableRow key={`empty-${idx}`} sx={{ height: 53 }}>
<TableCell colSpan={4} sx={{ borderBottom: 'none' }} />
</TableRow>
))
}
</>
)
}
</TableBody>
</Table>
</TableContainer>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
pt={{ xs: 1, sm: 2 }}
sx={{
position: 'sticky',
bottom: 0,
backgroundColor: theme.palette.background.paper,
borderTop: '1px solid #f0f0f0',
}}
>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '11px', sm: '14px' }, whiteSpace: 'nowrap' }}
>
Showing {(currentPage - 1) * itemsPerPage + 1} - {Math.min(currentPage * itemsPerPage, ordersData.length)} of {ordersData.length}
</Typography>
<SimplePagination currentPage={currentPage} pageCount={pageCount} onChange={setCurrentPage} />
</Box>
</Box>
);
};
export default Orders;
+136
View File
@@ -0,0 +1,136 @@
import React from 'react';
import {
AppBar,
Toolbar,
Box,
ListItemIcon,
ListItemText,
Typography,
useTheme,
useMediaQuery,
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import LogoutIcon from '@mui/icons-material/Logout';
import authService from '../../../services/authService';
const KitchPlusAppBar = ({ onDrawerToggle, sidebarOpen, isMobile }) => {
const theme = useTheme();
const navigate = useNavigate();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
// دالة تسجيل الخروج المعدلة
const handleLogout = async () => {
try {
const result = await authService.logout();
if (result.success) {
localStorage.removeItem('token');
navigate('/login');
} else {
console.error('Logout failed:', result.message);
}
} catch (error) {
console.error('Logout error:', error);
}
};
return (
<AppBar
sx={{
height: { xs: 56, sm: 64, md: 66 },
backgroundColor: '#ffffff',
color: 'black',
boxShadow: 'none',
borderBottom: '1px solid #e0e0e0',
position: 'sticky',
top: 0,
zIndex: theme.zIndex.appBar,
}}
>
<Toolbar
sx={{
px: { xs: 2, sm: 3, md: '24px' },
minHeight: { xs: '56px !important', sm: '64px !important' },
display: 'flex',
justifyContent: 'space-between',
}}
>
{/* Left: Logo */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
component="img"
src="/image.png"
alt="logo"
sx={{
width: 40,
height: 40,
objectFit: 'contain',
mr: 1.5,
}}
/>
<Typography
variant="h6"
sx={{
fontWeight: 400,
fontSize: '1.25rem',
color: 'text.primary',
}}
>
KITCH
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 400,
fontSize: '1.25rem',
color: 'primary.main',
ml: 0.5,
}}
>
PLUS
</Typography>
</Box>
{/* Right: Log Out Button */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.8, sm: 1, md: 1.5 } }}>
<Box
onClick={handleLogout}
sx={{
display: 'flex',
alignItems: 'center',
borderRadius: '5px',
px: 2,
py: 1,
cursor: 'pointer',
transition: 'background-color 0.2s',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: '#fffcf9d5',
},
}}
>
<ListItemIcon
sx={{
color: 'divider',
minWidth: 36,
}}
>
<LogoutIcon />
</ListItemIcon>
<ListItemText
primary="Log Out"
primaryTypographyProps={{
sx: {
color: 'divider',
fontSize: '1rem',
},
}}
/>
</Box>
</Box>
</Toolbar>
</AppBar>
);
};
export default KitchPlusAppBar;
@@ -0,0 +1,60 @@
import React from "react";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Button,
Box,
} from "@mui/material";
const RestaurantDetailsModal = ({ open, onClose, restaurant }) => {
if (!restaurant) return null;
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{restaurant.name}</DialogTitle>
<DialogContent dividers>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
{restaurant.cuisine_type && (
<Typography><b>Cuisine Type:</b> {restaurant.cuisine_type.name}</Typography>
)}
<Typography><b>Location:</b> {restaurant.location}</Typography>
<Typography><b>Brand Details:</b> {restaurant.brand_details || 'N/A'}</Typography>
<Typography><b>Age Group:</b> {restaurant.age_group}</Typography>
<Typography><b>Menu Status:</b> {restaurant.menu_status ? 'Active' : 'Inactive'}</Typography>
<Typography><b>Need Help:</b> {restaurant.need_help || 'N/A'}</Typography>
{restaurant.operational_details && (
<>
<Typography><b>Staff Members:</b> {restaurant.operational_details.staff_members}</Typography>
<Typography><b>Equipment:</b> {restaurant.operational_details.equipment}</Typography>
<Typography><b>Specialized Equipment:</b> {restaurant.operational_details.specialized_equipment}</Typography>
<Typography><b>Expansion Cities:</b> {restaurant.operational_details.expansion_plan_cities}</Typography>
</>
)}
{restaurant.budget_expansion && (
<>
<Typography><b>Estimated Budget:</b> {restaurant.budget_expansion.estimated_budget}</Typography>
<Typography><b>Expansion Branches:</b> {restaurant.budget_expansion.expansion_branches}</Typography>
</>
)}
{restaurant.created_by && (
<Typography><b>Created By:</b> {restaurant.created_by.name}</Typography>
)}
</Box>
</DialogContent>
<DialogActions>
<Button
onClick={onClose}
variant="outlined"
sx={{ textTransform: "none", minWidth: 120 }}
>
Close
</Button>
</DialogActions>
</Dialog>
);
};
export default RestaurantDetailsModal;
@@ -0,0 +1,76 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from './AppBar';
import RestaurantSelection from './RestaurantSelection';
import authService from '../../../services/authService';
const RestaurantProfile = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [restaurants, setRestaurants] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchRestaurants = async () => {
setLoading(true);
const result = await authService.getRestaurants();
if (result.success) {
const mappedRestaurants = result.data.map((res) => ({
id: res.id,
name: res.name,
location: res.location || '',
image: res.image_url,
address: res.address || '',
}));
setRestaurants(mappedRestaurants);
} else {
alert(result.message || 'خطأ في جلب المطاعم');
}
setLoading(false);
};
fetchRestaurants();
}, []);
const handleDrawerToggle = () => {};
const handleCreateRestaurant = () => {
alert('Redirect to restaurant creation page or open a form!');
};
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
height: '100vh',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}
>
<KitchPlusAppBar
onDrawerToggle={handleDrawerToggle}
sidebarOpen={false}
isMobile={isMobile}
/>
<Box
sx={{
flexGrow: 1,
overflowY: 'auto',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
'&::-webkit-scrollbar': { display: 'none' },
}}
>
<RestaurantSelection
restaurants={restaurants}
onCreateRestaurant={handleCreateRestaurant}
/>
</Box>
</Box>
);
};
export default RestaurantProfile;
@@ -0,0 +1,186 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Grid,
useTheme,
useMediaQuery,
IconButton,
Skeleton
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import AddToPhotosIcon from '@mui/icons-material/AddToPhotos';
import InfoIcon from '@mui/icons-material/Info';
import { useRestaurant } from '../../../contexts/RestaurantContext';
import RestaurantDetailsModal from './RestaurantDetailsModal';
import authService from '../../../services/authService';
const RestaurantSelection = ({ restaurants = [], loading = false }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down('sm'));
const navigate = useNavigate(); // استخدم navigate للتوجيه
const { setRestaurantId } = useRestaurant();
const [selectedRestaurant, setSelectedRestaurant] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
const handleSelectRestaurant = (id) => {
setRestaurantId(id);
navigate('/dashboard');
};
const fetchRestaurantDetails = async (id) => {
try {
const result = await authService.getRestaurantById(id);
if (result.success) {
setSelectedRestaurant(result.data);
setModalOpen(true);
} else {
alert(result.message || 'Failed to fetch restaurant details');
}
} catch (error) {
console.error('Error fetching restaurant details:', error);
}
};
// ✅ دالة التوجيه عند الضغط على زر الإنشاء
const handleCreateClick = () => {
navigate('/create-restaurant'); // توجه مباشرة إلى صفحة الإنشاء
};
return (
<Box sx={{ py: 8, backgroundColor: '#FAFAFA', minHeight: '100vh' }}>
{/* زر إنشاء مطعم */}
<Grid container spacing={1} justifyContent={isSmall ? 'center' : 'flex-start'} pl={isSmall ? 0 : 2}>
<Box
onClick={handleCreateClick}
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
borderRadius: 3,
height: '100%',
width: '100%',
maxWidth: 250,
maxHeight: 186,
cursor: 'pointer',
transition: 'transform 0.3s',
backgroundColor: '#fff',
ml: { xs: 0, sm: 8, md: 28 },
'&:hover': { transform: 'translateY(-5px)' },
}}
>
<Box sx={{ width: 52, height: 52, borderRadius: '50%', backgroundColor: '#F6F6F6', display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 1 }}>
<AddToPhotosIcon sx={{ fontSize: 30, color: theme.palette.primary.main }} />
</Box>
<Typography fontWeight="600" variant="subtitle1" fontSize="1.25rem">
Create New Restaurant
</Typography>
<Typography variant="body2" color="text.secondary">
Start from scratch and build your restaurant profile
</Typography>
</Box>
</Grid>
{/* قائمة المطاعم */}
<Box sx={{ mt: 5, ml: { xs: 0, sm: 10, md: 30 } }}>
<Typography variant="h6" fontWeight="bold" mb={3} textAlign={isSmall ? 'center' : 'flex-start'}>
Existing Restaurants
</Typography>
{restaurants.length === 0 && !loading ? (
<Typography textAlign={isSmall ? 'center' : 'flex-start'} color="text.secondary" fontSize="1rem">
No restaurants found. Create one to get started.
</Typography>
) : (
<Grid container spacing={4} justifyContent={isSmall ? 'center' : 'flex-start'}>
{(loading ? Array.from(new Array(3)) : restaurants).map((restaurant, index) => (
<Grid key={restaurant?.id || index}>
{loading ? (
<Skeleton variant="rectangular" width={230} height={162} sx={{ borderRadius: 3 }} />
) : (
<Box
sx={{
position: 'relative',
borderRadius: 3,
p: 3,
width: '230px',
height: '90%',
maxHeight: '162px',
transition: 'transform 0.2s',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
backgroundColor: '#fff',
cursor: 'pointer',
'&:hover': { transform: 'translateY(-3px)' },
mx: 'auto',
}}
onClick={() => handleSelectRestaurant(restaurant.id)}
>
{/* أيقونة فتح المودال */}
<IconButton
onClick={(e) => {
e.stopPropagation();
fetchRestaurantDetails(restaurant.id);
}}
sx={{
position: 'absolute',
top: 8,
right: 8,
backgroundColor: 'rgba(255,255,255,0.8)',
'&:hover': {
backgroundColor: 'rgba(255,255,255,1)',
transform: 'scale(1.1)',
transition: 'transform 0.2s'
},
zIndex: 2,
padding: 0.5
}}
size="small"
>
<InfoIcon fontSize="small" />
</IconButton>
{/* صورة المطعم */}
<Box
sx={{ width: 110, height: 100, overflow: 'hidden', border: '2px solid #e0e0e0', mb: 1.5, mx: 'auto' }}
>
<Box
component="img"
src={restaurant.image || '/images/default-restaurant.png'}
alt={restaurant.name}
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</Box>
{/* باقي الكارت */}
<Box sx={{ width: '100%' }}>
<Typography fontWeight="600" textAlign="center" fontSize="1.25rem" mb={0.5}>
{restaurant.name}
</Typography>
<Typography variant="body2" color="text.secondary" fontSize="0.9rem" textAlign="center" mb={1}>
{restaurant.location || ' '}
</Typography>
</Box>
</Box>
)}
</Grid>
))}
</Grid>
)}
</Box>
{/* مودال المطعم */}
<RestaurantDetailsModal
open={modalOpen}
onClose={() => setModalOpen(false)}
restaurant={selectedRestaurant}
/>
</Box>
);
};
export default RestaurantSelection;
@@ -0,0 +1,161 @@
import React, { useState } from 'react';
import { Box } from '@mui/material';
import BasicInformation from './contcet/BasicInformation';
import ContactInformation from './contcet/ContactInformation';
import BusinessHours from './contcet/BusinessHours';
import Equipment from './contcet/Equipment';
import TypeOfRestaurant from './contcet/TypeOfRestaurant';
import OperationalCapacity from './contcet/OperationalCapacity';
import SideProfile from './SideProfile';
import AccountProfile from '../Settings/AccountProfile';
import authService from '../../../services/authService';
import { useRestaurant } from '../../../contexts/RestaurantContext';
import ConfirmationDialog from './contcet/ConfirmationDialog';
const RestaurantProfile = () => {
const { restaurantId } = useRestaurant();
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState({});
const [modalOpen, setModalOpen] = useState(false);
const [showSetting, setShowSetting] = useState(false);
const updateFormData = (newData) =>
setFormData((prev) => ({ ...prev, ...newData }));
const handleSubmit = async () => {
if (!restaurantId) return;
const payload = {
restaurant_name: formData.restaurant_name || '',
restaurant_type: formData.restaurant_type || '',
Address: formData.Address || '',
City: formData.City || '',
Postal_code: formData.Postal_code || '',
Phone: formData.Phone || '',
Email: formData.Email || '',
Operation_hour: formData.Operation_hour || '',
closed_days: formData.closed_days || [],
equipment: {
...formData.equipment // ✅ أخذ جميع المعدات من Equipment مباشرة
},
Maximum_orders_per_day: formData.Maximum_orders_per_day || 0,
Number_of_Cheff: formData.Number_of_Cheff || 0,
Number_of_Waiters: formData.Number_of_Waiters || 0,
Number_of_Cookers: formData.Number_of_Cookers || 0,
host_type: formData.ownRestaurantType || '',
collaboration_type: formData.collaborateRestaurantType || ''
};
try {
const res = await authService.createRestaurantProfile(restaurantId, payload);
if (res.success) {
setModalOpen(true);
// بعد 10 ثواني يغلق المودال ويظهر الـ AccountProfile
setTimeout(() => {
setModalOpen(false);
setShowSetting(true);
}, 10000); // ✅ صار 10 ثواني بدل 5
} else {
console.error("Profile creation failed:", res.message, res.errors);
}
} catch (error) {
console.error(error);
}
};
const steps = [
<BasicInformation
key="step-0"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep((p) => p + 1)}
/>,
<ContactInformation
key="step-1"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep((p) => p + 1)}
onBack={() => setCurrentStep((p) => p - 1)}
/>,
<BusinessHours
key="step-2"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep((p) => p + 1)}
onBack={() => setCurrentStep((p) => p - 1)}
/>,
<Equipment
key="step-3"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep((p) => p + 1)}
onBack={() => setCurrentStep((p) => p - 1)}
/>,
<OperationalCapacity
key="step-4"
formData={formData}
updateFormData={updateFormData}
onNext={() => setCurrentStep((p) => p + 1)}
onBack={() => setCurrentStep((p) => p - 1)}
/>,
<TypeOfRestaurant
key="step-5"
formData={formData}
updateFormData={updateFormData}
onBack={() => setCurrentStep((p) => p - 1)}
onRegister={handleSubmit}
/>
];
return (
<Box sx={{ display: 'flex', height: '100%', backgroundColor: '#F6F6F6', overflow: 'hidden' }}>
{/* إظهار SideProfile فقط إذا لم يتم التحويل إلى AccountProfile */}
{!showSetting && (
<Box
sx={{
height: '100%',
width: { md: '30%' },
display: { xs: 'none', sm: 'none', md: 'block' },
overflowY: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' }
}}
>
<Box sx={{ minHeight: '100%', pb: 15, pt: 1 }}>
<SideProfile
currentStepIndex={currentStep}
onBack={() => setCurrentStep((prev) => Math.max(prev - 1, 0))}
/>
</Box>
</Box>
)}
<Box
sx={{
ml: { xs: 2, md: 3 },
flexGrow: 1,
height: '100%',
pr: { sm: 2, md: 1 },
pt: 1,
mb: { sm: 20 },
width: { md: '60%' },
display: 'block',
overflowY: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' }
}}
>
<Box sx={{ minHeight: '100%', pb: 18 }}>
{showSetting ? <AccountProfile /> : steps[currentStep]}
</Box>
</Box>
{/* مودال بدون أزرار */}
<ConfirmationDialog open={modalOpen} onClose={() => {}} hideButtons={true} />
</Box>
);
};
export default RestaurantProfile;
@@ -0,0 +1,163 @@
import React from 'react';
import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
const steps = [
{ title: 'Basic Information', icon: '/images/createProfile/BasicInf.png' },
{ title: 'Contact Information', icon: '/images/createProfile/ContactInf.png' },
{ title: 'Business Hours', icon: '/images/createProfile/BusinessHours.png' },
// { title: 'Menu Upload', icon: '/images/createProfile/MenuUpload.png' },
{ title: 'Available Equipment', icon: '/images/createProfile/equipment.png' },
{ title: 'Operational Capacity', icon: '/images/icons/rocket.png' },
{ title: 'Type of Restaurant', icon: '/images/createProfile/TypeOfRestaurant.png' },
// { title: 'Upload Photos', icon: '/images/createProfile/UploadPhotos.png' },
{ title: 'Submit & Confirmation', icon: '/images/createProfile/Confirmation.png' },
];
const SideProfile = ({ currentStepIndex = 0, onBack }) => {
const theme = useTheme();
return (
<Box
sx={{
height: '100%',
backgroundColor: '#FFFFFF',
px: 3,
py: 4,
display: { xs: 'none', md: 'block' },
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
}}
>
{/* العنوان الرئيسي */}
<Typography fontSize="18px" fontWeight={600} mb={1.2}>
Restaurant Registration Flow
</Typography>
<Typography variant="body2" color="text.secondary" mb={3}>
Complete this process to register your restaurant on our amazing food platform.
</Typography>
{/* الخطوات */}
<Stack spacing={3} sx={{ ml: 1, position: 'relative', pb: 12 }}>
{steps.map((step, index) => (
<Box key={index} position="relative">
{/* الخط الرأسي بين الدوائر */}
{index !== steps.length - 1 && (
<Box
sx={{
position: 'absolute',
top: '28px',
left: '23px',
width: '2px',
height: 'calc(100% - 5px)',
backgroundColor: '#E0E0E0',
zIndex: 0,
}}
/>
)}
{/* محتوى كل خطوة */}
<Box display="flex" alignItems="center" gap={2}>
{/* الدائرة بالأيقونة */}
<Box
sx={{
position: 'relative',
width: 45,
height: 45,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
{/* الدائرة الخلفية - تظهر دائمًا */}
<Box
sx={{
position: 'absolute',
backgroundImage:
index <= currentStepIndex
? 'linear-gradient(0deg, #FF914D, #F3F3F3)'
: 'linear-gradient(0deg, #DFDFDF, #F0F0F0)',
width: 48,
height: 48,
borderRadius: '50%',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 0,
}}
/>
{/* الدائرة الأمامية بالأيقونة */}
<Box
sx={{
width: 40,
height: 40,
borderRadius: '50%',
backgroundColor: '#FFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}
>
<img
src={step.icon}
alt={step.title}
style={{
width: 20,
height: 20,
marginLeft: step.title === 'Available Equipment' ? 7 : 0,
objectFit: 'contain',
}}
/>
</Box>
</Box>
{/* العنوان */}
<Typography
sx={{
fontSize: '16px',
fontWeight: 600,
color: '#000',
}}
>
{step.title}
</Typography>
</Box>
</Box>
))}
</Stack>
{/* زر الرجوع */}
<Box
sx={{
position: 'absolute',
bottom: 16,
right: 24,
}}
>
<Button
variant="text"
startIcon={<ArrowBackIosIcon sx={{ fontSize: 20 }} />}
onClick={onBack}
disabled={currentStepIndex === 0}
sx={{
textTransform: 'none',
fontSize: '20px',
color: currentStepIndex === 0 ? '#C2C2C2' : '#6B7283',
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Box>
);
};
export default SideProfile;
@@ -0,0 +1,124 @@
import React, { useEffect, useState, useCallback } from "react";
import { CircularProgress, Box, useTheme, useMediaQuery, Skeleton } from "@mui/material";
import RestaurantProfile from "./RestaurantProfile";
import Setting from "../Settings/AccountProfile";
import authService from "../../../services/authService";
import { useRestaurant } from "../../../contexts/RestaurantContext";
import KitchPlusAppBar from "../AppBar";
import Sidebar from "../SideHome"
const drawerWidth = 230;
const RestaurantWrapper = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const { restaurantId } = useRestaurant();
const [loading, setLoading] = useState(true);
const [hasProfile, setHasProfile] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const fetchProfile = useCallback(async () => {
if (!restaurantId) {
setLoading(false);
return;
}
setLoading(true);
try {
const profile = await authService.getRestaurantProfile(restaurantId);
setHasProfile(profile && Object.keys(profile).length > 0);
} catch (error) {
console.error("Error checking restaurant profile:", error);
setHasProfile(false);
} finally {
setLoading(false);
}
}, [restaurantId]);
useEffect(() => {
fetchProfile();
}, [fetchProfile]);
useEffect(() => {
const handleResize = () =>
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [theme.breakpoints.values.md]);
const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen);
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: "100%",
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,
overflowY: "auto",
scrollbarWidth: "none",
"&::-webkit-scrollbar": { display: "none" },
pt: { xs: 2, sm: 3, md: 3 },
pl: { xs: 2, sm: 3, md: 3 },
pr: { xs: 2, sm: 3, md: 3 },
}}
>
{loading ? (
// Skeleton placeholder
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Skeleton variant="rectangular" height={40} width="40%" />
<Skeleton variant="rectangular" height={40} width="60%" />
<Skeleton variant="rectangular" height={200} width="100%" />
<Skeleton variant="rectangular" height={40} width="80%" />
</Box>
) : hasProfile ? (
<Setting />
) : (
<RestaurantProfile refreshProfile={fetchProfile} />
)}
</Box>
</Box>
</Box>
);
};
export default RestaurantWrapper;
@@ -0,0 +1,161 @@
import React, { useState } from 'react';
import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material';
const BasicInformation = ({ formData, updateFormData, onNext, onBack }) => {
const theme = useTheme();
const [errors, setErrors] = useState({});
const handleChange = (field, value) => {
updateFormData({ [field]: value });
// إزالة رسالة الخطأ عند التعديل
setErrors(prev => ({ ...prev, [field]: '' }));
};
const validateFields = () => {
const newErrors = {};
if (!formData.restaurant_name || formData.restaurant_name.trim() === '') newErrors.restaurant_name = 'Restaurant Name is required';
if (!formData.restaurant_type || formData.restaurant_type.trim() === '') newErrors.restaurant_type = 'Restaurant Type is required';
if (!formData.Address || formData.Address.trim() === '') newErrors.Address = 'Address is required';
if (!formData.City || formData.City.trim() === '') newErrors.City = 'City is required';
if (!formData.Postal_code || formData.Postal_code.trim() === '') newErrors.Postal_code = 'Postal Code is required';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextClick = () => {
if (validateFields()) {
onNext();
}
};
const fields = [
{ label: 'Restaurant Name', placeholder: 'Al-Baik Foods', field: 'restaurant_name' },
{ label: 'Restaurant Type', placeholder: 'Fast Food', field: 'restaurant_type' },
{ label: 'Address', placeholder: 'Ward # 13', field: 'Address' },
{ label: 'City', placeholder: 'Jordan', field: 'City' },
{ label: 'Postal Code', placeholder: '31200', field: 'Postal_code' },
];
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 4.5 },
pb: 10,
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={1.5}>
<Typography
fontWeight={700}
sx={{
fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' }
}}
>
Basic Information
</Typography>
<Box sx={{ width: '70%' }}>
<Typography
fontSize="16px"
color="text.secondary"
fontWeight={500}
sx={{ pb: 1 }}
>
Enter your basic information to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
{fields.map((fieldObj, index) => (
<Box key={index} sx={{ display: 'flex', flexDirection: 'column'}}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
{fieldObj.label}
</Typography>
<TextField
placeholder={fieldObj.placeholder}
variant="outlined"
fullWidth
value={formData[fieldObj.field] || ''}
onChange={(e) => handleChange(fieldObj.field, e.target.value)}
error={!!errors[fieldObj.field]}
helperText={errors[fieldObj.field] || ' '}
sx={{
pb:-1,
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
))}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleNextClick}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.hover,
},
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default BasicInformation;
@@ -0,0 +1,207 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Stack,
Button,
useTheme,
TextField,
Checkbox,
FormGroup,
FormControlLabel
} from '@mui/material';
const BusinessHours = ({ formData, updateFormData, onNext, onBack }) => {
const theme = useTheme();
const [selectedDays, setSelectedDays] = useState(formData.closed_days || []);
const [operationHours, setOperationHours] = useState(formData.Operation_hour || '');
const [errors, setErrors] = useState({});
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
useEffect(() => {
updateFormData({
Operation_hour: operationHours, // الاسم الآن مطابق للـ API
closed_days: selectedDays
});
}, [operationHours, selectedDays]);
const handleCheckboxChange = (day) => {
const updatedDays = selectedDays.includes(day)
? selectedDays.filter(d => d !== day)
: [...selectedDays, day];
setSelectedDays(updatedDays);
};
const validateFields = () => {
const newErrors = {};
if (!operationHours || operationHours.trim() === '') {
newErrors.operationHours = 'Operational hours are required';
} else {
// Regex للتحقق من الصيغة hh:mm AM/PM - hh:mm AM/PM
const regex = /^([0]?[1-9]|1[0-2]):([0-5][0-9])\s?(AM|PM)\s?-\s?([0]?[1-9]|1[0-2]):([0-5][0-9])\s?(AM|PM)$/i;
if (!regex.test(operationHours.trim())) {
newErrors.operationHours = 'Invalid format (e.g., "9:00 AM - 11:00 PM")';
}
}
if (selectedDays.length === 7) {
newErrors.closedDays = 'Restaurant cannot be closed all week';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextClick = () => {
if (validateFields()) {
onNext();
}
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 4.5 },
pb: 10,
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={1.5}>
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
Business Hours
</Typography>
<Box sx={{ width: '70%' }}>
<Typography fontSize="16px" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
Enter your business hours to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
{/* Operational Hours Input */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Operational Hours
</Typography>
<TextField
placeholder="9:00 AM - 11:00 PM"
variant="outlined"
fullWidth
value={operationHours}
onChange={(e) => setOperationHours(e.target.value)}
error={!!errors.operationHours}
helperText={errors.operationHours || ' '}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
{/* Checkboxes for Days of the Week */}
<Box sx={{ mt: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px', mb: 1 }}>
Closed Days
</Typography>
<FormGroup row sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{days.map(day => {
const isChecked = selectedDays.includes(day);
return (
<FormControlLabel
key={day}
control={
<Checkbox
checked={isChecked}
onChange={() => handleCheckboxChange(day)}
sx={{
transform: 'scale(1.5)',
color: '#F0EDED',
'&.Mui-checked': { color: '#FF914D' },
}}
/>
}
label={
<Typography sx={{ color: isChecked ? '#e57f3f' : '#969BA7', fontWeight: 500 }}>
{day}
</Typography>
}
sx={{ m: 0, width: { xs: '45%', sm: '30%', md: '22%' } }}
/>
);
})}
</FormGroup>
{errors.closedDays && (
<Typography color="error" fontSize="0.875rem" sx={{ mt: 0.5 }}>
{errors.closedDays}
</Typography>
)}
</Box>
{/* Next & Back Buttons */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleNextClick}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': { backgroundColor: theme.palette.primary.hover },
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': { backgroundColor: theme.palette.primary.light, borderColor: theme.palette.primary.main },
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default BusinessHours;
@@ -0,0 +1,109 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
Box,
Typography,
useTheme,
} from '@mui/material';
const ConfirmationDialog = ({ open, onClose }) => {
const theme = useTheme();
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
width: '525px',
height: '358px',
maxWidth: '100%',
maxHeight: '100%',
borderRadius: '12px',
overflow: 'hidden'
}
}}
>
<Box sx={{
p: 3,
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}}>
<Box sx={{
position: 'relative',
width: 100,
height: 100,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mb: 3
}}>
<Box sx={{
position: 'absolute',
backgroundColor: '#F5F8FF',
width: '100%',
height: '100%',
borderRadius: '50%',
zIndex: 0,
}} />
<Box sx={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: '#E9EFFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1,
}}>
<img
src='/images/createProfile/Successful.png'
style={{
width: 40,
height: 40,
objectFit: 'contain',
}}
alt="Success icon"
/>
</Box>
</Box>
<DialogTitle sx={{ p: 0 }}>
<Typography
variant="h4"
component="div"
sx={{
fontWeight: 600,
fontSize: '28px',
lineHeight: '1.2',
textAlign: 'center'
}}
>
Register successful!
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0, mt: 2 }}>
<DialogContentText sx={{
textAlign: 'center',
fontSize: '16px',
fontWeight: 500,
color: theme.palette.text.secondary
}}>
Congratulations! You have registered your restaurant successfully on our platform
</DialogContentText>
</DialogContent>
</Box>
</Dialog>
);
};
export default ConfirmationDialog;
@@ -0,0 +1,175 @@
import React, { useState } from 'react';
import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material';
const ContactInformation = ({ formData, updateFormData, onNext, onBack }) => {
const theme = useTheme();
const [errors, setErrors] = useState({});
const handleChange = (field, value) => {
updateFormData({ [field]: value });
setErrors(prev => ({ ...prev, [field]: '' })); // إزالة الخطأ عند التعديل
};
const validateFields = () => {
const newErrors = {};
if (!formData.Phone || formData.Phone.trim() === '') {
newErrors.Phone = 'Phone number is required';
} else if (!/^\+?\d{7,15}$/.test(formData.Phone.trim())) {
newErrors.Phone = 'Invalid phone number';
}
if (!formData.Email || formData.Email.trim() === '') {
newErrors.Email = 'Email is required';
} else if (!/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(formData.Email.trim())) {
newErrors.Email = 'Invalid Email address';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextClick = () => {
if (validateFields()) {
onNext();
}
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 14.5 },
pb: { xs: 20, sm: 20, md: 0 },
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={1.5}>
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
Contact Information
</Typography>
<Box sx={{ width: '70%' }}>
<Typography fontSize="16px" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
Enter your contact information to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
{/* Phone Number Input */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Phone Number
</Typography>
<TextField
placeholder="03055376864"
variant="outlined"
fullWidth
value={formData.Phone || ''}
onChange={(e) => handleChange('Phone', e.target.value)}
error={!!errors.Phone}
helperText={errors.Phone || ' '}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
{/* Email Input */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Email
</Typography>
<TextField
placeholder="example@example.com"
type="Email"
variant="outlined"
fullWidth
value={formData.Email || ''}
onChange={(e) => handleChange('Email', e.target.value)}
error={!!errors.Email}
helperText={errors.Email || ' '}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#3f51b5',
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
{/* Next & Back Buttons */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleNextClick}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': { backgroundColor: theme.palette.primary.hover },
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': { backgroundColor: theme.palette.primary.light, borderColor: theme.palette.primary.main },
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default ContactInformation;
@@ -0,0 +1,258 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Stack,
Button,
useTheme,
TextField,
Modal,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import { useNavigate } from 'react-router-dom';
const Equipment = ({ formData, updateFormData, onNext, onBack }) => {
const theme = useTheme();
const navigate = useNavigate();
const [equipment, setEquipment] = useState(formData.equipment || {
oven: '',
grill: '',
freezer: '',
});
const [errors, setErrors] = useState({});
const [modalOpen, setModalOpen] = useState(false);
const [newEquipment, setNewEquipment] = useState({ name: '', quantity: '' });
useEffect(() => {
updateFormData({ equipment });
}, [equipment]);
const handleInputChange = (key, value) => {
setEquipment(prev => ({ ...prev, [key]: value }));
};
const validateFields = () => {
const newErrors = {};
Object.entries(equipment).forEach(([key, value]) => {
if (!value || isNaN(value) || Number(value) < 0) {
newErrors[key] = `${key} must be a positive number`;
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextClick = () => {
if (validateFields()) {
if (onNext) onNext();
else navigate('/dashboard');
}
};
const handleAddEquipment = () => {
if (!newEquipment.name.trim() || !newEquipment.quantity || isNaN(newEquipment.quantity)) {
return;
}
setEquipment(prev => ({
...prev,
[newEquipment.name.toLowerCase()]: newEquipment.quantity,
}));
setNewEquipment({ name: '', quantity: '' });
setModalOpen(false);
};
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 12 },
pb: { xs: 20, sm: 12, md: 3 },
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
width: { xs: '85%', sm: '90%' },
position: 'relative',
}}
>
<Stack spacing={1}>
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
Equipment
</Typography>
<Box sx={{ width: '70%' }}>
<Typography fontSize="16px" color="text.secondary" fontWeight={500}>
Enter your kitchen equipment details to complete your restaurant registration.
</Typography>
</Box>
{/* Dynamic Equipment Fields */}
{Object.keys(equipment).map((key) => (
<InputField
key={key}
label={key.charAt(0).toUpperCase() + key.slice(1)}
placeholder="0"
theme={theme}
value={equipment[key]}
onChange={(val) => handleInputChange(key, val)}
error={errors[key]}
/>
))}
{/* Add More Equipment Button */}
<Box>
<Button
variant="text"
fullWidth
startIcon={<AddIcon />}
sx={{
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: theme.palette.primary.main,
'&:hover': { backgroundColor: 'transparent' }
}}
onClick={() => setModalOpen(true)}
>
Add More
</Button>
</Box>
{/* Next Button */}
<Box>
<Button
variant="contained"
fullWidth
onClick={handleNextClick}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': { backgroundColor: theme.palette.primary.hover },
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
{/* Modal for Adding Equipment */}
<Modal open={modalOpen} onClose={() => setModalOpen(false)}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
borderRadius: 2,
boxShadow: 24,
p: 4,
}}
>
<Typography variant="h6" sx={{ mb: 2 }}>
Add New Equipment
</Typography>
<TextField
fullWidth
label="Equipment Name"
value={newEquipment.name}
onChange={(e) => setNewEquipment({ ...newEquipment, name: e.target.value })}
sx={{ mb: 2 }}
/>
<TextField
fullWidth
label="Quantity"
type="number"
value={newEquipment.quantity}
onChange={(e) => setNewEquipment({ ...newEquipment, quantity: e.target.value })}
sx={{ mb: 2 }}
/>
<Button
variant="contained"
fullWidth
onClick={handleAddEquipment}
sx={{
color: '#fff',
fontWeight: 600,
borderRadius: '50px',
textTransform: 'none',
}}
>
Add
</Button>
</Box>
</Modal>
</Box>
);
};
const InputField = ({ label, placeholder, theme, value, onChange, error }) => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px', color: 'black' }}>
{label}
</Typography>
<TextField
placeholder={placeholder}
variant="outlined"
fullWidth
value={value}
onChange={(e) => onChange(e.target.value)}
error={!!error}
helperText={error || ' '}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255,145,77,0.1)',
}
}}
/>
</Box>
);
export default Equipment;
@@ -0,0 +1,182 @@
import React, { useState } from 'react';
import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material';
const OperationalCapacity = ({ formData, updateFormData, onNext, onBack }) => {
const theme = useTheme();
const [capacity, setCapacity] = useState({
Maximum_orders_per_day: formData.Maximum_orders_per_day || '',
Number_of_Cheff: formData.Number_of_Cheff || '',
Number_of_Waiters: formData.Number_of_Waiters || '',
Number_of_Cookers: formData.Number_of_Cookers || '',
});
const [errors, setErrors] = useState({});
const handleChange = (field, value) => {
if (/^\d*$/.test(value)) { // السماح فقط بالأرقام
setCapacity(prev => ({ ...prev, [field]: value }));
}
};
const validateFields = () => {
const newErrors = {};
Object.entries(capacity).forEach(([key, value]) => {
if (!value || isNaN(Number(value)) || Number(value) < 0) {
newErrors[key] = 'Please enter a positive number';
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextClick = () => {
if (validateFields()) {
updateFormData(capacity); // تحديث الأب عند الضغط على Next فقط
if (onNext) onNext();
}
};
const InputField = ({ label, value, onChange, placeholder, error }) => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: 500, fontSize: '16px' }}>
{label}
</Typography>
<TextField
type="number"
placeholder={placeholder}
variant="outlined"
fullWidth
value={value}
onChange={(e) => onChange(e.target.value)}
error={!!error}
helperText={error || ' '}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
}
}}
/>
</Box>
);
return (
<Box
sx={{
height: { xs: '90%', sm: '100%', md: 740 },
backgroundColor: '#FFFFFF',
px: 4,
pt: { xs: 4, md: 12 },
pb: 4,
display: 'block',
borderRadius: 2,
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
position: 'relative',
width: { xs: '85%', sm: '90%' },
}}
>
<Stack spacing={1.5}>
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
Operational Capacity
</Typography>
<Box sx={{ width: '70%' }}>
<Typography fontSize="16px" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
Enter your basic information to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
<InputField
label="Maximum Orders Per Day"
value={capacity.Maximum_orders_per_day}
onChange={(val) => handleChange('Maximum_orders_per_day', val)}
placeholder="5000"
error={errors.Maximum_orders_per_day}
/>
<InputField
label="Number of Chefs"
value={capacity.Number_of_Cheff}
onChange={(val) => handleChange('Number_of_Cheff', val)}
placeholder="20"
error={errors.Number_of_Cheff}
/>
<InputField
label="Number of Waiters"
value={capacity.Number_of_Waiters}
onChange={(val) => handleChange('Number_of_Waiters', val)}
placeholder="200"
error={errors.Number_of_Waiters}
/>
<InputField
label="Number of Kitchen Assistants"
value={capacity.Number_of_Cookers}
onChange={(val) => handleChange('Number_of_Cookers', val)}
placeholder="200"
error={errors.Number_of_Cookers}
/>
{/* Buttons */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleNextClick}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': { backgroundColor: theme.palette.primary.hover },
}}
>
Next
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default OperationalCapacity;
@@ -0,0 +1,157 @@
import React, { useState, useEffect } from 'react';
import { Box, Typography, Stack, Button, useTheme, FormHelperText, TextField, Select, MenuItem } from '@mui/material';
const TypeOfRestaurant = ({ formData, updateFormData, onBack, onRegister }) => {
const theme = useTheme();
const [typeData, setTypeData] = useState({
ownRestaurantType: formData.ownRestaurantType || '',
collaborateRestaurantType: formData.collaborateRestaurantType || ''
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const collaborationOptions = [
{ value: 'fine_dining', label: 'Fine Dining' },
{ value: 'catering_services', label: 'Catering Services' },
{ value: 'fast_food', label: 'Fast Food' },
{ value: 'cloud_kitchen', label: 'Cloud Kitchen' }
];
useEffect(() => {
updateFormData(typeData);
}, [typeData]);
const handleChange = (field, value) => setTypeData(prev => ({ ...prev, [field]: value }));
const validateFields = () => {
const newErrors = {};
if (!typeData.ownRestaurantType) newErrors.ownRestaurantType = 'This field is required';
if (!typeData.collaborateRestaurantType) newErrors.collaborateRestaurantType = 'This field is required';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleRegister = async () => {
if (!validateFields()) return;
setIsSubmitting(true);
try {
// استدعاء الدالة المرسلة من الأب لتسجيل المطعم
await onRegister();
} catch (error) {
console.error('Registration failed:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<Box sx={{ height: { xs: '90%', sm: '100%', md: 700 }, backgroundColor: '#FFFFFF', px: 4, pt: { xs: 4, md: 19.5 }, pb: { xs: 20, sm: 20, md: 0 }, display: 'block', borderRadius: 2, boxShadow: '0px 1px 4px rgba(0,0,0,0.05)', position: 'relative', width: { xs: '85%', sm: '90%' } }}>
<Stack spacing={2.5}>
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
Type of Restaurant
</Typography>
<Box sx={{ width: '70%' }}>
<Typography fontSize="16px" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
Enter your restaurant type you want to collaborate to proceed to registration of your own restaurant on this platform
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: 500, fontSize: '16px' }}>
Open to Host Restaurant/Cousin type*
</Typography>
<TextField
placeholder="Enter your restaurant type"
variant="outlined"
fullWidth
value={typeData.ownRestaurantType}
onChange={(e) => handleChange('ownRestaurantType', e.target.value)}
error={!!errors.ownRestaurantType}
helperText={errors.ownRestaurantType || ' '}
sx={{
'& input': { fontWeight: 500, fontSize: '15px' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
'&.Mui-focused fieldset': { borderColor: theme.palette.primary.main }
}
}}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 2 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: 500, fontSize: '16px' }}>
Select Restaurant type Willing to Collaborate With*
</Typography>
<Select
value={typeData.collaborateRestaurantType}
onChange={(e) => handleChange('collaborateRestaurantType', e.target.value)}
displayEmpty
fullWidth
sx={{
borderRadius: '10px',
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: theme.palette.primary.main },
'& .MuiSelect-select': { fontWeight: 500, fontSize: '15px', padding: '12px' }
}}
>
<MenuItem value="" disabled>Select an option</MenuItem>
{collaborationOptions.map(opt => (
<MenuItem key={opt.value} value={opt.value}>{opt.label}</MenuItem>
))}
</Select>
<FormHelperText error>{errors.collaborateRestaurantType || ' '}</FormHelperText>
</Box>
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleRegister}
disabled={isSubmitting}
sx={{
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': { backgroundColor: theme.palette.primary.hover }
}}
>
{isSubmitting ? 'Registering...' : 'Register'}
</Button>
<Button
variant="outlined"
fullWidth
onClick={onBack}
sx={{
mt: 2,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
borderRadius: '50px',
textTransform: 'none',
display: { xs: 'block', sm: 'block', md: 'none' },
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.light,
borderColor: theme.palette.primary.main,
},
}}
>
Back
</Button>
</Box>
</Stack>
</Box>
);
};
export default TypeOfRestaurant;
@@ -0,0 +1,403 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
TextField,
Button,
Select,
MenuItem,
Checkbox,
ListItemText,
OutlinedInput,
styled,
useTheme,
LinearProgress
} from '@mui/material';
import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined';
import authService from '../../../services/authService';
import { useRestaurant } from '../../../contexts/RestaurantContext';
import AddIcon from "@mui/icons-material/Add";
const AccountProfile = () => {
const theme = useTheme();
const { restaurantId } = useRestaurant();
const [editMode, setEditMode] = useState(false);
const [profileImage, setProfileImage] = useState(null);
const [form, setForm] = useState({
restaurant_name: '',
restaurant_type: '',
Address: '',
City: '',
Postal_code: '',
Phone: '',
Email: '',
Operation_hour: '',
host_type: '',
collaboration_type: '',
closed_days: [],
equipment: [], // array of {name, quantity}
Maximum_orders_per_day: 0,
Number_of_Cheff: 0,
Number_of_Waiters: 0,
Number_of_Cookers: 0,
});
const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
const Input = styled('input')({ display: 'none' });
useEffect(() => {
const fetchProfile = async () => {
if (!restaurantId) return;
const data = await authService.getRestaurantProfile(restaurantId);
console.log("PROFILE DATA ===>", data); // 👈 يوضح الأسماء الحقيقية
if (data) {
setForm({
restaurant_name: data.restaurant_name,
restaurant_type: data.restaurant_type,
Address: data.Address,
City: data.City,
Postal_code: data.Postal_code,
Phone: data.Phone,
Email: data.Email,
Operation_hour: data.Operation_hour,
host_type: data.host_type,
collaboration_type: data.collaboration_type,
closed_days: data.closed_days || [],
equipment: Object.entries(data.equipment || {}).map(([name, quantity]) => ({ name, quantity })),
Maximum_orders_per_day: data.Maximum_orders_per_day ?? data.maximum_orders_per_day ?? 0,
Number_of_Cheff: data.Number_of_Cheff ?? data.number_of_cheff ?? 0,
Number_of_Waiters: data.Number_of_Waiters ?? data.number_of_waiters ?? 0,
Number_of_Cookers: data.Number_of_Cookers ?? data.number_of_cookers ?? 0,
});
}
};
fetchProfile();
}, [restaurantId]);
const handleImageUpload = (event) => {
if (event.target.files && event.target.files[0]) {
setProfileImage(event.target.files[0]);
}
};
const handleChange = (field, value) => setForm(prev => ({ ...prev, [field]: value }));
const handleEquipmentChange = (index, key, value) => {
const newEquipment = [...form.equipment];
newEquipment[index][key] = value;
setForm(prev => ({ ...prev, equipment: newEquipment }));
};
const addEquipment = () => {
setForm(prev => ({
...prev,
equipment: [...prev.equipment, { name: '', quantity: 0 }]
}));
};
const removeEquipment = (index) => {
const newEquipment = [...form.equipment];
newEquipment.splice(index, 1);
setForm(prev => ({ ...prev, equipment: newEquipment }));
};
const renderField = (label, value, fieldKey, options = null) => (
<Box
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 1,
pb: 1, // padding bottom
borderBottom: '1px solid #ccc' // الخط الرمادي تحت كل حقل
}}
>
<Typography
variant="body2"
sx={{
fontWeight: 600,
fontSize: '18px',
color: 'black'
}}
>
{label}
</Typography>
{editMode ? (
options ? (
<Select
multiple
value={form[fieldKey]}
onChange={(e) => handleChange(fieldKey, e.target.value)}
input={<OutlinedInput label={label} />}
renderValue={(selected) => selected.join(', ')}
>
{options.map(day => (
<MenuItem key={day} value={day}>
<Checkbox checked={form[fieldKey].includes(day)} />
<ListItemText primary={day.charAt(0).toUpperCase() + day.slice(1)} />
</MenuItem>
))}
</Select>
) : (
<TextField
value={value}
onChange={(e) => handleChange(fieldKey, e.target.value)}
fullWidth
variant="outlined"
sx={{
'& input': { fontWeight: 500, fontSize: '16px', color: 'black' },
'& input::placeholder': { color: '#969BA7' },
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
transition: '0.3s',
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
},
},
}}
/>
)
) : (
<Typography
variant="body1"
sx={{
fontWeight: 500,
fontSize: '16px',
color: 'gray'
}}
>
{Array.isArray(value) ? value.join(', ') : value}
</Typography>
)}
</Box>
);
const handleSave = async () => {
if (!restaurantId) return;
const equipmentObj = {};
form.equipment.forEach(eq => {
if (eq.name) equipmentObj[eq.name] = eq.quantity;
});
const payload = {
...form,
equipment: equipmentObj
};
if (profileImage) payload.logo = profileImage;
const result = await authService.updateRestaurantProfile(restaurantId, payload);
if (result.success) {
alert("Profile updated successfully!");
setEditMode(false);
const updated = await authService.getRestaurantProfile(restaurantId);
if (updated) setForm({
...updated,
equipment: Object.entries(updated.equipment || {}).map(([name, quantity]) => ({ name, quantity })),
});
} else {
alert(`Update failed: ${result.message}`);
}
};
// داخل AccountProfile
const collaborationOptions = [
{ value: 'fine_dining', label: 'Fine Dining' },
{ value: 'catering_services', label: 'Catering Services' },
{ value: 'fast_food', label: 'Fast Food' },
{ value: 'cloud_kitchen', label: 'Cloud Kitchen' }
];
return (
<Box sx={{ p: { xs: 2, sm: 3 }, backgroundColor: '#FFFFFF', borderRadius: 2, maxWidth: '100%' }}>
<Typography variant="h4" sx={{ mb: 3, fontWeight: 'bold' }}>Restaurant Profile</Typography>
{/* صورة البروفايل */}
{/* <Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<label htmlFor="icon-button-file">
<Input accept="image/*" id="icon-button-file" type="file" onChange={handleImageUpload} />
<Box
component="span"
sx={{
width: 110, height: 110, border: '2px dashed #4C535F', borderRadius: 2,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
color: '#4C535F', cursor: 'pointer', textAlign: 'center', backgroundColor: '#f6f6f6'
}}
>
<AddAPhotoOutlinedIcon sx={{ fontSize: 28, mb: 1 }} />
<Typography sx={{ fontSize: '13px', fontWeight: 500 }}>Upload your photo</Typography>
</Box>
</label>
</Box> */}
<LinearProgress
variant="determinate"
value={100}
sx={{
mt: 2,
mb: 3,
height: 4,
borderRadius: 2,
backgroundColor: "#f0f0f0",
"& .MuiLinearProgress-bar": { backgroundColor: theme.palette.primary.main },
}}
/>
{/* الحقول */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 ,p:3, display: 'flex', flexWrap: 'wrap', border: '1px solid #e0e0e0', borderRadius: 2, boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)'}}>
<Box sx={{ display: 'flex', gap: 2 }}>
{renderField('Restaurant Name', form.restaurant_name, 'restaurant_name')}
{renderField('Restaurant Type', form.restaurant_type, 'restaurant_type')}
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
{renderField('Address', form.Address, 'Address')}
{renderField('City', form.City, 'City')}
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
{renderField('Postal Code', form.Postal_code, 'Postal_code')}
{renderField('Phone', form.Phone, 'Phone')}
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
{renderField('Email', form.Email, 'Email')}
{renderField('Operation Hours', form.Operation_hour, 'Operation_hour')}
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
{renderField('Host Type', form.host_type, 'host_type')}
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: '500', fontSize: '16px' }}>Collaboration Type</Typography>
{editMode ? (
<Select
value={form.collaboration_type || ''}
onChange={(e) => handleChange('collaboration_type', e.target.value)}
displayEmpty
fullWidth
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
}
}}
>
<MenuItem value="">
{/* <em>None</em> اختياري */}
</MenuItem>
{collaborationOptions.map(option => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
) : (
<Typography variant="body1" sx={{ fontWeight: 500, fontSize: '15px' }}>
{form.collaboration_type ? collaborationOptions.find(opt => opt.value === form.collaboration_type)?.label : 'None'}
</Typography>
)}
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
{renderField('Closed Days', form.closed_days, 'closed_days', daysOfWeek)}
</Box>
{/* القدرة التشغيلية */}
<Box sx={{ display: 'flex', gap: 2 }}>
{renderField('Maximum Orders per Day', form.Maximum_orders_per_day, 'Maximum_orders_per_day')}
{renderField('Number of Chefs', form.Number_of_Cheff, 'Number_of_Cheff')}
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
{renderField('Number of Waiters', form.Number_of_Waiters, 'Number_of_Waiters')}
{renderField('Number of Cookers', form.Number_of_Cookers, 'Number_of_Cookers')}
</Box>
{/* المعدات */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: '500', fontSize: '16px' }}>Equipment Name</Typography>
{form.equipment.map((eq, index) => (
<Box key={index} sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<TextField
value={eq.name}
onChange={(e) => handleEquipmentChange(index, 'name', e.target.value)}
fullWidth
variant="outlined"
disabled={!editMode}
sx={{ '& input': { fontWeight: 500, fontSize: '16px' } }}
/>
<TextField
label="Quantity"
type="number"
value={eq.quantity}
onChange={(e) => handleEquipmentChange(index, 'quantity', Number(e.target.value))}
fullWidth
variant="outlined"
disabled={!editMode}
sx={{ '& input': { fontWeight: 500, fontSize: '16px' } ,borderRadius:20}}
/>
{editMode && (
<Button color="error" onClick={() => removeEquipment(index)}>
Delete
</Button>
)}
</Box>
))}
{editMode && (
<Button variant="outlined" onClick={addEquipment} startIcon={<AddIcon/>} sx={{ mt: 1, width: 'fit-content' , textTransform:'none' }}>
Add Equipment
</Button>
)}
</Box>
</Box>
{/* أزرار التحكم */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, flexWrap: 'wrap', mt: 3 }}>
{editMode ? (
<>
<Button
variant="outlined"
onClick={() => setEditMode(false)}
sx={{ px: 4, py: 1.5, borderRadius: 2, textTransform: 'none', fontWeight: 'bold' }}
>
Cancel
</Button>
<Button
variant="contained"
onClick={handleSave}
sx={{
px: 4, py: 1.5, borderRadius: 2,
textTransform: 'none', fontWeight: 'bold',
color: '#FFF', backgroundColor: theme.palette.primary.main
}}
>
Save
</Button>
</>
) : (
<Button
variant="contained"
onClick={() => setEditMode(true)}
sx={{
px: 4, py: 1.5, borderRadius: 2,
textTransform: 'none', fontWeight: 'bold',
color: '#FFF', backgroundColor: theme.palette.primary.main
}}
>
Edit Profile
</Button>
)}
</Box>
</Box>
);
};
export default AccountProfile;
+101
View File
@@ -0,0 +1,101 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import AccountSettings from './AccountProfile';
const drawerWidth = 230;
const Setting = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
useEffect(() => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
}, [theme.breakpoints.values.md]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
const handleDrawerToggle = () => {
setSidebarOpen(!sidebarOpen);
};
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: '100%',
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: 2, sm: 3 },
pb: { xs: 2, sm: 4 },
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
transition: theme.transitions.create(['margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}>
<AccountSettings />
</Box>
</Box>
</Box>
);
};
export default Setting;
+214
View File
@@ -0,0 +1,214 @@
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import {
Drawer,
List,
ListItem,
ListItemIcon,
ListItemText,
Box,
useTheme,
Typography
} from '@mui/material';
import DashboardIcon from '@mui/icons-material/DashboardRounded';
import AnalyticsIcon from '@mui/icons-material/AutoGraph';
import RestaurantIcon from '@mui/icons-material/Restaurant';
import Employ from '@mui/icons-material/CountertopsTwoTone';
import CreateKitchenIcon from '@mui/icons-material/Queue';
import LogoutIcon from '@mui/icons-material/Logout';
import CategoryIcon from '@mui/icons-material/Category';
import authService from '../../services/authService';
const menuItems = [
{ text: 'Order & Occupancy', icon: <DashboardIcon />, path: '/dashboard' },
{ text: 'Tabels & Reservations', icon: <AnalyticsIcon />, path: '/analytics' },
{ text: 'Restaurant Profile', icon: <RestaurantIcon />, path: '/profile' },
{ text: 'Employ', icon: <Employ />, path: '/employ' },
{ text: 'Create Kitchen', icon: <CreateKitchenIcon />, path: '/create-kitchen' },
{ text: 'Catigores & Meals', icon: <CategoryIcon />, path: '/categori-meal' },
];
const bottomItems = [
{ text: 'Log Out', icon: <LogoutIcon />, path: '/login' },
];
const Sidebar = ({ open, onClose, isMobile, drawerWidth }) => {
const theme = useTheme();
const navigate = useNavigate();
const location = useLocation();
const handleLogout = async () => {
const result = await authService.logout();
localStorage.removeItem('token');
localStorage.removeItem('refresh_token');
navigate('/login', { replace: true });
if (isMobile) onClose();
};
const renderListItems = (items) =>
items.map((item, index) => {
const isActive = location.pathname === item.path;
return (
<ListItem key={index} disablePadding sx={{ px: 1 }}>
<Box
onClick={() => {
if (item.text === 'Log Out') {
handleLogout();
} else {
navigate(item.path);
if (isMobile) onClose();
}
}}
sx={{
display: 'flex',
alignItems: 'center',
width: '100%',
borderRadius: '5px',
backgroundColor: isActive ? '#F8F6F8' : 'transparent',
px: 2,
py: 1,
cursor: 'pointer',
transition: 'background-color 0.2s',
'&:hover': {
backgroundColor: '#fffcf9d5',
},
}}
>
<ListItemIcon
sx={{
color: isActive ? theme.palette.primary.main : '#A6ACB8',
minWidth: 36,
}}
>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.text}
primaryTypographyProps={{
sx: {
color: isActive ? theme.palette.primary.main : '#61677F',
fontSize: '0.875rem',
},
}}
/>
</Box>
</ListItem>
);
});
const drawer = (
<Box
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
bgcolor: 'background.paper',
}}
>
<Box
sx={{
px: 2,
py: 1.5,
borderColor: 'divider',
position: 'sticky',
top: 0,
zIndex: 1,
bgcolor: 'background.paper',
}}
>
<Box display="flex" alignItems="center">
<Box
component="img"
src="/image.png"
alt="logo"
sx={{
width: 40,
height: 40,
objectFit: 'contain',
mr: 1.5,
}}
/>
<Box display="flex" alignItems="center">
<Typography
variant="h6"
sx={{
fontWeight: 400,
fontSize: '1.25rem',
color: 'text.primary',
}}
>
KITCH
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 400,
fontSize: '1.25rem',
color: 'primary.main',
ml: 0.5,
}}
>
PLUS
</Typography>
</Box>
</Box>
</Box>
<Box
sx={{
flex: 1,
overflowY: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
}}
>
<List>{renderListItems(menuItems)}</List>
</Box>
<Box
sx={{
position: 'sticky',
bottom: 0,
bgcolor: 'background.paper',
borderColor: 'divider',
}}
>
<List>{renderListItems(bottomItems)}</List>
</Box>
</Box>
);
return (
<Box
component="nav"
sx={{
width: { xs: 0, sm: 0, md: drawerWidth },
flexShrink: { xs: 0, sm: 0, md: 0 },
boxShadow: 'none',
}}
>
<Drawer
variant={isMobile ? 'temporary' : 'persistent'}
open={open}
onClose={onClose}
ModalProps={{ keepMounted: true }}
sx={{
'& .MuiDrawer-paper': {
width: drawerWidth,
boxSizing: 'border-box',
borderRight: 'none',
boxShadow: 'none',
},
}}
>
{drawer}
</Drawer>
</Box>
);
};
export default Sidebar;
+71
View File
@@ -0,0 +1,71 @@
// SimplePagination.jsx
import React from 'react';
import { Box, IconButton, useTheme } from '@mui/material';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
const theme = useTheme();
const handlePrev = () => {
if (currentPage > 1) onChange(currentPage - 1);
};
const handleNext = () => {
if (currentPage < pageCount) onChange(currentPage + 1);
};
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
onClick={handlePrev}
disabled={currentPage <= 1}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
backgroundColor: theme.palette.primary.main,
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
fontSize: 14,
userSelect: 'none',
}}
>
{currentPage}
</Box>
<IconButton
size="small"
onClick={handleNext}
disabled={currentPage >= pageCount}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
</Box>
);
};
export default SimplePagination;
+230
View File
@@ -0,0 +1,230 @@
import React, { useState, useEffect } from "react";
import { Box, useTheme, useMediaQuery } from "@mui/material";
import KitchPlusAppBar from "../AppBar";
import Sidebar from "../SideHome";
import AccountSettings from "./contect/AccountSettings";
import AllProducts from "./contect/AllProducts";
import PopularProductSection from "./contect/PopularProductSection";
import ProductDetail from "./contect/ProductDetail";
import api from "../../../services/authService";
const IMAGE_URL = "http://127.0.0.1:8000/storage/";
const drawerWidth = 230;
const Supplier = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const [categories, setCategories] = useState([]);
const [products, setProducts] = useState([]);
const [selectedCategory, setSelectedCategory] = useState(null);
const [selectedCategoryName, setSelectedCategoryName] = useState(null);
const [selectedProduct, setSelectedProduct] = useState(null);
const [showAllPopular, setShowAllPopular] = useState(false);
const [showAllTopItems, setShowAllTopItems] = useState(false);
useEffect(() => {
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
}, [theme.breakpoints.values.md]);
useEffect(() => {
const handleResize = () => {
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [theme.breakpoints.values.md]);
useEffect(() => {
const fetchCategories = async () => {
try {
const res = await api.getAllCategory();
if (res.success) {
const formatted = res.data.map((category) => ({
id: category.id,
name: category.name,
icon: category.image
? IMAGE_URL + category.image
: "/images/default-product.png",
}));
setCategories(formatted);
}
} catch (err) {
console.error("❌ Error fetching categories:", err);
}
};
fetchCategories();
}, []);
useEffect(() => {
const fetchProducts = async () => {
if (!selectedCategory) return;
try {
const res = await api.getProductsByCategory(selectedCategory, 1);
if (res.success) {
const formatted = res.data.map((product) => ({
id: product.id,
name: product.name,
category: product.supplier_category_id,
price: product.price,
unit: "Kg",
image: product.image
? IMAGE_URL + product.image
: "/images/default-product.png",
}));
setProducts(formatted);
const categoryName = categories.find(
(cat) => cat.id === selectedCategory
)?.name;
setSelectedCategoryName(categoryName || "");
}
} catch (err) {
console.error("Error fetching products:", err);
}
};
fetchProducts();
}, [selectedCategory, categories]);
const handleDrawerToggle = () => {
setSidebarOpen(!sidebarOpen);
};
const toggleShowAllPopular = () => {
setShowAllPopular((prev) => !prev);
};
const toggleShowAllTopItems = () => {
setShowAllTopItems((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 },
scrollbarWidth: "none",
"&::-webkit-scrollbar": { display: "none" },
}}
>
{selectedProduct ? (
<ProductDetail
product={selectedProduct}
onBack={() => setSelectedProduct(null)}
/>
) : (
<>
<AccountSettings
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
categories={categories}
/>
{selectedCategory ? (
<Box sx={{ mt: 3 }}>
<AllProducts
products={products}
showAll={true}
onToggleShowAll={() => setSelectedCategory(null)}
onProductClick={setSelectedProduct}
categoryName={selectedCategoryName}
/>
</Box>
) : !showAllPopular && !showAllTopItems ? (
<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: 10,
}}
>
<PopularProductSection
products={products}
showAll={showAllPopular}
onToggleShowAll={toggleShowAllPopular}
onProductClick={setSelectedProduct}
/>
</Box>
</Box>
) : showAllPopular ? (
<Box sx={{ mt: 3, width: "100%" }}>
<PopularProductSection
products={products}
showAll={showAllPopular}
onToggleShowAll={toggleShowAllPopular}
onProductClick={setSelectedProduct}
/>
</Box>
) : showAllTopItems ? (
<Box sx={{ mt: 3, width: "100%" }}>
</Box>
) : null}
</>
)}
</Box>
</Box>
</Box>
);
};
export default Supplier;
+247
View File
@@ -0,0 +1,247 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
// بدل هذه المكونات عند عرض التفاصيل
import AccountSettings from './contect/AccountSettings';
import AllProducts from './contect/AllProducts';
import PopularProductSection from './contect/PopularProductSection';
import TopItems from './contect/TopItems';
import OrderList from './contect/OrderList';
import ProductDetail from './contect/ProductDetail'; // 👈 مكون التفاصيل
//import auth servise
import api from '../../../services/authService';
const IMAGE_URL = 'http://127.0.0.1:8000/storage/' ;
const categoriesInit0 = await api.getAllCategory();
const categoriesInit = categoriesInit0.data.map(category => ({
id: category.id,
name: category.name,
icon: category.image || "/images/default-product.png",
}));
const productList0 = await api.getAllProduct();
const productList = productList0.data.map(product => ({
id: product.id,
name: product.name,
category: product.supplier_category_id,
price: product.price,
unit: "Kg",
image: IMAGE_URL + product.image || "/images/default-product.png",
}));
// const productList = [
// { id: 1, name: 'Cabbage 1', category: 'Vegetable', price: '15.10', unit: '/kg', image: '/images/waitress2.png' },
// { id: 2, name: 'vegetables 2', category: 'Vegetable', price: '8.34', unit: '/kg', image: '/images/waitress2.png' },
// { id: 3, name: 'Brocoly 3', category: 'Vegetable', price: '5.60', unit: '/kg', image: '/images/waitress2.png' },
// { id: 4, name: 'Onion 4', category: 'Vegetable', price: '6.45', unit: '/kg', image: '/images/waitress2.png' },
// { id: 5, name: 'Bread 5', category: 'Bread', price: '3.50', unit: '/pcs', image: '/images/waitress2.png' },
// { id: 6, name: 'Meat 6', category: 'Meat', price: '20.00', unit: '/kg', image: '/images/waitress2.png' },
// { id: 7, name: 'Cabbage', category: 'Vegetable', price: '15.10', unit: '/kg', image: '/images/waitress2.png' },
// { id: 8, name: 'vegetables', category: 'Vegetable', price: '8.34', unit: '/kg', image: '/images/waitress2.png' },
// { id: 9, name: 'Brocoly', category: 'Vegetable', price: '5.60', unit: '/kg', image: '/images/waitress2.png' },
// { id: 10, name: 'Onion', category: 'Vegetable', price: '6.45', unit: '/kg', image: '/images/waitress2.png' },
// { id: 11, name: 'Bread', category: 'Bread', price: '3.50', unit: '/pcs', image: '/images/waitress2.png' },
// { id: 12, name: 'Meat', category: 'Meat', price: '20.00', unit: '/kg', image: '/images/waitress2.png' },
// { id: 13, name: 'Cabbage', category: 'Vegetable', price: '15.10', unit: '/kg', image: '/images/waitress2.png' },
// { id: 14, name: 'vegetables', category: 'Vegetable', price: '8.34', unit: '/kg', image: '/images/waitress2.png' },
// { id: 15, name: 'Brocoly', category: 'Vegetable', price: '5.60', unit: '/kg', image: '/images/waitress2.png' },
// { id: 16, name: 'Onion', category: 'Vegetable', price: '6.45', unit: '/kg', image: '/images/waitress2.png' },
// { id: 17, name: 'Bread', category: 'Bread', price: '3.50', unit: '/pcs', image: '/images/waitress2.png' },
// { id: 18, name: 'Meat', category: 'Meat', price: '20.00', unit: '/kg', image: '/images/waitress2.png' },
// ];
const item = [
{ name: 'Cabbage', price: '15.10', unit: '/kg', image: '/images/cabbage.png' },
{ name: 'Kale vegetables', price: '8.34', unit: '/kg', image: '/images/kale.png' },
{ name: 'Brocoly', price: '5.60', unit: '/kg', image: '/images/broccoli.png' },
{ name: 'Celery', price: '4.80', unit: '/kg', image: '/images/celery.png' },
{ name: 'Onion', price: '6.45', unit: '/kg', image: '/images/onion.png' },
{ name: 'Garlic', price: '9.90', unit: '/kg', image: '/images/garlic.png' },
];
const drawerWidth = 230;
const Supplier = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
// حالات العرض
const [showAllPopular, setShowAllPopular] = useState(false);
const [showAllTopItems, setShowAllTopItems] = useState(false);
const [selectedCategory, setSelectedCategory] = useState(null);
// ✅ المنتج المختار
const [selectedProduct, setSelectedProduct] = useState(null);
useEffect(() => {
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
}, [theme.breakpoints.values.md]);
useEffect(() => {
const handleResize = () => {
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
const handleDrawerToggle = () => {
setSidebarOpen(!sidebarOpen);
};
const toggleShowAllPopular = () => {
setShowAllPopular(prev => !prev);
};
const toggleShowAllTopItems = () => {
setShowAllTopItems(prev => !prev);
};
// اسم الفئة
const selectedCategoryName = selectedCategory
? categoriesInit.find(cat => cat.id === selectedCategory)?.name
: null;
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 },
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
}}
>
{/* ✅ إذا تم اختيار منتج نظهر تفاصيله فقط */}
{selectedProduct ? (
<ProductDetail product={selectedProduct} onBack={() => setSelectedProduct(null)} />
) : (
<>
<AccountSettings
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
/>
{selectedCategory ? (
<Box sx={{ mt: 3 }}>
<AllProducts
products={productList.filter(
(p) => p.category.toLowerCase() === selectedCategoryName?.toLowerCase()
)}
showAll={true}
onToggleShowAll={() => setSelectedCategory(null)}
onProductClick={setSelectedProduct}
categoryName={selectedCategoryName} // ✅ بدل القيمة الثابتة
/>
</Box>
) : !showAllPopular && !showAllTopItems ? (
<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: 10,
}}
>
<PopularProductSection
products={productList}
showAll={showAllPopular}
onToggleShowAll={toggleShowAllPopular}
onProductClick={setSelectedProduct}
/>
{/* <Box sx={{ mt: 3 }}>
<TopItems
item={item}
showAll={showAllTopItems}
onToggleShowAll={toggleShowAllTopItems}
onProductClick={setSelectedProduct}
/>
</Box> */}
</Box>
{/* <Box
sx={{
flexBasis: { xs: '100%', md: '40%' },
display: 'flex',
flexDirection: 'column',
pb: 10,
pr: { xs: 3, md: 0 },
}}
>
<Box sx={{ height: '100%' }}>
<OrderList />
</Box>
</Box> */}
</Box>
) : showAllPopular ? (
<Box sx={{ mt: 3, width: '100%' }}>
<PopularProductSection
products={productList}
showAll={showAllPopular}
onToggleShowAll={toggleShowAllPopular}
onProductClick={setSelectedProduct}
/>
</Box>
) : showAllTopItems ? (
<Box sx={{ mt: 3, width: '100%' }}>
{/* <TopItems
item={item}
showAll={showAllTopItems}
onToggleShowAll={toggleShowAllTopItems}
onProductClick={setSelectedProduct}
/> */}
</Box>
) : null}
</>
)}
</Box>
</Box>
</Box>
);
};
export default Supplier;
@@ -0,0 +1,307 @@
import React, { useState, useRef, useEffect } from 'react';
import {
Menu,
MenuItem,
Modal,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
useMediaQuery,
Box,
Typography,
Button,
useTheme,
IconButton,
} from '@mui/material';
import AddCategory from './AddCategory'; // مكون لإضافة/تعديل الفئة (يفترض موجود)
import AddIcon from '@mui/icons-material/Add';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import TuneIcon from '@mui/icons-material/Tune';
import CategoryScrollList from './CategoryScrollList'; // مكون لعرض الفئات بشكل أفقي مع تمرير
//import auth servise
import api from '../../../../services/authService';
const IMAGE_URL = 'http://127.0.0.1:8000/storage/' ;
const categoriesInit0 = await api.getAllCategory();
const categoriesInit = categoriesInit0.data.map(category => (
{
id: category.id,
name: category.name,
icon: IMAGE_URL + category.icon || "/images/default-product.png",
}));
const AccountSettings = ({ selectedCategory, setSelectedCategory }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [categories, setCategories] = useState(categoriesInit);
const [selectedCategoryForEdit, setSelectedCategoryForEdit] = useState(null);
const [contextMenu, setContextMenu] = useState(null);
const [openAddModal, setOpenAddModal] = useState(false);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const scrollRef = useRef();
const scroll = (offset) => {
if (scrollRef.current) {
scrollRef.current.scrollLeft += offset;
}
};
const handleAddCategory = (newCategory) => {
setCategories((prev) => [...prev, { ...newCategory, id: Date.now() }]);
setOpenAddModal(false);
};
const handleUpdateCategory = (updatedCategory) => {
setCategories((prev) =>
prev.map((cat) =>
cat.id === selectedCategoryForEdit.id ? { ...cat, ...updatedCategory } : cat
)
);
setOpenAddModal(false);
};
const handleDeleteCategory = () => {
setCategories((prev) => prev.filter((cat) => cat.id !== selectedCategoryForEdit.id));
setConfirmDeleteOpen(false);
setSelectedCategoryForEdit(null);
if (selectedCategory === selectedCategoryForEdit?.id) {
setSelectedCategory(null); // أبطل اختيار الفئة
}
};
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: 160 },
}}
>
{/* رأس القسم */}
<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> */}
<Button
variant="outlined"
sx={{
textTransform: 'none',
color: '#667085',
borderColor: '#e0e0e0',
backgroundColor: '#fff',
borderRadius: '8px',
height: '40px',
width: { xs: '100%', sm: '120px', md: '99px' },
fontSize: { xs: '12px', sm: '13px', md: '14px' },
fontWeight: 600,
p: 0,
m: 0,
whiteSpace: 'nowrap',
minWidth: 'unset',
}}
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
>
{isMobile ? '' : 'Filters'}
</Button>
<IconButton
onClick={() => scroll(-200)}
sx={{
backgroundColor: theme.palette.primary.main,
color: '#fff',
width: 40,
height: 40,
'&:hover': { backgroundColor: '#e96b00' },
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<IconButton
onClick={() => scroll(200)}
sx={{
backgroundColor: theme.palette.primary.main,
color: '#fff',
width: 40,
height: 40,
'&:hover': { backgroundColor: '#e96b00' },
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
</Box>
</Box>
{/* قائمة التمرير للفئات */}
<Box
sx={{
width: { xs: '90%', sm: '95%', md: '100%' },
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center' },
mb: 3,
pl: 1,
pr: { xs: 1, sm: 4 },
flexDirection: { xs: 'column', sm: 'row' },
overflowX: 'hidden',
}}
>
<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,186 @@
import React, { useState, useEffect } from 'react';
import { Box, Button, TextField, Typography, IconButton } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined';
const AddCategory = ({ onAdd, editingCategory }) => {
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 handleRemoveImage = () => {
setProductImage(null);
};
const handleSubmit = () => {
const newCategory = {
name: productName,
description,
price,
discount,
icon: productImage,
};
if (onAdd) {
onAdd(newCategory);
}
// إعادة تعيين الحقول فقط عند الإضافة الجديدة
if (!editingCategory) {
setProductName('');
setDescription('');
setPrice('');
setDiscount('');
setProductImage(null);
}
};
return (
<Box p={2} maxWidth={400} mx="auto">
{/* ✅ صورة التصنيف */}
{productImage ? (
<Box
sx={{
position: 'relative',
width: '100%',
height: 150,
borderRadius: 2,
overflow: 'hidden',
mb: 2,
}}
>
<img
src={productImage}
alt="Preview"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
<IconButton
onClick={handleRemoveImage}
sx={{
position: 'absolute',
top: 4,
right: 4,
backgroundColor: 'rgba(0,0,0,0.5)',
color: 'white',
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.7)',
},
}}
size="small"
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
) : (
<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,
transition: 'border-color 0.3s',
'&:hover': {
borderColor: '#FF914D',
backgroundColor: '#fffaf5',
}
}}
>
Upload Menu Picture
<AddAPhotoOutlinedIcon sx={{ fontSize: 32, color: '#9e9e9e', mt: 1 }} />
<input
id="upload-image"
type="file"
accept="image/*"
hidden
onChange={handleImageUpload}
/>
</Box>
)}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.1, mt: 4 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Categories 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)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
borderColor: '#FF914D',
boxShadow: '0 0 0 2px rgba(255,145,77,0.15)'
}
}}
/>
</Box>
<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,93 @@
import React from "react";
import { Box, Typography } from "@mui/material";
import ProductCard from "./ProductCard";
const AllProducts = ({ products, showAll, onToggleShowAll, onProductClick, categoryName }) => {
const visibleProducts = showAll ? products : products.slice(0, 6);
return (
<Box
sx={{
mb: showAll ? { xs: 10, sm: 10 } : 0,
mr: showAll ? { xs: 2, sm: 3, md: 0 } : { 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",
maxWidth: showAll ? { xs: "90%", sm: "100%" } : undefined,
borderRadius: "10px",
padding: "20px",
display: "flex",
flexDirection: "column",
gap: "20px",
overflowY: showAll ? "auto" : "hidden",
}}
>
{/* العنوان */}
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Typography variant="h6" fontWeight={600}>
{categoryName ? categoryName : "All Products"}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
</Box>
<Typography
variant="body2"
color="primary"
sx={{ cursor: "pointer" }}
onClick={onToggleShowAll}
>
{showAll ? "Show Less" : "See All"}
</Typography>
</Box>
{/* عرض المنتجات أو رسالة لا يوجد منتجات */}
{visibleProducts.length === 0 ? (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "150px",
}}
>
<Typography variant="body1" color="text.secondary">
{categoryName
? `There is no product in ${categoryName}`
: "There is no product"}
</Typography>
</Box>
) : (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: 2,
}}
>
{visibleProducts.map((product, index) => (
<Box
key={index}
sx={{
// flex: "1 1 calc(33.333% - 16px)", // 3 منتجات بالصف في الديسكتوب
// minWidth: "250px", // أصغر حجم للمنتج
}}
onClick={() => onProductClick(product)}
>
<ProductCard
name={product.name}
price={product.price}
unit={product.unit}
image={product.image}
/>
</Box>
))}
</Box>
)}
</Box>
);
};
export default AllProducts;
@@ -0,0 +1,88 @@
import React from 'react';
import { Box, Avatar, 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) => (
<Box
key={cat.id}
sx={{
p: 1,
borderRadius: '12px',
backgroundColor: selectedCategory === cat.id
? `${theme.palette.primary.main}`
: '#F9F9FC',
border: selectedCategory === cat.id
? `0px solid ${theme.palette.primary.main}`
: '0px solid #ddd',
textAlign: 'center',
minWidth: '70px',
cursor: 'pointer',
flexShrink: 0,
}}
onClick={() => setSelectedCategory(cat.id)}
onContextMenu={(e) => {
e.preventDefault();
setSelectedCategoryForEdit(cat);
setContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6 });
}}
>
<Box
component="img"
src={cat.icon}
sx={{
width: 60,
height: 50,
mx: 'auto',
mb: 1,
}}
/>
<Typography
variant="body2"
fontWeight={500}
sx={{ color: selectedCategory === cat.id ? '#fff' : '#121212' }}
>
{cat.name}
</Typography>
</Box>
))}
</Box>
);
};
export default CategoryScrollList;
@@ -0,0 +1,359 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Chip,
IconButton,
TableContainer,
Button,
Menu,
MenuItem,
} from '@mui/material';
import TuneIcon from '@mui/icons-material/Tune';
import ReplayIcon from '@mui/icons-material/Replay';
import VisibilityIcon from '@mui/icons-material/Visibility';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { useTheme } from '@mui/material/styles';
import { useMediaQuery } from '@mui/material';
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
const theme = useTheme();
const handlePrev = () => {
if (currentPage > 1) onChange(currentPage - 1);
};
const handleNext = () => {
if (currentPage < pageCount) onChange(currentPage + 1);
};
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
onClick={handlePrev}
disabled={currentPage <= 1}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': {
color: '#ccc',
backgroundColor: '#FFF5E6',
},
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
backgroundColor: theme.palette.primary.main, // لون الخلفية من الثيم الرئيسي
color: '#fff', // لون الخط أبيض
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
fontSize: 14,
userSelect: 'none',
boxShadow: `0 0 0 1px ${theme.palette.primary.main}`,
}}
>
{currentPage}
</Box>
<IconButton
size="small"
onClick={handleNext}
disabled={currentPage >= pageCount}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': {
color: '#ccc',
backgroundColor: '#FFF5E6',
},
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
</Box>
);
};
const OrderList = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const initialOrders = [
{ restaurant: 'المطعم الاول المطعم الاول ', time: '5:00 Pm', status: 'Pending' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Delivered' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Completed' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'In-Progress' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Delivered' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Pending' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Completed' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Delivered' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Pending' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Delivered' },
{ restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Completed' },
];
const [currentPage, setCurrentPage] = useState(1);
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
const [statusFilter, setStatusFilter] = useState('all'); // 'all' يعني بدون فلتر
const itemsPerPage = 5;
// افتح قائمة الفلتر
const handleFilterClick = (event) => {
setFilterAnchorEl(event.currentTarget);
};
// اغلق قائمة الفلتر
const handleFilterClose = () => {
setFilterAnchorEl(null);
};
// عند اختيار فلتر جديد
const handleFilterSelect = (filter) => {
setStatusFilter(filter);
setCurrentPage(1); // العودة للصفحة الأولى بعد تغيير الفلتر
handleFilterClose();
};
// تصفية البيانات حسب الفلتر
const filteredOrders =
statusFilter === 'all'
? initialOrders
: initialOrders.filter(
(order) => order.status.toLowerCase() === statusFilter.toLowerCase()
);
const pageCount = Math.ceil(filteredOrders.length / itemsPerPage);
const paginatedData = filteredOrders.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const getStatusColor = (status) => {
switch (status) {
case 'Pending':
return { bg: '#FFF3E0', color: '#E65100' };
case 'Delivered':
return { bg: '#E8F5E9', color: '#2E7D32' };
case 'Completed':
return { bg: '#F3E5F5', color: '#6A1B9A' };
case 'In-Progress':
return { bg: '#F1F8E9', color: '#9E9D24' };
default:
return { bg: '#E0E0E0', color: '#424242' };
}
};
const rowHeight = 73; // ارتفاع الصف الواحد تقريبي
const emptyRowsCount = itemsPerPage - paginatedData.length;
return (
<Paper
sx={{
backgroundColor: '#FFFFFF',
width: { xs: '100%', md: 400 },
borderRadius: '10px',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
{/* Header */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
px: 2,
py: 2,
}}
>
<Typography variant="h6" sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
Order List
</Typography>
<Button
variant="outlined"
sx={{
textTransform: 'none',
color: '#667085',
borderColor: '#e0e0e0',
backgroundColor: '#fff',
borderRadius: '8px',
height: '40px',
fontSize: { xs: '12px', sm: '14px' },
fontWeight: 600,
}}
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
onClick={handleFilterClick}
>
{isMobile ? '' : 'Filters'}
</Button>
{/* قائمة الفلاتر */}
<Menu
anchorEl={filterAnchorEl}
open={Boolean(filterAnchorEl)}
onClose={handleFilterClose}
PaperProps={{
sx: {
backgroundColor: 'white',
px: 1,
position: 'relative',
width: '140px',
},
}}
>
{['all', 'Pending', 'Delivered', 'Completed', 'In-Progress'].map((status) => (
<MenuItem
key={status}
selected={statusFilter.toLowerCase() === status.toLowerCase()}
onClick={() => handleFilterSelect(status)}
sx={{
color: statusFilter.toLowerCase() === status.toLowerCase() ? '#FF914D' : '#4F5867',
backgroundColor:
statusFilter.toLowerCase() === status.toLowerCase() ? '#fffcf9d5' : 'transparent',
'&:hover': {
backgroundColor: '#f0f0f0ff',
transition: 'background-color 150ms ease-in-out',
},
}}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</MenuItem>
))}
</Menu>
</Box>
{/* Table */}
<TableContainer
sx={{
maxHeight: 430,
overflowY: 'auto',
overflowX: 'hidden',
'&::-webkit-scrollbar': { height: 4 },
}}
>
<Table size={isMobile ? 'small' : 'medium'}>
<TableHead sx={{ backgroundColor: '#F9FAFB' }}>
<TableRow>
<TableCell sx={{ pl: 2 }}>Restaurant</TableCell>
<TableCell sx={{ pl: 2 }}>Time</TableCell>
<TableCell sx={{ pl: 4 }}>Status</TableCell>
<TableCell sx={{ pl: 2 }}>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedData.length > 0 ? (
paginatedData.map((row, i) => {
const colors = getStatusColor(row.status);
return (
<TableRow key={i}>
<TableCell
sx={{
pl: 2,
whiteSpace: 'normal',
wordBreak: 'break-word',
lineHeight: 1, // للتحكم في المسافة بين الأسطر
}}
>
{row.restaurant}
</TableCell>
<TableCell>{row.time}</TableCell>
<TableCell>
<Chip
label={row.status}
sx={{
backgroundColor: colors.bg,
color: colors.color,
fontWeight: 600,
}}
/>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', gap: 0.4 }}>
<IconButton>
<ReplayIcon />
</IconButton>
<IconButton>
<VisibilityIcon />
</IconButton>
</Box>
</TableCell>
</TableRow>
);
})
) : (
<TableRow>
<TableCell colSpan={4} align="center" sx={{ py: 5 }}>
No orders found.
</TableCell>
</TableRow>
)}
{/* صفوف تعويضية فارغة لتثبيت ارتفاع الجدول */}
{emptyRowsCount > 0 &&
[...Array(emptyRowsCount)].map((_, idx) => (
<TableRow key={`empty-${idx}`} sx={{ height: rowHeight }}>
<TableCell colSpan={4} />
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{/* Pagination */}
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
p={{ xs: 1, sm: 2 }}
sx={{
position: 'sticky',
bottom: 0,
backgroundColor: theme.palette.background.paper,
borderTop: '1px solid #f0f0f0',
zIndex: 1,
}}
>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontSize: { xs: '11px', sm: '14px' },
whiteSpace: 'nowrap',
}}
>
Showing {(currentPage - 1) * itemsPerPage + 1} -{' '}
{Math.min(currentPage * itemsPerPage, filteredOrders.length)} of {filteredOrders.length}
</Typography>
<SimplePagination currentPage={currentPage} pageCount={pageCount} onChange={setCurrentPage} />
</Box>
</Paper>
);
};
export default OrderList;
@@ -0,0 +1,154 @@
import React, { useState } from 'react';
import { Box, Typography, Grid, useTheme, IconButton } from '@mui/material';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import ProductCard from './ProductCard';
const ITEMS_PER_PAGE = 10;
/* مكوّن الباجينيشن */
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
const theme = useTheme();
const handlePrev = () => {
if (currentPage > 1) onChange(currentPage - 1);
};
const handleNext = () => {
if (currentPage < pageCount) onChange(currentPage + 1);
};
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
onClick={handlePrev}
disabled={currentPage <= 1}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
backgroundColor: theme.palette.primary.main,
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
fontSize: 14,
userSelect: 'none',
}}
>
{currentPage}
</Box>
<IconButton
size="small"
onClick={handleNext}
disabled={currentPage >= pageCount}
sx={{
borderRadius: '8px',
backgroundColor: '#FFECE0',
'&:hover': { backgroundColor: '#FFD6B5' },
color: theme.palette.primary.main,
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
</Box>
);
};
/* قسم المنتجات */
const PopularProductSection = ({ products = [], onProductClick }) => {
const theme = useTheme();
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(products.length / ITEMS_PER_PAGE);
const startIdx = (currentPage - 1) * ITEMS_PER_PAGE;
const visibleProducts = products.slice(startIdx, startIdx + ITEMS_PER_PAGE);
return (
<Box
sx={{
pl: { xs: 2, sm: 3 },
pr: { xs: 2, sm: 3 },
pt: { xs: 2, sm: 1.5 },
pb: { xs: 2, sm: 3 },
backgroundColor: '#fff',
borderRadius: '10px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
overflow: 'hidden',
}}
>
{/* الهيدر */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6" fontWeight={600}>All Products</Typography>
</Box>
{/* المنتجات */}
<Grid container spacing={2}>
{visibleProducts.length > 0 ? (
visibleProducts.map((product, index) => (
<Grid item xs={12} sm={6} md={4} key={index}>
<ProductCard
{...product}
unit={product.unit || '/pk'}
onClick={() => onProductClick && onProductClick(product)}
/>
</Grid>
))
) : (
<Typography>There is no Product</Typography>
)}
</Grid>
{/* الباجينيشن */}
{totalPages > 1 && (
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
p={{ xs: 1, sm: 2 }}
sx={{
position: 'sticky',
bottom: 0,
backgroundColor: theme.palette.background.paper,
borderTop: '1px solid #f0f0f0',
zIndex: 1,
}}
>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '11px', sm: '14px' }, whiteSpace: 'nowrap' }}
>
Showing {startIdx + 1} - {Math.min(startIdx + ITEMS_PER_PAGE, products.length)} of {products.length}
</Typography>
<SimplePagination
currentPage={currentPage}
pageCount={totalPages}
onChange={setCurrentPage}
/>
</Box>
)}
</Box>
);
};
export default PopularProductSection;
@@ -0,0 +1,117 @@
import React from 'react';
import { Box, Typography, Card, CardContent, CardMedia, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
const ProductCard = ({ name, price, discountedPrice, unit, image, onClick, onAdd }) => {
return (
<Card
onClick={onClick}
sx={{
width: { xs: '160px', md: '152px' },
height: 245,
borderRadius: '20px',
p: '20px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#FCFCFC',
boxShadow: 'none',
border: 'none',
cursor: 'pointer',
transition: 'transform 0.2s ease-in-out',
'&:hover': { transform: 'scale(1.02)' },
}}
>
{/* صورة المنتج */}
<CardMedia
component="img"
image={image || '/placeholder.png'}
alt={name}
sx={{
width: '140px',
height: '140px',
objectFit: 'contain',
mb: 2,
pointerEvents: 'none', // ما يمنع الكليك على الكارد
}}
/>
{/* تفاصيل المنتج */}
<CardContent sx={{ p: 0, width: '100%' }}>
<Typography variant="body1" fontWeight={600} fontSize={20} noWrap>
{name}
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pt: 2,
gap: 1,
}}
>
{discountedPrice ? (
<>
<Typography
variant="body2"
fontWeight={600}
sx={{
textDecoration: 'line-through',
color: 'text.secondary',
fontSize: '14px',
}}
>
${price}
</Typography>
<Typography
variant="body2"
fontWeight={700}
sx={{ color: '#FF914D', fontSize: '16px' }}
>
${discountedPrice}{' '}
<Typography component="span" variant="caption" color="text.secondary">
{unit}
</Typography>
</Typography>
</>
) : (
<Typography
variant="body2"
fontWeight={600}
sx={{ color: '#FF914D', fontSize: '16px' }}
>
${price}{' '}
<Typography component="span" variant="caption" color="text.secondary">
{unit}
</Typography>
</Typography>
)}
{/* زر الإضافة */}
<IconButton
color="primary"
size="small"
onClick={(e) => {
e.stopPropagation(); // يمنع فتح التفاصيل عند الضغط على الزر
if (onAdd) onAdd();
}}
sx={{
backgroundColor: '#FF8551',
color: '#fff',
ml: 3,
width: 28,
height: 28,
'&:hover': { backgroundColor: '#ff7043' },
}}
>
<AddIcon fontSize="small" />
</IconButton>
</Box>
</CardContent>
</Card>
);
};
export default ProductCard;
@@ -0,0 +1,94 @@
import React, { useState, useContext, useEffect } from "react";
import { Box, Typography, Button, IconButton, LinearProgress, useTheme } from "@mui/material";
import RemoveIcon from "@mui/icons-material/Remove";
import AddIcon from "@mui/icons-material/Add";
import ShoppingCartOutlinedIcon from "@mui/icons-material/ShoppingCartOutlined";
import { CartContext } from "../../../../contexts/CartContextR";
// import { useRestaurant } from '../../../contexts/RestaurantContext';
import api from '../../../../services/authService';
const ProductDetail = ({ product, onBack }) => {
const theme = useTheme();
const { addToCart , createNewCart } = useContext(CartContext);
const [quantity, setQuantity] = useState(1);
const [totalPrice, setTotalPrice] = useState(Number(product?.price) || 0);
useEffect(() => {
if (product) {
setTotalPrice(Number(product.price) * quantity);
}
}, [quantity, product]);
if (!product) return null;
const handleIncrease = () => quantity < 9 && setQuantity(q => q + 1);
const handleDecrease = () => quantity > 1 && setQuantity(q => q - 1);
const handleAddToCart = () => {
addToCart(product, quantity);
createNewCart();
alert(`${product.name} added to cart! Quantity: ${quantity}, Total: $${totalPrice.toFixed(2)}`);
};
return (
<Box sx={{ display: "flex", backgroundColor: "#fff", borderRadius: "8px", p: 3, mx: "auto" }}>
<Box component="img" src={product.image} alt={product.name} sx={{ width: "50%", objectFit: "contain" }} />
<Box sx={{ width: "50%", pl: 4, display: "flex", flexDirection: "column" }}>
<Typography variant="h5" fontWeight={700} mb={0.5}>{product.name}</Typography>
<Typography variant="body2" sx={{ color: "#777", mb: 1 }}>{product.category}</Typography>
<Typography variant="h6" sx={{ color: theme.palette.primary.main, fontWeight: 700, mb: 1 }}>
${Number(product.price).toFixed(2)} {product.unit}
</Typography>
{/* <Typography variant="caption" sx={{ fontWeight: 600 }}>20 Item left</Typography> */}
<LinearProgress
variant="determinate"
value={100}
sx={{
mt: 3,
height: 4,
borderRadius: 2,
backgroundColor: "#f0f0f0",
"& .MuiLinearProgress-bar": { backgroundColor: theme.palette.primary.main },
mb: 2
}}
/>
<Typography variant="subtitle2" fontWeight={600}>Quantity</Typography>
<Box sx={{ display: "flex", alignItems: "center", border: "1px solid #ddd", borderRadius: "4px", width: "150px", mt: 1, mb: 0.5 }}>
<IconButton size="small" onClick={handleDecrease}><RemoveIcon /></IconButton>
<Typography sx={{ width: "40px", textAlign: "center" }}>{quantity}</Typography>
<IconButton size="small" onClick={handleIncrease}><AddIcon /></IconButton>
</Box>
<Typography variant="caption" sx={{ color: "#999", mb: 2 }}>Maximum purchase 9</Typography>
<Typography variant="h6" sx={{ color: theme.palette.primary.main, fontWeight: 700, mb: 2 }}>
Total: ${totalPrice.toFixed(2)}
</Typography>
<Box sx={{ display: "flex", gap: 1, mt: 10 }}>
<Button
fullWidth
variant="outlined"
startIcon={<ShoppingCartOutlinedIcon />}
sx={{ borderColor: theme.palette.primary.main, color: theme.palette.primary.main, textTransform: "none" }}
onClick={handleAddToCart}
>
Add To Cart
</Button>
<Button
fullWidth
variant="outlined"
onClick={onBack}
sx={{ borderColor: "#999", color: "#999" }}
>
Back
</Button>
</Box>
</Box>
</Box>
);
};
export default ProductDetail;

Some files were not shown because too many files have changed in this diff Show More