Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d90a99003 | ||
|
|
7b2f8840cb | ||
|
|
13891b47fd | ||
|
|
ea83abe001 | ||
|
|
9f05ca5f9c | ||
|
|
4a72e96c76 | ||
|
|
aa9a213676 | ||
|
|
f4d30ed0d7 | ||
|
|
89ad182ea6 | ||
|
|
824515d951 | ||
|
|
094d64320d |
@@ -0,0 +1,19 @@
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
deploy:
|
||||
needs: build_and_push_image
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: install ghaymah cli
|
||||
run: curl -sSl https://cli.ghaymah.systems/install.sh | bash
|
||||
|
||||
- name: login to ghaymah
|
||||
run: $HOME/ghaymah/bin/gy auth login --email "${{secrets.GHAYMAH_EMAIL}}" --password "${{secrets.GHAYMAH_PW}}"
|
||||
|
||||
- name: deploy
|
||||
run: $HOME/ghaymah/bin/gy resource app launch
|
||||
@@ -0,0 +1,29 @@
|
||||
# Build stage
|
||||
FROM node:18-alpine AS build
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the app
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built app from build stage
|
||||
COPY --from=build /app/build /usr/share/nginx/html
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "a978f1cb-2075-4132-b91b-13d29f1b06e5",
|
||||
"name": "First-CICD",
|
||||
"projectId": "c94a536a-9f46-488f-9a83-cdd52abddde4",
|
||||
"ports": [
|
||||
{
|
||||
"expose": true,
|
||||
"number": 80
|
||||
}
|
||||
],
|
||||
"publicAccess": {
|
||||
"enabled": true,
|
||||
"domain": "auto"
|
||||
},
|
||||
"resourceTier": "t2",
|
||||
"dockerFileName": "Dockerfile"
|
||||
}
|
||||
@@ -1,47 +1,122 @@
|
||||
{
|
||||
"name": "resturant-dashBord",
|
||||
|
||||
"name": "resturant-dashbord",
|
||||
|
||||
"version": "0.1.0",
|
||||
|
||||
"private": true,
|
||||
|
||||
"dependencies": {
|
||||
|
||||
"@emotion/react": "^11.14.0",
|
||||
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^7.1.0",
|
||||
"@mui/material": "^7.1.0",
|
||||
|
||||
"@mui/icons-material": "^7.0.2",
|
||||
|
||||
"@mui/material": "^7.0.2",
|
||||
|
||||
"@mui/x-date-pickers": "^8.5.2",
|
||||
|
||||
"@react-oauth/google": "^0.12.2",
|
||||
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
|
||||
"@testing-library/react": "^16.3.0",
|
||||
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
|
||||
"axios": "^1.9.0",
|
||||
|
||||
"date-fns": "^4.1.0",
|
||||
|
||||
"dayjs": "^1.11.13",
|
||||
|
||||
"file-saver": "^2.0.5",
|
||||
|
||||
"framer-motion": "^12.23.0",
|
||||
|
||||
"jspdf": "^2.5.1",
|
||||
|
||||
"jspdf-autotable": "^3.5.23",
|
||||
|
||||
"jwt-decode": "^4.0.0",
|
||||
|
||||
"react": "^19.1.0",
|
||||
|
||||
"react-circular-progressbar": "^2.2.0",
|
||||
|
||||
"react-dom": "^19.1.0",
|
||||
|
||||
"react-router-dom": "^7.6.0",
|
||||
|
||||
"react-scripts": "5.0.1",
|
||||
|
||||
"recharts": "^2.15.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
|
||||
"web-vitals": "^2.1.4",
|
||||
|
||||
"xlsx": "^0.18.5"
|
||||
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
|
||||
"start": "react-scripts start",
|
||||
|
||||
"build": "react-scripts build",
|
||||
|
||||
"test": "react-scripts test",
|
||||
|
||||
"eject": "react-scripts eject"
|
||||
|
||||
},
|
||||
|
||||
"eslintConfig": {
|
||||
|
||||
"extends": [
|
||||
|
||||
"react-app",
|
||||
|
||||
"react-app/jest"
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
"browserslist": {
|
||||
|
||||
"production": [
|
||||
|
||||
">0.2%",
|
||||
|
||||
"not dead",
|
||||
|
||||
"not op_mini all"
|
||||
|
||||
],
|
||||
|
||||
"development": [
|
||||
|
||||
"last 1 chrome version",
|
||||
|
||||
"last 1 firefox version",
|
||||
|
||||
"last 1 safari version"
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
"description": "To make it easy for you to get started with GitLab, here's a list of recommended next steps.",
|
||||
|
||||
"main": "restored-file.js",
|
||||
|
||||
"author": "",
|
||||
|
||||
"license": "ISC"
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 732 B |
|
After Width: | Height: | Size: 859 B |
|
After Width: | Height: | Size: 852 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1023 B |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 122 KiB |
@@ -2,7 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<link rel="icon" href="/image.png" />
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
@@ -11,13 +14,15 @@
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<style>
|
||||
* {
|
||||
margin: 0
|
||||
}
|
||||
</style>
|
||||
<title>Restaurant</title>
|
||||
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> -->
|
||||
<style>
|
||||
* {
|
||||
font-family: "Roboto", sans-serif;
|
||||
margin:0;
|
||||
}
|
||||
</style>
|
||||
<title>Resturant</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,7 +1,17 @@
|
||||
.App {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body,
|
||||
html,
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
@@ -22,7 +32,6 @@
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
|
||||
}
|
||||
|
||||
.App-link {
|
||||
@@ -38,87 +47,101 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* CUSTOM FONTS */
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-Thin.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-Thin.ttf');
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-ExtraLight.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-ExtraLight.ttf');
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-Light.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-Light.ttf');
|
||||
font-weight: 300;
|
||||
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-Regular.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-Regular.ttf');
|
||||
font-weight: 400;
|
||||
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-Medium.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-Medium.ttf');
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-SemiBold.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-SemiBold.ttf');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-Bold.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-Bold.ttf');
|
||||
font-weight: 700;
|
||||
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-ExtraBold.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-ExtraBold.ttf');
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: INTER;
|
||||
src: url('../public/fonts/inter/Inter_18pt-Black.ttf');
|
||||
src: url('/public/fonts/inter/Inter_18pt-Black.ttf');
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: PlusJakartaSans ;
|
||||
src: url('../public/fonts/PlusJakartaSans/PlusJakartaSans-Medium.ttf');
|
||||
font-family: PlusJakartaSans;
|
||||
src: url('/public/fonts/PlusJakartaSans/PlusJakartaSans-Medium.ttf');
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: PlusJakartaSans ;
|
||||
src: url('../public/fonts/PlusJakartaSans/PlusJakartaSans-SemiBold.ttf');
|
||||
font-family: PlusJakartaSans;
|
||||
src: url('/public/fonts/PlusJakartaSans/PlusJakartaSans-SemiBold.ttf');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: PlusJakartaSans ;
|
||||
src: url('../public/fonts/PlusJakartaSans/PlusJakartaSans-Regular.ttf');
|
||||
font-family: PlusJakartaSans;
|
||||
src: url('/public/fonts/PlusJakartaSans/PlusJakartaSans-Regular.ttf');
|
||||
font-weight: 400;
|
||||
}
|
||||
/* === CUSTOM FONTS ===*/
|
||||
|
||||
@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 === */
|
||||
|
||||
@@ -1,47 +1,60 @@
|
||||
import React from 'react';
|
||||
import './App.css';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import theme from './theme';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
|
||||
import theme from './theme';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import PrivateRoute from '../src/components/Routes/PrivateRoute';
|
||||
|
||||
// الصفحات العامة
|
||||
import LoginForm from './components/Authentication/SignUp_In/LoginForm';
|
||||
import RegisterForm from './components/Authentication/SignUp_In/RegisterForm';
|
||||
import ForgetPassword from './components/Authentication/ForgetPassword/ForgetFormMain';
|
||||
import PublicRoute from './components/Routes/PublicRoute';
|
||||
|
||||
import { TermsPage, PrivacyPage } from './components/Authentication/Legal/TermsPrivacyComponents';
|
||||
|
||||
import Dashboard from './components/Home/Dashboard/Dashboard';
|
||||
import Inventory from './components/Home/Inventory/Inventory';
|
||||
import Analytics from './components/Home/Analytics&Reporting/Analytics';
|
||||
import Supplier from './components/Home/Supplier/Supplier';
|
||||
import Cashier from './components/Home/Cashier/Cashier';
|
||||
import CreateYourRestaurant from './components/Home/CreateYourRestaurant/CreateRestaurant';
|
||||
import { RestaurantProvider } from './contexts/RestaurantContext';
|
||||
import ProtectedRoutes from './components/Routes/ProtectedRoutes';
|
||||
import { SnackbarProvider } from "./contexts/SnackbarContext";
|
||||
import { UserProvider } from './contexts/UserContext';
|
||||
|
||||
import Training from './components/Home/Training/Training';
|
||||
import RestaurantProfile from './components/Home/RestaurantProfile/RestaurantProfile';
|
||||
import HostKitchen from './components/Home/HostKitchen/HostKitchen';
|
||||
import Settings from './components/Home/Settings/Setting';
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<LoginForm />} />
|
||||
<Route path="/login" element={<LoginForm />} />
|
||||
<Route path="/register" element={<RegisterForm />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/inventory" element={<Inventory />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/profile" element={<RestaurantProfile />} />
|
||||
<Route path="/create-kitchen" element={<CreateYourRestaurant />} />
|
||||
<Route path="/host-kitchen" element={<HostKitchen />} />
|
||||
<Route path="/cashier" element={<Cashier />} />
|
||||
<Route path="/supplier" element={<Supplier />} />
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/forget" element={<ForgetPassword />} />
|
||||
<Route path="/training" element={<Training />} />
|
||||
<GoogleOAuthProvider clientId="443509481732-4i5ne811hr90rn75q3fbqevqi9eu3lvk.apps.googleusercontent.com">
|
||||
<Router>
|
||||
<UserProvider>
|
||||
{/* ✅ لفّ كل المشروع بـ SnackbarProvider */}
|
||||
<SnackbarProvider>
|
||||
<Routes>
|
||||
{/* التوجيه التلقائي للصفحة الرئيسية */}
|
||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||
|
||||
</Routes>
|
||||
</Router>
|
||||
<Route path="/login" element={<PublicRoute><LoginForm /></PublicRoute>} />
|
||||
<Route path="/register" element={<PublicRoute><RegisterForm /></PublicRoute>} />
|
||||
<Route path="/forget" element={<PublicRoute><ForgetPassword /></PublicRoute>} />
|
||||
|
||||
{/* روابط الشروط والخصوصية */}
|
||||
<Route path="/terms" element={<TermsPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPage />} />
|
||||
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<RestaurantProvider>
|
||||
|
||||
<ProtectedRoutes />
|
||||
|
||||
</RestaurantProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</SnackbarProvider>
|
||||
</UserProvider>
|
||||
</Router>
|
||||
</GoogleOAuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -13,7 +13,7 @@ const Congratulations = ({ emailOrPhone, setEmailOrPhone }) => {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'auto',
|
||||
// overflow: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
|
||||
@@ -1,147 +1,226 @@
|
||||
// EmailInputSection.js
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Button, TextField, Typography ,useTheme} from '@mui/material';
|
||||
import { Box, Button, TextField, Typography, useTheme, Snackbar, Alert } from '@mui/material';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
const EmailInputSection = ({ emailOrPhone, setEmailOrPhone }) => {
|
||||
const theme = useTheme();
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const slides = [0, 1, 2, 3];
|
||||
const EmailInputSection = ({ emailOrPhone, setEmailOrPhone, onStepComplete }) => {
|
||||
const theme = useTheme();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSend = () => {
|
||||
// عملية الإرسال
|
||||
};
|
||||
// حالات Snackbar
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState('success');
|
||||
const [inputError, setInputError] = useState('');
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentSlide < slides.length - 1) {
|
||||
setCurrentSlide(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
const handleCloseSnackbar = (event, reason) => {
|
||||
if (reason === 'clickaway') return;
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentSlide > 0) {
|
||||
setCurrentSlide(prev => prev - 1);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Box
|
||||
component="img"
|
||||
src="/image.png"
|
||||
alt="logo"
|
||||
sx={{
|
||||
width: { xs: '5vh', sm: '7vh' },
|
||||
maxWidth: { xs: '40px', sm: '80px' },
|
||||
height: 'auto',
|
||||
objectFit: 'contain',
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
const validateEmail = (email) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Forget Password
|
||||
</Typography>
|
||||
const handleSend = async () => {
|
||||
setInputError(''); // مسح الأخطاء السابقة
|
||||
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Enter your email to reset it and regain access to your account.
|
||||
</Typography>
|
||||
if (!emailOrPhone) {
|
||||
setInputError("Please enter your email.");
|
||||
return;
|
||||
}
|
||||
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
Email/Phone
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="Enter your email"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={emailOrPhone}
|
||||
onChange={(e) => setEmailOrPhone(e.target.value)}
|
||||
sx={{
|
||||
'& input': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px'
|
||||
},
|
||||
'& input::placeholder': {
|
||||
color: '#969BA7'
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
|
||||
borderColor: '#FF914D',
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
if (!validateEmail(emailOrPhone)) {
|
||||
setInputError("Please enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
|
||||
{/* send */}
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleSend}
|
||||
sx={{
|
||||
color:'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: '30px',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
height: '48px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover ,
|
||||
transition: 'all 0.4s ease'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
setLoading(true);
|
||||
const response = await authService.resetPassword(emailOrPhone);
|
||||
setLoading(false);
|
||||
|
||||
<Typography
|
||||
textAlign="center"
|
||||
fontWeight={500}
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
mx: 'auto',
|
||||
color: '#969BA7'
|
||||
}}
|
||||
>
|
||||
By log in, I agree to the
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Terms of Service</a> and
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Privacy Policy</a>
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
if (response.success !== false) {
|
||||
setSnackbarMessage("A reset code has been sent to your email.");
|
||||
setSnackbarSeverity('success');
|
||||
setSnackbarOpen(true);
|
||||
|
||||
setTimeout(() => {
|
||||
if (onStepComplete) onStepComplete();
|
||||
}, 5000);
|
||||
|
||||
} else {
|
||||
setInputError('');
|
||||
let errorMessage = "";
|
||||
if (response.errors) {
|
||||
const firstError = Object.values(response.errors)[0];
|
||||
errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
||||
} else if (response.message) {
|
||||
errorMessage = response.message;
|
||||
} else {
|
||||
errorMessage = "Failed to send reset code.";
|
||||
}
|
||||
|
||||
if (
|
||||
errorMessage.toLowerCase().includes("not found") ||
|
||||
errorMessage.toLowerCase().includes("not registered") ||
|
||||
errorMessage.toLowerCase().includes("doesn't exist") ||
|
||||
errorMessage.toLowerCase().includes("email")
|
||||
) {
|
||||
setInputError("This email is not registered.");
|
||||
} else if (errorMessage.toLowerCase().includes("invalid credentials")) {
|
||||
setInputError("Email or password is incorrect.");
|
||||
} else {
|
||||
setSnackbarMessage(errorMessage);
|
||||
setSnackbarSeverity('error');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box>
|
||||
<Box
|
||||
component="img"
|
||||
src="/image.png"
|
||||
alt="logo"
|
||||
sx={{
|
||||
width: { xs: '5vh', sm: '7vh' },
|
||||
maxWidth: { xs: '40px', sm: '80px' },
|
||||
height: 'auto',
|
||||
objectFit: 'contain',
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
fontWeight={700}
|
||||
sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}
|
||||
>
|
||||
Forget Password
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" mb={2}>
|
||||
Enter your email to reset it and regain access to your account.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Email
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="Enter your email"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={emailOrPhone}
|
||||
onChange={(e) => setEmailOrPhone(e.target.value)}
|
||||
error={!!inputError}
|
||||
helperText={inputError}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#FF914D',
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleSend}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: '30px',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
height: '48px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
transition: 'all 0.4s ease',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? "Sending..." : "Send"}
|
||||
</Button>
|
||||
{/*
|
||||
<Typography
|
||||
textAlign="center"
|
||||
fontWeight={500}
|
||||
sx={{ fontSize: '16px', width: '100%', mx: 'auto', color: '#969BA7', mt: 2 }}
|
||||
>
|
||||
By log in, I agree to the
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>
|
||||
Terms of Service
|
||||
</a>{' '}
|
||||
and
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</Typography> */}
|
||||
|
||||
{/* Terms */}
|
||||
<Typography
|
||||
// variant="caption"
|
||||
textAlign="center"
|
||||
fontWeight={500}
|
||||
|
||||
sx={{ fontSize: '16px', width: '100%', mx: 'auto', color: '#969BA7', mt: 2 }}
|
||||
|
||||
>
|
||||
By logging in, I agree to the{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/terms', 'TermsOfService')}
|
||||
>
|
||||
Terms of Service
|
||||
</Box>{' '}
|
||||
and{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
|
||||
>
|
||||
Privacy Policy
|
||||
</Box>.
|
||||
</Typography>
|
||||
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={4000}
|
||||
onClose={handleCloseSnackbar}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
onClose={handleCloseSnackbar}
|
||||
severity={snackbarSeverity}
|
||||
sx={{ width: { xs: '40%', sm: '60%', md: '100%' }, color: 'white', fontSize: '16px', fontWeight: '500', backgroundColor: '#e57f3f', borderRadius: 6, mb: 6 }}
|
||||
variant="filled"
|
||||
>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailInputSection;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Stack ,useTheme} from '@mui/material';
|
||||
import { Box, Stack, useTheme } from '@mui/material';
|
||||
import Side from './Side';
|
||||
import EmailInputSection from './EmailInputSection';
|
||||
import OtpVerification from './OtpVerification';
|
||||
import NewPassword from './NewPassword';
|
||||
import Congratulations from './Congratulations';
|
||||
|
||||
|
||||
|
||||
const ForgetForm = () => {
|
||||
const [emailOrPhone, setEmailOrPhone] = useState('');
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const theme = useTheme();
|
||||
const [stepCompleted, setStepCompleted] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const slides = [
|
||||
{ component: EmailInputSection, title: "Email Verification" },
|
||||
{ component: OtpVerification, title: "OTP Verification" },
|
||||
@@ -22,33 +22,37 @@ const ForgetForm = () => {
|
||||
const handleNext = () => {
|
||||
if (currentSlide < slides.length - 1) {
|
||||
setCurrentSlide(prev => prev + 1);
|
||||
setStepCompleted(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentSlide > 0) {
|
||||
setCurrentSlide(prev => prev - 1);
|
||||
setStepCompleted(true);
|
||||
}
|
||||
};
|
||||
|
||||
const CurrentComponent = slides[currentSlide].component;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#fff',
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#fff' ,
|
||||
|
||||
}}
|
||||
>
|
||||
{/* ✅ Sidebar يظهر فقط من sm+ */}
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
}}>
|
||||
<Box sx={{ width: { sm: '40%', xs: '0%' }, display: { xs: 'none', sm: 'block' } }}>
|
||||
<Side currentStepIndex={currentSlide} onNext={handleNext} onBack={handleBack} />
|
||||
<Side
|
||||
currentStepIndex={currentSlide}
|
||||
onNext={handleNext}
|
||||
onBack={handleBack}
|
||||
stepCompleted={stepCompleted}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* ✅ Form */}
|
||||
<Box
|
||||
sx={{
|
||||
width: { sm: '60%' },
|
||||
@@ -62,32 +66,25 @@ const ForgetForm = () => {
|
||||
backgroundColor: '#fff',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 686,
|
||||
pr: { xs: 3, sm: 2, md: 1 },
|
||||
pt: { xs: 2, sm: 4, md: 1 },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: '100%', maxWidth: 686, pr: { xs: 3, sm: 2, md: 1 }, pt: { xs: 2, sm: 4, md: 1 } }}>
|
||||
<Stack spacing={2} alignItems="flex-start">
|
||||
|
||||
|
||||
{/* إظهار المكون الحالي فقط */}
|
||||
<CurrentComponent
|
||||
emailOrPhone={emailOrPhone}
|
||||
setEmailOrPhone={setEmailOrPhone}
|
||||
onStepComplete={() => {
|
||||
setStepCompleted(true);
|
||||
setCurrentSlide(prev => prev + 1); // هنا تنقل مباشرة للصفحة التالية
|
||||
}}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* ✅ مؤشر الشرائح السفلي */}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: { xs: 'absolute', sm: 'absolute' },
|
||||
bottom: { xs: '5%', sm: '-1%', md: '2%' },
|
||||
position: 'absolute',
|
||||
bottom: { xs: '5%', sm: '5%', md: '2%' },
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex',
|
||||
@@ -95,11 +92,7 @@ const ForgetForm = () => {
|
||||
width: '80%',
|
||||
maxWidth: 600,
|
||||
justifyContent: 'center',
|
||||
height: {
|
||||
xs: 3,
|
||||
sm: 5,
|
||||
md: 6,
|
||||
},
|
||||
height: { xs: 3, sm: 5, md: 6 },
|
||||
}}
|
||||
>
|
||||
{slides.map((_, index) => (
|
||||
@@ -108,12 +101,11 @@ const ForgetForm = () => {
|
||||
sx={{
|
||||
width: '100vh',
|
||||
height: 5,
|
||||
bgcolor: index === currentSlide ? theme.palette.primary.main : 'rgba(0, 0, 0, 0.2)',
|
||||
bgcolor: index === currentSlide ? theme.palette.primary.main : 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 2,
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.3s',
|
||||
}}
|
||||
// onClick={() => setCurrentSlide(index)} // يمكنك تفعيلها لاحقًا
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -122,4 +114,5 @@ const ForgetForm = () => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default ForgetForm;
|
||||
|
||||
@@ -1,217 +1,222 @@
|
||||
// EmailInputSection.js
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||
import { Box, Button, TextField, Typography, IconButton, InputAdornment } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { IconButton, InputAdornment } from '@mui/material';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
import { VisibilityOutlined } from '@mui/icons-material';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
const EmailInputSection = ({ emailOrPhone, setEmailOrPhone }) => {
|
||||
const theme = useTheme();
|
||||
const handleTogglePassword = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
const NewPassword = ({ emailOrPhone, onStepComplete }) => {
|
||||
const theme = useTheme();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const slides = [0, 1, 2, 3];
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleSend = () => {
|
||||
// عملية الإرسال
|
||||
};
|
||||
const handleToggleConfirmPassword = () => {
|
||||
setShowConfirmPassword((prev) => !prev);
|
||||
};
|
||||
const handleTogglePassword = () => setShowPassword(prev => !prev);
|
||||
const handleToggleConfirmPassword = () => setShowConfirmPassword(prev => !prev);
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentSlide < slides.length - 1) {
|
||||
setCurrentSlide(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
const handleSend = async () => {
|
||||
setErrors({});
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentSlide > 0) {
|
||||
setCurrentSlide(prev => prev - 1);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Box
|
||||
component="img"
|
||||
src="/image.png"
|
||||
alt="logo"
|
||||
sx={{
|
||||
width: { xs: '5vh', sm: '7vh' },
|
||||
maxWidth: { xs: '40px', sm: '80px' },
|
||||
height: 'auto',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
if (!password || !passwordConfirm) {
|
||||
alert("Please fill in both password fields.");
|
||||
return;
|
||||
}
|
||||
if (password !== passwordConfirm) {
|
||||
alert("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create a New Password
|
||||
</Typography>
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await authService.updatePassword({
|
||||
email: emailOrPhone,
|
||||
password,
|
||||
confirmPassword: passwordConfirm
|
||||
});
|
||||
setLoading(false);
|
||||
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Enter your email to reset it and regai access to your account.
|
||||
</Typography>
|
||||
if (response.success) {
|
||||
alert("Password updated successfully.");
|
||||
if (onStepComplete) onStepComplete();
|
||||
} else {
|
||||
alert(response.message || "Failed to update password.");
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
if (error.response && error.response.status === 422) {
|
||||
setErrors(error.response.data.errors || {});
|
||||
} else {
|
||||
alert("Error: " + (error.message || "Unknown error"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
{/* Password Input */}
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
sx={{
|
||||
'& input': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px'
|
||||
},
|
||||
'& input::placeholder': {
|
||||
color: '#969BA7'
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
|
||||
}
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleTogglePassword} edge="end">
|
||||
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Box
|
||||
component="img"
|
||||
src="/image.png"
|
||||
alt="logo"
|
||||
sx={{
|
||||
width: { xs: '5vh', sm: '7vh' },
|
||||
maxWidth: { xs: '40px', sm: '80px' },
|
||||
height: 'auto',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' }
|
||||
}}
|
||||
>
|
||||
Create a New Password
|
||||
</Typography>
|
||||
|
||||
{/* Confirm Password Input */}
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
Confirm Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
placeholder="Confirm your password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
sx={{
|
||||
'& input': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px'
|
||||
},
|
||||
'& input::placeholder': {
|
||||
color: '#969BA7'
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
|
||||
borderColor: '#FF914D',
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleToggleConfirmPassword} edge="end">
|
||||
{showConfirmPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Enter your new password to regain access to your account.
|
||||
</Typography>
|
||||
|
||||
{/* send */}
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleSend}
|
||||
sx={{
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: '30px',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
height: '48px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover ,
|
||||
transition: 'all 0.4s ease'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
fontWeight={500}
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
mx: 'auto',
|
||||
color: '#969BA7'
|
||||
}}
|
||||
>
|
||||
By log in, I agree to the
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Terms of Service</a> and
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Privacy Policy</a>
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
error={!!errors.password}
|
||||
helperText={errors.password && errors.password.join(' ')}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': { display: 'none' },
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleTogglePassword} edge="end">
|
||||
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Confirm Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
placeholder="Confirm your password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
value={passwordConfirm}
|
||||
onChange={e => setPasswordConfirm(e.target.value)}
|
||||
error={!!errors.password_confirmation}
|
||||
helperText={errors.password_confirmation && errors.password_confirmation.join(' ')}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#FF914D',
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': { display: 'none' },
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleToggleConfirmPassword} edge="end">
|
||||
{showConfirmPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleSend}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: '30px',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
height: '48px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
transition: 'all 0.4s ease'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? "Sending..." : "Send"}
|
||||
</Button>
|
||||
|
||||
{/* Terms */}
|
||||
<Typography
|
||||
// variant="caption"
|
||||
textAlign="center"
|
||||
fontWeight={500}
|
||||
|
||||
sx={{ fontSize: '16px', width: '100%', mx: 'auto', color: '#969BA7', mt: 2 }}
|
||||
|
||||
>
|
||||
By logging in, I agree to the{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/terms', 'TermsOfService')}
|
||||
>
|
||||
Terms of Service
|
||||
</Box>{' '}
|
||||
and{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
|
||||
>
|
||||
Privacy Policy
|
||||
</Box>.
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailInputSection;
|
||||
export default NewPassword;
|
||||
|
||||
@@ -1,18 +1,54 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Box, TextField, Typography, Button, Stack ,useTheme} from '@mui/material';
|
||||
import {
|
||||
Box, TextField, Typography, Button, Stack, useTheme,
|
||||
Snackbar, Alert
|
||||
} from '@mui/material';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
const OtpVerification = () => {
|
||||
const [emailOrPhone, setEmailOrPhone] = useState('');
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const slides = [0, 1, 2, 3];
|
||||
const OtpVerification = ({ emailOrPhone, onStepComplete }) => {
|
||||
const [otp, setOtp] = useState(Array(6).fill(''));
|
||||
const inputRefs = useRef(Array(6).fill().map(() => React.createRef()));
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [otp, setOtp] = useState(Array(7).fill(''));
|
||||
const inputRefs = useRef(Array(7).fill().map(() => React.createRef()));
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState('success');
|
||||
|
||||
const handleSend = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleSnackbarClose = (event, reason) => {
|
||||
if (reason === 'clickaway') return;
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
const otpCode = otp.join('');
|
||||
console.log('Submitted OTP:', otpCode);
|
||||
// عملية التحقق من الكود
|
||||
if (otpCode.length < 6) {
|
||||
setSnackbarMessage("Please enter the complete OTP code.");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await authService.verifyCode(emailOrPhone, otpCode);
|
||||
setLoading(false);
|
||||
if (response.success !== false) {
|
||||
setSnackbarMessage("OTP verified successfully.");
|
||||
setSnackbarSeverity("success");
|
||||
setSnackbarOpen(true);
|
||||
if (onStepComplete) onStepComplete(); // الانتقال للخطوة التالية
|
||||
} else {
|
||||
setSnackbarMessage(response.message || "Invalid OTP code.");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setSnackbarMessage("Failed to verify OTP. Please try again.");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtpChange = (e, index) => {
|
||||
@@ -23,7 +59,7 @@ const OtpVerification = () => {
|
||||
newOtp[index] = value;
|
||||
setOtp(newOtp);
|
||||
|
||||
if (value && index < 6) {
|
||||
if (value && index < otp.length - 1) {
|
||||
inputRefs.current[index + 1].current.focus();
|
||||
}
|
||||
};
|
||||
@@ -34,19 +70,6 @@ const OtpVerification = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentSlide < slides.length - 1) {
|
||||
setCurrentSlide(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentSlide > 0) {
|
||||
setCurrentSlide(prev => prev - 1);
|
||||
}
|
||||
};
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
@@ -77,12 +100,11 @@ const OtpVerification = () => {
|
||||
Enter OTP Verification
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Kindly enter the OTP code sent to your registered email/phone for account verification.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={{ xs: 0, sm: 1, md: 1.5 }}
|
||||
@@ -113,15 +135,15 @@ const OtpVerification = () => {
|
||||
height: { xs: '3rem', sm: '3.25rem', md: '4rem' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '0.5rem',
|
||||
height: '100%', // تأكيد تطابق الطول
|
||||
height: '100%',
|
||||
'& fieldset': {
|
||||
borderColor: '#ccc',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
'& .MuiInputBase-input': {
|
||||
@@ -135,47 +157,83 @@ const OtpVerification = () => {
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleSend}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color:'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: '30px',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
width: '95%',
|
||||
height: '48px',
|
||||
mx: 'auto',
|
||||
display: 'block',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover ,
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
transition: 'all 0.4s ease'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
{loading ? "Sending..." : "Send"}
|
||||
</Button>
|
||||
|
||||
|
||||
{/* Terms */}
|
||||
<Typography
|
||||
// variant="caption"
|
||||
textAlign="center"
|
||||
fontWeight={500}
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
mx: 'auto',
|
||||
color: '#969BA7'
|
||||
}}
|
||||
|
||||
sx={{ fontSize: '16px', width: '100%', mx: 'auto', color: '#969BA7', mt: 2 }}
|
||||
|
||||
>
|
||||
By log in, I agree to the
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Terms of Service</a> and
|
||||
<a href="#" style={{ color: '#007bff', textDecoration: 'none' }}>Privacy Policy</a>
|
||||
By logging in, I agree to the{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/terms', 'TermsOfService')}
|
||||
>
|
||||
Terms of Service
|
||||
</Box>{' '}
|
||||
and{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
|
||||
>
|
||||
Privacy Policy
|
||||
</Box>.
|
||||
</Typography>
|
||||
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={4000}
|
||||
onClose={handleSnackbarClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
onClose={handleSnackbarClose}
|
||||
severity={snackbarSeverity}
|
||||
sx={{
|
||||
width: '100%',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: 6,
|
||||
mb: 6
|
||||
}}
|
||||
variant="filled"
|
||||
>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Stack, Button ,useTheme } from '@mui/material';
|
||||
import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
|
||||
@@ -27,7 +27,7 @@ const steps = [
|
||||
];
|
||||
|
||||
const Side = ({ currentStepIndex, onNext, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -46,7 +46,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
|
||||
Instructions for secure password modification. Follow simple steps for password change.
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={6} position="relative" sx={{ ml: 3 }}>
|
||||
<Stack spacing={6} position="relative" sx={{ ml: {xs:-1,md:2.1} }}>
|
||||
{steps.map((step, index) => (
|
||||
<Box key={index} display="flex" alignItems="flex-start" gap={2} position="relative">
|
||||
{index !== steps.length - 1 && (
|
||||
@@ -109,7 +109,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box >
|
||||
<Typography variant="subtitle1" sx={{ fontSize: '16px', fontWeight: 600 }}>
|
||||
{step.title}
|
||||
</Typography>
|
||||
@@ -121,13 +121,11 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* ... (بقية محتوى الـ Side كما هو بدون تغيير) ... */}
|
||||
|
||||
{/* أزرار التنقل */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 2,
|
||||
bottom: {sm:31 , md:2},
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '90%',
|
||||
@@ -152,7 +150,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
color: theme.palette.primary.main
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -172,7 +170,7 @@ const Side = ({ currentStepIndex, onNext, onBack }) => {
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
color: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
transition: 'all 0.4s ease'
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, Typography, Container, Box, Paper } from '@mui/material';
|
||||
|
||||
/*
|
||||
File: TermsPrivacyComponents.jsx
|
||||
Purpose: Two simple standalone pages (Terms and Privacy) and an example LoginCard
|
||||
which opens them in a new tab/window and listens for the "agree" message.
|
||||
|
||||
How it works:
|
||||
- The LoginCard opens a new tab using window.open(...).
|
||||
- The opened page (TermsPage / PrivacyPage) will send a postMessage back to the opener
|
||||
when the user clicks "Agree & Confirm", then attempt to close itself.
|
||||
- The LoginCard listens for that postMessage and can react (e.g. show a toast,
|
||||
update state, or navigate). This keeps the original page in control of what
|
||||
happens after the user agreed.
|
||||
|
||||
Note: Some browsers may block window.close() on tabs not opened by window.open.
|
||||
Because LoginCard uses window.open, the close() call should succeed. If the
|
||||
page is opened directly (no opener) the Agree button will fall back to
|
||||
navigating back (history.back()).
|
||||
*/
|
||||
|
||||
/***********************
|
||||
* Terms / Privacy Pages
|
||||
***********************/
|
||||
|
||||
function sendAgreeToOpener(kind = 'terms') {
|
||||
try {
|
||||
if (window.opener && !window.opener.closed) {
|
||||
// Structured message so opener can identify the response
|
||||
window.opener.postMessage({ type: 'AGREE_CONFIRM', page: kind }, '*');
|
||||
// Try to close this tab/window (works when opened by window.open)
|
||||
window.close();
|
||||
} else {
|
||||
// No opener (user opened directly) — go back in history as a fallback
|
||||
if (window.history && window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
// Last fallback: navigate to the site's root
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to send message to opener or close window', e);
|
||||
// fallback navigation
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
export function TermsPage() {
|
||||
React.useEffect(() => {
|
||||
document.title = "Resturant-Terms of Service";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 6 }}>
|
||||
<Paper elevation={3} sx={{ p: 4 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Terms of Service
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" paragraph>
|
||||
Welcome to our Terms of Service. Please read carefully. Lorem ipsum dolor sit amet,
|
||||
consectetur adipiscing elit. Integer posuere erat a ante.
|
||||
</Typography>
|
||||
|
||||
<Box textAlign="center" sx={{ mt: 4 , color:'white'}}>
|
||||
<Button variant="contained" onClick={() => sendAgreeToOpener('terms')} sx={{color:'white' , textTransform:'none'}}>
|
||||
Agree & Confirm
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function PrivacyPage() {
|
||||
React.useEffect(() => {
|
||||
document.title = "Resturant-Privacy Policy";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 6 }}>
|
||||
<Paper elevation={3} sx={{ p: 4 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" paragraph>
|
||||
This Privacy Policy explains how we collect and use personal information. Lorem ipsum dolor
|
||||
sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore.
|
||||
</Typography>
|
||||
|
||||
<Box textAlign="center" sx={{ mt: 4 ,color:'white'}}>
|
||||
<Button variant="contained" onClick={() => sendAgreeToOpener('privacy')} sx={{color:'white' , textTransform:'none'}}>
|
||||
Agree & Confirm
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/***********************
|
||||
* Example LoginCard
|
||||
***********************/
|
||||
|
||||
export default function LoginCard() {
|
||||
useEffect(() => {
|
||||
// Listen to messages from the terms/privacy pages.
|
||||
const handler = (event) => {
|
||||
// IMPORTANT: In production, check event.origin to be sure the message comes from
|
||||
// a trusted origin. For demo purposes we accept any origin.
|
||||
const data = event.data || {};
|
||||
if (data && data.type === 'AGREE_CONFIRM') {
|
||||
// data.page === 'terms' or 'privacy'
|
||||
const page = data.page || 'unknown';
|
||||
// Do whatever you need when user agreed: update state, show toast, close dialog, etc.
|
||||
// Example: simple alert (replace with MUI Snackbar or app-level handler)
|
||||
alert(`User agreed to ${page === 'terms' ? 'Terms of Service' : 'Privacy Policy'}`);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
return () => window.removeEventListener('message', handler);
|
||||
}, []);
|
||||
|
||||
// Helper that opens a new tab (ensures opener exists)
|
||||
function openInNewTab(path, name) {
|
||||
// If your app uses router paths (e.g. /terms), build a full URL. Here we create
|
||||
// a URL relative to current origin so the opened page belongs to the same origin
|
||||
const url = new URL(path, window.location.href).toString();
|
||||
// Use window.open (not anchor target="_blank") so the new tab has window.opener
|
||||
window.open(url, name, 'noopener=false');
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper elevation={2} sx={{ p: 4, maxWidth: 420, margin: '40px auto' }}>
|
||||
<Typography variant="h6" textAlign="center" gutterBottom>
|
||||
{/* Login title */}
|
||||
Sign in
|
||||
</Typography>
|
||||
|
||||
{/* Your existing Terms text (you provided earlier) */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ fontSize: '14px', textAlign: 'center', color: '#969BA7', pt: 5 }}
|
||||
>
|
||||
By logging in, I agree to the{' '}
|
||||
<Box component="span" sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }} onClick={() => openInNewTab('/terms', 'TermsOfService')}>
|
||||
Terms of Service
|
||||
</Box>{' '}
|
||||
and{' '}
|
||||
<Box component="span" sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }} onClick={() => openInNewTab('/privacy', 'PrivacyPolicy')}>
|
||||
Privacy Policy
|
||||
</Box>
|
||||
.
|
||||
</Typography>
|
||||
|
||||
{/* The rest of your login UI goes here (inputs, submit, etc.) */}
|
||||
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
Usage notes:
|
||||
- If you're using a router (react-router), create routes for /terms and /privacy
|
||||
that render <TermsPage /> and <PrivacyPage /> respectively.
|
||||
|
||||
Example (react-router v6):
|
||||
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import LoginCard, { TermsPage, PrivacyPage } from './TermsPrivacyComponents';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<LoginCard />} />
|
||||
<Route path="/terms" element={<TermsPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
Security reminder: validate event.origin in the message handler in production.
|
||||
*/
|
||||
@@ -3,27 +3,94 @@ import { TextField, Button, Typography, Stack, Box, IconButton, InputAdornment }
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
import { VisibilityOutlined } from '@mui/icons-material';
|
||||
import SidePanel from './SidePanel'; // استدعاء SidePanel من نفس المجلد أو حسب المسار المناسب
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import SidePanel from './SidePanel';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
import { GoogleLogin, useGoogleLogin } from '@react-oauth/google';
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
const LoginForm = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleTogglePassword = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
setShowPassword(prev => !prev);
|
||||
};
|
||||
const handleLogin = () => {
|
||||
// بعد تنفيذ عملية تسجيل الدخول بنجاح، يتم التوجيه
|
||||
navigate('/dashboard');
|
||||
|
||||
const handleLogin = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const result = await authService.login(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
// 🔹 أولًا: نتحقق إن كانت الرسالة تشير إلى أن الحساب غير موجود
|
||||
if (result.message && result.message.includes('Account not found')) {
|
||||
setError('This account is not registered.');
|
||||
}
|
||||
// 🔹 ثانيًا: نتحقق من "Invalid credentials"
|
||||
else if (result.message && result.message.includes('Invalid credentials')) {
|
||||
setError('Email or password is incorrect.');
|
||||
}
|
||||
// 🔹 ثالثًا: نعرض أول خطأ موجود في كائن errors
|
||||
else if (result.errors) {
|
||||
const firstError = Object.values(result.errors)[0][0];
|
||||
setError(firstError);
|
||||
}
|
||||
// 🔹 أخيرًا: fallback لأي رسالة أخرى
|
||||
else {
|
||||
setError(result.message || 'Login failed');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('token', result.data.token);
|
||||
localStorage.setItem('refresh_token', result.data.refresh_token);
|
||||
setLoading(false);
|
||||
navigate('/restaurant', { replace: true });
|
||||
};
|
||||
|
||||
const handleGoogleSuccess = async (tokenResponse) => {
|
||||
try {
|
||||
const res = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenResponse.access_token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo = await res.json();
|
||||
console.log('Google User Info:', userInfo);
|
||||
|
||||
if (userInfo.email) {
|
||||
setEmail(userInfo.email);
|
||||
}
|
||||
|
||||
setPassword('');
|
||||
localStorage.setItem("google_user", JSON.stringify(userInfo));
|
||||
} catch (e) {
|
||||
console.error("Google login failed:", e);
|
||||
setError("Google login failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleError = () => {
|
||||
setError("Google login failed");
|
||||
};
|
||||
|
||||
const loginWithGoogle = useGoogleLogin({
|
||||
onSuccess: handleGoogleSuccess,
|
||||
onError: handleGoogleError,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
|
||||
<Box
|
||||
bgcolor={"background.default"}
|
||||
color={"text.primary"}
|
||||
@@ -38,14 +105,11 @@ const LoginForm = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* SidePanel ثابت */}
|
||||
<SidePanel />
|
||||
|
||||
{/* Login Content */}
|
||||
<Box
|
||||
sx={{
|
||||
marginLeft: { xs: 0, sm: '50%' },
|
||||
width: { xs: '100%', sm: '50%' ,md:'70%'},
|
||||
marginLeft: { xs: 0, sm: '50%' },
|
||||
width: { xs: '100%', sm: '50%', md: '70%' },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -76,7 +140,7 @@ const LoginForm = () => {
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
|
||||
{/* Logo */}
|
||||
{/* الشعار */}
|
||||
<Box>
|
||||
<img
|
||||
src="/image.png"
|
||||
@@ -90,30 +154,17 @@ const LoginForm = () => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Login
|
||||
{/* عنوان تسجيل الدخول */}
|
||||
<Typography variant="h4" fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
|
||||
Welcome you canLogin
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ pb: 1 }}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
|
||||
Enter your username and password to access your account securely. Welcome back to our service!
|
||||
</Typography>
|
||||
|
||||
{/* Email Input */}
|
||||
|
||||
{/* إدخال البريد */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Email
|
||||
@@ -123,15 +174,17 @@ const LoginForm = () => {
|
||||
type="email"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
@@ -142,7 +195,7 @@ const LoginForm = () => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Password Input */}
|
||||
{/* إدخال كلمة السر */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Password
|
||||
@@ -153,15 +206,17 @@ const LoginForm = () => {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
@@ -184,14 +239,22 @@ const LoginForm = () => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Login Button */}
|
||||
{/* عرض الخطأ */}
|
||||
{error && (
|
||||
<Typography color="error" textAlign="center">
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* زر تسجيل الدخول */}
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleLogin}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans' ,
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
@@ -204,23 +267,23 @@ const LoginForm = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Log in
|
||||
{loading ? 'Loading...' : 'Log in'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 2 }}>
|
||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, fontSize: '16px', color: 'black' , fontFamily: 'PlusJakartaSans' }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, fontSize: '16px', color: 'black', fontFamily: 'PlusJakartaSans' }}>
|
||||
Or
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
|
||||
</Box>
|
||||
|
||||
{/* Google Button */}
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={loginWithGoogle}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
@@ -233,8 +296,8 @@ const LoginForm = () => {
|
||||
'&:hover': {
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
, fontFamily: 'PlusJakartaSans'
|
||||
},
|
||||
fontFamily: 'PlusJakartaSans'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
@@ -245,10 +308,10 @@ const LoginForm = () => {
|
||||
/>
|
||||
</Box>
|
||||
Login with Google
|
||||
</Button>
|
||||
</Button> */}
|
||||
|
||||
{/* Facebook Button */}
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{
|
||||
@@ -264,7 +327,7 @@ const LoginForm = () => {
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
, fontFamily: 'PlusJakartaSans'
|
||||
, fontFamily: 'PlusJakartaSans'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
@@ -275,8 +338,7 @@ const LoginForm = () => {
|
||||
/>
|
||||
</Box>
|
||||
Login with Facebook
|
||||
</Button>
|
||||
|
||||
</Button> */}
|
||||
|
||||
{/* Register Link */}
|
||||
<Typography
|
||||
@@ -285,7 +347,7 @@ const LoginForm = () => {
|
||||
fontSize: '16px',
|
||||
textAlign: 'center',
|
||||
color: '#969BA7',
|
||||
pt: 3
|
||||
pt: 2
|
||||
}}
|
||||
>
|
||||
Don’t have an account?{' '}
|
||||
@@ -323,6 +385,7 @@ const LoginForm = () => {
|
||||
</Typography>
|
||||
|
||||
{/* Terms */}
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
@@ -333,14 +396,25 @@ const LoginForm = () => {
|
||||
}}
|
||||
>
|
||||
By logging in, I agree to the{' '}
|
||||
<a href="#" style={{ color: '#2261FF', textDecoration: 'none' }}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/terms', 'TermsOfService')}
|
||||
>
|
||||
Terms of Service
|
||||
</a>{' '}
|
||||
</Box>{' '}
|
||||
and{' '}
|
||||
<a href="#" style={{ color: '#2261FF', textDecoration: 'none' }}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
|
||||
>
|
||||
Privacy Policy
|
||||
</a>.
|
||||
</Box>.
|
||||
</Typography>
|
||||
|
||||
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -350,3 +424,4 @@ const LoginForm = () => {
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
|
||||
|
||||
@@ -1,15 +1,42 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState ,useContext } from 'react';
|
||||
import { TextField, Button, Typography, Stack, Box, IconButton, InputAdornment } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
import { VisibilityOutlined } from '@mui/icons-material';
|
||||
import SidePanel from './SidePanel';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import authService from '../../../services/authService';
|
||||
import { UserContext } from '../../../contexts/UserContext';
|
||||
|
||||
|
||||
// import { useGoogleRegister } from '@react-oauth/google';
|
||||
const generateStrongPassword = () => {
|
||||
const length = 12;
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
|
||||
let password = "";
|
||||
for (let i = 0, n = charset.length; i < length; ++i) {
|
||||
password += charset.charAt(Math.floor(Math.random() * n));
|
||||
}
|
||||
return password;
|
||||
};
|
||||
|
||||
const Register = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
|
||||
const { setUser } = useContext(UserContext);
|
||||
|
||||
// تفعيل/تعطيل إظهار كلمة السر
|
||||
const handleTogglePassword = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
@@ -18,21 +45,112 @@ const Register = () => {
|
||||
setShowConfirmPassword((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box bgcolor={"background.default"} color={"text.primary"} display="flex" sx={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}>
|
||||
{/* SidePanel ثابت */}
|
||||
|
||||
const handleRegister = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setSuccessMessage('');
|
||||
|
||||
const result = await authService.register(email, password, confirmPassword);
|
||||
|
||||
if (!result.success) {
|
||||
if (result.errors) {
|
||||
const firstError = Object.values(result.errors)[0][0];
|
||||
setError(firstError);
|
||||
} else if (result.message && result.message.includes('Duplicate entry')) {
|
||||
setError('The email has already been taken.');
|
||||
} else {
|
||||
setError(result.message || 'Registration failed');
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// تسجيل ناجح
|
||||
localStorage.setItem('token', result.data.token);
|
||||
localStorage.setItem('refresh_token', result.data.refresh_token);
|
||||
|
||||
// ✅ تحديث الـ User Context مباشرة بعد التسجيل
|
||||
setUser({
|
||||
email: email,
|
||||
token: result.data.token,
|
||||
refresh_token: result.data.refresh_token,
|
||||
adminData: result.data.Admin || null,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
navigate('/restaurant', { replace: true });
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
|
||||
const handleGoogleSuccess = async (tokenResponse) => {
|
||||
try {
|
||||
const res = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenResponse.access_token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo = await res.json();
|
||||
console.log('Google User Info:', userInfo);
|
||||
|
||||
if (userInfo.email) {
|
||||
const generatedPassword = generateStrongPassword(); // كلمة مرور مقترحة
|
||||
|
||||
setEmail(userInfo.email);
|
||||
setPassword(generatedPassword);
|
||||
setConfirmPassword(generatedPassword);
|
||||
|
||||
setError('');
|
||||
|
||||
setTimeout(() => {
|
||||
setSuccessMessage('');
|
||||
}, 5000);
|
||||
} else {
|
||||
setError('Google Register failed: No email found');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Google Register failed:', e);
|
||||
setError('Google Register failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleError = () => {
|
||||
setError('Google Register failed');
|
||||
};
|
||||
|
||||
const handleSnackbarClose = (event, reason) => {
|
||||
if (reason === 'clickaway') {
|
||||
return;
|
||||
}
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
// // تهيئة تسجيل الدخول عبر جوجل باستخدام useGoogleRegister
|
||||
// const RegisterWithGoogle = useGoogleRegister({
|
||||
// onSuccess: handleGoogleSuccess,
|
||||
// onError: handleGoogleError,
|
||||
// });
|
||||
|
||||
return (
|
||||
<Box
|
||||
bgcolor={'background.default'}
|
||||
color={'text.primary'}
|
||||
display="flex"
|
||||
sx={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SidePanel />
|
||||
|
||||
{/* Login Content */}
|
||||
<Box
|
||||
sx={{
|
||||
marginLeft: { xs: 0, sm: '50%' },
|
||||
@@ -60,14 +178,13 @@ const Register = () => {
|
||||
xs: '120%',
|
||||
sm: '100%',
|
||||
md: '120%',
|
||||
lg: '120%'
|
||||
lg: '120%',
|
||||
},
|
||||
maxWidth: '600px'
|
||||
maxWidth: '600px',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
|
||||
{/* Logo */}
|
||||
{/* logo */}
|
||||
<Box>
|
||||
<img
|
||||
src="/image.png"
|
||||
@@ -76,7 +193,7 @@ const Register = () => {
|
||||
width: '4vw',
|
||||
maxWidth: '80px',
|
||||
height: 'auto',
|
||||
objectFit: 'contain'
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -88,316 +205,291 @@ const Register = () => {
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem'
|
||||
}
|
||||
md: '2.2rem',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ pb: 1 }}
|
||||
>
|
||||
Please fill out the registration form with accurate information to
|
||||
create your account successfully.
|
||||
<Typography variant="body2" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
|
||||
Please fill out the registration form with accurate information to create your account successfully.
|
||||
</Typography>
|
||||
|
||||
{/* Email Input */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
Email
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="Enter your email"
|
||||
type="email"
|
||||
|
||||
{/* حقل الايميل */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Email
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="Enter your email"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* حقل كلمة المرور */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleTogglePassword} edge="end">
|
||||
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* حقل التاكيد لكلمة المرور */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Confirm Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
placeholder="Confirm your password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#FF914D',
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleToggleConfirmPassword} edge="end">
|
||||
{showConfirmPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Error & Success Message */}
|
||||
{error && (
|
||||
<Typography color="error" textAlign="center">
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
{successMessage && (
|
||||
<Typography color="green" textAlign="center">
|
||||
{successMessage}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* زر التسجيل */}
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleRegister}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Continue'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Divider */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 2 }}>
|
||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, fontSize: '16px', color: 'black' }}>
|
||||
Or
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
|
||||
</Box>
|
||||
|
||||
{/* زر التسجيل عبر جوجل */}
|
||||
{/* <Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{
|
||||
'& input': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px'
|
||||
},
|
||||
'& input::placeholder': {
|
||||
color: '#969BA7'
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Password Input */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
sx={{
|
||||
'& input': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px'
|
||||
},
|
||||
'& input::placeholder': {
|
||||
color: '#969BA7'
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleTogglePassword} edge="end">
|
||||
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Confirm Password Input */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
Confirm Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
placeholder="Confirm your password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
sx={{
|
||||
'& input': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px'
|
||||
},
|
||||
'& input::placeholder': {
|
||||
color: '#969BA7'
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': { // أضف هذا الجزء لتغيير لون الحدود عند التركيز
|
||||
borderColor: '#FF914D',
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)' // ظل برتقالي خفيف
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleToggleConfirmPassword} edge="end">
|
||||
{showConfirmPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Login Button */}
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: {
|
||||
xs: '14px',
|
||||
sm: '16px'
|
||||
},
|
||||
height: {
|
||||
xs: '45px',
|
||||
sm: '52px'
|
||||
},
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover
|
||||
}
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 2 }}>
|
||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
|
||||
<Typography
|
||||
variant="body1"
|
||||
// onClick={() => RegisterWithGoogle()}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
color: 'black'
|
||||
borderRadius: '50px',
|
||||
height: '50px',
|
||||
textTransform: 'none',
|
||||
gap: 1,
|
||||
borderColor: '#E6E6E6',
|
||||
color: 'black',
|
||||
'&:hover': {
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Or
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg"
|
||||
alt="Google"
|
||||
style={{ width: 25, height: 25 }}
|
||||
/>
|
||||
</Box>
|
||||
Register with Google
|
||||
</Button> */}
|
||||
|
||||
{/* زر فيسبوك - فقط للعرض (لم تُضاف وظيفة) */}
|
||||
{/* <Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
borderRadius: '50px',
|
||||
height: '50px',
|
||||
textTransform: 'none',
|
||||
gap: 1,
|
||||
borderColor: '#E6E6E6',
|
||||
color: 'black',
|
||||
'&:hover': {
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/0/05/Facebook_Logo_%282019%29.png"
|
||||
alt="Facebook"
|
||||
style={{ width: 25, height: 25 }}
|
||||
/>
|
||||
</Box>
|
||||
Register with Facebook
|
||||
</Button> */}
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
textAlign: 'center',
|
||||
color: '#969BA7',
|
||||
pt: 3,
|
||||
}}
|
||||
>
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
to="/login"
|
||||
style={{
|
||||
color: '#2261FF',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#E0E0E0' }} />
|
||||
</Box>
|
||||
|
||||
{/* Google Button */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
borderRadius: '50px',
|
||||
height: '50px',
|
||||
textTransform: 'none',
|
||||
gap: 1,
|
||||
borderColor: '#E6E6E6',
|
||||
color: 'black',
|
||||
'&:hover': {
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg"
|
||||
alt="Google"
|
||||
style={{ width: 25, height: 25 }}
|
||||
/>
|
||||
</Box>
|
||||
Login with Google
|
||||
</Button>
|
||||
|
||||
{/* Facebook Button */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
borderRadius: '50px',
|
||||
height: '50px',
|
||||
textTransform: 'none',
|
||||
gap: 1,
|
||||
borderColor: '#E6E6E6',
|
||||
color: 'black',
|
||||
'&:hover': {
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/0/05/Facebook_Logo_%282019%29.png"
|
||||
alt="Facebook"
|
||||
style={{ width: 25, height: 25 }}
|
||||
/>
|
||||
</Box>
|
||||
Login with Facebook
|
||||
</Button>
|
||||
|
||||
{/* login Link */}
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
textAlign: 'center',
|
||||
color: '#969BA7',
|
||||
pt: 3
|
||||
}}
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '14px',
|
||||
textAlign: 'center',
|
||||
color: '#969BA7',
|
||||
pt: 2
|
||||
}}
|
||||
>
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
to="/login"
|
||||
style={{
|
||||
color: '#2261FF',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
By logging in, I agree to the{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/terms', 'TermsOfService')}
|
||||
>
|
||||
Terms of Service
|
||||
</Box>{' '}
|
||||
and{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: '#2261FF', cursor: 'pointer', textDecoration: 'none' }}
|
||||
onClick={() => window.open('/privacy', 'PrivacyPolicy')}
|
||||
>
|
||||
Privacy Policy
|
||||
</Box>.
|
||||
</Typography>
|
||||
|
||||
{/* Terms */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '14px',
|
||||
textAlign: 'center',
|
||||
color: '#969BA7',
|
||||
pt: 5
|
||||
}}
|
||||
>
|
||||
By log in, I agree to the and {' '}
|
||||
<a href="#" style={{ color: '#2261FF', textDecoration: 'none' }}>
|
||||
Terms of Service
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="#" style={{ color: '#2261FF', textDecoration: 'none' }}>
|
||||
Privacy Policy
|
||||
</a>.
|
||||
</Typography>
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Stack,
|
||||
Box
|
||||
} from '@mui/material';
|
||||
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; // استيراد أيقونة السهم
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
|
||||
const SidePanel = ({ setMode, mode }) => {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
|
||||
const slides = [
|
||||
{
|
||||
image: '/images/waitress3.png',
|
||||
image: '/images/waitress1.png',
|
||||
title: "Welcome to our cutting-edge postal application,",
|
||||
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
|
||||
},
|
||||
{
|
||||
image: '/images/waitress3.png',
|
||||
image: '/images/waitress2.png',
|
||||
title: "Second Slide Title",
|
||||
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
|
||||
},
|
||||
@@ -30,7 +27,7 @@ const SidePanel = ({ setMode, mode }) => {
|
||||
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
|
||||
},
|
||||
{
|
||||
image: '/images/waitress3.png',
|
||||
image: '/images/waitress4.png',
|
||||
title: "Fourth Slide Title",
|
||||
description: "Welcome to our cutting-edge postal application, where sending and receiving mail has never been more convenient and efficient."
|
||||
}
|
||||
@@ -61,7 +58,7 @@ const SidePanel = ({ setMode, mode }) => {
|
||||
sx={{
|
||||
display: { xs: "none", sm: "block" },
|
||||
position: 'sticky',
|
||||
height: 'calc(102vh - 40px)', // لضمان ملاءمته للشاشة
|
||||
height: 'calc(102vh - 40px)',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -96,9 +93,9 @@ const SidePanel = ({ setMode, mode }) => {
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: {
|
||||
md: '37vh', // الافتراضي للشاشات الصغيرة
|
||||
md: '37vh',
|
||||
// xs: '5vh',
|
||||
sm: '42vh',// عندما تكون الشاشة متوسطة أو أكبر
|
||||
sm: '42vh',
|
||||
lg: '31vh', // من 1200px إلى أقل من 1536px
|
||||
|
||||
},
|
||||
@@ -119,17 +116,17 @@ const SidePanel = ({ setMode, mode }) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => window.open('https://www.google.com', '_blank')}
|
||||
onClick={() => window.open('https://landing.velocrea.com/', '_blank')}
|
||||
sx={{
|
||||
|
||||
mb: 1,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.24)',
|
||||
color: 'white',
|
||||
textTransform: 'none',
|
||||
borderRadius: '12px', // زاوية منحنية
|
||||
display: 'flex', // لتوزيع الأيقونة والنص بشكل أفقي
|
||||
alignItems: 'center', // لضبط الأيقونة والنص في المنتصف
|
||||
padding: '6px 20px', // يمكن تعديل المسافة حول النص والأيقونة
|
||||
borderRadius: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '6px 20px',
|
||||
borderRadius: '50px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 500,
|
||||
@@ -138,8 +135,8 @@ const SidePanel = ({ setMode, mode }) => {
|
||||
|
||||
>
|
||||
KITCHPLUS
|
||||
{/* إضافة أيقونة السهم مع تعديل زاوية الدوران */}
|
||||
<ArrowForwardIcon sx={{ ml: 1, transform: 'rotate(-45deg)' }} /> {/* تعديل زاوية السهم */}
|
||||
|
||||
<ArrowForwardIcon sx={{ ml: 1, transform: 'rotate(-45deg)' }} />
|
||||
</Button>
|
||||
|
||||
<Typography
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery, Skeleton } from '@mui/material';
|
||||
import { Box, useTheme, useMediaQuery } from '@mui/material';
|
||||
import KitchPlusAppBar from '../AppBar';
|
||||
import Sidebar from '../SideHome';
|
||||
import AnalyticsContect from './AnalyticsContect';
|
||||
@@ -15,11 +15,11 @@ const AnalyticsPage = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
|
||||
// محاكاة التحقق من المنتجات
|
||||
|
||||
useEffect(() => {
|
||||
const checkProducts = async () => {
|
||||
setIsLoading(true);
|
||||
const productsExist = await checkIfProductsExist(); // استبدل بمنطقك
|
||||
const productsExist = await checkIfProductsExist();
|
||||
setHasProducts(productsExist);
|
||||
setIsLoading(false);
|
||||
};
|
||||
@@ -28,7 +28,7 @@ const AnalyticsPage = () => {
|
||||
}, []);
|
||||
|
||||
const checkIfProductsExist = async () => {
|
||||
return new Promise((resolve) => setTimeout(() => resolve(true), 1500)); // محاكاة تأخير
|
||||
return new Promise((resolve) => setTimeout(() => resolve(true), 1500));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,8 +57,6 @@ const AnalyticsPage = () => {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
};
|
||||
return (
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', height: '100vh', backgroundColor: '#F6F6F6', overflow: 'hidden' }}>
|
||||
<Sidebar open={sidebarOpen} onClose={handleDrawerToggle} isMobile={isMobile} drawerWidth={drawerWidth} />
|
||||
|
||||
@@ -94,15 +92,9 @@ const AnalyticsPage = () => {
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton variant="rectangular" height={50} sx={{ mb: 2 }} />
|
||||
<Skeleton variant="rectangular" height={200} sx={{ mb: 2 }} />
|
||||
<Skeleton variant="rectangular" height={300} sx={{ mb: 2 }} />
|
||||
</>
|
||||
) : (
|
||||
|
||||
<AnalyticsContect />
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,104 +1,143 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRestaurant } from '../../../contexts/RestaurantContext';
|
||||
import StatisticsCard from './StatisticsCard';
|
||||
import TopSellingProduct from './TopSellingProduct';
|
||||
import SalesByLocation from './SalesByLocation';
|
||||
import TablesManager from './TablesManager';
|
||||
import {
|
||||
Box,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Skeleton,
|
||||
Button,
|
||||
Typography,
|
||||
ButtonGroup
|
||||
ButtonGroup,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
|
||||
import authService from '../../../services/authService';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const AnalyticsPage = () => {
|
||||
const [timeFrame, setTimeFrame] = useState('month');
|
||||
const { restaurantId } = useRestaurant();
|
||||
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
|
||||
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [hasProducts, setHasProducts] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
const [chartData, setChartData] = useState([]);
|
||||
|
||||
const dailyData = [
|
||||
{ date: '10:00', visitors: 1500, conversions: 300 },
|
||||
{ date: '11:00', visitors: 1800, conversions: 320 },
|
||||
];
|
||||
|
||||
const weeklyData = [
|
||||
{ label: 'Week 1', visitors: 1500, conversions: 200 },
|
||||
{ label: 'Week 2', visitors: 2300, conversions: 400 },
|
||||
];
|
||||
|
||||
const monthlyData = [
|
||||
{ label: 'Jan', visitors: 15000, conversions: 3000 },
|
||||
{ label: 'Feb', visitors: 18000, conversions: 4000 },
|
||||
];
|
||||
|
||||
const yearlyData = [
|
||||
{ label: '2024', visitors: 25000, conversions: 7000 },
|
||||
{ label: '2025', visitors: 38000, conversions: 12000 },
|
||||
];
|
||||
|
||||
|
||||
const getData = () => {
|
||||
switch (timeFrame) {
|
||||
case '24h':
|
||||
return dailyData;
|
||||
case '7d':
|
||||
return weeklyData;
|
||||
case '30d':
|
||||
return monthlyData;
|
||||
case '12m':
|
||||
return yearlyData;
|
||||
case 'all':
|
||||
return [...yearlyData, ...monthlyData, ...weeklyData, ...dailyData];
|
||||
default:
|
||||
return dailyData;
|
||||
}
|
||||
const handleTimeFrameChange = (newTimeFrame) => {
|
||||
setTimeFrame(newTimeFrame);
|
||||
};
|
||||
|
||||
const topSellingProducts = [
|
||||
{ product: 'Apple Watch', sales: 150, amount: 45000, price: 299, status: 'Published' },
|
||||
{ product: 'Samsung Galaxy', sales: 90, amount: 36000, price: 400, status: 'Low Stock' },
|
||||
{ product: 'Sony Headphones', sales: 60, amount: 18000, price: 299, status: 'Draft' },
|
||||
];
|
||||
const handleCustomDateChange = (event) => {
|
||||
setCustomDate(event.target.value);
|
||||
};
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
if (!restaurantId) return;
|
||||
|
||||
let period = 'daily';
|
||||
let labels = [];
|
||||
let mappedData = [];
|
||||
|
||||
switch (timeFrame) {
|
||||
case '24h': // يومي
|
||||
period = 'daily';
|
||||
labels = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`);
|
||||
try {
|
||||
const response = await authService.getReservationStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label || item.date || '',
|
||||
reservations_total: item.reservations_total || 0,
|
||||
number_of_people_total: item.number_of_people_total || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
reservations_total: 0,
|
||||
number_of_people_total: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
reservations_total: 0,
|
||||
number_of_people_total: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case '30d': // شهري
|
||||
period = 'monthly';
|
||||
labels = Array.from({ length: 30 }, (_, i) => dayjs().startOf('month').add(i, 'day').format('MM-DD'));
|
||||
try {
|
||||
const response = await authService.getReservationStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label ? dayjs(item.label).format('MM-DD') : item.date ? dayjs(item.date).format('MM-DD') : '',
|
||||
reservations_total: item.reservations_total || 0,
|
||||
number_of_people_total: item.number_of_people_total || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
reservations_total: 0,
|
||||
number_of_people_total: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
reservations_total: 0,
|
||||
number_of_people_total: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case '12m': // سنوي
|
||||
default:
|
||||
period = 'yearly';
|
||||
labels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
try {
|
||||
const response = await authService.getReservationStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label || item.date || '',
|
||||
reservations_total: item.reservations_total || 0,
|
||||
number_of_people_total: item.number_of_people_total || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
reservations_total: 0,
|
||||
number_of_people_total: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
reservations_total: 0,
|
||||
number_of_people_total: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
setChartData(mappedData);
|
||||
};
|
||||
|
||||
const salesData = [
|
||||
{ country: 'United Kingdom', amount: 17678, change: 12, sales: 340 },
|
||||
{ country: 'Spain', amount: 5500, change: -5, sales: 100 },
|
||||
{ country: 'Germany', amount: 24189, change: -25, sales: 540 },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const checkProducts = async () => {
|
||||
setIsLoading(true);
|
||||
const productsExist = await new Promise((resolve) =>
|
||||
setTimeout(() => resolve(true), 1500)
|
||||
);
|
||||
setHasProducts(productsExist);
|
||||
setIsLoading(false);
|
||||
};
|
||||
checkProducts();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [theme.breakpoints.values.md]);
|
||||
fetchStatistics();
|
||||
}, [timeFrame, restaurantId, customDate]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={{ xs: 1, sm: 2 }}
|
||||
maxWidth="100%"
|
||||
overflowX="hidden"
|
||||
>
|
||||
<Box p={{ xs: 1, sm: 2 }} maxWidth="100%" overflowX="hidden">
|
||||
{/* Tables Manager */}
|
||||
<Box mb={1.5}>
|
||||
{restaurantId && <TablesManager restaurantId={restaurantId} />}
|
||||
</Box>
|
||||
|
||||
{/* Header Buttons */}
|
||||
<Box
|
||||
display="flex"
|
||||
@@ -106,7 +145,7 @@ const yearlyData = [
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
mb={3}
|
||||
mb={1.5}
|
||||
>
|
||||
<ButtonGroup
|
||||
size="small"
|
||||
@@ -131,94 +170,73 @@ const yearlyData = [
|
||||
}
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: 'All Time', value: 'all' },
|
||||
{ label: '12 Months', value: '12m' },
|
||||
{ label: '30 Days', value: '30d' },
|
||||
{ label: '7 Days', value: '7d' },
|
||||
{ label: '24 Hour', value: '24h' },
|
||||
].map(({ label, value }) => (
|
||||
{[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => (
|
||||
<Button
|
||||
key={value}
|
||||
variant={timeFrame === value ? 'contained' : 'text'}
|
||||
onClick={() => setTimeFrame(value)}
|
||||
onClick={() => handleTimeFrameChange(value)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
<Box display="flex" gap={2} flexWrap="wrap" justifyContent={{ xs: 'space-between', sm: 'flex-end' }} width={{ xs: '100%', sm: 'auto' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
{/* Date Picker + Today Button */}
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: 2,
|
||||
p: '4px 8px',
|
||||
boxShadow: 'inset 0 0 0 1px #e0e0e0'
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="date"
|
||||
value={customDate}
|
||||
onChange={handleCustomDateChange}
|
||||
size="small"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: '#667085',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: 'none',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
border: '1px solid #e0e0e0',
|
||||
gap: 1,
|
||||
minWidth: '120px'
|
||||
width: isMobile ? '100%' : 150,
|
||||
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
|
||||
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
|
||||
}}
|
||||
>
|
||||
<CalendarTodayOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
{!isSmallScreen && 'Select Dates'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: '100px'
|
||||
}}
|
||||
>
|
||||
KPIs Filter
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Data Sections */}
|
||||
<Box display="flex" flexDirection={{ xs: 'column', md: 'row' }} gap={2} mb={3}>
|
||||
<Box flex={2}>
|
||||
<TopSellingProduct data={topSellingProducts} />
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<SalesByLocation data={salesData} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Chart Section */}
|
||||
<Box>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton variant="rectangular" height={50} sx={{ mb: 2 }} />
|
||||
<Skeleton variant="rectangular" height={200} sx={{ mb: 2 }} />
|
||||
<Skeleton variant="rectangular" height={300} sx={{ mb: 2 }} />
|
||||
</>
|
||||
) : (
|
||||
<StatisticsCard
|
||||
title="Analytics Overview"
|
||||
subtitle="Performance Metrics"
|
||||
data={getData()}
|
||||
dataKeys={[
|
||||
{ key: 'visitors', name: 'Visitors', color: '#4CAF50' },
|
||||
{ key: 'conversions', name: 'Conversions', color: '#9C27B0' },
|
||||
]}
|
||||
xDataKey={timeFrame === '24h' ? 'date' : 'label'}
|
||||
timeFrame={timeFrame}
|
||||
onTimeFrameChange={setTimeFrame}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setCustomDate(dayjs().format('YYYY-MM-DD'))}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 117, 34, 0.08)',
|
||||
color: '#ff5722',
|
||||
textTransform: 'none',
|
||||
boxShadow: 'none',
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
'&:hover': { backgroundColor: 'rgba(255, 117, 34, 0.15)' }
|
||||
}}
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* StatisticsCard */}
|
||||
<Box>
|
||||
<StatisticsCard
|
||||
title="Analytics Overview"
|
||||
subtitle="Reservation Statistics"
|
||||
data={chartData}
|
||||
dataKeys={[
|
||||
{ key: 'reservations_total', name: 'Reservations', color: '#4CAF50' },
|
||||
{ key: 'number_of_people_total', name: 'People', color: '#9C27B0' },
|
||||
]}
|
||||
xDataKey="label"
|
||||
valueFormatter={(value) => value}
|
||||
timeFrame={timeFrame}
|
||||
onTimeFrameChange={handleTimeFrameChange}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -22,9 +22,9 @@ const SalesByLocation = ({ data }) => {
|
||||
p: { xs: 1, sm: 2 },
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.1)',
|
||||
height: '95%', // إضافة هذه السطر
|
||||
display: 'flex', // إضافة
|
||||
flexDirection: 'column' // إضافة
|
||||
height: '95%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
@@ -56,7 +56,7 @@ const SalesByLocation = ({ data }) => {
|
||||
</Box>
|
||||
|
||||
<List dense sx={{
|
||||
flexGrow: 1, // إضافة هذه السطر
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Box,
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
Paper,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery
|
||||
useMediaQuery,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import {
|
||||
AreaChart,
|
||||
@@ -18,37 +22,64 @@ import {
|
||||
ResponsiveContainer,
|
||||
CartesianGrid
|
||||
} from 'recharts';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`;
|
||||
if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`;
|
||||
return `$${value}`;
|
||||
};
|
||||
// import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
const StatisticsCard = ({
|
||||
title = "Statistics",
|
||||
subtitle = "Delivery Times",
|
||||
data = [],
|
||||
dataKeys = [
|
||||
{ key: 'revenue', name: 'Revenue', color: '#E46A11' }, // << هنا تغيير اللون
|
||||
{ key: 'sales', name: 'Sales', color: '#0182FC' } // << وهنا أيضاً
|
||||
{ key: 'revenue', name: 'Revenue', color: '#E46A11' },
|
||||
{ key: 'sales', name: 'Sales', color: '#0182FC' }
|
||||
],
|
||||
xDataKey = 'month',
|
||||
valueFormatter = formatCurrency,
|
||||
valueFormatter = (value) => value,
|
||||
timeFrame = 'month',
|
||||
onTimeFrameChange
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
|
||||
const [selectedDate, setSelectedDate] = useState('');
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const handleTimeFrameChange = (newTimeFrame) => {
|
||||
if (onTimeFrameChange) onTimeFrameChange(newTimeFrame);
|
||||
const handleMenuOpen = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleDateChange = (event) => {
|
||||
setSelectedDate(event.target.value);
|
||||
if (onTimeFrameChange) onTimeFrameChange(timeFrame, event.target.value);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleTodayClick = () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
setSelectedDate(today);
|
||||
if (onTimeFrameChange) onTimeFrameChange(timeFrame, today);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
// حساب العلامات الديناميكية للمحور الشاقولي
|
||||
const ticksArray = (() => {
|
||||
if (!data || data.length === 0) return [];
|
||||
const maxValue = Math.max(
|
||||
...data.flatMap(d => dataKeys.map(k => d[k.key] || 0))
|
||||
);
|
||||
const desiredTicks = 8; // عدد العلامات المطلوب
|
||||
const step = Math.ceil(maxValue / desiredTicks) || 1;
|
||||
const arr = [];
|
||||
for (let i = 0; i <= maxValue; i += step) {
|
||||
arr.push(i);
|
||||
}
|
||||
return arr;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Box sx={{ borderRadius: 2, width: { sm: '100%', md: '167vh' }}}>
|
||||
<Box sx={{ borderRadius: 2, width: { sm: '100%', md: '167vh' } }}>
|
||||
<Paper sx={{
|
||||
p: { xs: 1.5, sm: 2 },
|
||||
mb: { xs: 2, sm: 2 },
|
||||
@@ -64,10 +95,34 @@ const StatisticsCard = ({
|
||||
right: { xs: 4, sm: 8 },
|
||||
color: '#667085'
|
||||
}}
|
||||
onClick={handleMenuOpen}
|
||||
>
|
||||
<MoreVertIcon fontSize={isMobile ? 'small' : 'medium'} />
|
||||
{/* <MoreVertIcon fontSize={isMobile ? 'small' : 'medium'} /> */}
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<MenuItem>
|
||||
<TextField
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={handleDateChange}
|
||||
size="small"
|
||||
sx={{ width: 150 }}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<Button variant="outlined" size="small" onClick={handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{/* Header */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
@@ -88,7 +143,7 @@ const StatisticsCard = ({
|
||||
</Box>
|
||||
|
||||
{/* Chart */}
|
||||
<ResponsiveContainer width="100%" height={isMobile ? 250 : isTablet ? 290 : 295}>
|
||||
<ResponsiveContainer width="100%" height={isMobile ? 350 : isTablet ? 390 : 395}>
|
||||
<AreaChart data={data} margin={{
|
||||
top: isMobile ? -30 : -45,
|
||||
right: isMobile ? 15 : 30,
|
||||
@@ -112,18 +167,20 @@ const StatisticsCard = ({
|
||||
tickMargin={isMobile ? 8 : 15}
|
||||
tick={{
|
||||
fontSize: isMobile ? 11 : 12,
|
||||
angle: -25, // تدوير النص لتفادي التزاحم
|
||||
angle: -25,
|
||||
textAnchor: 'end'
|
||||
}}
|
||||
interval={0} // عرض كل القيم على محور X
|
||||
interval={0}
|
||||
/>
|
||||
<YAxis
|
||||
ticks={ticksArray}
|
||||
tickFormatter={valueFormatter}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tickMargin={isMobile ? 8 : 15}
|
||||
tick={{ fontSize: isMobile ? 11 : 12 }}
|
||||
/>
|
||||
|
||||
<Tooltip formatter={valueFormatter} />
|
||||
<Legend
|
||||
verticalAlign="top"
|
||||
@@ -138,9 +195,9 @@ const StatisticsCard = ({
|
||||
name={name}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={color} // لون الخط
|
||||
stroke={color}
|
||||
strokeWidth={isMobile ? 2 : 3}
|
||||
fill={`url(#color-${key})`} // لون التعبئة بتدرج
|
||||
fill={`url(#color-${key})`}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
Button,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Menu,
|
||||
Chip,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import SaveIcon from "@mui/icons-material/Save";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import TuneIcon from "@mui/icons-material/Tune";
|
||||
import authService from "../../../services/authService";
|
||||
|
||||
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
|
||||
const theme = useTheme();
|
||||
const handlePrev = () => currentPage > 1 && onChange(currentPage - 1);
|
||||
const handleNext = () => currentPage < pageCount && onChange(currentPage + 1);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage <= 1}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#FFECE0",
|
||||
// "&:hover": { backgroundColor: "#FFD6B5" },
|
||||
color: theme.palette.primary.main,
|
||||
// "&.Mui-disabled": { color: "#ccc", backgroundColor: "#FFF5E6" },
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosNewIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "8px",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: "#fff",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{currentPage}
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage >= pageCount}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#FFECE0",
|
||||
// "&:hover": { backgroundColor: "#FFD6B5" },
|
||||
color: theme.palette.primary.main,
|
||||
// "&.Mui-disabled": { color: "#ccc", backgroundColor: "#FFF5E6" },
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIosIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const statusOptions = ["available", "unavailable"];
|
||||
|
||||
const TablesManager = ({ restaurantId }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const itemsPerPage = 4;
|
||||
|
||||
const [rows, setRows] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [addingRow, setAddingRow] = useState(false);
|
||||
const [newRow, setNewRow] = useState({
|
||||
restaurant_id: restaurantId,
|
||||
table_number: "",
|
||||
capacity: "",
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [rowToDelete, setRowToDelete] = useState(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// فلتر
|
||||
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
const fetchTables = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await authService.getTablesByRestaurant(restaurantId);
|
||||
if (result.success) setRows(result.data);
|
||||
} catch (err) {
|
||||
console.error("❌ Error fetching tables:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (restaurantId) fetchTables();
|
||||
}, [restaurantId]);
|
||||
|
||||
const handleSaveRow = async () => {
|
||||
if (!newRow.table_number || !newRow.capacity) return;
|
||||
try {
|
||||
const result = await authService.addTable(newRow);
|
||||
if (result.success) {
|
||||
setRows((prev) => [result.data, ...prev]);
|
||||
setAddingRow(false);
|
||||
setNewRow({
|
||||
restaurant_id: restaurantId,
|
||||
table_number: "",
|
||||
capacity: "",
|
||||
status: "available",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error adding table:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRow = (row) => {
|
||||
setRowToDelete(row);
|
||||
setConfirmDeleteOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!rowToDelete) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const result = await authService.deleteTable(rowToDelete.id);
|
||||
if (result.success) setRows((prev) => prev.filter((r) => r.id !== rowToDelete.id));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
setConfirmDeleteOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterClick = (event) => setFilterAnchorEl(event.currentTarget);
|
||||
const handleFilterClose = () => setFilterAnchorEl(null);
|
||||
const handleFilterSelect = (value) => {
|
||||
setStatusFilter(value);
|
||||
setFilterAnchorEl(null);
|
||||
};
|
||||
|
||||
const filteredRows =
|
||||
(statusFilter === "all" ? rows : rows.filter((r) => r.status === statusFilter))
|
||||
.sort((a, b) => parseInt(a.table_number) - parseInt(b.table_number));
|
||||
|
||||
const pageCount = Math.ceil(filteredRows.length / itemsPerPage);
|
||||
const paginatedRows = filteredRows.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
const getStatusChipProps = (status) => {
|
||||
switch (status?.toLowerCase()) {
|
||||
case "available":
|
||||
return {
|
||||
label: "Available",
|
||||
sx: {
|
||||
backgroundColor: "#E7F4EE",
|
||||
color: "#0D894F",
|
||||
fontWeight: 600,
|
||||
fontSize: "13px",
|
||||
minWidth: 90,
|
||||
},
|
||||
};
|
||||
case "unavailable":
|
||||
return {
|
||||
label: "Unavailable",
|
||||
sx: {
|
||||
backgroundColor: "#fde8e8ff",
|
||||
color: "#e41111ff",
|
||||
fontWeight: 600,
|
||||
fontSize: "13px",
|
||||
minWidth: 100,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: status,
|
||||
sx: {
|
||||
backgroundColor: "#FDF1E8",
|
||||
color: "#E46A11",
|
||||
fontWeight: 600,
|
||||
fontSize: "13px",
|
||||
minWidth: 80,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: "100%", sm: "95.5%" },
|
||||
pt: { xs: 2, sm: 2 },
|
||||
pl: { xs: 2, sm: 3 },
|
||||
pr: { xs: 2, sm: 3 },
|
||||
pb: { xs: 2, sm: 2 },
|
||||
// p: { xs: 1.5, sm: 2 },
|
||||
// mb: { xs: 2, sm: 2 },
|
||||
backgroundColor: "white",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
justifyContent: "space-between",
|
||||
alignItems: { xs: "stretch", sm: "center" },
|
||||
mb: 2,
|
||||
gap: { xs: 1, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: "18px", sm: "20px" },
|
||||
textAlign: { xs: "center", sm: "left" },
|
||||
}}
|
||||
>
|
||||
Tables
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
{/* فلتر */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
color: "#667085",
|
||||
borderColor: "#e0e0e0",
|
||||
borderRadius: "8px",
|
||||
height: "40px",
|
||||
width: { xs: "120px", sm: "120px", md: "99px" },
|
||||
fontSize: { xs: "0", sm: "13px", md: "14px" },
|
||||
fontWeight: 600,
|
||||
minWidth: "unset",
|
||||
}}
|
||||
startIcon={<TuneIcon fontSize={isMobile ? "small" : "small"} />}
|
||||
onClick={handleFilterClick}
|
||||
>
|
||||
{isMobile ? "" : "Filters"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={addingRow ? "outlined" : "contained"}
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setAddingRow(!addingRow)}
|
||||
sx={{
|
||||
color: addingRow ? "primary.main" : "white",
|
||||
borderRadius: "8px",
|
||||
fontWeight: 600,
|
||||
fontSize: "14px",
|
||||
height: "40px",
|
||||
width: { xs: "100%", sm: "135px" },
|
||||
textTransform: "none",
|
||||
}}
|
||||
>
|
||||
{addingRow ? "Cancel" : "Add"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Menu الفلاتر */}
|
||||
<Menu anchorEl={filterAnchorEl} open={Boolean(filterAnchorEl)} onClose={handleFilterClose}>
|
||||
{["all", "available", "unavailable"].map((status) => (
|
||||
<MenuItem
|
||||
key={status}
|
||||
selected={statusFilter === status}
|
||||
onClick={() => handleFilterSelect(status)}
|
||||
sx={{
|
||||
color: statusFilter === status ? "#FF914D" : "#4F5867",
|
||||
fontWeight: statusFilter === status ? 700 : 500,
|
||||
}}
|
||||
>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
|
||||
{/* جدول */}
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: 2,
|
||||
minWidth: 320,
|
||||
height: {xs:"310px",md:"349px"},
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
sx={{ minWidth: { sm: 550, md: 650 }, tableLayout: "auto" }}
|
||||
aria-label="tables"
|
||||
size={isMobile ? "small" : "medium"}
|
||||
>
|
||||
<TableHead sx={{ backgroundColor: "#f5f5f5" }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ pl: { md: 7 } }}>Table Number</TableCell>
|
||||
<TableCell>Capacity</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading
|
||||
? Array.from({ length: itemsPerPage }).map((_, idx) => (
|
||||
<TableRow key={`skeleton-${idx}`}>
|
||||
<TableCell ><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: (
|
||||
<>
|
||||
{addingRow && (
|
||||
<TableRow>
|
||||
<TableCell >
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Table Number"
|
||||
value={newRow.table_number}
|
||||
onChange={(e) => setNewRow({ ...newRow, table_number: e.target.value })}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
placeholder="Capacity"
|
||||
value={newRow.capacity}
|
||||
onChange={(e) => setNewRow({ ...newRow, capacity: e.target.value })}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label="Available"
|
||||
sx={{
|
||||
backgroundColor: "#E7F4EE",
|
||||
color: "#0D894F",
|
||||
fontWeight: 600,
|
||||
fontSize: "13px",
|
||||
minWidth: 90,
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton color="success" onClick={handleSaveRow}>
|
||||
<SaveIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
|
||||
{paginatedRows.map((row) => (
|
||||
<TableRow key={row.id} >
|
||||
<TableCell sx={{ pl: { md: 9 } ,color:'#689dffff'}}>{row.table_number}</TableCell>
|
||||
<TableCell>{row.capacity}</TableCell>
|
||||
<TableCell>
|
||||
<Chip {...getStatusChipProps(row.status)} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton color="error" onClick={() => handleDeleteRow(row)}>
|
||||
<DeleteOutlineIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Pagination Footer */}
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
pt={2}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Showing {(currentPage - 1) * itemsPerPage + 1} -{" "}
|
||||
{Math.min(currentPage * itemsPerPage, filteredRows.length)} of {filteredRows.length}
|
||||
</Typography>
|
||||
<SimplePagination currentPage={currentPage} pageCount={pageCount} onChange={setCurrentPage} />
|
||||
</Box>
|
||||
|
||||
{/* Dialog الحذف */}
|
||||
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Are you sure you want to delete table "{rowToDelete?.table_number}"?
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleConfirmDelete}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TablesManager;
|
||||
@@ -1,215 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Chip,
|
||||
Pagination,
|
||||
Button,
|
||||
Avatar,
|
||||
TableContainer
|
||||
} from '@mui/material';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import { green } from '@mui/material/colors';
|
||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||
|
||||
const TopSellingProduct = ({ data = [] }) => {
|
||||
const theme = useTheme();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 5;
|
||||
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const pageCount = Math.ceil(data.length / itemsPerPage);
|
||||
const paginatedData = data.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper sx={{
|
||||
borderRadius: 2,
|
||||
width: '100%',
|
||||
maxWidth: { xs: '100%', md: '115vh' },
|
||||
height: { xs: 'auto', md: '100vh' },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
boxShadow: theme.shadows[1]
|
||||
}}>
|
||||
{/* العنوان وزر الفلاتر */}
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
p={{ xs: 1, sm: 2 }}
|
||||
sx={{ backgroundColor: theme.palette.background.paper }}
|
||||
>
|
||||
<Typography variant="h6" sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
||||
Top Selling Product
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{
|
||||
fontSize: { xs: '12px', sm: '16px' },
|
||||
fontWeight: 500,
|
||||
border: '1px solid #e0e0e0',
|
||||
color: '#667085',
|
||||
textTransform: 'none',
|
||||
p: { xs: '4px 8px', sm: '6px 16px' }
|
||||
}}
|
||||
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
|
||||
>
|
||||
{isMobile ? '' : 'Filters'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* جدول البيانات */}
|
||||
<TableContainer
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflowX: 'auto',
|
||||
maxHeight: { xs: '60vh', md: 'calc(100vh - 160px)' },
|
||||
'&::-webkit-scrollbar': { height: 4 },
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
sx={{
|
||||
minWidth: 650,
|
||||
'& .MuiTableCell-root': {
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
py: { xs: 0.5, sm: 1.5 },
|
||||
px: { xs: 0.5, sm: 2 },
|
||||
},
|
||||
'& thead .MuiTableCell-root': {
|
||||
borderBottom: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '11px', sm: '14px' },
|
||||
px: { xs: 0.5, sm: 2 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableHead sx={{ backgroundColor: '#F0F1F3' }}>
|
||||
<TableRow sx={{ height: { xs: '6vh', sm: '10vh' } }}>
|
||||
<TableCell>Product</TableCell>
|
||||
{!isMobile && <TableCell>Sales</TableCell>}
|
||||
<TableCell>Amount</TableCell>
|
||||
{!isMobile && <TableCell>Price</TableCell>}
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paginatedData.map((row, i) => (
|
||||
<TableRow key={i} sx={{ height: { xs: '8vh', sm: '13vh' } }}>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 2 } }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: green[50],
|
||||
width: { xs: 28, sm: 40 },
|
||||
height: { xs: 28, sm: 40 }
|
||||
}}
|
||||
variant="rounded"
|
||||
>
|
||||
<AssignmentIcon fontSize={isMobile ? 'small' : 'medium'} />
|
||||
</Avatar>
|
||||
<Box sx={{
|
||||
fontSize: { xs: '11px', sm: '14px' },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: { xs: '80px', sm: '200px' }
|
||||
}}>
|
||||
{row.product}
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
{!isMobile && <TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>{row.sales}</TableCell>}
|
||||
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>${row.amount.toLocaleString()}</TableCell>
|
||||
{!isMobile && <TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>${row.price}</TableCell>}
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={isMobile ? row.status.substring(0, 3) : row.status}
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
sx={{
|
||||
fontSize: { xs: '10px', sm: '14px' },
|
||||
fontWeight: 600,
|
||||
backgroundColor:
|
||||
row.status === 'Published' ? '#E7F4EE' :
|
||||
row.status === 'Low Stock' ? '#FDF1E8' :
|
||||
row.status === 'Out of Stock' ? '#FFCDD2' : '#E0E0E0',
|
||||
color:
|
||||
row.status === 'Published' ? '#0D894F' :
|
||||
row.status === 'Low Stock' ? '#E46A11' :
|
||||
row.status === 'Out of Stock' ? '#C62828' : '#424242',
|
||||
minWidth: { xs: '50px', sm: '100px' }
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* التذييل مع الترقيم */}
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
p={{ xs: 1, sm: 2 }}
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontSize: { xs: '11px', sm: '14px' },
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, data.length)} of {data.length}
|
||||
</Typography>
|
||||
|
||||
<Pagination
|
||||
count={pageCount}
|
||||
page={currentPage}
|
||||
onChange={(event, value) => setCurrentPage(value)}
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
sx={{
|
||||
'& .MuiPaginationItem-root': {
|
||||
fontSize: { xs: '12px', sm: '14px' },
|
||||
minWidth: { xs: 24, sm: 32 },
|
||||
height: { xs: 24, sm: 32 },
|
||||
backgroundColor: '#FFECE0',
|
||||
color: theme.palette.primary.main,
|
||||
borderRadius: '8px',
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: '#FFB088',
|
||||
color: '#fff',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFD6B5',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopSellingProduct;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
// src/components/AppBar/KitchPlusAppBar.jsx
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
@@ -7,31 +8,37 @@ import {
|
||||
IconButton,
|
||||
Divider,
|
||||
Avatar,
|
||||
Button,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
const top100Films = [
|
||||
{ title: 'The Shawshank Redemption' },
|
||||
{ title: 'The Godfather' },
|
||||
{ title: 'The Dark Knight' },
|
||||
{ title: 'Pulp Fiction' },
|
||||
];
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import { useRestaurant } from '../../contexts/RestaurantContext';
|
||||
import authService from '../../services/authService';
|
||||
import { UserContext } from '../../contexts/UserContext'; // ✅ استدعاء UserContext
|
||||
|
||||
const KitchPlusAppBar = ({ onDrawerToggle, sidebarOpen, isMobile }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isMediumScreen = useMediaQuery(theme.breakpoints.between('sm', 'md'));
|
||||
const { restaurantId } = useRestaurant();
|
||||
|
||||
const { user } = useContext(UserContext); // ✅ استخدم الـ Context بدلاً من localStorage
|
||||
const [restaurantLogo, setRestaurantLogo] = useState('/images/default-restaurant.png');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRestaurantLogo = async () => {
|
||||
if (!restaurantId) return;
|
||||
const res = await authService.getRestaurantById(restaurantId);
|
||||
if (res.success && res.data) {
|
||||
setRestaurantLogo(res.data.image_url);
|
||||
}
|
||||
};
|
||||
fetchRestaurantLogo();
|
||||
}, [restaurantId]);
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
@@ -54,152 +61,62 @@ const KitchPlusAppBar = ({ onDrawerToggle, sidebarOpen, isMobile }) => {
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Left side with toggle button */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{/* Toggle button for mobile/tablet */}
|
||||
{/* Left side */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
mr: 2,
|
||||
}}
|
||||
>
|
||||
{(isMobile || isMediumScreen) && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={onDrawerToggle}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
<IconButton color="inherit" edge="start" onClick={onDrawerToggle}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{location.pathname === '/dashboard' ? (
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="div"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: { xs: '18px', sm: '20px', md: '24px' },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: { xs: '200px', sm: 'none' }
|
||||
}}
|
||||
>
|
||||
Welcome to KitchPlus
|
||||
</Typography>
|
||||
) : (
|
||||
<Autocomplete
|
||||
sx={{
|
||||
width: { xs: '200px', sm: '200px', md: '250px' },
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
freeSolo
|
||||
id="free-solo-2-demo"
|
||||
disableClearable
|
||||
options={top100Films.map((option) => option.title)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder="Search"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
type: 'search',
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchIcon sx={{ color: '#667085' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
backgroundColor: 'white',
|
||||
height: { xs: 36, sm: 38, md: 40 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Typography variant="h6" sx={{ fontWeight: '500' }}>
|
||||
Welcome to KitchPlus
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Right side */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.8, sm: 1, md: 1.5 } }}>
|
||||
{location.pathname === '/dashboard' && !isSmallScreen && (
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
display: { xs: 'none', sm: 'none', md: 'flex' }, // هذا السطر يخفي الزر عند xs و sm ويظهره من md وما فوق
|
||||
color: 'white',
|
||||
height: { xs: 32, sm: 36, md: 40 },
|
||||
mr: { xs: '6px', sm: '8px', md: '0px' },
|
||||
backgroundColor: '#61677F',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '12px', sm: '13px', md: '14px' },
|
||||
textTransform: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{isMediumScreen ? 'Switch Company' : 'Switch Company Profits'}
|
||||
</Button>
|
||||
)}
|
||||
{location.pathname === '/dashboard' && (
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
flexItem
|
||||
sx={{
|
||||
height: { xs: 30, sm: 36, md: 40 },
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton color="#667085" size={isSmallScreen ? 'small' : 'medium'}>
|
||||
<NotificationsOutlinedIcon fontSize={isSmallScreen ? 'small' : 'medium'} />
|
||||
{location.pathname === '/dashboard' && <Divider orientation="vertical" flexItem sx={{ height: 40 }} />}
|
||||
|
||||
<IconButton
|
||||
color="#667085"
|
||||
size={isSmallScreen ? 'small' : 'medium'}
|
||||
onClick={() => navigate('/restaurant')}
|
||||
>
|
||||
<HomeIcon fontSize={isSmallScreen ? 'medium' : 'medium'} />
|
||||
</IconButton>
|
||||
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
flexItem
|
||||
sx={{
|
||||
height: { xs: 30, sm: 36, md: 40 },
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem sx={{ height: 40 }} />
|
||||
|
||||
<Avatar
|
||||
alt="Admin"
|
||||
src="/images/waitress3.png"
|
||||
sx={{
|
||||
width: { xs: 28, sm: 32, md: 40 },
|
||||
height: { xs: 28, sm: 32, md: 40 }
|
||||
}}
|
||||
alt="Restaurant Logo"
|
||||
src={restaurantLogo}
|
||||
sx={{ width: { xs: 28, sm: 32, md: 40 }, height: { xs: 28, sm: 32, md: 40 } }}
|
||||
/>
|
||||
|
||||
{!isSmallScreen && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
ml: { sm: 0.5, md: 1 },
|
||||
ml: 1,
|
||||
color: '#61677F',
|
||||
fontSize: { xs: '12px', sm: '13px', md: '14px' },
|
||||
fontSize: { xs: 12, sm: 13, md: 14 },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: { xs: '100px', sm: '120px', md: 'none' }
|
||||
overflow: 'visible',
|
||||
textOverflow: 'clip',
|
||||
maxWidth: 200,
|
||||
}}
|
||||
>
|
||||
Admin@gmail.com
|
||||
{user?.email || 'Admin@gmail.com'}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
color="inherit"
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
color: '#A6ACB8',
|
||||
padding: { xs: '4px', sm: '8px' }
|
||||
}}
|
||||
size={isSmallScreen ? 'small' : 'medium'}
|
||||
>
|
||||
<KeyboardArrowDownIcon fontSize={isSmallScreen ? 'small' : 'medium'} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery } from '@mui/material';
|
||||
import KitchPlusAppBar from '../AppBar';
|
||||
import Sidebar from '../SideHome';
|
||||
import Orders from './contect/Orders';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
const drawerWidth = 230;
|
||||
|
||||
const Cart = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setSidebarOpen(window.innerWidth >= theme.breakpoints.values.md);
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [theme.breakpoints.values.md]);
|
||||
|
||||
useEffect(() => {
|
||||
const admin = authService.getAdminData();
|
||||
// console.log('Admin Info:', admin);
|
||||
|
||||
const adminId = authService.getAdminId();
|
||||
// console.log('Admin ID:', adminId);
|
||||
}, []);
|
||||
|
||||
const admin = authService.getAdminData();
|
||||
const adminId = authService.getAdminId();
|
||||
const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100vh', backgroundColor: '#F6F6F6', overflow: 'hidden' }}>
|
||||
<Sidebar open={sidebarOpen} onClose={handleDrawerToggle} isMobile={isMobile} drawerWidth={drawerWidth} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
transition: theme.transitions.create(['width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<KitchPlusAppBar onDrawerToggle={handleDrawerToggle} sidebarOpen={sidebarOpen} isMobile={isMobile} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 6,
|
||||
width: { xs: '90%', sm: '95%', md: '96%' },
|
||||
pt: { xs: 2, sm: 3 },
|
||||
pl: { xs: 2, sm: 3 },
|
||||
pb: { xs: 2, sm: 4 },
|
||||
pr: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<Orders adminId={adminId} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cart;
|
||||
@@ -0,0 +1,223 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Button,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import authService from '../../../../services/authService';
|
||||
import { useSnackbar } from "../../../../contexts/SnackbarContext";
|
||||
|
||||
const CartDetails = ({ cart, onClose, onUpdated, onDeleted }) => {
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [totalPrice, setTotalPrice] = useState(cart?.attributes?.total_price || "");
|
||||
const [cartItems, setCartItems] = useState(cart.relationships?.cartItems || cart.relationships?.cart_items || []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const { showSnackbar } = useSnackbar();
|
||||
if (!cart) return null;
|
||||
|
||||
const handleSaveAll = async () => {
|
||||
setLoading(true);
|
||||
const payload = {
|
||||
data: {
|
||||
type: "cart",
|
||||
attributes: {
|
||||
totalPrice: Number(totalPrice),
|
||||
},
|
||||
relationships: {
|
||||
cartItems: cartItems.map(item => ({
|
||||
attributes: {
|
||||
quantity: Number(item.attributes.quantity),
|
||||
},
|
||||
relationships: {
|
||||
product: {
|
||||
data: {
|
||||
id: item.relationships?.supplier_product?.id ||
|
||||
item.relationships?.product?.data?.id
|
||||
}
|
||||
}
|
||||
}
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await authService.updateCart(cart.id, payload);
|
||||
if (result.success) {
|
||||
onUpdated(result.data);
|
||||
setEditMode(false);
|
||||
} else {
|
||||
// alert(result.message || 'Failed to update cart');
|
||||
showSnackbar(result.message || "Failed to update cart", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
// alert('An error occurred while updating the cart');
|
||||
showSnackbar("An error occurred while updating the cart", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCart = async () => {
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const result = await authService.deleteCart(cart.id);
|
||||
if (result.success) {
|
||||
onDeleted(cart.id);
|
||||
onClose();
|
||||
} else {
|
||||
// alert(result.message || 'Failed to delete cart');
|
||||
showSnackbar(result.message || "Failed to update cart", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
// alert('An error occurred while deleting the cart');
|
||||
showSnackbar("An error occurred while deleting the cart", "error");
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
setOpenDeleteDialog(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeQuantity = (itemId, newQuantity) => {
|
||||
setCartItems(prev =>
|
||||
prev.map(item =>
|
||||
item.id === itemId
|
||||
? { ...item, attributes: { ...item.attributes, quantity: newQuantity } }
|
||||
: item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper sx={{ p: 3, mt: 2, borderRadius: 2, border: '1px solid #e0e0e0' }}>
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
Cart #{cart.id} Details
|
||||
</Typography>
|
||||
|
||||
{editMode ? (
|
||||
<>
|
||||
<TextField
|
||||
label="Total Price"
|
||||
type="number"
|
||||
value={totalPrice}
|
||||
onChange={(e) => setTotalPrice(e.target.value)}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography sx={{ mt: 2, fontWeight: 500 }}>Items:</Typography>
|
||||
<List>
|
||||
{cartItems.map(item => (
|
||||
<ListItem key={item.id} sx={{ pl: 0 }}>
|
||||
<ListItemText
|
||||
primary={`Item ID: ${item.id} | Product: ${item.relationships?.supplier_product?.id ||
|
||||
item.relationships?.product?.data?.id
|
||||
}`}
|
||||
secondary={
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
label="Quantity"
|
||||
value={item.attributes.quantity}
|
||||
onChange={(e) => handleChangeQuantity(item.id, Number(e.target.value))}
|
||||
sx={{ width: '120px' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
|
||||
<Button variant="contained" onClick={handleSaveAll} disabled={loading}>
|
||||
{loading ? "Saving..." : "Save All"}
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={() => setEditMode(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography>Total Price: {cart.attributes.total_price || cart.attributes.totalPrice}</Typography>
|
||||
<Typography>
|
||||
Created At: {new Date(cart.attributes.createdAt).toLocaleString()}
|
||||
</Typography>
|
||||
<Typography sx={{ mt: 2, fontWeight: 500 }}>Items:</Typography>
|
||||
<List>
|
||||
{cartItems.map(item => (
|
||||
<ListItem key={item.id} sx={{ pl: 0 }}>
|
||||
<ListItemText
|
||||
primary={`Item ID: ${item.id}`}
|
||||
secondary={`Quantity: ${item.attributes.quantity} | Product: ${item.relationships?.supplier_product?.id ||
|
||||
item.relationships?.product?.data?.id
|
||||
}`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
|
||||
<Button variant="contained" onClick={() => setEditMode(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setOpenDeleteDialog(true)}
|
||||
sx={{ ml: 'auto' }}
|
||||
>
|
||||
Delete Cart
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
Back to Orders
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={openDeleteDialog}
|
||||
onClose={() => setOpenDeleteDialog(false)}
|
||||
>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete this cart? This action cannot be undone.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenDeleteDialog(false)} disabled={deleteLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDeleteCart}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
{deleteLoading ? <CircularProgress size={24} /> : 'Delete'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartDetails;
|
||||
@@ -0,0 +1,170 @@
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { Box, Typography, Button, LinearProgress, IconButton } from "@mui/material";
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { CartContext } from "../../../../contexts/CartContextR";
|
||||
import { useSnackbar } from "../../../../contexts/SnackbarContext";
|
||||
|
||||
const CartView = ({ onClose, onCartCreated, adminId }) => {
|
||||
const { cart, clearCart, createNewCart } = useContext(CartContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Admin ID from props in:CartView', adminId);
|
||||
}, [adminId]);
|
||||
|
||||
const totalPrice = cart.reduce((sum, item) => sum + (item.totalPrice || 0), 0);
|
||||
|
||||
const handleSendCart = async () => {
|
||||
if (!cart.length) return;
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// تحقق من أن adminId موجود ضمن قائمة صالحة (يمكنك تعديلها حسب بياناتك)
|
||||
const validAdminIds = [1, 2]; // IDs موجودة في DB
|
||||
if (!validAdminIds.includes(adminId)) {
|
||||
console.error("Invalid admin ID");
|
||||
showSnackbar("Selected admin is not valid.", "error");
|
||||
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// تحقق من أن جميع المنتجات موجودة في قاعدة البيانات
|
||||
const validProductIds = [1, 2, 3, 4]; // IDs المنتجات الموجودة
|
||||
for (let item of cart) {
|
||||
if (!validProductIds.includes(item.id)) {
|
||||
console.error(`Invalid product ID: ${item.id}`);
|
||||
// alert(`Product with ID ${item.id} does not exist.`);
|
||||
showSnackbar(`Product with ID ${item.id} does not exist.`, "error");
|
||||
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// تجهيز البيانات حسب شكل الـ backend
|
||||
const cartData = {
|
||||
data: {
|
||||
type: "cart",
|
||||
attributes: {
|
||||
totalPrice: cart.reduce((sum, item) => sum + (item.totalPrice || 0), 0),
|
||||
},
|
||||
relationships: {
|
||||
admin: {
|
||||
data: { id: adminId },
|
||||
},
|
||||
cartItems: cart.map(item => ({
|
||||
attributes: { quantity: item.quantity },
|
||||
relationships: { product: { data: { id: item.id } } },
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const newCart = await createNewCart(cartData);
|
||||
|
||||
if (newCart && newCart.success) {
|
||||
onCartCreated(newCart.data);
|
||||
clearCart();
|
||||
onClose();
|
||||
showSnackbar("Cart sent successfully!", "success");
|
||||
} else {
|
||||
console.error("Failed to create cart:", newCart.message);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error("Error sending cart:", error);
|
||||
showSnackbar("Failed to create cart.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (loading) return <LinearProgress />;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: "100%", sm: "93.5%" },
|
||||
p: { xs: 2, sm: 3 },
|
||||
backgroundColor: "white",
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{/* السهم للعودة */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<IconButton onClick={onClose} size="small" sx={{ mr: 1 }}>
|
||||
<ArrowBackIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600, fontSize: { xs: "18px", sm: "20px" } }}>
|
||||
Current Cart
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{cart.length === 0 ? (
|
||||
<Typography sx={{ mt: 2, textAlign: "center" }}>No items in cart.</Typography>
|
||||
) : (
|
||||
<>
|
||||
{cart.map(item => (
|
||||
<Box
|
||||
key={item.id}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
border: "1px solid #e0e0e0",
|
||||
backgroundColor: "#fafafa",
|
||||
flexWrap: "wrap",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography fontWeight={600}>{item.name}</Typography>
|
||||
<Typography variant="body2">Unit: {item.unit}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ textAlign: { xs: "left", sm: "right" } }}>
|
||||
<Typography variant="body2">Quantity: {item.quantity}</Typography>
|
||||
<Typography variant="body2">Total: ${item.totalPrice}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Total Price: ${totalPrice.toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{cart.length > 0 && (
|
||||
<Box sx={{ display: "flex", gap: 1, mt: 2, flexWrap: { xs: "wrap", sm: "nowrap" }, justifyContent: { xs: "center", sm: "flex-start" } }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSendCart}
|
||||
sx={{ color: 'white', borderRadius: '8px', fontWeight: 600, fontSize: '14px', height: '40px', width: { xs: '100%', sm: '200px' }, textTransform: 'none', minWidth: { xs: 'unset', sm: '200px' } }}
|
||||
>
|
||||
Send Cart to Server
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={clearCart}
|
||||
sx={{ borderRadius: '8px', fontWeight: 600, fontSize: '14px', height: '40px', width: { xs: '100%', sm: '150px' }, textTransform: 'none', minWidth: { xs: 'unset', sm: '150px' } }}
|
||||
>
|
||||
Clear Cart
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartView;
|
||||
@@ -0,0 +1,389 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
CircularProgress,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
import authService from '../../../../services/authService';
|
||||
import { useRestaurant } from "../../../../contexts/RestaurantContext";
|
||||
import CartDetails from './CartDetails';
|
||||
import CartView from './CartView';
|
||||
|
||||
|
||||
|
||||
|
||||
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
|
||||
const theme = useTheme();
|
||||
const handlePrev = () => { if (currentPage > 1) onChange(currentPage - 1); };
|
||||
const handleNext = () => { if (currentPage < pageCount) onChange(currentPage + 1); };
|
||||
const { restaurantId } = useRestaurant();
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage <= 1}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#FFECE0',
|
||||
'&:hover': { backgroundColor: '#FFD6B5' },
|
||||
color: theme.palette.primary.main,
|
||||
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosNewIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
userSelect: 'none',
|
||||
boxShadow: `0 0 0 1px ${theme.palette.primary.main}`,
|
||||
}}
|
||||
>
|
||||
{currentPage}
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage >= pageCount}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#FFECE0',
|
||||
'&:hover': { backgroundColor: '#FFD6B5' },
|
||||
color: theme.palette.primary.main,
|
||||
'&.Mui-disabled': { color: '#ccc', backgroundColor: '#FFF5E6' },
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIosIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Orders = ({ adminId }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const { restaurantId } = useRestaurant();
|
||||
const [ordersData, setOrdersData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedCart, setSelectedCart] = useState(null);
|
||||
const [cartLoading, setCartLoading] = useState(false);
|
||||
const [showCartView, setShowCartView] = useState(false);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 6;
|
||||
|
||||
// ────────────── Fetch Orders ──────────────
|
||||
useEffect(() => {
|
||||
const fetchOrders = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await authService.getCart();
|
||||
if (result.success) {
|
||||
console.log(Array.isArray(result.data) ? result.data : []);
|
||||
setOrdersData(Array.isArray(result.data) ? result.data : []);
|
||||
} else {
|
||||
console.error(result.message);
|
||||
setOrdersData([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch orders:', error);
|
||||
setOrdersData([]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
fetchOrders();
|
||||
}, []);
|
||||
|
||||
// ────────────── Row Click ──────────────
|
||||
const handleRowClick = async (cartId) => {
|
||||
setCartLoading(true);
|
||||
try {
|
||||
const result = await authService.getCartById(cartId);
|
||||
if (result.success) setSelectedCart(result.data);
|
||||
else console.error(result.message);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch cart details:', error);
|
||||
}
|
||||
setCartLoading(false);
|
||||
};
|
||||
|
||||
const handleCartUpdated = (updatedCart) => {
|
||||
setOrdersData(prev =>
|
||||
prev.map(cart => cart.id === updatedCart.id ? updatedCart : cart)
|
||||
);
|
||||
setSelectedCart(null);
|
||||
};
|
||||
|
||||
const handleCloseCartDetails = () => { setSelectedCart(null); };
|
||||
|
||||
const handleAddOrder = (newOrder) => {
|
||||
setOrdersData(prev => [newOrder, ...prev]);
|
||||
};
|
||||
|
||||
const pageCount = Math.ceil(ordersData.length / itemsPerPage);
|
||||
const paginatedOrders = ordersData.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
if (cartLoading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedCart) {
|
||||
return (
|
||||
<CartDetails
|
||||
cart={selectedCart}
|
||||
onClose={handleCloseCartDetails}
|
||||
onUpdated={handleCartUpdated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showCartView) {
|
||||
return (
|
||||
<CartView
|
||||
onClose={() => setShowCartView(false)}
|
||||
onSend={handleAddOrder}
|
||||
onCartCreated={(newCart) => {
|
||||
setOrdersData(prev => [newCart, ...prev]);
|
||||
setShowCartView(false);
|
||||
}}
|
||||
adminId={adminId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ────────────── Main Orders Table ──────────────
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '93.5%' },
|
||||
p: { xs: 2, sm: 3 },
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'stretch', sm: 'center' },
|
||||
mb: 2,
|
||||
gap: { xs: 1, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '18px', sm: '20px' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
textAlign: { xs: 'center', sm: 'left' },
|
||||
}}
|
||||
>
|
||||
Cart
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowCartView(true)}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
width: { xs: '100%', sm: '135px' },
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: 'unset', sm: '135px' },
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
Current Cart
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
boxShadow: 'none',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 2,
|
||||
minWidth: 320,
|
||||
height: '380px',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
sx={{ minWidth: { sm: 550, md: 650 }, tableLayout: 'auto' }}
|
||||
aria-label="orders table"
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
>
|
||||
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
|
||||
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Cart ID</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Total Price</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Created At</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Items Count</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F' }}>Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading
|
||||
? Array.from({ length: itemsPerPage }).map((_, idx) => (
|
||||
<TableRow key={`skeleton-${idx}`}>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: (
|
||||
<>
|
||||
{paginatedOrders.map(order => {
|
||||
const attributes = order?.attributes || {};
|
||||
const relationships = order?.relationships || {};
|
||||
const cartItems = relationships?.cartItems || relationships?.cart_items || [];
|
||||
return (
|
||||
<TableRow
|
||||
key={order?.id}
|
||||
hover
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.3s',
|
||||
'&:hover': {
|
||||
backgroundColor: '#ffe4d0ff !important',
|
||||
'& td': { color: '#000000ff' },
|
||||
},
|
||||
}}
|
||||
onClick={() => handleRowClick(order.id)}
|
||||
>
|
||||
<TableCell>{order?.id || '--'}</TableCell>
|
||||
<TableCell>{attributes?.totalPrice ?? attributes?.total_price ?? '0.00'}</TableCell>
|
||||
<TableCell>{attributes?.createdAt ? new Date(attributes.createdAt).toLocaleString() : '--'}</TableCell>
|
||||
<TableCell>{cartItems.length}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={ async (e) => {
|
||||
e.stopPropagation();
|
||||
const cart = order;
|
||||
if (!cart) return;
|
||||
|
||||
const orderData = {
|
||||
data: {
|
||||
type: "order",
|
||||
attributes: {
|
||||
totalPrice: parseFloat(cart.attributes.total_price) || 0,
|
||||
},
|
||||
relationships: {
|
||||
restaurant: {
|
||||
data: {
|
||||
id: restaurantId,
|
||||
},
|
||||
},
|
||||
orderItems: (cart.relationships.cart_items || []).map((item) => ({
|
||||
attributes: {
|
||||
quantity: item.attributes.quantity,
|
||||
},
|
||||
relationships: {
|
||||
product: {
|
||||
data: {
|
||||
id: item.relationships.supplier_product.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
authService.confirmOrder(orderData);
|
||||
|
||||
const result =await authService.confirmOrder(orderData);
|
||||
if (result.success) {
|
||||
setOrdersData(prev => prev.filter(c => c.id !== cart.id));
|
||||
} else {
|
||||
console.error(result.message);
|
||||
}
|
||||
}}>
|
||||
Confirm
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
{paginatedOrders.length < itemsPerPage &&
|
||||
Array.from({ length: itemsPerPage - paginatedOrders.length }).map((_, idx) => (
|
||||
<TableRow key={`empty-${idx}`} sx={{ height: 53 }}>
|
||||
<TableCell colSpan={4} sx={{ borderBottom: 'none' }} />
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
pt={{ xs: 1, sm: 2 }}
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: { xs: '11px', sm: '14px' }, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Showing {(currentPage - 1) * itemsPerPage + 1} - {Math.min(currentPage * itemsPerPage, ordersData.length)} of {ordersData.length}
|
||||
</Typography>
|
||||
|
||||
<SimplePagination currentPage={currentPage} pageCount={pageCount} onChange={setCurrentPage} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Orders;
|
||||
@@ -8,27 +8,25 @@ const drawerWidth = 230;
|
||||
const Cashier = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [hasProducts, setHasProducts] = useState(false); // حالة لتتبع وجود المنتجات
|
||||
const [hasProducts, setHasProducts] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
|
||||
// محاكاة للتحقق من وجود المنتجات (استبدل هذا بمنطقك الفعلي)
|
||||
|
||||
useEffect(() => {
|
||||
// هنا يجب استبدال هذا بمنطق فعلي للتحقق من وجود المنتجات
|
||||
// مثلاً استدعاء API أو التحقق من state
|
||||
|
||||
const checkProducts = async () => {
|
||||
// محاكاة لاستدعاء API
|
||||
const productsExist = await checkIfProductsExist(); // استبدل هذه الدالة بمنطقك الفعلي
|
||||
|
||||
const productsExist = await checkIfProductsExist();
|
||||
setHasProducts(productsExist);
|
||||
};
|
||||
|
||||
checkProducts();
|
||||
}, []);
|
||||
|
||||
// دالة مساعدة لمحاكاة التحقق من المنتجات (استبدلها بمنطقك الفعلي)
|
||||
|
||||
const checkIfProductsExist = async () => {
|
||||
// محاكاة - يمكن أن يكون هذا استدعاء لـ API أو تحقق من state
|
||||
// return true;
|
||||
return false; // غير هذه القيمة حسب منطقك
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,7 +60,7 @@ const Cashier = () => {
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
backgroundColor:'#F6F6F6',
|
||||
backgroundColor: '#F6F6F6',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<Sidebar
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Box,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
const KitchPlusAppBar = ({ onDrawerToggle, sidebarOpen, isMobile }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
const result = await authService.logout();
|
||||
|
||||
if (result.success) {
|
||||
localStorage.removeItem('token');
|
||||
navigate('/login');
|
||||
} else {
|
||||
console.error('Logout failed:', result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
sx={{
|
||||
height: { xs: 56, sm: 64, md: 66 },
|
||||
backgroundColor: '#ffffff',
|
||||
color: 'black',
|
||||
boxShadow: 'none',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: theme.zIndex.appBar,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
sx={{
|
||||
px: { xs: 2, sm: 3, md: '24px' },
|
||||
minHeight: { xs: '56px !important', sm: '64px !important' },
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Left: Logo */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/image.png"
|
||||
alt="logo"
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
objectFit: 'contain',
|
||||
mr: 1.5,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: '1.25rem',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
KITCH
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: '1.25rem',
|
||||
color: 'primary.main',
|
||||
ml: 0.5,
|
||||
}}
|
||||
>
|
||||
PLUS
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Right: Log Out Button */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.8, sm: 1, md: 1.5 } }}>
|
||||
<Box
|
||||
onClick={handleLogout}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
px: 2,
|
||||
py: 1,
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s',
|
||||
backgroundColor: 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor: '#fffcf9d5',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
color: 'divider',
|
||||
minWidth: 36,
|
||||
}}
|
||||
>
|
||||
<LogoutIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Log Out"
|
||||
primaryTypographyProps={{
|
||||
sx: {
|
||||
color: 'divider',
|
||||
fontSize: '1rem',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default KitchPlusAppBar;
|
||||
@@ -0,0 +1,228 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery, Snackbar, Alert } from '@mui/material';
|
||||
import AppBar from './AppBar';
|
||||
|
||||
import CloudKitchenProject from '../CreateYourRestaurant/contcet/CloudKitchenProject';
|
||||
import OperationalDetails from '../CreateYourRestaurant/contcet/OperationalDetails';
|
||||
import RequiredEquipments from '../CreateYourRestaurant/contcet/RequiredEquipments';
|
||||
import VisualIdentity from '../CreateYourRestaurant/contcet/VisualIdentity';
|
||||
import Budget from '../CreateYourRestaurant/contcet/Budget';
|
||||
import AdditionalNotes from '../CreateYourRestaurant/contcet/AdditionalNotes';
|
||||
import SideProfile from './SideProfile';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
const { createNewRestaurant } = authService;
|
||||
const drawerWidth = 230;
|
||||
|
||||
const CreateRestaurant = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [formData, setFormData] = useState({});
|
||||
const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' });
|
||||
|
||||
const updateFormData = (newData) => {
|
||||
setFormData(prev => {
|
||||
const updated = { ...prev, ...newData };
|
||||
console.log('Updated formData:', updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = (event, reason) => {
|
||||
if (reason === 'clickaway') return;
|
||||
setSnackbar(prev => ({ ...prev, open: false }));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const result = await createNewRestaurant(formData);
|
||||
if (result.success) {
|
||||
setSnackbar({ open: true, message: 'Restaurant created successfully!', severity: 'success' });
|
||||
} else {
|
||||
setSnackbar({ open: true, message: `Error: ${result.message}`, severity: 'error' });
|
||||
}
|
||||
console.log("Form Data being submitted:", formData);
|
||||
};
|
||||
|
||||
const steps = [
|
||||
<CloudKitchenProject
|
||||
key="step-0"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<OperationalDetails
|
||||
key="step-1"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<RequiredEquipments
|
||||
key="step-2"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<VisualIdentity
|
||||
key="step-3"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<Budget
|
||||
key="step-4"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<AdditionalNotes
|
||||
key="step-5"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= theme.breakpoints.values.md) {
|
||||
setSidebarOpen(true);
|
||||
} else {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [theme.breakpoints.values.md]);
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
backgroundColor: '#F6F6F6',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: { xs: '100%', sm: '100%', md: '100%' },
|
||||
marginLeft: { xs: 0, sm: sidebarOpen ? `${drawerWidth}px` : 0, md: 0 },
|
||||
transition: theme.transitions.create(['width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<AppBar
|
||||
onDrawerToggle={handleDrawerToggle}
|
||||
sidebarOpen={sidebarOpen}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
{/* Sidebar profile for desktop */}
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
ml: 3,
|
||||
mb: 2,
|
||||
width: { md: '30%' },
|
||||
display: { xs: 'none', sm: 'none', md: 'block' },
|
||||
overflowY: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100%',
|
||||
pb: 15,
|
||||
pt: 3,
|
||||
}}
|
||||
>
|
||||
<SideProfile
|
||||
currentStepIndex={currentStep}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Step content */}
|
||||
<Box
|
||||
sx={{
|
||||
ml: { xs: 2, md: 3 },
|
||||
flexGrow: 1,
|
||||
height: '100vh',
|
||||
pr: { sm: 2, md: 1 },
|
||||
pt: 3,
|
||||
mb: { sm: 20 },
|
||||
width: { md: '60%' },
|
||||
display: { xs: 'block', sm: 'block', md: 'block' },
|
||||
overflowY: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ minHeight: '100%', pb: 18 }}>
|
||||
{steps[currentStep]}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
<Snackbar
|
||||
open={snackbar.open}
|
||||
autoHideDuration={4000}
|
||||
onClose={handleCloseSnackbar}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert onClose={handleCloseSnackbar} severity={snackbar.severity}
|
||||
sx={{
|
||||
width: { xs: '40%', sm: '60%', md: '100%' },
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
backgroundColor: '#e57f3f',
|
||||
borderRadius: 6, mb: 6
|
||||
}}
|
||||
>
|
||||
{snackbar.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateRestaurant;
|
||||
@@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||
|
||||
const steps = [
|
||||
{ title: 'Cloud Kitchen Project', icon: '/images/createProfile/BasicInf.png' },
|
||||
{ title: 'OperationalDetails', icon: '/images/icons/rocket.png' },
|
||||
{ title: 'Required Equipments', icon: '/images/createProfile/equipment.png'},
|
||||
{ title: 'Visual Identity', icon: '/images/createProfile/Vector.png' },
|
||||
{ title: 'Budget & Expansion', icon: '/images/createProfile/Expansion.png' },
|
||||
{ title: 'Additional Notes & Concerns', icon: '/images/createProfile/Group.png' },
|
||||
{ title: 'Submit & Confirmation', icon: '/images/createProfile/Confirmation.png' },
|
||||
];
|
||||
|
||||
const SideProfile = ({ currentStepIndex = 0, onBack }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100%',
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 3,
|
||||
py: 4,
|
||||
display: { xs: 'none', md: 'block' },
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* العنوان الرئيسي */}
|
||||
<Typography fontSize="18px" fontWeight={600} mb={1.2}>
|
||||
Create Your Restaurant
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" mb={3}>
|
||||
Complete this process to register your restaurant on our amazing food platform.
|
||||
</Typography>
|
||||
|
||||
{/* الخطوات */}
|
||||
<Stack spacing={3} sx={{ ml: 1, position: 'relative', pb: 12 }}>
|
||||
{steps.map((step, index) => (
|
||||
<Box key={index} position="relative">
|
||||
{/* الخط الرأسي بين الدوائر */}
|
||||
{index !== steps.length - 1 && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '28px',
|
||||
left: '23px',
|
||||
width: '2px',
|
||||
height: 'calc(100% - 5px)',
|
||||
backgroundColor: '#E0E0E0',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* محتوى كل خطوة */}
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
{/* الدائرة بالأيقونة */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: 45,
|
||||
height: 45,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{/* الدائرة الخلفية - تظهر دائمًا */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
backgroundImage:
|
||||
index <= currentStepIndex
|
||||
? 'linear-gradient(0deg, #FF914D, #F3F3F3)'
|
||||
: 'linear-gradient(0deg, #DFDFDF, #F0F0F0)',
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: '50%',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#FFF',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={step.icon}
|
||||
alt={step.title}
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginLeft: step.title === 'Required Equipments' ? 7 : 0,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* العنوان */}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
color: '#000',
|
||||
}}
|
||||
>
|
||||
{step.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* زر الرجوع */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<ArrowBackIosIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={onBack}
|
||||
disabled={currentStepIndex === 0}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '20px',
|
||||
color: currentStepIndex === 0 ? '#C2C2C2' : '#6B7283',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideProfile;
|
||||
@@ -1,36 +1,119 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery, Typography } from '@mui/material';
|
||||
import { Box, useTheme, useMediaQuery, Snackbar, Alert } from '@mui/material';
|
||||
import KitchPlusAppBar from '../AppBar';
|
||||
import Sidebar from '../SideHome';
|
||||
import CloudKitchenProject from './contcet/CloudKitchenProject';
|
||||
import OperationalDetails from './contcet/OperationalDetails';
|
||||
import RequiredEquipments from './contcet/RequiredEquipments';
|
||||
import VisualIdentity from './contcet/VisualIdentity';
|
||||
import Budget from './contcet/Budget';
|
||||
import AdditionalSupport from './contcet/AdditionalSupport';
|
||||
import AdditionalNotes from './contcet/AdditionalNotes';
|
||||
import SideProfile from './SideProfile';
|
||||
import authService from '../../../services/authService';
|
||||
|
||||
const { createNewRestaurant } = authService;
|
||||
const drawerWidth = 230;
|
||||
|
||||
const CreateRestaurant = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [hasProducts, setHasProducts] = useState(false); // حالة لتتبع وجود المنتجات
|
||||
const [hasProducts, setHasProducts] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
|
||||
// محاكاة للتحقق من وجود المنتجات (استبدل هذا بمنطقك الفعلي)
|
||||
useEffect(() => {
|
||||
// هنا يجب استبدال هذا بمنطق فعلي للتحقق من وجود المنتجات
|
||||
// مثلاً استدعاء API أو التحقق من state
|
||||
const checkProducts = async () => {
|
||||
// محاكاة لاستدعاء API
|
||||
const productsExist = await checkIfProductsExist(); // استبدل هذه الدالة بمنطقك الفعلي
|
||||
setHasProducts(productsExist);
|
||||
};
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
checkProducts();
|
||||
}, []);
|
||||
// إدارة بيانات النموذج لجميع الخطوات (مشاركة البيانات بين الخطوات)
|
||||
const [formData, setFormData] = useState({
|
||||
|
||||
// دالة مساعدة لمحاكاة التحقق من المنتجات (استبدلها بمنطقك الفعلي)
|
||||
const checkIfProductsExist = async () => {
|
||||
// محاكاة - يمكن أن يكون هذا استدعاء لـ API أو تحقق من state
|
||||
// return true;
|
||||
return false; // غير هذه القيمة حسب منطقك
|
||||
});
|
||||
|
||||
// دالة لتحديث بيانات النموذج بشكل مرن (دمج الجديد مع القديم)
|
||||
const updateFormData = (newData) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
...newData,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
// دالة لإغلاق السناك بار
|
||||
const handleSnackbarClose = (event, reason) => {
|
||||
if (reason === 'clickaway') return;
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await createNewRestaurant(formData);
|
||||
setSnackbarOpen(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const steps = [
|
||||
<CloudKitchenProject
|
||||
key="step-0"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<OperationalDetails
|
||||
key="step-1"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<RequiredEquipments
|
||||
key="step-2"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<VisualIdentity
|
||||
key="step-3"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<Budget
|
||||
key="step-4"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={() => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1))}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>,
|
||||
<AdditionalNotes
|
||||
key="step-5"
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
];
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// const checkProducts = async () => {
|
||||
// const productsExist = await checkIfProductsExist();
|
||||
// setHasProducts(productsExist);
|
||||
// };
|
||||
// checkProducts();
|
||||
// }, []);
|
||||
|
||||
// const checkIfProductsExist = async () => {
|
||||
// return false;
|
||||
// };
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth >= theme.breakpoints.values.md) {
|
||||
setSidebarOpen(true);
|
||||
@@ -50,7 +133,6 @@ const CreateRestaurant = () => {
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [theme.breakpoints.values.md]);
|
||||
|
||||
@@ -59,12 +141,14 @@ const CreateRestaurant = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
backgroundColor: '#F6F6F6',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
backgroundColor: '#F6F6F6',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
@@ -72,32 +156,100 @@ const CreateRestaurant = () => {
|
||||
drawerWidth={drawerWidth}
|
||||
/>
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: {
|
||||
xs: '100%',
|
||||
sm: '100%',
|
||||
md: '100%'
|
||||
},
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: sidebarOpen ? `${drawerWidth}px` : 0,
|
||||
md: 0
|
||||
},
|
||||
transition: theme.transitions.create(['width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: { xs: '100%', sm: '100%', md: '100%' },
|
||||
marginLeft: { xs: 0, sm: sidebarOpen ? `${drawerWidth}px` : 0, md: 0 },
|
||||
transition: theme.transitions.create(['width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<KitchPlusAppBar
|
||||
onDrawerToggle={handleDrawerToggle}
|
||||
sidebarOpen={sidebarOpen}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<Typography>CreateYourRestaurant</Typography>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
ml: 3,
|
||||
mb: 2,
|
||||
width: { md: '30%' },
|
||||
display: { xs: 'none', sm: 'none', md: 'block' },
|
||||
overflowY: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100%',
|
||||
pb: 15,
|
||||
pt: 3,
|
||||
}}
|
||||
>
|
||||
<SideProfile
|
||||
currentStepIndex={currentStep}
|
||||
onBack={() => setCurrentStep(prev => Math.max(prev - 1, 0))}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
ml: { xs: 2, md: 3 },
|
||||
flexGrow: 1,
|
||||
height: '100vh',
|
||||
pr: { sm: 2, md: 1 },
|
||||
pt: 3,
|
||||
mb: { sm: 20 },
|
||||
width: { md: '60%' },
|
||||
display: { xs: 'block', sm: 'block', md: 'block' },
|
||||
overflowY: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100%',
|
||||
pb: 18,
|
||||
}}
|
||||
>
|
||||
{steps[currentStep]}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={4000}
|
||||
onClose={handleSnackbarClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert onClose={handleSnackbarClose} severity="success" sx={{ width: '100%' }}>
|
||||
عملية إنشاء المطعم تمت بنجاح
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Stack, Button, useTheme } from '@mui/material';
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||
|
||||
const steps = [
|
||||
{ title: 'Cloud Kitchen Project', icon: '/images/createProfile/BasicInf.png' },
|
||||
{ title: 'OperationalDetails', icon: '/images/icons/rocket.png' },
|
||||
{ title: 'Required Equipments', icon: '/images/createProfile/equipment.png'},
|
||||
{ title: 'Visual Identity', icon: '/images/createProfile/Vector.png' },
|
||||
{ title: 'Budget & Expansion', icon: '/images/createProfile/Expansion.png' },
|
||||
{ title: 'Additional Notes & Concerns', icon: '/images/createProfile/Group.png' },
|
||||
{ title: 'Submit & Confirmation', icon: '/images/createProfile/Confirmation.png' },
|
||||
];
|
||||
|
||||
const SideProfile = ({ currentStepIndex = 0, onBack }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100%',
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 3,
|
||||
py: 4,
|
||||
display: { xs: 'none', md: 'block' },
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* العنوان الرئيسي */}
|
||||
<Typography fontSize="18px" fontWeight={600} mb={1.2}>
|
||||
Create Your Restaurant
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" mb={3}>
|
||||
Complete this process to register your restaurant on our amazing food platform.
|
||||
</Typography>
|
||||
|
||||
{/* الخطوات */}
|
||||
<Stack spacing={3} sx={{ ml: 1, position: 'relative', pb: 12 }}>
|
||||
{steps.map((step, index) => (
|
||||
<Box key={index} position="relative">
|
||||
{/* الخط الرأسي بين الدوائر */}
|
||||
{index !== steps.length - 1 && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '28px',
|
||||
left: '23px',
|
||||
width: '2px',
|
||||
height: 'calc(100% - 5px)',
|
||||
backgroundColor: '#E0E0E0',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* محتوى كل خطوة */}
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
{/* الدائرة بالأيقونة */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: 45,
|
||||
height: 45,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{/* الدائرة الخلفية - تظهر دائمًا */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
backgroundImage:
|
||||
index <= currentStepIndex
|
||||
? 'linear-gradient(0deg, #FF914D, #F3F3F3)'
|
||||
: 'linear-gradient(0deg, #DFDFDF, #F0F0F0)',
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: '50%',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* الدائرة الأمامية بالأيقونة */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#FFF',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={step.icon}
|
||||
alt={step.title}
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginLeft: step.title === 'Required Equipments' ? 7 : 0,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* العنوان */}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
color: '#000',
|
||||
}}
|
||||
>
|
||||
{step.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* زر الرجوع */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<ArrowBackIosIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={onBack}
|
||||
disabled={currentStepIndex === 0}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '20px',
|
||||
color: currentStepIndex === 0 ? '#C2C2C2' : '#6B7283',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideProfile;
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import ConfirmationDialog from './ConfirmationDialog';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
const AdditionalNotes = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData, onSubmit }) => {
|
||||
const theme = useTheme();
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const handleOpenModal = () => setOpenModal(true);
|
||||
const handleCloseModal = () => setOpenModal(false);
|
||||
|
||||
const handleNotesChange = (e) => {
|
||||
updateFormData({ need_help: e.target.value });
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (onSubmit) {
|
||||
await onSubmit();
|
||||
}
|
||||
setOpenModal(false);
|
||||
|
||||
// التوجيه فقط إذا كانت الصفحة هي /create-restaurant
|
||||
if (location.pathname === '/create-restaurant') {
|
||||
navigate('/restaurant');
|
||||
}
|
||||
// إذا كانت من رابط آخر مثل /create-kitchen لا نفعل شيئًا
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: { xs: '90%', sm: '100%', md: 740 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 4,
|
||||
pt: { xs: 4, md: 11 },
|
||||
pb: 10,
|
||||
display: 'block',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
width: { xs: '85%', sm: '90%' },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5}>
|
||||
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
|
||||
Additional Notes
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '70%' }}>
|
||||
<Typography fontSize="16px" color="text.secondary" fontWeight={500} sx={{ pb: 1 }}>
|
||||
Enter your basic information to proceed to registration of your own restaurant on this platform
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px' }}>
|
||||
Notes / Concerns
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="Write for us"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={7}
|
||||
value={formData.need_help || ''}
|
||||
onChange={handleNotesChange}
|
||||
sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px',
|
||||
alignItems: 'start',
|
||||
},
|
||||
'& textarea::placeholder': {
|
||||
color: '#969BA7',
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleOpenModal}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
display: { xs: 'block', sm: 'block', md: 'none' },
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
borderColor: theme.palette.primary.main,
|
||||
}
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={openModal}
|
||||
onClose={handleCloseModal}
|
||||
onConfirm={handleConfirmSubmit}
|
||||
loading={loading}
|
||||
title="Confirm Submission"
|
||||
description="Are you sure you want to proceed to the next step?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdditionalNotes;
|
||||
@@ -0,0 +1,175 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
} from '@mui/material';
|
||||
|
||||
const AdditionalSupport = ({ currentStepIndex = 0, onNext, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const [selectedOptions, setSelectedOptions] = useState([]);
|
||||
|
||||
const handleCheckboxChange = (option) => {
|
||||
setSelectedOptions((prev) =>
|
||||
prev.includes(option)
|
||||
? prev.filter((o) => o !== option)
|
||||
: [...prev, option]
|
||||
);
|
||||
};
|
||||
|
||||
const options = ['Supplier Assistance', 'Staff Training'];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: { xs: '90%', sm: '100%', md: 740 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 4,
|
||||
pt: { xs: 4, md: 12 },
|
||||
pb: 4,
|
||||
display: 'block',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
width: { xs: '85%', sm: '90%' },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5}>
|
||||
<Typography
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Additional Support
|
||||
</Typography>
|
||||
<Box sx={{ width: '70%' }}>
|
||||
<Typography
|
||||
fontSize="16px"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ pb: 1 }}
|
||||
>
|
||||
Enter your basic information to proceed to registration of your own
|
||||
restaurant on this platform
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Checkboxes for Additional Support Options */}
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{ fontWeight: '500', fontSize: '16px', mb: 1 }}
|
||||
>
|
||||
Select Additional Support Options
|
||||
</Typography>
|
||||
<FormGroup
|
||||
row
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const isChecked = selectedOptions.includes(option);
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={option}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onChange={() => handleCheckboxChange(option)}
|
||||
sx={{
|
||||
transform: 'scale(1.5)',
|
||||
color: '#F0EDED',
|
||||
'&.Mui-checked': {
|
||||
color: '#FF914D',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography
|
||||
sx={{
|
||||
color: isChecked ? '#e57f3f' : '#969BA7',
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</Typography>
|
||||
}
|
||||
sx={{
|
||||
m: 0,
|
||||
width: { xs: '100%', sm: '48%', md: '40%' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</FormGroup>
|
||||
</Box>
|
||||
|
||||
{/* Buttons */}
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={onNext}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
display: { xs: 'block', sm: 'block', md: 'none' },
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdditionalSupport;
|
||||
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Stack, Button, useTheme, TextField } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Budget = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleNext = () => {
|
||||
if (onNext) onNext();
|
||||
else navigate('/dashboard');
|
||||
};
|
||||
|
||||
const handleInputChange = (field) => (e) => {
|
||||
updateFormData({ [field]: e.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: { xs: '90%', sm: '100%', md: 740 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 4,
|
||||
pt: { xs: 4, md: 12 },
|
||||
pb: { xs: 20, sm: 12, md: 3 },
|
||||
display: 'block',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
width: { xs: '85%', sm: '90%' },
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5}>
|
||||
<Typography fontWeight={700} sx={{ fontSize: { xs: '1.8rem', sm: '2rem', md: '2.2rem' } }}>
|
||||
Budget & Expansion
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '70%' }}>
|
||||
<Typography
|
||||
fontSize="16px"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ pb: 1 }}
|
||||
>
|
||||
Enter your basic information to proceed to registration of your own restaurant on this platform
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Estimated Budget Input */}
|
||||
<InputField
|
||||
label="Estimated Budget"
|
||||
placeholder="$4200"
|
||||
theme={theme}
|
||||
value={formData.estimated_budget || ''}
|
||||
onChange={handleInputChange('estimated_budget')}
|
||||
/>
|
||||
|
||||
{/* Expansion Plans Through Cloud Kitchen (No of Branches) Input */}
|
||||
<InputField
|
||||
label="Expansion Plans Through Cloud Kitchen (No of Branches)"
|
||||
placeholder="200"
|
||||
theme={theme}
|
||||
value={formData.expansion_branches || ''}
|
||||
onChange={handleInputChange('expansion_branches')}
|
||||
/>
|
||||
|
||||
{/* Next Button */}
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
display: { xs: 'block', sm: 'block', md: 'none' },
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const InputField = ({ label, placeholder, theme, value, onChange }) => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px', color: 'black' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder={placeholder}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255,145,77,0.1)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default Budget;
|
||||
@@ -0,0 +1,438 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
TextField,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
FormHelperText
|
||||
} from '@mui/material';
|
||||
|
||||
import authService from '../../../../services/authService'; // تأكد من المسار الصحيح
|
||||
|
||||
const CloudKitchenProject = ({
|
||||
currentStepIndex = 0,
|
||||
onNext,
|
||||
formData,
|
||||
updateFormData,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [cuisineTypes, setCuisineTypes] = useState([]);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
//cuisineTypes
|
||||
useEffect(() => {
|
||||
const fetchCuisineTypes = async () => {
|
||||
const data = await authService.cuisineTypes();
|
||||
setCuisineTypes(data);
|
||||
};
|
||||
fetchCuisineTypes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.menu_status === undefined) {
|
||||
updateFormData({ menu_status: false });
|
||||
}
|
||||
if (formData.is_existing_brand === undefined) {
|
||||
updateFormData({ is_existing_brand: false });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
if (name === 'is_existing_brand' || name === 'menu_status') {
|
||||
updateFormData({ [name]: value === 'yes' });
|
||||
} else {
|
||||
updateFormData({ [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const validate = () => {
|
||||
let tempErrors = {};
|
||||
if (!formData.name || formData.name.trim() === '') {
|
||||
tempErrors.name = 'Restaurant Name is required';
|
||||
}
|
||||
if (!formData.cuisine_type_id) {
|
||||
tempErrors.cuisine_type_id = 'Cuisine Type is required';
|
||||
}
|
||||
if (typeof formData.is_existing_brand !== 'boolean') {
|
||||
tempErrors.is_existing_brand = 'Existing Brand selection is required';
|
||||
}
|
||||
if (!formData.location || formData.location.trim() === '') {
|
||||
tempErrors.location = 'Location is required';
|
||||
}
|
||||
if (typeof formData.menu_status !== 'boolean') {
|
||||
tempErrors.menu_status = 'Menu Status selection is required';
|
||||
}
|
||||
setErrors(tempErrors);
|
||||
return Object.keys(tempErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (validate()) {
|
||||
onNext();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: { xs: '90%', sm: '100%', md: '92.5%' },
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 4,
|
||||
pt: 5,
|
||||
pb: 11,
|
||||
display: 'block',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
width: { xs: '85%', sm: '90%' },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5}>
|
||||
<Typography
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cloud Kitchen Project
|
||||
</Typography>
|
||||
<Box sx={{ width: '80%' }}>
|
||||
<Typography
|
||||
fontSize="16px"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ pb: 1 }}
|
||||
>
|
||||
Enter your basic information to proceed to registration of your own restaurant on this platform
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Restaurant Name */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Restaurant Name
|
||||
</Typography>
|
||||
<TextField
|
||||
name="name"
|
||||
placeholder="Al-Baik Foods"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formData.name || ''}
|
||||
onChange={handleChange}
|
||||
error={!!errors.name}
|
||||
helperText={errors.name}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Cuisine Type */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Cuisine Type
|
||||
</Typography>
|
||||
<TextField
|
||||
select
|
||||
name="cuisine_type_id"
|
||||
placeholder="Select Cuisine Type"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formData.cuisine_type_id || ''}
|
||||
onChange={handleChange}
|
||||
error={!!errors.cuisine_type_id}
|
||||
helperText={errors.cuisine_type_id}
|
||||
sx={{
|
||||
'& .MuiSelect-select': { fontWeight: 500, fontSize: '15px' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{cuisineTypes.length > 0 ? (
|
||||
cuisineTypes.map((type) => (
|
||||
<MenuItem key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</MenuItem>
|
||||
))
|
||||
) : (
|
||||
<MenuItem disabled>No Cuisine Types Found</MenuItem>
|
||||
)}
|
||||
</TextField>
|
||||
</Box>
|
||||
|
||||
{/* Existing Brand */}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
|
||||
>
|
||||
Existing Brand
|
||||
</Typography>
|
||||
<FormControl error={!!errors.is_existing_brand}>
|
||||
<RadioGroup
|
||||
row
|
||||
name="is_existing_brand"
|
||||
value={formData.is_existing_brand ? 'yes' : 'no'}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="yes"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
color: 'rgba(150, 155, 167, 0.6)',
|
||||
transform: 'scale(0.85)',
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Yes"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="no"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
color: 'rgba(150, 155, 167, 0.6)',
|
||||
transform: 'scale(0.85)',
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="No"
|
||||
/>
|
||||
</RadioGroup>
|
||||
{errors.is_existing_brand && (
|
||||
<FormHelperText>{errors.is_existing_brand}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{/* Further Brand Details */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px' }}>
|
||||
Further Brand Details
|
||||
</Typography>
|
||||
<TextField
|
||||
name="brand_details"
|
||||
placeholder="We need Pizza Machine"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={3}
|
||||
value={formData.brand_details || ''}
|
||||
onChange={handleChange}
|
||||
error={!!errors.brand_details}
|
||||
helperText={errors.brand_details}
|
||||
sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px',
|
||||
alignItems: 'start',
|
||||
},
|
||||
'& textarea::placeholder': {
|
||||
color: '#969BA7',
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Age Group */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Age Group
|
||||
</Typography>
|
||||
<TextField
|
||||
name="age_group"
|
||||
placeholder="15 - 75"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formData.age_group || ''}
|
||||
onChange={handleChange}
|
||||
error={!!errors.age_group}
|
||||
helperText={errors.age_group}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Location */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Location
|
||||
</Typography>
|
||||
<TextField
|
||||
name="location"
|
||||
placeholder="Street 123, Jordan"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formData.location || ''}
|
||||
onChange={handleChange}
|
||||
error={!!errors.location}
|
||||
helperText={errors.location}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Menu Status */}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
|
||||
>
|
||||
Menu Status
|
||||
</Typography>
|
||||
<FormControl error={!!errors.menu_status}>
|
||||
<RadioGroup
|
||||
row
|
||||
name="menu_status"
|
||||
value={formData.menu_status ? 'yes' : 'no'}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="yes"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
color: 'rgba(150, 155, 167, 0.6)',
|
||||
transform: 'scale(0.85)',
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Yes"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="no"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
color: 'rgba(150, 155, 167, 0.6)',
|
||||
transform: 'scale(0.85)',
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="No"
|
||||
/>
|
||||
</RadioGroup>
|
||||
{errors.menu_status && (
|
||||
<FormHelperText>{errors.menu_status}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{/* زر Next */}
|
||||
<Box sx={{ pt: 2.8 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudKitchenProject;
|
||||
@@ -0,0 +1,182 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
const ConfirmationDialog = ({ open, onClose, onConfirm, loading }) => {
|
||||
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: '525px',
|
||||
height: '350px',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
|
||||
p: 3,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
mb: 3,
|
||||
ml: -5,
|
||||
}}
|
||||
>
|
||||
{/* الدوائر */}
|
||||
<Box sx={{
|
||||
position: 'relative',
|
||||
width: 100,
|
||||
height: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
{/* الدائرة الخلفية */}
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
backgroundColor: '#F5F8FF',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
zIndex: 0,
|
||||
}} />
|
||||
|
||||
|
||||
<Box sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#E9EFFF',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
}}>
|
||||
<img
|
||||
src='/images/createProfile/Successful.png'
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
alt="Success icon"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<DialogTitle sx={{ p: 0 }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="div"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: '28px',
|
||||
lineHeight: '1.2'
|
||||
}}
|
||||
>
|
||||
Register successful!
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
</Box>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<DialogContentText sx={{
|
||||
textAlign: 'center',
|
||||
mb: 0,
|
||||
fontSize: '16px',
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary
|
||||
}}>
|
||||
Congratulations! you well register your restaurant successfully on our platform
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: 0,
|
||||
px: 3,
|
||||
pt: 2,
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
height: '48px',
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Censel
|
||||
</Button>
|
||||
|
||||
{/* الزر الأول - Filled */}
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={onConfirm}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
height: '48px',
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={24} sx={{ color: 'white' }} />
|
||||
) : (
|
||||
'Submit'
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
||||
@@ -0,0 +1,292 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
TextField,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import authService from '../../../../services/authService'; // ← عدّل المسار حسب مكانك
|
||||
|
||||
const OperationalDetails = ({
|
||||
currentStepIndex = 0,
|
||||
onNext,
|
||||
onBack,
|
||||
formData,
|
||||
updateFormData,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [countries, setCountries] = useState([]);
|
||||
const [loadingCountries, setLoadingCountries] = useState(false);
|
||||
const [errors, setErrors] = useState({}); // <-- حالة الأخطاء
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCountries = async () => {
|
||||
setLoadingCountries(true);
|
||||
const result = await authService.getCountries();
|
||||
if (result.success) {
|
||||
setCountries(result.data);
|
||||
} else {
|
||||
console.error(result.message);
|
||||
}
|
||||
setLoadingCountries(false);
|
||||
};
|
||||
|
||||
fetchCountries();
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
updateFormData({ [name]: value });
|
||||
};
|
||||
|
||||
// التحقق من صحة الحقول
|
||||
const validate = () => {
|
||||
let tempErrors = {};
|
||||
|
||||
// التحقق من أن الحقل غير فارغ
|
||||
if (!formData.staff_members || formData.staff_members.trim() === '') {
|
||||
tempErrors.staff_members = 'Staff Members is required';
|
||||
}
|
||||
// التحقق من أن القيمة رقمية
|
||||
else if (isNaN(formData.staff_members)) {
|
||||
tempErrors.staff_members = 'Staff Members must be a number';
|
||||
}
|
||||
|
||||
if (!formData.country_id || formData.country_id === '') {
|
||||
tempErrors.country_id = 'Country selection is required';
|
||||
}
|
||||
|
||||
setErrors(tempErrors);
|
||||
return Object.keys(tempErrors).length === 0;
|
||||
};
|
||||
|
||||
|
||||
const handleNext = () => {
|
||||
if (validate()) {
|
||||
onNext();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: { xs: '90%', sm: '100%', md: 740 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 4,
|
||||
pt: { xs: 4, md: 14.5 },
|
||||
pb: { xs: 20, sm: 20, md: 0 },
|
||||
display: 'block',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
width: { xs: '85%', sm: '90%' },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5}>
|
||||
<Typography
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Operational Details
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '70%' }}>
|
||||
<Typography
|
||||
fontSize="16px"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ pb: 1 }}
|
||||
>
|
||||
Enter your basic information to proceed to registration of your
|
||||
own restaurant on this platform
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Staff Members Input */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{ fontWeight: '500', fontSize: '16px' }}
|
||||
>
|
||||
Staff Members
|
||||
</Typography>
|
||||
<TextField
|
||||
name="staff_members"
|
||||
placeholder="200 (100 Chefs, 100 Cooks)"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formData.staff_members || ''}
|
||||
onChange={handleChange}
|
||||
error={!!errors.staff_members}
|
||||
helperText={errors.staff_members}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Country Selector */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{ fontWeight: '500', fontSize: '16px' }}
|
||||
>
|
||||
Country
|
||||
</Typography>
|
||||
<TextField
|
||||
select
|
||||
name="country_id"
|
||||
fullWidth
|
||||
value={formData.country_id || ''}
|
||||
onChange={handleChange}
|
||||
disabled={loadingCountries}
|
||||
error={!!errors.country_id}
|
||||
helperText={errors.country_id}
|
||||
sx={{
|
||||
'& .MuiSelect-select': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px',
|
||||
minHeight: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'& .MuiOutlinedInput-input': { padding: '10.5px 14px' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="" disabled hidden>
|
||||
{loadingCountries ? 'Loading...' : 'Select your country'}
|
||||
</MenuItem>
|
||||
{countries.map((country) => (
|
||||
<MenuItem key={country.id} value={country.id}>
|
||||
{country.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
|
||||
{/* Expansion Plan Cities Input */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="black"
|
||||
sx={{ fontWeight: '500', fontSize: '16px' }}
|
||||
>
|
||||
Expansion Plan Cities
|
||||
</Typography>
|
||||
<TextField
|
||||
name="expansion_plan_cities" // ← ✅ الاسم الصحيح المتوقع من الـ API
|
||||
placeholder="Layyah Lahore Islamabad"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formData.expansion_plan_cities || ''}
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Next Button */}
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
{/* Back Button */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
display: { xs: 'block', sm: 'block', md: 'none' },
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperationalDetails;
|
||||
@@ -0,0 +1,285 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
TextField,
|
||||
FormControlLabel,
|
||||
RadioGroup,
|
||||
Radio
|
||||
} from '@mui/material';
|
||||
|
||||
const RequiredEquipments = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => {
|
||||
const theme = useTheme();
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
// تعيين القيمة الافتراضية لـ needSpecializedEquipment إذا غير موجودة
|
||||
useEffect(() => {
|
||||
if (!formData.needSpecializedEquipment) {
|
||||
updateFormData({ needSpecializedEquipment: 'no' });
|
||||
}
|
||||
}, [formData.needSpecializedEquipment, updateFormData]);
|
||||
|
||||
// التعامل مع تغير القيمة النصية للمعدات العامة
|
||||
const handleEquipmentChange = (e) => {
|
||||
updateFormData({ equipment: e.target.value });
|
||||
};
|
||||
|
||||
|
||||
const handleNeedSpecializedChange = (e) => {
|
||||
updateFormData({ needSpecializedEquipment: e.target.value });
|
||||
};
|
||||
|
||||
// التعامل مع القيمة النصية للمعدات المتخصصة
|
||||
const handleSpecializedEquipmentChange = (e) => {
|
||||
updateFormData({ specialized_equipment: e.target.value });
|
||||
};
|
||||
|
||||
// دالة التحقق من صحة الحقول
|
||||
const validate = () => {
|
||||
let tempErrors = {};
|
||||
|
||||
// if (!formData.equipment || formData.equipment.trim() === '') {
|
||||
// tempErrors.equipment = 'Equipment is required';
|
||||
// }
|
||||
// if (!formData.needSpecializedEquipment || (formData.needSpecializedEquipment !== 'yes' && formData.needSpecializedEquipment !== 'no')) {
|
||||
// tempErrors.needSpecializedEquipment = 'Please select Yes or No';
|
||||
// }
|
||||
if (formData.needSpecializedEquipment === 'yes' && (!formData.specialized_equipment || formData.specialized_equipment.trim() === '')) {
|
||||
tempErrors.specialized_equipment = 'Please specify specialized equipments';
|
||||
}
|
||||
|
||||
setErrors(tempErrors);
|
||||
return Object.keys(tempErrors).length === 0;
|
||||
};
|
||||
|
||||
// دالة معالجة الضغط على زر Next مع تحقق الفالديشن
|
||||
const handleNext = () => {
|
||||
if (validate()) {
|
||||
onNext();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: { xs: '90%', sm: '100%', md: 740 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 4,
|
||||
pt: { xs: 4, md: 4.5 },
|
||||
pb: 10,
|
||||
display: 'block',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
width: { xs: '85%', sm: '90%' },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5}>
|
||||
<Typography
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Required Equipments
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '70%' }}>
|
||||
<Typography
|
||||
fontSize="16px"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ pb: 1 }}
|
||||
>
|
||||
Enter your basic information to proceed to registration of your own restaurant on this platform
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Equipment Input */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Equipment
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="Ovens Refrigerators Pizza Machines |"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formData.equipment || ''}
|
||||
onChange={handleEquipmentChange}
|
||||
error={!!errors.equipment}
|
||||
helperText={errors.equipment}
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Need Specialized Equipment */}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
|
||||
>
|
||||
Need Specialized Equipment
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
name="needSpecializedEquipment"
|
||||
value={formData.needSpecializedEquipment || 'no'}
|
||||
onChange={handleNeedSpecializedChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="yes"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
color: 'rgba(150, 155, 167, 0.6)',
|
||||
transform: 'scale(0.85)',
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Yes"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="no"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
pl: 5,
|
||||
color: 'rgba(150, 155, 167, 0.6)',
|
||||
transform: 'scale(0.85)',
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="No"
|
||||
/>
|
||||
</RadioGroup>
|
||||
{errors.needSpecializedEquipment && (
|
||||
<Typography color="error" variant="caption" sx={{ mt: 0.5 }}>
|
||||
{errors.needSpecializedEquipment}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Specialized Equipments For Food Preparation */}
|
||||
{formData.needSpecializedEquipment === 'yes' && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '16px' }}>
|
||||
Specialized Equipments For Food Preparation
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="Knife Spoon Handle |"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={3}
|
||||
value={formData.specialized_equipment || ''}
|
||||
onChange={handleSpecializedEquipmentChange}
|
||||
error={!!errors.specialized_equipment}
|
||||
helperText={errors.specialized_equipment
|
||||
}
|
||||
sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
fontWeight: 500,
|
||||
fontSize: '15px',
|
||||
alignItems: 'start',
|
||||
},
|
||||
'& textarea::placeholder': {
|
||||
color: '#969BA7',
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
display: { xs: 'block', sm: 'block', md: 'none' },
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequiredEquipments;
|
||||
@@ -0,0 +1,256 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
useTheme,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Typography as MuiTypography
|
||||
} from '@mui/material';
|
||||
|
||||
const VisualIdentity = ({ currentStepIndex = 0, onNext, onBack, formData, updateFormData }) => {
|
||||
const theme = useTheme();
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
// تأكد من وجود قيمة افتراضية
|
||||
useEffect(() => {
|
||||
if (!formData.visualIdentity) {
|
||||
updateFormData({ visualIdentity: 'no' });
|
||||
}
|
||||
}, [formData.visualIdentity, updateFormData]);
|
||||
|
||||
const handleVisualIdentityChange = (e) => {
|
||||
updateFormData({ visualIdentity: e.target.value });
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
updateFormData({ logo: file });
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
let tempErrors = {};
|
||||
|
||||
if (!formData.visualIdentity || (formData.visualIdentity !== 'yes' && formData.visualIdentity !== 'no')) {
|
||||
tempErrors.visualIdentity = 'Please select Yes or No';
|
||||
}
|
||||
|
||||
/*
|
||||
if (!formData.logo) {
|
||||
tempErrors.logo = 'Please upload a logo or color scheme image';
|
||||
}
|
||||
*/
|
||||
|
||||
setErrors(tempErrors);
|
||||
return Object.keys(tempErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (validate()) {
|
||||
onNext();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: { xs: '90%', sm: '100%', md: 740 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
px: 4,
|
||||
pt: { xs: 4, md: 4.5 },
|
||||
pb: 10,
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 1px 4px rgba(0,0,0,0.05)',
|
||||
width: { xs: '85%', sm: '90%' },
|
||||
mx: 'auto'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5}>
|
||||
<Typography
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: '1.8rem',
|
||||
sm: '2rem',
|
||||
md: '2.2rem'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Visual Identity
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '70%' }}>
|
||||
<Typography
|
||||
fontSize="16px"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ pb: 1 }}
|
||||
>
|
||||
Enter your basic information to proceed to registration of your own restaurant on this platform
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 500, fontSize: '16px', mb: 1, mt: 2, color: '#191635' }}
|
||||
>
|
||||
Visual Identity
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
name="visualIdentity"
|
||||
value={formData.visualIdentity || ''}
|
||||
onChange={handleVisualIdentityChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="yes"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
color: 'rgba(150, 155, 167, 0.6)',
|
||||
transform: 'scale(0.85)',
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Yes"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="no"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
pl: 5,
|
||||
color: 'rgba(150, 155, 167, 0.6)',
|
||||
transform: 'scale(0.85)',
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="No"
|
||||
/>
|
||||
</RadioGroup>
|
||||
{errors.visualIdentity && (
|
||||
<MuiTypography color="error" variant="caption" sx={{ mt: 0.5 }}>
|
||||
{errors.visualIdentity}
|
||||
</MuiTypography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Upload Logo / Color Scheme */}
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '16px',
|
||||
color: 'black',
|
||||
mb: 2,
|
||||
pl: 1,
|
||||
}}
|
||||
>
|
||||
Upload Logo / Color Scheme If Any
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
component="label"
|
||||
htmlFor="upload-image"
|
||||
sx={{
|
||||
border: errors.logo_path ? '2px solid red' : '2px dashed #ccc',
|
||||
borderRadius: '10px',
|
||||
height: '150px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
color: '#969BA7',
|
||||
backgroundColor: '#FAFAFA',
|
||||
fontWeight: 500,
|
||||
fontSize: '15px',
|
||||
textAlign: 'center',
|
||||
transition: 'border-color 0.3s',
|
||||
'&:hover': {
|
||||
borderColor: '#FF914D',
|
||||
backgroundColor: '#fffaf5',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formData.logo ? (formData.logo.name || 'Uploaded Image') : 'Upload Picture'}
|
||||
<input
|
||||
id="upload-image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</Box>
|
||||
{errors.logo && (
|
||||
<MuiTypography color="error" variant="caption" sx={{ mt: 0.5 }}>
|
||||
{errors.logo}
|
||||
</MuiTypography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Buttons */}
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
height: { xs: '45px', sm: '52px' },
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
display: { xs: 'block', sm: 'block', md: 'none' },
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisualIdentity;
|
||||
@@ -0,0 +1,244 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRestaurant } from '../../../contexts/RestaurantContext';
|
||||
import StatisticsCard from './StatisticsCard';
|
||||
// import TablesManager from './TablesManager';
|
||||
import {
|
||||
Box,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import authService from '../../../services/authService';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const AnalyticsPage = () => {
|
||||
const { restaurantId } = useRestaurant();
|
||||
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
|
||||
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [chartData, setChartData] = useState([]);
|
||||
|
||||
const handleTimeFrameChange = (newTimeFrame) => {
|
||||
setTimeFrame(newTimeFrame);
|
||||
};
|
||||
|
||||
const handleCustomDateChange = (event) => {
|
||||
setCustomDate(event.target.value);
|
||||
};
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
if (!restaurantId) return;
|
||||
|
||||
let period = 'yearly';
|
||||
let labels = [];
|
||||
let mappedData = [];
|
||||
|
||||
switch (timeFrame) {
|
||||
case '24h': // يومي
|
||||
period = 'daily';
|
||||
labels = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`);
|
||||
try {
|
||||
const response = await authService.getOrderStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label || item.date || '',
|
||||
orders_total: item.orders_total || 0,
|
||||
items_total: item.items_total || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
orders_total: 0,
|
||||
items_total: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
orders_total: 0,
|
||||
items_total: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case '30d': // شهري
|
||||
period = 'monthly';
|
||||
labels = Array.from({ length: 30 }, (_, i) => dayjs().startOf('month').add(i, 'day').format('DD'));
|
||||
try {
|
||||
const response = await authService.getOrderStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label ? dayjs(item.label).format('DD') : item.date ? dayjs(item.date).format('DD') : '',
|
||||
orders_total: item.orders_total || 0,
|
||||
items_total: item.items_total || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
orders_total: 0,
|
||||
items_total: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
orders_total: 0,
|
||||
items_total: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case '12m': // سنوي
|
||||
default:
|
||||
period = 'yearly';
|
||||
labels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
try {
|
||||
const response = await authService.getOrderStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label || item.date || '',
|
||||
orders_total: item.orders_total || 0,
|
||||
items_total: item.items_total || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
orders_total: 0,
|
||||
items_total: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
orders_total: 0,
|
||||
items_total: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
setChartData(mappedData);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatistics();
|
||||
}, [timeFrame, restaurantId, customDate]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
pr={{ xs: 1, sm: 2 }}
|
||||
maxWidth="100%"
|
||||
overflowX="hidden">
|
||||
{/* Header Buttons */}
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
mb={1.5}
|
||||
>
|
||||
<ButtonGroup
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'inset 0 0 0 1px #e0e0e0',
|
||||
'& .MuiButton-root': {
|
||||
textTransform: 'none',
|
||||
fontSize: '13px',
|
||||
padding: '6px 12px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: '#555',
|
||||
'&:hover': { backgroundColor: '#f5f5f5' }
|
||||
},
|
||||
'& .MuiButton-contained': {
|
||||
backgroundColor: 'rgba(255, 117, 34, 0.08)',
|
||||
color: '#ff5722',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => (
|
||||
<Button
|
||||
key={value}
|
||||
variant={timeFrame === value ? 'contained' : 'text'}
|
||||
onClick={() => handleTimeFrameChange(value)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
{/* Date Picker + Today Button */}
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: 2,
|
||||
p: '4px 8px',
|
||||
boxShadow: 'inset 0 0 0 1px #e0e0e0'
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="date"
|
||||
value={customDate}
|
||||
onChange={handleCustomDateChange}
|
||||
size="small"
|
||||
sx={{
|
||||
width: isMobile ? '100%' : 150,
|
||||
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
|
||||
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setCustomDate(dayjs().format('YYYY-MM-DD'))}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 117, 34, 0.08)',
|
||||
color: '#ff5722',
|
||||
textTransform: 'none',
|
||||
boxShadow: 'none',
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
'&:hover': { backgroundColor: 'rgba(255, 117, 34, 0.15)' }
|
||||
}}
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* StatisticsCard */}
|
||||
<Box>
|
||||
<StatisticsCard
|
||||
title="Analytics Overview"
|
||||
subtitle="Orders Statistics"
|
||||
data={chartData}
|
||||
dataKeys={[
|
||||
{ key: 'orders_total', name: 'Orders', color: '#E46A11' },
|
||||
{ key: 'items_total', name: 'Items', color: '#0182FC' },
|
||||
]}
|
||||
xDataKey="label"
|
||||
valueFormatter={(value) => value}
|
||||
timeFrame={timeFrame}
|
||||
onTimeFrameChange={handleTimeFrameChange}
|
||||
height={320}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsPage;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRestaurant } from '../../../contexts/RestaurantContext';
|
||||
import OrderStatusCard from './OrderStatusCard';
|
||||
import { Box, useTheme, useMediaQuery } from '@mui/material';
|
||||
import authService from '../../../services/authService';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const AnalyticsPageOccupancy = () => {
|
||||
const { restaurantId } = useRestaurant();
|
||||
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
|
||||
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [occupancyRate, setOccupancyRate] = useState(0);
|
||||
|
||||
const fetchOccupancyRate = async () => {
|
||||
if (!restaurantId) return;
|
||||
|
||||
let period = 'yearly';
|
||||
switch (timeFrame) {
|
||||
case '24h':
|
||||
period = 'daily';
|
||||
break;
|
||||
case '30d':
|
||||
period = 'monthly';
|
||||
break;
|
||||
case '12m':
|
||||
default:
|
||||
period = 'yearly';
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authService.getOccupancyRate(
|
||||
restaurantId,
|
||||
period,
|
||||
customDate
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
// occupancy_rate جايه كنص مثل "11.83%" → نحولها لرقم
|
||||
const rateValue = parseFloat(response.data.replace('%', '').trim());
|
||||
setOccupancyRate(rateValue);
|
||||
} else {
|
||||
setOccupancyRate(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setOccupancyRate(0);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchOccupancyRate();
|
||||
}, [timeFrame, restaurantId, customDate]);
|
||||
|
||||
return (
|
||||
<Box pr={{ xs: 1, sm: 2 }} maxWidth="100%" overflowX="hidden">
|
||||
{/* Occupancy Card */}
|
||||
<Box>
|
||||
<OrderStatusCard
|
||||
title="Occupancy Rate"
|
||||
period={
|
||||
timeFrame === '24h'
|
||||
? 'Today'
|
||||
: timeFrame === '30d'
|
||||
? 'This Month'
|
||||
: 'This Year'
|
||||
}
|
||||
progress={occupancyRate} // نسبة الإشغال
|
||||
description={`Current occupancy rate is ${occupancyRate}% for selected period.`}
|
||||
timeFrame={timeFrame}
|
||||
setTimeFrame={setTimeFrame}
|
||||
customDate={customDate}
|
||||
setCustomDate={setCustomDate}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsPageOccupancy;
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRestaurant } from '../../../contexts/RestaurantContext';
|
||||
import StatisticsCard from './StatisticsCard';
|
||||
// import TablesManager from './TablesManager';
|
||||
import {
|
||||
Box,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import authService from '../../../services/authService';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const AnalyticsPagebill = () => {
|
||||
const { restaurantId } = useRestaurant();
|
||||
const [timeFrame, setTimeFrame] = useState('12m'); // '12m', '30d', '24h'
|
||||
const [customDate, setCustomDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [chartData, setChartData] = useState([]);
|
||||
|
||||
const handleTimeFrameChange = (newTimeFrame) => {
|
||||
setTimeFrame(newTimeFrame);
|
||||
};
|
||||
|
||||
const handleCustomDateChange = (event) => {
|
||||
setCustomDate(event.target.value);
|
||||
};
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
if (!restaurantId) return;
|
||||
|
||||
let period = 'yearly';
|
||||
let labels = [];
|
||||
let mappedData = [];
|
||||
|
||||
switch (timeFrame) {
|
||||
case '24h': // يومي
|
||||
period = 'daily';
|
||||
labels = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`);
|
||||
try {
|
||||
const response = await authService.getBillStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label || item.date || '',
|
||||
bills_total: item.bills_total || 0,
|
||||
total_price: item.total_price || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
bills_total: 0,
|
||||
total_price: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
bills_total: 0,
|
||||
total_price: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case '30d': // شهري
|
||||
period = 'monthly';
|
||||
labels = Array.from({ length: 30 }, (_, i) => dayjs().startOf('month').add(i, 'day').format('DD'));
|
||||
try {
|
||||
const response = await authService.getBillStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label ? dayjs(item.label).format('DD') : item.date ? dayjs(item.date).format('DD') : '',
|
||||
bills_total: item.bills_total || 0,
|
||||
total_price: item.total_price || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
bills_total: 0,
|
||||
total_price: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
bills_total: 0,
|
||||
total_price: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case '12m': // سنوي
|
||||
default:
|
||||
period = 'yearly';
|
||||
labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
try {
|
||||
const response = await authService.getBillStatistics(restaurantId, period, customDate);
|
||||
if (response.success && response.data.length > 0) {
|
||||
mappedData = response.data.map(item => ({
|
||||
label: item.label || item.date || '',
|
||||
bills_total: item.bills_total || 0,
|
||||
total_price: item.total_price || 0
|
||||
}));
|
||||
} else {
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
bills_total: 0,
|
||||
total_price: 0
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
mappedData = labels.map(label => ({
|
||||
label,
|
||||
bills_total: 0,
|
||||
total_price: 0
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
setChartData(mappedData);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatistics();
|
||||
}, [timeFrame, restaurantId, customDate]);
|
||||
|
||||
return (
|
||||
<Box p={{ xs: 1, sm: 2 }} maxWidth="100%" overflowX="hidden">
|
||||
{/* Header Buttons */}
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
mb={1}
|
||||
>
|
||||
<ButtonGroup
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'inset 0 0 0 1px #e0e0e0',
|
||||
'& .MuiButton-root': {
|
||||
textTransform: 'none',
|
||||
fontSize: '13px',
|
||||
padding: '6px 12px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: '#555',
|
||||
'&:hover': { backgroundColor: '#f5f5f5' }
|
||||
},
|
||||
'& .MuiButton-contained': {
|
||||
backgroundColor: 'rgba(255, 117, 34, 0.08)',
|
||||
color: '#ff5722',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => (
|
||||
<Button
|
||||
key={value}
|
||||
variant={timeFrame === value ? 'contained' : 'text'}
|
||||
onClick={() => handleTimeFrameChange(value)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
{/* Date Picker + Today Button */}
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: 2,
|
||||
p: '4px 8px',
|
||||
boxShadow: 'inset 0 0 0 1px #e0e0e0'
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="date"
|
||||
value={customDate}
|
||||
onChange={handleCustomDateChange}
|
||||
size="small"
|
||||
sx={{
|
||||
width: isMobile ? '100%' : 150,
|
||||
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
|
||||
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setCustomDate(dayjs().format('YYYY-MM-DD'))}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 117, 34, 0.08)',
|
||||
color: '#ff5722',
|
||||
textTransform: 'none',
|
||||
boxShadow: 'none',
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
'&:hover': { backgroundColor: 'rgba(255, 117, 34, 0.15)' }
|
||||
}}
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* StatisticsCard */}
|
||||
<Box>
|
||||
<StatisticsCard
|
||||
title="Analytics Overview"
|
||||
subtitle="Bills Statistics"
|
||||
data={chartData}
|
||||
dataKeys={[
|
||||
{ key: 'bills_total', name: 'Bills', color: '#11e4c1ff' },
|
||||
{ key: 'total_price', name: 'Total price', color: '#fcce01ff' },
|
||||
]}
|
||||
xDataKey="label"
|
||||
valueFormatter={(value) => value}
|
||||
timeFrame={timeFrame}
|
||||
onTimeFrameChange={handleTimeFrameChange}
|
||||
height={420}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsPagebill;
|
||||
@@ -11,23 +11,23 @@ const Dashboard = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [hasProducts, setHasProducts] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
// const [isLoading, setIsLoading] = useState(true);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
|
||||
// محاكاة التحقق من المنتجات
|
||||
|
||||
useEffect(() => {
|
||||
const checkProducts = async () => {
|
||||
setIsLoading(true);
|
||||
const productsExist = await checkIfProductsExist(); // استبدل بمنطقك
|
||||
// setIsLoading(true);
|
||||
const productsExist = await checkIfProductsExist();
|
||||
setHasProducts(productsExist);
|
||||
setIsLoading(false);
|
||||
// setIsLoading(false);
|
||||
};
|
||||
|
||||
checkProducts();
|
||||
}, []);
|
||||
|
||||
const checkIfProductsExist = async () => {
|
||||
return new Promise((resolve) => setTimeout(() => resolve(true), 1500)); // محاكاة تأخير
|
||||
return new Promise((resolve) => setTimeout(() => resolve(true), 1500));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -76,8 +76,8 @@ const Dashboard = () => {
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
width: sidebarOpen ? 'calc(100% - 20px)' : '100%',
|
||||
pt: { xs: 0.5, sm: 1 },
|
||||
mt: { xs: 1, sm: 2 },
|
||||
// pt: { xs: 0.5, sm: 1 },
|
||||
// mt: { xs: 1, sm: 2 },
|
||||
overflowY: 'auto',
|
||||
pl: { xs: 0, sm: 2 },
|
||||
pr: { xs: 1, sm: 2 },
|
||||
@@ -89,15 +89,18 @@ const Dashboard = () => {
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}>
|
||||
{isLoading ? (
|
||||
{/* {isLoading ? (
|
||||
<>
|
||||
<Skeleton variant="rectangular" height={50} sx={{ mb: 2 }} />
|
||||
<Skeleton variant="rectangular" height={200} sx={{ mb: 2 }} />
|
||||
<Skeleton variant="rectangular" height={300} sx={{ mb: 2 }} />
|
||||
</>
|
||||
) : (
|
||||
hasProducts ? <DashbordContect /> : <NoProdectDash />
|
||||
)}
|
||||
hasProducts ?
|
||||
<DashbordContect />
|
||||
: <NoProdectDash />
|
||||
)} */}
|
||||
<DashbordContect />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -7,9 +7,9 @@ import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
|
||||
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import OrderStatusCard from './OrderStatusCard';
|
||||
import StatisticsCard from './StatisticsCard';
|
||||
import RecentActivity from './RecentActivity';
|
||||
import AnalyticsPage from './AnalyticsPage';
|
||||
import AnalyticsPageOccupancy from './AnalyticsPageOccupancy';
|
||||
import AnalyticsPagebill from './AnalyticsPagebill';
|
||||
|
||||
const IconCircle = ({ children, bgColor = '#DEDEFA', outerColor = '#EFEFFD' }) => (
|
||||
<Box
|
||||
@@ -52,111 +52,6 @@ const IconCircle = ({ children, bgColor = '#DEDEFA', outerColor = '#EFEFFD' }) =
|
||||
</Box>
|
||||
);
|
||||
|
||||
const StatusCard = ({
|
||||
icon,
|
||||
statusText,
|
||||
statusColor,
|
||||
iconColor,
|
||||
outerColor,
|
||||
innerColor,
|
||||
height = {
|
||||
xs: 'calc(140px + 0.5vh)',
|
||||
sm: 'calc(150px + 0.5vh)',
|
||||
md: 'calc(162px + 0.5vh)'
|
||||
},
|
||||
width = {
|
||||
xs: 'min(90%, 600px)', // تقليل من 90% إلى 85% والحد الأقصى من 300px إلى 280px
|
||||
sm: 'clamp(220px, 23vw, 280px)', // تقليل جميع القيم
|
||||
md: 'clamp(220px, 19vw, 300px)'
|
||||
},
|
||||
transition = 'width 0.3s ease',
|
||||
iconSpacing = { xs: 6, sm: 8, md: 12 },
|
||||
extraButton,
|
||||
statusButtonWidth,
|
||||
title = 'Point Of Sale',
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
return (
|
||||
<Card sx={{
|
||||
height,
|
||||
width,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
p: 0,
|
||||
pb: 2,
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'none',
|
||||
minWidth: { xs: 180, sm: 220 }
|
||||
}}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: iconSpacing }}>
|
||||
<IconCircle bgColor={innerColor} outerColor={outerColor}>
|
||||
{React.cloneElement(icon, { sx: { color: iconColor, fontSize: { xs: 16, sm: 18, md: 20 } } })}
|
||||
</IconCircle>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: '#E7F4EE',
|
||||
color: statusColor,
|
||||
textTransform: 'none',
|
||||
boxShadow: 'none',
|
||||
borderRadius: '100px',
|
||||
width: statusButtonWidth || 'auto',
|
||||
fontSize: { xs: '0.7rem', sm: '0.8rem', md: '0.9rem' },
|
||||
px: { xs: 1, sm: 1.5 },
|
||||
py: 0.5
|
||||
}}
|
||||
>
|
||||
{statusText}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
gutterBottom
|
||||
mt={1.5}
|
||||
sx={{
|
||||
color: '#667085',
|
||||
fontSize: { xs: '14px', sm: '15px', md: '16px' },
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
|
||||
{extraButton ? extraButton : <Box />}
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: '#F7F7F7',
|
||||
width: { xs: '26px', sm: '28px', md: '30px' },
|
||||
height: { xs: '26px', sm: '28px', md: '30px' },
|
||||
minWidth: '0',
|
||||
padding: 0,
|
||||
borderRadius: '12px',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#e0e0e0',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowOutwardIcon
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
width: { xs: '16px', sm: '18px', md: '20px' },
|
||||
height: { xs: '16px', sm: '18px', md: '20px' },
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const DashboardContect = () => {
|
||||
const theme = useTheme();
|
||||
@@ -172,7 +67,7 @@ const DashboardContect = () => {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
|
||||
mb: 3,
|
||||
mb: 1,
|
||||
pl: 1,
|
||||
pr: { xs: 1, sm: 3 },
|
||||
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
|
||||
@@ -192,7 +87,7 @@ const DashboardContect = () => {
|
||||
</Typography>
|
||||
|
||||
{/* Buttons */}
|
||||
<Box sx={{
|
||||
{/* <Box sx={{
|
||||
display: 'flex',
|
||||
gap: { xs: 1, sm: 2 },
|
||||
flexWrap: { xs: 'wrap', sm: 'nowrap' },
|
||||
@@ -260,11 +155,11 @@ const DashboardContect = () => {
|
||||
>
|
||||
Check Inventory
|
||||
</Button>
|
||||
</Box>
|
||||
</Box> */}
|
||||
</Box>
|
||||
|
||||
{/* Status Cards */}
|
||||
<Box
|
||||
{/* <Box
|
||||
sx={{
|
||||
|
||||
display: 'flex',
|
||||
@@ -333,7 +228,7 @@ const DashboardContect = () => {
|
||||
iconSpacing={{ xs: 3, sm: 5, md: 7 }}
|
||||
title="Delivery"
|
||||
/>
|
||||
</Box>
|
||||
</Box> */}
|
||||
|
||||
{/* OrderStatusCard/ StatisticsCard */}
|
||||
<Box
|
||||
@@ -343,12 +238,8 @@ const DashboardContect = () => {
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: { xs: 1, sm: 1, md: 7 }, // gap مرن
|
||||
mb: 1,
|
||||
pl: 1,
|
||||
pr: 1,
|
||||
mt: 2,
|
||||
mr: { xs: 4, sm: 3, md: 0 },
|
||||
ml: { xs: 2, sm: 0 },
|
||||
mr: 2,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
@@ -357,12 +248,13 @@ const DashboardContect = () => {
|
||||
sx={{
|
||||
flexGrow: 0,
|
||||
width: { xs: '100%', sm: '35%', md: '30%' },
|
||||
minWidth: 220,
|
||||
minWidth: 20,
|
||||
ml: { xs: 0, sm: -1 },
|
||||
flexShrink: 0,
|
||||
mt:8
|
||||
}}
|
||||
>
|
||||
<OrderStatusCard />
|
||||
<AnalyticsPageOccupancy />
|
||||
</Box>
|
||||
|
||||
{/* StatisticsCard */}
|
||||
@@ -370,11 +262,11 @@ const DashboardContect = () => {
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
ml: { xs: 0, sm: 2 },
|
||||
// ml: { xs: 0, sm: 2 },
|
||||
// backgroundColor:'red'
|
||||
}}
|
||||
>
|
||||
<StatisticsCard />
|
||||
<AnalyticsPage />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -382,11 +274,11 @@ const DashboardContect = () => {
|
||||
{/* RecentActivity */}
|
||||
<Box
|
||||
sx={{
|
||||
mr: { xs: 4, sm: 3 ,md:0 },
|
||||
mr: { xs: 4, sm: 3, md: 0 },
|
||||
ml: { xs: 2, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<RecentActivity />
|
||||
<AnalyticsPagebill />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,22 +23,14 @@ const NoProdectDash = () => {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
|
||||
mb: 3,
|
||||
mt:10,
|
||||
pl: 1,
|
||||
pr: { xs: 1, sm: 3 },
|
||||
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
|
||||
gap: { xs: 2, sm: 0 }
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: { xs: '20px', sm: '22px', md: '24px' },
|
||||
color: '#121212'
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</Typography>
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Empty State Content */}
|
||||
@@ -76,7 +68,7 @@ const NoProdectDash = () => {
|
||||
fontSize: { xs: '14px', md: '18px' },
|
||||
mb: 2,
|
||||
color: '#5F6868',
|
||||
whiteSpace: 'pre-line' // هذا يسمح بكسر السطر عند المسافات
|
||||
whiteSpace: 'pre-line' // يسمح بكسر السطر عند المسافات
|
||||
}}
|
||||
>
|
||||
<Box component="span" sx={{ fontWeight: 600, fontSize: '18px' }}>Sorry!</Box>{' '}
|
||||
|
||||
@@ -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' ? <TrendingUpIcon fontSize="small" /> : <TrendingDownIcon fontSize="small" />;
|
||||
const highlightedDescription = description.replace(
|
||||
/(\$\d+(?:\.\d+)?[kKmM]?)/g,
|
||||
'<span style="color:#000;font-weight:500;">$1</span>'
|
||||
);
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
|
||||
const open = Boolean(anchorEl);
|
||||
const handleMenuOpen = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
width: { xs: '96%', sm: '94%', md: 360 },
|
||||
height: '400',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 4px 5px rgba(0, 0, 0, 0.1)',
|
||||
p: 1,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height={isMobile ? '75vh' : isTablet ? '59vh' : 385}>
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
width: { xs: '96%', sm: '94%', md: 360 },
|
||||
height: 'auto',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0px 4px 5px rgba(0, 0, 0, 0.1)',
|
||||
pl: { xs: 1, sm: 1 },
|
||||
pr: { xs: 1, sm: 1 },
|
||||
pb: { xs: 1, sm: 1 },
|
||||
pt: { xs: 0, sm: 0 },
|
||||
|
||||
position: 'relative' // إضافة هذه الخاصية
|
||||
}}
|
||||
{/* زر القائمة العلوية */}
|
||||
<IconButton
|
||||
aria-label="more-actions"
|
||||
onClick={handleMenuOpen}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
color: '#667085'
|
||||
}}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height={isMobile ? '75vh' : isTablet ? '59vh' : 385}>
|
||||
{/* زر الإجراءات في الزاوية العلوية اليمنى */}
|
||||
<IconButton
|
||||
aria-label="more-actions"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
color: '#667085'
|
||||
}}
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* القائمة المنسدلة */}
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<Box sx={{ p: 2, minWidth: 250 }}>
|
||||
{/* Header Buttons (Timeframe) */}
|
||||
<ButtonGroup
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'inset 0 0 0 1px #e0e0e0',
|
||||
mb: 2,
|
||||
width: '100%',
|
||||
'& .MuiButton-root': {
|
||||
textTransform: 'none',
|
||||
fontSize: '13px',
|
||||
padding: '6px 12px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: '#555',
|
||||
'&:hover': { backgroundColor: '#f5f5f5' }
|
||||
},
|
||||
'& .MuiButton-contained': {
|
||||
backgroundColor: 'rgba(255, 117, 34, 0.08)',
|
||||
color: '#ff5722',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{[{ label: '12 Months', value: '12m' }, { label: '30 Days', value: '30d' }, { label: '24 Hours', value: '24h' }].map(({ label, value }) => (
|
||||
<Button
|
||||
key={value}
|
||||
variant={timeFrame === value ? 'contained' : 'text'}
|
||||
onClick={() => { setTimeFrame(value); handleMenuClose(); }}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
<CardContent>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
{/* Date Picker + Today Button */}
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: 2,
|
||||
p: '4px 8px',
|
||||
boxShadow: 'inset 0 0 0 1px #e0e0e0'
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="date"
|
||||
value={customDate}
|
||||
onChange={(e) => setCustomDate(e.target.value)}
|
||||
size="small"
|
||||
sx={{
|
||||
width: isMobile ? '100%' : 150,
|
||||
'& .MuiInputBase-input': { fontSize: isMobile ? 12 : 13, padding: '6px 8px' },
|
||||
'& .MuiOutlinedInput-notchedOutline': { border: 'none' }
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setCustomDate(dayjs().format('YYYY-MM-DD'))}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 117, 34, 0.08)',
|
||||
color: '#ff5722',
|
||||
textTransform: 'none',
|
||||
boxShadow: 'none',
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
'&:hover': { backgroundColor: 'rgba(255, 117, 34, 0.15)' }
|
||||
}}
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu>
|
||||
|
||||
ml: 0 // هامش يسار لإفساح المجال للأيقونة
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontSize: { xs: '16px', sm: '18px' },
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: { xs: '12px', sm: '14px' },
|
||||
fontWeight: 500,
|
||||
color: '#667085'
|
||||
}}
|
||||
>
|
||||
{period}
|
||||
</Typography>
|
||||
</Box>
|
||||
{/* المحتوى الأساسي للكارت */}
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', ml: 0 }}>
|
||||
<Typography variant="h6" sx={{ fontSize: { xs: '16px', sm: '18px' }, fontWeight: 500 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: { xs: '12px', sm: '14px' }, fontWeight: 500, color: '#667085', mb: 3 }}>
|
||||
{period}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Progress Circle */}
|
||||
<Box sx={{ height: 120, mb: 2, position: 'relative' }}>
|
||||
{/* تغيير النسبة تحت الدائرة */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '80%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={progressChange}
|
||||
size="small"
|
||||
icon={progressIcon}
|
||||
sx={{
|
||||
height: 22,
|
||||
fontSize: '0.75rem',
|
||||
color: progressDirection === 'up' ? '#087443' : '#B42318',
|
||||
backgroundColor: progressDirection === 'up' ? '#ECFDF3' : '#FEF3F2',
|
||||
'.MuiChip-icon': {
|
||||
color: progressDirection === 'up' ? '#087443' : '#B42318',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* دائرة التقدم */}
|
||||
<Box sx={{ height: 120, mb: 2, position: 'relative' }}>
|
||||
<Box sx={{ position: 'relative', width: '100%', maxWidth: 350, mx: 'auto' }}>
|
||||
<CircularProgressbar
|
||||
value={100}
|
||||
circleRatio={0.5}
|
||||
strokeWidth={5}
|
||||
styles={buildStyles({
|
||||
rotation: 0.75,
|
||||
strokeLinecap: 'round',
|
||||
trailColor: '#eee',
|
||||
pathColor: '#eee',
|
||||
})}
|
||||
/>
|
||||
<Box sx={{ position: 'absolute', top: -1.5, left: 0, width: '100%', height: '100%' }}>
|
||||
<CircularProgressbar
|
||||
value={progress}
|
||||
circleRatio={0.5}
|
||||
strokeWidth={6}
|
||||
styles={buildStyles({
|
||||
rotation: 0.75,
|
||||
strokeLinecap: 'round',
|
||||
trailColor: 'transparent',
|
||||
pathColor: theme.palette.primary.main,
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* دائرة التقدم */}
|
||||
<Box sx={{ position: 'relative', width: '100%', maxWidth: 250, mx: 'auto' }}>
|
||||
<CircularProgressbar
|
||||
value={100}
|
||||
circleRatio={0.5}
|
||||
strokeWidth={5}
|
||||
styles={buildStyles({
|
||||
rotation: 0.75,
|
||||
strokeLinecap: 'round',
|
||||
trailColor: '#eee',
|
||||
pathColor: '#eee',
|
||||
})}
|
||||
/>
|
||||
<Box sx={{ position: 'absolute', top: -1.5, left: 0, width: '100%', height: '100%' }}>
|
||||
<CircularProgressbar
|
||||
value={progress}
|
||||
circleRatio={0.5}
|
||||
strokeWidth={6}
|
||||
styles={buildStyles({
|
||||
rotation: 0.75,
|
||||
strokeLinecap: 'round',
|
||||
trailColor: 'transparent',
|
||||
pathColor: theme.palette.primary.main,
|
||||
textColor: 'transparent',
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
{/* النص داخل الدائرة */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '36%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
fontSize: { xs: '20px', sm: '26px' },
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{`${progress}%`}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* النص داخل الدائرة */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '22%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
fontSize: { xs: '18px', sm: '24px' },
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{`${progress}%`}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* الوصف */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mb: 1,
|
||||
mt: 5,
|
||||
color: '#667085',
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: highlightedDescription }}
|
||||
/>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{/* الحقول الثلاثة (Completed / Canceled / Pending) */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: { xs: 1, sm: 2 },
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
ml: -1
|
||||
}}
|
||||
>
|
||||
{/* لكل قسم: */}
|
||||
{[{
|
||||
label: 'Completed',
|
||||
icon: <ArrowUpwardIcon color="success" fontSize="small" sx={{ mt: 0.2, ml: -1 }} />,
|
||||
value: completed
|
||||
}, {
|
||||
label: 'Canceled',
|
||||
icon: <ArrowDownwardIcon color="error" fontSize="small" sx={{ mt: 0.6, ml: -1 }} />,
|
||||
value: canceled
|
||||
}, {
|
||||
label: 'Pending',
|
||||
icon: <ArrowDownwardIcon color="warning" fontSize="small" sx={{ mt: 0.6, ml: -1 }} />,
|
||||
value: pending
|
||||
}].map(({ label, icon, value }, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
mb: { xs: 1, sm: 0 }
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: '#667085', fontSize: '12px', fontWeight: 500, mb: 1 }}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{icon}
|
||||
<Typography sx={{ color: '#333843', fontSize: { xs: '14px', sm: '16px',md:'24px'}, fontWeight: 500 }}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
{/* الوصف */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mb: 1,
|
||||
mt: 10,
|
||||
color: '#667085',
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderStatusCard;
|
||||
|
||||
@@ -12,9 +12,6 @@ import {
|
||||
Button,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
// import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||
// import InventoryIcon from '@mui/icons-material/Inventory';
|
||||
// import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||
|
||||
const RecentActivity = () => {
|
||||
// Sample data
|
||||
|
||||
@@ -1,60 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Box, IconButton, Paper, Typography, useTheme, useMediaQuery } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
Box,
|
||||
IconButton,
|
||||
Paper,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
defs,
|
||||
linearGradient,
|
||||
stop
|
||||
CartesianGrid
|
||||
} from 'recharts';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
// import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
const data = [
|
||||
{ month: 'Jan', revenue: 4000, sales: 2400 },
|
||||
{ month: 'Feb', revenue: 3000, sales: 1398 },
|
||||
{ month: 'Mar', revenue: 2000, sales: 9800 },
|
||||
{ month: 'Apr', revenue: 2780, sales: 3908 },
|
||||
{ month: 'May', revenue: 1890, sales: 4800 },
|
||||
{ month: 'Jun', revenue: 2390, sales: 3800 },
|
||||
{ month: 'Jul', revenue: 3490, sales: 4300 },
|
||||
{ month: 'Aug', revenue: 5000, sales: 4000 },
|
||||
{ month: 'Sep', revenue: 4700, sales: 4200 },
|
||||
{ month: 'Oct', revenue: 5200, sales: 4600 },
|
||||
{ month: 'Nov', revenue: 4800, sales: 4400 },
|
||||
{ month: 'Dec', revenue: 5300, sales: 4800 },
|
||||
];
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
if (value >= 1000000) {
|
||||
return `$${(value / 1000000).toFixed(1)}M`;
|
||||
} else if (value >= 1000) {
|
||||
return `$${(value / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return `$${value}`;
|
||||
};
|
||||
|
||||
const StatisticsCard = () => {
|
||||
const StatisticsCard = ({
|
||||
title = "Statistics",
|
||||
subtitle = "Delivery Times",
|
||||
data = [],
|
||||
dataKeys = [
|
||||
{ key: 'revenue', name: 'Revenue', color: '#E46A11' },
|
||||
{ key: 'sales', name: 'Sales', color: '#0182FC' }
|
||||
],
|
||||
xDataKey = 'month',
|
||||
valueFormatter = (value) => value,
|
||||
timeFrame = 'month',
|
||||
onTimeFrameChange,
|
||||
height // 👈 جديد
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
|
||||
const [selectedDate, setSelectedDate] = useState('');
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const handleMenuOpen = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleDateChange = (event) => {
|
||||
setSelectedDate(event.target.value);
|
||||
if (onTimeFrameChange) onTimeFrameChange(timeFrame, event.target.value);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleTodayClick = () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
setSelectedDate(today);
|
||||
if (onTimeFrameChange) onTimeFrameChange(timeFrame, today);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
// حساب العلامات الديناميكية للمحور الشاقولي
|
||||
const ticksArray = (() => {
|
||||
if (!data || data.length === 0) return [];
|
||||
const maxValue = Math.max(
|
||||
...data.flatMap(d => dataKeys.map(k => d[k.key] || 0))
|
||||
);
|
||||
const desiredTicks = 8; // عدد العلامات المطلوب
|
||||
const step = Math.ceil(maxValue / desiredTicks) || 1;
|
||||
const arr = [];
|
||||
for (let i = 0; i <= maxValue; i += step) {
|
||||
arr.push(i);
|
||||
}
|
||||
return arr;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ borderRadius: 2, width: '100%' }}>
|
||||
<Paper sx={{
|
||||
p: { xs: 1.5, sm: 2 },
|
||||
mb: { xs: 2, sm: 3 },
|
||||
mb: { xs: 2, sm: 2 },
|
||||
boxShadow: '0px 4px 5px rgba(0, 0, 0, 0.1)',
|
||||
position: 'relative',
|
||||
borderRadius: 2 ,
|
||||
|
||||
borderRadius: 2,
|
||||
position: 'relative'
|
||||
}}>
|
||||
<IconButton
|
||||
aria-label="more-actions"
|
||||
@@ -64,32 +96,59 @@ const StatisticsCard = () => {
|
||||
right: { xs: 4, sm: 8 },
|
||||
color: '#667085'
|
||||
}}
|
||||
onClick={handleMenuOpen}
|
||||
>
|
||||
<MoreVertIcon fontSize={isMobile ? 'small' : 'medium'} />
|
||||
{/* <MoreVertIcon fontSize={isMobile ? 'small' : 'medium'} /> */}
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<MenuItem>
|
||||
<TextField
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={handleDateChange}
|
||||
size="small"
|
||||
sx={{ width: 150 }}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<Button variant="outlined" size="small" onClick={handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{/* Header */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
mb: { xs: 1, sm: 2 },
|
||||
pr: { xs: 3, sm: 4 }
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 2,
|
||||
flexWrap: 'wrap',
|
||||
gap: 1
|
||||
}}>
|
||||
<Typography variant="h6" sx={{
|
||||
fontSize: { xs: '15px', sm: '18px' },
|
||||
fontWeight: 500
|
||||
}}>
|
||||
Statistics
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
fontSize: { xs: '11px', sm: '14px' },
|
||||
fontWeight: 500,
|
||||
color: '#667085'
|
||||
}}>
|
||||
Delivery Times
|
||||
</Typography>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontSize: { xs: '15px', sm: '18px' }, fontWeight: 500 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: { xs: '11px', sm: '14px' }, fontWeight: 500, color: '#667085' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ResponsiveContainer width="100%" height={isMobile ? 250 : isTablet ? 290 : 295}>
|
||||
{/* Chart */}
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={height || (isMobile ? 350 : isTablet ? 390 : 395)} // 👈 أولوية للـ prop
|
||||
>
|
||||
|
||||
<AreaChart data={data} margin={{
|
||||
top: isMobile ? -30 : -45,
|
||||
right: isMobile ? 15 : 30,
|
||||
@@ -97,95 +156,55 @@ const StatisticsCard = () => {
|
||||
bottom: isMobile ? 10 : 20
|
||||
}}>
|
||||
<defs>
|
||||
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={theme.palette.primary.main} stopOpacity={0.8} />
|
||||
<stop offset="100%" stopColor={theme.palette.primary.main} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorSales" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#81C2FF" stopOpacity={0.8} />
|
||||
<stop offset="100%" stopColor="#81C2FF" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
{dataKeys.map(({ key, color }) => (
|
||||
<linearGradient key={key} id={`color-${key}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
|
||||
<CartesianGrid stroke="#eee" strokeDasharray="0 0" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
dataKey={xDataKey}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tickMargin={isMobile ? 8 : 15}
|
||||
tick={{ fontSize: isMobile ? 11 : 12 }}
|
||||
tick={{
|
||||
fontSize: isMobile ? 11 : 12,
|
||||
angle: -25,
|
||||
textAnchor: 'end'
|
||||
}}
|
||||
interval={0}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={formatCurrency}
|
||||
ticks={ticksArray}
|
||||
tickFormatter={valueFormatter}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tickMargin={isMobile ? 8 : 15}
|
||||
tick={{ fontSize: isMobile ? 11 : 12 }}
|
||||
/>
|
||||
<Tooltip />
|
||||
|
||||
<Tooltip formatter={valueFormatter} />
|
||||
<Legend
|
||||
verticalAlign="top"
|
||||
height={isMobile ? 30 : 36}
|
||||
iconType="circle"
|
||||
iconSize={isMobile ? 8 : 10}
|
||||
wrapperStyle={{
|
||||
paddingBottom: isMobile ? '15px' : '20px'
|
||||
}}
|
||||
/>
|
||||
|
||||
<Area
|
||||
name="Revenue"
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
stroke='#E46A11'
|
||||
strokeWidth={isMobile ? 2 : 3}
|
||||
fill="url(#colorRevenue)"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Sales"
|
||||
dataKey="sales"
|
||||
stroke="#0182FC"
|
||||
strokeWidth={isMobile ? 2 : 3}
|
||||
fill="url(#colorSales)"
|
||||
/>
|
||||
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
stroke={theme.palette.primary.main}
|
||||
strokeWidth={isMobile ? 2 : 3}
|
||||
dot={{
|
||||
r: isMobile ? 4 : 6,
|
||||
fill: theme.palette.primary.main,
|
||||
stroke: '#fff',
|
||||
strokeWidth: 2
|
||||
}}
|
||||
activeDot={{
|
||||
r: isMobile ? 6 : 8,
|
||||
fill: theme.palette.primary.main,
|
||||
stroke: '#fff',
|
||||
strokeWidth: 2
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="sales"
|
||||
stroke="#0182FC"
|
||||
strokeWidth={isMobile ? 2 : 3}
|
||||
dot={{
|
||||
r: isMobile ? 4 : 6,
|
||||
fill: "#0182FC",
|
||||
stroke: '#fff',
|
||||
strokeWidth: 2
|
||||
}}
|
||||
activeDot={{
|
||||
r: isMobile ? 6 : 8,
|
||||
fill: "#0182FC",
|
||||
stroke: '#fff',
|
||||
strokeWidth: 2
|
||||
}}
|
||||
wrapperStyle={{ paddingBottom: isMobile ? '15px' : '20px' }}
|
||||
/>
|
||||
{dataKeys.map(({ key, name, color }) => (
|
||||
<Area
|
||||
key={key}
|
||||
name={name}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={color}
|
||||
strokeWidth={isMobile ? 2 : 3}
|
||||
fill={`url(#color-${key})`}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Paper>
|
||||
@@ -193,4 +212,19 @@ const StatisticsCard = () => {
|
||||
);
|
||||
};
|
||||
|
||||
StatisticsCard.propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
dataKeys: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
})),
|
||||
xDataKey: PropTypes.string,
|
||||
valueFormatter: PropTypes.func,
|
||||
timeFrame: PropTypes.oneOf(['day', 'week', 'month', 'year']),
|
||||
onTimeFrameChange: PropTypes.func
|
||||
};
|
||||
|
||||
export default StatisticsCard;
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery } from '@mui/material';
|
||||
import KitchPlusAppBar from '../AppBar';
|
||||
import Sidebar from '../SideHome';
|
||||
import Waiter from './contcet/waiter';
|
||||
import Cooker from './contcet/cooker';
|
||||
import Accountant from './contcet/Accountant';
|
||||
import { useRestaurant } from '../../../contexts/RestaurantContext';
|
||||
|
||||
const drawerWidth = 230;
|
||||
|
||||
const Employ = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isMdUp = useMediaQuery(theme.breakpoints.up('md'));
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
const { restaurantId } = useRestaurant();
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= theme.breakpoints.values.md) {
|
||||
setSidebarOpen(true);
|
||||
} else {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [theme.breakpoints.values.md]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
backgroundColor: '#F6F6F6',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(!sidebarOpen)}
|
||||
isMobile={isMobile}
|
||||
drawerWidth={drawerWidth}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: sidebarOpen ? `${drawerWidth}px` : 0,
|
||||
md: 0,
|
||||
},
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<KitchPlusAppBar
|
||||
onDrawerToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||
sidebarOpen={sidebarOpen}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
width: sidebarOpen ? 'calc(100% - 40px)' : '100%',
|
||||
pt: { xs: 2, md: 3 },
|
||||
pl: { xs: 1, sm: 3, md: 3 },
|
||||
pb: { xs: 1, md: 4 },
|
||||
pr: {
|
||||
xs: 2,
|
||||
md: 2,
|
||||
},
|
||||
|
||||
overflowY: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
transition: theme.transitions.create(['margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{/* المحتوى: Waiter و Cooker */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
width: { xs: '90%', sm: '95%', md: '100%' },
|
||||
mt: 0,
|
||||
}}
|
||||
>
|
||||
<Waiter restaurantId={restaurantId} />
|
||||
<Cooker restaurantId={restaurantId} />
|
||||
<Accountant restaurantId={restaurantId} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Employ;
|
||||
@@ -0,0 +1,526 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Button,
|
||||
Chip,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@mui/material';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import AddEmployModal from './AddEmployModal';
|
||||
import authService from '../../../../services/authService';
|
||||
import IOSSwitch from './IOSSwitch';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
import EmployeeDetailsModal from './EmployeeDetailsModal';
|
||||
|
||||
const Accountant = ({ restaurantId }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [switchStates, setSwitchStates] = useState({});
|
||||
const [shiftFilter, setShiftFilter] = useState('all');
|
||||
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
|
||||
const [Accountants, setAccountants] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
||||
const [AccountantDetails, setAccountantDetails] = useState(null);
|
||||
|
||||
|
||||
// حالات الحذف
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [selectedAccountant, setSelectedAccountant] = useState(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAccountants = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await authService.getAllAccountants(restaurantId);
|
||||
if (response.accountants) {
|
||||
setAccountants(response.accountants);
|
||||
|
||||
// تهيئة switchStates حسب قيمة active
|
||||
const initialStates = {};
|
||||
response.accountants.forEach(accountants => {
|
||||
initialStates[accountants.id] = accountants.active === 1; // أو Boolean(waiter.active)
|
||||
});
|
||||
setSwitchStates(initialStates);
|
||||
} else {
|
||||
setAccountants([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch waiters:", error);
|
||||
setAccountants([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAccountants();
|
||||
}, [restaurantId]);
|
||||
|
||||
const handleSwitchChange = async (id, email) => {
|
||||
try {
|
||||
const response = await authService.toggleAccountStatus(email, "Accountant");
|
||||
|
||||
if (response.success) {
|
||||
setSwitchStates((prev) => ({
|
||||
...prev,
|
||||
[id]: response.data,
|
||||
}));
|
||||
// alert(response.message);
|
||||
} else {
|
||||
alert("Failed: " + response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleOpenModal = () => setModalOpen(true);
|
||||
const handleCloseModal = () => setModalOpen(false);
|
||||
|
||||
const handleConfirm = async (formData) => {
|
||||
try {
|
||||
const AccountantData = {
|
||||
...formData,
|
||||
restaurant_id: restaurantId,
|
||||
};
|
||||
|
||||
const response = await authService.registerAccountant(AccountantData);
|
||||
|
||||
if (response.success) {
|
||||
alert('Accountant registered successfully!');
|
||||
setModalOpen(false);
|
||||
|
||||
// تحديث الحالة مباشرة بدل إعادة جلب كل البيانات
|
||||
const newAccountant = response.Accountant || AccountantData;
|
||||
// تأكد أن الـ API يعيد الـ Accountant المضاف، أو استخدم البيانات المحلية
|
||||
setAccountants((prev) => [newAccountant, ...prev]);
|
||||
|
||||
} else {
|
||||
if (response.errors) {
|
||||
const errorsList = Object.entries(response.errors)
|
||||
.map(([field, msgs]) => {
|
||||
if (Array.isArray(msgs)) {
|
||||
return `${field}: ${msgs.join(', ')}`;
|
||||
} else {
|
||||
return `${field}: ${msgs}`;
|
||||
}
|
||||
})
|
||||
.join('\n');
|
||||
alert(`Failed to register Accountant due to validation errors:\n${errorsList}`);
|
||||
} else {
|
||||
alert('Failed to register Accountant: ' + response.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('An error occurred: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getShiftChipProps = (shiftType) => {
|
||||
switch (shiftType?.toLowerCase()) {
|
||||
case 'morning':
|
||||
return {
|
||||
label: 'Morning',
|
||||
sx: {
|
||||
backgroundColor: '#E7F4EE',
|
||||
color: '#0D894F',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
case 'evening':
|
||||
return {
|
||||
label: 'Evening',
|
||||
sx: {
|
||||
backgroundColor: '#FDF1E8',
|
||||
color: '#E46A11',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: shiftType,
|
||||
sx: {
|
||||
backgroundColor: '#E0E0E0',
|
||||
color: '#424242',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAccountants =
|
||||
shiftFilter === 'all'
|
||||
? Accountants
|
||||
: Accountants.filter((w) => w.shift_type.toLowerCase() === shiftFilter);
|
||||
|
||||
const displayedAccountants = showAll ? filteredAccountants : filteredAccountants.slice(0, 3);
|
||||
|
||||
const handleFilterClick = (event) => {
|
||||
setFilterAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleFilterClose = () => {
|
||||
setFilterAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleFilterSelect = (value) => {
|
||||
setShiftFilter(value);
|
||||
setFilterAnchorEl(null);
|
||||
};
|
||||
|
||||
// فتح مودال الحذف
|
||||
const handleDeleteClick = (Accountant) => {
|
||||
setSelectedAccountant(Accountant);
|
||||
setConfirmDeleteOpen(true);
|
||||
};
|
||||
|
||||
// تأكيد الحذف
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!selectedAccountant) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const response = await authService.deleteAccountant(selectedAccountant.id);
|
||||
|
||||
// هنا تحقق من وجود message بدل success
|
||||
if (response.message && response.message.toLowerCase().includes("successfully")) {
|
||||
// alert(response.message);
|
||||
setAccountants((prev) => prev.filter((w) => w.id !== selectedAccountant.id));
|
||||
} else {
|
||||
alert("Failed to delete Accountant: " + (response.message || "Unknown error"));
|
||||
}
|
||||
} catch (error) {
|
||||
alert("An error occurred: " + error.message);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setConfirmDeleteOpen(false);
|
||||
setSelectedAccountant(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDetails = async (id) => {
|
||||
try {
|
||||
const response = await authService.getAccountantById(id);
|
||||
if (response.accountant) {
|
||||
setAccountantDetails(response.accountant);
|
||||
setDetailsModalOpen(true);
|
||||
} else {
|
||||
alert("Failed to fetch Accountant details");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '93.5%' },
|
||||
p: { xs: 2, sm: 3 },
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'stretch', sm: 'center' },
|
||||
mb: 2,
|
||||
gap: { xs: 1, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '18px', sm: '20px' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
textAlign: { xs: 'center', sm: 'left' },
|
||||
}}
|
||||
>
|
||||
Accountant
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
}}
|
||||
>
|
||||
{Accountants.length > 3 && (
|
||||
<Button
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
textTransform: 'none',
|
||||
minWidth: 90,
|
||||
}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{showAll ? 'See Less' : 'See All'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: '#667085',
|
||||
borderColor: '#e0e0e0',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
width: { xs: '120px', sm: '120px', md: '99px' },
|
||||
fontSize: { xs: '0', sm: '13px', md: '14px' },
|
||||
fontWeight: 600,
|
||||
p: 0,
|
||||
m: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 'unset',
|
||||
}}
|
||||
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
|
||||
onClick={handleFilterClick}
|
||||
>
|
||||
{isMobile ? '' : 'Filters'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleOpenModal}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
width: { xs: '100%', sm: '135px' },
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: 'unset', sm: '135px' },
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
Add Accountant
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
boxShadow: 'none',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 2,
|
||||
minWidth: 320,
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
sx={{
|
||||
minWidth: { sm: 550, md: 650 },
|
||||
tableLayout: 'auto',
|
||||
}}
|
||||
aria-label="recent activity table"
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
>
|
||||
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
|
||||
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Name
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Email
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 10 } }}>
|
||||
Shift Type
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Actions
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : displayedAccountants.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
|
||||
No activities found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayedAccountants.map((Accountant) => (
|
||||
<TableRow key={Accountant.id}>
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#296adaff",
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
pl: { xs: 1, sm: 6 },
|
||||
wordBreak: "break-word",
|
||||
cursor: "pointer",
|
||||
"&:hover": { color: "#71a5ffff" },
|
||||
}}
|
||||
onClick={() => handleOpenDetails(Accountant.id)}
|
||||
>
|
||||
{Accountant.name}
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '#4F5867',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
pl: { xs: 1, sm: 3 },
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{Accountant.email}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ pl: { xs: 1, sm: 8 } }}>
|
||||
<Chip {...getShiftChipProps(Accountant.shift_type)} />
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: '28%', pl: { xs: 1, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
<IOSSwitch
|
||||
checked={!!switchStates[Accountant.id]}
|
||||
onChange={() => handleSwitchChange(Accountant.id, Accountant.email)}
|
||||
inputProps={{ 'aria-label': 'accountant switch' }}
|
||||
/>
|
||||
<DeleteForeverIcon
|
||||
sx={{ cursor: 'pointer', color: '#e53935' }}
|
||||
onClick={() => handleDeleteClick(Accountant)}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<AddEmployModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onConfirm={handleConfirm}
|
||||
title="Add Accountant"
|
||||
buttonText="Add Accountant"
|
||||
/>
|
||||
|
||||
<Menu
|
||||
anchorEl={filterAnchorEl}
|
||||
open={Boolean(filterAnchorEl)}
|
||||
onClose={handleFilterClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
backgroundColor: 'white',
|
||||
px: 1,
|
||||
position: 'relative',
|
||||
width: '120px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{['all', 'morning', 'evening'].map((shift) => (
|
||||
<MenuItem
|
||||
key={shift}
|
||||
selected={shiftFilter === shift}
|
||||
onClick={() => handleFilterSelect(shift)}
|
||||
sx={{
|
||||
color: shiftFilter === shift ? '#FF914D' : '#4F5867',
|
||||
backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f0f0f0ff',
|
||||
transition: 'background-color 150ms ease-in-out',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{shift.charAt(0).toUpperCase() + shift.slice(1)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
|
||||
{/* مودال تأكيد الحذف */}
|
||||
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Are you sure you want to delete "{selectedAccountant?.name}"? This action cannot be undone.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleDeleteConfirm}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<EmployeeDetailsModal
|
||||
open={detailsModalOpen}
|
||||
onClose={() => setDetailsModalOpen(false)}
|
||||
employee={AccountantDetails}
|
||||
type="Accountant"
|
||||
/>
|
||||
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accountant;
|
||||
@@ -0,0 +1,330 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
useTheme,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import VisibilityOutlined from '@mui/icons-material/VisibilityOutlined';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
|
||||
const AddUserModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title = 'Add User',
|
||||
buttonText = 'Add User',
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [formValues, setFormValues] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
shift_type: '',
|
||||
working_hours: '',
|
||||
monthly_salary: '',
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const handleTogglePassword = () => setShowPassword((prev) => !prev);
|
||||
const handleToggleConfirmPassword = () => setShowConfirmPassword((prev) => !prev);
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
setFormValues((prev) => ({ ...prev, [key]: value }));
|
||||
setErrors((prev) => ({ ...prev, [key]: null })); // مسح الخطأ عند التعديل
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
let tempErrors = {};
|
||||
|
||||
if (!formValues.name.trim()) tempErrors.name = 'Name is required';
|
||||
|
||||
if (!formValues.email.trim()) tempErrors.email = 'Email is required';
|
||||
else {
|
||||
// Regex بسيط للتحقق من صحة الإيميل
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(formValues.email))
|
||||
tempErrors.email = 'Invalid email format';
|
||||
}
|
||||
|
||||
if (!formValues.password) {
|
||||
tempErrors.password = 'Password is required';
|
||||
} else if (formValues.password.length < 6) {
|
||||
tempErrors.password = 'Password must be at least 6 characters long';
|
||||
}
|
||||
|
||||
if (!formValues.password_confirmation)
|
||||
tempErrors.password_confirmation = 'Confirm password is required';
|
||||
|
||||
if (
|
||||
formValues.password &&
|
||||
formValues.password_confirmation &&
|
||||
formValues.password !== formValues.password_confirmation
|
||||
)
|
||||
tempErrors.password_confirmation = 'Passwords do not match';
|
||||
|
||||
if (!formValues.shift_type) tempErrors.shift_type = 'Shift type is required';
|
||||
|
||||
if (!formValues.working_hours) tempErrors.working_hours = 'Working hours is required';
|
||||
|
||||
if (!formValues.monthly_salary) tempErrors.monthly_salary = 'Monthly salary is required';
|
||||
|
||||
setErrors(tempErrors);
|
||||
|
||||
return Object.keys(tempErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (validate()) {
|
||||
onConfirm(formValues);
|
||||
|
||||
// إعادة تعيين الحقول بعد النجاح
|
||||
setFormValues({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
shift_type: '',
|
||||
working_hours: '',
|
||||
monthly_salary: '',
|
||||
});
|
||||
|
||||
setErrors({}); // تصفير الأخطاء أيضًا
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 600, fontSize: '20px' }}>{title}</DialogTitle>
|
||||
<DialogContent
|
||||
dividers
|
||||
sx={{
|
||||
overflow: 'auto', // يسمح بالتمرير
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none', // يخفي الـ scrollbar على Webkit (Chrome, Edge, Safari)
|
||||
},
|
||||
scrollbarWidth: 'none', // يخفي scrollbar على Firefox
|
||||
msOverflowStyle: 'none', // يخفي scrollbar على IE 10+
|
||||
}}
|
||||
>
|
||||
{/* Name */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Name
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="John Doe"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formValues.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
sx={fieldStyle(theme)}
|
||||
error={!!errors.name}
|
||||
helperText={errors.name}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Email */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Email
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="example@example.com"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formValues.email}
|
||||
onChange={(e) => handleChange('email', e.target.value)}
|
||||
sx={fieldStyle(theme)}
|
||||
error={!!errors.email}
|
||||
helperText={errors.email}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Password */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
value={formValues.password}
|
||||
onChange={(e) => handleChange('password', e.target.value)}
|
||||
sx={fieldStyle(theme)}
|
||||
error={!!errors.password}
|
||||
helperText={errors.password}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleTogglePassword} edge="end">
|
||||
{showPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Confirm Password */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Confirm Password
|
||||
</Typography>
|
||||
<TextField
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
placeholder="Confirm password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
autoComplete="new-password"
|
||||
value={formValues.password_confirmation}
|
||||
onChange={(e) => handleChange('password_confirmation', e.target.value)}
|
||||
sx={fieldStyle(theme)}
|
||||
error={!!errors.password_confirmation}
|
||||
helperText={errors.password_confirmation}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleToggleConfirmPassword} edge="end">
|
||||
{showConfirmPassword ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlined />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Shift Type */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Shift Type
|
||||
</Typography>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={formValues.shift_type}
|
||||
onChange={(e) => handleChange('shift_type', e.target.value)}
|
||||
sx={fieldStyle(theme)}
|
||||
error={!!errors.shift_type}
|
||||
helperText={errors.shift_type}
|
||||
placeholder="Select shift type"
|
||||
>
|
||||
<MenuItem value="morning">Morning</MenuItem>
|
||||
<MenuItem value="evening">Evening</MenuItem>
|
||||
</TextField>
|
||||
</Box>
|
||||
|
||||
{/* Working Hours */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Working Hours
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="12"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formValues.working_hours}
|
||||
onChange={(e) => handleChange('working_hours', e.target.value)}
|
||||
sx={fieldStyle(theme)}
|
||||
error={!!errors.working_hours}
|
||||
helperText={errors.working_hours}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Monthly Salary */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2 }}>
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
Monthly Salary
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="500"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={formValues.monthly_salary}
|
||||
onChange={(e) => handleChange('monthly_salary', e.target.value)}
|
||||
sx={fieldStyle(theme)}
|
||||
error={!!errors.monthly_salary}
|
||||
helperText={errors.monthly_salary}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ pr: 3, pb: 2 }}>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
width: '135px',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
width: '135px',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const fieldStyle = (theme) => ({
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& label': { fontWeight: 600, fontSize: '15px', color: 'black' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '8px',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.grey[400],
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
'& input::-ms-reveal, & input::-ms-clear': {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
export default AddUserModal;
|
||||
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
const ConfirmationDialog = ({ open, onClose, onConfirm }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: '525px',
|
||||
height: '300px',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
|
||||
p: 3,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
mb: 3,
|
||||
ml: -5,
|
||||
}}
|
||||
>
|
||||
{/* الدوائر */}
|
||||
<Box sx={{
|
||||
position: 'relative',
|
||||
width: 100,
|
||||
height: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
{/* الدائرة الخلفية */}
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
backgroundColor: '#F5F8FF',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
zIndex: 0,
|
||||
}} />
|
||||
|
||||
{/* الدائرة الأمامية مع الأيقونة */}
|
||||
<Box sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#E9EFFF',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
}}>
|
||||
<img
|
||||
src='/images/createProfile/Successful.png'
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
alt="Success icon"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* العنوان بجانب الدائرة */}
|
||||
<DialogTitle sx={{ p: 0 }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="div"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: '28px',
|
||||
lineHeight: '1.2'
|
||||
}}
|
||||
>
|
||||
Register successful!
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
</Box>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<DialogContentText sx={{
|
||||
textAlign: 'center',
|
||||
mb: 0,
|
||||
fontSize: '16px',
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary
|
||||
}}>
|
||||
Congratulations! you have registered your restaurant successfully on our platform
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: 0,
|
||||
px: 3,
|
||||
pt: 2,
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{/* الزر الأول - Filled */}
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
height: '48px',
|
||||
borderRadius: '50px',
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
|
||||
</DialogActions>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography,
|
||||
Button,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
|
||||
const EmployeeDetailsModal = ({ open, onClose, employee, type }) => {
|
||||
if (!employee) return null;
|
||||
|
||||
// دالة لتحويل الحالة الرقمية إلى نصية
|
||||
const getStatusText = (active) => (active ? "Active" : "Inactive");
|
||||
|
||||
// عنوان المودال حسب نوع الموظف
|
||||
const getTitle = () => {
|
||||
switch (type?.toLowerCase()) {
|
||||
case "waiter":
|
||||
return "Waiter Details";
|
||||
case "cooker":
|
||||
return "Cooker Details";
|
||||
case "accountant":
|
||||
return "Accountant Details";
|
||||
default:
|
||||
return "Employee Details";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{getTitle()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{employee.name && <Typography><b>Name:</b> {employee.name}</Typography>}
|
||||
{employee.email && <Typography><b>Email:</b> {employee.email}</Typography>}
|
||||
{employee.shift_type && <Typography><b>Shift:</b> {employee.shift_type}</Typography>}
|
||||
{employee.working_hours !== undefined && (
|
||||
<Typography><b>Working Hours:</b> {employee.working_hours}</Typography>
|
||||
)}
|
||||
{employee.monthly_salary !== undefined && (
|
||||
<Typography><b>Monthly Salary:</b> {employee.monthly_salary}</Typography>
|
||||
)}
|
||||
{employee.active !== undefined && (
|
||||
<Typography><b>Status:</b> {getStatusText(employee.active)}</Typography>
|
||||
)}
|
||||
{employee.code && <Typography><b>Code:</b> {employee.code}</Typography>}
|
||||
{employee.expires_at && <Typography><b>Expires At:</b> {employee.expires_at}</Typography>}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="outlined"
|
||||
sx={{ textTransform: "none", minWidth: { md: 120 } }}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeDetailsModal;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Switch from '@mui/material/Switch';
|
||||
|
||||
const IOSSwitch = styled((props) => (
|
||||
<Switch focusVisibleClassName=".Mui-focusVisible" disableRipple {...props} />
|
||||
))(({ theme }) => ({
|
||||
width: 42,
|
||||
height: 26,
|
||||
padding: 0,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
padding: 0,
|
||||
margin: 2,
|
||||
transitionDuration: '300ms',
|
||||
'&.Mui-checked': {
|
||||
transform: 'translateX(16px)',
|
||||
color: '#fff',
|
||||
'& + .MuiSwitch-track': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
opacity: 1,
|
||||
border: 0,
|
||||
},
|
||||
'&.Mui-disabled + .MuiSwitch-track': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
'&.Mui-focusVisible .MuiSwitch-thumb': {
|
||||
color: theme.palette.primary.main,
|
||||
border: '6px solid #fff',
|
||||
},
|
||||
'&.Mui-disabled .MuiSwitch-thumb': {
|
||||
color:
|
||||
theme.palette.mode === 'light'
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[600],
|
||||
},
|
||||
'&.Mui-disabled + .MuiSwitch-track': {
|
||||
opacity: theme.palette.mode === 'light' ? 0.7 : 0.3,
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
boxSizing: 'border-box',
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
borderRadius: 26 / 2,
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'light' ? '#E9E9EA' : 'rgba(255,255,255,0.35)',
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create(['background-color'], {
|
||||
duration: 500,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
export default IOSSwitch;
|
||||
@@ -0,0 +1,526 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Button,
|
||||
Chip,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@mui/material';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import AddEmployModal from './AddEmployModal';
|
||||
import authService from '../../../../services/authService';
|
||||
import IOSSwitch from './IOSSwitch';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
import EmployeeDetailsModal from './EmployeeDetailsModal';
|
||||
|
||||
const Cooker = ({ restaurantId }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [switchStates, setSwitchStates] = useState({});
|
||||
const [shiftFilter, setShiftFilter] = useState('all');
|
||||
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
|
||||
const [Cookers, setCookers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
||||
const [CookerDetails, setCookerDetails] = useState(null);
|
||||
|
||||
|
||||
// حالات الحذف
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [selectedCooker, setSelectedCooker] = useState(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// استدعاء API
|
||||
useEffect(() => {
|
||||
const fetchCookers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await authService.getAllCookers(restaurantId);
|
||||
if (response.cookers) {
|
||||
setCookers(response.cookers);
|
||||
|
||||
const initialStates = {};
|
||||
response.cookers.forEach(cookers => {
|
||||
initialStates[cookers.id] = cookers.active === 1; // أو Boolean(waiter.active)
|
||||
});
|
||||
setSwitchStates(initialStates);
|
||||
} else {
|
||||
setCookers([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Cookers:", error);
|
||||
setCookers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCookers();
|
||||
}, [restaurantId]);
|
||||
|
||||
const handleSwitchChange = async (id, email) => {
|
||||
try {
|
||||
const response = await authService.toggleAccountStatus(email, "Cooker");
|
||||
|
||||
if (response.success) {
|
||||
setSwitchStates((prev) => ({
|
||||
...prev,
|
||||
[id]: response.data,
|
||||
}));
|
||||
// alert(response.message);
|
||||
} else {
|
||||
alert("Failed: " + response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenModal = () => setModalOpen(true);
|
||||
const handleCloseModal = () => setModalOpen(false);
|
||||
|
||||
const handleConfirm = async (formData) => {
|
||||
try {
|
||||
const CookerData = {
|
||||
...formData,
|
||||
restaurant_id: restaurantId,
|
||||
};
|
||||
|
||||
const response = await authService.registerCooker(CookerData);
|
||||
|
||||
if (response.success) {
|
||||
alert('Cooker registered successfully!');
|
||||
setModalOpen(false);
|
||||
|
||||
// تحديث الحالة مباشرة بدل إعادة جلب كل البيانات
|
||||
const newCooker = response.Cooker || CookerData;
|
||||
// تأكد أن الـ API يعيد الـ Cooker المضاف، أو استخدم البيانات المحلية
|
||||
setCookers((prev) => [newCooker, ...prev]);
|
||||
|
||||
} else {
|
||||
if (response.errors) {
|
||||
const errorsList = Object.entries(response.errors)
|
||||
.map(([field, msgs]) => {
|
||||
if (Array.isArray(msgs)) {
|
||||
return `${field}: ${msgs.join(', ')}`;
|
||||
} else {
|
||||
return `${field}: ${msgs}`;
|
||||
}
|
||||
})
|
||||
.join('\n');
|
||||
alert(`Failed to register Cooker due to validation errors:\n${errorsList}`);
|
||||
} else {
|
||||
alert('Failed to register Cooker: ' + response.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('An error occurred: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getShiftChipProps = (shiftType) => {
|
||||
switch (shiftType?.toLowerCase()) {
|
||||
case 'morning':
|
||||
return {
|
||||
label: 'Morning',
|
||||
sx: {
|
||||
backgroundColor: '#E7F4EE',
|
||||
color: '#0D894F',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
case 'evening':
|
||||
return {
|
||||
label: 'Evening',
|
||||
sx: {
|
||||
backgroundColor: '#FDF1E8',
|
||||
color: '#E46A11',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: shiftType,
|
||||
sx: {
|
||||
backgroundColor: '#E0E0E0',
|
||||
color: '#424242',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCookers =
|
||||
shiftFilter === 'all'
|
||||
? Cookers
|
||||
: Cookers.filter((w) => w.shift_type.toLowerCase() === shiftFilter);
|
||||
|
||||
const displayedCookers = showAll ? filteredCookers : filteredCookers.slice(0, 3);
|
||||
|
||||
const handleFilterClick = (event) => {
|
||||
setFilterAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleFilterClose = () => {
|
||||
setFilterAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleFilterSelect = (value) => {
|
||||
setShiftFilter(value);
|
||||
setFilterAnchorEl(null);
|
||||
};
|
||||
|
||||
// فتح مودال الحذف
|
||||
const handleDeleteClick = (Cooker) => {
|
||||
setSelectedCooker(Cooker);
|
||||
setConfirmDeleteOpen(true);
|
||||
};
|
||||
|
||||
// تأكيد الحذف
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!selectedCooker) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const response = await authService.deleteCooker(selectedCooker.id);
|
||||
|
||||
// هنا تحقق من وجود message بدل success
|
||||
if (response.message && response.message.toLowerCase().includes("successfully")) {
|
||||
// alert(response.message);
|
||||
setCookers((prev) => prev.filter((w) => w.id !== selectedCooker.id));
|
||||
} else {
|
||||
alert("Failed to delete Cooker: " + (response.message || "Unknown error"));
|
||||
}
|
||||
} catch (error) {
|
||||
alert("An error occurred: " + error.message);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setConfirmDeleteOpen(false);
|
||||
setSelectedCooker(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDetails = async (id) => {
|
||||
try {
|
||||
const response = await authService.getCookerById(id);
|
||||
if (response.cooker) {
|
||||
setCookerDetails(response.cooker);
|
||||
setDetailsModalOpen(true);
|
||||
} else {
|
||||
alert("Failed to fetch Cooker details");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '93.5%' },
|
||||
p: { xs: 2, sm: 3 },
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'stretch', sm: 'center' },
|
||||
mb: 2,
|
||||
gap: { xs: 1, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '18px', sm: '20px' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
textAlign: { xs: 'center', sm: 'left' },
|
||||
}}
|
||||
>
|
||||
Cooker
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
}}
|
||||
>
|
||||
{Cookers.length > 3 && (
|
||||
<Button
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
textTransform: 'none',
|
||||
minWidth: 90,
|
||||
}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{showAll ? 'See Less' : 'See All'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: '#667085',
|
||||
borderColor: '#e0e0e0',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
width: { xs: '120px', sm: '120px', md: '99px' },
|
||||
fontSize: { xs: '0', sm: '13px', md: '14px' },
|
||||
fontWeight: 600,
|
||||
p: 0,
|
||||
m: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 'unset',
|
||||
}}
|
||||
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
|
||||
onClick={handleFilterClick}
|
||||
>
|
||||
{isMobile ? '' : 'Filters'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleOpenModal}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
width: { xs: '100%', sm: '135px' },
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: 'unset', sm: '135px' },
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
Add Cooker
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
boxShadow: 'none',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 2,
|
||||
minWidth: 320,
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
sx={{
|
||||
minWidth: { sm: 550, md: 650 },
|
||||
tableLayout: 'auto',
|
||||
}}
|
||||
aria-label="recent activity table"
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
>
|
||||
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
|
||||
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Name
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Email
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 10 } }}>
|
||||
Shift Type
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Actions
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : displayedCookers.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
|
||||
No activities found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayedCookers.map((Cooker) => (
|
||||
<TableRow key={Cooker.id}>
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#296adaff",
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
pl: { xs: 1, sm: 6 },
|
||||
wordBreak: "break-word",
|
||||
cursor: "pointer",
|
||||
"&:hover": { color: "#71a5ffff" },
|
||||
}}
|
||||
onClick={() => handleOpenDetails(Cooker.id)}
|
||||
>
|
||||
{Cooker.name}
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '#4F5867',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
pl: { xs: 1, sm: 3 },
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{Cooker.email}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ pl: { xs: 1, sm: 8 } }}>
|
||||
<Chip {...getShiftChipProps(Cooker.shift_type)} />
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: '28%', pl: { xs: 1, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
<IOSSwitch
|
||||
checked={!!switchStates[Cooker.id]}
|
||||
onChange={() => handleSwitchChange(Cooker.id, Cooker.email)}
|
||||
inputProps={{ 'aria-label': 'accountant switch' }}
|
||||
/>
|
||||
|
||||
<DeleteForeverIcon
|
||||
sx={{ cursor: 'pointer', color: '#e53935' }}
|
||||
onClick={() => handleDeleteClick(Cooker)}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<AddEmployModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onConfirm={handleConfirm}
|
||||
title="Add Cooker"
|
||||
buttonText="Add Cooker"
|
||||
/>
|
||||
|
||||
<Menu
|
||||
anchorEl={filterAnchorEl}
|
||||
open={Boolean(filterAnchorEl)}
|
||||
onClose={handleFilterClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
backgroundColor: 'white',
|
||||
px: 1,
|
||||
position: 'relative',
|
||||
width: '120px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{['all', 'morning', 'evening'].map((shift) => (
|
||||
<MenuItem
|
||||
key={shift}
|
||||
selected={shiftFilter === shift}
|
||||
onClick={() => handleFilterSelect(shift)}
|
||||
sx={{
|
||||
color: shiftFilter === shift ? '#FF914D' : '#4F5867',
|
||||
backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f0f0f0ff',
|
||||
transition: 'background-color 150ms ease-in-out',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{shift.charAt(0).toUpperCase() + shift.slice(1)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
|
||||
{/* مودال تأكيد الحذف */}
|
||||
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Are you sure you want to delete "{selectedCooker?.name}"? This action cannot be undone.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleDeleteConfirm}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<EmployeeDetailsModal
|
||||
open={detailsModalOpen}
|
||||
onClose={() => setDetailsModalOpen(false)}
|
||||
employee={CookerDetails}
|
||||
type="Cooker"
|
||||
/>
|
||||
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cooker;
|
||||
@@ -0,0 +1,519 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Button,
|
||||
Chip,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@mui/material';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import AddEmployModal from './AddEmployModal';
|
||||
import authService from '../../../../services/authService';
|
||||
import IOSSwitch from './IOSSwitch';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
import EmployeeDetailsModal from './EmployeeDetailsModal';
|
||||
|
||||
const Waiter = ({ restaurantId }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [switchStates, setSwitchStates] = useState({});
|
||||
const [shiftFilter, setShiftFilter] = useState('all');
|
||||
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
|
||||
const [waiters, setWaiters] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
||||
const [waiterDetails, setWaiterDetails] = useState(null);
|
||||
|
||||
|
||||
// حالات الحذف
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [selectedWaiter, setSelectedWaiter] = useState(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// استدعاء API
|
||||
useEffect(() => {
|
||||
const fetchWaiters = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await authService.getAllWaiters(restaurantId);
|
||||
if (response.waiters) {
|
||||
setWaiters(response.waiters);
|
||||
|
||||
// تهيئة switchStates حسب قيمة active
|
||||
const initialStates = {};
|
||||
response.waiters.forEach(waiter => {
|
||||
initialStates[waiter.id] = waiter.active === 1; // أو Boolean(waiter.active)
|
||||
});
|
||||
setSwitchStates(initialStates);
|
||||
} else {
|
||||
setWaiters([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch waiters:", error);
|
||||
setWaiters([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchWaiters();
|
||||
}, [restaurantId]);
|
||||
|
||||
|
||||
const handleSwitchChange = async (id, email) => {
|
||||
try {
|
||||
const response = await authService.toggleAccountStatus(email, "Waiter");
|
||||
|
||||
if (response.success) {
|
||||
setSwitchStates((prev) => ({
|
||||
...prev,
|
||||
[id]: response.data,
|
||||
}));
|
||||
// alert(response.message);
|
||||
} else {
|
||||
alert("Failed: " + response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleOpenModal = () => setModalOpen(true);
|
||||
const handleCloseModal = () => setModalOpen(false);
|
||||
|
||||
const handleConfirm = async (formData) => {
|
||||
try {
|
||||
const waiterData = {
|
||||
...formData,
|
||||
restaurant_id: restaurantId,
|
||||
};
|
||||
|
||||
const response = await authService.registerWaiter(waiterData);
|
||||
|
||||
if (response.success) {
|
||||
alert('Waiter registered successfully!');
|
||||
setModalOpen(false);
|
||||
// إعادة التحميل بعد الإضافة
|
||||
const updated = await authService.getAllWaiters(restaurantId);
|
||||
if (updated.waiters) setWaiters(updated.waiters);
|
||||
} else {
|
||||
if (response.errors) {
|
||||
const errorsList = Object.entries(response.errors)
|
||||
.map(([field, msgs]) => `${field}: ${msgs.join(', ')}`)
|
||||
.join('\n');
|
||||
alert(`Failed to register waiter due to validation errors:\n${errorsList}`);
|
||||
} else {
|
||||
alert('Failed to register waiter: ' + response.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('An error occurred: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const getShiftChipProps = (shiftType) => {
|
||||
switch (shiftType?.toLowerCase()) {
|
||||
case 'morning':
|
||||
return {
|
||||
label: 'Morning',
|
||||
sx: {
|
||||
backgroundColor: '#E7F4EE',
|
||||
color: '#0D894F',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
case 'evening':
|
||||
return {
|
||||
label: 'Evening',
|
||||
sx: {
|
||||
backgroundColor: '#FDF1E8',
|
||||
color: '#E46A11',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: shiftType,
|
||||
sx: {
|
||||
backgroundColor: '#E0E0E0',
|
||||
color: '#424242',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
minWidth: 80,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const filteredWaiters =
|
||||
shiftFilter === 'all'
|
||||
? waiters
|
||||
: waiters.filter((w) => w.shift_type.toLowerCase() === shiftFilter);
|
||||
|
||||
const displayedWaiters = showAll ? filteredWaiters : filteredWaiters.slice(0, 3);
|
||||
|
||||
const handleFilterClick = (event) => {
|
||||
setFilterAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleFilterClose = () => {
|
||||
setFilterAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleFilterSelect = (value) => {
|
||||
setShiftFilter(value);
|
||||
setFilterAnchorEl(null);
|
||||
};
|
||||
|
||||
// فتح مودال الحذف
|
||||
const handleDeleteClick = (waiter) => {
|
||||
setSelectedWaiter(waiter);
|
||||
setConfirmDeleteOpen(true);
|
||||
};
|
||||
|
||||
// تأكيد الحذف
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!selectedWaiter) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const response = await authService.deleteWaiter(selectedWaiter.id);
|
||||
|
||||
// هنا تحقق من وجود message بدل success
|
||||
if (response.message && response.message.toLowerCase().includes("successfully")) {
|
||||
// alert(response.message);
|
||||
setWaiters((prev) => prev.filter((w) => w.id !== selectedWaiter.id));
|
||||
} else {
|
||||
alert("Failed to delete waiter: " + (response.message || "Unknown error"));
|
||||
}
|
||||
} catch (error) {
|
||||
alert("An error occurred: " + error.message);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setConfirmDeleteOpen(false);
|
||||
setSelectedWaiter(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDetails = async (id) => {
|
||||
try {
|
||||
const response = await authService.getWaiterById(id);
|
||||
if (response.waiter) {
|
||||
setWaiterDetails(response.waiter);
|
||||
setDetailsModalOpen(true);
|
||||
} else {
|
||||
alert("Failed to fetch waiter details");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '93.5%' },
|
||||
p: { xs: 2, sm: 3 },
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'stretch', sm: 'center' },
|
||||
mb: 2,
|
||||
gap: { xs: 1, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '18px', sm: '20px' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
textAlign: { xs: 'center', sm: 'left' },
|
||||
}}
|
||||
>
|
||||
Waiter
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
}}
|
||||
>
|
||||
{waiters.length > 3 && (
|
||||
<Button
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
textTransform: 'none',
|
||||
minWidth: 90,
|
||||
}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{showAll ? 'See Less' : 'See All'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: '#667085',
|
||||
borderColor: '#e0e0e0',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
width: { xs: '120px', sm: '120px', md: '99px' },
|
||||
fontSize: { xs: '0', sm: '13px', md: '14px' },
|
||||
fontWeight: 600,
|
||||
p: 0,
|
||||
m: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 'unset',
|
||||
}}
|
||||
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
|
||||
onClick={handleFilterClick}
|
||||
>
|
||||
{isMobile ? '' : 'Filters'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleOpenModal}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
width: { xs: '100%', sm: '135px' },
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: 'unset', sm: '135px' },
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
Add Waiter
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
boxShadow: 'none',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 2,
|
||||
minWidth: 320,
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
sx={{
|
||||
minWidth: { sm: 550, md: 650 },
|
||||
tableLayout: 'auto',
|
||||
}}
|
||||
aria-label="recent activity table"
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
>
|
||||
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
|
||||
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Name
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Email
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 10 } }}>
|
||||
Shift Type
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
||||
Actions
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : displayedWaiters.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
|
||||
No activities found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayedWaiters.map((waiter) => (
|
||||
<TableRow key={waiter.id}>
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#296adaff",
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
pl: { xs: 1, sm: 6 },
|
||||
wordBreak: "break-word",
|
||||
cursor: "pointer",
|
||||
"&:hover": { color: "#71a5ffff" },
|
||||
}}
|
||||
onClick={() => handleOpenDetails(waiter.id)}
|
||||
>
|
||||
{waiter.name}
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '#4F5867',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
pl: { xs: 1, sm: 3 },
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{waiter.email}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ pl: { xs: 1, sm: 8 } }}>
|
||||
<Chip {...getShiftChipProps(waiter.shift_type)} />
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: '28%', pl: { xs: 1, sm: 8 } }}>
|
||||
<Box sx={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
<IOSSwitch
|
||||
checked={!!switchStates[waiter.id]}
|
||||
onChange={() => handleSwitchChange(waiter.id, waiter.email)}
|
||||
inputProps={{ 'aria-label': 'accountant switch' }}
|
||||
/>
|
||||
|
||||
<DeleteForeverIcon
|
||||
sx={{ cursor: 'pointer', color: '#e53935' }}
|
||||
onClick={() => handleDeleteClick(waiter)}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<AddEmployModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onConfirm={handleConfirm}
|
||||
title="Add Waiter"
|
||||
buttonText="Add Waiter"
|
||||
/>
|
||||
|
||||
<Menu
|
||||
anchorEl={filterAnchorEl}
|
||||
open={Boolean(filterAnchorEl)}
|
||||
onClose={handleFilterClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
backgroundColor: 'white',
|
||||
px: 1,
|
||||
position: 'relative',
|
||||
width: '120px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{['all', 'morning', 'evening'].map((shift) => (
|
||||
<MenuItem
|
||||
key={shift}
|
||||
selected={shiftFilter === shift}
|
||||
onClick={() => handleFilterSelect(shift)}
|
||||
sx={{
|
||||
color: shiftFilter === shift ? '#FF914D' : '#4F5867',
|
||||
backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f0f0f0ff',
|
||||
transition: 'background-color 150ms ease-in-out',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{shift.charAt(0).toUpperCase() + shift.slice(1)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
|
||||
{/* مودال تأكيد الحذف */}
|
||||
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Are you sure you want to delete "{selectedWaiter?.name}"? This action cannot be undone.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleDeleteConfirm}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<EmployeeDetailsModal
|
||||
open={detailsModalOpen}
|
||||
onClose={() => setDetailsModalOpen(false)}
|
||||
employee={waiterDetails} // بدل waiter
|
||||
type="waiter"
|
||||
/>
|
||||
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Waiter;
|
||||
@@ -1,103 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery, Typography } from '@mui/material';
|
||||
import KitchPlusAppBar from '../AppBar';
|
||||
import Sidebar from '../SideHome';
|
||||
|
||||
const drawerWidth = 230;
|
||||
|
||||
const HostKitchen = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [hasProducts, setHasProducts] = useState(false); // حالة لتتبع وجود المنتجات
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const checkProducts = async () => {
|
||||
|
||||
const productsExist = await checkIfProductsExist();
|
||||
setHasProducts(productsExist);
|
||||
};
|
||||
|
||||
checkProducts();
|
||||
}, []);
|
||||
|
||||
// دالة مساعدة لمحاكاة التحقق من المنتجات (استبدلها بمنطقك الفعلي)
|
||||
const checkIfProductsExist = async () => {
|
||||
// محاكاة - يمكن أن يكون هذا استدعاء لـ API أو تحقق من state
|
||||
// return true;
|
||||
return false; // غير هذه القيمة حسب منطقك
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth >= theme.breakpoints.values.md) {
|
||||
setSidebarOpen(true);
|
||||
} else {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
}, [theme.breakpoints.values.md]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= theme.breakpoints.values.md) {
|
||||
setSidebarOpen(true);
|
||||
} else {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [theme.breakpoints.values.md]);
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
backgroundColor: '#F6F6F6',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
isMobile={isMobile}
|
||||
drawerWidth={drawerWidth}
|
||||
/>
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: {
|
||||
xs: '100%',
|
||||
sm: '100%',
|
||||
md: '100%'
|
||||
},
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: sidebarOpen ? `${drawerWidth}px` : 0,
|
||||
md: 0
|
||||
},
|
||||
transition: theme.transitions.create(['width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}>
|
||||
<KitchPlusAppBar
|
||||
onDrawerToggle={handleDrawerToggle}
|
||||
sidebarOpen={sidebarOpen}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<Typography>HostKitchen</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostKitchen;
|
||||
@@ -1,102 +1,139 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery, Typography } from '@mui/material';
|
||||
import { Box, useTheme, useMediaQuery } from '@mui/material';
|
||||
import KitchPlusAppBar from '../AppBar';
|
||||
import Sidebar from '../SideHome';
|
||||
import ProductEntry from './contect/ProductEntry';
|
||||
import TableView from './contect/TableView';
|
||||
|
||||
const drawerWidth = 230;
|
||||
|
||||
const initialProducts = [
|
||||
{
|
||||
product: 'Apple Watch',
|
||||
sales: 150,
|
||||
amount: 45000,
|
||||
price: 299,
|
||||
status: 'Published',
|
||||
expiration: '30 Days Left',
|
||||
},
|
||||
{
|
||||
product: 'Samsung Galaxy',
|
||||
sales: 90,
|
||||
amount: 36000,
|
||||
price: 400,
|
||||
status: 'Low Stock',
|
||||
expiration: '12 Days Left',
|
||||
},
|
||||
{
|
||||
product: 'Sony Headphones',
|
||||
sales: 60,
|
||||
amount: 18000,
|
||||
price: 299,
|
||||
status: 'Draft',
|
||||
expiration: '5 Days Left',
|
||||
},
|
||||
];
|
||||
|
||||
const Inventory = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [hasProducts, setHasProducts] = useState(false); // حالة لتتبع وجود المنتجات
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
|
||||
// محاكاة للتحقق من وجود المنتجات (استبدل هذا بمنطقك الفعلي)
|
||||
useEffect(() => {
|
||||
// هنا يجب استبدال هذا بمنطق فعلي للتحقق من وجود المنتجات
|
||||
// مثلاً استدعاء API أو التحقق من state
|
||||
const checkProducts = async () => {
|
||||
// محاكاة لاستدعاء API
|
||||
const productsExist = await checkIfProductsExist(); // استبدل هذه الدالة بمنطقك الفعلي
|
||||
setHasProducts(productsExist);
|
||||
const [products, setProducts] = useState(initialProducts);
|
||||
const [showProductEntry, setShowProductEntry] = useState(true);
|
||||
|
||||
const addProduct = (newProduct) => {
|
||||
const formattedProduct = {
|
||||
product: newProduct.fullName || 'Unnamed Product',
|
||||
sales: newProduct.stocksLevel ? Number(newProduct.stocksLevel) : 0,
|
||||
amount: newProduct.productPrice
|
||||
? Number(newProduct.productPrice) * (newProduct.stocksLevel || 1)
|
||||
: 0,
|
||||
price: newProduct.productPrice ? Number(newProduct.productPrice) : 0,
|
||||
status: newProduct.consumptionStatus ? 'Published' : 'Draft',
|
||||
expiration: newProduct.expirationDate
|
||||
? `${Math.ceil(
|
||||
(new Date(newProduct.expirationDate) - new Date()) /
|
||||
(1000 * 60 * 60 * 24)
|
||||
)} Days Left`
|
||||
: 'N/A',
|
||||
};
|
||||
|
||||
checkProducts();
|
||||
}, []);
|
||||
setProducts((prev) => [...prev, formattedProduct]);
|
||||
|
||||
// دالة مساعدة لمحاكاة التحقق من المنتجات (استبدلها بمنطقك الفعلي)
|
||||
const checkIfProductsExist = async () => {
|
||||
// محاكاة - يمكن أن يكون هذا استدعاء لـ API أو تحقق من state
|
||||
// return true;
|
||||
return false; // غير هذه القيمة حسب منطقك
|
||||
setShowProductEntry(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth >= theme.breakpoints.values.md) {
|
||||
setSidebarOpen(true);
|
||||
} else {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
}, [theme.breakpoints.values.md]);
|
||||
const handleAddNewProduct = () => {
|
||||
setShowProductEntry(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= theme.breakpoints.values.md) {
|
||||
setSidebarOpen(true);
|
||||
} else {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [theme.breakpoints.values.md]);
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
const handleShowTable = () => {
|
||||
setShowProductEntry(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
backgroundColor: '#F6F6F6',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
backgroundColor: '#F6F6F6',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
onClose={() => setSidebarOpen(!sidebarOpen)}
|
||||
isMobile={isMobile}
|
||||
drawerWidth={drawerWidth}
|
||||
/>
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: {
|
||||
xs: '100%',
|
||||
sm: '100%',
|
||||
md: '100%'
|
||||
},
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: sidebarOpen ? `${drawerWidth}px` : 0,
|
||||
md: 0
|
||||
},
|
||||
transition: theme.transitions.create(['width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: sidebarOpen ? `${drawerWidth}px` : 0,
|
||||
md: 0,
|
||||
},
|
||||
transition: theme.transitions.create(['width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<KitchPlusAppBar
|
||||
onDrawerToggle={handleDrawerToggle}
|
||||
onDrawerToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||
sidebarOpen={sidebarOpen}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<Typography>Inventory</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
width: sidebarOpen ? 'calc(100% - 40px)' : '100%',
|
||||
pt: { xs: 2, sm: 3 },
|
||||
overflowY: 'auto',
|
||||
pl: { xs: 2, sm: 3 },
|
||||
pb: { xs: 2, sm: 4 },
|
||||
pr: { xs: 2, sm: 3 },
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
transition: theme.transitions.create(['margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{showProductEntry ? (
|
||||
<ProductEntry onAdd={addProduct} onShowTable={handleShowTable} />
|
||||
) : (
|
||||
<TableView data={products} onAddNewProduct={handleAddNewProduct} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
import TableView from './TableView';
|
||||
|
||||
const initialProducts = [
|
||||
{
|
||||
product: 'Apple Watch',
|
||||
sales: 150,
|
||||
amount: 45000,
|
||||
price: 299,
|
||||
status: 'Published',
|
||||
expiration: '30 Days Left',
|
||||
},
|
||||
{
|
||||
product: 'Samsung Galaxy',
|
||||
sales: 90,
|
||||
amount: 36000,
|
||||
price: 400,
|
||||
status: 'Low Stock',
|
||||
expiration: '12 Days Left',
|
||||
},
|
||||
{
|
||||
product: 'Sony Headphones',
|
||||
sales: 60,
|
||||
amount: 18000,
|
||||
price: 299,
|
||||
status: 'Draft',
|
||||
expiration: '5 Days Left',
|
||||
},
|
||||
];
|
||||
|
||||
const InventoryTablePage = () => {
|
||||
const [products, setProducts] = useState(initialProducts);
|
||||
|
||||
const handleAddNewProduct = () => {
|
||||
alert('Navigate to Add Product page or open form here');
|
||||
};
|
||||
|
||||
return <TableView data={products} onAddNewProduct={handleAddNewProduct} />;
|
||||
};
|
||||
|
||||
export default InventoryTablePage;
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
|
||||
const NoTraining = () => {
|
||||
const NoProdectDash = () => {
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
@@ -23,24 +23,15 @@ const NoTraining = () => {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
|
||||
mb: 3,
|
||||
mt:10,
|
||||
pl: 1,
|
||||
pr: { xs: 1, sm: 3 },
|
||||
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
|
||||
gap: { xs: 2, sm: 0 }
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
fontSize: { xs: '20px', sm: '22px', md: '24px' },
|
||||
color: '#121212'
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
{/* Empty State Content */}
|
||||
<Card
|
||||
sx={{
|
||||
@@ -80,16 +71,31 @@ const NoTraining = () => {
|
||||
}}
|
||||
>
|
||||
<Box component="span" sx={{ fontWeight: 600, fontSize: '18px' }}>Sorry!</Box>{' '}
|
||||
There are no updates now..
|
||||
{'\n'}check back later.
|
||||
There is no campaign available.
|
||||
Please initiate the {'\n'}creation of a new campaign.
|
||||
</Typography>
|
||||
|
||||
{/* Action Button */}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
borderRadius: 2,
|
||||
color:'white',
|
||||
width:179
|
||||
}}
|
||||
>
|
||||
Add a Product
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoTraining;
|
||||
export default NoProdectDash;
|
||||
@@ -0,0 +1,434 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
styled,
|
||||
useTheme,
|
||||
MenuItem,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio as MuiRadio
|
||||
} from '@mui/material';
|
||||
import axios from 'axios';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import TableView from './TableView';
|
||||
|
||||
const ProductEntry = ({ onAdd, onShowTable }) => {
|
||||
const theme = useTheme();
|
||||
const [profileImage, setProfileImage] = useState(null);
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [bio, setBio] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [selectedBranch, setSelectedBranch] = useState('default');
|
||||
const [branches, setBranches] = useState([
|
||||
{ label: 'Damascus Branch', value: 'damascus' },
|
||||
{ label: 'Aleppo Branch', value: 'aleppo' },
|
||||
{ label: 'Homs Branch', value: 'homs' }
|
||||
]);
|
||||
const [expirationDate, setExpirationDate] = useState(null);
|
||||
const [consumptionStatus, setConsumptionStatus] = useState(false);
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
const [products, setProducts] = useState([]);
|
||||
|
||||
// حالات جديدة للحفظ
|
||||
const [stocksLevel, setStocksLevel] = useState('');
|
||||
const [productCategory, setProductCategory] = useState('default');
|
||||
const [supplierName, setSupplierName] = useState('');
|
||||
const [supplierNumber, setSupplierNumber] = useState('');
|
||||
const [productPrice, setProductPrice] = useState('');
|
||||
const [phoneNumber, setPhoneNumber] = useState('');
|
||||
const [productCost, setProductCost] = useState('');
|
||||
|
||||
// قائمة فئات المنتجات
|
||||
const productCategories = [
|
||||
{ label: 'Food', value: 'food' },
|
||||
{ label: 'Beverages', value: 'beverages' },
|
||||
{ label: 'Electronics', value: 'electronics' },
|
||||
{ label: 'Clothing', value: 'clothing' },
|
||||
];
|
||||
const handleShowTable = () => {
|
||||
setShowTable(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
axios.get('https://mocki.io/v1/0b9d4121-d7e7-42f4-b1d5-b14f82e35b69')
|
||||
.then((response) => {
|
||||
setBranches(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching branches:', error);
|
||||
});
|
||||
|
||||
const savedData = localStorage.getItem('productEntryData');
|
||||
if (savedData) {
|
||||
const data = JSON.parse(savedData);
|
||||
setFullName(data.fullName || '');
|
||||
setUsername(data.username || '');
|
||||
setEmail(data.email || '');
|
||||
setSelectedBranch(data.selectedBranch || 'default');
|
||||
setExpirationDate(data.expirationDate ? new Date(data.expirationDate) : null);
|
||||
setConsumptionStatus(data.consumptionStatus || false);
|
||||
setStocksLevel(data.stocksLevel || '');
|
||||
setProductCategory(data.productCategory || 'default');
|
||||
setSupplierName(data.supplierName || '');
|
||||
setSupplierNumber(data.supplierNumber || '');
|
||||
setProductPrice(data.productPrice || '');
|
||||
setPhoneNumber(data.phoneNumber || '');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const dataToSave = {
|
||||
fullName,
|
||||
username,
|
||||
email,
|
||||
selectedBranch,
|
||||
expirationDate: expirationDate ? expirationDate.toISOString() : null,
|
||||
consumptionStatus,
|
||||
stocksLevel,
|
||||
productCategory,
|
||||
supplierName,
|
||||
supplierNumber,
|
||||
productPrice,
|
||||
phoneNumber,
|
||||
};
|
||||
localStorage.setItem('productEntryData', JSON.stringify(dataToSave));
|
||||
}, [
|
||||
fullName,
|
||||
username,
|
||||
email,
|
||||
selectedBranch,
|
||||
expirationDate,
|
||||
consumptionStatus,
|
||||
stocksLevel,
|
||||
productCategory,
|
||||
supplierName,
|
||||
supplierNumber,
|
||||
productPrice,
|
||||
phoneNumber,
|
||||
]);
|
||||
|
||||
const handleImageUpload = (event) => {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
setProfileImage(URL.createObjectURL(event.target.files[0]));
|
||||
}
|
||||
};
|
||||
|
||||
const Input = styled('input')({
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
const customInputStyle = {
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused': {
|
||||
borderColor: '#3f51b5',
|
||||
boxShadow: '0 0 0 2px rgba(63,81,181,0.1)',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
const renderField = (label, value, onChange, type = 'text', placeholder = '') => (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: { xs: '100%', sm: '45%' },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={customInputStyle}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderSelectField = (label, value, onChange, options = []) => (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: { xs: '100%', sm: '45%' },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: '500', fontSize: '16px' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<TextField
|
||||
select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
sx={customInputStyle}
|
||||
>
|
||||
<MenuItem value="default" disabled sx={{ color: '#9e9e9e' }}>
|
||||
Select {label}...
|
||||
</MenuItem>
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
// احصل على أكبر رقم منتج موجود في القائمة
|
||||
const maxProductNumber = products.length > 0
|
||||
? Math.max(...products.map(p => Number(p.username) || 0))
|
||||
: 0;
|
||||
|
||||
const newProductNumber = maxProductNumber + 1;
|
||||
|
||||
const newProduct = {
|
||||
fullName,
|
||||
username: newProductNumber.toString(), // هنا رقم المنتج يتزاد تلقائياً
|
||||
email,
|
||||
selectedBranch,
|
||||
expirationDate: expirationDate ? expirationDate.toISOString().split('T')[0] : '',
|
||||
consumptionStatus,
|
||||
stocksLevel,
|
||||
productCategory,
|
||||
supplierName,
|
||||
supplierNumber,
|
||||
productPrice,
|
||||
phoneNumber,
|
||||
productCost,
|
||||
};
|
||||
|
||||
// أضف المنتج الجديد لقائمة المنتجات
|
||||
setProducts(prev => [...prev, newProduct]);
|
||||
|
||||
// استدعي دالة onAdd إن كانت موجودة
|
||||
if (onAdd) {
|
||||
onAdd(newProduct);
|
||||
}
|
||||
|
||||
// إعادة تعيين الحقول (يمكنك إزالة إعادة تعيين username لأنه أصبح تلقائي)
|
||||
setFullName('');
|
||||
setEmail('');
|
||||
setSelectedBranch('default');
|
||||
setExpirationDate(null);
|
||||
setConsumptionStatus(false);
|
||||
setStocksLevel('');
|
||||
setProductCategory('default');
|
||||
setSupplierName('');
|
||||
setSupplierNumber('');
|
||||
setProductPrice('');
|
||||
setPhoneNumber('');
|
||||
setProductCost('');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: { xs: 2, sm: 3 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
maxWidth: { xs: '90%', md: '100%' },
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{/* الحقول النصية */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{/* السطر الأول: Product Name + Barcode Maker */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
|
||||
{renderField('Product Name', fullName, (e) => setFullName(e.target.value), 'text', 'Pizza')}
|
||||
{renderField('Barcode Maker', email, (e) => setEmail(e.target.value), 'text', '12344')}
|
||||
</Box>
|
||||
|
||||
{/* السطر الثاني: Branch Select + Product Number */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
|
||||
{renderField('Product Number', username, (e) => setUsername(e.target.value), 'text', '#123')}
|
||||
{renderSelectField('Branch Select', selectedBranch, (e) => setSelectedBranch(e.target.value), branches)}
|
||||
</Box>
|
||||
|
||||
{/* السطر الثالث: Expiration Date + Consumption Status */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: { xs: '100%', sm: '45%' },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: '500', fontSize: '16px', mb: 1 }}
|
||||
>
|
||||
Expiration Date
|
||||
</Typography>
|
||||
<DatePicker
|
||||
value={expirationDate}
|
||||
onChange={(newValue) => setExpirationDate(newValue)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '2px !important',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: '0 0 0 2px rgba(255, 145, 77, 0.2)',
|
||||
},
|
||||
},
|
||||
'& fieldset': {
|
||||
borderRadius: '20px !important',
|
||||
},
|
||||
flex: 1,
|
||||
minWidth: { xs: '100%', sm: '45%' },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</LocalizationProvider>
|
||||
|
||||
<Box sx={{ flex: 1, minWidth: { xs: '100%', sm: '45%' } }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: '500', fontSize: '16px', mb: 1 }}
|
||||
>
|
||||
Consumption Status
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-label="consumption-status"
|
||||
name="row-radio-buttons-group"
|
||||
value={consumptionStatus ? 'yes' : 'no'}
|
||||
onChange={(e) => setConsumptionStatus(e.target.value === 'yes')}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="yes"
|
||||
control={<MuiRadio />}
|
||||
label="Yes"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="no"
|
||||
control={<MuiRadio />}
|
||||
label="No"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* السطر الرابع: Stocks Level + Product Category */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
|
||||
{renderField('Stocks Level', stocksLevel, (e) => setStocksLevel(e.target.value), 'number', 'Enter level')}
|
||||
{renderSelectField('Product Category', productCategory, (e) => setProductCategory(e.target.value), productCategories)}
|
||||
</Box>
|
||||
|
||||
{/* السطر الخامس: Supplier Name + Supplier Number */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
|
||||
{renderField('Supplier Name', supplierName, (e) => setSupplierName(e.target.value), 'text', 'Supplier XYZ')}
|
||||
{renderField('Supplier Number', supplierNumber, (e) => setSupplierNumber(e.target.value), 'text', '0900-78601')}
|
||||
</Box>
|
||||
|
||||
{/* السطر السادس: Product Price + Phone Number */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 3 }}>
|
||||
{renderField('Product Price', productPrice, (e) => setProductPrice(e.target.value), 'number', '$200')}
|
||||
{renderField('Product Cost', productCost, (e) => setProductCost(e.target.value), 'number', '$230')}
|
||||
</Box>
|
||||
|
||||
{/* زر الحفظ */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
mb: 2,
|
||||
gap:2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
sx={{
|
||||
width: '150px',
|
||||
height: '50px',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: 'white',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px',
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Save Product
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onShowTable}
|
||||
sx={{
|
||||
width: '150px',
|
||||
height: '50px',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px',
|
||||
textTransform: 'none',
|
||||
color: theme.palette.primary.main,
|
||||
borderColor: theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFECE0',
|
||||
color: theme.palette.primary.main,
|
||||
borderColor: '#FFECE0',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Show Table
|
||||
</Button>
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductEntry;
|
||||
@@ -0,0 +1,498 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Chip,
|
||||
Pagination,
|
||||
Button,
|
||||
Avatar,
|
||||
TableContainer,
|
||||
TextField,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
|
||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||
import DriveFileRenameOutlineSharpIcon from '@mui/icons-material/DriveFileRenameOutlineSharp';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { green } from '@mui/material/colors';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { saveAs } from 'file-saver';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
|
||||
|
||||
const TableView = ({ data = [], onAddNewProduct }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const itemsPerPage = 5;
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// فلترة البيانات بناءً على نص البحث (غير حساس لحالة الأحرف)
|
||||
const filteredData = data.filter((item) =>
|
||||
item.product.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const pageCount = Math.ceil(filteredData.length / itemsPerPage);
|
||||
|
||||
const paginatedData = filteredData.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
const handleSearchChange = (e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1); // العودة للصفحة الأولى عند تغيير البحث
|
||||
};
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const doc = new jsPDF();
|
||||
doc.setFontSize(14);
|
||||
doc.text('Top Selling Products', 14, 20);
|
||||
|
||||
const tableColumn = ['No.', 'Product', 'Sales', 'Amount', 'Price', 'Expiration', 'Status'];
|
||||
const tableRows = data.map((row, i) => ([
|
||||
i + 1,
|
||||
row.product,
|
||||
row.sales,
|
||||
`$${row.amount}`,
|
||||
`$${row.price}`,
|
||||
row.expiration,
|
||||
row.status
|
||||
]));
|
||||
|
||||
autoTable(doc, {
|
||||
startY: 30,
|
||||
head: [tableColumn],
|
||||
body: tableRows,
|
||||
styles: { fontSize: 10 },
|
||||
headStyles: { fillColor: [240, 240, 240] }
|
||||
});
|
||||
|
||||
doc.save('top_selling_products.pdf');
|
||||
};
|
||||
|
||||
const handleExportSpreadsheet = () => {
|
||||
const wsData = [
|
||||
['No.', 'Product', 'Sales', 'Amount', 'Price', 'Expiration', 'Status'],
|
||||
...data.map((row, i) => [
|
||||
i + 1,
|
||||
row.product,
|
||||
row.sales,
|
||||
`$${row.amount}`,
|
||||
`$${row.price}`,
|
||||
row.expiration,
|
||||
row.status
|
||||
])
|
||||
];
|
||||
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(wsData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "Top Selling");
|
||||
|
||||
const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||
const blob = new Blob([wbout], { type: 'application/octet-stream' });
|
||||
saveAs(blob, 'top_selling_products.xlsx');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
backgroundColor: '#FFFFFF',
|
||||
maxWidth: { xs: '90%', md: '100%' },
|
||||
borderRadius: 2,
|
||||
height: { xs: 'auto', md: '100vh' },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
boxShadow: theme.shadows[1],
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'flex-start', sm: 'center', md: 'center' },
|
||||
mb: 3,
|
||||
pl: { xs: 1, sm: 3 },
|
||||
pt: 2,
|
||||
pr: { xs: 1, sm: 3 },
|
||||
flexDirection: { xs: 'column', sm: 'row', md: 'row' },
|
||||
gap: { xs: 2, sm: 0 }
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
||||
Table View
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
flexWrap={{ xs: 'wrap', sm: 'nowrap' }}
|
||||
width={{ xs: '100%', sm: 'auto' }}
|
||||
justifyContent={{ xs: 'space-between', sm: 'flex-end' }}
|
||||
>
|
||||
{/* زر إضافة منتج جديد */}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onAddNewProduct}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
fontSize: { xs: '12px', sm: '14px' },
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
|
||||
Add New Product
|
||||
|
||||
</Button>
|
||||
{/* زر تصدير جدول (Spreadsheet) */}
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: '#5F6868',
|
||||
boxShadow: 'none',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
width: { xs: '48%', sm: '140px', md: '166px' },
|
||||
fontSize: { xs: '12px', sm: '13px', md: '14px' },
|
||||
fontWeight: 600,
|
||||
p: 0,
|
||||
m: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 'unset',
|
||||
}}
|
||||
onClick={handleExportSpreadsheet}
|
||||
>
|
||||
{isMobile ? 'Export' : 'Export Spreadsheet'}
|
||||
</Button>
|
||||
|
||||
{/* زر PDF */}
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: 'white',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
boxShadow: 'none',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
width: { xs: '48%', sm: '90px', md: '105px' },
|
||||
fontSize: { xs: '12px', sm: '13px', md: '14px' },
|
||||
fontWeight: 600,
|
||||
p: 0,
|
||||
m: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 'unset',
|
||||
}}
|
||||
onClick={handleExportPDF}
|
||||
>
|
||||
Export PDF
|
||||
</Button>
|
||||
|
||||
{/* زر الفلاتر */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: '#667085',
|
||||
borderColor: '#e0e0e0',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
width: { xs: '100%', sm: '120px', md: '99px' },
|
||||
fontSize: { xs: '12px', sm: '13px', md: '14px' },
|
||||
fontWeight: 600,
|
||||
p: 0,
|
||||
m: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 'unset',
|
||||
}}
|
||||
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
|
||||
>
|
||||
{isMobile ? '' : 'Filters'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
{/* جدول البيانات */}
|
||||
<TableContainer
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflowX: 'auto',
|
||||
maxHeight: { xs: '60vh', md: 'calc(100vh - 160px)' },
|
||||
'&::-webkit-scrollbar': { height: 4 },
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
sx={{
|
||||
minWidth: 750,
|
||||
'& .MuiTableCell-root': {
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
py: { xs: 0.5, sm: 1.5 },
|
||||
},
|
||||
'& thead .MuiTableCell-root': {
|
||||
borderBottom: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '11px', sm: '14px' },
|
||||
px: { xs: 0.5, sm: 2 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableHead sx={{ backgroundColor: '#F0F1F3' }}>
|
||||
<TableRow sx={{ height: { xs: '6vh', sm: '10vh' } }}>
|
||||
<TableCell width="6%">No.</TableCell>
|
||||
|
||||
<TableCell width="20%" sx={{ position: 'relative' }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'pointer', userSelect: 'none' }}
|
||||
onClick={() => setShowSearch(prev => !prev)}
|
||||
>
|
||||
<Typography>Product Name</Typography>
|
||||
<ArrowDropDownIcon
|
||||
fontSize="small"
|
||||
sx={{
|
||||
transform: showSearch ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.3s ease',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{showSearch && (
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder="Search product..."
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
sx={{ mt: 1, width: '100%' }}
|
||||
InputProps={{
|
||||
endAdornment: searchTerm && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setSearchTerm('')}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
|
||||
{!isMobile && <TableCell width="10%">Barcode</TableCell>}
|
||||
<TableCell width="10%">Branch</TableCell>
|
||||
{!isMobile && <TableCell width="10%">Expiration</TableCell>}
|
||||
{!isMobile && <TableCell width="15%">Stock Quantity </TableCell>}
|
||||
<TableCell width="15%">Consumption Status</TableCell>
|
||||
<TableCell width="10%" align="center">
|
||||
Action
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody
|
||||
sx={{
|
||||
'& .MuiTableCell-root': {
|
||||
px: { xs: 2, sm: 3 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((row, i) => (
|
||||
<TableRow key={i} sx={{ height: { xs: '8vh', sm: '13vh' } }}>
|
||||
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
|
||||
{(currentPage - 1) * itemsPerPage + i + 1}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: { xs: 1, sm: 2 },
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: green[50],
|
||||
width: { xs: 28, sm: 40 },
|
||||
height: { xs: 28, sm: 40 },
|
||||
}}
|
||||
variant="rounded"
|
||||
>
|
||||
<AssignmentIcon
|
||||
fontSize={isMobile ? 'small' : 'medium'}
|
||||
/>
|
||||
</Avatar>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: { xs: '11px', sm: '14px' },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: { xs: '80px', sm: '200px' },
|
||||
}}
|
||||
>
|
||||
{row.product}
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
{!isMobile && (
|
||||
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
|
||||
{row.sales}
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
|
||||
${row.amount.toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
{!isMobile && (
|
||||
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
|
||||
${row.price}
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<TableCell sx={{ fontSize: { xs: '11px', sm: '14px' } }}>
|
||||
{row.expiration}
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={isMobile ? row.status.substring(0, 3) : row.status}
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
sx={{
|
||||
fontSize: { xs: '10px', sm: '14px' },
|
||||
fontWeight: 600,
|
||||
backgroundColor:
|
||||
row.status === 'Published'
|
||||
? '#E7F4EE'
|
||||
: row.status === 'Low Stock'
|
||||
? '#FDF1E8'
|
||||
: row.status === 'Out of Stock'
|
||||
? '#FFCDD2'
|
||||
: '#E0E0E0',
|
||||
color:
|
||||
row.status === 'Published'
|
||||
? '#0D894F'
|
||||
: row.status === 'Low Stock'
|
||||
? '#E46A11'
|
||||
: row.status === 'Out of Stock'
|
||||
? '#C62828'
|
||||
: '#424242',
|
||||
minWidth: { xs: '50px', sm: '100px' },
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={() => console.log('Edit', row)}
|
||||
sx={{ minWidth: 0, padding: 0 }}
|
||||
>
|
||||
<DriveFileRenameOutlineSharpIcon
|
||||
fontSize={isMobile ? 'small' : 'medium'}
|
||||
sx={{ color: '#5F6868' }}
|
||||
/>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} align="center" sx={{ py: 5 }}>
|
||||
No products found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Pagination Footer */}
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
p={{ xs: 1, sm: 2 }}
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontSize: { xs: '11px', sm: '14px' },
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
Showing{' '}
|
||||
{(currentPage - 1) * itemsPerPage + 1} to{' '}
|
||||
{Math.min(currentPage * itemsPerPage, filteredData.length)} of{' '}
|
||||
{filteredData.length}
|
||||
</Typography>
|
||||
|
||||
<Pagination
|
||||
count={pageCount}
|
||||
page={currentPage}
|
||||
onChange={(event, value) => setCurrentPage(value)}
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
sx={{
|
||||
'& .MuiPaginationItem-root': {
|
||||
fontSize: { xs: '12px', sm: '14px' },
|
||||
minWidth: { xs: 24, sm: 32 },
|
||||
height: { xs: 24, sm: 32 },
|
||||
backgroundColor: '#FFECE0',
|
||||
color: theme.palette.primary.main,
|
||||
borderRadius: '8px',
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: '#fff',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFD6B5',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableView;
|
||||
@@ -0,0 +1,206 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useTheme, useMediaQuery, Typography } from '@mui/material';
|
||||
import KitchPlusAppBar from '../AppBar';
|
||||
import Sidebar from '../SideHome';
|
||||
import AccountSettings from './contect/AccountSettings';
|
||||
import MealsByCateg from './contect/MealsByCateg';
|
||||
import AllMeals from './contect/AllMeals';
|
||||
import ProductDetail from './contect/ProductDetail';
|
||||
import authService from '../../../services/authService';
|
||||
import { useRestaurant } from '../../../contexts/RestaurantContext';
|
||||
|
||||
const drawerWidth = 230;
|
||||
|
||||
const Meal = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
|
||||
const { restaurantId } = useRestaurant();
|
||||
|
||||
const [showAllPopular, setShowAllPopular] = useState(false);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState(null);
|
||||
const [categoryName, setCategoryName] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState(null);
|
||||
const [meals, setMeals] = useState([]);
|
||||
const [loadingMeals, setLoadingMeals] = useState(false);
|
||||
|
||||
// جلب الفئات عند التحميل
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
if (!restaurantId) return;
|
||||
const result = await authService.getCategoriesByRestaurant(restaurantId);
|
||||
if (result.success) setCategories(result.data);
|
||||
else setCategories([]);
|
||||
};
|
||||
fetchCategories();
|
||||
}, [restaurantId]);
|
||||
|
||||
// تحديث اسم الفئة عند تغيير selectedCategoryId أو categories
|
||||
useEffect(() => {
|
||||
if (selectedCategoryId && categories.length > 0) {
|
||||
const selected = categories.find(c => c.id === selectedCategoryId);
|
||||
setCategoryName(selected ? selected.name : '');
|
||||
} else {
|
||||
setCategoryName('');
|
||||
}
|
||||
}, [selectedCategoryId, categories]);
|
||||
|
||||
// جلب الوجبات عند اختيار الفئة
|
||||
useEffect(() => {
|
||||
const fetchMeals = async () => {
|
||||
if (selectedCategoryId && restaurantId) {
|
||||
setLoadingMeals(true);
|
||||
try {
|
||||
const result = await authService.getMealsByCategory(restaurantId, selectedCategoryId);
|
||||
if (result.success) {
|
||||
const selectedCategory = categories.find(c => c.id === selectedCategoryId);
|
||||
const mealsWithImages = result.data.map(meal => ({
|
||||
id: meal.id,
|
||||
name: meal.name,
|
||||
price: meal.price,
|
||||
unit: meal.unit || '/pcs',
|
||||
image: meal.photo || '/images/default-product.png',
|
||||
category: selectedCategory ? selectedCategory.name : '', // اسم الفئة
|
||||
category_id: meal.category_id,
|
||||
restaurant_id: meal.restaurant_id,
|
||||
description: meal.description || [],
|
||||
additions: meal.additions || []
|
||||
}));
|
||||
setMeals(mealsWithImages);
|
||||
} else setMeals([]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setMeals([]);
|
||||
} finally {
|
||||
setLoadingMeals(false);
|
||||
}
|
||||
} else setMeals([]);
|
||||
};
|
||||
fetchMeals();
|
||||
}, [selectedCategoryId, restaurantId, categories]);
|
||||
|
||||
const handleDrawerToggle = () => setSidebarOpen(!sidebarOpen);
|
||||
const toggleShowAllPopular = () => setShowAllPopular(prev => !prev);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100vh', backgroundColor: '#F6F6F6', overflow: 'hidden' }}>
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
isMobile={isMobile}
|
||||
drawerWidth={drawerWidth}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '10%',
|
||||
marginLeft: { xs: 0, sm: sidebarOpen ? `${drawerWidth}px` : 0, md: 0 },
|
||||
transition: theme.transitions.create(['width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<KitchPlusAppBar
|
||||
onDrawerToggle={handleDrawerToggle}
|
||||
sidebarOpen={sidebarOpen}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
width: sidebarOpen ? 'calc(100% - 40px)' : '100%',
|
||||
pt: { xs: 2, sm: 3 },
|
||||
overflowY: 'auto',
|
||||
pl: { xs: 1, sm: 2 },
|
||||
pr: { xs: 1, sm: 2 },
|
||||
md: 10,
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
}}
|
||||
>
|
||||
{selectedProduct ? (
|
||||
<ProductDetail
|
||||
product={selectedProduct}
|
||||
onBack={() => setSelectedProduct(null)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<AccountSettings
|
||||
selectedCategory={selectedCategoryId}
|
||||
setSelectedCategory={setSelectedCategoryId}
|
||||
restaurantId={restaurantId}
|
||||
/>
|
||||
|
||||
{selectedCategoryId ? (
|
||||
<Box sx={{ mt: 3, md: 10, }}>
|
||||
|
||||
<MealsByCateg
|
||||
categoryId={selectedCategoryId}
|
||||
restaurantId={restaurantId}
|
||||
categoryName={categoryName}
|
||||
onBack={() => setSelectedCategoryId(null)}
|
||||
onProductClick={(meal) => {
|
||||
const category = categories.find(c => c.id === meal.category_id);
|
||||
setSelectedProduct({
|
||||
...meal,
|
||||
category: category ? category.name : '',
|
||||
category_id: meal.category_id,
|
||||
restaurant_id: restaurantId
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
) : !showAllPopular ? (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 3,
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
alignItems: 'stretch',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flexBasis: { xs: '100%', md: '100%' },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
pb: { xs: 4, md: 4 },
|
||||
pr: { xs: 1, md: 0 },
|
||||
mb: 5,
|
||||
}}
|
||||
>
|
||||
<AllMeals
|
||||
restaurantId={restaurantId}
|
||||
categories={categories}
|
||||
showAll={showAllPopular}
|
||||
onToggleShowAll={toggleShowAllPopular}
|
||||
onProductClick={(meal) => {
|
||||
const category = categories.find(c => c.id === meal.category_id);
|
||||
setSelectedProduct({
|
||||
...meal,
|
||||
category: category ? category.name : '',
|
||||
category_id: meal.category_id,
|
||||
restaurant_id: restaurantId
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Meal;
|
||||
@@ -0,0 +1,330 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Skeleton,
|
||||
} from '@mui/material';
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
import CategoryScrollList from './CategoryScrollList';
|
||||
import AddCategory from './AddCategory';
|
||||
import authService from '../../../../services/authService';
|
||||
|
||||
const AccountSettings = ({ selectedCategory, setSelectedCategory, restaurantId }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [loading, setLoading] = useState(true); // <-- حالة التحميل
|
||||
const [selectedCategoryForEdit, setSelectedCategoryForEdit] = useState(null);
|
||||
const [contextMenu, setContextMenu] = useState(null);
|
||||
const [openAddModal, setOpenAddModal] = useState(false);
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
|
||||
const scrollRef = useRef();
|
||||
|
||||
// تحميل الفئات عند فتح الصفحة
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
setLoading(true); // بدء التحميل
|
||||
const result = await authService.getCategoriesByRestaurant(restaurantId);
|
||||
if (result.success) setCategories(result.data);
|
||||
else console.error(result.message);
|
||||
setLoading(false); // انتهاء التحميل
|
||||
};
|
||||
if (restaurantId) fetchCategories();
|
||||
}, [restaurantId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCategory) localStorage.setItem('selectedCategoryId', selectedCategory);
|
||||
}, [selectedCategory]);
|
||||
|
||||
const scroll = (offset) => {
|
||||
if (scrollRef.current) scrollRef.current.scrollLeft += offset;
|
||||
};
|
||||
|
||||
const handleAddCategory = async (payload) => {
|
||||
try {
|
||||
const response = await authService.addCategory(payload);
|
||||
setCategories((prev) => [...prev, response.category]);
|
||||
setOpenAddModal(false);
|
||||
} catch (error) {
|
||||
console.error('خطأ أثناء الإضافة:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCategory = async (payload) => {
|
||||
try {
|
||||
const response = await authService.updateCategory(selectedCategoryForEdit.id, payload);
|
||||
setCategories((prev) =>
|
||||
prev.map((cat) => (cat.id === selectedCategoryForEdit.id ? response.category : cat))
|
||||
);
|
||||
setOpenAddModal(false);
|
||||
} catch (error) {
|
||||
console.error('خطأ أثناء التعديل:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCategory = async () => {
|
||||
try {
|
||||
await authService.deleteCategory(selectedCategoryForEdit.id);
|
||||
setCategories((prev) =>
|
||||
prev.filter((cat) => cat.id !== selectedCategoryForEdit.id)
|
||||
);
|
||||
setConfirmDeleteOpen(false);
|
||||
setSelectedCategoryForEdit(null);
|
||||
|
||||
if (selectedCategory === selectedCategoryForEdit?.id) {
|
||||
setSelectedCategory(null);
|
||||
localStorage.removeItem('selectedCategoryId');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('خطأ أثناء الحذف:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!openAddModal) setSelectedCategoryForEdit(null);
|
||||
}, [openAddModal]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
pl: { xs: 2, sm: 3 },
|
||||
pr: { xs: 2, sm: 3 },
|
||||
pb: { xs: 2, sm: 3 },
|
||||
pt: { xs: 2, sm: 1.5 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
maxWidth: { xs: '90%', md: '100%' },
|
||||
borderRadius: 2,
|
||||
maxHeight: { xs: '293px', sm: '30%', md: 140 },
|
||||
}}
|
||||
>
|
||||
{/* رأس القسم */}
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||
mb: 3,
|
||||
pl: 1,
|
||||
pr: { xs: 1, sm: 0 },
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: '600',
|
||||
fontSize: { xs: '20px', sm: '22px', md: '24px' },
|
||||
color: '#121212',
|
||||
}}
|
||||
>
|
||||
Categories
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: { xs: 1, sm: 2 },
|
||||
flexWrap: { xs: 'wrap', sm: 'nowrap' },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
justifyContent: { xs: 'space-between', sm: 'flex-end' },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: '#fff',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
boxShadow: 'none',
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
width: { xs: '100%', sm: '130px', md: '150px' },
|
||||
fontSize: { xs: '12px', sm: '14px', md: '16px' },
|
||||
fontWeight: 700,
|
||||
minWidth: 'unset',
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedCategoryForEdit(null);
|
||||
setOpenAddModal(true);
|
||||
}}
|
||||
>
|
||||
Add Category
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
onClick={() => scroll(-200)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: '#fff',
|
||||
width: 40,
|
||||
height: 40,
|
||||
'&:hover': { backgroundColor: '#ffddbfff' },
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosNewIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={() => scroll(200)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: '#fff',
|
||||
width: 40,
|
||||
height: 40,
|
||||
'&:hover': { backgroundColor: '#ffddbfff' },
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIosIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* قائمة الفئات */}
|
||||
{/* قائمة الفئات */}
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '90%', sm: '95%', md: '100%' },
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
mb: 3,
|
||||
pl: 1,
|
||||
mt: 5,
|
||||
pr: { xs: 1, sm: 4 },
|
||||
flexDirection: 'row',
|
||||
overflowX: 'auto',
|
||||
gap: 2,
|
||||
}}
|
||||
ref={scrollRef}
|
||||
>
|
||||
{loading ? (
|
||||
// Skeleton Loader
|
||||
Array.from({ length: 5 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
variant="rectangular"
|
||||
width={120}
|
||||
height={60}
|
||||
sx={{ borderRadius: 2 }}
|
||||
/>
|
||||
))
|
||||
) : categories.length === 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '60px',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
No categories available
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<CategoryScrollList
|
||||
categories={categories}
|
||||
selectedCategory={selectedCategory}
|
||||
setSelectedCategory={setSelectedCategory}
|
||||
setSelectedCategoryForEdit={setSelectedCategoryForEdit}
|
||||
setContextMenu={setContextMenu}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
{/* مودال الإضافة / التعديل */}
|
||||
<Modal
|
||||
open={openAddModal}
|
||||
onClose={() => setOpenAddModal(false)}
|
||||
aria-labelledby="add-category-modal"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 2,
|
||||
boxShadow: 24,
|
||||
p: 2,
|
||||
maxWidth: '90vw',
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
<AddCategory
|
||||
onAdd={selectedCategoryForEdit ? handleUpdateCategory : handleAddCategory}
|
||||
editingCategory={selectedCategoryForEdit}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
{/* قائمة السياق */}
|
||||
<Menu
|
||||
open={contextMenu !== null}
|
||||
onClose={() => setContextMenu(null)}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
contextMenu !== null
|
||||
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
||||
: undefined
|
||||
}
|
||||
sx={{ zIndex: 2000 }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpenAddModal(true);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
>
|
||||
Edit Category
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setConfirmDeleteOpen(true);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
sx={{ color: 'red' }}
|
||||
>
|
||||
Delete Category
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{/* تأكيد الحذف */}
|
||||
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>Are you sure you want to delete this category?</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleDeleteCategory} color="error" variant="contained">
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSettings;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||
import { useRestaurant } from '../../../../contexts/RestaurantContext';
|
||||
|
||||
const AddCategory = ({ onAdd, editingCategory }) => {
|
||||
const { restaurantId } = useRestaurant();
|
||||
const [productName, setProductName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [price, setPrice] = useState('');
|
||||
const [discount, setDiscount] = useState('');
|
||||
const [productImage, setProductImage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (editingCategory) {
|
||||
setProductName(editingCategory.name || '');
|
||||
setDescription(editingCategory.description || '');
|
||||
setPrice(editingCategory.price || '');
|
||||
setDiscount(editingCategory.discount || '');
|
||||
setProductImage(editingCategory.icon || null);
|
||||
}
|
||||
}, [editingCategory]);
|
||||
|
||||
const handleImageUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => setProductImage(reader.result);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!productName) {
|
||||
alert("Please enter category name");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!restaurantId) {
|
||||
alert("Restaurant ID not set");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: productName,
|
||||
restaurant_id: restaurantId,
|
||||
description,
|
||||
price,
|
||||
discount,
|
||||
icon: productImage,
|
||||
};
|
||||
|
||||
// فقط أرسل البيانات للمكون الأب
|
||||
if (onAdd) onAdd(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box p={2} maxWidth={400} mx="auto">
|
||||
<Typography variant="body2" color="black" sx={{ fontWeight: 600, fontSize: '24px' }}>
|
||||
Category Name
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="Enter category name"
|
||||
value={productName}
|
||||
onChange={(e) => setProductName(e.target.value)}
|
||||
margin="normal"
|
||||
sx={{
|
||||
'& input': { fontWeight: 500, fontSize: '15px' },
|
||||
'& input::placeholder': { color: '#969BA7' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
transition: '0.3s',
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#FF914D',
|
||||
boxShadow: '0 0 0 2px rgba(255,145,77,0.2)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{
|
||||
mt: 2,
|
||||
backgroundColor: '#FF914D',
|
||||
'&:hover': { backgroundColor: '#e57f3c' },
|
||||
borderRadius: 2,
|
||||
color: '#fff',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{editingCategory ? 'Update Category' : 'Add Category'}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCategory;
|
||||
@@ -0,0 +1,251 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import authService from '../../../../services/authService';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
const MAX_FILE_SIZE_MB = 2;
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
|
||||
const AddMeal = ({ open, onClose, onAdd, selectedCategory, restaurantId }) => {
|
||||
const [mealName, setMealName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [additions, setAdditions] = useState(['']);
|
||||
const [price, setPrice] = useState('');
|
||||
const [mealImage, setMealImage] = useState(null);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setMealName('');
|
||||
setDescription('');
|
||||
setAdditions(['']);
|
||||
setPrice('');
|
||||
setMealImage(null);
|
||||
setErrors({});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleImageUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
setErrors(prev => ({ ...prev, image: 'Allowed types: jpg, jpeg, png' }));
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
|
||||
setErrors(prev => ({ ...prev, image: `Image must be smaller than ${MAX_FILE_SIZE_MB} MB` }));
|
||||
return;
|
||||
}
|
||||
|
||||
setMealImage(file);
|
||||
setErrors(prev => ({ ...prev, image: '' }));
|
||||
};
|
||||
|
||||
const handleAddAddition = () => setAdditions([...additions, '']);
|
||||
const handleRemoveAddition = (index) => setAdditions(additions.filter((_, i) => i !== index));
|
||||
const handleChangeAddition = (index, value) => {
|
||||
const newAdditions = [...additions];
|
||||
newAdditions[index] = value;
|
||||
setAdditions(newAdditions);
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const newErrors = {};
|
||||
if (!mealName.trim()) newErrors.name = 'Meal name is required';
|
||||
if (!description.trim()) newErrors.description = 'Description is required';
|
||||
if (!price || isNaN(price) || Number(price) <= 0) newErrors.price = 'Valid price is required';
|
||||
if (!mealImage) newErrors.image = 'Meal image is required';
|
||||
if (!selectedCategory) newErrors.category = 'Category must be selected';
|
||||
additions.forEach((add, i) => {
|
||||
if (!add.trim()) newErrors[`addition_${i}`] = 'Cannot be empty';
|
||||
});
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validate()) return;
|
||||
setIsSubmitting(true);
|
||||
|
||||
const mealData = {
|
||||
name: mealName.trim(),
|
||||
category_id: selectedCategory,
|
||||
price,
|
||||
restaurant_id: restaurantId,
|
||||
photo: mealImage,
|
||||
description: [description.trim()],
|
||||
additions: additions.filter(a => a.trim() !== ''),
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await authService.addMeal(mealData);
|
||||
|
||||
// عرض رسالة Snackbar أو alert
|
||||
alert(result.message);
|
||||
|
||||
// تمرير الوجبة الجديدة للمكون الأب لتحديث القائمة
|
||||
if (result.data && onAdd) {
|
||||
const newMeal = {
|
||||
id: result.data.id,
|
||||
name: result.data.name,
|
||||
price: result.data.price,
|
||||
description: result.data.description || [],
|
||||
additions: result.data.additions || [],
|
||||
unit: result.data.unit || "/pcs",
|
||||
photo: result.data.photo || "/images/default-product.png",
|
||||
category_id: result.data.category_id || selectedCategory,
|
||||
};
|
||||
onAdd(newMeal);
|
||||
}
|
||||
|
||||
// إغلاق المودال بعد الإضافة
|
||||
onClose();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Add meal error:', error);
|
||||
alert(error.message || "Something went wrong");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 600, fontSize: '20px' }}>Add Meal</DialogTitle>
|
||||
<DialogContent
|
||||
dividers
|
||||
sx={{
|
||||
overflow: 'auto',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
{/* Meal Image */}
|
||||
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Meal Image</Typography>
|
||||
<Box
|
||||
component="label"
|
||||
htmlFor="upload-image"
|
||||
sx={{
|
||||
border: '2px dashed #ccc',
|
||||
borderRadius: '10px',
|
||||
height: '150px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
color: '#969BA7',
|
||||
backgroundColor: '#FAFAFA',
|
||||
fontWeight: 500,
|
||||
fontSize: '18px',
|
||||
textAlign: 'center',
|
||||
mb: 2,
|
||||
'&:hover': { borderColor: '#FF914D', backgroundColor: '#fffaf5' }
|
||||
}}
|
||||
>
|
||||
{mealImage ? (
|
||||
<Box sx={{ position: 'relative', width: '100%', height: '100%', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<img src={mealImage instanceof File ? URL.createObjectURL(mealImage) : mealImage} alt="Preview" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
<Button onClick={() => setMealImage(null)} sx={{ position: 'absolute', top: 4, right: 4, minWidth: 0, padding: '2px 6px', fontSize: 12 }}>Remove</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
Upload Meal Picture
|
||||
<AddAPhotoOutlinedIcon sx={{ fontSize: 32, color: '#9e9e9e', mt: 1 }} />
|
||||
</>
|
||||
)}
|
||||
<input id="upload-image" type="file" accept="image/*" hidden onChange={handleImageUpload} />
|
||||
</Box>
|
||||
{errors.image && <Typography color="error" sx={{ mb: 1 }}>{errors.image}</Typography>}
|
||||
|
||||
{/* Meal Name */}
|
||||
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Meal Name</Typography>
|
||||
<TextField fullWidth placeholder="Meal Name" value={mealName} onChange={e => setMealName(e.target.value)} error={!!errors.name} helperText={errors.name} sx={{ mb: 2 }} />
|
||||
|
||||
{/* Description */}
|
||||
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Description</Typography>
|
||||
<TextField fullWidth placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} error={!!errors.description} helperText={errors.description} sx={{ mb: 2 }} />
|
||||
|
||||
{/* Additions */}
|
||||
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Additions</Typography>
|
||||
{additions.map((add, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder={`Addition ${i + 1}`}
|
||||
value={add}
|
||||
onChange={e => handleChangeAddition(i, e.target.value)}
|
||||
error={!!errors[`addition_${i}`]}
|
||||
helperText={errors[`addition_${i}`]}
|
||||
/>
|
||||
<IconButton onClick={() => handleRemoveAddition(i)} color="error"><DeleteIcon /></IconButton>
|
||||
</Box>
|
||||
))}
|
||||
<Button variant="text" startIcon={<AddIcon />} onClick={handleAddAddition} sx={{ mb: 2 ,textTransform:'none'}}>Add Another</Button>
|
||||
|
||||
{/* Price */}
|
||||
<Typography variant="body2" fontWeight={500} sx={{ mb: 1 }}>Price</Typography>
|
||||
<TextField fullWidth type="number" placeholder="Price" value={price} onChange={e => setPrice(e.target.value)} error={!!errors.price} helperText={errors.price} sx={{ mb: 2 }} />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ pr: 3, pb: 2 }}>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
width: '135px',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
width: '135px',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
Add Meal
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddMeal;
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Typography, Skeleton, useTheme } from "@mui/material";
|
||||
import MealCard from "./MealCard";
|
||||
import authService from "../../../../services/authService";
|
||||
import SimplePagination from "../../SimplePagination";
|
||||
|
||||
const AllMeals = ({ restaurantId, categories = [], onProductClick }) => {
|
||||
const theme = useTheme();
|
||||
const [products, setProducts] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pagination, setPagination] = useState(null);
|
||||
|
||||
const itemsPerPage = 6;
|
||||
|
||||
useEffect(() => {
|
||||
if (!restaurantId) return;
|
||||
|
||||
const fetchMeals = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await authService.getAllMealsByRestaurant(restaurantId, page);
|
||||
|
||||
if (result.success) {
|
||||
const mealsWithImages = result.data.map(meal => {
|
||||
const category = categories.find(c => c.id === meal.category_id);
|
||||
return {
|
||||
id: meal.id,
|
||||
name: meal.name,
|
||||
price: meal.price,
|
||||
description: meal.description || [],
|
||||
additions: meal.additions || [],
|
||||
unit: meal.unit || "/pcs",
|
||||
photo: meal.photo || meal.photo_url || "/images/default-product.png",
|
||||
category_id: meal.category_id,
|
||||
category: category ? category.name : null,
|
||||
};
|
||||
});
|
||||
|
||||
setProducts(mealsWithImages);
|
||||
setPagination(result.pagination);
|
||||
} else {
|
||||
setProducts([]);
|
||||
setPagination(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("Failed to fetch meals.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMeals();
|
||||
}, [restaurantId, page, categories]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mb: 0,
|
||||
mr: { xs: 2, sm: 2, md: 0 },
|
||||
pl: { xs: 2, sm: 3 },
|
||||
pr: { xs: 2, sm: 3 },
|
||||
pb: { xs: 2, sm: 3 },
|
||||
pt: { xs: 2, sm: 1.5 },
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderRadius: "10px",
|
||||
padding: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "20px",
|
||||
overflowY: "auto",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Typography variant="h6" fontWeight={600}>All Meals</Typography>
|
||||
</Box>
|
||||
|
||||
{loading ? (
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
|
||||
{Array.from({ length: itemsPerPage }).map((_, index) => (
|
||||
<Box key={index} sx={{ width: "calc(33.33% - 16px)" }}>
|
||||
<Skeleton variant="rectangular" width="100%" height={180} sx={{ borderRadius: 2 }} />
|
||||
<Skeleton variant="text" width="80%" sx={{ mt: 1 }} />
|
||||
<Skeleton variant="text" width="40%" />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}>
|
||||
<Typography variant="body1" color="error">{error}</Typography>
|
||||
</Box>
|
||||
) : products.length === 0 ? (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "150px" }}>
|
||||
<Typography variant="body1" color="text.secondary">There is no meal available</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
|
||||
{products.map(product => (
|
||||
<Box key={product.id} onClick={() => onProductClick(product)} >
|
||||
<MealCard
|
||||
meal={product}
|
||||
onClick={() => onProductClick(product)}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Pagination Footer */}
|
||||
{pagination && !loading && (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mt={2} sx={{ borderTop: "1px solid #f0f0f0", pt: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Showing {(page - 1) * itemsPerPage + 1} to {Math.min(page * itemsPerPage, products.length)} of {pagination.total} meals
|
||||
</Typography>
|
||||
|
||||
<SimplePagination
|
||||
currentPage={page}
|
||||
pageCount={pagination.last_page}
|
||||
onChange={(newPage) => setPage(newPage)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllMeals;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
const CategoryScrollList = ({
|
||||
categories,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
setSelectedCategoryForEdit,
|
||||
setContextMenu,
|
||||
scrollRef,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={scrollRef}
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflowX: 'auto',
|
||||
scrollBehavior: 'smooth',
|
||||
pl: 1,
|
||||
pr: { xs: 1, sm: 0 },
|
||||
mb: 3,
|
||||
mx: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
scrollbarWidth: 'none',
|
||||
}}
|
||||
>
|
||||
{categories.map((cat) => {
|
||||
const isSelected = selectedCategory === cat.id;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategory(cat.id)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedCategoryForEdit(cat);
|
||||
setContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6 });
|
||||
}}
|
||||
sx={{
|
||||
p: 1,
|
||||
borderRadius: '12px',
|
||||
backgroundColor: isSelected ? theme.palette.primary.main : '#F9F9FC',
|
||||
border: isSelected ? `0px solid ${theme.palette.primary.main}` : '0px solid #ddd',
|
||||
textAlign: 'center',
|
||||
minWidth: '70px',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
transition: '0.3s',
|
||||
'&:hover': {
|
||||
backgroundColor: isSelected ? theme.palette.primary.dark : '#EDEDED',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={500}
|
||||
sx={{ color: isSelected ? '#fff' : '#121212' }}
|
||||
>
|
||||
{cat.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryScrollList;
|
||||
@@ -0,0 +1,138 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
IconButton,
|
||||
Chip,
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
const MealCard = ({ meal, onClick, onAdd }) => {
|
||||
const { name, price, unit, photo, image, description, additions } = meal;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
width: { xs: "160px", md: "160px" },
|
||||
height: 250,
|
||||
borderRadius: "20px",
|
||||
p: "15px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: "#FCFCFC",
|
||||
boxShadow: "none",
|
||||
border: "1px solid #f0f0f0",
|
||||
cursor: "pointer",
|
||||
transition: "transform 0.2s ease-in-out",
|
||||
"&:hover": { transform: "scale(1.02)" },
|
||||
}}
|
||||
>
|
||||
{/* صورة الوجبة */}
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={photo || "/images/default-product.png"}
|
||||
alt={name}
|
||||
sx={{
|
||||
width: "120px",
|
||||
height: "120px",
|
||||
objectFit: "cover",
|
||||
borderRadius: "12px",
|
||||
// mb: 1,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* تفاصيل الوجبة */}
|
||||
<CardContent sx={{ p: 0, width: "100%", textAlign: "center" }}>
|
||||
<Typography variant="body1" fontWeight={600} fontSize={16} noWrap>
|
||||
{name}
|
||||
</Typography>
|
||||
|
||||
{/* السعر */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={600}
|
||||
sx={{ color: "#FF914D", fontSize: "15px", mt: 0.5 }}
|
||||
>
|
||||
${price}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
>
|
||||
{unit || "/pcs"}
|
||||
</Typography>
|
||||
</Typography>
|
||||
|
||||
{/* الوصف (أول عنصر فقط للتصغير) */}
|
||||
{description && description.length > 0 && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
sx={{ display: "block", mt: 0.5 }}
|
||||
>
|
||||
{description[0]}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* الإضافات (أول 2 فقط) */}
|
||||
{/* {additions && additions.length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 0.5,
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
{additions.slice(0, 2).map((add, i) => (
|
||||
<Chip
|
||||
key={i}
|
||||
label={add}
|
||||
size="small"
|
||||
sx={{
|
||||
fontSize: "10px",
|
||||
height: 18,
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)} */}
|
||||
|
||||
{/* زر الإضافة */}
|
||||
{/* <IconButton
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onAdd) onAdd(meal);
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: "#FF8551",
|
||||
color: "#fff",
|
||||
mt: 1,
|
||||
width: 28,
|
||||
height: 28,
|
||||
"&:hover": { backgroundColor: "#ff7043" },
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton> */}
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MealCard;
|
||||