diff --git a/package-lock.json b/package-lock.json index 10d4ce7..00ab32b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b94d356..36a6a65 100644 --- a/package.json +++ b/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" diff --git a/public/fonts/Hind Siliguri/HindSiliguri-Bold.ttf b/public/fonts/Hind Siliguri/HindSiliguri-Bold.ttf new file mode 100644 index 0000000..6b68991 Binary files /dev/null and b/public/fonts/Hind Siliguri/HindSiliguri-Bold.ttf differ diff --git a/public/fonts/Hind Siliguri/HindSiliguri-Light.ttf b/public/fonts/Hind Siliguri/HindSiliguri-Light.ttf new file mode 100644 index 0000000..fed3dd6 Binary files /dev/null and b/public/fonts/Hind Siliguri/HindSiliguri-Light.ttf differ diff --git a/public/fonts/Hind Siliguri/HindSiliguri-Medium.ttf b/public/fonts/Hind Siliguri/HindSiliguri-Medium.ttf new file mode 100644 index 0000000..c953178 Binary files /dev/null and b/public/fonts/Hind Siliguri/HindSiliguri-Medium.ttf differ diff --git a/public/fonts/Hind Siliguri/HindSiliguri-Regular.ttf b/public/fonts/Hind Siliguri/HindSiliguri-Regular.ttf new file mode 100644 index 0000000..864e220 Binary files /dev/null and b/public/fonts/Hind Siliguri/HindSiliguri-Regular.ttf differ diff --git a/public/fonts/Hind Siliguri/HindSiliguri-SemiBold.ttf b/public/fonts/Hind Siliguri/HindSiliguri-SemiBold.ttf new file mode 100644 index 0000000..c0440c1 Binary files /dev/null and b/public/fonts/Hind Siliguri/HindSiliguri-SemiBold.ttf differ diff --git a/public/image.png b/public/image.png index 01900c7..92b6e9e 100644 Binary files a/public/image.png and b/public/image.png differ diff --git a/public/images/dash.png b/public/images/dash.png new file mode 100644 index 0000000..384dcbe Binary files /dev/null and b/public/images/dash.png differ diff --git a/public/images/image.png b/public/images/image.png new file mode 100644 index 0000000..4682bc5 Binary files /dev/null and b/public/images/image.png differ diff --git a/src/App.css b/src/App.css index bb7659d..2d8ce00 100644 --- a/src/App.css +++ b/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 === */ diff --git a/src/App.js b/src/App.js index 486d96e..6e8d6d5 100644 --- a/src/App.js +++ b/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 ( - - - {/* الصفحات العامة */} - } /> + + + + {/* ✅ لفّ كل المشروع بـ SnackbarProvider */} + + + {/* التوجيه التلقائي للصفحة الرئيسية */} + } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> - {/* الصفحات المحمية */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - + {/* روابط الشروط والخصوصية */} + } /> + } /> + + + + + + + } + /> + + + + + ); } - export default App; diff --git a/src/components/Authentication/ForgetPassword/Congratulations.js b/src/components/Authentication/ForgetPassword/Congratulations.js index 8f6fec2..7f9797a 100644 --- a/src/components/Authentication/ForgetPassword/Congratulations.js +++ b/src/components/Authentication/ForgetPassword/Congratulations.js @@ -13,7 +13,7 @@ const Congratulations = ({ emailOrPhone, setEmailOrPhone }) => { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - overflow: 'auto', + // overflow: 'auto', scrollbarWidth: 'none', '&::-webkit-scrollbar': { display: 'none', diff --git a/src/components/Authentication/ForgetPassword/EmailInputSection.js b/src/components/Authentication/ForgetPassword/EmailInputSection.js index dd801be..4133607 100644 --- a/src/components/Authentication/ForgetPassword/EmailInputSection.js +++ b/src/components/Authentication/ForgetPassword/EmailInputSection.js @@ -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 ( - <> - - - + const validateEmail = (email) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; - - Forget Password - + const handleSend = async () => { + setInputError(''); // مسح الأخطاء السابقة - - Enter your email to reset it and regain access to your account. - + if (!emailOrPhone) { + setInputError("Please enter your email."); + return; + } - - - Email/Phone - - 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)' - } - }} - /> - + if (!validateEmail(emailOrPhone)) { + setInputError("Please enter a valid email address."); + return; + } - {/* send */} - + setLoading(true); + const response = await authService.resetPassword(emailOrPhone); + setLoading(false); - - By log in, I agree to the  - Terms of Service and  - Privacy Policy - - - ); + 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 ( + + + + + + + Forget Password + + + + Enter your email to reset it and regain access to your account. + + + + + Email + + 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)', + }, + }} + /> + + + + {/* + + By log in, I agree to the  + + Terms of Service + {' '} + and  + + Privacy Policy + + */} + + {/* Terms */} + + By logging in, I agree to the{' '} + window.open('/terms', 'TermsOfService')} + > + Terms of Service + {' '} + and{' '} + window.open('/privacy', 'PrivacyPolicy')} + > + Privacy Policy + . + + + + + {snackbarMessage} + + + + ); }; export default EmailInputSection; diff --git a/src/components/Authentication/ForgetPassword/ForgetFormMain.js b/src/components/Authentication/ForgetPassword/ForgetFormMain.js index 84cbbae..41a469d 100644 --- a/src/components/Authentication/ForgetPassword/ForgetFormMain.js +++ b/src/components/Authentication/ForgetPassword/ForgetFormMain.js @@ -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 ( - - {/* ✅ Sidebar يظهر فقط من sm+ */} + - + - {/* ✅ Form */} { backgroundColor: '#fff', }} > - + - - - {/* إظهار المكون الحالي فقط */} { + setStepCompleted(true); + setCurrentSlide(prev => prev + 1); // هنا تنقل مباشرة للصفحة التالية + }} + onBack={handleBack} /> - - - {/* ✅ مؤشر الشرائح السفلي */} + { 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)} // يمكنك تفعيلها لاحقًا /> ))} @@ -122,4 +114,5 @@ const ForgetForm = () => { ); }; + export default ForgetForm; diff --git a/src/components/Authentication/ForgetPassword/NewPassword.js b/src/components/Authentication/ForgetPassword/NewPassword.js index 2dcea07..595dc0e 100644 --- a/src/components/Authentication/ForgetPassword/NewPassword.js +++ b/src/components/Authentication/ForgetPassword/NewPassword.js @@ -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 ( - <> - - - + if (!password || !passwordConfirm) { + alert("Please fill in both password fields."); + return; + } + if (password !== passwordConfirm) { + alert("Passwords do not match."); + return; + } - - Create a New Password - + setLoading(true); + try { + const response = await authService.updatePassword({ + email: emailOrPhone, + password, + confirmPassword: passwordConfirm + }); + setLoading(false); - - Enter your email to reset it and regai access to your account. - + 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 */} - - - Password - - - - {showPassword ? : } - - - ) - }} - /> - + return ( + <> + + + + + Create a New Password + - {/* Confirm Password Input */} - - - Confirm Password - - - - {showConfirmPassword ? : } - - - ) - }} - /> - + + Enter your new password to regain access to your account. + - {/* send */} - - - By log in, I agree to the  - Terms of Service and  - Privacy Policy - - - ); + + + Password + + 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: ( + + + {showPassword ? : } + + + ) + }} + /> + + + + + Confirm Password + + 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: ( + + + {showConfirmPassword ? : } + + + ) + }} + /> + + + + + {/* Terms */} + + By logging in, I agree to the{' '} + window.open('/terms', 'TermsOfService')} + > + Terms of Service + {' '} + and{' '} + window.open('/privacy', 'PrivacyPolicy')} + > + Privacy Policy + . + + + ); }; -export default EmailInputSection; +export default NewPassword; diff --git a/src/components/Authentication/ForgetPassword/OtpVerification.js b/src/components/Authentication/ForgetPassword/OtpVerification.js index dd36b1c..ea3775b 100644 --- a/src/components/Authentication/ForgetPassword/OtpVerification.js +++ b/src/components/Authentication/ForgetPassword/OtpVerification.js @@ -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 - + Kindly enter the OTP code sent to your registered email/phone for account verification. - - + { 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 = () => { /> ))} - - + - + {/* Terms */} - By log in, I agree to the  - Terms of Service and  - Privacy Policy + By logging in, I agree to the{' '} + window.open('/terms', 'TermsOfService')} + > + Terms of Service + {' '} + and{' '} + window.open('/privacy', 'PrivacyPolicy')} + > + Privacy Policy + . + + + + {snackbarMessage} + + + ); }; diff --git a/src/components/Authentication/ForgetPassword/Side.js b/src/components/Authentication/ForgetPassword/Side.js index e9c8712..addfd4d 100644 --- a/src/components/Authentication/ForgetPassword/Side.js +++ b/src/components/Authentication/ForgetPassword/Side.js @@ -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 ( { Instructions for secure password modification. Follow simple steps for password change. - + {steps.map((step, index) => ( {index !== steps.length - 1 && ( @@ -109,7 +109,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => { - + {step.title} @@ -121,13 +121,11 @@ const Side = ({ currentStepIndex, onNext, onBack }) => { ))} - {/* ... (بقية محتوى الـ Side كما هو بدون تغيير) ... */} - {/* أزرار التنقل */} { 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' } }} diff --git a/src/components/Authentication/Legal/TermsPrivacyComponents.js b/src/components/Authentication/Legal/TermsPrivacyComponents.js new file mode 100644 index 0000000..3e6347d --- /dev/null +++ b/src/components/Authentication/Legal/TermsPrivacyComponents.js @@ -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 ( + + + + Terms of Service + + + + Welcome to our Terms of Service. Please read carefully. Lorem ipsum dolor sit amet, + consectetur adipiscing elit. Integer posuere erat a ante. + + + + + + + + ); +} + +export function PrivacyPage() { + React.useEffect(() => { + document.title = "Resturant-Privacy Policy"; + }, []); + + return ( + + + + Privacy Policy + + + + 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. + + + + + + + + ); +} + + +/*********************** + * 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 ( + + + {/* Login title */} + Sign in + + + {/* Your existing Terms text (you provided earlier) */} + + By logging in, I agree to the{' '} + openInNewTab('/terms', 'TermsOfService')}> + Terms of Service + {' '} + and{' '} + openInNewTab('/privacy', 'PrivacyPolicy')}> + Privacy Policy + + . + + + {/* The rest of your login UI goes here (inputs, submit, etc.) */} + + + ); +} + +/* + Usage notes: + - If you're using a router (react-router), create routes for /terms and /privacy + that render and respectively. + + Example (react-router v6): + + import { BrowserRouter, Routes, Route } from 'react-router-dom'; + import LoginCard, { TermsPage, PrivacyPage } from './TermsPrivacyComponents'; + + function App() { + return ( + + + } /> + } /> + } /> + + + ); + } + + export default App; + + Security reminder: validate event.origin in the message handler in production. +*/ diff --git a/src/components/Authentication/SignUp_In/LoginForm.js b/src/components/Authentication/SignUp_In/LoginForm.js index b42b508..ca9faf7 100644 --- a/src/components/Authentication/SignUp_In/LoginForm.js +++ b/src/components/Authentication/SignUp_In/LoginForm.js @@ -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 ( { Enter your username and password to access your account securely. Welcome back to our service! + {/* إدخال البريد */} @@ -232,10 +280,10 @@ const LoginForm = () => { - {/* Google Button */} - + */} {/* Facebook Button */} - - + */} {/* Register Link */} { fontSize: '16px', textAlign: 'center', color: '#969BA7', - pt: 3 + pt: 2 }} > Don’t have an account?{' '} @@ -338,6 +385,7 @@ const LoginForm = () => { {/* Terms */} + { }} > By logging in, I agree to the{' '} - + window.open('/terms', 'TermsOfService')} + > Terms of Service - {' '} + {' '} and{' '} - + window.open('/privacy', 'PrivacyPolicy')} + > Privacy Policy - . + . + + + @@ -365,3 +424,4 @@ const LoginForm = () => { }; export default LoginForm; + diff --git a/src/components/Authentication/SignUp_In/RegisterForm.js b/src/components/Authentication/SignUp_In/RegisterForm.js index ed391cf..cca293c 100644 --- a/src/components/Authentication/SignUp_In/RegisterForm.js +++ b/src/components/Authentication/SignUp_In/RegisterForm.js @@ -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 ( - + - + { xs: '120%', sm: '100%', md: '120%', - lg: '120%' + lg: '120%', }, - maxWidth: '600px' + maxWidth: '600px', }} > + {/* logo */} { width: '4vw', maxWidth: '80px', height: 'auto', - objectFit: 'contain' + objectFit: 'contain', }} /> @@ -121,323 +205,291 @@ const Register = () => { fontSize: { xs: '1.8rem', sm: '2rem', - md: '2.2rem' - } + md: '2.2rem', + }, }} > Register - - Please fill out the registration form with accurate information to - create your account successfully. + + Please fill out the registration form with accurate information to create your account successfully. - - - Email - - + + Email + + 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)', + }, + }} + /> + + + {/* حقل كلمة المرور */} + + + Password + + 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: ( + + + {showPassword ? : } + + + ), + }} + /> + + + {/* حقل التاكيد لكلمة المرور */} + + + Confirm Password + + 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: ( + + + {showConfirmPassword ? : } + + + ), + }} + /> + + + {/* Error & Success Message */} + {error && ( + + {error} + + )} + {successMessage && ( + + {successMessage} + + )} + + {/* زر التسجيل */} + + + + + + {/* Divider */} + + + + Or + + + + + {/* زر التسجيل عبر جوجل */} + {/* - - - {/* Divider */} - - - 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 + + Google + + Register with Google + */} + + {/* زر فيسبوك - فقط للعرض (لم تُضاف وظيفة) */} + {/* */} + + + Already have an account?{' '} + + Login + - - - - {/* Google */} - - - {/* Facebook */} - - Already have an account?{' '} - - Login - + By logging in, I agree to the{' '} + window.open('/terms', 'TermsOfService')} + > + Terms of Service + {' '} + and{' '} + window.open('/privacy', 'PrivacyPolicy')} + > + Privacy Policy + . + - - By log in, I agree to the and{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - . - diff --git a/src/components/Authentication/SignUp_In/SidePanel.js b/src/components/Authentication/SignUp_In/SidePanel.js index f847db0..7735f8e 100644 --- a/src/components/Authentication/SignUp_In/SidePanel.js +++ b/src/components/Authentication/SignUp_In/SidePanel.js @@ -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 }) => { { 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 ( - - @@ -94,15 +92,9 @@ const AnalyticsPage = () => { duration: theme.transitions.duration.leavingScreen, }), }}> - {isLoading ? ( - <> - - - - - ) : ( + - )} + diff --git a/src/components/Home/Analytics&Reporting/AnalyticsContect.js b/src/components/Home/Analytics&Reporting/AnalyticsContect.js index 8065230..acd06c1 100644 --- a/src/components/Home/Analytics&Reporting/AnalyticsContect.js +++ b/src/components/Home/Analytics&Reporting/AnalyticsContect.js @@ -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 ( - + + {/* Tables Manager */} + + {restaurantId && } + + {/* Header Buttons */} { justifyContent="space-between" alignItems="center" gap={2} - mb={3} + mb={1.5} > { } }} > - {[ - { 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 }) => ( ))} - - - - - - - - {/* Data Sections */} - - - - - - - - - - {/* Chart Section */} - - {isLoading ? ( - <> - - - - - ) : ( - - )} + + + + + {/* StatisticsCard */} + + value} + timeFrame={timeFrame} + onTimeFrameChange={handleTimeFrameChange} + /> ); diff --git a/src/components/Home/Analytics&Reporting/SalesByLocation.js b/src/components/Home/Analytics&Reporting/SalesByLocation.js index 94b19b3..1959365 100644 --- a/src/components/Home/Analytics&Reporting/SalesByLocation.js +++ b/src/components/Home/Analytics&Reporting/SalesByLocation.js @@ -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' }}> { { - 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 ( - + - + {/* */} + + + + + + + + + {/* Header */} {/* Chart */} - + + ))} diff --git a/src/components/Home/Analytics&Reporting/TablesManager.js b/src/components/Home/Analytics&Reporting/TablesManager.js new file mode 100644 index 0000000..5c5f5f8 --- /dev/null +++ b/src/components/Home/Analytics&Reporting/TablesManager.js @@ -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 ( + + + + + + + {currentPage} + + + = pageCount} + sx={{ + borderRadius: "8px", + backgroundColor: "#FFECE0", + // "&:hover": { backgroundColor: "#FFD6B5" }, + color: theme.palette.primary.main, + // "&.Mui-disabled": { color: "#ccc", backgroundColor: "#FFF5E6" }, + }} + > + + + + ); +}; + +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 ( + + {/* Header */} + + + Tables + + + + {/* فلتر */} + + + + + + + {/* Menu الفلاتر */} + + {["all", "available", "unavailable"].map((status) => ( + handleFilterSelect(status)} + sx={{ + color: statusFilter === status ? "#FF914D" : "#4F5867", + fontWeight: statusFilter === status ? 700 : 500, + }} + > + {status.charAt(0).toUpperCase() + status.slice(1)} + + ))} + + + {/* جدول */} + + + + + Table Number + Capacity + Status + Actions + + + + {loading + ? Array.from({ length: itemsPerPage }).map((_, idx) => ( + + + + + + + )) + : ( + <> + {addingRow && ( + + + setNewRow({ ...newRow, table_number: e.target.value })} + /> + + + setNewRow({ ...newRow, capacity: e.target.value })} + /> + + + + + + + + + + + )} + + + {paginatedRows.map((row) => ( + + {row.table_number} + {row.capacity} + + + + + handleDeleteRow(row)}> + + + + + ))} + + )} + +
+
+ + {/* Pagination Footer */} + + + Showing {(currentPage - 1) * itemsPerPage + 1} -{" "} + {Math.min(currentPage * itemsPerPage, filteredRows.length)} of {filteredRows.length} + + + + + {/* Dialog الحذف */} + setConfirmDeleteOpen(false)}> + Confirm Delete + + + Are you sure you want to delete table "{rowToDelete?.table_number}"? + + + + + + + +
+ ); +}; + +export default TablesManager; diff --git a/src/components/Home/Analytics&Reporting/TopSellingProduct.js b/src/components/Home/Analytics&Reporting/TopSellingProduct.js deleted file mode 100644 index 83fce6e..0000000 --- a/src/components/Home/Analytics&Reporting/TopSellingProduct.js +++ /dev/null @@ -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 ( - - {/* العنوان وزر الفلاتر */} - - - Top Selling Product - - - - - {/* جدول البيانات */} - - - - - Product - {!isMobile && Sales} - Amount - {!isMobile && Price} - Status - - - - {paginatedData.map((row, i) => ( - - - - - - - - {row.product} - - - - {!isMobile && {row.sales}} - ${row.amount.toLocaleString()} - {!isMobile && ${row.price}} - - - - - ))} - -
-
- - {/* التذييل مع الترقيم */} - - - Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, data.length)} of {data.length} - - - 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', - }, - }, - }} - /> - -
- ); -}; - -export default TopSellingProduct; diff --git a/src/components/Home/AppBar.js b/src/components/Home/AppBar.js index 4dc6987..ea4fc38 100644 --- a/src/components/Home/AppBar.js +++ b/src/components/Home/AppBar.js @@ -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 ( { justifyContent: 'space-between', }} > - {/* Left side with toggle button */} - - {/* Toggle button for mobile/tablet */} + {/* Left side */} + {(isMobile || isMediumScreen) && ( - + )} - {location.pathname === '/dashboard' ? ( - - Welcome to KitchPlus - - ) : ( - option.title)} - renderInput={(params) => ( - - - - ), - }} - sx={{ - '& .MuiInputBase-root': { - backgroundColor: 'white', - height: { xs: 36, sm: 38, md: 40 }, - }, - }} - /> - )} - /> - )} + + Welcome to KitchPlus + {/* Right side */} - {location.pathname === '/dashboard' && !isSmallScreen && ( - - )} - {location.pathname === '/dashboard' && ( - - )} - - + {location.pathname === '/dashboard' && } + + navigate('/restaurant')} + > + - + {!isSmallScreen && ( - Admin@gmail.com + {user?.email || 'Admin@gmail.com'} )} - - - - ); }; -export default KitchPlusAppBar; \ No newline at end of file +export default KitchPlusAppBar; diff --git a/src/components/Home/Cart/Cart.js b/src/components/Home/Cart/Cart.js new file mode 100644 index 0000000..8a39298 --- /dev/null +++ b/src/components/Home/Cart/Cart.js @@ -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 ( + + + + + + + + + + + + ); +}; + +export default Cart; diff --git a/src/components/Home/Cart/contect/CartDetails.js b/src/components/Home/Cart/contect/CartDetails.js new file mode 100644 index 0000000..04c6395 --- /dev/null +++ b/src/components/Home/Cart/contect/CartDetails.js @@ -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 ( + <> + + + Cart #{cart.id} Details + + + {editMode ? ( + <> + setTotalPrice(e.target.value)} + fullWidth + sx={{ mb: 2 }} + /> + + Items: + + {cartItems.map(item => ( + + handleChangeQuantity(item.id, Number(e.target.value))} + sx={{ width: '120px' }} + /> + } + /> + + ))} + + + + + + + + ) : ( + <> + Total Price: {cart.attributes.total_price || cart.attributes.totalPrice} + + Created At: {new Date(cart.attributes.createdAt).toLocaleString()} + + Items: + + {cartItems.map(item => ( + + + + ))} + + + + + + + + + )} + + + {/* Delete Confirmation Dialog */} + setOpenDeleteDialog(false)} + > + Confirm Delete + + + Are you sure you want to delete this cart? This action cannot be undone. + + + + + + + + + ); +}; + +export default CartDetails; \ No newline at end of file diff --git a/src/components/Home/Cart/contect/CartView.js b/src/components/Home/Cart/contect/CartView.js new file mode 100644 index 0000000..de7a7f8 --- /dev/null +++ b/src/components/Home/Cart/contect/CartView.js @@ -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 ; + + return ( + + {/* السهم للعودة */} + + + + + + Current Cart + + + + {cart.length === 0 ? ( + No items in cart. + ) : ( + <> + {cart.map(item => ( + + + {item.name} + Unit: {item.unit} + + + Quantity: {item.quantity} + Total: ${item.totalPrice} + + + ))} + + + + Total Price: ${totalPrice.toFixed(2)} + + + + )} + + {cart.length > 0 && ( + + + + + )} + + ); +}; + +export default CartView; diff --git a/src/components/Home/Cart/contect/Orders.js b/src/components/Home/Cart/contect/Orders.js new file mode 100644 index 0000000..e705972 --- /dev/null +++ b/src/components/Home/Cart/contect/Orders.js @@ -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 ( + + + + + + + {currentPage} + + + = pageCount} + sx={{ + borderRadius: '8px', + backgroundColor: '#FFECE0', + '&:hover': { backgroundColor: '#FFD6B5' }, + color: theme.palette.primary.main, + '&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' }, + }} + > + + + + ); +}; + +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 ( + + + + ); + } + + if (selectedCart) { + return ( + + ); + } + + if (showCartView) { + return ( + setShowCartView(false)} + onSend={handleAddOrder} + onCartCreated={(newCart) => { + setOrdersData(prev => [newCart, ...prev]); + setShowCartView(false); + }} + adminId={adminId} + /> + ); + } + + // ────────────── Main Orders Table ────────────── + return ( + + + + Cart + + + + + + + + + + Cart ID + Total Price + Created At + Items Count + Action + + + + {loading + ? Array.from({ length: itemsPerPage }).map((_, idx) => ( + + + + + + + + )) + : ( + <> + {paginatedOrders.map(order => { + const attributes = order?.attributes || {}; + const relationships = order?.relationships || {}; + const cartItems = relationships?.cartItems || relationships?.cart_items || []; + return ( + handleRowClick(order.id)} + > + {order?.id || '--'} + {attributes?.totalPrice ?? attributes?.total_price ?? '0.00'} + {attributes?.createdAt ? new Date(attributes.createdAt).toLocaleString() : '--'} + {cartItems.length} + + + + + ); + })} + + {paginatedOrders.length < itemsPerPage && + Array.from({ length: itemsPerPage - paginatedOrders.length }).map((_, idx) => ( + + + + )) + } + + )} + +
+
+ + + + Showing {(currentPage - 1) * itemsPerPage + 1} - {Math.min(currentPage * itemsPerPage, ordersData.length)} of {ordersData.length} + + + + +
+ ); +}; + +export default Orders; diff --git a/src/components/Home/Cashier/Cashier.js b/src/components/Home/Cashier/Cashier.js index ab11add..c294836 100644 --- a/src/components/Home/Cashier/Cashier.js +++ b/src/components/Home/Cashier/Cashier.js @@ -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 = () => { { + 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 ( + + + {/* Left: Logo */} + + + + KITCH + + + PLUS + + + + {/* Right: Log Out Button */} + + + + + + + + + + + ); +}; + +export default KitchPlusAppBar; diff --git a/src/components/Home/CreateNewRestaurant/CreateRestaurant.js b/src/components/Home/CreateNewRestaurant/CreateRestaurant.js new file mode 100644 index 0000000..d6ec97c --- /dev/null +++ b/src/components/Home/CreateNewRestaurant/CreateRestaurant.js @@ -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 = [ + setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))} + onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))} + />, + setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))} + onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))} + />, + setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))} + onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))} + />, + setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))} + onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))} + />, + setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))} + onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))} + />, + 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 ( + + + + + + + + + {/* Sidebar profile for desktop */} + + + setCurrentStep(prev => Math.max(prev - 1, 0))} + /> + + + + {/* Step content */} + + + {steps[currentStep]} + + + + + + + + + + {snackbar.message} + + + + ); +}; + +export default CreateRestaurant; diff --git a/src/components/Home/HostKitchen/SideProfile.js b/src/components/Home/CreateNewRestaurant/SideProfile.js similarity index 81% rename from src/components/Home/HostKitchen/SideProfile.js rename to src/components/Home/CreateNewRestaurant/SideProfile.js index a146e38..5ef8e16 100644 --- a/src/components/Home/HostKitchen/SideProfile.js +++ b/src/components/Home/CreateNewRestaurant/SideProfile.js @@ -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 }) => { > {/* العنوان الرئيسي */} - Host Kitchen Flow + Create Your Restaurant - Complete this process to register your restaurant on our amazing food platform. + Complete this process to register your restaurant on our amazing food platform. {/* الخطوات */} - + {steps.map((step, index) => ( {/* الخط الرأسي بين الدوائر */} @@ -45,10 +45,10 @@ 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 }} > + return ( + + + + Additional Notes + - - - - - {/* ✅ Confirmation Modal */} - + + + Enter your basic information to proceed to registration of your own restaurant on this platform + - ); + + + + Notes / Concerns + + + + + + + + + + + + + + ); }; export default AdditionalNotes; diff --git a/src/components/Home/CreateYourRestaurant/contcet/Budget.js b/src/components/Home/CreateYourRestaurant/contcet/Budget.js index 773af14..a2ea84c 100644 --- a/src/components/Home/CreateYourRestaurant/contcet/Budget.js +++ b/src/components/Home/CreateYourRestaurant/contcet/Budget.js @@ -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 ( { {/* Estimated Budget Input */} - + {/* Expansion Plans Through Cloud Kitchen (No of Branches) Input */} - - - + {/* Next Button */} - - {/* زر Back تحت زر Next */} -
); }; -// مكون فرعي لتقليل التكرار في الحقول -const InputField = ({ label, placeholder, theme }) => ( +const InputField = ({ label, placeholder, theme, value, onChange }) => ( {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' }, diff --git a/src/components/Home/CreateYourRestaurant/contcet/CloudKitchenProject.js b/src/components/Home/CreateYourRestaurant/contcet/CloudKitchenProject.js index 7e17b58..939dacb 100644 --- a/src/components/Home/CreateYourRestaurant/contcet/CloudKitchenProject.js +++ b/src/components/Home/CreateYourRestaurant/contcet/CloudKitchenProject.js @@ -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 ( { - {[ - { label: 'Restaurant Name', placeholder: 'Al-Baik Foods' }, - { label: 'Cuisine Type', placeholder: 'Italian, Chinese, etc.' }, - ].map((field, index) => ( - - - {field.label} - - + + Restaurant Name + + - - ))} + }, + '& .MuiOutlinedInput-root.Mui-focused': { + borderColor: '#3f51b5', + boxShadow: '0 0 0 2px rgba(63,81,181,0.1)' + } + }} + /> +
+ {/* Cuisine Type */} + + + Cuisine Type + + + {cuisineTypes.length > 0 ? ( + cuisineTypes.map((type) => ( + + {type.name} + + )) + ) : ( + No Cuisine Types Found + )} + + - + {/* Existing Brand */} + Existing Brand - - - } - label="Yes" - /> - - } - label="No" - /> - - + + + + } + label="Yes" + /> + + } + label="No" + /> + + {errors.is_existing_brand && ( + {errors.is_existing_brand} + )} + - {/* Further Brand Details Input */} + {/* Further Brand Details */} Further Brand Details { /> - {[ - { label: 'Age Group', placeholder: '15 - 75' }, - { label: 'Location', placeholder: 'Street 123, Jordan' }, - ].map((field, index) => ( - - - {field.label} - - + + Age Group + + - - ))} + }, + '& .MuiOutlinedInput-root.Mui-focused': { + borderColor: '#3f51b5', + boxShadow: '0 0 0 2px rgba(63,81,181,0.1)' + } + }} + /> + + {/* Location */} + + + Location + + + - + {/* Menu Status */} + Menu Status - - - } - label="Yes" - /> - - } - label="No" - /> - - - - - {/* Further Brand Details Input */} - - - Need Help - - + + + + } + label="Yes" + /> + + } + label="No" + /> + + {errors.menu_status && ( + {errors.menu_status} + )} + + {/* زر Next */} - - {/* زر Back تحت زر Next */} - - ); diff --git a/src/components/Home/CreateYourRestaurant/contcet/ConfirmationDialog.js b/src/components/Home/CreateYourRestaurant/contcet/ConfirmationDialog.js index 6cf94a1..296eae3 100644 --- a/src/components/Home/CreateYourRestaurant/contcet/ConfirmationDialog.js +++ b/src/components/Home/CreateYourRestaurant/contcet/ConfirmationDialog.js @@ -9,10 +9,10 @@ import { Box, Typography, useTheme, - Divider + CircularProgress } from '@mui/material'; -import EditIcon from '@mui/icons-material/Edit'; -const ConfirmationDialog = ({ open, onClose, onConfirm }) => { +const ConfirmationDialog = ({ open, onClose, onConfirm, loading }) => { + const theme = useTheme(); return ( { PaperProps={{ sx: { width: '525px', - height: '300px', + height: '350px', maxWidth: '100%', maxHeight: '100%', borderRadius: '12px', @@ -69,7 +69,7 @@ const ConfirmationDialog = ({ open, onClose, onConfirm }) => { zIndex: 0, }} /> - {/* الدائرة الأمامية مع الأيقونة */} + {
- {/* العنوان بجانب الدائرة */} { fontWeight: 500, color: theme.palette.text.secondary }}> - Congratulations! you have registered your restaurant successfully on our platform + Congratulations! you well register your restaurant successfully on our platform @@ -128,7 +127,7 @@ const ConfirmationDialog = ({ open, onClose, onConfirm }) => { gap: 2, }} > - {/* الزر الأول - Filled */} + + {/* الزر الأول - Filled */} +
diff --git a/src/components/Home/CreateYourRestaurant/contcet/OperationalDetails.js b/src/components/Home/CreateYourRestaurant/contcet/OperationalDetails.js index d3c6f64..ab59167 100644 --- a/src/components/Home/CreateYourRestaurant/contcet/OperationalDetails.js +++ b/src/components/Home/CreateYourRestaurant/contcet/OperationalDetails.js @@ -1,20 +1,71 @@ -import React from 'react'; -import { Box, Typography, Stack, Button, useTheme, TextField, MenuItem } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { + Box, + Typography, + Stack, + Button, + useTheme, + TextField, + MenuItem, +} from '@mui/material'; +import authService from '../../../../services/authService'; // ← عدّل المسار حسب مكانك -const OperationalDetails = ({ currentStepIndex = 0, onNext, onBack }) => { +const OperationalDetails = ({ + currentStepIndex = 0, + onNext, + onBack, + formData, + updateFormData, +}) => { const theme = useTheme(); - const countries = [ - { code: 'US', name: 'United States' }, - { code: 'GB', name: 'United Kingdom' }, - { code: 'FR', name: 'France' }, - { code: 'DE', name: 'Germany' }, - { code: 'SA', name: 'Saudi Arabia' }, - { code: 'EG', name: 'Egypt' }, - { code: 'AE', name: 'United Arab Emirates' }, - // أضف المزيد حسب الحاجة - ]; + const [countries, setCountries] = useState([]); + const [loadingCountries, setLoadingCountries] = useState(false); + const [errors, setErrors] = useState({}); // <-- حالة الأخطاء + + useEffect(() => { + const fetchCountries = async () => { + setLoadingCountries(true); + const result = await authService.getCountries(); + if (result.success) { + setCountries(result.data); + } else { + console.error(result.message); + } + setLoadingCountries(false); + }; + + fetchCountries(); + }, []); + + const handleChange = (e) => { + const { name, value } = e.target; + updateFormData({ [name]: value }); + }; + + // التحقق من صحة الحقول + const validate = () => { + let tempErrors = {}; + + // التحقق من أن الحقل غير فارغ + if (!formData.staff_members || formData.staff_members.trim() === '') { + tempErrors.staff_members = 'Staff Members is required'; + } + // التحقق من أن القيمة رقمية + else if (isNaN(formData.staff_members)) { + tempErrors.staff_members = 'Staff Members must be a number'; + } + + if (!formData.country_id || formData.country_id === '') { + tempErrors.country_id = 'Country selection is required'; + } + + setErrors(tempErrors); + return Object.keys(tempErrors).length === 0; +}; + + const handleNext = () => { - if (onNext) { + if (validate()) { onNext(); } }; @@ -41,12 +92,13 @@ const OperationalDetails = ({ currentStepIndex = 0, onNext, onBack }) => { fontSize: { xs: '1.8rem', sm: '2rem', - md: '2.2rem' - } + md: '2.2rem', + }, }} > Operational Details + { fontWeight={500} sx={{ pb: 1 }} > - Enter your basic information to proceed to registration of your own restaurant on this platform + Enter your basic information to proceed to registration of your + own restaurant on this platform - {/* Staff MembersInput */} + {/* Staff Members Input */} - + Staff Members { transition: '0.3s', '&.Mui-focused fieldset': { borderColor: theme.palette.primary.main, - boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' - } + boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)', + }, }, '& .MuiOutlinedInput-root.Mui-focused': { borderColor: '#3f51b5', - boxShadow: '0 0 0 2px rgba(63,81,181,0.1)' - } + boxShadow: '0 0 0 2px rgba(63,81,181,0.1)', + }, }} /> - {/* Country Selector */} - + Country { transition: '0.3s', '&.Mui-focused fieldset': { borderColor: theme.palette.primary.main, - boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' - } + boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)', + }, }, '& .MuiOutlinedInput-root.Mui-focused': { borderColor: '#3f51b5', - boxShadow: '0 0 0 2px rgba(63,81,181,0.1)' - } + boxShadow: '0 0 0 2px rgba(63,81,181,0.1)', + }, }} - > {countries.map((country) => ( - + {country.name} ))} - - {/* Expansion Plan Cities MembersInput */} + {/* Expansion Plan Cities Input */} - - Expansion Plan Cities + + Expansion Plan Cities { transition: '0.3s', '&.Mui-focused fieldset': { borderColor: theme.palette.primary.main, - boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' - } + boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)', + }, }, '& .MuiOutlinedInput-root.Mui-focused': { borderColor: '#3f51b5', - boxShadow: '0 0 0 2px rgba(63,81,181,0.1)' - } + boxShadow: '0 0 0 2px rgba(63,81,181,0.1)', + }, }} /> - - {/* Next Button */} +
+ + {/* Next Button */} - {/* زر Back تحت زر Next */} + {/* Back Button */} -
); diff --git a/src/components/Home/CreateYourRestaurant/contcet/RequiredEquipments.js b/src/components/Home/CreateYourRestaurant/contcet/RequiredEquipments.js index f43f732..12d4028 100644 --- a/src/components/Home/CreateYourRestaurant/contcet/RequiredEquipments.js +++ b/src/components/Home/CreateYourRestaurant/contcet/RequiredEquipments.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, Typography, @@ -11,10 +11,56 @@ import { Radio } from '@mui/material'; -const RequiredEquipments = ({ currentStepIndex = 0, onNext, onBack }) => { +const RequiredEquipments = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => { const theme = useTheme(); + const [errors, setErrors] = useState({}); - const [selectedDays, setSelectedDays] = useState([]); + // تعيين القيمة الافتراضية لـ needSpecializedEquipment إذا غير موجودة + useEffect(() => { + if (!formData.needSpecializedEquipment) { + updateFormData({ needSpecializedEquipment: 'no' }); + } + }, [formData.needSpecializedEquipment, updateFormData]); + + // التعامل مع تغير القيمة النصية للمعدات العامة + const handleEquipmentChange = (e) => { + updateFormData({ equipment: e.target.value }); + }; + + + const handleNeedSpecializedChange = (e) => { + updateFormData({ needSpecializedEquipment: e.target.value }); + }; + + // التعامل مع القيمة النصية للمعدات المتخصصة + const handleSpecializedEquipmentChange = (e) => { + updateFormData({ specialized_equipment: e.target.value }); + }; + + // دالة التحقق من صحة الحقول + const validate = () => { + let tempErrors = {}; + + // if (!formData.equipment || formData.equipment.trim() === '') { + // tempErrors.equipment = 'Equipment is required'; + // } + // if (!formData.needSpecializedEquipment || (formData.needSpecializedEquipment !== 'yes' && formData.needSpecializedEquipment !== 'no')) { + // tempErrors.needSpecializedEquipment = 'Please select Yes or No'; + // } + if (formData.needSpecializedEquipment === 'yes' && (!formData.specialized_equipment || formData.specialized_equipment.trim() === '')) { + tempErrors.specialized_equipment = 'Please specify specialized equipments'; + } + + setErrors(tempErrors); + return Object.keys(tempErrors).length === 0; + }; + + // دالة معالجة الضغط على زر Next مع تحقق الفالديشن + const handleNext = () => { + if (validate()) { + onNext(); + } + }; return ( { - {/* Operational Hours Input */} + {/* Equipment Input */} Equipment { /> - + {/* Need Specialized Equipment */} + Need Specialized Equipment - + { label="No" /> + {errors.needSpecializedEquipment && ( + + {errors.needSpecializedEquipment} + + )} - - - {/* Specialized Equipments For Food Preparation Input */} - - - Specialized Equipments For Food Preparation - - + + Specialized Equipments For Food Preparation + + - - - {/* Next Button */} + '& 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)', + } + }} + /> + + )} + {/* Buttons */} - {/* زر Back تحت زر Next */} -
); diff --git a/src/components/Home/CreateYourRestaurant/contcet/VisualIdentity.js b/src/components/Home/CreateYourRestaurant/contcet/VisualIdentity.js index 872894a..ef55b04 100644 --- a/src/components/Home/CreateYourRestaurant/contcet/VisualIdentity.js +++ b/src/components/Home/CreateYourRestaurant/contcet/VisualIdentity.js @@ -1,19 +1,61 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, Typography, Stack, Button, useTheme, - TextField, RadioGroup, FormControlLabel, - Radio + Radio, + Typography as MuiTypography } from '@mui/material'; -import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; -const VisualIdentity = ({ currentStepIndex = 0, onNext, onBack }) => { +const VisualIdentity = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => { const theme = useTheme(); + const [errors, setErrors] = useState({}); + + // تأكد من وجود قيمة افتراضية + useEffect(() => { + if (!formData.visualIdentity) { + updateFormData({ visualIdentity: 'no' }); + } + }, [formData.visualIdentity, updateFormData]); + + const handleVisualIdentityChange = (e) => { + updateFormData({ visualIdentity: e.target.value }); + }; + + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (file) { + updateFormData({ logo: file }); + } + + }; + + const validate = () => { + let tempErrors = {}; + + if (!formData.visualIdentity || (formData.visualIdentity !== 'yes' && formData.visualIdentity !== 'no')) { + tempErrors.visualIdentity = 'Please select Yes or No'; + } + + /* + if (!formData.logo) { + tempErrors.logo = 'Please upload a logo or color scheme image'; + } + */ + + setErrors(tempErrors); + return Object.keys(tempErrors).length === 0; + }; + + const handleNext = () => { + if (validate()) { + onNext(); + } + }; return ( { - - + Visual Identity - + { label="No" /> + {errors.visualIdentity && ( + + {errors.visualIdentity} + + )} - {/* OR Separator */} + {/* Upload Logo / Color Scheme */} - Upload Logo / Color Scheme If Any - {/* Upload Menu Picture */} { } }} > - Upload Picture - + {formData.logo ? (formData.logo.name || 'Uploaded Image') : 'Upload Picture'} + + {errors.logo && ( + + {errors.logo} + + )} - - {/* Buttons: Back and Next */} - + {/* Buttons */} - {/* زر Back تحت زر Next */} - ); diff --git a/src/components/Home/Dashboard/AnalyticsPage.js b/src/components/Home/Dashboard/AnalyticsPage.js new file mode 100644 index 0000000..18e8bd0 --- /dev/null +++ b/src/components/Home/Dashboard/AnalyticsPage.js @@ -0,0 +1,244 @@ +import React, { useState, useEffect } from 'react'; +import { useRestaurant } from '../../../contexts/RestaurantContext'; +import StatisticsCard from './StatisticsCard'; +// import TablesManager from './TablesManager'; +import { + Box, + useTheme, + useMediaQuery, + Button, + ButtonGroup, + TextField +} from '@mui/material'; +import authService from '../../../services/authService'; +import dayjs from 'dayjs'; + +const AnalyticsPage = () => { + const { restaurantId } = useRestaurant(); + const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h' + const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD')); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [chartData, setChartData] = useState([]); + + const handleTimeFrameChange = (newTimeFrame) => { + setTimeFrame(newTimeFrame); + }; + + const handleCustomDateChange = (event) => { + setCustomDate(event.target.value); + }; + + const fetchStatistics = async () => { + if (!restaurantId) return; + + let period = 'yearly'; + let labels = []; + let mappedData = []; + + switch (timeFrame) { + case '24h': // يومي + period = 'daily'; + labels = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`); + try { + const response = await authService.getOrderStatistics(restaurantId, period, customDate); + if (response.success && response.data.length > 0) { + mappedData = response.data.map(item => ({ + label: item.label || item.date || '', + orders_total: item.orders_total || 0, + items_total: item.items_total || 0 + })); + } else { + mappedData = labels.map(label => ({ + label, + orders_total: 0, + items_total: 0 + })); + } + } catch (error) { + console.error(error); + mappedData = labels.map(label => ({ + label, + orders_total: 0, + items_total: 0 + })); + } + break; + + case '30d': // شهري + period = 'monthly'; + labels = Array.from({ length: 30 }, (_, i) => dayjs().startOf('month').add(i, 'day').format('DD')); + try { + const response = await authService.getOrderStatistics(restaurantId, period, customDate); + if (response.success && response.data.length > 0) { + mappedData = response.data.map(item => ({ + label: item.label ? dayjs(item.label).format('DD') : item.date ? dayjs(item.date).format('DD') : '', + orders_total: item.orders_total || 0, + items_total: item.items_total || 0 + })); + } else { + mappedData = labels.map(label => ({ + label, + orders_total: 0, + items_total: 0 + })); + } + } catch (error) { + console.error(error); + mappedData = labels.map(label => ({ + label, + orders_total: 0, + items_total: 0 + })); + } + break; + + case '12m': // سنوي + default: + period = 'yearly'; + labels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + try { + const response = await authService.getOrderStatistics(restaurantId, period, customDate); + if (response.success && response.data.length > 0) { + mappedData = response.data.map(item => ({ + label: item.label || item.date || '', + orders_total: item.orders_total || 0, + items_total: item.items_total || 0 + })); + } else { + mappedData = labels.map(label => ({ + label, + orders_total: 0, + items_total: 0 + })); + } + } catch (error) { + console.error(error); + mappedData = labels.map(label => ({ + label, + orders_total: 0, + items_total: 0 + })); + } + break; + } + + setChartData(mappedData); +}; + + + useEffect(() => { + fetchStatistics(); + }, [timeFrame, restaurantId, customDate]); + + return ( + + {/* Header Buttons */} + + + {[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => ( + + ))} + + + {/* Date Picker + Today Button */} + + + + + + + {/* StatisticsCard */} + + value} + timeFrame={timeFrame} + onTimeFrameChange={handleTimeFrameChange} + height={320} + /> + + + ); +}; + +export default AnalyticsPage; diff --git a/src/components/Home/Dashboard/AnalyticsPageOccupancy.js b/src/components/Home/Dashboard/AnalyticsPageOccupancy.js new file mode 100644 index 0000000..8ab787f --- /dev/null +++ b/src/components/Home/Dashboard/AnalyticsPageOccupancy.js @@ -0,0 +1,82 @@ +import React, { useState, useEffect } from 'react'; +import { useRestaurant } from '../../../contexts/RestaurantContext'; +import OrderStatusCard from './OrderStatusCard'; +import { Box, useTheme, useMediaQuery } from '@mui/material'; +import authService from '../../../services/authService'; +import dayjs from 'dayjs'; + +const AnalyticsPageOccupancy = () => { + const { restaurantId } = useRestaurant(); + const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h' + const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD')); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [occupancyRate, setOccupancyRate] = useState(0); + + const fetchOccupancyRate = async () => { + if (!restaurantId) return; + + let period = 'yearly'; + switch (timeFrame) { + case '24h': + period = 'daily'; + break; + case '30d': + period = 'monthly'; + break; + case '12m': + default: + period = 'yearly'; + break; + } + + try { + const response = await authService.getOccupancyRate( + restaurantId, + period, + customDate + ); + + if (response.success) { + // occupancy_rate جايه كنص مثل "11.83%" → نحولها لرقم + const rateValue = parseFloat(response.data.replace('%', '').trim()); + setOccupancyRate(rateValue); + } else { + setOccupancyRate(0); + } + } catch (error) { + console.error(error); + setOccupancyRate(0); + } + }; + + useEffect(() => { + fetchOccupancyRate(); + }, [timeFrame, restaurantId, customDate]); + + return ( + + {/* Occupancy Card */} + + + + + ); +}; + +export default AnalyticsPageOccupancy; diff --git a/src/components/Home/Dashboard/AnalyticsPagebill.js b/src/components/Home/Dashboard/AnalyticsPagebill.js new file mode 100644 index 0000000..8e0ee10 --- /dev/null +++ b/src/components/Home/Dashboard/AnalyticsPagebill.js @@ -0,0 +1,241 @@ +import React, { useState, useEffect } from 'react'; +import { useRestaurant } from '../../../contexts/RestaurantContext'; +import StatisticsCard from './StatisticsCard'; +// import TablesManager from './TablesManager'; +import { + Box, + useTheme, + useMediaQuery, + Button, + ButtonGroup, + TextField +} from '@mui/material'; +import authService from '../../../services/authService'; +import dayjs from 'dayjs'; + +const AnalyticsPagebill = () => { + const { restaurantId } = useRestaurant(); + const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h' + const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD')); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [chartData, setChartData] = useState([]); + + const handleTimeFrameChange = (newTimeFrame) => { + setTimeFrame(newTimeFrame); + }; + + const handleCustomDateChange = (event) => { + setCustomDate(event.target.value); + }; + + const fetchStatistics = async () => { + if (!restaurantId) return; + + let period = 'yearly'; + let labels = []; + let mappedData = []; + + switch (timeFrame) { + case '24h': // يومي + period = 'daily'; + labels = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`); + try { + const response = await authService.getBillStatistics(restaurantId, period, customDate); + if (response.success && response.data.length > 0) { + mappedData = response.data.map(item => ({ + label: item.label || item.date || '', + bills_total: item.bills_total || 0, + total_price: item.total_price || 0 + })); + } else { + mappedData = labels.map(label => ({ + label, + bills_total: 0, + total_price: 0 + })); + } + } catch (error) { + console.error(error); + mappedData = labels.map(label => ({ + label, + bills_total: 0, + total_price: 0 + })); + } + break; + + case '30d': // شهري + period = 'monthly'; + labels = Array.from({ length: 30 }, (_, i) => dayjs().startOf('month').add(i, 'day').format('DD')); + try { + const response = await authService.getBillStatistics(restaurantId, period, customDate); + if (response.success && response.data.length > 0) { + mappedData = response.data.map(item => ({ + label: item.label ? dayjs(item.label).format('DD') : item.date ? dayjs(item.date).format('DD') : '', + bills_total: item.bills_total || 0, + total_price: item.total_price || 0 + })); + } else { + mappedData = labels.map(label => ({ + label, + bills_total: 0, + total_price: 0 + })); + } + } catch (error) { + console.error(error); + mappedData = labels.map(label => ({ + label, + bills_total: 0, + total_price: 0 + })); + } + break; + + case '12m': // سنوي + default: + period = 'yearly'; + labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + try { + const response = await authService.getBillStatistics(restaurantId, period, customDate); + if (response.success && response.data.length > 0) { + mappedData = response.data.map(item => ({ + label: item.label || item.date || '', + bills_total: item.bills_total || 0, + total_price: item.total_price || 0 + })); + } else { + mappedData = labels.map(label => ({ + label, + bills_total: 0, + total_price: 0 + })); + } + } catch (error) { + console.error(error); + mappedData = labels.map(label => ({ + label, + bills_total: 0, + total_price: 0 + })); + } + break; + } + + setChartData(mappedData); + }; + + + useEffect(() => { + fetchStatistics(); + }, [timeFrame, restaurantId, customDate]); + + return ( + + {/* Header Buttons */} + + + {[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => ( + + ))} + + + {/* Date Picker + Today Button */} + + + + + + + {/* StatisticsCard */} + + value} + timeFrame={timeFrame} + onTimeFrameChange={handleTimeFrameChange} + height={420} + /> + + + ); +}; + +export default AnalyticsPagebill; diff --git a/src/components/Home/Dashboard/Dashboard.js b/src/components/Home/Dashboard/Dashboard.js index 7969b6a..6001f9d 100644 --- a/src/components/Home/Dashboard/Dashboard.js +++ b/src/components/Home/Dashboard/Dashboard.js @@ -11,23 +11,23 @@ const Dashboard = () => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const [hasProducts, setHasProducts] = useState(false); - const [isLoading, setIsLoading] = useState(true); + // const [isLoading, setIsLoading] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(!isMobile); - // محاكاة التحقق من المنتجات + useEffect(() => { const checkProducts = async () => { - setIsLoading(true); - const productsExist = await checkIfProductsExist(); // استبدل بمنطقك + // setIsLoading(true); + const productsExist = await checkIfProductsExist(); setHasProducts(productsExist); - setIsLoading(false); + // setIsLoading(false); }; checkProducts(); }, []); const checkIfProductsExist = async () => { - return new Promise((resolve) => setTimeout(() => resolve(true), 1500)); // محاكاة تأخير + return new Promise((resolve) => setTimeout(() => resolve(true), 1500)); }; useEffect(() => { @@ -76,8 +76,8 @@ const Dashboard = () => { { duration: theme.transitions.duration.leavingScreen, }), }}> - {isLoading ? ( + {/* {isLoading ? ( <> ) : ( - hasProducts ? : - )} + hasProducts ? + + : + )} */} +
diff --git a/src/components/Home/Dashboard/DashboardContcet.js b/src/components/Home/Dashboard/DashboardContcet.js index 39977bf..7455f82 100644 --- a/src/components/Home/Dashboard/DashboardContcet.js +++ b/src/components/Home/Dashboard/DashboardContcet.js @@ -7,9 +7,9 @@ import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard'; import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; import { useTheme } from '@mui/material/styles'; import AddIcon from '@mui/icons-material/Add'; -import OrderStatusCard from './OrderStatusCard'; -import StatisticsCard from './StatisticsCard'; -import RecentActivity from './RecentActivity'; +import AnalyticsPage from './AnalyticsPage'; +import AnalyticsPageOccupancy from './AnalyticsPageOccupancy'; +import AnalyticsPagebill from './AnalyticsPagebill'; const IconCircle = ({ children, bgColor = '#DEDEFA', outerColor = '#EFEFFD' }) => ( ); -const StatusCard = ({ - icon, - statusText, - statusColor, - iconColor, - outerColor, - innerColor, - height = { - xs: 'calc(140px + 0.5vh)', - sm: 'calc(150px + 0.5vh)', - md: 'calc(162px + 0.5vh)' - }, - width = { - xs: 'min(90%, 600px)', // تقليل من 90% إلى 85% والحد الأقصى من 300px إلى 280px - sm: 'clamp(220px, 23vw, 280px)', // تقليل جميع القيم - md: 'clamp(220px, 19vw, 300px)' - }, - transition = 'width 0.3s ease', - iconSpacing = { xs: 6, sm: 8, md: 12 }, - extraButton, - statusButtonWidth, - title = 'Point Of Sale', -}) => { - const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - - return ( - - - - - {React.cloneElement(icon, { sx: { color: iconColor, fontSize: { xs: 16, sm: 18, md: 20 } } })} - - - - - - {title} - - - - {extraButton ? extraButton : } - - - - - ); -}; const DashboardContect = () => { const theme = useTheme(); @@ -172,7 +67,7 @@ const DashboardContect = () => { display: 'flex', justifyContent: 'space-between', alignItems: { xs: 'flex-start', sm: 'center', md: 'center' }, - mb: 3, + mb: 1, pl: 1, pr: { xs: 1, sm: 3 }, flexDirection: { xs: 'column', sm: 'row', md: 'row' }, @@ -192,7 +87,7 @@ const DashboardContect = () => { {/* Buttons */} - { > Check Inventory - + */}
{/* Status Cards */} - { iconSpacing={{ xs: 3, sm: 5, md: 7 }} title="Delivery" /> - +
*/} {/* OrderStatusCard/ StatisticsCard */} { flexWrap: 'nowrap', flexDirection: { xs: 'column', sm: 'row' }, gap: { xs: 1, sm: 1, md: 7 }, // gap مرن - mb: 1, - pl: 1, - pr: 1, - mt: 2, + mr: { xs: 4, sm: 3, md: 0 }, ml: { xs: 2, sm: 0 }, - mr: 2, flexShrink: 0, }} > @@ -357,12 +248,13 @@ const DashboardContect = () => { sx={{ flexGrow: 0, width: { xs: '100%', sm: '35%', md: '30%' }, - minWidth: 220, + minWidth: 20, ml: { xs: 0, sm: -1 }, flexShrink: 0, + mt:8 }} > - + {/* StatisticsCard */} @@ -370,11 +262,11 @@ const DashboardContect = () => { sx={{ flexGrow: 1, minWidth: 0, - ml: { xs: 0, sm: 2 }, + // ml: { xs: 0, sm: 2 }, // backgroundColor:'red' }} > - +
@@ -382,11 +274,11 @@ const DashboardContect = () => { {/* RecentActivity */} - + ); diff --git a/src/components/Home/Dashboard/NoProdectDash.js b/src/components/Home/Dashboard/NoProdectDash.js index a472d02..8298865 100644 --- a/src/components/Home/Dashboard/NoProdectDash.js +++ b/src/components/Home/Dashboard/NoProdectDash.js @@ -68,7 +68,7 @@ const NoProdectDash = () => { fontSize: { xs: '14px', md: '18px' }, mb: 2, color: '#5F6868', - whiteSpace: 'pre-line' // هذا يسمح بكسر السطر عند المسافات + whiteSpace: 'pre-line' // يسمح بكسر السطر عند المسافات }} > Sorry!{' '} diff --git a/src/components/Home/Dashboard/OrderStatusCard.js b/src/components/Home/Dashboard/OrderStatusCard.js index b672730..ffce155 100644 --- a/src/components/Home/Dashboard/OrderStatusCard.js +++ b/src/components/Home/Dashboard/OrderStatusCard.js @@ -1,260 +1,241 @@ -import React from 'react'; +import React, { useState } from 'react'; import { - Box, - Typography, - Card, - CardContent, - Divider, - Chip, - useTheme, - IconButton, - useMediaQuery + Box, + Typography, + Card, + CardContent, + useTheme, + IconButton, + useMediaQuery, + Menu, + MenuItem, + Button, + ButtonGroup, + TextField } from '@mui/material'; import { - TrendingUp as TrendingUpIcon, - TrendingDown as TrendingDownIcon, - CheckCircle as CheckCircleIcon, - Cancel as CancelIcon, - Pending as PendingIcon + TrendingUp as TrendingUpIcon, + TrendingDown as TrendingDownIcon, } from '@mui/icons-material'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import { - CircularProgressbar, - buildStyles + CircularProgressbar, + buildStyles } from 'react-circular-progressbar'; import 'react-circular-progressbar/dist/styles.css'; -import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; -import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; -// import { Box, IconButton, Paper, Typography, useTheme, useMediaQuery } from '@mui/material'; -import { - - ResponsiveContainer, - -} from 'recharts'; - +import { ResponsiveContainer } from 'recharts'; +import dayjs from 'dayjs'; const OrderStatusCard = ({ - title = 'Order Status', - period = 'This Quarter', - progress = 70.5, - progressChange = '+10%', - progressDirection = 'up', // or 'down' - description = 'You succeeded in earning $240 today, it’s higher than yesterday', - completed = '$20k', - canceled = '$16k', - pending = '$1.5k' + title = 'Order Status', + period = 'This Quarter', + progress = 70.5, + description = 'You succeeded in earning $240 today, it’s higher than yesterday', + timeFrame, + setTimeFrame, + customDate, + setCustomDate }) => { - const theme = useTheme(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md')); + const [anchorEl, setAnchorEl] = useState(null); - const progressColor = progressDirection === 'up' ? theme.palette.success.main : theme.palette.error.main; - const progressIcon = progressDirection === 'up' ? : ; - const highlightedDescription = description.replace( - /(\$\d+(?:\.\d+)?[kKmM]?)/g, - '$1' - ); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md')); + const open = Boolean(anchorEl); + const handleMenuOpen = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleMenuClose = () => { + setAnchorEl(null); + }; - - return ( - + + + {/* زر القائمة العلوية */} + - - {/* زر الإجراءات في الزاوية العلوية اليمنى */} - + + + {/* القائمة المنسدلة */} + + + {/* Header Buttons (Timeframe) */} + + {[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => ( + + ))} + - - {/* Header */} - + setCustomDate(e.target.value)} + size="small" + sx={{ + width: isMobile ? '100%' : 150, + '& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' }, + '& .MuiOutlinedInput-notchedOutline': { border: 'none' } + }} + /> + + + + - ml: 0 // هامش يسار لإفساح المجال للأيقونة - }} - > - - {title} - - - {period} - - + {/* المحتوى الأساسي للكارت */} + + + + {title} + + + {period} + + - {/* Progress Circle */} - - {/* تغيير النسبة تحت الدائرة */} - - - + {/* دائرة التقدم */} + + + + + + - {/* دائرة التقدم */} - - - - - + {/* النص داخل الدائرة */} + + {`${progress}%`} + + + - {/* النص داخل الدائرة */} - - {`${progress}%`} - - - - - {/* الوصف */} - - - - - {/* الحقول الثلاثة (Completed / Canceled / Pending) */} - - {/* لكل قسم: */} - {[{ - label: 'Completed', - icon: , - value: completed - }, { - label: 'Canceled', - icon: , - value: canceled - }, { - label: 'Pending', - icon: , - value: pending - }].map(({ label, icon, value }, i) => ( - - - {label} - - - {icon} - - {value} - - - - ))} - - - - - ); + {/* الوصف */} + + {description} + + + + + ); }; export default OrderStatusCard; diff --git a/src/components/Home/Dashboard/RecentActivity.js b/src/components/Home/Dashboard/RecentActivity.js index f24b20f..6f16218 100644 --- a/src/components/Home/Dashboard/RecentActivity.js +++ b/src/components/Home/Dashboard/RecentActivity.js @@ -12,9 +12,6 @@ import { Button, Chip } from '@mui/material'; -// import LocalShippingIcon from '@mui/icons-material/LocalShipping'; -// import InventoryIcon from '@mui/icons-material/Inventory'; -// import AssignmentIcon from '@mui/icons-material/Assignment'; const RecentActivity = () => { // Sample data diff --git a/src/components/Home/Dashboard/StatisticsCard.js b/src/components/Home/Dashboard/StatisticsCard.js index 24874b1..fcaa5e6 100644 --- a/src/components/Home/Dashboard/StatisticsCard.js +++ b/src/components/Home/Dashboard/StatisticsCard.js @@ -1,60 +1,92 @@ -import React from 'react'; -import { Box, IconButton, Paper, Typography, useTheme, useMediaQuery } from '@mui/material'; +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; import { - LineChart, - Line, + Box, + IconButton, + Paper, + Typography, + useTheme, + useMediaQuery, + Menu, + MenuItem, + TextField, + Button +} from '@mui/material'; +import { + AreaChart, + Area, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, - Area, - AreaChart, - CartesianGrid, - defs, - linearGradient, - stop + CartesianGrid } from 'recharts'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; +// import MoreVertIcon from '@mui/icons-material/MoreVert'; -const data = [ - { month: 'Jan', revenue: 4000, sales: 2400 }, - { month: 'Feb', revenue: 3000, sales: 1398 }, - { month: 'Mar', revenue: 2000, sales: 9800 }, - { month: 'Apr', revenue: 2780, sales: 3908 }, - { month: 'May', revenue: 1890, sales: 4800 }, - { month: 'Jun', revenue: 2390, sales: 3800 }, - { month: 'Jul', revenue: 3490, sales: 4300 }, - { month: 'Aug', revenue: 5000, sales: 4000 }, - { month: 'Sep', revenue: 4700, sales: 4200 }, - { month: 'Oct', revenue: 5200, sales: 4600 }, - { month: 'Nov', revenue: 4800, sales: 4400 }, - { month: 'Dec', revenue: 5300, sales: 4800 }, -]; - -const formatCurrency = (value) => { - if (value >= 1000000) { - return `$${(value / 1000000).toFixed(1)}M`; - } else if (value >= 1000) { - return `$${(value / 1000).toFixed(1)}K`; - } - return `$${value}`; -}; - -const StatisticsCard = () => { +const StatisticsCard = ({ + title = "Statistics", + subtitle = "Delivery Times", + data = [], + dataKeys = [ + { key: 'revenue', name: 'Revenue', color: '#E46A11' }, + { key: 'sales', name: 'Sales', color: '#0182FC' } + ], + xDataKey = 'month', + valueFormatter = (value) => value, + timeFrame = 'month', + onTimeFrameChange, + height // 👈 جديد +}) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md')); + const [selectedDate, setSelectedDate] = useState(''); + const [anchorEl, setAnchorEl] = useState(null); + + const handleMenuOpen = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleMenuClose = () => { + setAnchorEl(null); + }; + + const handleDateChange = (event) => { + setSelectedDate(event.target.value); + if (onTimeFrameChange) onTimeFrameChange(timeFrame, event.target.value); + handleMenuClose(); + }; + + const handleTodayClick = () => { + const today = new Date().toISOString().split('T')[0]; + setSelectedDate(today); + if (onTimeFrameChange) onTimeFrameChange(timeFrame, today); + handleMenuClose(); + }; + + // حساب العلامات الديناميكية للمحور الشاقولي + const ticksArray = (() => { + if (!data || data.length === 0) return []; + const maxValue = Math.max( + ...data.flatMap(d => dataKeys.map(k => d[k.key] || 0)) + ); + const desiredTicks = 8; // عدد العلامات المطلوب + const step = Math.ceil(maxValue / desiredTicks) || 1; + const arr = []; + for (let i = 0; i <= maxValue; i += step) { + arr.push(i); + } + return arr; + })(); return ( - - + { right: { xs: 4, sm: 8 }, color: '#667085' }} + onClick={handleMenuOpen} > - + {/* */} + + + + + + + + + + {/* Header */} - - Statistics - - - Delivery Times - + + + {title} + + + {subtitle} + + - - + + - - - - - - - - + {dataKeys.map(({ key, color }) => ( + + + + + ))} - + + - - - - - - + {dataKeys.map(({ key, name, color }) => ( + + ))} @@ -193,4 +212,19 @@ const StatisticsCard = () => { ); }; -export default StatisticsCard; \ No newline at end of file +StatisticsCard.propTypes = { + title: PropTypes.string, + subtitle: PropTypes.string, + data: PropTypes.arrayOf(PropTypes.object), + dataKeys: PropTypes.arrayOf(PropTypes.shape({ + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + color: PropTypes.string.isRequired + })), + xDataKey: PropTypes.string, + valueFormatter: PropTypes.func, + timeFrame: PropTypes.oneOf(['day', 'week', 'month', 'year']), + onTimeFrameChange: PropTypes.func +}; + +export default StatisticsCard; diff --git a/src/components/Home/Employ/Employ.js b/src/components/Home/Employ/Employ.js new file mode 100644 index 0000000..f7c49e3 --- /dev/null +++ b/src/components/Home/Employ/Employ.js @@ -0,0 +1,114 @@ +import React, { useState, useEffect } from 'react'; +import { Box, useTheme, useMediaQuery } from '@mui/material'; +import KitchPlusAppBar from '../AppBar'; +import Sidebar from '../SideHome'; +import Waiter from './contcet/waiter'; +import Cooker from './contcet/cooker'; +import Accountant from './contcet/Accountant'; +import { useRestaurant } from '../../../contexts/RestaurantContext'; + +const drawerWidth = 230; + +const Employ = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const isMdUp = useMediaQuery(theme.breakpoints.up('md')); + + const [sidebarOpen, setSidebarOpen] = useState(!isMobile); + const { restaurantId } = useRestaurant(); + + useEffect(() => { + const handleResize = () => { + if (window.innerWidth >= theme.breakpoints.values.md) { + setSidebarOpen(true); + } else { + setSidebarOpen(false); + } + }; + + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [theme.breakpoints.values.md]); + + return ( + + setSidebarOpen(!sidebarOpen)} + isMobile={isMobile} + drawerWidth={drawerWidth} + /> + + + setSidebarOpen(!sidebarOpen)} + sidebarOpen={sidebarOpen} + isMobile={isMobile} + /> + + + {/* المحتوى: Waiter و Cooker */} + + + + + + + + + ); +}; + +export default Employ; diff --git a/src/components/Home/Employ/contcet/Accountant.js b/src/components/Home/Employ/contcet/Accountant.js new file mode 100644 index 0000000..cb7ea9c --- /dev/null +++ b/src/components/Home/Employ/contcet/Accountant.js @@ -0,0 +1,526 @@ +import React, { useState, useEffect } from 'react'; +import { + useMediaQuery, + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Button, + Chip, + Menu, + MenuItem, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from '@mui/material'; +import TuneIcon from '@mui/icons-material/Tune'; +import AddEmployModal from './AddEmployModal'; +import authService from '../../../../services/authService'; +import IOSSwitch from './IOSSwitch'; +import { useTheme } from '@mui/material/styles'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import EmployeeDetailsModal from './EmployeeDetailsModal'; + +const Accountant = ({ restaurantId }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [modalOpen, setModalOpen] = useState(false); + const [showAll, setShowAll] = useState(false); + const [switchStates, setSwitchStates] = useState({}); + const [shiftFilter, setShiftFilter] = useState('all'); + const [filterAnchorEl, setFilterAnchorEl] = useState(null); + const [Accountants, setAccountants] = useState([]); + const [loading, setLoading] = useState(true); + + const [detailsModalOpen, setDetailsModalOpen] = useState(false); + const [AccountantDetails, setAccountantDetails] = useState(null); + + + // حالات الحذف + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + const [selectedAccountant, setSelectedAccountant] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + + useEffect(() => { + const fetchAccountants = async () => { + setLoading(true); + try { + const response = await authService.getAllAccountants(restaurantId); + if (response.accountants) { + setAccountants(response.accountants); + + // تهيئة switchStates حسب قيمة active + const initialStates = {}; + response.accountants.forEach(accountants => { + initialStates[accountants.id] = accountants.active === 1; // أو Boolean(waiter.active) + }); + setSwitchStates(initialStates); + } else { + setAccountants([]); + } + } catch (error) { + console.error("Failed to fetch waiters:", error); + setAccountants([]); + } finally { + setLoading(false); + } + }; + + fetchAccountants(); + }, [restaurantId]); + + const handleSwitchChange = async (id, email) => { + try { + const response = await authService.toggleAccountStatus(email, "Accountant"); + + if (response.success) { + setSwitchStates((prev) => ({ + ...prev, + [id]: response.data, + })); + // alert(response.message); + } else { + alert("Failed: " + response.message); + } + } catch (error) { + alert("Error: " + error.message); + } + }; + + + const handleOpenModal = () => setModalOpen(true); + const handleCloseModal = () => setModalOpen(false); + + const handleConfirm = async (formData) => { + try { + const AccountantData = { + ...formData, + restaurant_id: restaurantId, + }; + + const response = await authService.registerAccountant(AccountantData); + + if (response.success) { + alert('Accountant registered successfully!'); + setModalOpen(false); + + // تحديث الحالة مباشرة بدل إعادة جلب كل البيانات + const newAccountant = response.Accountant || AccountantData; + // تأكد أن الـ API يعيد الـ Accountant المضاف، أو استخدم البيانات المحلية + setAccountants((prev) => [newAccountant, ...prev]); + + } else { + if (response.errors) { + const errorsList = Object.entries(response.errors) + .map(([field, msgs]) => { + if (Array.isArray(msgs)) { + return `${field}: ${msgs.join(', ')}`; + } else { + return `${field}: ${msgs}`; + } + }) + .join('\n'); + alert(`Failed to register Accountant due to validation errors:\n${errorsList}`); + } else { + alert('Failed to register Accountant: ' + response.message); + } + } + } catch (error) { + alert('An error occurred: ' + error.message); + } +}; + + + const getShiftChipProps = (shiftType) => { + switch (shiftType?.toLowerCase()) { + case 'morning': + return { + label: 'Morning', + sx: { + backgroundColor: '#E7F4EE', + color: '#0D894F', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + case 'evening': + return { + label: 'Evening', + sx: { + backgroundColor: '#FDF1E8', + color: '#E46A11', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + default: + return { + label: shiftType, + sx: { + backgroundColor: '#E0E0E0', + color: '#424242', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + } + }; + + const filteredAccountants = + shiftFilter === 'all' + ? Accountants + : Accountants.filter((w) => w.shift_type.toLowerCase() === shiftFilter); + + const displayedAccountants = showAll ? filteredAccountants : filteredAccountants.slice(0, 3); + + const handleFilterClick = (event) => { + setFilterAnchorEl(event.currentTarget); + }; + + const handleFilterClose = () => { + setFilterAnchorEl(null); + }; + + const handleFilterSelect = (value) => { + setShiftFilter(value); + setFilterAnchorEl(null); + }; + + // فتح مودال الحذف + const handleDeleteClick = (Accountant) => { + setSelectedAccountant(Accountant); + setConfirmDeleteOpen(true); + }; + + // تأكيد الحذف + const handleDeleteConfirm = async () => { + if (!selectedAccountant) return; + setIsDeleting(true); + try { + const response = await authService.deleteAccountant(selectedAccountant.id); + + // هنا تحقق من وجود message بدل success + if (response.message && response.message.toLowerCase().includes("successfully")) { + // alert(response.message); + setAccountants((prev) => prev.filter((w) => w.id !== selectedAccountant.id)); + } else { + alert("Failed to delete Accountant: " + (response.message || "Unknown error")); + } + } catch (error) { + alert("An error occurred: " + error.message); + } finally { + setIsDeleting(false); + setConfirmDeleteOpen(false); + setSelectedAccountant(null); + } + }; + + const handleOpenDetails = async (id) => { + try { + const response = await authService.getAccountantById(id); + if (response.accountant) { + setAccountantDetails(response.accountant); + setDetailsModalOpen(true); + } else { + alert("Failed to fetch Accountant details"); + } + } catch (error) { + alert("Error: " + error.message); + } + }; + + + return ( + + + + Accountant + + + {Accountants.length > 3 && ( + + )} + + + + + + + + + + + + + Name + + + Email + + + Shift Type + + + Actions + + + + + {loading ? ( + + + Loading... + + + ) : displayedAccountants.length === 0 ? ( + + + No activities found. + + + ) : ( + displayedAccountants.map((Accountant) => ( + + + handleOpenDetails(Accountant.id)} + > + {Accountant.name} + + + + + + {Accountant.email} + + + + + + + + + + handleSwitchChange(Accountant.id, Accountant.email)} + inputProps={{ 'aria-label': 'accountant switch' }} + /> + handleDeleteClick(Accountant)} + /> + + + + )) + )} + +
+
+ + + + + {['all', 'morning', 'evening'].map((shift) => ( + handleFilterSelect(shift)} + sx={{ + color: shiftFilter === shift ? '#FF914D' : '#4F5867', + backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent', + '&:hover': { + backgroundColor: '#f0f0f0ff', + transition: 'background-color 150ms ease-in-out', + }, + }} + > + {shift.charAt(0).toUpperCase() + shift.slice(1)} + + ))} + + + {/* مودال تأكيد الحذف */} + setConfirmDeleteOpen(false)}> + Confirm Delete + + + Are you sure you want to delete "{selectedAccountant?.name}"? This action cannot be undone. + + + + + + + + + setDetailsModalOpen(false)} + employee={AccountantDetails} + type="Accountant" + /> + + +
+ ); +}; + +export default Accountant; diff --git a/src/components/Home/Employ/contcet/AddEmployModal.js b/src/components/Home/Employ/contcet/AddEmployModal.js new file mode 100644 index 0000000..e008dde --- /dev/null +++ b/src/components/Home/Employ/contcet/AddEmployModal.js @@ -0,0 +1,330 @@ +import React, { useState } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Typography, + Box, + Button, + useTheme, + IconButton, + InputAdornment, + MenuItem, +} from '@mui/material'; +import VisibilityOutlined from '@mui/icons-material/VisibilityOutlined'; +import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; + +const AddUserModal = ({ + open, + onClose, + onConfirm, + title = 'Add User', + buttonText = 'Add User', +}) => { + const theme = useTheme(); + + const [formValues, setFormValues] = useState({ + name: '', + email: '', + password: '', + password_confirmation: '', + shift_type: '', + working_hours: '', + monthly_salary: '', + }); + + const [errors, setErrors] = useState({}); + + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const handleTogglePassword = () => setShowPassword((prev) => !prev); + const handleToggleConfirmPassword = () => setShowConfirmPassword((prev) => !prev); + + const handleChange = (key, value) => { + setFormValues((prev) => ({ ...prev, [key]: value })); + setErrors((prev) => ({ ...prev, [key]: null })); // مسح الخطأ عند التعديل + }; + + const validate = () => { + let tempErrors = {}; + + if (!formValues.name.trim()) tempErrors.name = 'Name is required'; + + if (!formValues.email.trim()) tempErrors.email = 'Email is required'; + else { + // Regex بسيط للتحقق من صحة الإيميل + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formValues.email)) + tempErrors.email = 'Invalid email format'; + } + + if (!formValues.password) { + tempErrors.password = 'Password is required'; + } else if (formValues.password.length < 6) { + tempErrors.password = 'Password must be at least 6 characters long'; + } + + if (!formValues.password_confirmation) + tempErrors.password_confirmation = 'Confirm password is required'; + + if ( + formValues.password && + formValues.password_confirmation && + formValues.password !== formValues.password_confirmation + ) + tempErrors.password_confirmation = 'Passwords do not match'; + + if (!formValues.shift_type) tempErrors.shift_type = 'Shift type is required'; + + if (!formValues.working_hours) tempErrors.working_hours = 'Working hours is required'; + + if (!formValues.monthly_salary) tempErrors.monthly_salary = 'Monthly salary is required'; + + setErrors(tempErrors); + + return Object.keys(tempErrors).length === 0; + }; + + const handleSubmit = () => { + if (validate()) { + onConfirm(formValues); + + // إعادة تعيين الحقول بعد النجاح + setFormValues({ + name: '', + email: '', + password: '', + password_confirmation: '', + shift_type: '', + working_hours: '', + monthly_salary: '', + }); + + setErrors({}); // تصفير الأخطاء أيضًا + } +}; + + + return ( + + {title} + + {/* Name */} + + + Name + + handleChange('name', e.target.value)} + sx={fieldStyle(theme)} + error={!!errors.name} + helperText={errors.name} + /> + + + {/* Email */} + + + Email + + handleChange('email', e.target.value)} + sx={fieldStyle(theme)} + error={!!errors.email} + helperText={errors.email} + /> + + + {/* Password */} + + + Password + + handleChange('password', e.target.value)} + sx={fieldStyle(theme)} + error={!!errors.password} + helperText={errors.password} + InputProps={{ + endAdornment: ( + + + {showPassword ? : } + + + ), + }} + /> + + + {/* Confirm Password */} + + + Confirm Password + + handleChange('password_confirmation', e.target.value)} + sx={fieldStyle(theme)} + error={!!errors.password_confirmation} + helperText={errors.password_confirmation} + InputProps={{ + endAdornment: ( + + + {showConfirmPassword ? : } + + + ), + }} + /> + + + {/* Shift Type */} + + + Shift Type + + handleChange('shift_type', e.target.value)} + sx={fieldStyle(theme)} + error={!!errors.shift_type} + helperText={errors.shift_type} + placeholder="Select shift type" + > + Morning + Evening + + + + {/* Working Hours */} + + + Working Hours + + handleChange('working_hours', e.target.value)} + sx={fieldStyle(theme)} + error={!!errors.working_hours} + helperText={errors.working_hours} + /> + + + {/* Monthly Salary */} + + + Monthly Salary + + handleChange('monthly_salary', e.target.value)} + sx={fieldStyle(theme)} + error={!!errors.monthly_salary} + helperText={errors.monthly_salary} + /> + + + + + + + + + + ); +}; + +const fieldStyle = (theme) => ({ + '& input': { fontWeight: 500, fontSize: '15px' }, + '& label': { fontWeight: 600, fontSize: '15px', color: 'black' }, + '& .MuiOutlinedInput-root': { + borderRadius: '8px', + }, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.grey[400], + }, + '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.primary.main, + }, + '& input::-ms-reveal, & input::-ms-clear': { + display: 'none', + }, +}); + +export default AddUserModal; diff --git a/src/components/Home/HostKitchen/contcet/ConfirmationDialog.js b/src/components/Home/Employ/contcet/ConfirmationDialog.js similarity index 100% rename from src/components/Home/HostKitchen/contcet/ConfirmationDialog.js rename to src/components/Home/Employ/contcet/ConfirmationDialog.js diff --git a/src/components/Home/Employ/contcet/EmployeeDetailsModal.js b/src/components/Home/Employ/contcet/EmployeeDetailsModal.js new file mode 100644 index 0000000..988eaf0 --- /dev/null +++ b/src/components/Home/Employ/contcet/EmployeeDetailsModal.js @@ -0,0 +1,66 @@ +import React from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Typography, + Button, + Box, +} from "@mui/material"; + +const EmployeeDetailsModal = ({ open, onClose, employee, type }) => { + if (!employee) return null; + + // دالة لتحويل الحالة الرقمية إلى نصية + const getStatusText = (active) => (active ? "Active" : "Inactive"); + + // عنوان المودال حسب نوع الموظف + const getTitle = () => { + switch (type?.toLowerCase()) { + case "waiter": + return "Waiter Details"; + case "cooker": + return "Cooker Details"; + case "accountant": + return "Accountant Details"; + default: + return "Employee Details"; + } + }; + + return ( + + {getTitle()} + + + {employee.name && Name: {employee.name}} + {employee.email && Email: {employee.email}} + {employee.shift_type && Shift: {employee.shift_type}} + {employee.working_hours !== undefined && ( + Working Hours: {employee.working_hours} + )} + {employee.monthly_salary !== undefined && ( + Monthly Salary: {employee.monthly_salary} + )} + {employee.active !== undefined && ( + Status: {getStatusText(employee.active)} + )} + {employee.code && Code: {employee.code}} + {employee.expires_at && Expires At: {employee.expires_at}} + + + + + + + ); +}; + +export default EmployeeDetailsModal; diff --git a/src/components/Home/Employ/contcet/IOSSwitch.js b/src/components/Home/Employ/contcet/IOSSwitch.js new file mode 100644 index 0000000..be34356 --- /dev/null +++ b/src/components/Home/Employ/contcet/IOSSwitch.js @@ -0,0 +1,57 @@ +import { styled } from '@mui/material/styles'; +import Switch from '@mui/material/Switch'; + +const IOSSwitch = styled((props) => ( + +))(({ theme }) => ({ + width: 42, + height: 26, + padding: 0, + '& .MuiSwitch-switchBase': { + padding: 0, + margin: 2, + transitionDuration: '300ms', + '&.Mui-checked': { + transform: 'translateX(16px)', + color: '#fff', + '& + .MuiSwitch-track': { + backgroundColor: theme.palette.primary.main, + opacity: 1, + border: 0, + }, + '&.Mui-disabled + .MuiSwitch-track': { + opacity: 0.5, + }, + }, + '&.Mui-focusVisible .MuiSwitch-thumb': { + color: theme.palette.primary.main, + border: '6px solid #fff', + }, + '&.Mui-disabled .MuiSwitch-thumb': { + color: + theme.palette.mode === 'light' + ? theme.palette.grey[100] + : theme.palette.grey[600], + }, + '&.Mui-disabled + .MuiSwitch-track': { + opacity: theme.palette.mode === 'light' ? 0.7 : 0.3, + }, + }, + '& .MuiSwitch-thumb': { + boxSizing: 'border-box', + width: 22, + height: 22, + borderRadius: 11, + }, + '& .MuiSwitch-track': { + borderRadius: 26 / 2, + backgroundColor: + theme.palette.mode === 'light' ? '#E9E9EA' : 'rgba(255,255,255,0.35)', + opacity: 1, + transition: theme.transitions.create(['background-color'], { + duration: 500, + }), + }, +})); + +export default IOSSwitch; diff --git a/src/components/Home/Employ/contcet/cooker.js b/src/components/Home/Employ/contcet/cooker.js new file mode 100644 index 0000000..398758b --- /dev/null +++ b/src/components/Home/Employ/contcet/cooker.js @@ -0,0 +1,526 @@ +import React, { useState, useEffect } from 'react'; +import { + useMediaQuery, + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Button, + Chip, + Menu, + MenuItem, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from '@mui/material'; +import TuneIcon from '@mui/icons-material/Tune'; +import AddEmployModal from './AddEmployModal'; +import authService from '../../../../services/authService'; +import IOSSwitch from './IOSSwitch'; +import { useTheme } from '@mui/material/styles'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import EmployeeDetailsModal from './EmployeeDetailsModal'; + +const Cooker = ({ restaurantId }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [modalOpen, setModalOpen] = useState(false); + const [showAll, setShowAll] = useState(false); + const [switchStates, setSwitchStates] = useState({}); + const [shiftFilter, setShiftFilter] = useState('all'); + const [filterAnchorEl, setFilterAnchorEl] = useState(null); + const [Cookers, setCookers] = useState([]); + const [loading, setLoading] = useState(true); + + const [detailsModalOpen, setDetailsModalOpen] = useState(false); + const [CookerDetails, setCookerDetails] = useState(null); + + + // حالات الحذف + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + const [selectedCooker, setSelectedCooker] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + + // استدعاء API + useEffect(() => { + const fetchCookers = async () => { + setLoading(true); + try { + const response = await authService.getAllCookers(restaurantId); + if (response.cookers) { + setCookers(response.cookers); + + const initialStates = {}; + response.cookers.forEach(cookers => { + initialStates[cookers.id] = cookers.active === 1; // أو Boolean(waiter.active) + }); + setSwitchStates(initialStates); + } else { + setCookers([]); + } + } catch (error) { + console.error("Failed to fetch Cookers:", error); + setCookers([]); + } finally { + setLoading(false); + } + }; + + fetchCookers(); + }, [restaurantId]); + + const handleSwitchChange = async (id, email) => { + try { + const response = await authService.toggleAccountStatus(email, "Cooker"); + + if (response.success) { + setSwitchStates((prev) => ({ + ...prev, + [id]: response.data, + })); + // alert(response.message); + } else { + alert("Failed: " + response.message); + } + } catch (error) { + alert("Error: " + error.message); + } + }; + + const handleOpenModal = () => setModalOpen(true); + const handleCloseModal = () => setModalOpen(false); + + const handleConfirm = async (formData) => { + try { + const CookerData = { + ...formData, + restaurant_id: restaurantId, + }; + + const response = await authService.registerCooker(CookerData); + + if (response.success) { + alert('Cooker registered successfully!'); + setModalOpen(false); + + // تحديث الحالة مباشرة بدل إعادة جلب كل البيانات + const newCooker = response.Cooker || CookerData; + // تأكد أن الـ API يعيد الـ Cooker المضاف، أو استخدم البيانات المحلية + setCookers((prev) => [newCooker, ...prev]); + + } else { + if (response.errors) { + const errorsList = Object.entries(response.errors) + .map(([field, msgs]) => { + if (Array.isArray(msgs)) { + return `${field}: ${msgs.join(', ')}`; + } else { + return `${field}: ${msgs}`; + } + }) + .join('\n'); + alert(`Failed to register Cooker due to validation errors:\n${errorsList}`); + } else { + alert('Failed to register Cooker: ' + response.message); + } + } + } catch (error) { + alert('An error occurred: ' + error.message); + } +}; + + + const getShiftChipProps = (shiftType) => { + switch (shiftType?.toLowerCase()) { + case 'morning': + return { + label: 'Morning', + sx: { + backgroundColor: '#E7F4EE', + color: '#0D894F', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + case 'evening': + return { + label: 'Evening', + sx: { + backgroundColor: '#FDF1E8', + color: '#E46A11', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + default: + return { + label: shiftType, + sx: { + backgroundColor: '#E0E0E0', + color: '#424242', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + } + }; + + const filteredCookers = + shiftFilter === 'all' + ? Cookers + : Cookers.filter((w) => w.shift_type.toLowerCase() === shiftFilter); + + const displayedCookers = showAll ? filteredCookers : filteredCookers.slice(0, 3); + + const handleFilterClick = (event) => { + setFilterAnchorEl(event.currentTarget); + }; + + const handleFilterClose = () => { + setFilterAnchorEl(null); + }; + + const handleFilterSelect = (value) => { + setShiftFilter(value); + setFilterAnchorEl(null); + }; + + // فتح مودال الحذف + const handleDeleteClick = (Cooker) => { + setSelectedCooker(Cooker); + setConfirmDeleteOpen(true); + }; + + // تأكيد الحذف + const handleDeleteConfirm = async () => { + if (!selectedCooker) return; + setIsDeleting(true); + try { + const response = await authService.deleteCooker(selectedCooker.id); + + // هنا تحقق من وجود message بدل success + if (response.message && response.message.toLowerCase().includes("successfully")) { + // alert(response.message); + setCookers((prev) => prev.filter((w) => w.id !== selectedCooker.id)); + } else { + alert("Failed to delete Cooker: " + (response.message || "Unknown error")); + } + } catch (error) { + alert("An error occurred: " + error.message); + } finally { + setIsDeleting(false); + setConfirmDeleteOpen(false); + setSelectedCooker(null); + } + }; + + const handleOpenDetails = async (id) => { + try { + const response = await authService.getCookerById(id); + if (response.cooker) { + setCookerDetails(response.cooker); + setDetailsModalOpen(true); + } else { + alert("Failed to fetch Cooker details"); + } + } catch (error) { + alert("Error: " + error.message); + } + }; + + + return ( + + + + Cooker + + + {Cookers.length > 3 && ( + + )} + + + + + + + + + + + + + Name + + + Email + + + Shift Type + + + Actions + + + + + {loading ? ( + + + Loading... + + + ) : displayedCookers.length === 0 ? ( + + + No activities found. + + + ) : ( + displayedCookers.map((Cooker) => ( + + + handleOpenDetails(Cooker.id)} + > + {Cooker.name} + + + + + + {Cooker.email} + + + + + + + + + + handleSwitchChange(Cooker.id, Cooker.email)} + inputProps={{ 'aria-label': 'accountant switch' }} + /> + + handleDeleteClick(Cooker)} + /> + + + + )) + )} + +
+
+ + + + + {['all', 'morning', 'evening'].map((shift) => ( + handleFilterSelect(shift)} + sx={{ + color: shiftFilter === shift ? '#FF914D' : '#4F5867', + backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent', + '&:hover': { + backgroundColor: '#f0f0f0ff', + transition: 'background-color 150ms ease-in-out', + }, + }} + > + {shift.charAt(0).toUpperCase() + shift.slice(1)} + + ))} + + + {/* مودال تأكيد الحذف */} + setConfirmDeleteOpen(false)}> + Confirm Delete + + + Are you sure you want to delete "{selectedCooker?.name}"? This action cannot be undone. + + + + + + + + + setDetailsModalOpen(false)} + employee={CookerDetails} + type="Cooker" + /> + + +
+ ); +}; + +export default Cooker; diff --git a/src/components/Home/Employ/contcet/waiter.js b/src/components/Home/Employ/contcet/waiter.js new file mode 100644 index 0000000..a62c5be --- /dev/null +++ b/src/components/Home/Employ/contcet/waiter.js @@ -0,0 +1,519 @@ +import React, { useState, useEffect } from 'react'; +import { + useMediaQuery, + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Button, + Chip, + Menu, + MenuItem, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from '@mui/material'; +import TuneIcon from '@mui/icons-material/Tune'; +import AddEmployModal from './AddEmployModal'; +import authService from '../../../../services/authService'; +import IOSSwitch from './IOSSwitch'; +import { useTheme } from '@mui/material/styles'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import EmployeeDetailsModal from './EmployeeDetailsModal'; + +const Waiter = ({ restaurantId }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [modalOpen, setModalOpen] = useState(false); + const [showAll, setShowAll] = useState(false); + const [switchStates, setSwitchStates] = useState({}); + const [shiftFilter, setShiftFilter] = useState('all'); + const [filterAnchorEl, setFilterAnchorEl] = useState(null); + const [waiters, setWaiters] = useState([]); + const [loading, setLoading] = useState(true); + + const [detailsModalOpen, setDetailsModalOpen] = useState(false); + const [waiterDetails, setWaiterDetails] = useState(null); + + + // حالات الحذف + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + const [selectedWaiter, setSelectedWaiter] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + + // استدعاء API + useEffect(() => { + const fetchWaiters = async () => { + setLoading(true); + try { + const response = await authService.getAllWaiters(restaurantId); + if (response.waiters) { + setWaiters(response.waiters); + + // تهيئة switchStates حسب قيمة active + const initialStates = {}; + response.waiters.forEach(waiter => { + initialStates[waiter.id] = waiter.active === 1; // أو Boolean(waiter.active) + }); + setSwitchStates(initialStates); + } else { + setWaiters([]); + } + } catch (error) { + console.error("Failed to fetch waiters:", error); + setWaiters([]); + } finally { + setLoading(false); + } + }; + + fetchWaiters(); +}, [restaurantId]); + + + const handleSwitchChange = async (id, email) => { + try { + const response = await authService.toggleAccountStatus(email, "Waiter"); + + if (response.success) { + setSwitchStates((prev) => ({ + ...prev, + [id]: response.data, + })); + // alert(response.message); + } else { + alert("Failed: " + response.message); + } + } catch (error) { + alert("Error: " + error.message); + } + }; + + + const handleOpenModal = () => setModalOpen(true); + const handleCloseModal = () => setModalOpen(false); + + const handleConfirm = async (formData) => { + try { + const waiterData = { + ...formData, + restaurant_id: restaurantId, + }; + + const response = await authService.registerWaiter(waiterData); + + if (response.success) { + alert('Waiter registered successfully!'); + setModalOpen(false); + // إعادة التحميل بعد الإضافة + const updated = await authService.getAllWaiters(restaurantId); + if (updated.waiters) setWaiters(updated.waiters); + } else { + if (response.errors) { + const errorsList = Object.entries(response.errors) + .map(([field, msgs]) => `${field}: ${msgs.join(', ')}`) + .join('\n'); + alert(`Failed to register waiter due to validation errors:\n${errorsList}`); + } else { + alert('Failed to register waiter: ' + response.message); + } + } + } catch (error) { + alert('An error occurred: ' + error.message); + } + }; + + const getShiftChipProps = (shiftType) => { + switch (shiftType?.toLowerCase()) { + case 'morning': + return { + label: 'Morning', + sx: { + backgroundColor: '#E7F4EE', + color: '#0D894F', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + case 'evening': + return { + label: 'Evening', + sx: { + backgroundColor: '#FDF1E8', + color: '#E46A11', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + default: + return { + label: shiftType, + sx: { + backgroundColor: '#E0E0E0', + color: '#424242', + fontWeight: 600, + fontSize: '14px', + minWidth: 80, + textTransform: 'capitalize', + }, + }; + } + }; + + const filteredWaiters = + shiftFilter === 'all' + ? waiters + : waiters.filter((w) => w.shift_type.toLowerCase() === shiftFilter); + + const displayedWaiters = showAll ? filteredWaiters : filteredWaiters.slice(0, 3); + + const handleFilterClick = (event) => { + setFilterAnchorEl(event.currentTarget); + }; + + const handleFilterClose = () => { + setFilterAnchorEl(null); + }; + + const handleFilterSelect = (value) => { + setShiftFilter(value); + setFilterAnchorEl(null); + }; + + // فتح مودال الحذف + const handleDeleteClick = (waiter) => { + setSelectedWaiter(waiter); + setConfirmDeleteOpen(true); + }; + + // تأكيد الحذف + const handleDeleteConfirm = async () => { + if (!selectedWaiter) return; + setIsDeleting(true); + try { + const response = await authService.deleteWaiter(selectedWaiter.id); + + // هنا تحقق من وجود message بدل success + if (response.message && response.message.toLowerCase().includes("successfully")) { + // alert(response.message); + setWaiters((prev) => prev.filter((w) => w.id !== selectedWaiter.id)); + } else { + alert("Failed to delete waiter: " + (response.message || "Unknown error")); + } + } catch (error) { + alert("An error occurred: " + error.message); + } finally { + setIsDeleting(false); + setConfirmDeleteOpen(false); + setSelectedWaiter(null); + } + }; + + const handleOpenDetails = async (id) => { + try { + const response = await authService.getWaiterById(id); + if (response.waiter) { + setWaiterDetails(response.waiter); + setDetailsModalOpen(true); + } else { + alert("Failed to fetch waiter details"); + } + } catch (error) { + alert("Error: " + error.message); + } + }; + + + return ( + + + + Waiter + + + {waiters.length > 3 && ( + + )} + + + + + + + + + + + + + Name + + + Email + + + Shift Type + + + Actions + + + + + {loading ? ( + + + Loading... + + + ) : displayedWaiters.length === 0 ? ( + + + No activities found. + + + ) : ( + displayedWaiters.map((waiter) => ( + + + handleOpenDetails(waiter.id)} + > + {waiter.name} + + + + + + {waiter.email} + + + + + + + + + + handleSwitchChange(waiter.id, waiter.email)} + inputProps={{ 'aria-label': 'accountant switch' }} + /> + + handleDeleteClick(waiter)} + /> + + + + )) + )} + +
+
+ + + + + {['all', 'morning', 'evening'].map((shift) => ( + handleFilterSelect(shift)} + sx={{ + color: shiftFilter === shift ? '#FF914D' : '#4F5867', + backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent', + '&:hover': { + backgroundColor: '#f0f0f0ff', + transition: 'background-color 150ms ease-in-out', + }, + }} + > + {shift.charAt(0).toUpperCase() + shift.slice(1)} + + ))} + + + {/* مودال تأكيد الحذف */} + setConfirmDeleteOpen(false)}> + Confirm Delete + + + Are you sure you want to delete "{selectedWaiter?.name}"? This action cannot be undone. + + + + + + + + + setDetailsModalOpen(false)} + employee={waiterDetails} // بدل waiter + type="waiter" + /> + + +
+ ); +}; + +export default Waiter; diff --git a/src/components/Home/HostKitchen/HostKitchen.js b/src/components/Home/HostKitchen/HostKitchen.js deleted file mode 100644 index 592316d..0000000 --- a/src/components/Home/HostKitchen/HostKitchen.js +++ /dev/null @@ -1,183 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Box, useTheme, useMediaQuery } from '@mui/material'; -import KitchPlusAppBar from '../AppBar'; -import Sidebar from '../SideHome'; -import PostSubmission from './contcet/PostSubmission'; -import CloudKitchenHosting from './contcet/CloudKitchenHosting'; -import Infrastructure from './contcet/Infrastructure'; -import RstaurantOperations from './contcet/RstaurantOperations'; -import Facilitation from './contcet/Facilitation'; -import Expansion from './contcet/Expansion'; - -import SideProfile from './SideProfile'; - -const drawerWidth = 230; - -const HostKitchen = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const [hasProducts, setHasProducts] = useState(false); - const [sidebarOpen, setSidebarOpen] = useState(!isMobile); - - // ⬇️ إدارة الخطوة الحالية - const [currentStep, setCurrentStep] = useState(0); - - const steps = [ - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - - ]; - - useEffect(() => { - const checkProducts = async () => { - const productsExist = await checkIfProductsExist(); - setHasProducts(productsExist); - }; - checkProducts(); - }, []); - - const checkIfProductsExist = async () => { - return false; - }; - - useEffect(() => { - if (window.innerWidth >= theme.breakpoints.values.md) { - setSidebarOpen(true); - } else { - setSidebarOpen(false); - } - }, [theme.breakpoints.values.md]); - - useEffect(() => { - const handleResize = () => { - if (window.innerWidth >= theme.breakpoints.values.md) { - setSidebarOpen(true); - } else { - setSidebarOpen(false); - } - }; - - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [theme.breakpoints.values.md]); - - const handleDrawerToggle = () => { - setSidebarOpen(!sidebarOpen); - }; - - return ( - - - - - - {/* - - */} - - - - - - setCurrentStep(prev => Math.max(prev - 1, 0))} - /> - - - - - - - - {steps[currentStep]} - - - - - - - ); -}; - -export default HostKitchen; diff --git a/src/components/Home/HostKitchen/contcet/CloudKitchenHosting.js b/src/components/Home/HostKitchen/contcet/CloudKitchenHosting.js deleted file mode 100644 index 19030da..0000000 --- a/src/components/Home/HostKitchen/contcet/CloudKitchenHosting.js +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material'; - -const CloudKitchenHosting = ({ currentStepIndex = 0, onNext, onBack }) => { - const theme = useTheme(); - - return ( - - - - Cloud Kitchen Hosting - - - - Enter your basic information to proceed to registration of your own restaurant on this platform - - - - {/* Inputs (Restaurant Name, Type, Address, City, Postal Code) */} - {[ - { label: 'Restaurant Name', placeholder: 'Al-Baik Foods' }, - { label: 'Restaurant Location', placeholder: 'Street 123, Jordan' }, - { label: 'Availabel Space', placeholder: '300 sq. feet' }, - { label: 'Number of Employees', placeholder: '200' }, - ].map((field, index) => ( - - - {field.label} - - - - ))} - - - - - - {/* زر Back تحت زر Next */} - - - - - - ); -}; - -export default CloudKitchenHosting; diff --git a/src/components/Home/HostKitchen/contcet/Expansion.js b/src/components/Home/HostKitchen/contcet/Expansion.js deleted file mode 100644 index af7596f..0000000 --- a/src/components/Home/HostKitchen/contcet/Expansion.js +++ /dev/null @@ -1,233 +0,0 @@ -import React, { useState } from 'react'; -import { - Box, Typography, Stack, Button, useTheme, TextField, Radio, - RadioGroup, - FormControlLabel, -} from '@mui/material'; -import ConfirmationDialog from './ConfirmationDialog'; // ✅ استدعاء المودال المنفصل - -const Expansion = ({ currentStepIndex = 0, onNext, onBack }) => { - const theme = useTheme(); - const [openModal, setOpenModal] = useState(false); - - const handleOpenModal = () => setOpenModal(true); - const handleCloseModal = () => setOpenModal(false); - const handleConfirmNext = () => { - handleCloseModal(); - onNext(); - }; - return ( - - - - Expansion & Future Cooperation - - - - Enter your basic information to proceed to registration of your own restaurant on this platform - - - - - Hosting Multiple Cloud Kitchens - - - - } - label="Yes" - /> - - } - label="No" - /> - - - - - {/* Inputs (Restaurant Name, Type, Address, City, Postal Code) */} - {[ - { label: 'Future Kitchen Plans', placeholder: '20' }, - ].map((field, index) => ( - - - {field.label} - - - - ))} - - - - Partnership & Support - - - - } - label="Yes" - /> - - } - label="No" - /> - - - - - - - {/* زر Back تحت زر Next */} - - - - - {/* ✅ Confirmation Modal */} - - - ); -}; - -export default Expansion; diff --git a/src/components/Home/HostKitchen/contcet/Facilitation.js b/src/components/Home/HostKitchen/contcet/Facilitation.js deleted file mode 100644 index 2d0360a..0000000 --- a/src/components/Home/HostKitchen/contcet/Facilitation.js +++ /dev/null @@ -1,260 +0,0 @@ -import React from 'react'; -import { - Box, Typography, Stack, Button, useTheme, TextField, Radio, - RadioGroup, - FormControlLabel, -} from '@mui/material'; - -const Facilitation = ({ currentStepIndex = 0, onNext, onBack }) => { - const theme = useTheme(); - - return ( - - - - Facilitation & Cooperation - - - - Enter your basic information to proceed to registration of your own restaurant on this platform - - - - - - - Additional Services Provided - - - - } - label="Yes" - /> - - } - label="No" - /> - - - - - {/* Additional Services Input */} - - - Additional Services - - - - - - - - - Ventilation and Cooling System - - - - } - label="Yes" - /> - - } - label="No" - /> - - - - - {/* Inputs (Restaurant Name, Type, Address, City, Postal Code) */} - {[ - { label: 'Working Hours', placeholder: '8 hours' }, - ].map((field, index) => ( - - - {field.label} - - - - ))} - - - - - - {/* زر Back تحت زر Next */} - - - - - - ); -}; - -export default Facilitation; diff --git a/src/components/Home/HostKitchen/contcet/Infrastructure.js b/src/components/Home/HostKitchen/contcet/Infrastructure.js deleted file mode 100644 index a5010aa..0000000 --- a/src/components/Home/HostKitchen/contcet/Infrastructure.js +++ /dev/null @@ -1,214 +0,0 @@ -import React from 'react'; -import { - Box, Typography, Stack, Button, useTheme, TextField, Radio, - RadioGroup, - FormControlLabel, -} from '@mui/material'; - -const Infrastructure = ({ currentStepIndex = 0, onNext, onBack }) => { - const theme = useTheme(); - - return ( - - - - Infrastructure & Equipments - - - - Enter your basic information to proceed to registration of your own restaurant on this platform - - - - {/* Inputs (Restaurant Name, Type, Address, City, Postal Code) */} - {[ - { label: 'Current Equipment', placeholder: 'oven, refrgerators' }, - ].map((field, index) => ( - - - {field.label} - - - - ))} - - - - - Additional Facilities Needed - - - - } - label="Yes" - /> - - } - label="No" - /> - - - - - {/* Description Input */} - - - Description - - - - - - - - {/* زر Back تحت زر Next */} - - - - - - ); -}; - -export default Infrastructure; diff --git a/src/components/Home/HostKitchen/contcet/PostSubmission.js b/src/components/Home/HostKitchen/contcet/PostSubmission.js deleted file mode 100644 index 7a4cc1b..0000000 --- a/src/components/Home/HostKitchen/contcet/PostSubmission.js +++ /dev/null @@ -1,230 +0,0 @@ -import React from 'react'; -import { Box, Button, Card, CardContent, Stack, Typography, useMediaQuery } from '@mui/material'; -import AirportShuttleIcon from '@mui/icons-material/AirportShuttle'; -import CampaignIcon from '@mui/icons-material/Campaign'; -import Inventory2Icon from '@mui/icons-material/Inventory2'; -import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard'; -import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; -import { useTheme } from '@mui/material/styles'; -import AddIcon from '@mui/icons-material/Add'; - - -const IconCircle = ({ children, bgColor = '#DEDEFA', outerColor = '#EFEFFD' }) => ( - - - - {children} - - -); - -const StatusCard = ({ - icon, - statusText, - statusColor, - iconColor, - outerColor, - innerColor, - height = { - xs: 'calc(140px + 0.5vh)', - sm: 'calc(150px + 0.5vh)', - md: 'calc(162px + 0.5vh)' - }, - width = { - xs: 'min(90%, 600px)', // تقليل من 90% إلى 85% والحد الأقصى من 300px إلى 280px - sm: 'clamp(220px, 23vw, 280px)', // تقليل جميع القيم - md: 'clamp(220px, 19vw, 300px)' - }, - transition = 'width 0.3s ease', - iconSpacing = { xs: 6, sm: 8, md: 12 }, - extraButton, - statusButtonWidth, - title = 'Point Of Sale', -}) => { - const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - - return ( - - - - - {React.cloneElement(icon, { sx: { color: iconColor, fontSize: { xs: 16, sm: 18, md: 20 } } })} - - - - - - {title} - - - - {extraButton ? extraButton : } - - - - - ); -}; - -const PostSubmission = () => { - const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - const isMediumScreen = useMediaQuery(theme.breakpoints.between('sm', 'md')); - - return ( - <> - {/* Header Section */} - - {/* Title */} - - Post Submission - - - {/* Buttons */} - - - - - - - - - ); -}; - -export default PostSubmission; diff --git a/src/components/Home/HostKitchen/contcet/RstaurantOperations.js b/src/components/Home/HostKitchen/contcet/RstaurantOperations.js deleted file mode 100644 index 67dbb66..0000000 --- a/src/components/Home/HostKitchen/contcet/RstaurantOperations.js +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material'; - -const CloudKitchenHosting = ({ currentStepIndex = 0, onNext, onBack }) => { - const theme = useTheme(); - - return ( - - - - Rstaurant Operations - - - - Enter your basic information to proceed to registration of your own restaurant on this platform - - - - {/* Inputs (Restaurant Name, Type, Address, City, Postal Code) */} - {[ - { label: 'Monthly Sales Value', placeholder: '$400' }, - { label: 'Operating Size (Customers/Day)', placeholder: '20' }, - { label: 'Peak Hours', placeholder:'Breakfast' }, - { label: 'Preferred Cuisine to Host', placeholder: 'Fast Food' }, - ].map((field, index) => ( - - - {field.label} - - - - ))} - - - - - - {/* زر Back تحت زر Next */} - - - - - - ); -}; - -export default CloudKitchenHosting; diff --git a/src/components/Home/Inventory/Inventory.js b/src/components/Home/Inventory/Inventory.js index dd91307..376e029 100644 --- a/src/components/Home/Inventory/Inventory.js +++ b/src/components/Home/Inventory/Inventory.js @@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react'; import { Box, useTheme, useMediaQuery } from '@mui/material'; import KitchPlusAppBar from '../AppBar'; import Sidebar from '../SideHome'; -import ProductEntry from './ProductEntry'; -import TableView from './TableView'; +import ProductEntry from './contect/ProductEntry'; +import TableView from './contect/TableView'; const drawerWidth = 230; @@ -53,9 +53,9 @@ const Inventory = () => { status: newProduct.consumptionStatus ? 'Published' : 'Draft', expiration: newProduct.expirationDate ? `${Math.ceil( - (new Date(newProduct.expirationDate) - new Date()) / - (1000 * 60 * 60 * 24) - )} Days Left` + (new Date(newProduct.expirationDate) - new Date()) / + (1000 * 60 * 60 * 24) + )} Days Left` : 'N/A', }; diff --git a/src/components/Home/Inventory/InventoryTablePage.js b/src/components/Home/Inventory/contect/InventoryTablePage.js similarity index 99% rename from src/components/Home/Inventory/InventoryTablePage.js rename to src/components/Home/Inventory/contect/InventoryTablePage.js index f8672b2..a0c2e77 100644 --- a/src/components/Home/Inventory/InventoryTablePage.js +++ b/src/components/Home/Inventory/contect/InventoryTablePage.js @@ -30,7 +30,7 @@ const initialProducts = [ const InventoryTablePage = () => { const [products, setProducts] = useState(initialProducts); - + const handleAddNewProduct = () => { alert('Navigate to Add Product page or open form here'); }; diff --git a/src/components/Home/Inventory/NoProdectDash.js b/src/components/Home/Inventory/contect/NoProdectDash.js similarity index 100% rename from src/components/Home/Inventory/NoProdectDash.js rename to src/components/Home/Inventory/contect/NoProdectDash.js diff --git a/src/components/Home/Inventory/ProductEntry.js b/src/components/Home/Inventory/contect/ProductEntry.js similarity index 98% rename from src/components/Home/Inventory/ProductEntry.js rename to src/components/Home/Inventory/contect/ProductEntry.js index 857f2d1..7f0779f 100644 --- a/src/components/Home/Inventory/ProductEntry.js +++ b/src/components/Home/Inventory/contect/ProductEntry.js @@ -416,9 +416,9 @@ const ProductEntry = ({ onAdd, onShowTable }) => { color: theme.palette.primary.main, borderColor: theme.palette.primary.main, '&:hover': { - backgroundColor: theme.palette.primary.main, - color: '#fff', - borderColor: theme.palette.primary.dark, + backgroundColor: '#FFECE0', + color: theme.palette.primary.main, + borderColor: '#FFECE0', }, }} > diff --git a/src/components/Home/Inventory/TableView.js b/src/components/Home/Inventory/contect/TableView.js similarity index 100% rename from src/components/Home/Inventory/TableView.js rename to src/components/Home/Inventory/contect/TableView.js diff --git a/src/components/Home/Meal/Meal.js b/src/components/Home/Meal/Meal.js new file mode 100644 index 0000000..04f5292 --- /dev/null +++ b/src/components/Home/Meal/Meal.js @@ -0,0 +1,206 @@ +import React, { useState, useEffect } from 'react'; +import { Box, useTheme, useMediaQuery, Typography } from '@mui/material'; +import KitchPlusAppBar from '../AppBar'; +import Sidebar from '../SideHome'; +import AccountSettings from './contect/AccountSettings'; +import MealsByCateg from './contect/MealsByCateg'; +import AllMeals from './contect/AllMeals'; +import ProductDetail from './contect/ProductDetail'; +import authService from '../../../services/authService'; +import { useRestaurant } from '../../../contexts/RestaurantContext'; + +const drawerWidth = 230; + +const Meal = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [sidebarOpen, setSidebarOpen] = useState(!isMobile); + const { restaurantId } = useRestaurant(); + + const [showAllPopular, setShowAllPopular] = useState(false); + const [categories, setCategories] = useState([]); + const [selectedCategoryId, setSelectedCategoryId] = useState(null); + const [categoryName, setCategoryName] = useState(''); + const [selectedProduct, setSelectedProduct] = useState(null); + const [meals, setMeals] = useState([]); + const [loadingMeals, setLoadingMeals] = useState(false); + + // جلب الفئات عند التحميل + useEffect(() => { + const fetchCategories = async () => { + if (!restaurantId) return; + const result = await authService.getCategoriesByRestaurant(restaurantId); + if (result.success) setCategories(result.data); + else setCategories([]); + }; + fetchCategories(); + }, [restaurantId]); + + // تحديث اسم الفئة عند تغيير selectedCategoryId أو categories + useEffect(() => { + if (selectedCategoryId && categories.length > 0) { + const selected = categories.find(c => c.id === selectedCategoryId); + setCategoryName(selected ? selected.name : ''); + } else { + setCategoryName(''); + } + }, [selectedCategoryId, categories]); + + // جلب الوجبات عند اختيار الفئة + useEffect(() => { + const fetchMeals = async () => { + if (selectedCategoryId && restaurantId) { + setLoadingMeals(true); + try { + const result = await authService.getMealsByCategory(restaurantId, selectedCategoryId); + if (result.success) { + const selectedCategory = categories.find(c => c.id === selectedCategoryId); + const mealsWithImages = result.data.map(meal => ({ + id: meal.id, + name: meal.name, + price: meal.price, + unit: meal.unit || '/pcs', + image: meal.photo || '/images/default-product.png', + category: selectedCategory ? selectedCategory.name : '', // اسم الفئة + category_id: meal.category_id, + restaurant_id: meal.restaurant_id, + description: meal.description || [], + additions: meal.additions || [] + })); + setMeals(mealsWithImages); + } else setMeals([]); + } catch (err) { + console.error(err); + setMeals([]); + } finally { + setLoadingMeals(false); + } + } else setMeals([]); + }; + fetchMeals(); + }, [selectedCategoryId, restaurantId, categories]); + + const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen); + const toggleShowAllPopular = () => setShowAllPopular(prev => !prev); + + return ( + + + + + + + + {selectedProduct ? ( + setSelectedProduct(null)} + /> + ) : ( + <> + + + {selectedCategoryId ? ( + + + setSelectedCategoryId(null)} + onProductClick={(meal) => { + const category = categories.find(c => c.id === meal.category_id); + setSelectedProduct({ + ...meal, + category: category ? category.name : '', + category_id: meal.category_id, + restaurant_id: restaurantId + }); + }} + /> + + + ) : !showAllPopular ? ( + + + { + const category = categories.find(c => c.id === meal.category_id); + setSelectedProduct({ + ...meal, + category: category ? category.name : '', + category_id: meal.category_id, + restaurant_id: restaurantId + }); + }} + /> + + + ) : null} + + )} + + + + ); +}; + +export default Meal; \ No newline at end of file diff --git a/src/components/Home/Meal/contect/AccountSettings.js b/src/components/Home/Meal/contect/AccountSettings.js new file mode 100644 index 0000000..42db264 --- /dev/null +++ b/src/components/Home/Meal/contect/AccountSettings.js @@ -0,0 +1,330 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { + Box, + Typography, + Button, + IconButton, + Menu, + MenuItem, + Modal, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + useTheme, + useMediaQuery, + Skeleton, +} from '@mui/material'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import CategoryScrollList from './CategoryScrollList'; +import AddCategory from './AddCategory'; +import authService from '../../../../services/authService'; + +const AccountSettings = ({ selectedCategory, setSelectedCategory, restaurantId }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); // <-- حالة التحميل + const [selectedCategoryForEdit, setSelectedCategoryForEdit] = useState(null); + const [contextMenu, setContextMenu] = useState(null); + const [openAddModal, setOpenAddModal] = useState(false); + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + + const scrollRef = useRef(); + + // تحميل الفئات عند فتح الصفحة + useEffect(() => { + const fetchCategories = async () => { + setLoading(true); // بدء التحميل + const result = await authService.getCategoriesByRestaurant(restaurantId); + if (result.success) setCategories(result.data); + else console.error(result.message); + setLoading(false); // انتهاء التحميل + }; + if (restaurantId) fetchCategories(); + }, [restaurantId]); + + useEffect(() => { + if (selectedCategory) localStorage.setItem('selectedCategoryId', selectedCategory); + }, [selectedCategory]); + + const scroll = (offset) => { + if (scrollRef.current) scrollRef.current.scrollLeft += offset; + }; + + const handleAddCategory = async (payload) => { + try { + const response = await authService.addCategory(payload); + setCategories((prev) => [...prev, response.category]); + setOpenAddModal(false); + } catch (error) { + console.error('خطأ أثناء الإضافة:', error); + } + }; + + const handleUpdateCategory = async (payload) => { + try { + const response = await authService.updateCategory(selectedCategoryForEdit.id, payload); + setCategories((prev) => + prev.map((cat) => (cat.id === selectedCategoryForEdit.id ? response.category : cat)) + ); + setOpenAddModal(false); + } catch (error) { + console.error('خطأ أثناء التعديل:', error); + } + }; + + const handleDeleteCategory = async () => { + try { + await authService.deleteCategory(selectedCategoryForEdit.id); + setCategories((prev) => + prev.filter((cat) => cat.id !== selectedCategoryForEdit.id) + ); + setConfirmDeleteOpen(false); + setSelectedCategoryForEdit(null); + + if (selectedCategory === selectedCategoryForEdit?.id) { + setSelectedCategory(null); + localStorage.removeItem('selectedCategoryId'); + } + } catch (error) { + console.error('خطأ أثناء الحذف:', error); + } + }; + + useEffect(() => { + if (!openAddModal) setSelectedCategoryForEdit(null); + }, [openAddModal]); + + return ( + + {/* رأس القسم */} + + + Categories + + + + + + scroll(-200)} + sx={{ + backgroundColor: theme.palette.primary.main, + color: '#fff', + width: 40, + height: 40, + '&:hover': { backgroundColor: '#ffddbfff' }, + }} + > + + + + scroll(200)} + sx={{ + backgroundColor: theme.palette.primary.main, + color: '#fff', + width: 40, + height: 40, + '&:hover': { backgroundColor: '#ffddbfff' }, + }} + > + + + + + + {/* قائمة الفئات */} + {/* قائمة الفئات */} + + {loading ? ( + // Skeleton Loader + Array.from({ length: 5 }).map((_, index) => ( + + )) + ) : categories.length === 0 ? ( + + + No categories available + + + ) : ( + + )} + + + + {/* مودال الإضافة / التعديل */} + setOpenAddModal(false)} + aria-labelledby="add-category-modal" + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + p: 2, + }} + > + + + + + + {/* قائمة السياق */} + setContextMenu(null)} + anchorReference="anchorPosition" + anchorPosition={ + contextMenu !== null + ? { top: contextMenu.mouseY, left: contextMenu.mouseX } + : undefined + } + sx={{ zIndex: 2000 }} + > + { + setOpenAddModal(true); + setContextMenu(null); + }} + > + Edit Category + + { + setConfirmDeleteOpen(true); + setContextMenu(null); + }} + sx={{ color: 'red' }} + > + Delete Category + + + + {/* تأكيد الحذف */} + setConfirmDeleteOpen(false)}> + Confirm Delete + + Are you sure you want to delete this category? + + + + + + + + ); +}; + +export default AccountSettings; diff --git a/src/components/Home/Meal/contect/AddCategory.js b/src/components/Home/Meal/contect/AddCategory.js new file mode 100644 index 0000000..30d487a --- /dev/null +++ b/src/components/Home/Meal/contect/AddCategory.js @@ -0,0 +1,102 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Button, TextField, Typography } from '@mui/material'; +import { useRestaurant } from '../../../../contexts/RestaurantContext'; + +const AddCategory = ({ onAdd, editingCategory }) => { + const { restaurantId } = useRestaurant(); + const [productName, setProductName] = useState(''); + const [description, setDescription] = useState(''); + const [price, setPrice] = useState(''); + const [discount, setDiscount] = useState(''); + const [productImage, setProductImage] = useState(null); + + useEffect(() => { + if (editingCategory) { + setProductName(editingCategory.name || ''); + setDescription(editingCategory.description || ''); + setPrice(editingCategory.price || ''); + setDiscount(editingCategory.discount || ''); + setProductImage(editingCategory.icon || null); + } + }, [editingCategory]); + + const handleImageUpload = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => setProductImage(reader.result); + reader.readAsDataURL(file); + } + }; + + const handleSubmit = () => { + if (!productName) { + alert("Please enter category name"); + return; + } + + if (!restaurantId) { + alert("Restaurant ID not set"); + return; + } + + const payload = { + name: productName, + restaurant_id: restaurantId, + description, + price, + discount, + icon: productImage, + }; + + // فقط أرسل البيانات للمكون الأب + if (onAdd) onAdd(payload); + }; + + return ( + + + Category Name + + setProductName(e.target.value)} + margin="normal" + sx={{ + '& input': { fontWeight: 500, fontSize: '15px' }, + '& input::placeholder': { color: '#969BA7' }, + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + transition: '0.3s', + '&.Mui-focused fieldset': { + borderColor: '#FF914D', + boxShadow: '0 0 0 2px rgba(255,145,77,0.2)', + }, + }, + }} + /> + + + + ); +}; + +export default AddCategory; diff --git a/src/components/Home/Meal/contect/AddMeal.js b/src/components/Home/Meal/contect/AddMeal.js new file mode 100644 index 0000000..d998e24 --- /dev/null +++ b/src/components/Home/Meal/contect/AddMeal.js @@ -0,0 +1,251 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Typography, + Box, + Button, + IconButton, + InputAdornment, +} from '@mui/material'; +import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined'; +import DeleteIcon from '@mui/icons-material/Delete'; +import authService from '../../../../services/authService'; +import AddIcon from '@mui/icons-material/Add'; + +const MAX_FILE_SIZE_MB = 2; +const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/jpg']; + +const AddMeal = ({ open, onClose, onAdd, selectedCategory, restaurantId }) => { + const [mealName, setMealName] = useState(''); + const [description, setDescription] = useState(''); + const [additions, setAdditions] = useState(['']); + const [price, setPrice] = useState(''); + const [mealImage, setMealImage] = useState(null); + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (!open) { + setMealName(''); + setDescription(''); + setAdditions(['']); + setPrice(''); + setMealImage(null); + setErrors({}); + } + }, [open]); + + const handleImageUpload = (e) => { + const file = e.target.files[0]; + if (!file) return; + + if (!ALLOWED_TYPES.includes(file.type)) { + setErrors(prev => ({ ...prev, image: 'Allowed types: jpg, jpeg, png' })); + return; + } + if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) { + setErrors(prev => ({ ...prev, image: `Image must be smaller than ${MAX_FILE_SIZE_MB} MB` })); + return; + } + + setMealImage(file); + setErrors(prev => ({ ...prev, image: '' })); + }; + + const handleAddAddition = () => setAdditions([...additions, '']); + const handleRemoveAddition = (index) => setAdditions(additions.filter((_, i) => i !== index)); + const handleChangeAddition = (index, value) => { + const newAdditions = [...additions]; + newAdditions[index] = value; + setAdditions(newAdditions); + }; + + const validate = () => { + const newErrors = {}; + if (!mealName.trim()) newErrors.name = 'Meal name is required'; + if (!description.trim()) newErrors.description = 'Description is required'; + if (!price || isNaN(price) || Number(price) <= 0) newErrors.price = 'Valid price is required'; + if (!mealImage) newErrors.image = 'Meal image is required'; + if (!selectedCategory) newErrors.category = 'Category must be selected'; + additions.forEach((add, i) => { + if (!add.trim()) newErrors[`addition_${i}`] = 'Cannot be empty'; + }); + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + +const handleSubmit = async () => { + if (!validate()) return; + setIsSubmitting(true); + + const mealData = { + name: mealName.trim(), + category_id: selectedCategory, + price, + restaurant_id: restaurantId, + photo: mealImage, + description: [description.trim()], + additions: additions.filter(a => a.trim() !== ''), + }; + + try { + const result = await authService.addMeal(mealData); + + // عرض رسالة Snackbar أو alert + alert(result.message); + + // تمرير الوجبة الجديدة للمكون الأب لتحديث القائمة + if (result.data && onAdd) { + const newMeal = { + id: result.data.id, + name: result.data.name, + price: result.data.price, + description: result.data.description || [], + additions: result.data.additions || [], + unit: result.data.unit || "/pcs", + photo: result.data.photo || "/images/default-product.png", + category_id: result.data.category_id || selectedCategory, + }; + onAdd(newMeal); + } + + // إغلاق المودال بعد الإضافة + onClose(); + + } catch (error) { + console.error('Add meal error:', error); + alert(error.message || "Something went wrong"); + } finally { + setIsSubmitting(false); + } +}; + + + + return ( + + Add Meal + + + + {/* Meal Image */} + Meal Image + + {mealImage ? ( + + Preview + + + ) : ( + <> + Upload Meal Picture + + + )} + + + {errors.image && {errors.image}} + + {/* Meal Name */} + Meal Name + setMealName(e.target.value)} error={!!errors.name} helperText={errors.name} sx={{ mb: 2 }} /> + + {/* Description */} + Description + setDescription(e.target.value)} error={!!errors.description} helperText={errors.description} sx={{ mb: 2 }} /> + + {/* Additions */} + Additions + {additions.map((add, i) => ( + + handleChangeAddition(i, e.target.value)} + error={!!errors[`addition_${i}`]} + helperText={errors[`addition_${i}`]} + /> + handleRemoveAddition(i)} color="error"> + + ))} + + + {/* Price */} + Price + setPrice(e.target.value)} error={!!errors.price} helperText={errors.price} sx={{ mb: 2 }} /> + + + + + + + + ); +}; + +export default AddMeal; diff --git a/src/components/Home/Meal/contect/AllMeals.js b/src/components/Home/Meal/contect/AllMeals.js new file mode 100644 index 0000000..d31ec3b --- /dev/null +++ b/src/components/Home/Meal/contect/AllMeals.js @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from "react"; +import { Box, Typography, Skeleton, useTheme } from "@mui/material"; +import MealCard from "./MealCard"; +import authService from "../../../../services/authService"; +import SimplePagination from "../../SimplePagination"; + +const AllMeals = ({ restaurantId, categories = [], onProductClick }) => { + const theme = useTheme(); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const [pagination, setPagination] = useState(null); + + const itemsPerPage = 6; + + useEffect(() => { + if (!restaurantId) return; + + const fetchMeals = async () => { + setLoading(true); + setError(null); + try { + const result = await authService.getAllMealsByRestaurant(restaurantId, page); + + if (result.success) { + const mealsWithImages = result.data.map(meal => { + const category = categories.find(c => c.id === meal.category_id); + return { + id: meal.id, + name: meal.name, + price: meal.price, + description: meal.description || [], + additions: meal.additions || [], + unit: meal.unit || "/pcs", + photo: meal.photo || meal.photo_url || "/images/default-product.png", + category_id: meal.category_id, + category: category ? category.name : null, + }; + }); + + setProducts(mealsWithImages); + setPagination(result.pagination); + } else { + setProducts([]); + setPagination(null); + } + } catch (err) { + console.error(err); + setError("Failed to fetch meals."); + } finally { + setLoading(false); + } + }; + + fetchMeals(); + }, [restaurantId, page, categories]); + + return ( + + + All Meals + + + {loading ? ( + + {Array.from({ length: itemsPerPage }).map((_, index) => ( + + + + + + ))} + + ) : error ? ( + + {error} + + ) : products.length === 0 ? ( + + There is no meal available + + ) : ( + + {products.map(product => ( + onProductClick(product)} > + onProductClick(product)} + /> + + ))} + + )} + + {/* Pagination Footer */} + {pagination && !loading && ( + + + Showing {(page - 1) * itemsPerPage + 1} to {Math.min(page * itemsPerPage, products.length)} of {pagination.total} meals + + + setPage(newPage)} + /> + + )} + + ); +}; + +export default AllMeals; diff --git a/src/components/Home/Meal/contect/CategoryScrollList.js b/src/components/Home/Meal/contect/CategoryScrollList.js new file mode 100644 index 0000000..827b42e --- /dev/null +++ b/src/components/Home/Meal/contect/CategoryScrollList.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; + +const CategoryScrollList = ({ + categories, + selectedCategory, + setSelectedCategory, + setSelectedCategoryForEdit, + setContextMenu, + scrollRef, +}) => { + const theme = useTheme(); + + return ( + + {categories.map((cat) => { + const isSelected = selectedCategory === cat.id; + + return ( + setSelectedCategory(cat.id)} + onContextMenu={(e) => { + e.preventDefault(); + setSelectedCategoryForEdit(cat); + setContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6 }); + }} + sx={{ + p: 1, + borderRadius: '12px', + backgroundColor: isSelected ? theme.palette.primary.main : '#F9F9FC', + border: isSelected ? `0px solid ${theme.palette.primary.main}` : '0px solid #ddd', + textAlign: 'center', + minWidth: '70px', + cursor: 'pointer', + flexShrink: 0, + transition: '0.3s', + '&:hover': { + backgroundColor: isSelected ? theme.palette.primary.dark : '#EDEDED', + }, + }} + > + + {cat.name} + + + ); + })} + + ); +}; + +export default CategoryScrollList; diff --git a/src/components/Home/Meal/contect/MealCard.js b/src/components/Home/Meal/contect/MealCard.js new file mode 100644 index 0000000..5871767 --- /dev/null +++ b/src/components/Home/Meal/contect/MealCard.js @@ -0,0 +1,138 @@ +import React from "react"; +import { + Box, + Typography, + Card, + CardContent, + CardMedia, + IconButton, + Chip, +} from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; + +const MealCard = ({ meal, onClick, onAdd }) => { + const { name, price, unit, photo, image, description, additions } = meal; + + + + return ( + + {/* صورة الوجبة */} + + + {/* تفاصيل الوجبة */} + + + {name} + + + {/* السعر */} + + ${price}{" "} + + {unit || "/pcs"} + + + + {/* الوصف (أول عنصر فقط للتصغير) */} + {description && description.length > 0 && ( + + {description[0]} + + )} + + {/* الإضافات (أول 2 فقط) */} + {/* {additions && additions.length > 0 && ( + + {additions.slice(0, 2).map((add, i) => ( + + ))} + + )} */} + + {/* زر الإضافة */} + {/* { + e.stopPropagation(); + if (onAdd) onAdd(meal); + }} + sx={{ + backgroundColor: "#FF8551", + color: "#fff", + mt: 1, + width: 28, + height: 28, + "&:hover": { backgroundColor: "#ff7043" }, + }} + > + + */} + + + + ); +}; + +export default MealCard; diff --git a/src/components/Home/Meal/contect/MealsByCateg.js b/src/components/Home/Meal/contect/MealsByCateg.js new file mode 100644 index 0000000..53a59de --- /dev/null +++ b/src/components/Home/Meal/contect/MealsByCateg.js @@ -0,0 +1,202 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { Box, Typography, Skeleton, useTheme, Button } from "@mui/material"; +import MealCard from "./MealCard"; +import authService from "../../../../services/authService"; +import AddMeal from "./AddMeal"; +import SimplePagination from "../../SimplePagination"; + +const MealsByCateg = ({ + restaurantId, + categoryId, + onProductClick, + onAddMeal, + categoryName, + onBack, +}) => { + const theme = useTheme(); + + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [openModal, setOpenModal] = useState(false); + const [page, setPage] = useState(1); + const [pagination, setPagination] = useState(null); + + const itemsPerPage = 6; + + // استخدام useCallback لمنع إنشاء الدالة عند كل إعادة render + const fetchMeals = useCallback(async () => { + if (!restaurantId || !categoryId) return; + setLoading(true); + setError(null); + try { + const result = await authService.getMealsByCategory( + restaurantId, + categoryId, + page + ); + + if (result.success) { + const mealsWithImages = result.data.map((meal) => ({ + id: meal.id, + name: meal.name, + price: meal.price, + description: meal.description || [], + additions: meal.additions || [], + unit: meal.unit || "/pcs", + photo: meal.photo || "/images/default-product.png", + category_id: meal.category_id || categoryId, + })); + setProducts(mealsWithImages); + setPagination(result.pagination); + } else { + setProducts([]); + setPagination(null); + } + } catch (err) { + console.error(err); + setError("Failed to fetch meals."); + setProducts([]); + setPagination(null); + } finally { + setLoading(false); + } + }, [restaurantId, categoryId, page]); + + // جلب البيانات عند تغيير restaurantId أو categoryId أو page + useEffect(() => { + fetchMeals(); + }, [fetchMeals]); + + // إضافة وجبة جديدة وتحديث القائمة فورًا +// في MealsByCateg +const handleAddProduct = async (meal) => { + setOpenModal(false); // اغلاق مودال الإضافة + setPage(1); // العودة للصفحة الأولى + const result = await fetchMeals(); // جلب الوجبات بعد الإضافة + if (onAddMeal) onAddMeal(meal); // تمرير الوجبة للمكون الأب +}; + + return ( + + {/* العنوان + أزرار */} + + + {categoryName || "Meals"} + + + + + + + + {/* حالة التحميل أو الخطأ */} + {loading ? ( + + {Array.from({ length: itemsPerPage }).map((_, index) => ( + + + + + + ))} + + ) : error ? ( + + {error} + + ) : products.length === 0 ? ( + + + There is no meal available + + + ) : ( + + {products.map((product) => ( + + onProductClick({ + ...product, + category: categoryName, + category_id: categoryId, + }) + } + > + + onProductClick({ + ...product, + category: categoryName, + category_id: categoryId, + }) + } + onAdd={onAddMeal} + /> + + ))} + + )} + + {/* Pagination Footer */} + {pagination && !loading && pagination.last_page > 1 && ( + + + Showing {(page - 1) * itemsPerPage + 1} to{" "} + {Math.min(page * itemsPerPage, pagination.total)} of {pagination.total} meals + + + setPage(newPage)} + /> + + )} + + {/* Add Meal Modal */} + setOpenModal(false)} + onAdd={handleAddProduct} + selectedCategory={categoryId} + restaurantId={restaurantId} + /> + + ); +}; + +export default MealsByCateg; diff --git a/src/components/Home/Meal/contect/ProductDetail.js b/src/components/Home/Meal/contect/ProductDetail.js new file mode 100644 index 0000000..b09ce98 --- /dev/null +++ b/src/components/Home/Meal/contect/ProductDetail.js @@ -0,0 +1,551 @@ +import React, { useState, useEffect } from "react"; +import { + Box, + Typography, + Button, + TextField, + Chip, + LinearProgress, + useTheme, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + FormControl, + Select, + MenuItem, + FormHelperText, +} from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; +import ClearIcon from "@mui/icons-material/Clear"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import authService from "../../../../services/authService"; + +const ProductDetail = ({ product, onBack }) => { + const theme = useTheme(); + const [isEditing, setIsEditing] = useState(false); + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + const [formData, setFormData] = useState({ + name: "", + category_id: "", + restaurant_id: "", + description: [], + additions: [], + price: "", + photoFile: null, + }); + + const [newDescription, setNewDescription] = useState(""); + const [newAddition, setNewAddition] = useState(""); + const [isSaving, setIsSaving] = useState(false); + const [errors, setErrors] = useState({}); + const [categories, setCategories] = useState([]); + + useEffect(() => { + if (product) { + setFormData({ + name: product.name || "", + category_id: product.category_id || "", + restaurant_id: product.restaurant_id || "", + description: product.description || [], + additions: product.additions || [], + price: product.price || "", + photoFile: null, + }); + } + + const fetchCategories = async () => { + try { + const result = await authService.getCategoriesByRestaurant(product?.restaurant_id); + if (result.success) setCategories(result.data); + } catch (error) { + console.error("Error fetching categories:", error); + } + }; + + if (product?.restaurant_id) fetchCategories(); + }, [product]); + + const validateForm = () => { + const newErrors = {}; + if (!formData.name.trim()) newErrors.name = "Name is required"; + if (!formData.category_id) newErrors.category_id = "Category is required"; + if (!formData.price || isNaN(formData.price) || formData.price <= 0) { + newErrors.price = "Valid price is required"; + } + if (formData.description.length === 0) newErrors.description = "At least one description is required"; + if (formData.additions.length === 0) newErrors.additions = "At least one addition is required"; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + if (errors[name]) setErrors((prev) => ({ ...prev, [name]: "" })); + }; + + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (!file) return; + + if (!file.type.startsWith('image/')) { + setErrors((prev) => ({ ...prev, photoFile: "Please select an image file" })); + return; + } + + if (file.size > 5 * 1024 * 1024) { + setErrors((prev) => ({ ...prev, photoFile: "File size should be less than 5MB" })); + return; + } + + setFormData((prev) => ({ ...prev, photoFile: file })); + setErrors((prev) => ({ ...prev, photoFile: "" })); + }; + + const handleAddDescription = () => { + if (newDescription.trim() !== "") { + setFormData((prev) => ({ + ...prev, + description: [...prev.description, newDescription.trim()], + })); + setNewDescription(""); + setErrors((prev) => ({ ...prev, description: "" })); + } + }; + + const handleRemoveDescription = (idx) => { + const newDesc = formData.description.filter((_, i) => i !== idx); + setFormData((prev) => ({ ...prev, description: newDesc })); + if (newDesc.length === 0) setErrors((prev) => ({ ...prev, description: "At least one description is required" })); + }; + + const handleAddAddition = () => { + if (newAddition.trim() !== "") { + setFormData((prev) => ({ + ...prev, + additions: [...prev.additions, newAddition.trim()], + })); + setNewAddition(""); + setErrors((prev) => ({ ...prev, additions: "" })); + } + }; + + const handleRemoveAddition = (idx) => { + const newAdds = formData.additions.filter((_, i) => i !== idx); + setFormData((prev) => ({ ...prev, additions: newAdds })); + if (newAdds.length === 0) setErrors((prev) => ({ ...prev, additions: "At least one addition is required" })); + }; + + const handleEditDescription = (idx, value) => { + const newDesc = [...formData.description]; + newDesc[idx] = value; + setFormData((prev) => ({ ...prev, description: newDesc })); + }; + + const handleEditAddition = (idx, value) => { + const newAdds = [...formData.additions]; + newAdds[idx] = value; + setFormData((prev) => ({ ...prev, additions: newAdds })); + }; + + const handleSave = async () => { + if (!validateForm()) return; + + try { + setIsSaving(true); + const updateData = { + name: formData.name.trim(), + category_id: formData.category_id, + restaurant_id: formData.restaurant_id, + description: formData.description, + additions: formData.additions, + price: formData.price, + photo: formData.photoFile ? [formData.photoFile] : [], + }; + + const res = await authService.updateMeal(product.id, updateData); + + // ✅ حدّث state بآخر نسخة من السيرفر + setFormData({ + name: res.meal.name || "", + category_id: res.meal.category_id || "", + restaurant_id: res.meal.restaurant_id || "", + description: res.meal.description || [], + additions: res.meal.additions || [], + price: res.meal.price || "", + photoFile: null, + }); + + setIsEditing(false); + alert("Meal updated successfully!"); + + // ✅ إغلاق المكون بعد النجاح + if (onBack) { + onBack(); + } + + } catch (err) { + console.error(err); + alert("Error updating meal: " + (err.message || "Something went wrong")); + } finally { + setIsSaving(false); + } + }; + + const handleDeleteMeal = async () => { + try { + setIsSaving(true); + await authService.deleteMeal(product.id, product.restaurant_id, product.category_id); + onBack(); + // alert("Meal deleted successfully!"); + } catch (err) { + console.error(err); + alert("Error deleting meal: " + (err.message || "Something went wrong")); + } finally { + setIsSaving(false); + setConfirmDeleteOpen(false); + } + }; + + return ( + + {/* صندوق الصورة */} + + + + + + + + + + {isEditing && ( + + + {errors.photoFile && {errors.photoFile}} + {formData.photoFile && Selected: {formData.photoFile.name}} + + )} + + + {/* التفاصيل */} + + {/* الاسم */} + + {isEditing ? ( + + ) : ( + {product.name} + )} + + + {/* الفئة */} + + {isEditing ? ( + + + {errors.category_id && ( + {errors.category_id} + )} + + ) : ( + + {product.category || + categories.find((c) => c.id === (product.category_id || formData.category_id))?.name || + `Category #${product.category_id || formData.category_id}`} + + )} + + + + {/* السعر */} + + Price + {isEditing ? ( + + ) : ( + + ${Number(product.price).toFixed(2)} {product.unit || "/pcs"} + + )} + + + {/* الوصف */} + + + Description {isEditing && "*"} + + {isEditing ? ( + + {formData.description.map((desc, idx) => ( + + handleEditDescription(idx, e.target.value)} + placeholder={`Description ${idx + 1}`} + size="small" + /> + + ))} + {errors.description && {errors.description}} + + ) : ( + + {formData.description.map((desc, idx) => • {desc})} + + )} + + + {/* الإضافات */} + + + Additions {isEditing && "*"} + + {isEditing ? ( + + {formData.additions.map((add, idx) => ( + + handleEditAddition(idx, e.target.value)} + placeholder={`Addition ${idx + 1}`} + size="small" + /> + handleRemoveAddition(idx)} size="small"> + + + + ))} + + setNewAddition(e.target.value)} + size="small" + /> + + + + + {errors.additions && {errors.additions}} + + ) : ( + + {formData.additions.map((add, idx) => )} + + )} + + + {/* شريط تقدم */} + + + + {/* أزرار التحكم */} + + {isEditing ? ( + <> + + + + + ) : ( + <> + + + + + )} + + + + + {/* مودال تأكيد الحذف */} + setConfirmDeleteOpen(false)}> + Confirm Delete + + + Are you sure you want to delete "{product.name}"? This action cannot be undone. + + + + + + + + + ); +}; + +export default ProductDetail; diff --git a/src/components/Home/Order/Order.js b/src/components/Home/Order/Order.js new file mode 100644 index 0000000..1d67eb4 --- /dev/null +++ b/src/components/Home/Order/Order.js @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from 'react'; +import { Box, useTheme, useMediaQuery } from '@mui/material'; +import KitchPlusAppBar from '../AppBar'; +import Sidebar from '../SideHome'; +import Orders from './contect/Orders'; +import authService from '../../../services/authService'; + +const drawerWidth = 230; + +const Order = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [sidebarOpen, setSidebarOpen] = useState(!isMobile); + + useEffect(() => { + const handleResize = () => { + setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md); + }; + + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [theme.breakpoints.values.md]); + + useEffect(() => { + const admin = authService.getAdminData(); + console.log('Admin Info:', admin); + + const adminId = authService.getAdminId(); + console.log('Admin ID:', adminId); + }, []); + +const admin = authService.getAdminData(); +const adminId = authService.getAdminId(); + const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen); + + return ( + + + + + + + + + + + + ); +}; + +export default Order; diff --git a/src/components/Home/Order/contect/CartView.js b/src/components/Home/Order/contect/CartView.js new file mode 100644 index 0000000..de7a7f8 --- /dev/null +++ b/src/components/Home/Order/contect/CartView.js @@ -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 ; + + return ( + + {/* السهم للعودة */} + + + + + + Current Cart + + + + {cart.length === 0 ? ( + No items in cart. + ) : ( + <> + {cart.map(item => ( + + + {item.name} + Unit: {item.unit} + + + Quantity: {item.quantity} + Total: ${item.totalPrice} + + + ))} + + + + Total Price: ${totalPrice.toFixed(2)} + + + + )} + + {cart.length > 0 && ( + + + + + )} + + ); +}; + +export default CartView; diff --git a/src/components/Home/Order/contect/OrderDetails.js b/src/components/Home/Order/contect/OrderDetails.js new file mode 100644 index 0000000..04c6395 --- /dev/null +++ b/src/components/Home/Order/contect/OrderDetails.js @@ -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 ( + <> + + + Cart #{cart.id} Details + + + {editMode ? ( + <> + setTotalPrice(e.target.value)} + fullWidth + sx={{ mb: 2 }} + /> + + Items: + + {cartItems.map(item => ( + + handleChangeQuantity(item.id, Number(e.target.value))} + sx={{ width: '120px' }} + /> + } + /> + + ))} + + + + + + + + ) : ( + <> + Total Price: {cart.attributes.total_price || cart.attributes.totalPrice} + + Created At: {new Date(cart.attributes.createdAt).toLocaleString()} + + Items: + + {cartItems.map(item => ( + + + + ))} + + + + + + + + + )} + + + {/* Delete Confirmation Dialog */} + setOpenDeleteDialog(false)} + > + Confirm Delete + + + Are you sure you want to delete this cart? This action cannot be undone. + + + + + + + + + ); +}; + +export default CartDetails; \ No newline at end of file diff --git a/src/components/Home/Order/contect/Orders.js b/src/components/Home/Order/contect/Orders.js new file mode 100644 index 0000000..e82ea5a --- /dev/null +++ b/src/components/Home/Order/contect/Orders.js @@ -0,0 +1,326 @@ +import React, { useState, useEffect } from 'react'; +import { + useMediaQuery, + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Skeleton, + CircularProgress, + Button +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import authService from '../../../../services/authService'; +import CartDetails from './OrderDetails'; +import CartView from './CartView'; + +const SimplePagination = ({ currentPage, pageCount, onChange }) => { + const theme = useTheme(); + const handlePrev = () => { if (currentPage > 1) onChange(currentPage - 1); }; + const handleNext = () => { if (currentPage < pageCount) onChange(currentPage + 1); }; + + return ( + + + + + + + {currentPage} + + + = pageCount} + sx={{ + borderRadius: '8px', + backgroundColor: '#FFECE0', + '&:hover': { backgroundColor: '#FFD6B5' }, + color: theme.palette.primary.main, + '&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' }, + }} + > + + + + ); +}; + +const Orders = ({ adminId }) => { + useEffect(() => { + console.log('Admin ID from props:', adminId); + }, [adminId]); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [ordersData, setOrdersData] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedCart, setSelectedCart] = useState(null); + const [cartLoading, setCartLoading] = useState(false); + const [showCartView, setShowCartView] = useState(false); // ← التحكم بعرض CartView هنا + + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 6; + const [orders, setOrders] = useState([]); + + const handleAddOrder = (newOrder) => { + setOrders((prevOrders) => [...prevOrders, newOrder]); + }; + + useEffect(() => { + const fetchOrders = async () => { + setLoading(true); + try { + const result = await authService.getCart(); + if (result.success) { + setOrdersData(Array.isArray(result.data) ? result.data : []); + } else { + console.error(result.message); + setOrdersData([]); + } + } catch (error) { + console.error('Failed to fetch orders:', error); + setOrdersData([]); + } + setLoading(false); + }; + fetchOrders(); + }, []); + + const handleRowClick = async (cartId) => { + setCartLoading(true); + try { + const result = await authService.getCartById(cartId); + if (result.success) setSelectedCart(result.data); + else console.error(result.message); + } catch (error) { + console.error('Failed to fetch cart details:', error); + } + setCartLoading(false); + }; + + const handleCartUpdated = (updatedCart) => { + setOrdersData(prev => + prev.map(cart => cart.id === updatedCart.id ? updatedCart : cart) + ); + setSelectedCart(null); + }; + + const handleCloseCartDetails = () => { setSelectedCart(null); }; + + const pageCount = Math.ceil(ordersData.length / itemsPerPage); + const paginatedOrders = ordersData.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + if (cartLoading) { + return ( + + + + ); + } + + if (selectedCart) { + return ( + + ); + } + + // عرض CartView بدل الطلبات إذا تم الضغط على الزر + // داخل Orders.js +if (showCartView) { + return ( + setShowCartView(false)} + onSend={handleAddOrder} + onCartCreated={(newCart) => { + setOrdersData(prev => [newCart, ...prev]); // ضف الكارت الجديد مباشرة + setShowCartView(false); + }} + adminId={adminId} // ← أرسل الـ adminId هنا + /> + ); +} + + + + return ( + + + + Order + + + + + + + {/* جدول الطلبات */} + + + + + Cart ID + Total Price + Created At + Items Count + + + + {loading + ? Array.from({ length: itemsPerPage }).map((_, idx) => ( + + + + + + + )) + : ( + <> + {paginatedOrders.map(order => { + const attributes = order?.attributes || {}; + const relationships = order?.relationships || {}; + const cartItems = relationships?.cartItems || relationships?.cart_items || []; + return ( + handleRowClick(order.id)} + > + {order?.id || '--'} + {attributes?.totalPrice ?? attributes?.total_price ?? '0.00'} + {attributes?.createdAt ? new Date(attributes.createdAt).toLocaleString() : '--'} + {cartItems.length} + + ); + })} + + {paginatedOrders.length < itemsPerPage && + Array.from({ length: itemsPerPage - paginatedOrders.length }).map((_, idx) => ( + + + + )) + } + + + + ) + } + +
+
+ + + + Showing {(currentPage - 1) * itemsPerPage + 1} - {Math.min(currentPage * itemsPerPage, ordersData.length)} of {ordersData.length} + + + + +
+ ); +}; + +export default Orders; diff --git a/src/components/Home/ResList/AppBar.js b/src/components/Home/ResList/AppBar.js new file mode 100644 index 0000000..bf65dd5 --- /dev/null +++ b/src/components/Home/ResList/AppBar.js @@ -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 ( + + + {/* Left: Logo */} + + + + KITCH + + + PLUS + + + + {/* Right: Log Out Button */} + + + + + + + + + + + ); +}; + +export default KitchPlusAppBar; diff --git a/src/components/Home/ResList/RestaurantDetailsModal.js b/src/components/Home/ResList/RestaurantDetailsModal.js new file mode 100644 index 0000000..c8a1f32 --- /dev/null +++ b/src/components/Home/ResList/RestaurantDetailsModal.js @@ -0,0 +1,60 @@ +import React from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Typography, + Button, + Box, +} from "@mui/material"; + +const RestaurantDetailsModal = ({ open, onClose, restaurant }) => { + if (!restaurant) return null; + + return ( + + {restaurant.name} + + + {restaurant.cuisine_type && ( + Cuisine Type: {restaurant.cuisine_type.name} + )} + Location: {restaurant.location} + Brand Details: {restaurant.brand_details || 'N/A'} + Age Group: {restaurant.age_group} + Menu Status: {restaurant.menu_status ? 'Active' : 'Inactive'} + Need Help: {restaurant.need_help || 'N/A'} + {restaurant.operational_details && ( + <> + Staff Members: {restaurant.operational_details.staff_members} + Equipment: {restaurant.operational_details.equipment} + Specialized Equipment: {restaurant.operational_details.specialized_equipment} + Expansion Cities: {restaurant.operational_details.expansion_plan_cities} + + )} + {restaurant.budget_expansion && ( + <> + Estimated Budget: {restaurant.budget_expansion.estimated_budget} + Expansion Branches: {restaurant.budget_expansion.expansion_branches} + + )} + {restaurant.created_by && ( + Created By: {restaurant.created_by.name} + )} + + + + + + + ); +}; + +export default RestaurantDetailsModal; diff --git a/src/components/Home/ResList/RestaurantProfile.js b/src/components/Home/ResList/RestaurantProfile.js new file mode 100644 index 0000000..a7052b3 --- /dev/null +++ b/src/components/Home/ResList/RestaurantProfile.js @@ -0,0 +1,76 @@ +import React, { useState, useEffect } from 'react'; +import { Box, useTheme, useMediaQuery } from '@mui/material'; +import KitchPlusAppBar from './AppBar'; +import RestaurantSelection from './RestaurantSelection'; +import authService from '../../../services/authService'; + +const RestaurantProfile = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [restaurants, setRestaurants] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchRestaurants = async () => { + setLoading(true); + const result = await authService.getRestaurants(); + if (result.success) { + const mappedRestaurants = result.data.map((res) => ({ + id: res.id, + name: res.name, + location: res.location || '', + image: res.image_url, + address: res.address || '', + })); + setRestaurants(mappedRestaurants); + } else { + alert(result.message || 'خطأ في جلب المطاعم'); + } + setLoading(false); + }; + + fetchRestaurants(); + }, []); + + const handleDrawerToggle = () => {}; + const handleCreateRestaurant = () => { + alert('Redirect to restaurant creation page or open a form!'); + }; + + return ( + + + + + + + + + + ); +}; + +export default RestaurantProfile; diff --git a/src/components/Home/ResList/RestaurantSelection.js b/src/components/Home/ResList/RestaurantSelection.js new file mode 100644 index 0000000..79522fc --- /dev/null +++ b/src/components/Home/ResList/RestaurantSelection.js @@ -0,0 +1,186 @@ +import React, { useState } from 'react'; +import { + Box, + Typography, + Grid, + useTheme, + useMediaQuery, + IconButton, + Skeleton +} from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import AddToPhotosIcon from '@mui/icons-material/AddToPhotos'; +import InfoIcon from '@mui/icons-material/Info'; +import { useRestaurant } from '../../../contexts/RestaurantContext'; +import RestaurantDetailsModal from './RestaurantDetailsModal'; +import authService from '../../../services/authService'; +const RestaurantSelection = ({ restaurants = [], loading = false }) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down('sm')); + const navigate = useNavigate(); // استخدم navigate للتوجيه + const { setRestaurantId } = useRestaurant(); + + const [selectedRestaurant, setSelectedRestaurant] = useState(null); + const [modalOpen, setModalOpen] = useState(false); + + const handleSelectRestaurant = (id) => { + setRestaurantId(id); + navigate('/dashboard'); + }; + + const fetchRestaurantDetails = async (id) => { + try { + const result = await authService.getRestaurantById(id); + if (result.success) { + setSelectedRestaurant(result.data); + setModalOpen(true); + } else { + alert(result.message || 'Failed to fetch restaurant details'); + } + } catch (error) { + console.error('Error fetching restaurant details:', error); + } + }; + + // ✅ دالة التوجيه عند الضغط على زر الإنشاء + const handleCreateClick = () => { + navigate('/create-restaurant'); // توجه مباشرة إلى صفحة الإنشاء + }; + return ( + + {/* زر إنشاء مطعم */} + + + + + + + + Create New Restaurant + + + Start from scratch and build your restaurant profile + + + + + {/* قائمة المطاعم */} + + + Existing Restaurants + + + {restaurants.length === 0 && !loading ? ( + + No restaurants found. Create one to get started. + + ) : ( + + {(loading ? Array.from(new Array(3)) : restaurants).map((restaurant, index) => ( + + {loading ? ( + + ) : ( + handleSelectRestaurant(restaurant.id)} + > + {/* أيقونة فتح المودال */} + { + e.stopPropagation(); + fetchRestaurantDetails(restaurant.id); + }} + sx={{ + position: 'absolute', + top: 8, + right: 8, + backgroundColor: 'rgba(255,255,255,0.8)', + '&:hover': { + backgroundColor: 'rgba(255,255,255,1)', + transform: 'scale(1.1)', + transition: 'transform 0.2s' + }, + zIndex: 2, + padding: 0.5 + }} + size="small" + > + + + + {/* صورة المطعم */} + + + + + {/* باقي الكارت */} + + + {restaurant.name} + + + + {restaurant.location || ' '} + + + + )} + + ))} + + )} + + + {/* مودال المطعم */} + setModalOpen(false)} + restaurant={selectedRestaurant} + /> + + ); +}; + +export default RestaurantSelection; diff --git a/src/components/Home/RestaurantProfile/RestaurantProfile.js b/src/components/Home/RestaurantProfile/RestaurantProfile.js index be3da8d..f67ad6b 100644 --- a/src/components/Home/RestaurantProfile/RestaurantProfile.js +++ b/src/components/Home/RestaurantProfile/RestaurantProfile.js @@ -1,165 +1,159 @@ -import React, { useState, useEffect } from 'react'; -import { Box, useTheme, useMediaQuery } from '@mui/material'; -import KitchPlusAppBar from '../AppBar'; -import Sidebar from '../SideHome'; -import LoginRestaurant from './contcet/LoginRestaurant'; +import React, { useState } from 'react'; +import { Box } from '@mui/material'; import BasicInformation from './contcet/BasicInformation'; import ContactInformation from './contcet/ContactInformation'; import BusinessHours from './contcet/BusinessHours'; -import UploadMenu from './contcet/UploadMenu'; import Equipment from './contcet/Equipment'; import TypeOfRestaurant from './contcet/TypeOfRestaurant'; import OperationalCapacity from './contcet/OperationalCapacity'; -import UploadPhotos from './contcet/UploadPhotos'; import SideProfile from './SideProfile'; - -const drawerWidth = 230; +import AccountProfile from '../Settings/AccountProfile'; +import authService from '../../../services/authService'; +import { useRestaurant } from '../../../contexts/RestaurantContext'; +import ConfirmationDialog from './contcet/ConfirmationDialog'; const RestaurantProfile = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const [hasProducts, setHasProducts] = useState(false); - const [sidebarOpen, setSidebarOpen] = useState(!isMobile); - - // ⬇️ إدارة الخطوة الحالية + const { restaurantId } = useRestaurant(); const [currentStep, setCurrentStep] = useState(0); + const [formData, setFormData] = useState({}); + const [modalOpen, setModalOpen] = useState(false); + const [showSetting, setShowSetting] = useState(false); + + const updateFormData = (newData) => + setFormData((prev) => ({ ...prev, ...newData })); + + const handleSubmit = async () => { + if (!restaurantId) return; + + const payload = { + restaurant_name: formData.restaurant_name || '', + restaurant_type: formData.restaurant_type || '', + Address: formData.Address || '', + City: formData.City || '', + Postal_code: formData.Postal_code || '', + Phone: formData.Phone || '', + Email: formData.Email || '', + Operation_hour: formData.Operation_hour || '', + closed_days: formData.closed_days || [], + equipment: { + ...formData.equipment // ✅ أخذ جميع المعدات من Equipment مباشرة + }, + Maximum_orders_per_day: formData.Maximum_orders_per_day || 0, + Number_of_Cheff: formData.Number_of_Cheff || 0, + Number_of_Waiters: formData.Number_of_Waiters || 0, + Number_of_Cookers: formData.Number_of_Cookers || 0, + host_type: formData.ownRestaurantType || '', + collaboration_type: formData.collaborateRestaurantType || '' + }; + + try { + const res = await authService.createRestaurantProfile(restaurantId, payload); + if (res.success) { + setModalOpen(true); + + // بعد 10 ثواني يغلق المودال ويظهر الـ AccountProfile + setTimeout(() => { + setModalOpen(false); + setShowSetting(true); + }, 10000); // ✅ صار 10 ثواني بدل 5 + } else { + console.error("Profile creation failed:", res.message, res.errors); + } + } catch (error) { + console.error(error); + } + }; const steps = [ - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep + 1)} onBack={() => setCurrentStep(currentStep - 1)} />, - setCurrentStep(currentStep - 1)} />, + setCurrentStep((p) => p + 1)} + />, + setCurrentStep((p) => p + 1)} + onBack={() => setCurrentStep((p) => p - 1)} + />, + setCurrentStep((p) => p + 1)} + onBack={() => setCurrentStep((p) => p - 1)} + />, + setCurrentStep((p) => p + 1)} + onBack={() => setCurrentStep((p) => p - 1)} + />, + setCurrentStep((p) => p + 1)} + onBack={() => setCurrentStep((p) => p - 1)} + />, + setCurrentStep((p) => p - 1)} + onRegister={handleSubmit} + /> ]; - useEffect(() => { - const checkProducts = async () => { - const productsExist = await checkIfProductsExist(); - setHasProducts(productsExist); - }; - checkProducts(); - }, []); - - const checkIfProductsExist = async () => { - return false; - }; - - useEffect(() => { - if (window.innerWidth >= theme.breakpoints.values.md) { - setSidebarOpen(true); - } else { - setSidebarOpen(false); - } - }, [theme.breakpoints.values.md]); - - useEffect(() => { - const handleResize = () => { - if (window.innerWidth >= theme.breakpoints.values.md) { - setSidebarOpen(true); - } else { - setSidebarOpen(false); - } - }; - - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [theme.breakpoints.values.md]); - - const handleDrawerToggle = () => { - setSidebarOpen(!sidebarOpen); - }; - return ( - - - - - - - - - - - setCurrentStep(prev => Math.max(prev - 1, 0))} - /> - - - - - - - - {steps[currentStep]} - - + + + {/* إظهار SideProfile فقط إذا لم يتم التحويل إلى AccountProfile */} + {!showSetting && ( + + + setCurrentStep((prev) => Math.max(prev - 1, 0))} + /> + )} + + + + {showSetting ? : steps[currentStep]} + + + {/* مودال بدون أزرار */} + {}} hideButtons={true} /> ); }; diff --git a/src/components/Home/RestaurantProfile/SideProfile.js b/src/components/Home/RestaurantProfile/SideProfile.js index e7508c0..c63f45c 100644 --- a/src/components/Home/RestaurantProfile/SideProfile.js +++ b/src/components/Home/RestaurantProfile/SideProfile.js @@ -6,11 +6,11 @@ const steps = [ { title: 'Basic Information', icon: '/images/createProfile/BasicInf.png' }, { title: 'Contact Information', icon: '/images/createProfile/ContactInf.png' }, { title: 'Business Hours', icon: '/images/createProfile/BusinessHours.png' }, - { title: 'Menu Upload', icon: '/images/createProfile/MenuUpload.png' }, + // { title: 'Menu Upload', icon: '/images/createProfile/MenuUpload.png' }, { title: 'Available Equipment', icon: '/images/createProfile/equipment.png' }, { title: 'Operational Capacity', icon: '/images/icons/rocket.png' }, { title: 'Type of Restaurant', icon: '/images/createProfile/TypeOfRestaurant.png' }, - { title: 'Upload Photos', icon: '/images/createProfile/UploadPhotos.png' }, + // { title: 'Upload Photos', icon: '/images/createProfile/UploadPhotos.png' }, { title: 'Submit & Confirmation', icon: '/images/createProfile/Confirmation.png' }, ]; diff --git a/src/components/Home/RestaurantProfile/all.js b/src/components/Home/RestaurantProfile/all.js new file mode 100644 index 0000000..b5ab999 --- /dev/null +++ b/src/components/Home/RestaurantProfile/all.js @@ -0,0 +1,124 @@ +import React, { useEffect, useState, useCallback } from "react"; +import { CircularProgress, Box, useTheme, useMediaQuery, Skeleton } from "@mui/material"; +import RestaurantProfile from "./RestaurantProfile"; +import Setting from "../Settings/AccountProfile"; +import authService from "../../../services/authService"; +import { useRestaurant } from "../../../contexts/RestaurantContext"; +import KitchPlusAppBar from "../AppBar"; +import Sidebar from "../SideHome" +const drawerWidth = 230; + +const RestaurantWrapper = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const { restaurantId } = useRestaurant(); + + const [loading, setLoading] = useState(true); + const [hasProfile, setHasProfile] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(!isMobile); + + const fetchProfile = useCallback(async () => { + if (!restaurantId) { + setLoading(false); + return; + } + setLoading(true); + try { + const profile = await authService.getRestaurantProfile(restaurantId); + setHasProfile(profile && Object.keys(profile).length > 0); + } catch (error) { + console.error("Error checking restaurant profile:", error); + setHasProfile(false); + } finally { + setLoading(false); + } + }, [restaurantId]); + + useEffect(() => { + fetchProfile(); + }, [fetchProfile]); + + useEffect(() => { + const handleResize = () => + setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md); + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [theme.breakpoints.values.md]); + + const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen); + + return ( + + {/* البار الجانبي */} + + + {/* المحتوى الرئيسي */} + + {/* البار العلوي */} + + + {/* المحتوى حسب وجود البروفايل */} + + {loading ? ( + // Skeleton placeholder + + + + + + + ) : hasProfile ? ( + + ) : ( + + )} + + + + ); +}; + +export default RestaurantWrapper; diff --git a/src/components/Home/RestaurantProfile/contcet/BasicInformation.js b/src/components/Home/RestaurantProfile/contcet/BasicInformation.js index 9b03896..d531179 100644 --- a/src/components/Home/RestaurantProfile/contcet/BasicInformation.js +++ b/src/components/Home/RestaurantProfile/contcet/BasicInformation.js @@ -1,17 +1,49 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material'; -const BasicInformation = ({ currentStepIndex = 0, onNext, onBack }) => { +const BasicInformation = ({ formData, updateFormData, onNext, onBack }) => { const theme = useTheme(); + const [errors, setErrors] = useState({}); + + const handleChange = (field, value) => { + updateFormData({ [field]: value }); + // إزالة رسالة الخطأ عند التعديل + setErrors(prev => ({ ...prev, [field]: '' })); + }; + + const validateFields = () => { + const newErrors = {}; + if (!formData.restaurant_name || formData.restaurant_name.trim() === '') newErrors.restaurant_name = 'Restaurant Name is required'; + if (!formData.restaurant_type || formData.restaurant_type.trim() === '') newErrors.restaurant_type = 'Restaurant Type is required'; + if (!formData.Address || formData.Address.trim() === '') newErrors.Address = 'Address is required'; + if (!formData.City || formData.City.trim() === '') newErrors.City = 'City is required'; + if (!formData.Postal_code || formData.Postal_code.trim() === '') newErrors.Postal_code = 'Postal Code is required'; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleNextClick = () => { + if (validateFields()) { + onNext(); + } + }; + + const fields = [ + { label: 'Restaurant Name', placeholder: 'Al-Baik Foods', field: 'restaurant_name' }, + { label: 'Restaurant Type', placeholder: 'Fast Food', field: 'restaurant_type' }, + { label: 'Address', placeholder: 'Ward # 13', field: 'Address' }, + { label: 'City', placeholder: 'Jordan', field: 'City' }, + { label: 'Postal Code', placeholder: '31200', field: 'Postal_code' }, + ]; + return ( { width: { xs: '85%', sm: '90%' }, }} > - + Basic Information + { - {/* Inputs (Restaurant Name, Type, Address, City, Postal Code) */} - {[ - { label: 'Restaurant Name', placeholder: 'Al-Baik Foods' }, - { label: 'Restaurant Type', placeholder: 'Fast Food' }, - { label: 'Address', placeholder: 'Ward # 13' }, - { label: 'City', placeholder: 'Jordan' }, - { label: 'Postal Code', placeholder: '31200' }, - ].map((field, index) => ( - + {fields.map((fieldObj, index) => ( + - {field.label} + {fieldObj.label} handleChange(fieldObj.field, e.target.value)} + error={!!errors[fieldObj.field]} + helperText={errors[fieldObj.field] || ' '} sx={{ + pb:-1, '& input': { fontWeight: 500, fontSize: '15px' }, '& input::placeholder': { color: '#969BA7' }, '& .MuiOutlinedInput-root': { @@ -80,12 +107,11 @@ const BasicInformation = ({ currentStepIndex = 0, onNext, onBack }) => { ))} - - {/* زر Back تحت زر Next */} - ); diff --git a/src/components/Home/RestaurantProfile/contcet/BusinessHours.js b/src/components/Home/RestaurantProfile/contcet/BusinessHours.js index 4cc47ac..aa64844 100644 --- a/src/components/Home/RestaurantProfile/contcet/BusinessHours.js +++ b/src/components/Home/RestaurantProfile/contcet/BusinessHours.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, Typography, @@ -11,28 +11,62 @@ import { FormControlLabel } from '@mui/material'; -const BusinessHours = ({ currentStepIndex = 0, onNext, onBack }) => { +const BusinessHours = ({ formData, updateFormData, onNext, onBack }) => { const theme = useTheme(); - const [selectedDays, setSelectedDays] = useState([]); - - const handleCheckboxChange = (day) => { - setSelectedDays((prev) => - prev.includes(day) - ? prev.filter((d) => d !== day) - : [...prev, day] - ); - }; + const [selectedDays, setSelectedDays] = useState(formData.closed_days || []); + const [operationHours, setOperationHours] = useState(formData.Operation_hour || ''); + const [errors, setErrors] = useState({}); const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + useEffect(() => { + updateFormData({ + Operation_hour: operationHours, // الاسم الآن مطابق للـ API + closed_days: selectedDays + }); + }, [operationHours, selectedDays]); + + const handleCheckboxChange = (day) => { + const updatedDays = selectedDays.includes(day) + ? selectedDays.filter(d => d !== day) + : [...selectedDays, day]; + setSelectedDays(updatedDays); + }; + + const validateFields = () => { + const newErrors = {}; + if (!operationHours || operationHours.trim() === '') { + newErrors.operationHours = 'Operational hours are required'; + } else { + // Regex للتحقق من الصيغة hh:mm AM/PM - hh:mm AM/PM + const regex = /^([0]?[1-9]|1[0-2]):([0-5][0-9])\s?(AM|PM)\s?-\s?([0]?[1-9]|1[0-2]):([0-5][0-9])\s?(AM|PM)$/i; + if (!regex.test(operationHours.trim())) { + newErrors.operationHours = 'Invalid format (e.g., "9:00 AM - 11:00 PM")'; + } + } + + if (selectedDays.length === 7) { + newErrors.closedDays = 'Restaurant cannot be closed all week'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleNextClick = () => { + if (validateFields()) { + onNext(); + } + }; + return ( { width: { xs: '85%', sm: '90%' }, }} > - - + + Business Hours - + Enter your business hours to proceed to registration of your own restaurant on this platform @@ -72,9 +92,13 @@ const BusinessHours = ({ currentStepIndex = 0, onNext, onBack }) => { Operational Hours setOperationHours(e.target.value)} + error={!!errors.operationHours} + helperText={errors.operationHours || ' '} sx={{ '& input': { fontWeight: 500, fontSize: '15px' }, '& input::placeholder': { color: '#969BA7' }, @@ -96,22 +120,11 @@ const BusinessHours = ({ currentStepIndex = 0, onNext, onBack }) => { {/* Checkboxes for Days of the Week */} - + Closed Days - - {days.map((day) => { + + {days.map(day => { const isChecked = selectedDays.includes(day); return ( { sx={{ transform: 'scale(1.5)', color: '#F0EDED', - '&.Mui-checked': { - color: '#FF914D', - }, + '&.Mui-checked': { color: '#FF914D' }, }} /> } label={ - + {day} } - sx={{ - m: 0, - width: { xs: '45%', sm: '30%', md: '22%' }, - }} + sx={{ m: 0, width: { xs: '45%', sm: '30%', md: '22%' } }} /> ); })} + {errors.closedDays && ( + + {errors.closedDays} + + )} - {/* Next Button */} - + {/* Next & Back Buttons */} - - - {/* زر Back تحت زر Next */} - - - + + + + ); diff --git a/src/components/Home/RestaurantProfile/contcet/ConfirmationDialog.js b/src/components/Home/RestaurantProfile/contcet/ConfirmationDialog.js index ed1108a..b92b113 100644 --- a/src/components/Home/RestaurantProfile/contcet/ConfirmationDialog.js +++ b/src/components/Home/RestaurantProfile/contcet/ConfirmationDialog.js @@ -4,22 +4,19 @@ import { DialogTitle, DialogContent, DialogContentText, - DialogActions, - Button, Box, Typography, useTheme, - Divider } from '@mui/material'; -import EditIcon from '@mui/icons-material/Edit'; -const ConfirmationDialog = ({ open, onClose, onConfirm }) => { + +const ConfirmationDialog = ({ open, onClose }) => { const theme = useTheme(); + return ( { }} > - - {/* الدوائر */} + + - {/* الدائرة الخلفية */} - - - {/* الدائرة الأمامية مع الأيقونة */} - - Success icon - - - - {/* العنوان بجانب الدائرة */} - - - Register successful! - - + alt="Success icon" + /> + - - - Congratulations! you have registered your restaurant successfully on our platform - - - - - {/* الزر الأول - Filled */} - - {/* الزر الثاني - Outlined + أيقونة Edit */} - - + + + Register successful! + + + + + + Congratulations! You have registered your restaurant successfully on our platform + + ); }; -export default ConfirmationDialog; \ No newline at end of file +export default ConfirmationDialog; diff --git a/src/components/Home/RestaurantProfile/contcet/ContactInformation.js b/src/components/Home/RestaurantProfile/contcet/ContactInformation.js index 4f5d00c..a2cece0 100644 --- a/src/components/Home/RestaurantProfile/contcet/ContactInformation.js +++ b/src/components/Home/RestaurantProfile/contcet/ContactInformation.js @@ -1,11 +1,35 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material'; -const ContactInformation = ({ currentStepIndex = 0, onNext, onBack }) => { +const ContactInformation = ({ formData, updateFormData, onNext, onBack }) => { const theme = useTheme(); + const [errors, setErrors] = useState({}); - const handleNext = () => { - if (onNext) { + const handleChange = (field, value) => { + updateFormData({ [field]: value }); + setErrors(prev => ({ ...prev, [field]: '' })); // إزالة الخطأ عند التعديل + }; + + const validateFields = () => { + const newErrors = {}; + if (!formData.Phone || formData.Phone.trim() === '') { + newErrors.Phone = 'Phone number is required'; + } else if (!/^\+?\d{7,15}$/.test(formData.Phone.trim())) { + newErrors.Phone = 'Invalid phone number'; + } + + if (!formData.Email || formData.Email.trim() === '') { + newErrors.Email = 'Email is required'; + } else if (!/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(formData.Email.trim())) { + newErrors.Email = 'Invalid Email address'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleNextClick = () => { + if (validateFields()) { onNext(); } }; @@ -25,26 +49,13 @@ const ContactInformation = ({ currentStepIndex = 0, onNext, onBack }) => { width: { xs: '85%', sm: '90%' }, }} > - - + + Contact Information + - + Enter your contact information to proceed to registration of your own restaurant on this platform @@ -58,6 +69,10 @@ const ContactInformation = ({ currentStepIndex = 0, onNext, onBack }) => { placeholder="03055376864" variant="outlined" fullWidth + value={formData.Phone || ''} + onChange={(e) => handleChange('Phone', e.target.value)} + error={!!errors.Phone} + helperText={errors.Phone || ' '} sx={{ '& input': { fontWeight: 500, fontSize: '15px' }, '& input::placeholder': { color: '#969BA7' }, @@ -84,9 +99,13 @@ const ContactInformation = ({ currentStepIndex = 0, onNext, onBack }) => { handleChange('Email', e.target.value)} + error={!!errors.Email} + helperText={errors.Email || ' '} sx={{ '& input': { fontWeight: 500, fontSize: '15px' }, '& input::placeholder': { color: '#969BA7' }, @@ -105,56 +124,49 @@ const ContactInformation = ({ currentStepIndex = 0, onNext, onBack }) => { }} /> - {/* Next Button */} - - + {/* Next & Back Buttons */} + + - {/* زر Back تحت زر Next */} - - - + + ); diff --git a/src/components/Home/RestaurantProfile/contcet/Equipment.js b/src/components/Home/RestaurantProfile/contcet/Equipment.js index a3fc4d3..2473e32 100644 --- a/src/components/Home/RestaurantProfile/contcet/Equipment.js +++ b/src/components/Home/RestaurantProfile/contcet/Equipment.js @@ -1,15 +1,66 @@ -import React from 'react'; -import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material'; +import React, { useState, useEffect } from 'react'; +import { + Box, + Typography, + Stack, + Button, + useTheme, + TextField, + Modal, +} from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import { useNavigate } from 'react-router-dom'; -const Equipment = ({ currentStepIndex = 0, onNext, onBack }) => { +const Equipment = ({ formData, updateFormData, onNext, onBack }) => { const theme = useTheme(); const navigate = useNavigate(); - const handleNext = () => { - if (onNext) onNext(); - else navigate('/dashboard'); + const [equipment, setEquipment] = useState(formData.equipment || { + oven: '', + grill: '', + freezer: '', + }); + + const [errors, setErrors] = useState({}); + const [modalOpen, setModalOpen] = useState(false); + const [newEquipment, setNewEquipment] = useState({ name: '', quantity: '' }); + + useEffect(() => { + updateFormData({ equipment }); + }, [equipment]); + + const handleInputChange = (key, value) => { + setEquipment(prev => ({ ...prev, [key]: value })); + }; + + const validateFields = () => { + const newErrors = {}; + Object.entries(equipment).forEach(([key, value]) => { + if (!value || isNaN(value) || Number(value) < 0) { + newErrors[key] = `${key} must be a positive number`; + } + }); + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleNextClick = () => { + if (validateFields()) { + if (onNext) onNext(); + else navigate('/dashboard'); + } + }; + + const handleAddEquipment = () => { + if (!newEquipment.name.trim() || !newEquipment.quantity || isNaN(newEquipment.quantity)) { + return; + } + setEquipment(prev => ({ + ...prev, + [newEquipment.name.toLowerCase()]: newEquipment.quantity, + })); + setNewEquipment({ name: '', quantity: '' }); + setModalOpen(false); }; return ( @@ -27,30 +78,29 @@ const Equipment = ({ currentStepIndex = 0, onNext, onBack }) => { position: 'relative', }} > - + Equipment - + Enter your kitchen equipment details to complete your restaurant registration. - {/* Oven Input */} - - - {/* Grill Input */} - - - {/* Refrigerators Input */} - + {/* Dynamic Equipment Fields */} + {Object.keys(equipment).map((key) => ( + handleInputChange(key, val)} + error={errors[key]} + /> + ))} {/* Add More Equipment Button */} @@ -65,23 +115,20 @@ const Equipment = ({ currentStepIndex = 0, onNext, onBack }) => { borderRadius: '50px', textTransform: 'none', color: theme.palette.primary.main, - '&:hover': { - backgroundColor: 'transparent', - } + '&:hover': { backgroundColor: 'transparent' } }} - onClick={() => console.log('Add More Equipment')} + onClick={() => setModalOpen(true)} > Add More {/* Next Button */} - - + - {/* زر Back تحت زر Next */} - + + {/* Modal for Adding Equipment */} + setModalOpen(false)}> + + + Add New Equipment + + setNewEquipment({ ...newEquipment, name: e.target.value })} + sx={{ mb: 2 }} + /> + setNewEquipment({ ...newEquipment, quantity: e.target.value })} + sx={{ mb: 2 }} + /> + + + ); }; -// مكون فرعي لتقليل التكرار في الحقول -const InputField = ({ label, placeholder, theme }) => ( +const InputField = ({ label, placeholder, theme, value, onChange, error }) => ( {label} @@ -140,6 +231,10 @@ const InputField = ({ label, placeholder, theme }) => ( placeholder={placeholder} variant="outlined" fullWidth + value={value} + onChange={(e) => onChange(e.target.value)} + error={!!error} + helperText={error || ' '} sx={{ '& input': { fontWeight: 500, fontSize: '15px' }, '& input::placeholder': { color: '#969BA7' }, diff --git a/src/components/Home/RestaurantProfile/contcet/LoginRestaurant.js b/src/components/Home/RestaurantProfile/contcet/LoginRestaurant.js deleted file mode 100644 index 8c91a08..0000000 --- a/src/components/Home/RestaurantProfile/contcet/LoginRestaurant.js +++ /dev/null @@ -1,261 +0,0 @@ -import React, { useState } from 'react'; -import { TextField, Button, Typography, Stack, Box, IconButton, InputAdornment } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; -import { VisibilityOutlined } from '@mui/icons-material'; -import { useNavigate } from 'react-router-dom'; -import { Link } from 'react-router-dom'; - - -const LoginForm = () => { - - const navigate = useNavigate(); - const theme = useTheme(); - const [showPassword, setShowPassword] = useState(false); - - const handleTogglePassword = () => { - setShowPassword((prev) => !prev); - }; - const handleLogin = () => { - // بعد تنفيذ عملية تسجيل الدخول بنجاح، يتم التوجيه - navigate('/dashboard'); - }; - return ( - - - - - {/* Login Content */} - - - - - - - - Login - - - - Enter your username and password to access your account securely. Welcome back to our service! - - - {/* Email Input */} - - - Email - - - - - {/* Password Input */} - - - Password - - - - {showPassword ? : } - - - ) - }} - /> - - - {/* Login Button */} - - - - - {/* Divider */} - - - - Or - - - - - {/* Google Button */} - - - - - - {/* Register Link */} - - Don’t have an account?{' '} - - Register - - - - - - - - - ); -}; - -export default LoginForm; diff --git a/src/components/Home/RestaurantProfile/contcet/OperationalCapacity.js b/src/components/Home/RestaurantProfile/contcet/OperationalCapacity.js index 418b9ea..5547b5f 100644 --- a/src/components/Home/RestaurantProfile/contcet/OperationalCapacity.js +++ b/src/components/Home/RestaurantProfile/contcet/OperationalCapacity.js @@ -1,16 +1,83 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material'; -const OperationalCapacity = ({ currentStepIndex = 0, onNext, onBack }) => { +const OperationalCapacity = ({ formData, updateFormData, onNext, onBack }) => { const theme = useTheme(); + const [capacity, setCapacity] = useState({ + Maximum_orders_per_day: formData.Maximum_orders_per_day || '', + Number_of_Cheff: formData.Number_of_Cheff || '', + Number_of_Waiters: formData.Number_of_Waiters || '', + Number_of_Cookers: formData.Number_of_Cookers || '', + }); + + const [errors, setErrors] = useState({}); + + const handleChange = (field, value) => { + if (/^\d*$/.test(value)) { // السماح فقط بالأرقام + setCapacity(prev => ({ ...prev, [field]: value })); + } + }; + + const validateFields = () => { + const newErrors = {}; + Object.entries(capacity).forEach(([key, value]) => { + if (!value || isNaN(Number(value)) || Number(value) < 0) { + newErrors[key] = 'Please enter a positive number'; + } + }); + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleNextClick = () => { + if (validateFields()) { + updateFormData(capacity); // تحديث الأب عند الضغط على Next فقط + if (onNext) onNext(); + } + }; + + const InputField = ({ label, value, onChange, placeholder, error }) => ( + + + {label} + + onChange(e.target.value)} + error={!!error} + helperText={error || ' '} + sx={{ + '& input': { fontWeight: 500, fontSize: '15px' }, + '& input::placeholder': { color: '#969BA7' }, + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + transition: '0.3s', + '&.Mui-focused fieldset': { + borderColor: theme.palette.primary.main, + boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' + } + }, + '& .MuiOutlinedInput-root.Mui-focused': { + borderColor: theme.palette.primary.main, + boxShadow: '0 0 0 2px rgba(63,81,181,0.1)' + } + }} + /> + + ); + return ( { width: { xs: '85%', sm: '90%' }, }} > - - + + Operational Capacity + - + Enter your basic information to proceed to registration of your own restaurant on this platform - {/* Maximum Orders Per Day Input */} - - - Maximum Orders Per Day - - - + handleChange('Maximum_orders_per_day', val)} + placeholder="5000" + error={errors.Maximum_orders_per_day} + /> - {/* Number of Cheff Input */} - - - Number of Cheff - - - + handleChange('Number_of_Cheff', val)} + placeholder="20" + error={errors.Number_of_Cheff} + /> - {/* Number of Waiters Input */} - - - Number of Waiters - - - + handleChange('Number_of_Waiters', val)} + placeholder="200" + error={errors.Number_of_Waiters} + /> - {/* Number of Kitchen Assistant Input */} - - - Number of Kitchen Assistant - - - + handleChange('Number_of_Cookers', val)} + placeholder="200" + error={errors.Number_of_Cookers} + /> {/* Buttons */} - - - - - {/* زر Back تحت زر Next */} - - - + + + + + ); diff --git a/src/components/Home/RestaurantProfile/contcet/TypeOfRestaurant.js b/src/components/Home/RestaurantProfile/contcet/TypeOfRestaurant.js index e271a57..1b1e69b 100644 --- a/src/components/Home/RestaurantProfile/contcet/TypeOfRestaurant.js +++ b/src/components/Home/RestaurantProfile/contcet/TypeOfRestaurant.js @@ -1,165 +1,157 @@ -// TypeOfRestaurant.jsx -import React from 'react'; -import { - Box, - Typography, - Stack, - Button, - useTheme, - TextField -} from '@mui/material'; +import React, { useState, useEffect } from 'react'; +import { Box, Typography, Stack, Button, useTheme, FormHelperText, TextField, Select, MenuItem } from '@mui/material'; -const TypeOfRestaurant = ({ currentStepIndex = 0, onNext, onBack }) => { - const theme = useTheme(); +const TypeOfRestaurant = ({ formData, updateFormData, onBack, onRegister }) => { + const theme = useTheme(); + const [typeData, setTypeData] = useState({ + ownRestaurantType: formData.ownRestaurantType || '', + collaborateRestaurantType: formData.collaborateRestaurantType || '' + }); + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); - return ( - - - {/* العنوان */} - - Type of Restaurant - + const collaborationOptions = [ + { value: 'fine_dining', label: 'Fine Dining' }, + { value: 'catering_services', label: 'Catering Services' }, + { value: 'fast_food', label: 'Fast Food' }, + { value: 'cloud_kitchen', label: 'Cloud Kitchen' } + ]; - {/* الوصف */} - - - Enter your restaurant type you want to collaborate to proceed to registration of your own restaurant on this platform - - + useEffect(() => { + updateFormData(typeData); + }, [typeData]); - {/* الحقول */} - - - Open to Host Restaurant/Cousin type* - - - + const handleChange = (field, value) => setTypeData(prev => ({ ...prev, [field]: value })); - - - Select Restaurant type Willing to Collaborate With - - - + const validateFields = () => { + const newErrors = {}; + if (!typeData.ownRestaurantType) newErrors.ownRestaurantType = 'This field is required'; + if (!typeData.collaborateRestaurantType) newErrors.collaborateRestaurantType = 'This field is required'; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; - - - - {/* زر Back تحت زر Next */} - - + const handleRegister = async () => { + if (!validateFields()) return; - + setIsSubmitting(true); + try { + // استدعاء الدالة المرسلة من الأب لتسجيل المطعم + await onRegister(); + } catch (error) { + console.error('Registration failed:', error); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + Type of Restaurant + + + + + Enter your restaurant type you want to collaborate to proceed to registration of your own restaurant on this platform + - ); + + + + Open to Host Restaurant/Cousin type* + + handleChange('ownRestaurantType', e.target.value)} + error={!!errors.ownRestaurantType} + helperText={errors.ownRestaurantType || ' '} + sx={{ + '& input': { fontWeight: 500, fontSize: '15px' }, + '& input::placeholder': { color: '#969BA7' }, + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + '&.Mui-focused fieldset': { borderColor: theme.palette.primary.main } + } + }} + /> + + + + + Select Restaurant type Willing to Collaborate With* + + + {errors.collaborateRestaurantType || ' '} + + + + + + + + + + ); }; export default TypeOfRestaurant; diff --git a/src/components/Home/RestaurantProfile/contcet/UploadMenu.js b/src/components/Home/RestaurantProfile/contcet/UploadMenu.js deleted file mode 100644 index eb860f1..0000000 --- a/src/components/Home/RestaurantProfile/contcet/UploadMenu.js +++ /dev/null @@ -1,240 +0,0 @@ -import React from 'react'; -import { - Box, - Typography, - Stack, - Button, - useTheme, - TextField -} from '@mui/material'; -import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; - -const UploadMenu = ({ currentStepIndex = 0, onNext, onBack }) => { - const theme = useTheme(); - - return ( - - - - Upload Menu - - - - - Enter your basic information to proceed to registration of your own restaurant on this platform - - - - {/* Item Name Input */} - - - Item Name - - - - - {/* Price Input */} - - - Price - - - - - {/* Description Input */} - - - Description - - - - - {/* OR Separator */} - - - - - OR - - - - - {/* Upload Menu Picture */} - - Upload Menu Picture - - - - - {/* Buttons: Back and Next */} - - - - - {/* زر Back تحت زر Next */} - - - - - - ); -}; - -export default UploadMenu; diff --git a/src/components/Home/RestaurantProfile/contcet/UploadPhotos.js b/src/components/Home/RestaurantProfile/contcet/UploadPhotos.js deleted file mode 100644 index ecff280..0000000 --- a/src/components/Home/RestaurantProfile/contcet/UploadPhotos.js +++ /dev/null @@ -1,183 +0,0 @@ -import React, { useState } from 'react'; -import { - Box, - Typography, - Stack, - Button, - useTheme -} from '@mui/material'; -import { useNavigate } from 'react-router-dom'; -import ConfirmationDialog from './ConfirmationDialog'; // ✅ استدعاء المودال المنفصل - -const UploadPhotos = ({ currentStepIndex = 0, onNext, onBack }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const [openModal, setOpenModal] = useState(false); - - const handleOpenModal = () => setOpenModal(true); - const handleCloseModal = () => setOpenModal(false); - const handleConfirmNext = () => { - handleCloseModal(); - onNext(); - }; - - return ( - - - - Upload Photos - - - - - Enter your basic information to proceed to registration of your own restaurant on this platform - - - - {/* Kitchen Interior’s */} - - - Kitchen Interior’s - - - - Upload Interior Picture - - - - - - {/* Kitchen Equipment’s */} - - - Kitchen Equipment’s - - - - Upload Equipment Picture - - - - - - {/* Buttons */} - - - - - - - - {/* ✅ Confirmation Modal */} - - - ); -}; - -export default UploadPhotos; diff --git a/src/components/Home/Settings/AccountProfile.js b/src/components/Home/Settings/AccountProfile.js new file mode 100644 index 0000000..1ce714c --- /dev/null +++ b/src/components/Home/Settings/AccountProfile.js @@ -0,0 +1,403 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Typography, + TextField, + Button, + Select, + MenuItem, + Checkbox, + ListItemText, + OutlinedInput, + styled, + useTheme, + LinearProgress +} from '@mui/material'; +import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined'; +import authService from '../../../services/authService'; +import { useRestaurant } from '../../../contexts/RestaurantContext'; +import AddIcon from "@mui/icons-material/Add"; +const AccountProfile = () => { + const theme = useTheme(); + const { restaurantId } = useRestaurant(); + const [editMode, setEditMode] = useState(false); + const [profileImage, setProfileImage] = useState(null); + const [form, setForm] = useState({ + restaurant_name: '', + restaurant_type: '', + Address: '', + City: '', + Postal_code: '', + Phone: '', + Email: '', + Operation_hour: '', + host_type: '', + collaboration_type: '', + closed_days: [], + equipment: [], // array of {name, quantity} + Maximum_orders_per_day: 0, + Number_of_Cheff: 0, + Number_of_Waiters: 0, + Number_of_Cookers: 0, + }); + + const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; + + const Input = styled('input')({ display: 'none' }); + + useEffect(() => { + const fetchProfile = async () => { + if (!restaurantId) return; + const data = await authService.getRestaurantProfile(restaurantId); + console.log("PROFILE DATA ===>", data); // 👈 يوضح الأسماء الحقيقية + if (data) { + setForm({ + restaurant_name: data.restaurant_name, + restaurant_type: data.restaurant_type, + Address: data.Address, + City: data.City, + Postal_code: data.Postal_code, + Phone: data.Phone, + Email: data.Email, + Operation_hour: data.Operation_hour, + host_type: data.host_type, + collaboration_type: data.collaboration_type, + closed_days: data.closed_days || [], + equipment: Object.entries(data.equipment || {}).map(([name, quantity]) => ({ name, quantity })), + Maximum_orders_per_day: data.Maximum_orders_per_day ?? data.maximum_orders_per_day ?? 0, + Number_of_Cheff: data.Number_of_Cheff ?? data.number_of_cheff ?? 0, + Number_of_Waiters: data.Number_of_Waiters ?? data.number_of_waiters ?? 0, + Number_of_Cookers: data.Number_of_Cookers ?? data.number_of_cookers ?? 0, + }); + } + }; + fetchProfile(); + }, [restaurantId]); + + + const handleImageUpload = (event) => { + if (event.target.files && event.target.files[0]) { + setProfileImage(event.target.files[0]); + } + }; + + const handleChange = (field, value) => setForm(prev => ({ ...prev, [field]: value })); + + const handleEquipmentChange = (index, key, value) => { + const newEquipment = [...form.equipment]; + newEquipment[index][key] = value; + setForm(prev => ({ ...prev, equipment: newEquipment })); + }; + + const addEquipment = () => { + setForm(prev => ({ + ...prev, + equipment: [...prev.equipment, { name: '', quantity: 0 }] + })); + }; + + const removeEquipment = (index) => { + const newEquipment = [...form.equipment]; + newEquipment.splice(index, 1); + setForm(prev => ({ ...prev, equipment: newEquipment })); + }; + + const renderField = (label, value, fieldKey, options = null) => ( + + + {label} + + + {editMode ? ( + options ? ( + + ) : ( + handleChange(fieldKey, e.target.value)} + fullWidth + variant="outlined" + sx={{ + '& input': { fontWeight: 500, fontSize: '16px', color: 'black' }, + '& input::placeholder': { color: '#969BA7' }, + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + transition: '0.3s', + '&.Mui-focused fieldset': { + borderColor: theme.palette.primary.main, + boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)', + }, + }, + }} + /> + ) + ) : ( + + {Array.isArray(value) ? value.join(', ') : value} + + )} + +); + + + + const handleSave = async () => { + if (!restaurantId) return; + + const equipmentObj = {}; + form.equipment.forEach(eq => { + if (eq.name) equipmentObj[eq.name] = eq.quantity; + }); + + const payload = { + ...form, + equipment: equipmentObj + }; + + if (profileImage) payload.logo = profileImage; + + const result = await authService.updateRestaurantProfile(restaurantId, payload); + + if (result.success) { + alert("Profile updated successfully!"); + setEditMode(false); + + const updated = await authService.getRestaurantProfile(restaurantId); + if (updated) setForm({ + ...updated, + equipment: Object.entries(updated.equipment || {}).map(([name, quantity]) => ({ name, quantity })), + }); + } else { + alert(`Update failed: ${result.message}`); + } + }; + + + // داخل AccountProfile + + const collaborationOptions = [ + { value: 'fine_dining', label: 'Fine Dining' }, + { value: 'catering_services', label: 'Catering Services' }, + { value: 'fast_food', label: 'Fast Food' }, + { value: 'cloud_kitchen', label: 'Cloud Kitchen' } + ]; + + + return ( + + Restaurant Profile + + {/* صورة البروفايل */} + {/* + + */} + + + + {/* الحقول */} + + + {renderField('Restaurant Name', form.restaurant_name, 'restaurant_name')} + + {renderField('Restaurant Type', form.restaurant_type, 'restaurant_type')} + + + {renderField('Address', form.Address, 'Address')} + {renderField('City', form.City, 'City')} + + + {renderField('Postal Code', form.Postal_code, 'Postal_code')} + {renderField('Phone', form.Phone, 'Phone')} + + + {renderField('Email', form.Email, 'Email')} + {renderField('Operation Hours', form.Operation_hour, 'Operation_hour')} + + + {renderField('Host Type', form.host_type, 'host_type')} + + Collaboration Type + {editMode ? ( + + ) : ( + + {form.collaboration_type ? collaborationOptions.find(opt => opt.value === form.collaboration_type)?.label : 'None'} + + )} + + + + + {renderField('Closed Days', form.closed_days, 'closed_days', daysOfWeek)} + + + {/* القدرة التشغيلية */} + + {renderField('Maximum Orders per Day', form.Maximum_orders_per_day, 'Maximum_orders_per_day')} + {renderField('Number of Chefs', form.Number_of_Cheff, 'Number_of_Cheff')} + + + {renderField('Number of Waiters', form.Number_of_Waiters, 'Number_of_Waiters')} + {renderField('Number of Cookers', form.Number_of_Cookers, 'Number_of_Cookers')} + + + {/* المعدات */} + + Equipment Name + {form.equipment.map((eq, index) => ( + + handleEquipmentChange(index, 'name', e.target.value)} + fullWidth + variant="outlined" + disabled={!editMode} + sx={{ '& input': { fontWeight: 500, fontSize: '16px' } }} + /> + handleEquipmentChange(index, 'quantity', Number(e.target.value))} + fullWidth + variant="outlined" + disabled={!editMode} + sx={{ '& input': { fontWeight: 500, fontSize: '16px' } ,borderRadius:20}} + /> + {editMode && ( + + )} + + ))} + {editMode && ( + + )} + + + + {/* أزرار التحكم */} + + {editMode ? ( + <> + + + + ) : ( + + )} + + + ); +}; + +export default AccountProfile; diff --git a/src/components/Home/Settings/AccountSettings.js b/src/components/Home/Settings/AccountSettings.js deleted file mode 100644 index fd07243..0000000 --- a/src/components/Home/Settings/AccountSettings.js +++ /dev/null @@ -1,217 +0,0 @@ -import React, { useState } from 'react'; -import { - Box, - Typography, - TextField, - Button, - styled, - useTheme -} from '@mui/material'; -import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined'; - -const AccountSettings = () => { - const theme = useTheme(); - const [profileImage, setProfileImage] = useState(null); - const [fullName, setFullName] = useState(''); - const [username, setUsername] = useState(''); - const [bio, setBio] = useState(''); - const [email, setEmail] = useState(''); - const [phone, setPhone] = useState(''); - - const handleImageUpload = (event) => { - if (event.target.files && event.target.files[0]) { - setProfileImage(URL.createObjectURL(event.target.files[0])); - } - }; - - const Input = styled('input')({ - display: 'none', - }); - - const customInputStyle = { - '& input': { fontWeight: 500, fontSize: '15px' }, - '& input::placeholder': { color: '#969BA7' }, - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - transition: '0.3s', - '&.Mui-focused fieldset': { - borderColor: theme.palette.primary.main, - boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)', - }, - }, - '& .MuiOutlinedInput-root.Mui-focused': { - borderColor: '#3f51b5', - boxShadow: '0 0 0 2px rgba(63,81,181,0.1)', - }, - }; - - const renderField = (label, value, onChange, type = 'text') => ( - - - {label} - - - - ); - - return ( - - {/* العنوان الرئيسي */} - - Account Setting - - - {/* صورة الملف الشخصي */} - - Your Profile Picture - - - - - - - {/* الحقول النصية */} - - - Personal Information - - - {/* السطر الأول: Full Name + Email */} - - {renderField('Full Name', fullName, (e) => setFullName(e.target.value))} - {renderField('Email', email, (e) => setEmail(e.target.value), 'email')} - - - {/* السطر الثاني: Username + Phone Number */} - - {renderField('Username', username, (e) => setUsername(e.target.value))} - {renderField('Phone Number', phone, (e) => setPhone(e.target.value), 'tel')} - - - {/* الحقل Bio منفصل */} - - - Bio - - setBio(e.target.value)} - fullWidth - multiline - rows={3} - sx={customInputStyle} - /> - - - - {/* الأزرار */} - - - - - - ); -}; - -export default AccountSettings; diff --git a/src/components/Home/Settings/Setting.js b/src/components/Home/Settings/Setting.js index 8c9d906..3c121e3 100644 --- a/src/components/Home/Settings/Setting.js +++ b/src/components/Home/Settings/Setting.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Box, useTheme, useMediaQuery } from '@mui/material'; import KitchPlusAppBar from '../AppBar'; import Sidebar from '../SideHome'; -import AccountSettings from './AccountSettings'; +import AccountSettings from './AccountProfile'; const drawerWidth = 230; diff --git a/src/components/Home/SideHome.js b/src/components/Home/SideHome.js index 396e0e6..daa5c08 100644 --- a/src/components/Home/SideHome.js +++ b/src/components/Home/SideHome.js @@ -10,39 +10,28 @@ import { useTheme, Typography } from '@mui/material'; -import { - DashboardRounded as DashboardIcon, - PointOfSale as CashierIcon, - AllInbox as SupplierIcon, - Store as InventoryIcon, - School as TrainingIcon, - AutoGraph as AnalyticsIcon, - Restaurant as RestaurantIcon, - CountertopsTwoTone as HostKitchenIcon, - Queue as CreateKitchenIcon, - Settings as SettingsIcon, - Logout as LogoutIcon -} from '@mui/icons-material'; +import DashboardIcon from '@mui/icons-material/DashboardRounded'; +import AnalyticsIcon from '@mui/icons-material/AutoGraph'; +import RestaurantIcon from '@mui/icons-material/Restaurant'; +import Employ from '@mui/icons-material/CountertopsTwoTone'; +import CreateKitchenIcon from '@mui/icons-material/Queue'; +import LogoutIcon from '@mui/icons-material/Logout'; +import CategoryIcon from '@mui/icons-material/Category'; +import authService from '../../services/authService'; -import authService from '../../services/authService'; // تأكد من المسار الصحيح const menuItems = [ - { text: 'Dashboard', icon: , path: '/dashboard' }, - { text: 'Cashier', icon: , path: '/cashier' }, - { text: 'Supplier', icon: , path: '/supplier' }, - { text: 'Inventory', icon: , path: '/inventory' }, - { text: 'Training', icon: , path: '/training' }, - { text: 'Analytics & Reporting', icon: , path: '/analytics' }, + { text: 'Order & Occupancy', icon: , path: '/dashboard' }, + { text: 'Tabels & Reservations', icon: , path: '/analytics' }, { text: 'Restaurant Profile', icon: , path: '/profile' }, - // { text: 'Host Kitchen', icon: , path: '/host-kitchen' }, + { text: 'Employ', icon: , path: '/employ' }, { text: 'Create Kitchen', icon: , path: '/create-kitchen' }, + { text: 'Catigores & Meals', icon: , path: '/categori-meal' }, ]; const bottomItems = [ - { text: 'Settings', icon: , path: '/settings' }, { text: 'Log Out', icon: , path: '/login' }, ]; - const Sidebar = ({ open, onClose, isMobile, drawerWidth }) => { const theme = useTheme(); const navigate = useNavigate(); @@ -175,7 +164,6 @@ const Sidebar = ({ open, onClose, isMobile, drawerWidth }) => { overflowY: 'auto', scrollbarWidth: 'none', '&::-webkit-scrollbar': { display: 'none' }, - py: 1, }} > {renderListItems(menuItems)} diff --git a/src/components/Home/SimplePagination.js b/src/components/Home/SimplePagination.js new file mode 100644 index 0000000..3cd4628 --- /dev/null +++ b/src/components/Home/SimplePagination.js @@ -0,0 +1,71 @@ +// SimplePagination.jsx +import React from 'react'; +import { Box, IconButton, useTheme } from '@mui/material'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; + +const SimplePagination = ({ currentPage, pageCount, onChange }) => { + const theme = useTheme(); + + const handlePrev = () => { + if (currentPage > 1) onChange(currentPage - 1); + }; + + const handleNext = () => { + if (currentPage < pageCount) onChange(currentPage + 1); + }; + + return ( + + + + + + + {currentPage} + + + = pageCount} + sx={{ + borderRadius: '8px', + backgroundColor: '#FFECE0', + '&:hover': { backgroundColor: '#FFD6B5' }, + color: theme.palette.primary.main, + '&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' }, + }} + > + + + + ); +}; + +export default SimplePagination; diff --git a/src/components/Home/Supplier/Supplier.js b/src/components/Home/Supplier/Supplier.js index 76ef908..e305554 100644 --- a/src/components/Home/Supplier/Supplier.js +++ b/src/components/Home/Supplier/Supplier.js @@ -1,67 +1,122 @@ +import React, { useState, useEffect } from "react"; +import { Box, useTheme, useMediaQuery } from "@mui/material"; +import KitchPlusAppBar from "../AppBar"; +import Sidebar from "../SideHome"; -import React, { useState, useEffect } from 'react'; -import { Box, useTheme, useMediaQuery, Typography } from '@mui/material'; -import KitchPlusAppBar from '../AppBar'; -import Sidebar from '../SideHome'; +import AccountSettings from "./contect/AccountSettings"; +import AllProducts from "./contect/AllProducts"; +import PopularProductSection from "./contect/PopularProductSection"; +import ProductDetail from "./contect/ProductDetail"; + +import api from "../../../services/authService"; + +const IMAGE_URL = "http://127.0.0.1:8000/storage/"; const drawerWidth = 230; const Supplier = () => { const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const [hasProducts, setHasProducts] = useState(false); // حالة لتتبع وجود المنتجات + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const [sidebarOpen, setSidebarOpen] = useState(!isMobile); + const [categories, setCategories] = useState([]); + const [products, setProducts] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(null); + const [selectedCategoryName, setSelectedCategoryName] = useState(null); + + const [selectedProduct, setSelectedProduct] = useState(null); + + const [showAllPopular, setShowAllPopular] = useState(false); + const [showAllTopItems, setShowAllTopItems] = useState(false); useEffect(() => { - const checkProducts = async () => { - - const productsExist = await checkIfProductsExist(); - setHasProducts(productsExist); - }; - - checkProducts(); - }, []); - - - const checkIfProductsExist = async () => { - return false; - }; - - useEffect(() => { - if (window.innerWidth >= theme.breakpoints.values.md) { - setSidebarOpen(true); - } else { - setSidebarOpen(false); - } + setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md); }, [theme.breakpoints.values.md]); useEffect(() => { const handleResize = () => { - if (window.innerWidth >= theme.breakpoints.values.md) { - setSidebarOpen(true); - } else { - setSidebarOpen(false); + setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md); + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [theme.breakpoints.values.md]); + + useEffect(() => { + const fetchCategories = async () => { + try { + const res = await api.getAllCategory(); + if (res.success) { + const formatted = res.data.map((category) => ({ + id: category.id, + name: category.name, + icon: category.image + ? IMAGE_URL + category.image + : "/images/default-product.png", + })); + setCategories(formatted); + } + } catch (err) { + console.error("❌ Error fetching categories:", err); } }; - handleResize(); - window.addEventListener('resize', handleResize); + fetchCategories(); + }, []); - return () => window.removeEventListener('resize', handleResize); - }, [theme.breakpoints.values.md]); + useEffect(() => { + const fetchProducts = async () => { + if (!selectedCategory) return; + + try { + const res = await api.getProductsByCategory(selectedCategory, 1); + if (res.success) { + const formatted = res.data.map((product) => ({ + id: product.id, + name: product.name, + category: product.supplier_category_id, + price: product.price, + unit: "Kg", + image: product.image + ? IMAGE_URL + product.image + : "/images/default-product.png", + })); + setProducts(formatted); + + const categoryName = categories.find( + (cat) => cat.id === selectedCategory + )?.name; + setSelectedCategoryName(categoryName || ""); + } + } catch (err) { + console.error("Error fetching products:", err); + } + }; + + fetchProducts(); + }, [selectedCategory, categories]); const handleDrawerToggle = () => { setSidebarOpen(!sidebarOpen); }; + const toggleShowAllPopular = () => { + setShowAllPopular((prev) => !prev); + }; + + const toggleShowAllTopItems = () => { + setShowAllTopItems((prev) => !prev); + }; + return ( - + { drawerWidth={drawerWidth} /> - + - Supplier + + + {selectedProduct ? ( + setSelectedProduct(null)} + /> + ) : ( + <> + + + {selectedCategory ? ( + + setSelectedCategory(null)} + onProductClick={setSelectedProduct} + categoryName={selectedCategoryName} + /> + + ) : !showAllPopular && !showAllTopItems ? ( + + + + + + ) : showAllPopular ? ( + + + + ) : showAllTopItems ? ( + + + ) : null} + + )} + ); }; -export default Supplier; \ No newline at end of file +export default Supplier; diff --git a/src/components/Home/Supplier/Supplieree.js b/src/components/Home/Supplier/Supplieree.js new file mode 100644 index 0000000..593ab8c --- /dev/null +++ b/src/components/Home/Supplier/Supplieree.js @@ -0,0 +1,247 @@ +import React, { useState, useEffect } from 'react'; +import { Box, useTheme, useMediaQuery } from '@mui/material'; +import KitchPlusAppBar from '../AppBar'; +import Sidebar from '../SideHome'; + +// بدل هذه المكونات عند عرض التفاصيل +import AccountSettings from './contect/AccountSettings'; +import AllProducts from './contect/AllProducts'; +import PopularProductSection from './contect/PopularProductSection'; +import TopItems from './contect/TopItems'; +import OrderList from './contect/OrderList'; +import ProductDetail from './contect/ProductDetail'; // 👈 مكون التفاصيل +//import auth servise +import api from '../../../services/authService'; +const IMAGE_URL = 'http://127.0.0.1:8000/storage/' ; + + +const categoriesInit0 = await api.getAllCategory(); +const categoriesInit = categoriesInit0.data.map(category => ({ + id: category.id, + name: category.name, + icon: category.image || "/images/default-product.png", +})); + + +const productList0 = await api.getAllProduct(); +const productList = productList0.data.map(product => ({ + id: product.id, + name: product.name, + category: product.supplier_category_id, + price: product.price, + unit: "Kg", + image: IMAGE_URL + product.image || "/images/default-product.png", +})); + +// const productList = [ +// { id: 1, name: 'Cabbage 1', category: 'Vegetable', price: '15.10', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 2, name: 'vegetables 2', category: 'Vegetable', price: '8.34', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 3, name: 'Brocoly 3', category: 'Vegetable', price: '5.60', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 4, name: 'Onion 4', category: 'Vegetable', price: '6.45', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 5, name: 'Bread 5', category: 'Bread', price: '3.50', unit: '/pcs', image: '/images/waitress2.png' }, +// { id: 6, name: 'Meat 6', category: 'Meat', price: '20.00', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 7, name: 'Cabbage', category: 'Vegetable', price: '15.10', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 8, name: 'vegetables', category: 'Vegetable', price: '8.34', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 9, name: 'Brocoly', category: 'Vegetable', price: '5.60', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 10, name: 'Onion', category: 'Vegetable', price: '6.45', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 11, name: 'Bread', category: 'Bread', price: '3.50', unit: '/pcs', image: '/images/waitress2.png' }, +// { id: 12, name: 'Meat', category: 'Meat', price: '20.00', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 13, name: 'Cabbage', category: 'Vegetable', price: '15.10', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 14, name: 'vegetables', category: 'Vegetable', price: '8.34', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 15, name: 'Brocoly', category: 'Vegetable', price: '5.60', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 16, name: 'Onion', category: 'Vegetable', price: '6.45', unit: '/kg', image: '/images/waitress2.png' }, +// { id: 17, name: 'Bread', category: 'Bread', price: '3.50', unit: '/pcs', image: '/images/waitress2.png' }, +// { id: 18, name: 'Meat', category: 'Meat', price: '20.00', unit: '/kg', image: '/images/waitress2.png' }, +// ]; + +const item = [ + { name: 'Cabbage', price: '15.10', unit: '/kg', image: '/images/cabbage.png' }, + { name: 'Kale vegetables', price: '8.34', unit: '/kg', image: '/images/kale.png' }, + { name: 'Brocoly', price: '5.60', unit: '/kg', image: '/images/broccoli.png' }, + { name: 'Celery', price: '4.80', unit: '/kg', image: '/images/celery.png' }, + { name: 'Onion', price: '6.45', unit: '/kg', image: '/images/onion.png' }, + { name: 'Garlic', price: '9.90', unit: '/kg', image: '/images/garlic.png' }, +]; + +const drawerWidth = 230; + +const Supplier = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [sidebarOpen, setSidebarOpen] = useState(!isMobile); + + // حالات العرض + const [showAllPopular, setShowAllPopular] = useState(false); + const [showAllTopItems, setShowAllTopItems] = useState(false); + const [selectedCategory, setSelectedCategory] = useState(null); + + // ✅ المنتج المختار + const [selectedProduct, setSelectedProduct] = useState(null); + + useEffect(() => { + setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md); + }, [theme.breakpoints.values.md]); + + useEffect(() => { + const handleResize = () => { + setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [theme.breakpoints.values.md]); + + const handleDrawerToggle = () => { + setSidebarOpen(!sidebarOpen); + }; + + const toggleShowAllPopular = () => { + setShowAllPopular(prev => !prev); + }; + + const toggleShowAllTopItems = () => { + setShowAllTopItems(prev => !prev); + }; + + // اسم الفئة + const selectedCategoryName = selectedCategory + ? categoriesInit.find(cat => cat.id === selectedCategory)?.name + : null; + + return ( + + + + + + + + {/* ✅ إذا تم اختيار منتج نظهر تفاصيله فقط */} + {selectedProduct ? ( + setSelectedProduct(null)} /> + ) : ( + <> + + + {selectedCategory ? ( + + p.category.toLowerCase() === selectedCategoryName?.toLowerCase() + )} + showAll={true} + onToggleShowAll={() => setSelectedCategory(null)} + onProductClick={setSelectedProduct} + categoryName={selectedCategoryName} // ✅ بدل القيمة الثابتة + /> + + + ) : !showAllPopular && !showAllTopItems ? ( + + + + {/* + + */} + + + {/* + + + + */} + + ) : showAllPopular ? ( + + + + ) : showAllTopItems ? ( + + {/* */} + + ) : null} + + )} + + + + ); +}; + +export default Supplier; diff --git a/src/components/Home/Supplier/contect/AccountSettings.js b/src/components/Home/Supplier/contect/AccountSettings.js new file mode 100644 index 0000000..a00e5d0 --- /dev/null +++ b/src/components/Home/Supplier/contect/AccountSettings.js @@ -0,0 +1,307 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { + Menu, + MenuItem, + Modal, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + useMediaQuery, + Box, + Typography, + Button, + useTheme, + IconButton, +} from '@mui/material'; +import AddCategory from './AddCategory'; // مكون لإضافة/تعديل الفئة (يفترض موجود) +import AddIcon from '@mui/icons-material/Add'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import TuneIcon from '@mui/icons-material/Tune'; +import CategoryScrollList from './CategoryScrollList'; // مكون لعرض الفئات بشكل أفقي مع تمرير +//import auth servise +import api from '../../../../services/authService'; + +const IMAGE_URL = 'http://127.0.0.1:8000/storage/' ; + +const categoriesInit0 = await api.getAllCategory(); +const categoriesInit = categoriesInit0.data.map(category => ( + { + id: category.id, + name: category.name, + icon: IMAGE_URL + category.icon || "/images/default-product.png", +})); + + +const AccountSettings = ({ selectedCategory, setSelectedCategory }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const [categories, setCategories] = useState(categoriesInit); + const [selectedCategoryForEdit, setSelectedCategoryForEdit] = useState(null); + const [contextMenu, setContextMenu] = useState(null); + const [openAddModal, setOpenAddModal] = useState(false); + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + + const scrollRef = useRef(); + + const scroll = (offset) => { + if (scrollRef.current) { + scrollRef.current.scrollLeft += offset; + } + }; + + const handleAddCategory = (newCategory) => { + setCategories((prev) => [...prev, { ...newCategory, id: Date.now() }]); + setOpenAddModal(false); + }; + + const handleUpdateCategory = (updatedCategory) => { + setCategories((prev) => + prev.map((cat) => + cat.id === selectedCategoryForEdit.id ? { ...cat, ...updatedCategory } : cat + ) + ); + setOpenAddModal(false); + }; + + const handleDeleteCategory = () => { + setCategories((prev) => prev.filter((cat) => cat.id !== selectedCategoryForEdit.id)); + setConfirmDeleteOpen(false); + setSelectedCategoryForEdit(null); + + if (selectedCategory === selectedCategoryForEdit?.id) { + setSelectedCategory(null); // أبطل اختيار الفئة + } + }; + + + useEffect(() => { + if (!openAddModal) { + setSelectedCategoryForEdit(null); + } + }, [openAddModal]); + + return ( + + {/* رأس القسم */} + + + Categories + + + + {/* */} + + + + scroll(-200)} + sx={{ + backgroundColor: theme.palette.primary.main, + color: '#fff', + width: 40, + height: 40, + '&:hover': { backgroundColor: '#e96b00' }, + }} + > + + + + scroll(200)} + sx={{ + backgroundColor: theme.palette.primary.main, + color: '#fff', + width: 40, + height: 40, + '&:hover': { backgroundColor: '#e96b00' }, + }} + > + + + + + + {/* قائمة التمرير للفئات */} + + + + + + + {/* مودال الإضافة / التعديل */} + setOpenAddModal(false)} + aria-labelledby="add-category-modal" + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + p: 2, + }} + > + + + + + + {/* قائمة السياق (الزر الأيمن) */} + setContextMenu(null)} + anchorReference="anchorPosition" + anchorPosition={ + contextMenu !== null + ? { top: contextMenu.mouseY, left: contextMenu.mouseX } + : undefined + } + sx={{ zIndex: 2000 }} + > + { + setOpenAddModal(true); + setContextMenu(null); + }} + > + Edit Category + + { + setConfirmDeleteOpen(true); + setContextMenu(null); + }} + sx={{ color: 'red' }} + > + Delete Category + + + + {/* تأكيد الحذف */} + setConfirmDeleteOpen(false)}> + Confirm Delete + + Are you sure you want to delete this category? + + + + + + + + ); +}; + +export default AccountSettings; diff --git a/src/components/Home/Supplier/contect/AddCategory.js b/src/components/Home/Supplier/contect/AddCategory.js new file mode 100644 index 0000000..8244f62 --- /dev/null +++ b/src/components/Home/Supplier/contect/AddCategory.js @@ -0,0 +1,186 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Button, TextField, Typography, IconButton } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined'; + +const AddCategory = ({ onAdd, editingCategory }) => { + const [productName, setProductName] = useState(''); + const [description, setDescription] = useState(''); + const [price, setPrice] = useState(''); + const [discount, setDiscount] = useState(''); + const [productImage, setProductImage] = useState(null); + + useEffect(() => { + if (editingCategory) { + setProductName(editingCategory.name || ''); + setDescription(editingCategory.description || ''); + setPrice(editingCategory.price || ''); + setDiscount(editingCategory.discount || ''); + setProductImage(editingCategory.icon || null); + } + }, [editingCategory]); + + const handleImageUpload = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setProductImage(reader.result); + }; + reader.readAsDataURL(file); + } + }; + + const handleRemoveImage = () => { + setProductImage(null); + }; + + const handleSubmit = () => { + const newCategory = { + name: productName, + description, + price, + discount, + icon: productImage, + }; + + if (onAdd) { + onAdd(newCategory); + } + + // إعادة تعيين الحقول فقط عند الإضافة الجديدة + if (!editingCategory) { + setProductName(''); + setDescription(''); + setPrice(''); + setDiscount(''); + setProductImage(null); + } + }; + + return ( + + {/* ✅ صورة التصنيف */} + {productImage ? ( + + Preview + + + + + ) : ( + + Upload Menu Picture + + + + )} + + + + Categories name + + setProductName(e.target.value)} + margin="normal" + sx={{ + '& input': { fontWeight: 500, fontSize: '15px' }, + '& input::placeholder': { color: '#969BA7' }, + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + transition: '0.3s', + '&.Mui-focused fieldset': { + borderColor: '#FF914D', + boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' + } + }, + '& .MuiOutlinedInput-root.Mui-focused': { + borderColor: '#FF914D', + boxShadow: '0 0 0 2px rgba(255,145,77,0.15)' + } + }} + /> + + + + + ); +}; + +export default AddCategory; diff --git a/src/components/Home/Supplier/contect/AllProducts.js b/src/components/Home/Supplier/contect/AllProducts.js new file mode 100644 index 0000000..d0c66c0 --- /dev/null +++ b/src/components/Home/Supplier/contect/AllProducts.js @@ -0,0 +1,93 @@ +import React from "react"; +import { Box, Typography } from "@mui/material"; +import ProductCard from "./ProductCard"; + +const AllProducts = ({ products, showAll, onToggleShowAll, onProductClick, categoryName }) => { + const visibleProducts = showAll ? products : products.slice(0, 6); + + return ( + + {/* العنوان */} + + + {categoryName ? categoryName : "All Products"} + + + + + + + {showAll ? "Show Less" : "See All"} + + + + {/* عرض المنتجات أو رسالة لا يوجد منتجات */} + {visibleProducts.length === 0 ? ( + + + {categoryName + ? `There is no product in ${categoryName}` + : "There is no product"} + + + ) : ( + + {visibleProducts.map((product, index) => ( + onProductClick(product)} + > + + + ))} + + )} + + ); +}; + +export default AllProducts; diff --git a/src/components/Home/Supplier/contect/CategoryScrollList.js b/src/components/Home/Supplier/contect/CategoryScrollList.js new file mode 100644 index 0000000..7319a02 --- /dev/null +++ b/src/components/Home/Supplier/contect/CategoryScrollList.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { Box, Avatar, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; + +const CategoryScrollList = ({ + categories, + selectedCategory, + setSelectedCategory, + setSelectedCategoryForEdit, + setContextMenu, + scrollRef, +}) => { + const theme = useTheme(); + + return ( + + + {categories.map((cat) => ( + setSelectedCategory(cat.id)} + onContextMenu={(e) => { + e.preventDefault(); + setSelectedCategoryForEdit(cat); + setContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6 }); + }} + > + + + + {cat.name} + + + ))} + + + ); +}; + +export default CategoryScrollList; diff --git a/src/components/Home/Supplier/contect/OrderList.js b/src/components/Home/Supplier/contect/OrderList.js new file mode 100644 index 0000000..10d4446 --- /dev/null +++ b/src/components/Home/Supplier/contect/OrderList.js @@ -0,0 +1,359 @@ +import React, { useState } from 'react'; +import { + Box, + Typography, + Paper, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Chip, + IconButton, + TableContainer, + Button, + Menu, + MenuItem, +} from '@mui/material'; +import TuneIcon from '@mui/icons-material/Tune'; +import ReplayIcon from '@mui/icons-material/Replay'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import { useTheme } from '@mui/material/styles'; +import { useMediaQuery } from '@mui/material'; + +const SimplePagination = ({ currentPage, pageCount, onChange }) => { + const theme = useTheme(); + + const handlePrev = () => { + if (currentPage > 1) onChange(currentPage - 1); + }; + + const handleNext = () => { + if (currentPage < pageCount) onChange(currentPage + 1); + }; + + return ( + + + + + + + {currentPage} + + + = pageCount} + sx={{ + borderRadius: '8px', + backgroundColor: '#FFECE0', + '&:hover': { backgroundColor: '#FFD6B5' }, + color: theme.palette.primary.main, + '&.Mui-disabled': { + color: '#ccc', + backgroundColor: '#FFF5E6', + }, + }} + > + + + + ); +}; + +const OrderList = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const initialOrders = [ + { restaurant: 'المطعم الاول المطعم الاول ', time: '5:00 Pm', status: 'Pending' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Delivered' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Completed' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'In-Progress' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Delivered' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Pending' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Completed' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Delivered' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Pending' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Delivered' }, + { restaurant: 'Al-Baik', time: '5:00 Pm', status: 'Completed' }, + ]; + + const [currentPage, setCurrentPage] = useState(1); + const [filterAnchorEl, setFilterAnchorEl] = useState(null); + const [statusFilter, setStatusFilter] = useState('all'); // 'all' يعني بدون فلتر + + const itemsPerPage = 5; + + // افتح قائمة الفلتر + const handleFilterClick = (event) => { + setFilterAnchorEl(event.currentTarget); + }; + + // اغلق قائمة الفلتر + const handleFilterClose = () => { + setFilterAnchorEl(null); + }; + + // عند اختيار فلتر جديد + const handleFilterSelect = (filter) => { + setStatusFilter(filter); + setCurrentPage(1); // العودة للصفحة الأولى بعد تغيير الفلتر + handleFilterClose(); + }; + + // تصفية البيانات حسب الفلتر + const filteredOrders = + statusFilter === 'all' + ? initialOrders + : initialOrders.filter( + (order) => order.status.toLowerCase() === statusFilter.toLowerCase() + ); + + const pageCount = Math.ceil(filteredOrders.length / itemsPerPage); + + const paginatedData = filteredOrders.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + const getStatusColor = (status) => { + switch (status) { + case 'Pending': + return { bg: '#FFF3E0', color: '#E65100' }; + case 'Delivered': + return { bg: '#E8F5E9', color: '#2E7D32' }; + case 'Completed': + return { bg: '#F3E5F5', color: '#6A1B9A' }; + case 'In-Progress': + return { bg: '#F1F8E9', color: '#9E9D24' }; + default: + return { bg: '#E0E0E0', color: '#424242' }; + } + }; + + const rowHeight = 73; // ارتفاع الصف الواحد تقريبي + const emptyRowsCount = itemsPerPage - paginatedData.length; + + return ( + + {/* Header */} + + + Order List + + + + {/* قائمة الفلاتر */} + + {['all', 'Pending', 'Delivered', 'Completed', 'In-Progress'].map((status) => ( + handleFilterSelect(status)} + sx={{ + color: statusFilter.toLowerCase() === status.toLowerCase() ? '#FF914D' : '#4F5867', + backgroundColor: + statusFilter.toLowerCase() === status.toLowerCase() ? '#fffcf9d5' : 'transparent', + '&:hover': { + backgroundColor: '#f0f0f0ff', + transition: 'background-color 150ms ease-in-out', + }, + }} + > + {status.charAt(0).toUpperCase() + status.slice(1)} + + ))} + + + + {/* Table */} + + + + + Restaurant + Time + Status + Action + + + + + {paginatedData.length > 0 ? ( + paginatedData.map((row, i) => { + const colors = getStatusColor(row.status); + return ( + + + {row.restaurant} + + + {row.time} + + + + + + + + + + + + + + + ); + }) + ) : ( + + + No orders found. + + + )} + + {/* صفوف تعويضية فارغة لتثبيت ارتفاع الجدول */} + {emptyRowsCount > 0 && + [...Array(emptyRowsCount)].map((_, idx) => ( + + + + ))} + +
+
+ + {/* Pagination */} + + + Showing {(currentPage - 1) * itemsPerPage + 1} -{' '} + {Math.min(currentPage * itemsPerPage, filteredOrders.length)} of {filteredOrders.length} + + + + +
+ ); +}; + +export default OrderList; diff --git a/src/components/Home/Supplier/contect/PopularProductSection.js b/src/components/Home/Supplier/contect/PopularProductSection.js new file mode 100644 index 0000000..8196a94 --- /dev/null +++ b/src/components/Home/Supplier/contect/PopularProductSection.js @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import { Box, Typography, Grid, useTheme, IconButton } from '@mui/material'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import ProductCard from './ProductCard'; + +const ITEMS_PER_PAGE = 10; + +/* مكوّن الباجينيشن */ +const SimplePagination = ({ currentPage, pageCount, onChange }) => { + const theme = useTheme(); + + const handlePrev = () => { + if (currentPage > 1) onChange(currentPage - 1); + }; + + const handleNext = () => { + if (currentPage < pageCount) onChange(currentPage + 1); + }; + + return ( + + + + + + + {currentPage} + + + = pageCount} + sx={{ + borderRadius: '8px', + backgroundColor: '#FFECE0', + '&:hover': { backgroundColor: '#FFD6B5' }, + color: theme.palette.primary.main, + '&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' }, + }} + > + + + + ); +}; + +/* قسم المنتجات */ +const PopularProductSection = ({ products = [], onProductClick }) => { + const theme = useTheme(); + const [currentPage, setCurrentPage] = useState(1); + + const totalPages = Math.ceil(products.length / ITEMS_PER_PAGE); + const startIdx = (currentPage - 1) * ITEMS_PER_PAGE; + const visibleProducts = products.slice(startIdx, startIdx + ITEMS_PER_PAGE); + + return ( + + {/* الهيدر */} + + All Products + + + {/* المنتجات */} + + {visibleProducts.length > 0 ? ( + visibleProducts.map((product, index) => ( + + onProductClick && onProductClick(product)} + /> + + )) + ) : ( + There is no Product + )} + + + {/* الباجينيشن */} + {totalPages > 1 && ( + + + Showing {startIdx + 1} - {Math.min(startIdx + ITEMS_PER_PAGE, products.length)} of {products.length} + + + + + )} + + ); +}; + +export default PopularProductSection; diff --git a/src/components/Home/Supplier/contect/ProductCard.js b/src/components/Home/Supplier/contect/ProductCard.js new file mode 100644 index 0000000..5a1cada --- /dev/null +++ b/src/components/Home/Supplier/contect/ProductCard.js @@ -0,0 +1,117 @@ +import React from 'react'; +import { Box, Typography, Card, CardContent, CardMedia, IconButton } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; + +const ProductCard = ({ name, price, discountedPrice, unit, image, onClick, onAdd }) => { + return ( + + {/* صورة المنتج */} + + + {/* تفاصيل المنتج */} + + + {name} + + + + {discountedPrice ? ( + <> + + ${price} + + + ${discountedPrice}{' '} + + {unit} + + + + ) : ( + + ${price}{' '} + + {unit} + + + )} + + {/* زر الإضافة */} + { + e.stopPropagation(); // يمنع فتح التفاصيل عند الضغط على الزر + if (onAdd) onAdd(); + }} + sx={{ + backgroundColor: '#FF8551', + color: '#fff', + ml: 3, + width: 28, + height: 28, + '&:hover': { backgroundColor: '#ff7043' }, + }} + > + + + + + + ); +}; + +export default ProductCard; diff --git a/src/components/Home/Supplier/contect/ProductDetail.js b/src/components/Home/Supplier/contect/ProductDetail.js new file mode 100644 index 0000000..318a155 --- /dev/null +++ b/src/components/Home/Supplier/contect/ProductDetail.js @@ -0,0 +1,94 @@ +import React, { useState, useContext, useEffect } from "react"; +import { Box, Typography, Button, IconButton, LinearProgress, useTheme } from "@mui/material"; +import RemoveIcon from "@mui/icons-material/Remove"; +import AddIcon from "@mui/icons-material/Add"; +import ShoppingCartOutlinedIcon from "@mui/icons-material/ShoppingCartOutlined"; +import { CartContext } from "../../../../contexts/CartContextR"; +// import { useRestaurant } from '../../../contexts/RestaurantContext'; +import api from '../../../../services/authService'; + +const ProductDetail = ({ product, onBack }) => { + const theme = useTheme(); + const { addToCart , createNewCart } = useContext(CartContext); + const [quantity, setQuantity] = useState(1); + const [totalPrice, setTotalPrice] = useState(Number(product?.price) || 0); + + useEffect(() => { + if (product) { + setTotalPrice(Number(product.price) * quantity); + } + }, [quantity, product]); + + if (!product) return null; + + const handleIncrease = () => quantity < 9 && setQuantity(q => q + 1); + const handleDecrease = () => quantity > 1 && setQuantity(q => q - 1); + + const handleAddToCart = () => { + addToCart(product, quantity); + createNewCart(); + alert(`${product.name} added to cart! Quantity: ${quantity}, Total: $${totalPrice.toFixed(2)}`); + }; + + return ( + + + + + {product.name} + {product.category} + + ${Number(product.price).toFixed(2)} {product.unit} + + + {/* 20 Item left */} + + + Quantity + + + {quantity} + + + Maximum purchase 9 + + + Total: ${totalPrice.toFixed(2)} + + + + + + + + + ); +}; + +export default ProductDetail; diff --git a/src/components/Home/Supplier/contect/TopItems.js b/src/components/Home/Supplier/contect/TopItems.js new file mode 100644 index 0000000..5d06d3e --- /dev/null +++ b/src/components/Home/Supplier/contect/TopItems.js @@ -0,0 +1,140 @@ +import React from 'react'; +import { + Box, + Typography, + Card, + CardContent, + CardMedia, + IconButton, + Grid, + useTheme, + useMediaQuery, +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; + +const ProductCard = ({ name, price, unit, image }) => { + return ( + + + + + {name} + + + + ${price}{' '} + + {unit} + + + + + + + + + ); +}; + +const TopItems = ({ item, showAll, onToggleShowAll }) => { + const theme = useTheme(); + const isXs = useMediaQuery(theme.breakpoints.down('sm')); + + const visibleProducts = showAll ? item : item.slice(0, 6); + + const maxHeight = showAll ? 'auto' : isXs ? '340px' : '335px'; + const maxWidth = showAll ? { xs: '90%', sm: '100%' } : undefined; + + return ( + + + + Top Items + + + {showAll ? 'Show Less' : 'See All'} + + + + + {visibleProducts.map((product, index) => ( + + + + ))} + + + ); +}; + +export default TopItems; diff --git a/src/components/Home/Training/NoTraining.js b/src/components/Home/Training/NoTraining.js deleted file mode 100644 index c02c2d9..0000000 --- a/src/components/Home/Training/NoTraining.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { - Box, - Button, - Card, - CardContent, - Typography, - useMediaQuery -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; - - -const NoTraining = () => { - const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - - return ( - <> - {/* Header Section */} - - - Dashboard - - - - {/* Empty State Content */} - - - {/* Image Placeholder */} - - - {/* Title with colored "Sorry!" */} - - Sorry!{' '} - There are no updates now.. - {'\n'}check back later. - - - {/* Action Button */} - - - - - ); -}; - -export default NoTraining; \ No newline at end of file diff --git a/src/components/Home/Training/Training.js b/src/components/Home/Training/Training.js deleted file mode 100644 index 1877735..0000000 --- a/src/components/Home/Training/Training.js +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Box, useTheme, useMediaQuery, Skeleton } from '@mui/material'; -import KitchPlusAppBar from '../AppBar'; -import Sidebar from '../SideHome'; - -import NoTraining from './NoTraining'; - -const drawerWidth = 230; - -const Dashboard = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const [hasProducts, setHasProducts] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [sidebarOpen, setSidebarOpen] = useState(!isMobile); - - // محاكاة التحقق من المنتجات - useEffect(() => { - const checkProducts = async () => { - setIsLoading(true); - const productsExist = await checkIfProductsExist(); // استبدل بمنطقك - setHasProducts(productsExist); - setIsLoading(false); - }; - - checkProducts(); - }, []); - - const checkIfProductsExist = async () => { - return new Promise((resolve) => setTimeout(() => resolve(true), 1500)); // محاكاة تأخير - }; - - useEffect(() => { - if (window.innerWidth >= theme.breakpoints.values.md) { - setSidebarOpen(true); - } else { - setSidebarOpen(false); - } - }, [theme.breakpoints.values.md]); - - useEffect(() => { - const handleResize = () => { - if (window.innerWidth >= theme.breakpoints.values.md) { - setSidebarOpen(true); - } else { - setSidebarOpen(false); - } - }; - - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [theme.breakpoints.values.md]); - - const handleDrawerToggle = () => { - setSidebarOpen(!sidebarOpen); - }; - - return ( - - - - - - - - {isLoading ? ( - <> - - - - - ) : ( - - )} - - - - ); -}; - -export default Dashboard; diff --git a/src/components/Routes/PrivateRoute.js b/src/components/Routes/PrivateRoute.js index b26d9b6..3e5c59d 100644 --- a/src/components/Routes/PrivateRoute.js +++ b/src/components/Routes/PrivateRoute.js @@ -5,7 +5,6 @@ const PrivateRoute = ({ children }) => { const token = localStorage.getItem('token'); if (!token) { - // لو مافي توكن، نعيد التوجيه للصفحة تسجيل الدخول return ; } diff --git a/src/components/Routes/ProtectedRoutes.js b/src/components/Routes/ProtectedRoutes.js new file mode 100644 index 0000000..863f0a6 --- /dev/null +++ b/src/components/Routes/ProtectedRoutes.js @@ -0,0 +1,46 @@ + +import React from 'react'; +import { Routes, Route } from 'react-router-dom'; +import PrivateRoute from './PrivateRoute'; + +import Dashboard from '../Home/Dashboard/Dashboard'; +import Inventory from '../Home/Inventory/Inventory'; +import Analytics from '../Home/Analytics&Reporting/Analytics'; +import Supplier from '../Home/Supplier/Supplier'; +import Cashier from '../Home/Cashier/Cashier'; +import Cart from '../Home/Cart/Cart'; +import Order from '../Home/Order/Order'; +// import Training from '../Home/Training/Training'; +import RestaurantProfile from '../Home/RestaurantProfile/all'; +import Employ from '../Home/Employ/Employ'; +import Settings from '../Home/Settings/Setting'; +import RestaurantSelection from '../Home/ResList/RestaurantProfile'; +import Meal from '../Home/Meal/Meal'; +import CreateYourRestaurant from '../Home/CreateYourRestaurant/CreateRestaurant'; +import CreateNewRestaurant from '../Home/CreateNewRestaurant/CreateRestaurant'; +import { CartProvider } from "../../contexts/CartContextR"; + +const ProtectedRoutes = () => { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +}; + +export default ProtectedRoutes; diff --git a/src/components/Routes/PublicRoute.js b/src/components/Routes/PublicRoute.js index 1030091..ecba7f1 100644 --- a/src/components/Routes/PublicRoute.js +++ b/src/components/Routes/PublicRoute.js @@ -2,11 +2,10 @@ import React from 'react'; import { Navigate } from 'react-router-dom'; const PublicRoute = ({ children }) => { - const token = localStorage.getItem('token'); + const token = localStorage.getItem('token'); if (token) { - // لو فيه توكن، نعيد التوجيه للداشبورد - return ; + return ; } return children; diff --git a/src/components/common/CustomSnackbar.js b/src/components/common/CustomSnackbar.js new file mode 100644 index 0000000..4f7d96d --- /dev/null +++ b/src/components/common/CustomSnackbar.js @@ -0,0 +1,32 @@ +import React from "react"; +import { Snackbar, Alert } from "@mui/material"; + +const CustomSnackbar = ({ open, message, severity, onClose }) => { + return ( + + + {message} + + + ); +}; + +export default CustomSnackbar; diff --git a/src/contexts/CartContextR.js b/src/contexts/CartContextR.js new file mode 100644 index 0000000..65b3752 --- /dev/null +++ b/src/contexts/CartContextR.js @@ -0,0 +1,113 @@ +import React, { createContext, useState, useEffect, useContext } from "react"; +import axios from "axios"; +import { useRestaurant } from "./RestaurantContext"; + +export const CartContext = createContext(); + +const API_BASE_URL = "http://127.0.0.1:8000/api"; + +export const CartProvider = ({ children }) => { + const [cart, setCart] = useState([]); + const { restaurantId } = useRestaurant(); + + useEffect(() => { + const storedCart = localStorage.getItem("cart"); + if (storedCart) setCart(JSON.parse(storedCart)); + }, []); + + useEffect(() => { + localStorage.setItem("cart", JSON.stringify(cart)); + }, [cart]); + + const addToCart = (product, quantity = 1) => { + setCart(prev => { + const existingIdx = prev.findIndex(item => item.id === product.id); + if (existingIdx >= 0) { + const updated = [...prev]; + updated[existingIdx].quantity += quantity; + updated[existingIdx].totalPrice = updated[existingIdx].quantity * product.price; + return updated; + } else { + return [ + ...prev, + { + id: product.id, + name: product.name, + unit: product.unit, + price: product.price, + quantity: quantity, + totalPrice: product.price * quantity + } + ]; + } + }); + }; + + const clearCart = () => { + setCart([]); + localStorage.removeItem("cart"); + }; + + const createNewCart = async () => { + if (cart.length === 0) { + alert("Cart is empty!"); + return; + } + + if (!restaurantId) { + alert("Restaurant ID not found!"); + return; + } + + console.log("restaurantId:", restaurantId); + + const payload = { + data: { + type: "cart", + attributes: { + totalPrice: cart.reduce((sum, item) => sum + item.totalPrice, 0) + }, + relationships: { + restaurant: { + data: { id: restaurantId } // ✅ استخدم المتغير مش رقم ثابت + }, + cartItems: cart.map(item => ({ + attributes: { quantity: item.quantity }, + relationships: { product: { data: { id: item.id } } } + })) + } + } + }; + + + // try { + const token = localStorage.getItem("token"); + if (!token) throw new Error("No token found"); + + const response = await axios.post(`${API_BASE_URL}/Cart_Supplier/carts`, payload, { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + "Content-Type": "application/json" + } + }); + + if (response.data?.data) { + alert("Cart sent to server successfully!"); + console.log(response.data?.data); + clearCart(); + } else { + alert("Failed to create cart on server"); + } + // } catch (error) { + // console.error("Error creating cart:", error); + // alert(error.response?.data?.message || error.message || "Network error"); + // } + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/contexts/RestaurantContext.js b/src/contexts/RestaurantContext.js new file mode 100644 index 0000000..94eb5ac --- /dev/null +++ b/src/contexts/RestaurantContext.js @@ -0,0 +1,26 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +// إنشاء السياق +const RestaurantContext = createContext(); + +export const useRestaurant = () => useContext(RestaurantContext); + +export const RestaurantProvider = ({ children }) => { + const [restaurantId, setRestaurantId] = useState(() => { + // قراءة القيمة من localStorage عند أول تحميل + const storedId = localStorage.getItem('restaurantId'); + return storedId ? Number(storedId) : null; + }); + + // تحديث localStorage عند تغيير restaurantId + const saveRestaurantId = (id) => { + setRestaurantId(id); + localStorage.setItem('restaurantId', id); + }; + + return ( + + {children} + + ); +}; diff --git a/src/contexts/SnackbarContext.js b/src/contexts/SnackbarContext.js new file mode 100644 index 0000000..f5f2ed0 --- /dev/null +++ b/src/contexts/SnackbarContext.js @@ -0,0 +1,35 @@ +// src/context/SnackbarContext.js +import React, { createContext, useContext, useState } from "react"; +import CustomSnackbar from "../components/common/CustomSnackbar"; + +const SnackbarContext = createContext(); + +export const SnackbarProvider = ({ children }) => { + const [snackbar, setSnackbar] = useState({ + open: false, + message: "", + severity: "info", + }); + + const showSnackbar = (message, severity = "info") => { + setSnackbar({ open: true, message, severity }); + }; + + const closeSnackbar = () => { + setSnackbar((prev) => ({ ...prev, open: false })); + }; + + return ( + + {children} + + + ); +}; + +export const useSnackbar = () => useContext(SnackbarContext); diff --git a/src/contexts/UserContext.js b/src/contexts/UserContext.js new file mode 100644 index 0000000..348e085 --- /dev/null +++ b/src/contexts/UserContext.js @@ -0,0 +1,23 @@ +// src/contexts/UserContext.jsx +import React, { createContext, useState, useEffect } from 'react'; + +export const UserContext = createContext(); + +export const UserProvider = ({ children }) => { + const [user, setUser] = useState({ + email: localStorage.getItem('user_email') || '', + adminData: JSON.parse(localStorage.getItem('adminData')) || null, + }); + + useEffect(() => { + // تحديث localStorage عند تغير user + if (user.email) localStorage.setItem('user_email', user.email); + if (user.adminData) localStorage.setItem('adminData', JSON.stringify(user.adminData)); + }, [user]); + + return ( + + {children} + + ); +}; diff --git a/src/index.js b/src/index.js index f037b25..b2b3ef3 100644 --- a/src/index.js +++ b/src/index.js @@ -2,10 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; +import { GoogleOAuthProvider } from '@react-oauth/google'; + const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + - + ); diff --git a/src/services/authService.js b/src/services/authService.js index a8d40a5..6fda20d 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -1,47 +1,161 @@ import axios from 'axios'; -const API_BASE_URL = 'http://127.0.0.1:8000/api'; - +const API_BASE_URL = 'http://192.168.196.165:8000/api'; +const BASE_URL = process.env.REACT_APP_BASE_URL || 'http://192.168.196.165:8000'; +// const API_BASE_URL = 'https://kitchplus.velocrea.com/api'; +// const BASE_URL = 'https://kitchplus.velocrea.com/'; const authService = { - - register: async (email, password, confirmPassword) => { + // ======================= + // تسجيل المسؤول + // ======================= + register: async (email, password, password_confirmation) => { try { const response = await axios.post(`${API_BASE_URL}/Admin/register`, { email, password, - password_confirmation: confirmPassword + password_confirmation, }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); + if (response.data?.success && response.data.data?.Admin) { + localStorage.setItem('user_email', email); + localStorage.setItem('token', response.data.data.token); + localStorage.setItem('refresh_token', response.data.data.refresh_token); + localStorage.setItem('adminData', JSON.stringify(response.data.data.Admin)); + } + return response.data; } catch (error) { - // في حال كان الرد يحتوي على رسائل خطأ من الباك - if (error.response) { - return { - success: false, - status: error.response.status, - message: error.response.data.message || 'Registration failed', - errors: error.response.data.errors || null - }; - } - - // في حال كانت المشكلة من الشبكة أو من axios نفسه - return { + return error.response ? { success: false, - message: 'Network error. Please try again.' - }; + status: error.response.status, + message: error.response.data.message || 'Registration failed', + errors: error.response.data.errors || null + } : { success: false, message: 'Network error. Please try again.' }; } }, + // ======================= + // تسجيل الدخول + // ======================= login: async (email, password) => { try { - const response = await axios.post(`${API_BASE_URL}/Admin/login`, { + const response = await axios.post(`${API_BASE_URL}/Admin/login`, { email, password }, { + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } + }); + + if (response.data?.success && response.data.data?.Admin) { + localStorage.setItem('user_email', email); + localStorage.setItem('token', response.data.data.token); + localStorage.setItem('refresh_token', response.data.data.refresh_token); + localStorage.setItem('adminData', JSON.stringify(response.data.data.Admin)); + } + + return response.data; + } catch (error) { + return error.response ? { + success: false, + status: error.response.status, + message: error.response.data.message || 'Login failed', + errors: error.response.data.errors || null + } : { success: false, message: 'Network error. Please try again.' }; + } + }, + + // ======================= + // تسجيل الخروج + // ======================= + logout: () => { + localStorage.removeItem('token'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('user_email'); + localStorage.removeItem('adminData'); + return { success: true, message: 'Logged out successfully' }; + }, + + // ======================= + // التحقق من وجود توكن + // ======================= + isTokenValid: () => !!localStorage.getItem('token'), + + // ======================= + // استرجاع بيانات المسؤول + // ======================= + getAdminData: () => { + const adminData = JSON.parse(localStorage.getItem('adminData')); + return adminData || null; + }, + + // ======================= + // استرجاع ID المسؤول + // ======================= + getAdminId: () => { + const adminData = JSON.parse(localStorage.getItem('adminData')); + return adminData?.id || null; + }, + + // ======================= + // استرجاع التوكن + // ======================= + getToken: () => localStorage.getItem('token') || null, + getRefreshToken: () => localStorage.getItem('refresh_token') || null, + + resetPassword: async (email) => { + try { + const response = await axios.post(`${API_BASE_URL}/Admin/reset_password`, { email }); + return response.data; + } catch (error) { + return error.response + ? { + success: false, + status: error.response.status, + message: error.response.data.message || 'Reset request failed', + } + : { success: false, message: 'Network error. Please try again.' }; + } + }, + + verifyCode: async (email, code) => { + try { + const response = await axios.post(`${API_BASE_URL}/Admin/verify`, { email, code }); + return response.data; + } catch (error) { + return error.response + ? { + success: false, + status: error.response.status, + message: error.response.data.message || 'Verification failed', + } + : { success: false, message: 'Network error. Please try again.' }; + } + }, + + updatePassword: async ({ email, password, confirmPassword }) => { + try { + const response = await axios.post(`${API_BASE_URL}/Admin/updatePassword`, { email, - password + password, + password_confirmation: confirmPassword + }); + + return response.data; + } catch (error) { + return error.response + ? { + success: false, + status: error.response.status, + message: error.response.data.message || 'Update failed', + errors: error.response.data.errors || null + } + : { success: false, message: 'Network error. Please try again.' }; + } + }, + + googleLogin: async (googleAccessToken) => { + try { + const response = await axios.post(`${API_BASE_URL}/Admin/google-login`, { + token: googleAccessToken, }, { headers: { 'Content-Type': 'application/json', @@ -55,47 +169,1822 @@ const authService = { return { success: false, status: error.response.status, - message: error.response.data.message || 'Login failed', - errors: error.response.data.errors || null + message: error.response.data.message || 'Google login failed', }; } - return { success: false, message: 'Network error. Please try again.' }; } }, - logout: async () => { + + createNewRestaurant: async (restaurantData) => { try { const token = localStorage.getItem('token'); if (!token) { return { success: false, message: 'No token found' }; } - const response = await axios.post(`${API_BASE_URL}/Admin/logout`, {}, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json' - } - }); + const hasFile = restaurantData.logo instanceof File; + + if (hasFile) { + const fd = new FormData(); + for (const key in restaurantData) { + const value = restaurantData[key]; + if (value === null || value === undefined) continue; + + if (key === 'logo') { + fd.append('logo', value); + continue; + } + + if (typeof value === 'boolean') { + // أرسل 1/0 أو "true"/"false" حسب ما يقبله الـ backend. Laravel يقبل "true"/"false" و "1"/"0". + fd.append(key, value ? '1' : '0'); + } else if (typeof value === 'object') { + // إذا يحتوي على مصفوفة أو كائن — حوله لـ JSON + fd.append(key, JSON.stringify(value)); + } else { + fd.append(key, String(value)); + } + } + + // خيار: لعرض ما سترسله (debug) + // for (let pair of fd.entries()) console.log(pair[0], pair[1]); + + const response = await axios.post(`${API_BASE_URL}/restaurants`, fd, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + // 'Content-Type': 'multipart/form-data' + } + }); + + return response.data; + } else { + // لا يوجد ملف → أرسل JSON طبيعي (booleans تبقى boolean) + const response = await axios.post(`${API_BASE_URL}/restaurants`, restaurantData, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }); + return response.data; + } - return response.data; } catch (error) { if (error.response) { return { success: false, status: error.response.status, - message: error.response.data.message || 'Logout failed', + message: error.response.data.message || 'Creation failed', + errors: error.response.data.errors || null + }; + } + return { success: false, message: 'Network error. Please try again.' }; + } + }, + + cuisineTypes: async () => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return { success: false, message: 'No token found' }; + } + + const response = await axios.get(`${API_BASE_URL}/restaurants/getCusineTypes`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }); + + const cuisines = response?.data?.data?.data || []; + return cuisines; + } catch (error) { + console.error('Error fetching cuisine types:', error); + return []; + } + }, + + registerWaiter: async (waiterData) => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return { success: false, message: 'No token found' }; + } + + const response = await axios.post( + `${API_BASE_URL}/Admin/registerWaiter`, + waiterData, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + } + ); + + return response.data; + + } catch (error) { + if (error.response) { + // تفاصيل الأخطاء إن وجدت + const detailedErrors = error.response.data.errors || null; + const message = error.response.data.message || 'Failed to register waiter'; + + return { + success: false, + status: error.response.status, + message: message, + errors: detailedErrors, }; } return { success: false, - message: 'Network error. Please try again.' + message: 'Network error. Please try again.', }; } }, + registerCooker: async (cookerData) => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return { success: false, message: 'No token found' }; + } + + const response = await axios.post( + `${API_BASE_URL}/Admin/registerCooker`, + cookerData, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + } + ); + + return response.data; + + } catch (error) { + if (error.response) { + const detailedErrors = error.response.data.errors || null; + const message = error.response.data.message || 'Failed to register cooker'; + + return { + success: false, + status: error.response.status, + message: message, + errors: detailedErrors, + }; + } + return { + success: false, + message: 'Network error. Please try again.', + }; + } + }, + registerAccountant: async (accountantData) => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return { success: false, message: 'No token found' }; + } + + const response = await axios.post( + `${API_BASE_URL}/Admin/registerAccountant`, + accountantData, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + } + ); + + return response.data; + + } catch (error) { + if (error.response) { + const detailedErrors = error.response.data.errors || null; + const message = error.response.data.message || 'Failed to register accountant'; + + return { + success: false, + status: error.response.status, + message: message, + errors: detailedErrors, + }; + } + return { + success: false, + message: 'Network error. Please try again.', + }; + } + }, + + getAllWaiters: async (restaurantId) => { + try { + const token = localStorage.getItem("token"); + if (!token) { + return { success: false, message: "No token found" }; + } + + const response = await axios.get( + `${API_BASE_URL}/admin/show_all_waiters/${restaurantId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + return response.data; + /* + مثال الاستجابة: + { + "waiters": [ + { "id": 1, "name": "hussain", "email": "gjhu@gmail.com", "shift_type": "morning", "active": 0 }, + { "id": 2, "name": "hussain", "email": "gjhu11@gmail.com", "shift_type": "morning", "active": 0 } + ] + } + */ + + } catch (error) { + if (error.response) { + const message = + error.response.data.message || "Failed to fetch waiters"; + return { + success: false, + status: error.response.status, + message: message, + }; + } + return { + success: false, + message: "Network error. Please try again.", + }; + } + }, + + deleteWaiter: async (waiterId) => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return { message: 'No token found' }; + } + + const response = await axios.delete( + `${API_BASE_URL}/admin/delete_waiter/${waiterId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + } + ); + + // هنا نرجع الرسالة مباشرة مثل ما أرسلها السيرفر + return { message: response.data.message }; + + } catch (error) { + if (error.response) { + return { + message: error.response.data.message || 'Failed to delete waiter', + status: error.response.status, + }; + } + return { + message: 'Network error. Please try again.', + }; + } + }, + + getWaiterById: async (waiterId) => { + try { + const token = localStorage.getItem("token"); + if (!token) { + return { success: false, message: "No token found" }; + } + + const response = await axios.get( + `${API_BASE_URL}/admin/show_waiter/${waiterId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + return response.data; // هترجع { waiter: { ... } } + } catch (error) { + if (error.response) { + return { + success: false, + status: error.response.status, + message: error.response.data.message || "Failed to fetch waiter", + }; + } + return { success: false, message: "Network error. Please try again." }; + } + }, + // ✅ إحضار كل الطباخين لمطعم محدد + getAllCookers: async (restaurantId) => { + try { + const token = localStorage.getItem("token"); + if (!token) { + return { success: false, message: "No token found" }; + } + + const response = await axios.get( + `${API_BASE_URL}/admin/show_all_cookers/${restaurantId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + return response.data; + /* + مثال استجابة متوقعة: + { + "cookers": [ + { "id": 1, "name": "Ali", "email": "ali@gmail.com", "shift_type": "evening", "active": 1 }, + { "id": 2, "name": "Sara", "email": "sara@gmail.com", "shift_type": "morning", "active": 0 } + ] + } + */ + + } catch (error) { + if (error.response) { + const message = + error.response.data.message || "Failed to fetch cookers"; + return { + success: false, + status: error.response.status, + message: message, + }; + } + return { + success: false, + message: "Network error. Please try again.", + }; + } + }, + + // ✅ حذف طباخ + deleteCooker: async (cookerId) => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return { message: 'No token found' }; + } + + const response = await axios.delete( + `${API_BASE_URL}/admin/delete_cooker/${cookerId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + } + ); + + // نرجع الرسالة مثل ما يرسلها السيرفر + return { message: response.data.message }; + + } catch (error) { + if (error.response) { + return { + message: error.response.data.message || 'Failed to delete cooker', + status: error.response.status, + }; + } + return { + message: 'Network error. Please try again.', + }; + } + }, + + // ✅ إحضار تفاصيل طباخ محدد + getCookerById: async (cookerId) => { + try { + const token = localStorage.getItem("token"); + if (!token) { + return { success: false, message: "No token found" }; + } + + const response = await axios.get( + `${API_BASE_URL}/admin/show_cooker/${cookerId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + return response.data; // { cooker: { ... } } + + } catch (error) { + if (error.response) { + return { + success: false, + status: error.response.status, + message: error.response.data.message || "Failed to fetch cooker", + }; + } + return { success: false, message: "Network error. Please try again." }; + } + }, + + // ✅ إحضار كل المحاسبين لمطعم محدد + getAllAccountants: async (restaurantId) => { + try { + const token = localStorage.getItem("token"); + if (!token) { + return { success: false, message: "No token found" }; + } + + const response = await axios.get( + `${API_BASE_URL}/admin/show_all_accountants/${restaurantId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + return response.data; + /* + مثال استجابة متوقعة: + { + "accountants": [ + { "id": 1, "name": "Ahmed", "email": "ahmed@gmail.com", "active": 1 }, + { "id": 2, "name": "Lina", "email": "lina@gmail.com", "active": 0 } + ] + } + */ + + } catch (error) { + if (error.response) { + const message = + error.response.data.message || "Failed to fetch accountants"; + return { + success: false, + status: error.response.status, + message: message, + }; + } + return { + success: false, + message: "Network error. Please try again.", + }; + } + }, + + // ✅ حذف محاسب + deleteAccountant: async (accountantId) => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return { message: 'No token found' }; + } + + const response = await axios.delete( + `${API_BASE_URL}/admin/delete_accountant/${accountantId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + } + ); + + return { message: response.data.message }; + + } catch (error) { + if (error.response) { + return { + message: error.response.data.message || 'Failed to delete accountant', + status: error.response.status, + }; + } + return { + message: 'Network error. Please try again.', + }; + } + }, + + // ✅ إحضار تفاصيل محاسب محدد + getAccountantById: async (accountantId) => { + try { + const token = localStorage.getItem("token"); + if (!token) { + return { success: false, message: "No token found" }; + } + + const response = await axios.get( + `${API_BASE_URL}/admin/show_accountant/${accountantId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + return response.data; // { accountant: { ... } } + + } catch (error) { + if (error.response) { + return { + success: false, + status: error.response.status, + message: error.response.data.message || "Failed to fetch accountant", + }; + } + return { success: false, message: "Network error. Please try again." }; + } + }, + + toggleAccountStatus: async (email, model) => { + try { + const token = localStorage.getItem('token'); // جلب التوكن + if (!token) { + throw new Error("No token found. Please log in."); + } + + const response = await axios.post( + `${API_BASE_URL}/Admin/active`, + { email, model }, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + ); + return response.data; + } catch (error) { + console.error("Toggle account error:", error); + throw error; + } + }, + + + // ===================== + // دالة لتحويل مسار الصور + // ===================== +processImages: (items, imagePathKey = 'image', defaultImage = '/images/default.png') => { + return items.map(item => { + let imageUrl = defaultImage; + + // دعم المسارات المتداخلة مثل visual_identity.logo_url + const pathParts = imagePathKey.split('.'); + let value = item; + for (let key of pathParts) { + if (value && value[key] !== undefined && value[key] !== null) { + value = value[key]; + } else { + value = null; + break; + } + } + + if (value) { + // إذا القيمة تبدأ بـ "http" اعتبرها رابط كامل ولا تضف BASE_URL + if (value.startsWith('http://') || value.startsWith('https://')) { + imageUrl = value; + } else { + // أضف BASE_URL مع "storage" إذا لم يكن رابط كامل + imageUrl = `${BASE_URL}/storage/${value}`; + } + } + + return { + ...item, + image_url: imageUrl + }; + }); +}, + + getCategories: async () => { + try { + const token = localStorage.getItem('token'); + if (!token) return { success: false, message: 'No token found' }; + + const response = await axios.get(`${API_BASE_URL}/CategoryS/read_CS`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + }); + + if (response.data?.data) { + const categories = authService.processImages(response.data.data, 'image'); + return { success: true, data: categories }; + } + + return { success: false, message: 'No data found' }; + } catch (error) { + return { + success: false, + message: error.response?.data?.message || 'Network error. Please try again.' + }; + } + }, + + getCountries: async () => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return { success: false, message: 'No token found' }; + } + + const response = await axios.get(`${API_BASE_URL}/restaurants/getCountries`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }); + + if (response.data.status) { + // لاحظ: البيانات داخل data.data + const countries = response.data.data.data.map((country) => ({ + id: country.id, + name: country.name, + })); + return { success: true, data: countries }; + } + + return { success: false, message: 'Failed to fetch countries' }; + } catch (error) { + return { + success: false, + message: error.response?.data?.message || 'Network error. Please try again.', + }; + } + }, + + isTokenValid: () => { + return !!localStorage.getItem('token'); + }, + + // createCart: async (cartData) => { + // if (!authService.isTokenValid()) { + // return { success: false, message: 'Invalid token. Please login again.' }; + // } + + // try { + // const token = localStorage.getItem('token'); + // const response = await axios.post(`${API_BASE_URL}/Cart_Supplier/carts`, cartData, { + // headers: { + // Authorization: `Bearer ${token}`, + // Accept: 'application/json', + // 'Content-Type': 'application/json', + // }, + // }); + + // if (response.data) { + // return { success: true, data: response.data.data }; + // } + + // return { success: false, message: 'Failed to create cart' }; + // } catch (error) { + // console.error('Full error response from server:', error.response?.data); + // return { + // success: false, + // status: error.response?.status || null, + // message: error.response?.data?.message || 'Unknown error', + // errors: error.response?.data?.errors || null + // }; + // } + // }, + + + getCart: async () => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found" }; + + const response = await axios.get(`${API_BASE_URL}/Cart_Supplier/carts`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + }); + + if (response.data && response.data.data) { + return { success: true, data: response.data.data }; + } + + return { success: false, message: "No cart data found" }; + } catch (error) { + console.error("Error fetching cart:", error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || "Network error. Please try again.", + }; + } + }, + + getCartById: async (cartId) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found" }; + + const response = await axios.get(`${API_BASE_URL}/Cart_Supplier/carts/${cartId}`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + }); + + if (response.data && response.data.data) { + return { success: true, data: response.data.data }; + } + + return { success: false, message: "No cart data found" }; + } catch (error) { + console.error("Error fetching cart by ID:", error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || "Network error. Please try again.", + }; + } + }, + + updateCart: async (cartId, cartData) => { + try { + const token = localStorage.getItem('token'); + if (!token) return { success: false, message: 'No token found' }; + + const response = await axios.put(`${API_BASE_URL}/Cart_Supplier/carts/${cartId}`, cartData, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + + if (response.data && response.data.data) { + return { success: true, data: response.data.data }; + } + + return { success: false, message: 'Failed to update cart' }; + + } catch (error) { + console.error('Error updating cart:', error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || 'Network error. Please try again.', + errors: error.response?.data?.errors || null, + }; + } + }, + + deleteCart: async (cartId) => { + try { + const token = localStorage.getItem('token'); + if (!token) return { success: false, message: 'No token found' }; + + const response = await axios.delete(`${API_BASE_URL}/Cart_Supplier/carts/${cartId}`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }); + + return response.data; + } catch (error) { + console.error('Error deleting cart:', error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || 'Network error. Please try again.', + }; + } + }, + + addCategory: async (categoryData) => { + try { + const token = localStorage.getItem("token"); + const response = await axios.post( + `${API_BASE_URL}/admin/add_category`, + categoryData, + { + headers: { + Authorization: token ? `Bearer ${token}` : "", + Accept: "application/json", + "Content-Type": "application/json", + }, + } + ); + return response.data; + } catch (error) { + console.error("❌ Error adding category:", error); + throw error.response?.data || { message: "Something went wrong" }; + } + }, + + updateCategory: async (categoryId, categoryData) => { + try { + const token = localStorage.getItem("token"); + const response = await axios.put( + `${API_BASE_URL}/admin/update_category/${categoryId}`, + categoryData, + { + headers: { + Authorization: token ? `Bearer ${token}` : "", + Accept: "application/json", + "Content-Type": "application/json", + }, + } + ); + return response.data; // { message: "category updated successfully", category: {...} } + } catch (error) { + console.error("❌ Error updating category:", error); + throw error.response?.data || { message: "Something went wrong" }; + } + }, + + deleteCategory: async (categoryId) => { + try { + const token = localStorage.getItem("token"); + const response = await axios.delete( + `${API_BASE_URL}/admin/delete_category/${categoryId}`, + { + headers: { + Authorization: token ? `Bearer ${token}` : "", + Accept: "application/json", + }, + } + ); + return response.data; // { message: "category deleted successfully" } + } catch (error) { + console.error("❌ Error deleting category:", error); + throw error.response?.data || { message: "Something went wrong" }; + } + }, + + getCategoriesByRestaurant: async (restaurantId) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found" }; + + const response = await axios.get( + `${API_BASE_URL}/admin/all_categories/${restaurantId}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + if (response.data?.categories) { + return { success: true, data: response.data.categories }; + } + + return { success: false, message: "No categories found" }; + } catch (error) { + console.error("❌ Error fetching categories:", error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || "Network error. Please try again.", + }; + } + }, + + getMealsByCategory: async (restaurantId, categoryId, page = 1) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: [], pagination: null }; + + // ✅ شيل restaurantId من الـ URL + const response = await axios.get( + `${API_BASE_URL}/admin/category_meals/${categoryId}?page=${page}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const mealsData = response.data?.data?.category?.meals?.data || []; + const meta = response.data?.data?.category?.meals?.meta || {}; + + const meals = mealsData.map(meal => ({ + id: meal.id, + category_id: meal.category_id, + restaurant_id: meal.restaurant_id, + name: meal.name, + description: meal.description || [], + additions: meal.additions || [], + price: meal.price, + photo: meal.photo || "/images/default-product.png", + created_at: meal.created_at, + updated_at: meal.updated_at, + })); + + return { + success: true, + data: meals, + pagination: { + current_page: meta.current_page || 1, + per_page: meta.per_page || meals.length, + total: meta.total || meals.length, + last_page: meta.last_page || 1, + }, + }; + } catch (error) { + console.error("❌ Error fetching category meals:", error); + return { success: false, message: "Failed to fetch meals", data: [], pagination: null }; + } + }, + + getAllMealsByRestaurant: async (restaurantId, page = 1) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found" }; + + const response = await axios.get( + `${API_BASE_URL}/admin/all_meals/${restaurantId}?page=${page}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const mealsData = response.data.meals?.data || []; + + // تحويل كل وجبة بحيث تحتوي على الحقل photo مباشرة + const meals = mealsData.map((meal) => ({ + id: meal.id, + category_id: meal.category_id, + restaurant_id: meal.restaurant_id, + name: meal.name, + description: meal.description || [], + additions: meal.additions || [], + price: meal.price, + photo: meal.photo || "/images/default-product.png", + created_at: meal.created_at, + updated_at: meal.updated_at, + })); + + return { + success: true, + data: meals, + pagination: { + current_page: response.data.meals.current_page, + per_page: response.data.meals.per_page, + total: response.data.meals.total, + last_page: response.data.meals.last_page, + next_page_url: response.data.meals.next_page_url, + prev_page_url: response.data.meals.prev_page_url, + }, + }; + } catch (error) { + console.error("❌ Error fetching meals:", error); + return { + success: false, + status: error.response?.status || null, + message: + error.response?.data?.message || "Network error. Please try again.", + }; + } + }, + + addMeal: async (mealData) => { + try { + const token = localStorage.getItem("token"); + const formData = new FormData(); + + Object.keys(mealData).forEach(key => { + const value = mealData[key]; + if (Array.isArray(value)) { + value.forEach((item, idx) => formData.append(`${key}[${idx}]`, item)); + } else if (key === 'photo' && value) { + formData.append('photo', value); + } else if (value !== undefined && value !== null) { + formData.append(key, value); + } + }); + + const response = await axios.post(`${API_BASE_URL}/admin/add_meal`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: token ? `Bearer ${token}` : "", + Accept: "application/json", + }, + }); + + // التأكد أن دائمًا هناك message + return { + message: response.data?.message || "Meal added successfully", + data: response.data?.data || null, + }; + } catch (error) { + console.error("❌ Error adding meal:", error); + return { + message: error.response?.data?.message || "Something went wrong", + data: null, + }; + } + } + , + + updateMeal: async (mealId, data) => { + const token = localStorage.getItem("token"); + if (!token) throw { message: "No token found. Please login." }; + + const formData = new FormData(); + formData.append("name", data.name); + formData.append("category_id", data.category_id); + formData.append("restaurant_id", data.restaurant_id); + + // إضافة الوصف فقط مرة واحدة + (data.description || []).forEach((d) => formData.append('description[]', d)); + + // إضافة الإضافات + (data.additions || []).forEach((a) => formData.append('additions[]', a)); + + formData.append("price", data.price); + + // إضافة الصور إذا كانت موجودة + (data.photo || []).forEach((file) => formData.append("photo", file)); + + const res = await axios.post( + `${API_BASE_URL}/admin/update_meal/${mealId}`, + formData, + { + headers: { + // لا تضف Content-Type هنا! + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + return res.data; + }, + deleteMeal: async (mealId, restaurantId, categoryId) => { + try { + const token = localStorage.getItem("token"); + if (!token) throw { message: "No token found. Please login." }; + + const formData = new FormData(); + formData.append("_method", "DELETE"); // محاكاة طلب DELETE + formData.append("id", mealId); + formData.append("restaurant_id", restaurantId); + formData.append("category_id", categoryId); + + const response = await axios.post( + `${API_BASE_URL}/admin/delete_meal/${mealId}`, + formData, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + return response.data; + } catch (err) { + console.error("Error deleting meal:", err); + throw err.response?.data || { message: "Failed to delete meal." }; + } + }, + + + // 📌 جلب جميع الطاولات لمطعم معيّن + getTablesByRestaurant: async (restaurantId, page = 1) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: [] }; + + const response = await axios.get( + `${API_BASE_URL}/table/${restaurantId}?page=${page}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const tablesData = response.data?.data?.data || []; + const meta = response.data?.data || {}; + + return { + success: true, + data: tablesData, + pagination: { + current_page: meta.current_page || 1, + per_page: meta.per_page || tablesData.length, + total: meta.total || tablesData.length, + last_page: meta.last_page || 1, + }, + }; + } catch (error) { + console.error("❌ Error fetching tables:", error); + return { + success: false, + status: error.response?.status || null, + message: + error.response?.data?.message || "Network error. Please try again.", + data: [], + pagination: null, + }; + } + }, + // 📌 إضافة طاولة جديدة + addTable: async (tableData) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found" }; + + const response = await axios.post( + `${API_BASE_URL}/table`, + tableData, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + } + ); + + return { + success: response.data?.success || false, + message: response.data?.message || "Table created successfully", + data: response.data?.data || null, + }; + } catch (error) { + console.error("❌ Error adding table:", error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || "Something went wrong", + errors: error.response?.data?.errors || null, + }; + } + }, + deleteTable: async (tableId) => { + try { + const token = localStorage.getItem("token"); // إذا تستخدم توكن + const response = await axios.delete(`${API_BASE_URL}/table/${tableId}`, { + headers: { + Authorization: token ? `Bearer ${token}` : "", + "Content-Type": "application/json", + }, + }); + return { success: true, data: response.data }; + } catch (error) { + console.error("❌ Error deleting table:", error); + return { success: false, message: error.response?.data || error.message }; + } + }, + + createRestaurantProfile: async (restaurantId, profileData) => { + try { + const token = localStorage.getItem("token"); + if (!token) { + return { success: false, message: "No token found" }; + } + + // نتأكد من وجود id + if (!restaurantId) { + return { success: false, message: "Restaurant ID is required" }; + } + + // هل يوجد ملف (مثلاً logo)؟ + const hasFile = profileData.logo instanceof File; + + if (hasFile) { + const fd = new FormData(); + for (const key in profileData) { + const value = profileData[key]; + if (value === null || value === undefined) continue; + + if (key === "logo") { + fd.append("logo", value); + continue; + } + + if (typeof value === "boolean") { + fd.append(key, value ? "1" : "0"); + } else if (typeof value === "object") { + fd.append(key, JSON.stringify(value)); + } else { + fd.append(key, String(value)); + } + } + + const response = await axios.post( + `${API_BASE_URL}/restaurants/${restaurantId}/profile`, + fd, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + // axios يضبط Content-Type أوتوماتيك مع FormData + }, + } + ); + + return { success: true, data: response.data }; + } else { + // لا يوجد ملف → إرسال JSON طبيعي + const response = await axios.post( + `${API_BASE_URL}/restaurants/${restaurantId}/profile`, + profileData, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + } + ); + return { success: true, data: response.data }; + } + } catch (error) { + if (error.response) { + return { + success: false, + status: error.response.status, + message: error.response.data.message || "Profile creation failed", + errors: error.response.data.errors || null, + }; + } + return { success: false, message: "Network error. Please try again." }; + } + }, +// دالة لجلب بيانات ملف المطعم + getRestaurantProfile: async(restaurantId)=> { + try { + const response = await fetch( `${API_BASE_URL}/restaurants/${restaurantId}/profile`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + // إضافة Authorization هنا إذا كان مطلوبًا + // 'Authorization': 'Bearer YOUR_TOKEN' + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; // ترجع البيانات كاملة كما في المثال + } catch (error) { + console.error("Error fetching restaurant profile:", error); + return null; + } +}, +updateRestaurantProfile: async (restaurantId, profileData) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found" }; + if (!restaurantId) return { success: false, message: "Restaurant ID is required" }; + + const hasFile = profileData.logo instanceof File; + + if (hasFile) { + const fd = new FormData(); + for (const key in profileData) { + const value = profileData[key]; + if (value === null || value === undefined) continue; + + if (key === "logo") { + fd.append("logo", value); + continue; + } + + if (typeof value === "object") { + fd.append(key, JSON.stringify(value)); + } else { + fd.append(key, String(value)); + } + } + + const response = await axios.put( + `${API_BASE_URL}/restaurants/${restaurantId}/profile`, + fd, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + return { success: true, data: response.data }; + } else { + const response = await axios.put( + `${API_BASE_URL}/restaurants/${restaurantId}/profile`, + profileData, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + } + ); + return { success: true, data: response.data }; + } + } catch (error) { + if (error.response) { + return { + success: false, + status: error.response.status, + message: error.response.data.message || "Profile update failed", + errors: error.response.data.errors || null, + }; + } + return { success: false, message: "Network error. Please try again." }; + } +}, +// 📌 جلب إحصائيات الحجوزات لمطعم معيّن حسب الفترة + getReservationStatistics: async (restaurantId, period = "daily", date = null) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: [] }; + if (!restaurantId) return { success: false, message: "Restaurant ID is required", data: [] }; + + // التاريخ الافتراضي: اليوم الحالي + const selectedDate = date || new Date().toISOString().split('T')[0]; // YYYY-MM-DD + + const response = await axios.get( + `${API_BASE_URL}/admin/reservation_statistics/${restaurantId}`, + { + params: { period, date: selectedDate }, // إرسال period و date كـ query params + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const statsData = response.data?.data || []; + + return { + success: response.data?.success || false, + data: statsData, + }; + } catch (error) { + console.error("❌ Error fetching reservation statistics:", error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || "Network error. Please try again.", + data: [], + }; + } + }, + getOrderStatistics: async (restaurantId, period = "daily", date = null) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: [] }; + if (!restaurantId) return { success: false, message: "Restaurant ID is required", data: [] }; + + // التاريخ الافتراضي: اليوم الحالي + const selectedDate = date || new Date().toISOString().split('T')[0]; // YYYY-MM-DD + + const response = await axios.get( + `${API_BASE_URL}/admin/order_statistics/${restaurantId}`, + { + params: { period, date: selectedDate }, // إرسال period و date كـ query params + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const statsData = response.data?.data || []; + + return { + success: response.data?.success || false, + data: statsData, + }; + } catch (error) { + console.error("❌ Error fetching reservation statistics:", error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || "Network error. Please try again.", + data: [], + }; + } + }, + getBillStatistics: async (restaurantId, period = "daily", date = null) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: [] }; + if (!restaurantId) return { success: false, message: "Restaurant ID is required", data: [] }; + + // التاريخ الافتراضي: اليوم الحالي + const selectedDate = date || new Date().toISOString().split('T')[0]; // YYYY-MM-DD + + const response = await axios.get( + `${API_BASE_URL}/admin/bill_statistics/${restaurantId}`, + { + params: { period, date: selectedDate }, // إرسال period و date كـ query params + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const statsData = response.data?.data || []; + + return { + success: response.data?.success || false, + data: statsData, + }; + } catch (error) { + console.error("❌ Error fetching reservation statistics:", error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || "Network error. Please try again.", + data: [], + }; + } + }, +// 📌 جلب نسبة الإشغال (Occupancy Rate) لمطعم معيّن حسب الفترة +getOccupancyRate: async (restaurantId, period = "daily", date = null) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: null }; + if (!restaurantId) return { success: false, message: "Restaurant ID is required", data: null }; + + // التاريخ الافتراضي: اليوم الحالي + const selectedDate = date || new Date().toISOString().split('T')[0]; // YYYY-MM-DD + + const response = await axios.get( + `${API_BASE_URL}/admin/occupancy_rate/${restaurantId}`, + { + params: { period, date: selectedDate }, // period و date كـ query params + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const occupancyRate = response.data?.occupancy_rate || "0%"; + + return { + success: response.data?.success || false, + period: response.data?.period || period, + date: response.data?.date || selectedDate, + data: occupancyRate, + }; + } catch (error) { + console.error("❌ Error fetching occupancy rate:", error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || "Network error. Please try again.", + data: null, + }; + } +}, + + getRestaurants: async () => { + try { + const token = localStorage.getItem('token'); + if (!token) return { success: false, message: 'No token found' }; + + const response = await axios.get(`${API_BASE_URL}/restaurants/allresturant`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + }); + + if (response.data.success && response.data.data?.data) { + // استخدم الدالة الموحدة لتحويل الصور + const restaurants = authService.processImages(response.data.data.data, 'visual_identity.logo_url', '/images/default-restaurant.png'); + return { success: true, data: restaurants }; + } + + return { success: false, message: 'No data found' }; + } catch (error) { + console.error('Error fetching restaurants:', error); + return { + success: false, + status: error.response?.status || null, + message: error.response?.data?.message || 'Network error. Please try again.' + }; + } + }, + + +getRestaurantById: async (restaurantId) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found" }; + + const response = await axios.get(`${API_BASE_URL}/restaurants/${restaurantId}`, { + headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }, + }); + + if (response.data.success && response.data.data) { + const restaurant = response.data.data; + + // تأكد من تعريف BASE_URL في أعلى الملف + const processed = authService.processImages( + [restaurant], + 'visual_identity.logo_url', + '/images/default-restaurant.png' + ); + + return { success: true, data: processed[0] }; + } + + return { success: false, message: "No data found" }; + } catch (error) { + if (error.response) { + return { + success: false, + status: error.response.status, + message: error.response.data.message || "Failed to fetch restaurant", + }; + } + return { success: false, message: "Network error. Please try again." }; + } +}, + +// ///////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////// + + + + + + + + + getAllCategory: async () => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: [], pagination: null }; + + const response = await axios.get( + `${API_BASE_URL}/CategoryS/read_CS`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + // const CatSData = response.data?.data?.category?.CatS?.data || []; + const CatSData = response.data?.data || []; + const meta = response.data?.data?.category?.CatS?.meta || {}; + + const CatS = CatSData.map(Category => ({ + id: Category.id, + name: Category.name, + icon: Category.image, + })); + + return { + success: true, + data: CatS + }; + } catch (error) { + console.error("❌ Error fetching category meals:", error); + return { success: false, message: "Failed to fetch meals", data: [], pagination: null }; + } + }, + + + getAllProduct: async () => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: [], pagination: null }; + + const response = await axios.get( + `${API_BASE_URL}/ProductS/read_PS`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const Product = response.data?.data || []; + const meta = response.data?.data?.category?.CatS?.meta || {}; + + const Pro = Product.map(product => ({ + id: product.id, + name: product.name, + category: product.supplier_category_id, + price: product.price, + unit: "Kg", + image: product.image, + })); + + return { + success: true, + data: Pro + }; + } catch (error) { + console.error("Error fetching category meals:", error); + return { success: false, message: "Failed to fetch meals", data: [], pagination: null }; + } + }, + + + + +createCart: async (cartData) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: null }; + + const response = await axios.post( + `${API_BASE_URL}/Cart_Supplier/carts`, + cartData, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + } + ); + + return { + success: true, + data: response.data?.data || null, + }; + } catch (error) { + console.error("Error creating cart:", error); + return { success: false, message: "Failed to create cart", data: null }; + } +}, + + + +getProductsByCategory: async (categoryId, page = 1) => { + try { + const token = localStorage.getItem("token"); + if (!token) + return { + success: false, + message: "No token found", + data: [], + pagination: null, + }; + + const response = await axios.get( + `${API_BASE_URL}/ProductS/getproductsbycategory/${categoryId}?page=${page}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + } + ); + + const productsData = response.data?.data || []; + const meta = response.data?.meta || {}; + + const products = productsData.map((product) => ({ + id: product.id, + name: product.name, + price: product.price, + category_id: product.supplier_category_id, + image: product.image + // ? `${API_BASE_URL}/${product.image}` + || "/images/default-product.png", + created_at: product.created_at, + updated_at: product.updated_at, + })); + + return { + success: true, + data: products, + pagination: { + current_page: meta.current_page || 1, + per_page: meta.per_page || products.length, + total: meta.total || products.length, + last_page: meta.last_page || 1, + }, + }; + } catch (error) { + console.error("Error fetching products by category:", error); + return { + success: false, + message: "Failed to fetch products", + data: [], + pagination: null, + }; + } +}, + + + + + + + getOrders: async (page = 1) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: [], pagination: null }; + + const response = await axios.get(`${API_BASE_URL}/Cart_Supplier/carts`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + }); + + const ordersData = response.data?.data || []; + const meta = response.data?.meta || {}; + + const orders = ordersData.map(order => ({ + id: order.id, + totalPrice: order.attributes?.total_price || order.attributes?.totalPrice || "0.00", + createdAt: order.attributes?.createdAt || null, + cartItems: order.relationships?.cart_items || order.relationships?.cartItems || [], + })); + + return { + success: true, + data: orders, + pagination: { + current_page: meta.current_page || 1, + per_page: meta.per_page || orders.length, + total: meta.total || orders.length, + last_page: meta.last_page || 1, + }, + }; + + } catch (error) { + console.error("Error fetching orders:", error); + return { success: false, message: "Failed to fetch orders", data: [], pagination: null }; + } + }, + + + + + + confirmOrder: async (order) => { + try { + const token = localStorage.getItem("token"); + if (!token) return { success: false, message: "No token found", data: null }; + + const response = await axios.post( + `${API_BASE_URL}/Order_Supplier/myorders`, + order, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + } + ); + return { + success: true, + data: response.data?.data || null, + }; + } catch (error) { + console.error("Error creating cart:", error); + return { success: false, message: "Failed to create cart", data: null }; + } +}, + + + }; export default authService; diff --git a/src/services/cartHelpers.js b/src/services/cartHelpers.js new file mode 100644 index 0000000..057fbf8 --- /dev/null +++ b/src/services/cartHelpers.js @@ -0,0 +1,140 @@ + +import authService from './authService'; + +// === مفاتيح التخزين === +const CART_KEY = 'activeCartId'; + +// === LocalStorage Helpers === +export const setActiveCartId = (id) => { + try { localStorage.setItem(CART_KEY, String(id)); } catch {} +}; +export const getActiveCartId = () => { + try { return localStorage.getItem(CART_KEY); } catch { return null; } +}; +export const clearActiveCartId = () => { + try { localStorage.removeItem(CART_KEY); } catch {} +}; + +// === تحويل عناصر الكارت لصيغة JSON:API === +export const buildCartPayload = (items, totalPrice) => ({ + data: { + type: "cart", + attributes: { + totalPrice: typeof totalPrice === 'number' ? Number(totalPrice) : undefined, + }, + relationships: { + cartItems: items.map(it => ({ + attributes: { quantity: Number(it.quantity) }, + relationships: { + product: { data: { id: it.productId } } + } + })) + } + } +}); + +// === استخراج الـ productId من أي شكل محتمل === +export const extractProductId = (product) => ( + product?.relationships?.supplier_product?.id || + product?.relationships?.product?.data?.id || + product?.supplier_product_id || + product?.product_id || + product?.id +); + +// === استخراج عناصر الكارت من استجابة الباك === +export const getItemsFromCart = (cartData) => { + const rel = cartData?.relationships || {}; + const list = rel.cartItems || rel.cart_items || []; + return list.map(item => ({ + id: item.id, + productId: item.relationships?.supplier_product?.id || + item.relationships?.product?.data?.id, + quantity: Number(item?.attributes?.quantity || 0), + })).filter(x => x.productId); +}; + +// === إضافة/تحديث عنصر في قائمة الكارت === +export const upsertItem = (items, productId, quantity) => { + const idx = items.findIndex(x => String(x.productId) === String(productId)); + if (idx >= 0) { + const merged = [...items]; + merged[idx] = { ...merged[idx], quantity: Number(merged[idx].quantity) + Number(quantity) }; + return merged; + } + return [...items, { productId, quantity: Number(quantity) }]; +}; + +// === حساب إجمالي مؤقت إن توفر priceMap === +export const estimateTotal = (items, priceMap) => { + if (!priceMap) return undefined; + return items.reduce((sum, it) => sum + (Number(priceMap[it.productId]) || 0) * Number(it.quantity), 0); +}; + +// === التأكد من وجود كارت فعّالة وإنشاؤها إذا لم توجد === +export const ensureActiveCart = async () => { + const existing = getActiveCartId(); + if (existing) return existing; + + const token = localStorage.getItem('token'); + if (!token) return null; + + const payload = { + data: { + type: "cart", + attributes: { totalPrice: 0 }, + relationships: { cartItems: [] } + } + }; + + const res = await authService.createCart(payload); + if (res?.success && res?.data?.id) { + setActiveCartId(res.data.id); + return res.data.id; + } + return null; +}; + +// === إضافة منتج للكارت الفعّالة === +export const addItemToActiveCart = async ({ product, quantity = 1, priceMap = null }) => { + const token = localStorage.getItem('token'); + if (!token) return { success: false, message: 'No token found' }; + + const cartId = await ensureActiveCart(); + if (!cartId) return { success: false, message: 'Failed to create or get cart' }; + + const current = await authService.getCartById(cartId); + if (!current?.success || !current?.data) return { success: false, message: 'Failed to fetch active cart' }; + + const existingItems = getItemsFromCart(current.data); + const productId = extractProductId(product); + if (!productId) return { success: false, message: 'Invalid product id' }; + + const mergedItems = upsertItem(existingItems, productId, quantity); + const totalPrice = estimateTotal(mergedItems, priceMap); + const updatePayload = buildCartPayload(mergedItems, totalPrice); + + const updated = await authService.updateCart(cartId, updatePayload); + if (updated?.success && updated?.data?.id) { + setActiveCartId(updated.data.id); + return { success: true, data: updated.data, cartId: updated.data.id }; + } + + return { success: false, message: updated?.message || 'Failed to update cart', errors: updated?.errors || null }; +}; + +// === تصدير الأدوات كموديل واحد === +const cartHelpers = { + setActiveCartId, + getActiveCartId, + clearActiveCartId, + ensureActiveCart, + addItemToActiveCart, + buildCartPayload, + extractProductId, + getItemsFromCart, + upsertItem, + estimateTotal, +}; + +export default cartHelpers; diff --git a/src/test.js b/src/test.js index b28e130..e2d768a 100644 --- a/src/test.js +++ b/src/test.js @@ -3,9 +3,9 @@ import { useNavigate } from 'react-router-dom'; import { useTheme } from '@mui/material/styles'; import { Box, Typography } from '@mui/material'; -const Test = () => { // لاحظ الحرف الكبير في بداية الاسم - const navigate = useNavigate(); // صحيح الآن لأنها داخل مكون React - const theme = useTheme(); // تعريف theme باستخدام useTheme +const Test = () => { + const navigate = useNavigate(); + const theme = useTheme(); const handleClick = () => { navigate('/dashboard'); diff --git a/src/theme.js b/src/theme.js index 0a61cca..96abf0a 100644 --- a/src/theme.js +++ b/src/theme.js @@ -16,7 +16,7 @@ const theme = createTheme({ }, palette: { primary: { - main: '#FF914D', // ← اللون الأساسي الجديد + main: '#FF914D', hover: '#e57f3f' },