Compare commits

...
10 Commits
152 changed files with 50231 additions and 16039 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules
build
.git
.github
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
+15
View File
@@ -0,0 +1,15 @@
{
"id": "722d126b-c573-40f1-a738-57002876780b",
"name": "cicdproject",
"projectId": "896ba892-9448-4061-97e7-b0506ccc3e3a",
"ports": [{
"expose": true,
"number": 80
}],
"publicAccess": {
"enabled": true,
"domain": "auto"
},
"resourceTier": "t2",
"dockerFileName": "Dockerfile"
}
+25
View File
@@ -0,0 +1,25 @@
name: Deploy
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Ghaymah CLI
run: curl -sSL https://cli.ghaymah.systems/install.sh | bash
- name: Login to Ghaymah
run: $HOME/ghaymah/bin/gy auth login --email "${{ secrets.GHAYMAH_EMAIL }}" --password "${{ secrets.GHAYMAH_PW }}"
- name: Deploy application
run: $HOME/ghaymah/bin/gy resource app launch
+18
View File
@@ -0,0 +1,18 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine AS runtime
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+16
View File
@@ -0,0 +1,16 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
+30657 -11328
View File
File diff suppressed because it is too large Load Diff
+74 -3
View File
@@ -1,51 +1,122 @@
{
"name": "resturant-dashBord",
"name": "resturant-dashbord",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/x-date-pickers": "^8.5.2",
"@react-oauth/google": "^0.12.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.9.0",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"file-saver": "^2.0.5",
"framer-motion": "^12.23.0",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.5.23",
"jwt-decode": "^4.0.0",
"react": "^19.1.0",
"react-circular-progressbar": "^2.2.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",
"react-scripts": "5.0.1",
"recharts": "^2.15.3",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"xlsx": "^0.18.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
},
"description": "To make it easy for you to get started with GitLab, here's a list of recommended next steps.",
"main": "restored-file.js",
"author": "",
"license": "ISC"
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

+5 -16
View File
@@ -2,11 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<<<<<<< HEAD
<link rel="icon" href="/image.png" />
=======
<link rel="icon" href="%PUBLIC_URL%/image.png" />
>>>>>>> 094d64320df085d59dc613108366955085361a0b
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
@@ -14,18 +13,8 @@
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<<<<<<< HEAD
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<style>
* {
margin: 0
}
</style>
<title>Restaurant</title>
=======
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> -->
<style>
* {
font-family: "Roboto", sans-serif;
@@ -33,7 +22,7 @@
}
</style>
<title>Resturant</title>
>>>>>>> 094d64320df085d59dc613108366955085361a0b
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
BIN
View File
Binary file not shown.
+23
View File
@@ -121,4 +121,27 @@ html,
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 === */
+41 -31
View File
@@ -1,50 +1,60 @@
import React from 'react';
import './App.css';
import { ThemeProvider } from '@mui/material/styles';
import theme from './theme';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
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 Dashboard from './components/Home/Dashboard/Dashboard';
import Inventory from './components/Home/Inventory/Inventory';
import Analytics from './components/Home/Analytics&Reporting/Analytics';
import Supplier from './components/Home/Supplier/Supplier';
import Cashier from './components/Home/Cashier/Cashier';
import CreateYourRestaurant from './components/Home/CreateYourRestaurant/CreateRestaurant';
import { RestaurantProvider } from './contexts/RestaurantContext';
import ProtectedRoutes from './components/Routes/ProtectedRoutes';
import { SnackbarProvider } from "./contexts/SnackbarContext";
import { UserProvider } from './contexts/UserContext';
import Training from './components/Home/Training/Training';
import RestaurantProfile from './components/Home/RestaurantProfile/RestaurantProfile';
import HostKitchen from './components/Home/HostKitchen/HostKitchen';
import Settings from './components/Home/Settings/Setting';
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 />} />
<Router>
<Routes>
<Route path="/" element={<LoginForm />} />
<Route path="/login" element={<LoginForm />} />
<Route path="/register" element={<RegisterForm />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/inventory" element={<Inventory />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<RestaurantProfile />} />
<Route path="/create-kitchen" element={<CreateYourRestaurant />} />
<Route path="/host-kitchen" element={<HostKitchen />} />
<Route path="/cashier" element={<Cashier />} />
<Route path="/supplier" element={<Supplier />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/forget" element={<ForgetPassword />} />
<Route path="/training" element={<Training />} />
</Routes>
</Router>
<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>
);
}
@@ -13,7 +13,7 @@ const Congratulations = ({ emailOrPhone, setEmailOrPhone }) => {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
overflow: 'auto',
// overflow: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
@@ -1,147 +1,226 @@
// EmailInputSection.js
import React, { useState } from 'react';
import { Box, Button, TextField, Typography ,useTheme} from '@mui/material';
import { Box, Button, TextField, Typography, useTheme, Snackbar, Alert } from '@mui/material';
import authService from '../../../services/authService';
const EmailInputSection = ({ emailOrPhone, setEmailOrPhone }) => {
const theme = useTheme();
const [currentSlide, setCurrentSlide] = useState(0);
const slides = [0, 1, 2, 3];
const EmailInputSection = ({ emailOrPhone, setEmailOrPhone, onStepComplete }) => {
const theme = useTheme();
const [loading, setLoading] = useState(false);
const handleSend = () => {
// عملية الإرسال
};
// حالات Snackbar
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [snackbarSeverity, setSnackbarSeverity] = useState('success');
const [inputError, setInputError] = useState('');
const handleNext = () => {
if (currentSlide < slides.length - 1) {
setCurrentSlide(prev => prev + 1);
}
};
const handleCloseSnackbar = (event, reason) => {
if (reason === 'clickaway') return;
setSnackbarOpen(false);
};
const handleBack = () => {
if (currentSlide > 0) {
setCurrentSlide(prev => prev - 1);
}
};
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',
overflow: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}
/>
</Box>
const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
<Typography
variant="h4"
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem'
}
}}
>
Forget Password
</Typography>
const handleSend = async () => {
setInputError(''); // مسح الأخطاء السابقة
<Typography variant="body1" color="text.secondary">
Enter your email to reset it and regain access to your account.
</Typography>
if (!emailOrPhone) {
setInputError("Please enter your email.");
return;
}
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography
variant="body2"
color="black"
sx={{
fontWeight: '500',
fontSize: '16px'
}}
>
Email/Phone
</Typography>
<TextField
placeholder="Enter your email"
type="email"
variant="outlined"
fullWidth
value={emailOrPhone}
onChange={(e) => setEmailOrPhone(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)'
}
}}
/>
</Box>
if (!validateEmail(emailOrPhone)) {
setInputError("Please enter a valid email address.");
return;
}
{/* send */}
<Button
variant="contained"
fullWidth
onClick={handleSend}
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'
}
}}
>
Send
</Button>
setLoading(true);
const response = await authService.resetPassword(emailOrPhone);
setLoading(false);
<Typography
textAlign="center"
fontWeight={500}
sx={{
fontSize: '16px',
display: 'block',
width: '100%',
mx: 'auto',
color: '#969BA7'
}}
>
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>
</>
);
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;
@@ -1,17 +1,17 @@
import React, { useState } from 'react';
import { Box, Stack ,useTheme} from '@mui/material';
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 theme = useTheme();
const [stepCompleted, setStepCompleted] = useState(false);
const theme = useTheme();
const slides = [
{ component: EmailInputSection, title: "Email Verification" },
{ component: OtpVerification, title: "OTP Verification" },
@@ -22,33 +22,37 @@ const ForgetForm = () => {
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',
}}
>
{/* ✅ Sidebar يظهر فقط من sm+ */}
<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} />
<Side
currentStepIndex={currentSlide}
onNext={handleNext}
onBack={handleBack}
stepCompleted={stepCompleted}
/>
</Box>
{/* ✅ Form */}
<Box
sx={{
width: { sm: '60%' },
@@ -62,32 +66,25 @@ const ForgetForm = () => {
backgroundColor: '#fff',
}}
>
<Box
sx={{
width: '100%',
maxWidth: 686,
pr: { xs: 3, sm: 2, md: 1 },
pt: { xs: 2, sm: 4, md: 1 },
}}
>
<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: { xs: 'absolute', sm: 'absolute' },
bottom: { xs: '5%', sm: '-1%', md: '2%' },
position: 'absolute',
bottom: { xs: '5%', sm: '5%', md: '2%' },
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
@@ -95,11 +92,7 @@ const ForgetForm = () => {
width: '80%',
maxWidth: 600,
justifyContent: 'center',
height: {
xs: 3,
sm: 5,
md: 6,
},
height: { xs: 3, sm: 5, md: 6 },
}}
>
{slides.map((_, index) => (
@@ -108,12 +101,11 @@ const ForgetForm = () => {
sx={{
width: '100vh',
height: 5,
bgcolor: index === currentSlide ? theme.palette.primary.main : 'rgba(0, 0, 0, 0.2)',
bgcolor: index === currentSlide ? theme.palette.primary.main : 'rgba(0, 0, 0, 0.2)',
borderRadius: 2,
cursor: 'pointer',
transition: 'background-color 0.3s',
}}
// onClick={() => setCurrentSlide(index)} // يمكنك تفعيلها لاحقًا
/>
))}
</Box>
@@ -122,4 +114,5 @@ const ForgetForm = () => {
);
};
export default ForgetForm;
@@ -1,217 +1,222 @@
// EmailInputSection.js
import React, { useState } from 'react';
import { Box, Button, TextField, Typography } from '@mui/material';
import { Box, Button, TextField, Typography, IconButton, InputAdornment } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { IconButton, InputAdornment } from '@mui/material';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import { VisibilityOutlined } from '@mui/icons-material';
import authService from '../../../services/authService';
const EmailInputSection = ({ emailOrPhone, setEmailOrPhone }) => {
const theme = useTheme();
const handleTogglePassword = () => {
setShowPassword((prev) => !prev);
};
const NewPassword = ({ emailOrPhone, onStepComplete }) => {
const theme = useTheme();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [currentSlide, setCurrentSlide] = useState(0);
const slides = [0, 1, 2, 3];
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});
const handleSend = () => {
// عملية الإرسال
};
const handleToggleConfirmPassword = () => {
setShowConfirmPassword((prev) => !prev);
};
const handleTogglePassword = () => setShowPassword(prev => !prev);
const handleToggleConfirmPassword = () => setShowConfirmPassword(prev => !prev);
const handleNext = () => {
if (currentSlide < slides.length - 1) {
setCurrentSlide(prev => prev + 1);
}
};
const handleSend = async () => {
setErrors({});
const handleBack = () => {
if (currentSlide > 0) {
setCurrentSlide(prev => prev - 1);
}
};
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>
if (!password || !passwordConfirm) {
alert("Please fill in both password fields.");
return;
}
if (password !== passwordConfirm) {
alert("Passwords do not match.");
return;
}
<Typography
variant="h4"
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem'
}
}}
>
Create a New Password
</Typography>
setLoading(true);
try {
const response = await authService.updatePassword({
email: emailOrPhone,
password,
confirmPassword: passwordConfirm
});
setLoading(false);
<Typography variant="body1" color="text.secondary">
Enter your email to reset it and regai access to your account.
</Typography>
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"));
}
}
};
{/* Password Input */}
<Box sx={{ width: '100%', 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"
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>
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>
{/* Confirm Password Input */}
<Box sx={{ width: '100%', 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"
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>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
Enter your new password to regain access to your account.
</Typography>
{/* send */}
<Button
variant="contained"
fullWidth
onClick={handleSend}
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'
}
}}
>
Send
</Button>
<Typography
textAlign="center"
fontWeight={500}
sx={{
fontSize: '16px',
display: 'block',
width: '100%',
mx: 'auto',
color: '#969BA7'
}}
>
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>
</>
);
<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 EmailInputSection;
export default NewPassword;
@@ -1,18 +1,54 @@
import React, { useState, useRef } from 'react';
import { Box, TextField, Typography, Button, Stack ,useTheme} from '@mui/material';
import {
Box, TextField, Typography, Button, Stack, useTheme,
Snackbar, Alert
} from '@mui/material';
import authService from '../../../services/authService';
const OtpVerification = () => {
const [emailOrPhone, setEmailOrPhone] = useState('');
const [currentSlide, setCurrentSlide] = useState(0);
const slides = [0, 1, 2, 3];
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 [otp, setOtp] = useState(Array(7).fill(''));
const inputRefs = useRef(Array(7).fill().map(() => React.createRef()));
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [snackbarSeverity, setSnackbarSeverity] = useState('success');
const handleSend = () => {
const theme = useTheme();
const handleSnackbarClose = (event, reason) => {
if (reason === 'clickaway') return;
setSnackbarOpen(false);
};
const handleSend = async () => {
const otpCode = otp.join('');
console.log('Submitted OTP:', otpCode);
// عملية التحقق من الكود
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) => {
@@ -22,30 +58,17 @@ const OtpVerification = () => {
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
if (value && index < 6) {
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();
}
};
const handleNext = () => {
if (currentSlide < slides.length - 1) {
setCurrentSlide(prev => prev + 1);
}
};
const handleBack = () => {
if (currentSlide > 0) {
setCurrentSlide(prev => prev - 1);
}
};
const theme = useTheme();
return (
<>
@@ -77,12 +100,11 @@ const OtpVerification = () => {
Enter OTP Verification
</Typography>
<Typography variant="body1" color="text.secondary">
<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 }}>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
<Stack
direction="row"
spacing={{ xs: 0, sm: 1, md: 1.5 }}
@@ -113,15 +135,15 @@ const OtpVerification = () => {
height: { xs: '3rem', sm: '3.25rem', md: '4rem' },
'& .MuiOutlinedInput-root': {
borderRadius: '0.5rem',
height: '100%', // تأكيد تطابق الطول
height: '100%',
'& fieldset': {
borderColor: '#ccc',
},
'&:hover fieldset': {
borderColor: theme.palette.primary.main,
borderColor: theme.palette.primary.main,
},
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
borderColor: theme.palette.primary.main,
},
},
'& .MuiInputBase-input': {
@@ -135,47 +157,83 @@ const OtpVerification = () => {
/>
))}
</Stack>
</Box>
<Button
variant="contained"
fullWidth
onClick={handleSend}
disabled={loading}
sx={{
color:'white',
backgroundColor: theme.palette.primary.main,
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 ,
backgroundColor: theme.palette.primary.hover,
transition: 'all 0.4s ease'
}
}}
>
Send
{loading ? "Sending..." : "Send"}
</Button>
{/* Terms */}
<Typography
// variant="caption"
textAlign="center"
fontWeight={500}
sx={{
fontSize: '16px',
display: 'block',
width: '100%',
mx: 'auto',
color: '#969BA7'
}}
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>
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>
</>
);
};
@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Typography, Stack, Button ,useTheme } from '@mui/material';
import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
@@ -27,7 +27,7 @@ const steps = [
];
const Side = ({ currentStepIndex, onNext, onBack }) => {
const theme = useTheme();
const theme = useTheme();
return (
<Box
sx={{
@@ -46,7 +46,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
Instructions for secure password modification. Follow simple steps for password change.
</Typography>
<Stack spacing={6} position="relative" sx={{ ml: 3 }}>
<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 && (
@@ -109,7 +109,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
</Box>
</Box>
<Box>
<Box >
<Typography variant="subtitle1" sx={{ fontSize: '16px', fontWeight: 600 }}>
{step.title}
</Typography>
@@ -121,13 +121,11 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
))}
</Stack>
{/* ... (بقية محتوى الـ Side كما هو بدون تغيير) ... */}
{/* أزرار التنقل */}
<Box
sx={{
position: 'absolute',
bottom: 2,
bottom: {sm:31 , md:2},
left: '50%',
transform: 'translateX(-50%)',
width: '90%',
@@ -152,7 +150,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
textTransform: 'none',
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.primary.main
color: theme.palette.primary.main
}
}}
>
@@ -172,7 +170,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
textTransform: 'none',
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.primary.main,
color: theme.palette.primary.main,
transition: 'all 0.4s ease'
}
}}
@@ -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.
*/
@@ -3,27 +3,94 @@ import { TextField, Button, Typography, Stack, Box, IconButton, InputAdornment }
import { useTheme } from '@mui/material/styles';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import { VisibilityOutlined } from '@mui/icons-material';
import SidePanel from './SidePanel'; // استدعاء SidePanel من نفس المجلد أو حسب المسار المناسب
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
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);
setShowPassword(prev => !prev);
};
const handleLogin = () => {
// بعد تنفيذ عملية تسجيل الدخول بنجاح، يتم التوجيه
navigate('/dashboard');
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"}
@@ -38,14 +105,11 @@ const LoginForm = () => {
},
}}
>
{/* SidePanel ثابت */}
<SidePanel />
{/* Login Content */}
<Box
sx={{
marginLeft: { xs: 0, sm: '50%' },
width: { xs: '100%', sm: '50%' ,md:'70%'},
marginLeft: { xs: 0, sm: '50%' },
width: { xs: '100%', sm: '50%', md: '70%' },
}}
>
<Box
@@ -76,7 +140,7 @@ const LoginForm = () => {
>
<Stack spacing={2}>
{/* Logo */}
{/* الشعار */}
<Box>
<img
src="/image.png"
@@ -90,30 +154,17 @@ const LoginForm = () => {
/>
</Box>
<Typography
variant="h4"
fontWeight={700}
sx={{
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.2rem'
}
}}
>
{/* عنوان تسجيل الدخول */}
<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 }}
>
<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>
{/* Email Input */}
{/* إدخال البريد */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Email
@@ -123,15 +174,17 @@ const LoginForm = () => {
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': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
@@ -142,7 +195,7 @@ const LoginForm = () => {
/>
</Box>
{/* Password Input */}
{/* إدخال كلمة السر */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
Password
@@ -153,15 +206,17 @@ const LoginForm = () => {
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': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
}
},
'& .MuiOutlinedInput-root.Mui-focused': {
@@ -184,14 +239,22 @@ const LoginForm = () => {
/>
</Box>
{/* Login Button */}
{/* عرض الخطأ */}
{error && (
<Typography color="error" textAlign="center">
{error}
</Typography>
)}
{/* زر تسجيل الدخول */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
onClick={handleLogin}
disabled={loading}
sx={{
fontFamily: 'PlusJakartaSans' ,
fontFamily: 'PlusJakartaSans',
fontWeight: 600,
fontSize: { xs: '14px', sm: '16px' },
height: { xs: '45px', sm: '52px' },
@@ -204,23 +267,23 @@ const LoginForm = () => {
}
}}
>
Log in
{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' }}>
<Typography variant="body1" sx={{ fontWeight: 500, fontSize: '16px', color: 'black', fontFamily: 'PlusJakartaSans' }}>
Or
</Typography>
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
</Box>
{/* Google Button */}
<Button
{/* <Button
variant="outlined"
fullWidth
onClick={loginWithGoogle}
sx={{
fontWeight: 500,
fontSize: '16px',
@@ -233,8 +296,8 @@ const LoginForm = () => {
'&:hover': {
borderColor: 'black',
backgroundColor: 'transparent'
}
, fontFamily: 'PlusJakartaSans'
},
fontFamily: 'PlusJakartaSans'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
@@ -245,10 +308,10 @@ const LoginForm = () => {
/>
</Box>
Login with Google
</Button>
</Button> */}
{/* Facebook Button */}
<Button
{/* <Button
variant="outlined"
fullWidth
sx={{
@@ -264,7 +327,7 @@ const LoginForm = () => {
borderColor: 'black',
backgroundColor: 'transparent'
}
, fontFamily: 'PlusJakartaSans'
, fontFamily: 'PlusJakartaSans'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
@@ -275,8 +338,7 @@ const LoginForm = () => {
/>
</Box>
Login with Facebook
</Button>
</Button> */}
{/* Register Link */}
<Typography
@@ -285,7 +347,7 @@ const LoginForm = () => {
fontSize: '16px',
textAlign: 'center',
color: '#969BA7',
pt: 3
pt: 2
}}
>
Dont have an account?{' '}
@@ -323,6 +385,7 @@ const LoginForm = () => {
</Typography>
{/* Terms */}
<Typography
variant="caption"
sx={{
@@ -333,14 +396,25 @@ const LoginForm = () => {
}}
>
By logging in, I agree to the{' '}
<a href="#" style={{ color: '#2261FF', textDecoration: 'none' }}>
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/terms', 'TermsOfService')}
>
Terms of Service
</a>{' '}
</Box>{' '}
and{' '}
<a href="#" style={{ color: '#2261FF', textDecoration: 'none' }}>
<Box
component="span"
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
>
Privacy Policy
</a>.
</Box>.
</Typography>
</Stack>
</Box>
</Box>
@@ -350,3 +424,4 @@ const LoginForm = () => {
};
export default LoginForm;
@@ -1,15 +1,42 @@
import React, { useState } from 'react';
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 } from 'react-router-dom';
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);
};
@@ -18,21 +45,112 @@ const Register = () => {
setShowConfirmPassword((prev) => !prev);
};
return (
<Box bgcolor={"background.default"} color={"text.primary"} display="flex" sx={{
width: '100vw',
height: '100vh',
overflow: 'auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}>
{/* SidePanel ثابت */}
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 />
{/* Login Content */}
<Box
sx={{
marginLeft: { xs: 0, sm: '50%' },
@@ -60,14 +178,13 @@ const Register = () => {
xs: '120%',
sm: '100%',
md: '120%',
lg: '120%'
lg: '120%',
},
maxWidth: '600px'
maxWidth: '600px',
}}
>
<Stack spacing={2}>
{/* Logo */}
{/* logo */}
<Box>
<img
src="/image.png"
@@ -76,7 +193,7 @@ const Register = () => {
width: '4vw',
maxWidth: '80px',
height: 'auto',
objectFit: 'contain'
objectFit: 'contain',
}}
/>
</Box>
@@ -88,316 +205,291 @@ const Register = () => {
fontSize: {
xs: '1.8rem',
sm: '2rem',
md: '2.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 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>
{/* Email Input */}
<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"
{/* حقل الايميل */}
<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
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>
{/* Password Input */}
<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"
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>
{/* Confirm Password Input */}
<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"
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>
{/* Login Button */}
<Box sx={{ pt: 2 }}>
<Button
variant="contained"
fullWidth
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
}
}}
>
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"
// onClick={() => RegisterWithGoogle()}
sx={{
fontWeight: 500,
fontSize: '16px',
color: 'black'
borderRadius: '50px',
height: '50px',
textTransform: 'none',
gap: 1,
borderColor: '#E6E6E6',
color: 'black',
'&:hover': {
borderColor: 'black',
backgroundColor: 'transparent',
},
}}
>
Or
<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>
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
</Box>
{/* 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/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'
}
}}
>
<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>
{/* login Link */}
<Typography
variant="body2"
sx={{
fontSize: '16px',
textAlign: 'center',
color: '#969BA7',
pt: 3
}}
variant="caption"
sx={{
fontSize: '14px',
textAlign: 'center',
color: '#969BA7',
pt: 2
}}
>
Already have an account?{' '}
<Link
to="/login"
style={{
color: '#2261FF',
textDecoration: 'none'
}}
>
Login
</Link>
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>
{/* Terms */}
<Typography
variant="caption"
sx={{
fontSize: '14px',
textAlign: 'center',
color: '#969BA7',
pt: 5
}}
>
By log in, I agree to the and {' '}
<a href="#" style={{ color: '#2261FF', textDecoration: 'none' }}>
Terms of Service
</a>{' '}
and{' '}
<a href="#" style={{ color: '#2261FF', textDecoration: 'none' }}>
Privacy Policy
</a>.
</Typography>
</Stack>
</Box>
</Box>
@@ -406,4 +498,4 @@ const Register = () => {
);
};
export default Register;
export default Register;
@@ -1,26 +1,23 @@
import React, { useState, useEffect } from 'react';
import {
Button,
Typography,
Stack,
Box
} from '@mui/material';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; // استيراد أيقونة السهم
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
const SidePanel = ({ setMode, mode }) => {
const [currentSlide, setCurrentSlide] = useState(0);
const slides = [
{
image: '/images/waitress3.png',
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/waitress3.png',
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."
},
@@ -30,7 +27,7 @@ const SidePanel = ({ setMode, mode }) => {
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
},
{
image: '/images/waitress3.png',
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."
}
@@ -61,7 +58,7 @@ const SidePanel = ({ setMode, mode }) => {
sx={{
display: { xs: "none", sm: "block" },
position: 'sticky',
height: 'calc(102vh - 40px)', // لضمان ملاءمته للشاشة
height: 'calc(102vh - 40px)',
}}
>
@@ -96,9 +93,9 @@ const SidePanel = ({ setMode, mode }) => {
left: 0,
width: '100%',
height: {
md: '37vh', // الافتراضي للشاشات الصغيرة
md: '37vh',
// xs: '5vh',
sm: '42vh',// عندما تكون الشاشة متوسطة أو أكبر
sm: '42vh',
lg: '31vh', // من 1200px إلى أقل من 1536px
},
@@ -119,17 +116,17 @@ const SidePanel = ({ setMode, mode }) => {
<Button
variant="contained"
size="small"
onClick={() => window.open('https://www.google.com', '_blank')}
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: '12px',
display: 'flex',
alignItems: 'center',
padding: '6px 20px',
borderRadius: '50px',
fontSize: '16px',
fontWeight: 500,
@@ -138,8 +135,8 @@ const SidePanel = ({ setMode, mode }) => {
>
KITCHPLUS
{/* إضافة أيقونة السهم مع تعديل زاوية الدوران */}
<ArrowForwardIcon sx={{ ml: 1, transform: 'rotate(-45deg)' }} /> {/* تعديل زاوية السهم */}
<ArrowForwardIcon sx={{ ml: 1, transform: 'rotate(-45deg)' }} />
</Button>
<Typography
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery, Skeleton } from '@mui/material';
import { Box, useTheme, useMediaQuery } from '@mui/material';
import KitchPlusAppBar from '../AppBar';
import Sidebar from '../SideHome';
import AnalyticsContect from './AnalyticsContect';
@@ -15,11 +15,11 @@ const AnalyticsPage = () => {
const [isLoading, setIsLoading] = useState(true);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
// محاكاة التحقق من المنتجات
useEffect(() => {
const checkProducts = async () => {
setIsLoading(true);
const productsExist = await checkIfProductsExist(); // استبدل بمنطقك
const productsExist = await checkIfProductsExist();
setHasProducts(productsExist);
setIsLoading(false);
};
@@ -28,7 +28,7 @@ const AnalyticsPage = () => {
}, []);
const checkIfProductsExist = async () => {
return new Promise((resolve) => setTimeout(() => resolve(true), 1500)); // محاكاة تأخير
return new Promise((resolve) => setTimeout(() => resolve(true), 1500));
};
useEffect(() => {
@@ -57,8 +57,6 @@ const AnalyticsPage = () => {
setSidebarOpen(!sidebarOpen);
};
return (
<Box sx={{ display: 'flex', height: '100vh', backgroundColor: '#F6F6F6', overflow: 'hidden' }}>
<Sidebar open={sidebarOpen} onClose={handleDrawerToggle} isMobile={isMobile} drawerWidth={drawerWidth} />
@@ -94,15 +92,9 @@ const AnalyticsPage = () => {
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 }} />
</>
) : (
<AnalyticsContect />
)}
</Box>
</Box>
</Box>
@@ -1,104 +1,143 @@
import React, { useState, useEffect } from 'react';
import { useRestaurant } from '../../../contexts/RestaurantContext';
import StatisticsCard from './StatisticsCard';
import TopSellingProduct from './TopSellingProduct';
import SalesByLocation from './SalesByLocation';
import TablesManager from './TablesManager';
import {
Box,
useTheme,
useMediaQuery,
Skeleton,
Button,
Typography,
ButtonGroup
ButtonGroup,
TextField
} from '@mui/material';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import authService from '../../../services/authService';
import dayjs from 'dayjs';
const AnalyticsPage = () => {
const [timeFrame, setTimeFrame] = useState('month');
const { restaurantId } = useRestaurant();
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [hasProducts, setHasProducts] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const [chartData, setChartData] = useState([]);
const dailyData = [
{ date: '10:00', visitors: 1500, conversions: 300 },
{ date: '11:00', visitors: 1800, conversions: 320 },
];
const weeklyData = [
{ label: 'Week 1', visitors: 1500, conversions: 200 },
{ label: 'Week 2', visitors: 2300, conversions: 400 },
];
const monthlyData = [
{ label: 'Jan', visitors: 15000, conversions: 3000 },
{ label: 'Feb', visitors: 18000, conversions: 4000 },
];
const yearlyData = [
{ label: '2024', visitors: 25000, conversions: 7000 },
{ label: '2025', visitors: 38000, conversions: 12000 },
];
const getData = () => {
switch (timeFrame) {
case '24h':
return dailyData;
case '7d':
return weeklyData;
case '30d':
return monthlyData;
case '12m':
return yearlyData;
case 'all':
return [...yearlyData, ...monthlyData, ...weeklyData, ...dailyData];
default:
return dailyData;
}
const handleTimeFrameChange = (newTimeFrame) => {
setTimeFrame(newTimeFrame);
};
const topSellingProducts = [
{ product: 'Apple Watch', sales: 150, amount: 45000, price: 299, status: 'Published' },
{ product: 'Samsung Galaxy', sales: 90, amount: 36000, price: 400, status: 'Low Stock' },
{ product: 'Sony Headphones', sales: 60, amount: 18000, price: 299, status: 'Draft' },
];
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);
};
const salesData = [
{ country: 'United Kingdom', amount: 17678, change: 12, sales: 340 },
{ country: 'Spain', amount: 5500, change: -5, sales: 100 },
{ country: 'Germany', amount: 24189, change: -25, sales: 540 },
];
useEffect(() => {
const checkProducts = async () => {
setIsLoading(true);
const productsExist = await new Promise((resolve) =>
setTimeout(() => resolve(true), 1500)
);
setHasProducts(productsExist);
setIsLoading(false);
};
checkProducts();
}, []);
useEffect(() => {
const handleResize = () => {
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
fetchStatistics();
}, [timeFrame, restaurantId, customDate]);
return (
<Box
p={{ xs: 1, sm: 2 }}
maxWidth="100%"
overflowX="hidden"
>
<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"
@@ -106,7 +145,7 @@ const yearlyData = [
justifyContent="space-between"
alignItems="center"
gap={2}
mb={3}
mb={1.5}
>
<ButtonGroup
size="small"
@@ -131,94 +170,73 @@ const yearlyData = [
}
}}
>
{[
{ label: 'All Time', value: 'all' },
{ label: '12 Months', value: '12m' },
{ label: '30 Days', value: '30d' },
{ label: '7 Days', value: '7d' },
{ label: '24 Hour', value: '24h' },
].map(({ label, value }) => (
{[{ 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)}
onClick={() => handleTimeFrameChange(value)}
>
{label}
</Button>
))}
</ButtonGroup>
<Box display="flex" gap={2} flexWrap="wrap" justifyContent={{ xs: 'space-between', sm: 'flex-end' }} width={{ xs: '100%', sm: 'auto' }}>
<Button
variant="contained"
{/* 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={{
textTransform: 'none',
color: '#667085',
backgroundColor: 'white',
boxShadow: 'none',
borderRadius: '8px',
height: '40px',
fontSize: '13px',
fontWeight: 500,
border: '1px solid #e0e0e0',
gap: 1,
minWidth: '120px'
width: isMobile ? '100%' : 150,
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
}}
>
<CalendarTodayOutlinedIcon sx={{ fontSize: 16 }} />
{!isSmallScreen && 'Select Dates'}
</Button>
<Button
variant="contained"
sx={{
textTransform: 'none',
color: 'white',
backgroundColor: theme.palette.primary.main,
borderRadius: '8px',
height: '40px',
fontWeight: 600,
fontSize: '14px',
minWidth: '100px'
}}
>
KPIs Filter
</Button>
</Box>
</Box>
{/* Data Sections */}
<Box display="flex" flexDirection={{ xs: 'column', md: 'row' }} gap={2} mb={3}>
<Box flex={2}>
<TopSellingProduct data={topSellingProducts} />
</Box>
<Box flex={1}>
<SalesByLocation data={salesData} />
</Box>
</Box>
{/* Chart Section */}
<Box>
{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 }} />
</>
) : (
<StatisticsCard
title="Analytics Overview"
subtitle="Performance Metrics"
data={getData()}
dataKeys={[
{ key: 'visitors', name: 'Visitors', color: '#4CAF50' },
{ key: 'conversions', name: 'Conversions', color: '#9C27B0' },
]}
xDataKey={timeFrame === '24h' ? 'date' : 'label'}
timeFrame={timeFrame}
onTimeFrameChange={setTimeFrame}
/>
)}
<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>
);
@@ -22,9 +22,9 @@ const SalesByLocation = ({ data }) => {
p: { xs: 1, sm: 2 },
borderRadius: '12px',
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.1)',
height: '95%', // إضافة هذه السطر
display: 'flex', // إضافة
flexDirection: 'column' // إضافة
height: '95%',
display: 'flex',
flexDirection: 'column'
}}>
<Box sx={{
display: 'flex',
@@ -56,7 +56,7 @@ const SalesByLocation = ({ data }) => {
</Box>
<List dense sx={{
flexGrow: 1, // إضافة هذه السطر
flexGrow: 1,
overflowY: 'auto',
'&::-webkit-scrollbar': {
display: 'none'
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
Box,
@@ -6,7 +6,11 @@ import {
Paper,
Typography,
useTheme,
useMediaQuery
useMediaQuery,
Menu,
MenuItem,
TextField,
Button
} from '@mui/material';
import {
AreaChart,
@@ -18,37 +22,64 @@ import {
ResponsiveContainer,
CartesianGrid
} from 'recharts';
import MoreVertIcon from '@mui/icons-material/MoreVert';
const formatCurrency = (value) => {
if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`;
if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`;
return `$${value}`;
};
// 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' } // << وهنا أيضاً
{ key: 'revenue', name: 'Revenue', color: '#E46A11' },
{ key: 'sales', name: 'Sales', color: '#0182FC' }
],
xDataKey = 'month',
valueFormatter = formatCurrency,
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 handleTimeFrameChange = (newTimeFrame) => {
if (onTimeFrameChange) onTimeFrameChange(newTimeFrame);
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' }}}>
<Box sx={{ borderRadius: 2, width: { sm: '100%', md: '167vh' } }}>
<Paper sx={{
p: { xs: 1.5, sm: 2 },
mb: { xs: 2, sm: 2 },
@@ -64,10 +95,34 @@ const StatisticsCard = ({
right: { xs: 4, sm: 8 },
color: '#667085'
}}
onClick={handleMenuOpen}
>
<MoreVertIcon fontSize={isMobile ? 'small' : 'medium'} />
{/* <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',
@@ -88,7 +143,7 @@ const StatisticsCard = ({
</Box>
{/* Chart */}
<ResponsiveContainer width="100%" height={isMobile ? 250 : isTablet ? 290 : 295}>
<ResponsiveContainer width="100%" height={isMobile ? 350 : isTablet ? 390 : 395}>
<AreaChart data={data} margin={{
top: isMobile ? -30 : -45,
right: isMobile ? 15 : 30,
@@ -112,18 +167,20 @@ const StatisticsCard = ({
tickMargin={isMobile ? 8 : 15}
tick={{
fontSize: isMobile ? 11 : 12,
angle: -25, // تدوير النص لتفادي التزاحم
angle: -25,
textAnchor: 'end'
}}
interval={0} // عرض كل القيم على محور X
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"
@@ -138,9 +195,9 @@ const StatisticsCard = ({
name={name}
type="monotone"
dataKey={key}
stroke={color} // لون الخط
stroke={color}
strokeWidth={isMobile ? 2 : 3}
fill={`url(#color-${key})`} // لون التعبئة بتدرج
fill={`url(#color-${key})`}
/>
))}
</AreaChart>
@@ -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;
@@ -1,215 +0,0 @@
import React, { useState } from 'react';
import { useTheme } from '@mui/material/styles';
import { useMediaQuery } from '@mui/material';
import {
Box,
Typography,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Chip,
Pagination,
Button,
Avatar,
TableContainer
} from '@mui/material';
import TuneIcon from '@mui/icons-material/Tune';
import { green } from '@mui/material/colors';
import AssignmentIcon from '@mui/icons-material/Assignment';
const TopSellingProduct = ({ data = [] }) => {
const theme = useTheme();
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const pageCount = Math.ceil(data.length / itemsPerPage);
const paginatedData = data.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<Paper sx={{
borderRadius: 2,
width: '100%',
maxWidth: { xs: '100%', md: '115vh' },
height: { xs: 'auto', md: '100vh' },
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
boxShadow: theme.shadows[1]
}}>
{/* العنوان وزر الفلاتر */}
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
p={{ xs: 1, sm: 2 }}
sx={{ backgroundColor: theme.palette.background.paper }}
>
<Typography variant="h6" sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
Top Selling Product
</Typography>
<Button
sx={{
fontSize: { xs: '12px', sm: '16px' },
fontWeight: 500,
border: '1px solid #e0e0e0',
color: '#667085',
textTransform: 'none',
p: { xs: '4px 8px', sm: '6px 16px' }
}}
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
>
{isMobile ? '' : 'Filters'}
</Button>
</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: 650,
'& .MuiTableCell-root': {
borderBottom: '1px solid #e0e0e0',
py: { xs: 0.5, sm: 1.5 },
px: { xs: 0.5, sm: 2 },
},
'& 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>Product</TableCell>
{!isMobile && <TableCell>Sales</TableCell>}
<TableCell>Amount</TableCell>
{!isMobile && <TableCell>Price</TableCell>}
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedData.map((row, i) => (
<TableRow key={i} sx={{ height: { xs: '8vh', sm: '13vh' } }}>
<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>}
<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>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{/* التذييل مع الترقيم */}
<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, data.length)} of {data.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: '#FFB088',
color: '#fff',
},
'&:hover': {
backgroundColor: '#FFD6B5',
},
},
}}
/>
</Box>
</Paper>
);
};
export default TopSellingProduct;
+55 -138
View File
@@ -1,4 +1,5 @@
import React from 'react';
// src/components/AppBar/KitchPlusAppBar.jsx
import React, { useEffect, useState, useContext } from 'react';
import {
AppBar,
Toolbar,
@@ -7,31 +8,37 @@ import {
IconButton,
Divider,
Avatar,
Button,
Autocomplete,
TextField,
InputAdornment,
useTheme,
useMediaQuery,
} from '@mui/material';
import { useLocation } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import MenuIcon from '@mui/icons-material/Menu';
import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import SearchIcon from '@mui/icons-material/Search';
const top100Films = [
{ title: 'The Shawshank Redemption' },
{ title: 'The Godfather' },
{ title: 'The Dark Knight' },
{ title: 'Pulp Fiction' },
];
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
@@ -54,156 +61,66 @@ const KitchPlusAppBar = ({ onDrawerToggle, sidebarOpen, isMobile }) => {
justifyContent: 'space-between',
}}
>
{/* Left side with toggle button */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/* Toggle button for mobile/tablet */}
{/* Left side */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
mr: 2,
}}
>
{(isMobile || isMediumScreen) && (
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={onDrawerToggle}
sx={{ mr: 2 }}
>
<IconButton color="inherit" edge="start" onClick={onDrawerToggle}>
<MenuIcon />
</IconButton>
)}
{location.pathname === '/dashboard' ? (
<Typography
variant="h6"
component="div"
sx={{
fontWeight: '500',
fontSize: { xs: '18px', sm: '20px', md: '24px' },
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: { xs: '200px', sm: 'none' }
}}
>
Welcome to KitchPlus
</Typography>
) : (
<Autocomplete
sx={{
width: { xs: '200px', sm: '200px', md: '250px' },
borderRadius: '8px',
}}
freeSolo
id="free-solo-2-demo"
disableClearable
options={top100Films.map((option) => option.title)}
renderInput={(params) => (
<TextField
{...params}
placeholder="Search"
InputProps={{
...params.InputProps,
type: 'search',
endAdornment: (
<InputAdornment position="end">
<SearchIcon sx={{ color: '#667085' }} />
</InputAdornment>
),
}}
sx={{
'& .MuiInputBase-root': {
backgroundColor: 'white',
height: { xs: 36, sm: 38, md: 40 },
},
}}
/>
)}
/>
)}
<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' && !isSmallScreen && (
<Button
variant="contained"
sx={{
display: { xs: 'none', sm: 'none', md: 'flex' }, // هذا السطر يخفي الزر عند xs و sm ويظهره من md وما فوق
color: 'white',
height: { xs: 32, sm: 36, md: 40 },
mr: { xs: '6px', sm: '8px', md: '0px' },
backgroundColor: '#61677F',
borderRadius: '8px',
fontWeight: 600,
fontSize: { xs: '12px', sm: '13px', md: '14px' },
textTransform: 'none',
whiteSpace: 'nowrap',
}}
>
{isMediumScreen ? 'Switch Company' : 'Switch Company Profits'}
</Button>
)}
{location.pathname === '/dashboard' && (
<Divider
orientation="vertical"
flexItem
sx={{
height: { xs: 30, sm: 36, md: 40 },
alignSelf: 'center',
}}
/>
)}
<IconButton color="#667085" size={isSmallScreen ? 'small' : 'medium'}>
<NotificationsOutlinedIcon fontSize={isSmallScreen ? 'small' : 'medium'} />
{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: { xs: 30, sm: 36, md: 40 },
alignSelf: 'center'
}}
/>
<Divider orientation="vertical" flexItem sx={{ height: 40 }} />
<Avatar
alt="Admin"
src="/images/waitress3.png"
sx={{
width: { xs: 28, sm: 32, md: 40 },
height: { xs: 28, sm: 32, md: 40 }
}}
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: { sm: 0.5, md: 1 },
ml: 1,
color: '#61677F',
fontSize: { xs: '12px', sm: '13px', md: '14px' },
fontSize: { xs: 12, sm: 13, md: 14 },
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: { xs: '100px', sm: '120px', md: 'none' }
overflow: 'visible',
textOverflow: 'clip',
maxWidth: 200,
}}
>
Admin@gmail.com
{user?.email || 'Admin@gmail.com'}
</Typography>
)}
<IconButton
color="inherit"
sx={{
mt: 0.5,
color: '#A6ACB8',
padding: { xs: '4px', sm: '8px' }
}}
size={isSmallScreen ? 'small' : 'medium'}
>
<KeyboardArrowDownIcon fontSize={isSmallScreen ? 'small' : 'medium'} />
</IconButton>
</Box>
</Toolbar>
</AppBar>
);
};
export default KitchPlusAppBar;
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;
+9 -11
View File
@@ -8,27 +8,25 @@ const drawerWidth = 230;
const Cashier = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [hasProducts, setHasProducts] = useState(false); // حالة لتتبع وجود المنتجات
const [hasProducts, setHasProducts] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
// محاكاة للتحقق من وجود المنتجات (استبدل هذا بمنطقك الفعلي)
useEffect(() => {
// هنا يجب استبدال هذا بمنطق فعلي للتحقق من وجود المنتجات
// مثلاً استدعاء API أو التحقق من state
const checkProducts = async () => {
// محاكاة لاستدعاء API
const productsExist = await checkIfProductsExist(); // استبدل هذه الدالة بمنطقك الفعلي
const productsExist = await checkIfProductsExist();
setHasProducts(productsExist);
};
checkProducts();
}, []);
// دالة مساعدة لمحاكاة التحقق من المنتجات (استبدلها بمنطقك الفعلي)
const checkIfProductsExist = async () => {
// محاكاة - يمكن أن يكون هذا استدعاء لـ API أو تحقق من state
// return true;
return false; // غير هذه القيمة حسب منطقك
return false;
};
useEffect(() => {
@@ -62,7 +60,7 @@ const Cashier = () => {
<Box sx={{
display: 'flex',
height: '100vh',
backgroundColor:'#F6F6F6',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}>
<Sidebar
@@ -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;
@@ -1,36 +1,119 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery, Typography } from '@mui/material';
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 [hasProducts, setHasProducts] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
// محاكاة للتحقق من وجود المنتجات (استبدل هذا بمنطقك الفعلي)
useEffect(() => {
// هنا يجب استبدال هذا بمنطق فعلي للتحقق من وجود المنتجات
// مثلاً استدعاء API أو التحقق من state
const checkProducts = async () => {
// محاكاة لاستدعاء API
const productsExist = await checkIfProductsExist(); // استبدل هذه الدالة بمنطقك الفعلي
setHasProducts(productsExist);
};
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
checkProducts();
}, []);
// إدارة بيانات النموذج لجميع الخطوات (مشاركة البيانات بين الخطوات)
const [formData, setFormData] = useState({
// دالة مساعدة لمحاكاة التحقق من المنتجات (استبدلها بمنطقك الفعلي)
const checkIfProductsExist = async () => {
// محاكاة - يمكن أن يكون هذا استدعاء لـ API أو تحقق من state
// return true;
return false; // غير هذه القيمة حسب منطقك
});
// دالة لتحديث بيانات النموذج بشكل مرن (دمج الجديد مع القديم)
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);
@@ -50,7 +133,6 @@ const CreateRestaurant = () => {
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [theme.breakpoints.values.md]);
@@ -59,12 +141,14 @@ const CreateRestaurant = () => {
};
return (
<Box sx={{
display: 'flex',
height: '100vh',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}>
<Box
sx={{
display: 'flex',
height: '100vh',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}
>
<Sidebar
open={sidebarOpen}
onClose={handleDrawerToggle}
@@ -72,34 +156,102 @@ const CreateRestaurant = () => {
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,
}),
}}>
<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>CreateYourRestaurant</Typography>
<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;
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;
+14 -11
View File
@@ -11,23 +11,23 @@ const Dashboard = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [hasProducts, setHasProducts] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// const [isLoading, setIsLoading] = useState(true);
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
// محاكاة التحقق من المنتجات
useEffect(() => {
const checkProducts = async () => {
setIsLoading(true);
const productsExist = await checkIfProductsExist(); // استبدل بمنطقك
// setIsLoading(true);
const productsExist = await checkIfProductsExist();
setHasProducts(productsExist);
setIsLoading(false);
// setIsLoading(false);
};
checkProducts();
}, []);
const checkIfProductsExist = async () => {
return new Promise((resolve) => setTimeout(() => resolve(true), 1500)); // محاكاة تأخير
return new Promise((resolve) => setTimeout(() => resolve(true), 1500));
};
useEffect(() => {
@@ -76,8 +76,8 @@ const Dashboard = () => {
<Box sx={{
flexGrow: 1,
width: sidebarOpen ? 'calc(100% - 20px)' : '100%',
pt: { xs: 0.5, sm: 1 },
mt: { xs: 1, sm: 2 },
// pt: { xs: 0.5, sm: 1 },
// mt: { xs: 1, sm: 2 },
overflowY: 'auto',
pl: { xs: 0, sm: 2 },
pr: { xs: 1, sm: 2 },
@@ -89,15 +89,18 @@ const Dashboard = () => {
duration: theme.transitions.duration.leavingScreen,
}),
}}>
{isLoading ? (
{/* {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 />
)}
hasProducts ?
<DashbordContect />
: <NoProdectDash />
)} */}
<DashbordContect />
</Box>
</Box>
</Box>
+16 -124
View File
@@ -7,9 +7,9 @@ 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 OrderStatusCard from './OrderStatusCard';
import StatisticsCard from './StatisticsCard';
import RecentActivity from './RecentActivity';
import AnalyticsPage from './AnalyticsPage';
import AnalyticsPageOccupancy from './AnalyticsPageOccupancy';
import AnalyticsPagebill from './AnalyticsPagebill';
const IconCircle = ({ children, bgColor = '#DEDEFA', outerColor = '#EFEFFD' }) => (
<Box
@@ -52,111 +52,6 @@ const IconCircle = ({ children, bgColor = '#DEDEFA', outerColor = '#EFEFFD' }) =
</Box>
);
const StatusCard = ({
icon,
statusText,
statusColor,
iconColor,
outerColor,
innerColor,
height = {
xs: 'calc(140px + 0.5vh)',
sm: 'calc(150px + 0.5vh)',
md: 'calc(162px + 0.5vh)'
},
width = {
xs: 'min(90%, 600px)', // تقليل من 90% إلى 85% والحد الأقصى من 300px إلى 280px
sm: 'clamp(220px, 23vw, 280px)', // تقليل جميع القيم
md: 'clamp(220px, 19vw, 300px)'
},
transition = 'width 0.3s ease',
iconSpacing = { xs: 6, sm: 8, md: 12 },
extraButton,
statusButtonWidth,
title = 'Point Of Sale',
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<Card sx={{
height,
width,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
p: 0,
pb: 2,
borderRadius: '8px',
boxShadow: 'none',
minWidth: { xs: 180, sm: 220 }
}}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: iconSpacing }}>
<IconCircle bgColor={innerColor} outerColor={outerColor}>
{React.cloneElement(icon, { sx: { color: iconColor, fontSize: { xs: 16, sm: 18, md: 20 } } })}
</IconCircle>
<Button
variant="contained"
sx={{
backgroundColor: '#E7F4EE',
color: statusColor,
textTransform: 'none',
boxShadow: 'none',
borderRadius: '100px',
width: statusButtonWidth || 'auto',
fontSize: { xs: '0.7rem', sm: '0.8rem', md: '0.9rem' },
px: { xs: 1, sm: 1.5 },
py: 0.5
}}
>
{statusText}
</Button>
</Box>
<Typography
variant="h6"
gutterBottom
mt={1.5}
sx={{
color: '#667085',
fontSize: { xs: '14px', sm: '15px', md: '16px' },
fontWeight: 500
}}
>
{title}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
{extraButton ? extraButton : <Box />}
<Button
variant="contained"
sx={{
backgroundColor: '#F7F7F7',
width: { xs: '26px', sm: '28px', md: '30px' },
height: { xs: '26px', sm: '28px', md: '30px' },
minWidth: '0',
padding: 0,
borderRadius: '12px',
boxShadow: 'none',
'&:hover': {
backgroundColor: '#e0e0e0',
},
}}
>
<ArrowOutwardIcon
sx={{
color: theme.palette.primary.main,
width: { xs: '16px', sm: '18px', md: '20px' },
height: { xs: '16px', sm: '18px', md: '20px' },
}}
/>
</Button>
</Box>
</CardContent>
</Card>
);
};
const DashboardContect = () => {
const theme = useTheme();
@@ -172,7 +67,7 @@ const DashboardContect = () => {
display: 'flex',
justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
mb: 3,
mb: 1,
pl: 1,
pr: { xs: 1, sm: 3 },
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
@@ -192,7 +87,7 @@ const DashboardContect = () => {
</Typography>
{/* Buttons */}
<Box sx={{
{/* <Box sx={{
display: 'flex',
gap: { xs: 1, sm: 2 },
flexWrap: { xs: 'wrap', sm: 'nowrap' },
@@ -260,11 +155,11 @@ const DashboardContect = () => {
>
Check Inventory
</Button>
</Box>
</Box> */}
</Box>
{/* Status Cards */}
<Box
{/* <Box
sx={{
display: 'flex',
@@ -333,7 +228,7 @@ const DashboardContect = () => {
iconSpacing={{ xs: 3, sm: 5, md: 7 }}
title="Delivery"
/>
</Box>
</Box> */}
{/* OrderStatusCard/ StatisticsCard */}
<Box
@@ -343,12 +238,8 @@ const DashboardContect = () => {
flexWrap: 'nowrap',
flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 1, sm: 1, md: 7 }, // gap مرن
mb: 1,
pl: 1,
pr: 1,
mt: 2,
mr: { xs: 4, sm: 3, md: 0 },
ml: { xs: 2, sm: 0 },
mr: 2,
flexShrink: 0,
}}
>
@@ -357,12 +248,13 @@ const DashboardContect = () => {
sx={{
flexGrow: 0,
width: { xs: '100%', sm: '35%', md: '30%' },
minWidth: 220,
minWidth: 20,
ml: { xs: 0, sm: -1 },
flexShrink: 0,
mt:8
}}
>
<OrderStatusCard />
<AnalyticsPageOccupancy />
</Box>
{/* StatisticsCard */}
@@ -370,11 +262,11 @@ const DashboardContect = () => {
sx={{
flexGrow: 1,
minWidth: 0,
ml: { xs: 0, sm: 2 },
// ml: { xs: 0, sm: 2 },
// backgroundColor:'red'
}}
>
<StatisticsCard />
<AnalyticsPage />
</Box>
</Box>
@@ -382,11 +274,11 @@ const DashboardContect = () => {
{/* RecentActivity */}
<Box
sx={{
mr: { xs: 4, sm: 3 ,md:0 },
mr: { xs: 4, sm: 3, md: 0 },
ml: { xs: 2, sm: 0 },
}}
>
<RecentActivity />
<AnalyticsPagebill />
</Box>
</>
);
+3 -11
View File
@@ -23,22 +23,14 @@ const NoProdectDash = () => {
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 }
}}
>
<Typography
variant="h6"
sx={{
fontWeight: '500',
fontSize: { xs: '20px', sm: '22px', md: '24px' },
color: '#121212'
}}
>
Dashboard
</Typography>
</Box>
{/* Empty State Content */}
@@ -76,7 +68,7 @@ const NoProdectDash = () => {
fontSize: { xs: '14px', md: '18px' },
mb: 2,
color: '#5F6868',
whiteSpace: 'pre-line' // هذا يسمح بكسر السطر عند المسافات
whiteSpace: 'pre-line' // يسمح بكسر السطر عند المسافات
}}
>
<Box component="span" sx={{ fontWeight: 600, fontSize: '18px' }}>Sorry!</Box>{' '}
+218 -237
View File
@@ -1,260 +1,241 @@
import React from 'react';
import React, { useState } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Divider,
Chip,
useTheme,
IconButton,
useMediaQuery
Box,
Typography,
Card,
CardContent,
useTheme,
IconButton,
useMediaQuery,
Menu,
MenuItem,
Button,
ButtonGroup,
TextField
} from '@mui/material';
import {
TrendingUp as TrendingUpIcon,
TrendingDown as TrendingDownIcon,
CheckCircle as CheckCircleIcon,
Cancel as CancelIcon,
Pending as PendingIcon
TrendingUp as TrendingUpIcon,
TrendingDown as TrendingDownIcon,
} from '@mui/icons-material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import {
CircularProgressbar,
buildStyles
CircularProgressbar,
buildStyles
} from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
// import { Box, IconButton, Paper, Typography, useTheme, useMediaQuery } from '@mui/material';
import {
ResponsiveContainer,
} from 'recharts';
import { ResponsiveContainer } from 'recharts';
import dayjs from 'dayjs';
const OrderStatusCard = ({
title = 'Order Status',
period = 'This Quarter',
progress = 70.5,
progressChange = '+10%',
progressDirection = 'up', // or 'down'
description = 'You succeeded in earning $240 today, its higher than yesterday',
completed = '$20k',
canceled = '$16k',
pending = '$1.5k'
title = 'Order Status',
period = 'This Quarter',
progress = 70.5,
description = 'You succeeded in earning $240 today, its higher than yesterday',
timeFrame,
setTimeFrame,
customDate,
setCustomDate
}) => {
const theme = useTheme();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const [anchorEl, setAnchorEl] = useState(null);
const progressColor = progressDirection === 'up' ? theme.palette.success.main : theme.palette.error.main;
const progressIcon = progressDirection === 'up' ? <TrendingUpIcon fontSize="small" /> : <TrendingDownIcon fontSize="small" />;
const highlightedDescription = description.replace(
/(\$\d+(?:\.\d+)?[kKmM]?)/g,
'<span style="color:#000;font-weight:500;">$1</span>'
);
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
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: 'auto',
borderRadius: 2,
boxShadow: '0px 4px 5px rgba(0, 0, 0, 0.1)',
pl: { xs: 1, sm: 1 },
pr: { xs: 1, sm: 1 },
pb: { xs: 1, sm: 1 },
pt: { xs: 0, sm: 0 },
position: 'relative' // إضافة هذه الخاصية
}}
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'
}}
>
<ResponsiveContainer width="100%" height={isMobile ? '75vh' : isTablet ? '59vh' : 385}>
{/* زر الإجراءات في الزاوية العلوية اليمنى */}
<IconButton
aria-label="more-actions"
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(); }}
>
<MoreVertIcon />
</IconButton>
{label}
</Button>
))}
</ButtonGroup>
<CardContent>
{/* Header */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
{/* 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>
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'
}}
>
{period}
</Typography>
</Box>
{/* المحتوى الأساسي للكارت */}
<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>
{/* Progress Circle */}
<Box sx={{ height: 120, mb: 2, position: 'relative' }}>
{/* تغيير النسبة تحت الدائرة */}
<Box
sx={{
position: 'absolute',
top: '80%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
}}
>
<Chip
label={progressChange}
size="small"
icon={progressIcon}
sx={{
height: 22,
fontSize: '0.75rem',
color: progressDirection === 'up' ? '#087443' : '#B42318',
backgroundColor: progressDirection === 'up' ? '#ECFDF3' : '#FEF3F2',
'.MuiChip-icon': {
color: progressDirection === 'up' ? '#087443' : '#B42318',
},
}}
/>
</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: 'relative', width: '100%', maxWidth: 250, 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,
textColor: 'transparent',
})}
/>
</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>
{/* النص داخل الدائرة */}
<Box
sx={{
position: 'absolute',
top: '22%',
left: '50%',
transform: 'translate(-50%, -50%)',
fontSize: { xs: '18px', sm: '24px' },
fontWeight: 500,
color: theme.palette.text.primary,
}}
>
{`${progress}%`}
</Box>
</Box>
</Box>
{/* الوصف */}
<Typography
variant="body2"
sx={{
mb: 1,
mt: 5,
color: '#667085',
fontSize: { xs: '14px', sm: '16px' },
}}
dangerouslySetInnerHTML={{ __html: highlightedDescription }}
/>
<Divider sx={{ my: 2 }} />
{/* الحقول الثلاثة (Completed / Canceled / Pending) */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
gap: { xs: 1, sm: 2 },
flexDirection: { xs: 'column', sm: 'row' },
ml: -1
}}
>
{/* لكل قسم: */}
{[{
label: 'Completed',
icon: <ArrowUpwardIcon color="success" fontSize="small" sx={{ mt: 0.2, ml: -1 }} />,
value: completed
}, {
label: 'Canceled',
icon: <ArrowDownwardIcon color="error" fontSize="small" sx={{ mt: 0.6, ml: -1 }} />,
value: canceled
}, {
label: 'Pending',
icon: <ArrowDownwardIcon color="warning" fontSize="small" sx={{ mt: 0.6, ml: -1 }} />,
value: pending
}].map(({ label, icon, value }, i) => (
<Box
key={i}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
flex: 1,
mb: { xs: 1, sm: 0 }
}}
>
<Typography
variant="body2"
sx={{ color: '#667085', fontSize: '12px', fontWeight: 500, mb: 1 }}
>
{label}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{icon}
<Typography sx={{ color: '#333843', fontSize: { xs: '14px', sm: '16px',md:'24px'}, fontWeight: 500 }}>
{value}
</Typography>
</Box>
</Box>
))}
</Box>
</CardContent>
</ResponsiveContainer>
</Card>
);
{/* الوصف */}
<Typography
variant="body2"
sx={{
mb: 1,
mt: 10,
color: '#667085',
fontSize: { xs: '14px', sm: '16px' },
}}
>
{description}
</Typography>
</CardContent>
</ResponsiveContainer>
</Card>
);
};
export default OrderStatusCard;
@@ -12,9 +12,6 @@ import {
Button,
Chip
} from '@mui/material';
// import LocalShippingIcon from '@mui/icons-material/LocalShipping';
// import InventoryIcon from '@mui/icons-material/Inventory';
// import AssignmentIcon from '@mui/icons-material/Assignment';
const RecentActivity = () => {
// Sample data
+170 -136
View File
@@ -1,60 +1,92 @@
import React from 'react';
import { Box, IconButton, Paper, Typography, useTheme, useMediaQuery } from '@mui/material';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
LineChart,
Line,
Box,
IconButton,
Paper,
Typography,
useTheme,
useMediaQuery,
Menu,
MenuItem,
TextField,
Button
} from '@mui/material';
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
Area,
AreaChart,
CartesianGrid,
defs,
linearGradient,
stop
CartesianGrid
} from 'recharts';
import MoreVertIcon from '@mui/icons-material/MoreVert';
// import MoreVertIcon from '@mui/icons-material/MoreVert';
const data = [
{ month: 'Jan', revenue: 4000, sales: 2400 },
{ month: 'Feb', revenue: 3000, sales: 1398 },
{ month: 'Mar', revenue: 2000, sales: 9800 },
{ month: 'Apr', revenue: 2780, sales: 3908 },
{ month: 'May', revenue: 1890, sales: 4800 },
{ month: 'Jun', revenue: 2390, sales: 3800 },
{ month: 'Jul', revenue: 3490, sales: 4300 },
{ month: 'Aug', revenue: 5000, sales: 4000 },
{ month: 'Sep', revenue: 4700, sales: 4200 },
{ month: 'Oct', revenue: 5200, sales: 4600 },
{ month: 'Nov', revenue: 4800, sales: 4400 },
{ month: 'Dec', revenue: 5300, sales: 4800 },
];
const formatCurrency = (value) => {
if (value >= 1000000) {
return `$${(value / 1000000).toFixed(1)}M`;
} else if (value >= 1000) {
return `$${(value / 1000).toFixed(1)}K`;
}
return `$${value}`;
};
const StatisticsCard = () => {
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={{ width: '100%' }}>
<Paper sx={{
<Box sx={{ borderRadius: 2, width: '100%' }}>
<Paper sx={{
p: { xs: 1.5, sm: 2 },
mb: { xs: 2, sm: 3 },
boxShadow: '0px 4px 5px rgba(0, 0, 0, 0.1)',
position: 'relative',
borderRadius: 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"
@@ -64,128 +96,115 @@ const StatisticsCard = () => {
right: { xs: 4, sm: 8 },
color: '#667085'
}}
onClick={handleMenuOpen}
>
<MoreVertIcon fontSize={isMobile ? 'small' : 'medium'} />
{/* <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',
flexDirection: 'column',
mb: { xs: 1, sm: 2 },
pr: { xs: 3, sm: 4 }
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
flexWrap: 'wrap',
gap: 1
}}>
<Typography variant="h6" sx={{
fontSize: { xs: '15px', sm: '18px' },
fontWeight: 500
}}>
Statistics
</Typography>
<Typography sx={{
fontSize: { xs: '11px', sm: '14px' },
fontWeight: 500,
color: '#667085'
}}>
Delivery Times
</Typography>
<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>
<ResponsiveContainer width="100%" height={isMobile ? 250 : isTablet ? 290 : 295}>
<AreaChart data={data} margin={{
top: isMobile ? -30 : -45,
right: isMobile ? 15 : 30,
left: isMobile ? 10 : 20,
bottom: isMobile ? 10 : 20
{/* 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>
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={theme.palette.primary.main} stopOpacity={0.8} />
<stop offset="100%" stopColor={theme.palette.primary.main} stopOpacity={0} />
</linearGradient>
<linearGradient id="colorSales" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#81C2FF" stopOpacity={0.8} />
<stop offset="100%" stopColor="#81C2FF" stopOpacity={0} />
</linearGradient>
{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="month"
dataKey={xDataKey}
axisLine={false}
tickLine={false}
tickMargin={isMobile ? 8 : 15}
tick={{ fontSize: isMobile ? 11 : 12 }}
tick={{
fontSize: isMobile ? 11 : 12,
angle: -25,
textAnchor: 'end'
}}
interval={0}
/>
<YAxis
tickFormatter={formatCurrency}
ticks={ticksArray}
tickFormatter={valueFormatter}
axisLine={false}
tickLine={false}
tickMargin={isMobile ? 8 : 15}
tick={{ fontSize: isMobile ? 11 : 12 }}
/>
<Tooltip />
<Tooltip formatter={valueFormatter} />
<Legend
verticalAlign="top"
height={isMobile ? 30 : 36}
iconType="circle"
iconSize={isMobile ? 8 : 10}
wrapperStyle={{
paddingBottom: isMobile ? '15px' : '20px'
}}
/>
<Area
name="Revenue"
type="monotone"
dataKey="revenue"
stroke='#E46A11'
strokeWidth={isMobile ? 2 : 3}
fill="url(#colorRevenue)"
/>
<Area
type="monotone"
name="Sales"
dataKey="sales"
stroke="#0182FC"
strokeWidth={isMobile ? 2 : 3}
fill="url(#colorSales)"
/>
<Line
type="monotone"
dataKey="revenue"
stroke={theme.palette.primary.main}
strokeWidth={isMobile ? 2 : 3}
dot={{
r: isMobile ? 4 : 6,
fill: theme.palette.primary.main,
stroke: '#fff',
strokeWidth: 2
}}
activeDot={{
r: isMobile ? 6 : 8,
fill: theme.palette.primary.main,
stroke: '#fff',
strokeWidth: 2
}}
/>
<Line
type="monotone"
dataKey="sales"
stroke="#0182FC"
strokeWidth={isMobile ? 2 : 3}
dot={{
r: isMobile ? 4 : 6,
fill: "#0182FC",
stroke: '#fff',
strokeWidth: 2
}}
activeDot={{
r: isMobile ? 6 : 8,
fill: "#0182FC",
stroke: '#fff',
strokeWidth: 2
}}
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>
@@ -193,4 +212,19 @@ const StatisticsCard = () => {
);
};
export default StatisticsCard;
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;
@@ -1,103 +0,0 @@
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 HostKitchen = () => {
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 () => {
// محاكاة - يمكن أن يكون هذا استدعاء لـ API أو تحقق من state
// return true;
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>HostKitchen</Typography>
</Box>
</Box>
);
};
export default HostKitchen;
+107 -70
View File
@@ -1,105 +1,142 @@
import React, { useState, useEffect } from 'react';
import { Box, useTheme, useMediaQuery, Typography } from '@mui/material';
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 [hasProducts, setHasProducts] = useState(false); // حالة لتتبع وجود المنتجات
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
// محاكاة للتحقق من وجود المنتجات (استبدل هذا بمنطقك الفعلي)
useEffect(() => {
// هنا يجب استبدال هذا بمنطق فعلي للتحقق من وجود المنتجات
// مثلاً استدعاء API أو التحقق من state
const checkProducts = async () => {
// محاكاة لاستدعاء API
const productsExist = await checkIfProductsExist(); // استبدل هذه الدالة بمنطقك الفعلي
setHasProducts(productsExist);
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',
};
checkProducts();
}, []);
setProducts((prev) => [...prev, formattedProduct]);
// دالة مساعدة لمحاكاة التحقق من المنتجات (استبدلها بمنطقك الفعلي)
const checkIfProductsExist = async () => {
// محاكاة - يمكن أن يكون هذا استدعاء لـ API أو تحقق من state
// return true;
return false; // غير هذه القيمة حسب منطقك
setShowProductEntry(false);
};
useEffect(() => {
if (window.innerWidth >= theme.breakpoints.values.md) {
setSidebarOpen(true);
} else {
setSidebarOpen(false);
}
}, [theme.breakpoints.values.md]);
const handleAddNewProduct = () => {
setShowProductEntry(true);
};
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);
const handleShowTable = () => {
setShowProductEntry(false);
};
return (
<Box sx={{
display: 'flex',
height: '100vh',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}>
<Box
sx={{
display: 'flex',
height: '100vh',
backgroundColor: '#F6F6F6',
overflow: 'hidden',
}}
>
<Sidebar
open={sidebarOpen}
onClose={handleDrawerToggle}
onClose={() => setSidebarOpen(!sidebarOpen)}
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,
}),
}}>
<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}
onDrawerToggle={() => setSidebarOpen(!sidebarOpen)}
sidebarOpen={sidebarOpen}
isMobile={isMobile}
/>
<Typography>Inventory</Typography>
<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;
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;
@@ -8,9 +8,9 @@ import {
useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import AddIcon from '@mui/icons-material/Add';
const NoTraining = () => {
const NoProdectDash = () => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
@@ -23,24 +23,15 @@ const NoTraining = () => {
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 }
}}
>
<Typography
variant="h6"
sx={{
fontWeight: '500',
fontSize: { xs: '20px', sm: '22px', md: '24px' },
color: '#121212'
}}
>
Dashboard
</Typography>
</Box>
{/* Empty State Content */}
<Card
sx={{
@@ -80,16 +71,31 @@ const NoTraining = () => {
}}
>
<Box component="span" sx={{ fontWeight: 600, fontSize: '18px' }}>Sorry!</Box>{' '}
There are no updates now..
{'\n'}check back later.
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 NoTraining;
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;

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