forked from RaghadAlkhous/RestaurantDash
Initial commit - restaurant dashboard
This commit is contained in:
148
package-lock.json
generated
148
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
|
||||
"name": "resturant-dashBord",
|
||||
"name": "resturant-dashbord",
|
||||
|
||||
"version": "0.1.0",
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
|
||||
"": {
|
||||
|
||||
"name": "resturant-dashBord",
|
||||
"name": "resturant-dashbord",
|
||||
|
||||
"version": "0.1.0",
|
||||
|
||||
"license": "ISC",
|
||||
|
||||
"dependencies": {
|
||||
|
||||
"@emotion/react": "^11.14.0",
|
||||
@@ -28,6 +30,8 @@
|
||||
|
||||
"@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",
|
||||
@@ -40,12 +44,18 @@
|
||||
|
||||
"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",
|
||||
@@ -7210,6 +7220,26 @@
|
||||
|
||||
},
|
||||
|
||||
"node_modules/@react-oauth/google": {
|
||||
|
||||
"version": "0.12.2",
|
||||
|
||||
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz",
|
||||
|
||||
"integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==",
|
||||
|
||||
"license": "MIT",
|
||||
|
||||
"peerDependencies": {
|
||||
|
||||
"react": ">=16.8.0",
|
||||
|
||||
"react-dom": ">=16.8.0"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
"node_modules/@rollup/plugin-babel": {
|
||||
|
||||
"version": "5.3.1",
|
||||
@@ -14650,6 +14680,18 @@
|
||||
|
||||
},
|
||||
|
||||
"node_modules/dayjs": {
|
||||
|
||||
"version": "1.11.13",
|
||||
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
|
||||
"license": "MIT"
|
||||
|
||||
},
|
||||
|
||||
"node_modules/debug": {
|
||||
|
||||
"version": "4.4.0",
|
||||
@@ -18674,6 +18716,60 @@
|
||||
|
||||
},
|
||||
|
||||
"node_modules/framer-motion": {
|
||||
|
||||
"version": "12.23.0",
|
||||
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.0.tgz",
|
||||
|
||||
"integrity": "sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==",
|
||||
|
||||
"license": "MIT",
|
||||
|
||||
"dependencies": {
|
||||
|
||||
"motion-dom": "^12.22.0",
|
||||
|
||||
"motion-utils": "^12.19.0",
|
||||
|
||||
"tslib": "^2.4.0"
|
||||
|
||||
},
|
||||
|
||||
"peerDependencies": {
|
||||
|
||||
"@emotion/is-prop-valid": "*",
|
||||
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
|
||||
},
|
||||
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
"@emotion/is-prop-valid": {
|
||||
|
||||
"optional": true
|
||||
|
||||
},
|
||||
|
||||
"react": {
|
||||
|
||||
"optional": true
|
||||
|
||||
},
|
||||
|
||||
"react-dom": {
|
||||
|
||||
"optional": true
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
"node_modules/fresh": {
|
||||
|
||||
"version": "0.5.2",
|
||||
@@ -24062,6 +24158,24 @@
|
||||
|
||||
},
|
||||
|
||||
"node_modules/jwt-decode": {
|
||||
|
||||
"version": "4.0.0",
|
||||
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
|
||||
"license": "MIT",
|
||||
|
||||
"engines": {
|
||||
|
||||
"node": ">=18"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
"node_modules/keyv": {
|
||||
|
||||
"version": "4.5.4",
|
||||
@@ -24970,6 +25084,36 @@
|
||||
|
||||
},
|
||||
|
||||
"node_modules/motion-dom": {
|
||||
|
||||
"version": "12.22.0",
|
||||
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.22.0.tgz",
|
||||
|
||||
"integrity": "sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==",
|
||||
|
||||
"license": "MIT",
|
||||
|
||||
"dependencies": {
|
||||
|
||||
"motion-utils": "^12.19.0"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
"node_modules/motion-utils": {
|
||||
|
||||
"version": "12.19.0",
|
||||
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz",
|
||||
|
||||
"integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==",
|
||||
|
||||
"license": "MIT"
|
||||
|
||||
},
|
||||
|
||||
"node_modules/ms": {
|
||||
|
||||
"version": "2.1.3",
|
||||
|
||||
10
package.json
10
package.json
@@ -18,6 +18,8 @@
|
||||
|
||||
"@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",
|
||||
@@ -30,12 +32,18 @@
|
||||
|
||||
"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",
|
||||
@@ -106,8 +114,6 @@
|
||||
|
||||
"main": "restored-file.js",
|
||||
|
||||
"devDependencies": {},
|
||||
|
||||
"author": "",
|
||||
|
||||
"license": "ISC"
|
||||
|
||||
BIN
public/fonts/Hind Siliguri/HindSiliguri-Bold.ttf
Normal file
BIN
public/fonts/Hind Siliguri/HindSiliguri-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Hind Siliguri/HindSiliguri-Light.ttf
Normal file
BIN
public/fonts/Hind Siliguri/HindSiliguri-Light.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Hind Siliguri/HindSiliguri-Medium.ttf
Normal file
BIN
public/fonts/Hind Siliguri/HindSiliguri-Medium.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Hind Siliguri/HindSiliguri-Regular.ttf
Normal file
BIN
public/fonts/Hind Siliguri/HindSiliguri-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Hind Siliguri/HindSiliguri-SemiBold.ttf
Normal file
BIN
public/fonts/Hind Siliguri/HindSiliguri-SemiBold.ttf
Normal file
Binary file not shown.
BIN
public/image.png
BIN
public/image.png
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 1.0 MiB |
BIN
public/images/dash.png
Normal file
BIN
public/images/dash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
public/images/image.png
Normal file
BIN
public/images/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
24
src/App.css
24
src/App.css
@@ -2,7 +2,6 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
/* background-color: #9aa0b8; */
|
||||
}
|
||||
|
||||
body,
|
||||
@@ -122,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 === */
|
||||
|
||||
151
src/App.js
151
src/App.js
@@ -1,129 +1,62 @@
|
||||
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 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 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';
|
||||
import PrivateRoute from './components/Routes/PrivateRoute';
|
||||
import PublicRoute from './components/Routes/PublicRoute';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { TermsPage, PrivacyPage } from './components/Authentication/Legal/TermsPrivacyComponents';
|
||||
|
||||
import { RestaurantProvider } from './contexts/RestaurantContext';
|
||||
import ProtectedRoutes from './components/Routes/ProtectedRoutes';
|
||||
import { SnackbarProvider } from "./contexts/SnackbarContext";
|
||||
import { UserProvider } from './contexts/UserContext';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Router>
|
||||
<Routes>
|
||||
{/* الصفحات العامة */}
|
||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||
<GoogleOAuthProvider clientId="443509481732-4i5ne811hr90rn75q3fbqevqi9eu3lvk.apps.googleusercontent.com">
|
||||
<Router>
|
||||
<UserProvider>
|
||||
{/* ✅ لفّ كل المشروع بـ SnackbarProvider */}
|
||||
<SnackbarProvider>
|
||||
<Routes>
|
||||
{/* التوجيه التلقائي للصفحة الرئيسية */}
|
||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||
|
||||
<Route path="/" element={<PublicRoute><LoginForm /></PublicRoute>} />
|
||||
<Route path="/login" element={<PublicRoute><LoginForm /></PublicRoute>} />
|
||||
<Route path="/register" element={<PublicRoute><RegisterForm /></PublicRoute>} />
|
||||
<Route path="/forget" element={<PublicRoute><ForgetPassword /></PublicRoute>} />
|
||||
<Route path="/login" element={<PublicRoute><LoginForm /></PublicRoute>} />
|
||||
<Route path="/register" element={<PublicRoute><RegisterForm /></PublicRoute>} />
|
||||
<Route path="/forget" element={<PublicRoute><ForgetPassword /></PublicRoute>} />
|
||||
|
||||
{/* الصفحات المحمية */}
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Dashboard />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/inventory"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Inventory />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Settings />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/profile"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<RestaurantProfile />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/create-kitchen"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<CreateYourRestaurant />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/host-kitchen"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<HostKitchen />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/cashier"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Cashier />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/supplier"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Supplier />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/analytics"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Analytics />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/training"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Training />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Router>
|
||||
{/* روابط الشروط والخصوصية */}
|
||||
<Route path="/terms" element={<TermsPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPage />} />
|
||||
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<RestaurantProvider>
|
||||
|
||||
<ProtectedRoutes />
|
||||
|
||||
</RestaurantProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</SnackbarProvider>
|
||||
</UserProvider>
|
||||
</Router>
|
||||
</GoogleOAuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -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
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Terms of Service</a> and
|
||||
<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
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>
|
||||
Terms of Service
|
||||
</a>{' '}
|
||||
and
|
||||
<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
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Terms of Service</a> and
|
||||
<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
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Terms of Service</a> and
|
||||
<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'
|
||||
}
|
||||
}}
|
||||
|
||||
192
src/components/Authentication/Legal/TermsPrivacyComponents.js
Normal file
192
src/components/Authentication/Legal/TermsPrivacyComponents.js
Normal file
@@ -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.
|
||||
*/
|
||||
@@ -7,10 +7,12 @@ 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('');
|
||||
@@ -28,21 +30,66 @@ const LoginForm = () => {
|
||||
const result = await authService.login(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.message || 'Login failed');
|
||||
// 🔹 أولًا: نتحقق إن كانت الرسالة تشير إلى أن الحساب غير موجود
|
||||
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('/dashboard', { replace: true });
|
||||
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"}
|
||||
@@ -116,6 +163,7 @@ const LoginForm = () => {
|
||||
Enter your username and password to access your account securely. Welcome back to our service!
|
||||
</Typography>
|
||||
|
||||
|
||||
{/* إدخال البريد */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
@@ -232,10 +280,10 @@ const LoginForm = () => {
|
||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
|
||||
</Box>
|
||||
|
||||
{/* Google Button */}
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={loginWithGoogle}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
@@ -248,8 +296,8 @@ const LoginForm = () => {
|
||||
'&:hover': {
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
, fontFamily: 'PlusJakartaSans'
|
||||
},
|
||||
fontFamily: 'PlusJakartaSans'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
@@ -260,10 +308,10 @@ const LoginForm = () => {
|
||||
/>
|
||||
</Box>
|
||||
Login with Google
|
||||
</Button>
|
||||
</Button> */}
|
||||
|
||||
{/* Facebook Button */}
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{
|
||||
@@ -290,8 +338,7 @@ const LoginForm = () => {
|
||||
/>
|
||||
</Box>
|
||||
Login with Facebook
|
||||
</Button>
|
||||
|
||||
</Button> */}
|
||||
|
||||
{/* Register Link */}
|
||||
<Typography
|
||||
@@ -300,7 +347,7 @@ const LoginForm = () => {
|
||||
fontSize: '16px',
|
||||
textAlign: 'center',
|
||||
color: '#969BA7',
|
||||
pt: 3
|
||||
pt: 2
|
||||
}}
|
||||
>
|
||||
Don’t have an account?{' '}
|
||||
@@ -338,6 +385,7 @@ const LoginForm = () => {
|
||||
</Typography>
|
||||
|
||||
{/* Terms */}
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
@@ -348,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>
|
||||
@@ -365,3 +424,4 @@ const LoginForm = () => {
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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';
|
||||
@@ -6,6 +6,19 @@ import { VisibilityOutlined } from '@mui/icons-material';
|
||||
import SidePanel from './SidePanel';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import authService from '../../../services/authService';
|
||||
import { UserContext } from '../../../contexts/UserContext';
|
||||
|
||||
|
||||
// import { useGoogleRegister } from '@react-oauth/google';
|
||||
const generateStrongPassword = () => {
|
||||
const length = 12;
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
|
||||
let password = "";
|
||||
for (let i = 0, n = charset.length; i < length; ++i) {
|
||||
password += charset.charAt(Math.floor(Math.random() * n));
|
||||
}
|
||||
return password;
|
||||
};
|
||||
|
||||
const Register = () => {
|
||||
const theme = useTheme();
|
||||
@@ -19,7 +32,11 @@ const Register = () => {
|
||||
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);
|
||||
};
|
||||
@@ -28,7 +45,8 @@ const Register = () => {
|
||||
setShowConfirmPassword((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
|
||||
const handleRegister = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setSuccessMessage('');
|
||||
@@ -52,28 +70,93 @@ const Register = () => {
|
||||
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('/dashboard', { replace: true }); // استبدال الرابط وعدم السماح بالعودة
|
||||
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',
|
||||
},
|
||||
}}>
|
||||
<Box
|
||||
bgcolor={'background.default'}
|
||||
color={'text.primary'}
|
||||
display="flex"
|
||||
sx={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SidePanel />
|
||||
|
||||
<Box sx={{
|
||||
marginLeft: { xs: 0, sm: '50%' },
|
||||
width: { xs: '100%', sm: '50%' },
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
marginLeft: { xs: 0, sm: '50%' },
|
||||
width: { xs: '100%', sm: '50%' },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
flex={1}
|
||||
paddingTop={4}
|
||||
@@ -95,12 +178,13 @@ const Register = () => {
|
||||
xs: '120%',
|
||||
sm: '100%',
|
||||
md: '120%',
|
||||
lg: '120%'
|
||||
lg: '120%',
|
||||
},
|
||||
maxWidth: '600px'
|
||||
maxWidth: '600px',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
{/* logo */}
|
||||
<Box>
|
||||
<img
|
||||
src="/image.png"
|
||||
@@ -109,7 +193,7 @@ const Register = () => {
|
||||
width: '4vw',
|
||||
maxWidth: '80px',
|
||||
height: 'auto',
|
||||
objectFit: 'contain'
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -121,323 +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>
|
||||
|
||||
<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
|
||||
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"
|
||||
// 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
|
||||
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
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
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);
|
||||
@@ -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 AnalyticsPage = () => {
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
mb={3}
|
||||
mb={1.5}
|
||||
>
|
||||
<ButtonGroup
|
||||
size="small"
|
||||
@@ -131,94 +170,73 @@ const AnalyticsPage = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ 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>
|
||||
|
||||
456
src/components/Home/Analytics&Reporting/TablesManager.js
Normal file
456
src/components/Home/Analytics&Reporting/TablesManager.js
Normal file
@@ -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: theme.palette.primary.main,
|
||||
color: '#fff',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFD6B5',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopSellingProduct;
|
||||
@@ -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
src/components/Home/Cart/Cart.js
Normal file
74
src/components/Home/Cart/Cart.js
Normal 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;
|
||||
223
src/components/Home/Cart/contect/CartDetails.js
Normal file
223
src/components/Home/Cart/contect/CartDetails.js
Normal file
@@ -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;
|
||||
170
src/components/Home/Cart/contect/CartView.js
Normal file
170
src/components/Home/Cart/contect/CartView.js
Normal file
@@ -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
src/components/Home/Cart/contect/Orders.js
Normal file
389
src/components/Home/Cart/contect/Orders.js
Normal 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;
|
||||
@@ -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
|
||||
|
||||
136
src/components/Home/CreateNewRestaurant/AppBar.js
Normal file
136
src/components/Home/CreateNewRestaurant/AppBar.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Box,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
const KitchPlusAppBar = ({ onDrawerToggle, sidebarOpen, isMobile }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
const result = await authService.logout();
|
||||
|
||||
if (result.success) {
|
||||
localStorage.removeItem('token');
|
||||
navigate('/login');
|
||||
} else {
|
||||
console.error('Logout failed:', result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
sx={{
|
||||
height: { xs: 56, sm: 64, md: 66 },
|
||||
backgroundColor: '#ffffff',
|
||||
color: 'black',
|
||||
boxShadow: 'none',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: theme.zIndex.appBar,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
sx={{
|
||||
px: { xs: 2, sm: 3, md: '24px' },
|
||||
minHeight: { xs: '56px !important', sm: '64px !important' },
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Left: Logo */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/image.png"
|
||||
alt="logo"
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
objectFit: 'contain',
|
||||
mr: 1.5,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: '1.25rem',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
KITCH
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: '1.25rem',
|
||||
color: 'primary.main',
|
||||
ml: 0.5,
|
||||
}}
|
||||
>
|
||||
PLUS
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Right: Log Out Button */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.8, sm: 1, md: 1.5 } }}>
|
||||
<Box
|
||||
onClick={handleLogout}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
px: 2,
|
||||
py: 1,
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s',
|
||||
backgroundColor: 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor: '#fffcf9d5',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
color: 'divider',
|
||||
minWidth: 36,
|
||||
}}
|
||||
>
|
||||
<LogoutIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Log Out"
|
||||
primaryTypographyProps={{
|
||||
sx: {
|
||||
color: 'divider',
|
||||
fontSize: '1rem',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default KitchPlusAppBar;
|
||||
228
src/components/Home/CreateNewRestaurant/CreateRestaurant.js
Normal file
228
src/components/Home/CreateNewRestaurant/CreateRestaurant.js
Normal file
@@ -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;
|
||||
@@ -3,12 +3,12 @@ import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||
|
||||
const steps = [
|
||||
{ title: 'Cloud Kitchen Hosting', icon: '/images/createProfile/BasicInf.png' },
|
||||
{ title: 'Restaurant Operations', icon: '/images/icons/rocket.png' },
|
||||
{ title: 'Infrastructure & Equipment', icon: '/images/createProfile/equipment.png' },
|
||||
{ title: 'Facilitaion & Cooperation', icon: '/images/createProfile/Facilitaion.png' },
|
||||
{ title: 'Expansion & Future Cooperation', icon: '/images/createProfile/Expansion.png' },
|
||||
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
@@ -22,7 +22,7 @@ const SideProfile = ({ currentStepIndex = 0, onBack }) => {
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 3,
|
||||
py: 4,
|
||||
display: 'block' ,
|
||||
display: { xs: 'none', md: 'block' },
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
@@ -30,14 +30,14 @@ const SideProfile = ({ currentStepIndex = 0, onBack }) => {
|
||||
>
|
||||
{/* العنوان الرئيسي */}
|
||||
<Typography fontSize="18px" fontWeight={600} mb={1.2}>
|
||||
Host Kitchen Flow
|
||||
Create Your Restaurant
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" mb={3}>
|
||||
Complete this process to register your restaurant on our amazing food platform.
|
||||
Complete this process to register your restaurant on our amazing food platform.
|
||||
</Typography>
|
||||
|
||||
{/* الخطوات */}
|
||||
<Stack spacing={5} sx={{ ml: 1, position: 'relative', pb: 15 }}>
|
||||
<Stack spacing={3} sx={{ ml: 1, position: 'relative', pb: 12 }}>
|
||||
{steps.map((step, index) => (
|
||||
<Box key={index} position="relative">
|
||||
{/* الخط الرأسي بين الدوائر */}
|
||||
@@ -45,10 +45,10 @@ const SideProfile = ({ currentStepIndex = 0, onBack }) => {
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: {sm:'50px',md:'40px'},
|
||||
left: '22px',
|
||||
top: '28px',
|
||||
left: '23px',
|
||||
width: '2px',
|
||||
height: {sm:'130%',md:'calc(100% - 0px)'},
|
||||
height: 'calc(100% - 5px)',
|
||||
backgroundColor: '#E0E0E0',
|
||||
zIndex: 0,
|
||||
}}
|
||||
@@ -87,7 +87,6 @@ const SideProfile = ({ currentStepIndex = 0, onBack }) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* الدائرة الأمامية بالأيقونة */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
@@ -106,7 +105,7 @@ const SideProfile = ({ currentStepIndex = 0, onBack }) => {
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginLeft: step.title === 'Infrastructure & Equipment' ? 7 : 0,
|
||||
marginLeft: step.title === 'Required Equipments' ? 7 : 0,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
@@ -134,7 +133,6 @@ const SideProfile = ({ currentStepIndex = 0, onBack }) => {
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
right: 24,
|
||||
mt:10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery } 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';
|
||||
@@ -10,7 +10,9 @@ 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 = () => {
|
||||
@@ -19,30 +21,98 @@ const CreateRestaurant = () => {
|
||||
const [hasProducts, setHasProducts] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
|
||||
// ⬇️ إدارة الخطوة الحالية
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
// إدارة بيانات النموذج لجميع الخطوات (مشاركة البيانات بين الخطوات)
|
||||
const [formData, setFormData] = useState({
|
||||
|
||||
});
|
||||
|
||||
// دالة لتحديث بيانات النموذج بشكل مرن (دمج الجديد مع القديم)
|
||||
const updateFormData = (newData) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
...newData,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
// دالة لإغلاق السناك بار
|
||||
const handleSnackbarClose = (event, reason) => {
|
||||
if (reason === 'clickaway') return;
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await createNewRestaurant(formData);
|
||||
setSnackbarOpen(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const steps = [
|
||||
<CloudKitchenProject onNext={() => setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />,
|
||||
<OperationalDetails onNext={() => setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />,
|
||||
<RequiredEquipments onNext={() => setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />,
|
||||
<VisualIdentity onNext={() => setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />,
|
||||
<Budget onNext={() => setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />,
|
||||
<AdditionalSupport onNext={() => setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />,
|
||||
<AdditionalNotes onBack={() => setCurrentStep(currentStep - 1)} />,
|
||||
<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(() => {
|
||||
// const checkProducts = async () => {
|
||||
// const productsExist = await checkIfProductsExist();
|
||||
// setHasProducts(productsExist);
|
||||
// };
|
||||
// checkProducts();
|
||||
// }, []);
|
||||
|
||||
// const checkIfProductsExist = async () => {
|
||||
// return false;
|
||||
// };
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth >= theme.breakpoints.values.md) {
|
||||
@@ -71,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}
|
||||
@@ -84,82 +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}
|
||||
/>
|
||||
<Box>
|
||||
<Box sx={{
|
||||
display: 'flex', height: '100vh',
|
||||
|
||||
}}>
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
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,
|
||||
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
|
||||
}}>
|
||||
<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;
|
||||
|
||||
@@ -8,7 +8,6 @@ const steps = [
|
||||
{ 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 Support', icon: '/images/createProfile/hand.png' },
|
||||
{ title: 'Additional Notes & Concerns', icon: '/images/createProfile/Group.png' },
|
||||
{ title: 'Submit & Confirmation', icon: '/images/createProfile/Confirmation.png' },
|
||||
];
|
||||
|
||||
@@ -1,151 +1,171 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
TextField
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ConfirmationDialog from './ConfirmationDialog'; // ✅ استدعاء المودال المنفصل
|
||||
import ConfirmationDialog from './ConfirmationDialog';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
const AdditionalNotes = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const AdditionalNotes = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData, onSubmit }) => {
|
||||
const theme = useTheme();
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleOpenModal = () => setOpenModal(true);
|
||||
const handleCloseModal = () => setOpenModal(false);
|
||||
const handleConfirmNext = () => {
|
||||
handleCloseModal();
|
||||
onNext();
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
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>
|
||||
const handleOpenModal = () => setOpenModal(true);
|
||||
const handleCloseModal = () => setOpenModal(false);
|
||||
|
||||
<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>
|
||||
const handleNotesChange = (e) => {
|
||||
updateFormData({ need_help: e.target.value });
|
||||
};
|
||||
|
||||
{/*Notes / Concerns Input */}
|
||||
<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}
|
||||
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>
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
{/* Buttons */}
|
||||
<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>
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
{/* ✅ Confirmation Modal */}
|
||||
<ConfirmationDialog
|
||||
open={openModal}
|
||||
onClose={handleCloseModal}
|
||||
onConfirm={handleConfirmNext}
|
||||
title="Confirm Submission"
|
||||
description="Are you sure you want to proceed to the next step?"
|
||||
/>
|
||||
<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;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Budget = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
const Budget = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -12,6 +11,10 @@ const Budget = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
else navigate('/dashboard');
|
||||
};
|
||||
|
||||
const handleInputChange = (field) => (e) => {
|
||||
updateFormData({ [field]: e.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -44,20 +47,29 @@ const Budget = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
</Box>
|
||||
|
||||
{/* Estimated Budget Input */}
|
||||
<InputField label="Estimated Budget" placeholder="$4200" theme={theme} />
|
||||
<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} />
|
||||
|
||||
|
||||
<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={onNext}
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
@@ -75,7 +87,6 @@ const Budget = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
Next
|
||||
</Button>
|
||||
|
||||
{/* زر Back تحت زر Next */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
@@ -88,7 +99,7 @@ const Budget = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
display: { xs: 'block', sm: 'block', md: 'none' }, // يظهر فقط في xs و sm
|
||||
display: { xs: 'block', sm: 'block', md: 'none' },
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
@@ -100,14 +111,12 @@ const Budget = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// مكون فرعي لتقليل التكرار في الحقول
|
||||
const InputField = ({ label, placeholder, theme }) => (
|
||||
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}
|
||||
@@ -116,6 +125,8 @@ const InputField = ({ label, placeholder, theme }) => (
|
||||
placeholder={placeholder}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
|
||||
@@ -1,12 +1,85 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box, Typography, Stack, Button, useTheme, TextField, Radio,
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
TextField,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
FormHelperText
|
||||
} from '@mui/material';
|
||||
|
||||
const CloudKitchenProject = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
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
|
||||
@@ -47,92 +120,150 @@ const CloudKitchenProject = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{[
|
||||
{ label: 'Restaurant Name', placeholder: 'Al-Baik Foods' },
|
||||
{ label: 'Cuisine Type', placeholder: 'Italian, Chinese, etc.' },
|
||||
].map((field, index) => (
|
||||
<Box key={index} sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
{field.label}
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder={field.placeholder}
|
||||
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)'
|
||||
{/* 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)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
},
|
||||
'& .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>
|
||||
|
||||
<Box sx={{}}>
|
||||
{/* Existing Brand */}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
|
||||
>
|
||||
Existing Brand
|
||||
</Typography>
|
||||
<RadioGroup row name="additionalFacilities">
|
||||
<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>
|
||||
|
||||
<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 Input */}
|
||||
{/* 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,
|
||||
@@ -158,122 +289,130 @@ const CloudKitchenProject = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{[
|
||||
{ label: 'Age Group', placeholder: '15 - 75' },
|
||||
{ label: 'Location', placeholder: 'Street 123, Jordan' },
|
||||
].map((field, index) => (
|
||||
<Box key={index} sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
{field.label}
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder={field.placeholder}
|
||||
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)'
|
||||
{/* 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)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
},
|
||||
'& .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': {
|
||||