This commit is contained in:
yotakii
2026-01-14 14:57:45 +03:00
parent 9dda03b40d
commit 085eaf5fa2
8 changed files with 356 additions and 219 deletions

View File

@@ -33,7 +33,6 @@ import ContentManagement from './pages/admin/ContentManagement';
import RoomManagement from './pages/admin/RoomManagement'; import RoomManagement from './pages/admin/RoomManagement';
import BookingManagement from './pages/admin/BookingManagement'; import BookingManagement from './pages/admin/BookingManagement';
import MediaManagement from './pages/admin/MediaManagement'; import MediaManagement from './pages/admin/MediaManagement';
import BlogManagement from './pages/admin/BlogManagement';
import SettingsManagement from './pages/admin/SettingsManagement'; import SettingsManagement from './pages/admin/SettingsManagement';
// Loading Component // Loading Component
@@ -93,18 +92,16 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
> >
{/* NEW: redirect /admin -> /admin/dashboard */}
<Route index element={<Navigate to="dashboard" replace />} /> <Route index element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={<DashboardMain />} /> <Route path="dashboard" element={<DashboardMain />} />
<Route path="content" element={<ContentManagement />} /> <Route path="content" element={<ContentManagement />} />
<Route path="rooms" element={<RoomManagement />} /> <Route path="rooms" element={<RoomManagement />} />
<Route path="bookings" element={<BookingManagement />} /> <Route path="bookings" element={<BookingManagement />} />
<Route path="blog" element={<BlogManagement />} />
<Route path="media" element={<MediaManagement />} /> <Route path="media" element={<MediaManagement />} />
<Route path="settings" element={<SettingsManagement />} /> <Route path="settings" element={<SettingsManagement />} />
{/* Optional fallback: keep if you want unknown admin routes to land on dashboard */} {/* أي مسار غير معروف داخل /admin يروح للداشبورد */}
<Route path="*" element={<DashboardMain />} /> <Route path="*" element={<DashboardMain />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -22,37 +22,28 @@ import {
import { import {
Menu as MenuIcon, Menu as MenuIcon,
Dashboard as DashboardIcon, Dashboard as DashboardIcon,
Article as ArticleIcon,
Hotel as HotelIcon, Hotel as HotelIcon,
CalendarMonth as CalendarIcon, CalendarMonth as CalendarIcon,
Settings as SettingsIcon, Settings as SettingsIcon,
Image as ImageIcon, Image as ImageIcon,
ContentPaste as ContentIcon, ContentPaste as ContentIcon,
People as PeopleIcon,
Logout as LogoutIcon, Logout as LogoutIcon,
AccountCircle as AccountIcon, AccountCircle as AccountIcon
Assessment as AssessmentIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
const drawerWidth = 260; const drawerWidth = 260;
// ✅ Removed: Blog, Guests, Analytics (UI only)
const menuItems = [ const menuItems = [
{ title: 'Dashboard', path: '/admin/dashboard', icon: <DashboardIcon /> }, { title: 'Dashboard', path: '/admin/dashboard', icon: <DashboardIcon /> },
{ title: 'Content', path: '/admin/content', icon: <ContentIcon /> }, { title: 'Content', path: '/admin/content', icon: <ContentIcon /> },
{ title: 'Rooms', path: '/admin/rooms', icon: <HotelIcon /> }, { title: 'Rooms', path: '/admin/rooms', icon: <HotelIcon /> },
{ title: 'Bookings', path: '/admin/bookings', icon: <CalendarIcon /> }, { title: 'Bookings', path: '/admin/bookings', icon: <CalendarIcon /> },
{ title: 'Blog', path: '/admin/blog', icon: <ArticleIcon /> },
{ title: 'Media', path: '/admin/media', icon: <ImageIcon /> }, { title: 'Media', path: '/admin/media', icon: <ImageIcon /> },
{ title: 'Guests', path: '/admin/guests', icon: <PeopleIcon /> },
{ title: 'Analytics', path: '/admin/analytics', icon: <AssessmentIcon /> },
{ title: 'Settings', path: '/admin/settings', icon: <SettingsIcon /> }, { title: 'Settings', path: '/admin/settings', icon: <SettingsIcon /> },
]; ];
// ✅ hide from sidebar only (keep code/routes for future)
const hiddenMenuTitles = new Set(['Guests', 'Blog']);
const adminMenuItems = menuItems.filter(item => !hiddenMenuTitles.has(item.title));
const AdminLayout = () => { const AdminLayout = () => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@@ -82,9 +73,7 @@ const AdminLayout = () => {
const handleNavigate = (path) => { const handleNavigate = (path) => {
navigate(path); navigate(path);
if (isMobile) { if (isMobile) setMobileOpen(false);
setMobileOpen(false);
}
}; };
const drawer = ( const drawer = (
@@ -106,7 +95,7 @@ const AdminLayout = () => {
</Toolbar> </Toolbar>
<Divider /> <Divider />
<List> <List>
{adminMenuItems.map((item) => ( {menuItems.map((item) => (
<ListItem key={item.title} disablePadding> <ListItem key={item.title} disablePadding>
<ListItemButton <ListItemButton
selected={location.pathname === item.path} selected={location.pathname === item.path}
@@ -157,6 +146,7 @@ const AdminLayout = () => {
> >
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}> <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
The Old Vine Hotel The Old Vine Hotel
</Typography> </Typography>
@@ -175,14 +165,8 @@ const AdminLayout = () => {
anchorEl={anchorEl} anchorEl={anchorEl}
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
onClose={handleProfileMenuClose} onClose={handleProfileMenuClose}
anchorOrigin={{ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
vertical: 'bottom', transformOrigin={{ vertical: 'top', horizontal: 'right' }}
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
> >
<MenuItem disabled> <MenuItem disabled>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
@@ -206,38 +190,25 @@ const AdminLayout = () => {
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Box <Box component="nav" sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}>
component="nav"
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
>
{/* Mobile drawer */}
<Drawer <Drawer
variant="temporary" variant="temporary"
open={mobileOpen} open={mobileOpen}
onClose={handleDrawerToggle} onClose={handleDrawerToggle}
ModalProps={{ ModalProps={{ keepMounted: true }}
keepMounted: true, // Better open performance on mobile.
}}
sx={{ sx={{
display: { xs: 'block', md: 'none' }, display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': { '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
boxSizing: 'border-box',
width: drawerWidth,
},
}} }}
> >
{drawer} {drawer}
</Drawer> </Drawer>
{/* Desktop drawer */}
<Drawer <Drawer
variant="permanent" variant="permanent"
sx={{ sx={{
display: { xs: 'none', md: 'block' }, display: { xs: 'none', md: 'block' },
'& .MuiDrawer-paper': { '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
boxSizing: 'border-box',
width: drawerWidth,
},
}} }}
open open
> >

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
Box, Box,
Container, Container,
@@ -24,11 +24,74 @@ import {
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import api from '../../utils/api';
// ✅ Convert any weird value (like {coordinates}) into a safe string
const toText = (val, fallback = '') => {
if (val == null) return fallback;
if (typeof val === 'string') return val;
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
if (Array.isArray(val)) return val.map(v => toText(v, '')).filter(Boolean).join(', ');
if (typeof val === 'object') {
// common cases
if (typeof val.text === 'string') return val.text;
if (typeof val.label === 'string') return val.label;
if (typeof val.name === 'string') return val.name;
if (typeof val.address === 'string') return val.address;
// GeoJSON-ish: { coordinates: [lng, lat] } or { coordinates: {lat,lng} }
if (val.coordinates) {
if (Array.isArray(val.coordinates) && val.coordinates.length >= 2) {
const [lng, lat] = val.coordinates;
if (lat != null && lng != null) return `${lat}, ${lng}`;
}
if (typeof val.coordinates === 'object') {
const lat = val.coordinates.lat ?? val.coordinates.latitude;
const lng = val.coordinates.lng ?? val.coordinates.lon ?? val.coordinates.longitude;
if (lat != null && lng != null) return `${lat}, ${lng}`;
}
}
return fallback;
}
return fallback;
};
const Footer = () => { const Footer = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const [settings, setSettings] = useState(null);
useEffect(() => {
let mounted = true;
(async () => {
try {
const res = await api.get('/api/settings/public');
if (mounted) setSettings(res?.data?.data?.settings || null);
} catch (e) {
console.error('Footer settings load error:', e);
}
})();
return () => { mounted = false; };
}, []);
const hotelName = toText(settings?.hotel?.name, 'The Old Vine Hotel');
const address = toText(settings?.hotel?.address, 'Old Damascus City');
const phone = toText(settings?.hotel?.phone, '0112241609');
const email = toText(settings?.hotel?.email, 'reservations@oldvinehotel.com');
const whatsapp = toText(settings?.hotel?.whatsapp, '+963986105010');
const social = settings?.hotel?.socialMedia || {};
const socialLinks = useMemo(() => ([
{ icon: <Facebook />, url: toText(social.facebook, ''), label: 'Facebook' },
{ icon: <Instagram />, url: toText(social.instagram, ''), label: 'Instagram' },
{ icon: <Twitter />, url: toText(social.twitter, ''), label: 'Twitter' },
{ icon: <LinkedIn />, url: toText(social.linkedin, ''), label: 'LinkedIn' },
].filter(s => s.url)), [social]);
const quickLinks = [ const quickLinks = [
{ label: t('nav.home'), path: '/' }, { label: t('nav.home'), path: '/' },
{ label: t('nav.about'), path: '/about' }, { label: t('nav.about'), path: '/about' },
@@ -38,13 +101,6 @@ const Footer = () => {
{ label: t('nav.contact'), path: '/contact' }, { label: t('nav.contact'), path: '/contact' },
]; ];
const socialLinks = [
{ icon: <Facebook />, url: 'https://facebook.com/oldvinehotel', label: 'Facebook' },
{ icon: <Instagram />, url: 'https://instagram.com/oldvinehotel', label: 'Instagram' },
{ icon: <Twitter />, url: 'https://twitter.com/oldvinehotel', label: 'Twitter' },
{ icon: <LinkedIn />, url: 'https://linkedin.com/company/oldvinehotel', label: 'LinkedIn' },
];
return ( return (
<Box <Box
component="footer" component="footer"
@@ -58,7 +114,6 @@ const Footer = () => {
> >
<Container maxWidth="lg"> <Container maxWidth="lg">
<Grid container spacing={4}> <Grid container spacing={4}>
{/* Hotel Information */}
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@@ -75,19 +130,19 @@ const Footer = () => {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}} }}
> >
The Old Vine Hotel {hotelName}
</Typography> </Typography>
<Typography variant="body2" sx={{ mb: 3, lineHeight: 1.7 }}> <Typography variant="body2" sx={{ mb: 3, lineHeight: 1.7 }}>
{t('footer.description')} {t('footer.description')}
</Typography> </Typography>
{/* Social Media Links */}
<Box sx={{ display: 'flex', gap: 1, mb: 3 }}> <Box sx={{ display: 'flex', gap: 1, mb: 3 }}>
{socialLinks.map((social) => ( {socialLinks.map((s) => (
<IconButton <IconButton
key={social.label} key={s.label}
component="a" component="a"
href={social.url} href={s.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
sx={{ sx={{
@@ -100,14 +155,13 @@ const Footer = () => {
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
}} }}
> >
{social.icon} {s.icon}
</IconButton> </IconButton>
))} ))}
</Box> </Box>
</motion.div> </motion.div>
</Grid> </Grid>
{/* Quick Links */}
<Grid item xs={12} sm={6} md={2}> <Grid item xs={12} sm={6} md={2}>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@@ -122,31 +176,29 @@ const Footer = () => {
> >
{t('footer.quickLinks')} {t('footer.quickLinks')}
</Typography> </Typography>
<Box component="nav"> <Box component="nav">
{quickLinks.map((link) => ( {quickLinks.map((l) => (
<Link <Link
key={link.path} key={l.path}
component={RouterLink} component={RouterLink}
to={link.path} to={l.path}
sx={{ sx={{
display: 'block', display: 'block',
color: 'rgba(255, 255, 255, 0.8)', color: 'rgba(255, 255, 255, 0.8)',
textDecoration: 'none', textDecoration: 'none',
mb: 1, mb: 1,
transition: 'color 0.3s ease', transition: 'color 0.3s ease',
'&:hover': { '&:hover': { color: theme.palette.secondary.main },
color: theme.palette.secondary.main,
},
}} }}
> >
{link.label} {l.label}
</Link> </Link>
))} ))}
</Box> </Box>
</motion.div> </motion.div>
</Grid> </Grid>
{/* Contact Information */}
<Grid item xs={12} sm={6} md={3}> <Grid item xs={12} sm={6} md={3}>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@@ -161,38 +213,29 @@ const Footer = () => {
> >
{t('footer.contactInfo')} {t('footer.contactInfo')}
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<LocationOn sx={{ mr: 1, fontSize: 20 }} /> <LocationOn sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2"> <Typography variant="body2">{address}</Typography>
Old Damascus City
</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Phone sx={{ mr: 1, fontSize: 20 }} /> <Phone sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2"> <Typography variant="body2">{phone}</Typography>
0112241609
</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Email sx={{ mr: 1, fontSize: 20 }} /> <Email sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2"> <Typography variant="body2">{email}</Typography>
reservations@oldvinehotel.com
</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<WhatsApp sx={{ mr: 1, fontSize: 20 }} /> <WhatsApp sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2"> <Typography variant="body2">{whatsapp}</Typography>
+963986105010
</Typography>
</Box> </Box>
</motion.div> </motion.div>
</Grid> </Grid>
{/* Newsletter Signup */}
<Grid item xs={12} md={3}> <Grid item xs={12} md={3}>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@@ -210,7 +253,7 @@ const Footer = () => {
<Typography variant="body2" sx={{ mb: 2 }}> <Typography variant="body2" sx={{ mb: 2 }}>
{t('footer.newsletterText')} {t('footer.newsletterText')}
</Typography> </Typography>
<Box component="form" sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box component="form" sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField <TextField
placeholder={t('footer.emailPlaceholder')} placeholder={t('footer.emailPlaceholder')}
@@ -220,15 +263,9 @@ const Footer = () => {
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
backgroundColor: 'rgba(255, 255, 255, 0.1)', backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'white', color: 'white',
'& fieldset': { '& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' },
borderColor: 'rgba(255, 255, 255, 0.3)', '&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' },
}, '&.Mui-focused fieldset': { borderColor: theme.palette.secondary.main },
'&:hover fieldset': {
borderColor: 'rgba(255, 255, 255, 0.5)',
},
'&.Mui-focused fieldset': {
borderColor: theme.palette.secondary.main,
},
}, },
'& .MuiInputBase-input::placeholder': { '& .MuiInputBase-input::placeholder': {
color: 'rgba(255, 255, 255, 0.7)', color: 'rgba(255, 255, 255, 0.7)',
@@ -236,13 +273,7 @@ const Footer = () => {
}, },
}} }}
/> />
<Button <Button variant="contained" color="secondary" sx={{ fontWeight: 600 }}>
variant="contained"
color="secondary"
sx={{
fontWeight: 600,
}}
>
{t('footer.subscribe')} {t('footer.subscribe')}
</Button> </Button>
</Box> </Box>
@@ -252,7 +283,6 @@ const Footer = () => {
<Divider sx={{ my: 4, borderColor: 'rgba(255, 255, 255, 0.2)' }} /> <Divider sx={{ my: 4, borderColor: 'rgba(255, 255, 255, 0.2)' }} />
{/* Bottom Section */}
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -263,9 +293,9 @@ const Footer = () => {
}} }}
> >
<Typography variant="body2" color="rgba(255, 255, 255, 0.8)"> <Typography variant="body2" color="rgba(255, 255, 255, 0.8)">
© {new Date().getFullYear()} The Old Vine Hotel. {t('footer.rights')} © {new Date().getFullYear()} {hotelName}. {t('footer.rights')}
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 3 }}> <Box sx={{ display: 'flex', gap: 3 }}>
<Link <Link
href="/privacy" href="/privacy"
@@ -273,9 +303,7 @@ const Footer = () => {
color: 'rgba(255, 255, 255, 0.8)', color: 'rgba(255, 255, 255, 0.8)',
textDecoration: 'none', textDecoration: 'none',
fontSize: '0.875rem', fontSize: '0.875rem',
'&:hover': { '&:hover': { color: theme.palette.secondary.main },
color: theme.palette.secondary.main,
},
}} }}
> >
{t('footer.privacy')} {t('footer.privacy')}
@@ -286,9 +314,7 @@ const Footer = () => {
color: 'rgba(255, 255, 255, 0.8)', color: 'rgba(255, 255, 255, 0.8)',
textDecoration: 'none', textDecoration: 'none',
fontSize: '0.875rem', fontSize: '0.875rem',
'&:hover': { '&:hover': { color: theme.palette.secondary.main },
color: theme.palette.secondary.main,
},
}} }}
> >
{t('footer.terms')} {t('footer.terms')}
@@ -300,4 +326,4 @@ const Footer = () => {
); );
}; };
export default Footer; export default Footer;

View File

@@ -36,12 +36,6 @@ const Header = () => {
const lightHeaderText = '#1a1a1a'; const lightHeaderText = '#1a1a1a';
const WA_NUMBER = '963986105010';
const WA_TEXT = encodeURIComponent(
'For all booking inquiries and reservation confirmations, kindly contact us via WhatsApp'
);
const WA_LINK = `https://wa.me/${WA_NUMBER}?text=${WA_TEXT}`;
const navigationItems = [ const navigationItems = [
{ label: t('nav.home'), path: '/' }, { label: t('nav.home'), path: '/' },
{ label: t('nav.about'), path: '/about' }, { label: t('nav.about'), path: '/about' },
@@ -125,12 +119,11 @@ const Header = () => {
</ListItem> </ListItem>
))} ))}
{/* ✅ بدل واتساب: يروح على صفحة /booking */}
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton <ListItemButton
component="a" component={Link}
href={WA_LINK} to="/booking"
target="_blank"
rel="noopener noreferrer"
sx={{ sx={{
textAlign: 'center', textAlign: 'center',
backgroundColor: 'secondary.main', backgroundColor: 'secondary.main',
@@ -235,16 +228,15 @@ const Header = () => {
<LanguageIcon /> <LanguageIcon />
</IconButton> </IconButton>
{/* ✅ بدل واتساب: يروح على صفحة /booking */}
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.3 }} transition={{ duration: 0.5, delay: 0.3 }}
> >
<Button <Button
component="a" component={Link}
href={WA_LINK} to="/booking"
target="_blank"
rel="noopener noreferrer"
variant="contained" variant="contained"
color="secondary" color="secondary"
sx={{ fontWeight: 600, px: 3 }} sx={{ fontWeight: 600, px: 3 }}

View File

@@ -0,0 +1,42 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import api from '../utils/api';
const SettingsContext = createContext({ settings: null, loading: true });
const unwrapSettings = (res) => {
// يدعم كذا شكل للريسبونس
return res?.data?.data?.settings || res?.data?.data || res?.data || null;
};
export const SettingsProvider = ({ children }) => {
const [settings, setSettings] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let mounted = true;
(async () => {
try {
// ✅ public endpoint
const res = await api.get('/api/settings/public');
if (mounted) setSettings(unwrapSettings(res));
} catch (e) {
console.error('Error loading public settings:', e);
} finally {
if (mounted) setLoading(false);
}
})();
return () => { mounted = false; };
}, []);
const value = useMemo(() => ({ settings, loading }), [settings, loading]);
return (
<SettingsContext.Provider value={value}>
{children}
</SettingsContext.Provider>
);
};
export const useSettings = () => useContext(SettingsContext);

View File

@@ -1,11 +1,20 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Container, Typography, Box, TextField, Button, Container,
MenuItem, Alert, CircularProgress Typography,
Box,
TextField,
Button,
MenuItem,
Alert,
CircularProgress
} from '@mui/material'; } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import api from '../utils/api'; import api from '../utils/api';
const Booking = () => { const Booking = () => {
const navigate = useNavigate();
const [rooms, setRooms] = useState([]); const [rooms, setRooms] = useState([]);
const [loadingRooms, setLoadingRooms] = useState(true); const [loadingRooms, setLoadingRooms] = useState(true);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -25,35 +34,69 @@ const Booking = () => {
}); });
useEffect(() => { useEffect(() => {
let mounted = true;
const loadRooms = async () => { const loadRooms = async () => {
try { try {
const res = await api.get('/api/rooms?limit=100'); const res = await api.get('/api/rooms?limit=100');
setRooms(res?.data?.data?.rooms || []); const fetched = res?.data?.data?.rooms || [];
if (mounted) setRooms(fetched);
} catch (e) { } catch (e) {
setMessage({ type: 'error', text: 'Failed to load rooms' }); if (mounted) setMessage({ type: 'error', text: 'Failed to load rooms' });
} finally { } finally {
setLoadingRooms(false); if (mounted) setLoadingRooms(false);
} }
}; };
loadRooms(); loadRooms();
return () => { mounted = false; };
}, []); }, []);
const handleChange = (key) => (e) => { const handleChange = (key) => (e) => {
setForm(prev => ({ ...prev, [key]: e.target.value })); setForm(prev => ({ ...prev, [key]: e.target.value }));
}; };
const validateBeforeSubmit = () => {
// check dates
if (form.checkInDate && form.checkOutDate) {
const inD = new Date(form.checkInDate);
const outD = new Date(form.checkOutDate);
if (inD >= outD) {
setMessage({ type: 'error', text: 'Check-out date must be after check-in date' });
return false;
}
}
const adults = Number(form.adults);
const children = Number(form.children || 0);
if (!Number.isFinite(adults) || adults < 1) {
setMessage({ type: 'error', text: 'Adults must be at least 1' });
return false;
}
if (!Number.isFinite(children) || children < 0) {
setMessage({ type: 'error', text: 'Children cannot be negative' });
return false;
}
return true;
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setSubmitting(true);
setMessage(null); setMessage(null);
if (!validateBeforeSubmit()) return;
setSubmitting(true);
try { try {
const payload = { const payload = {
guestInfo: { guestInfo: {
firstName: form.firstName, firstName: form.firstName.trim(),
lastName: form.lastName, lastName: form.lastName.trim(),
email: form.email, email: form.email.trim(),
phone: form.phone phone: form.phone.trim()
}, },
roomId: form.roomId, roomId: form.roomId,
checkInDate: form.checkInDate, checkInDate: form.checkInDate,
@@ -65,24 +108,37 @@ const Booking = () => {
specialRequests: form.specialRequests specialRequests: form.specialRequests
}; };
// ✅ هذا لازم يكون موجود بالـ CMS: POST /api/bookings/request
const res = await api.post('/api/bookings/request', payload); const res = await api.post('/api/bookings/request', payload);
const bookingNumber = res?.data?.data?.booking?.bookingNumber;
setMessage({ const booking = res?.data?.data?.booking || null;
type: 'success', const bookingNumber = booking?.bookingNumber || res?.data?.data?.bookingNumber || null;
text: bookingNumber
? `Booking request submitted! Booking #: ${bookingNumber}` const selectedRoom = rooms.find(r => r._id === form.roomId);
: 'Booking request submitted successfully!'
}); // ✅ معلومات نبعثها لصفحة BookingConfirmation
const requestForConfirmation = {
fullName: `${form.firstName} ${form.lastName}`.trim(),
phone: form.phone,
email: form.email,
checkInDate: form.checkInDate,
checkOutDate: form.checkOutDate,
adults: Number(form.adults),
children: Number(form.children || 0),
roomCategory: selectedRoom?.name || '',
message: form.specialRequests || '',
bookingNumber: bookingNumber || '',
autoOpenWhatsApp: true // إذا صفحة الـ Confirmation عندك بتفتح واتساب تلقائيًا
};
// ✅ روح على صفحة التأكيد (بدون ما نظل هون)
navigate('/booking/confirmation', { state: { request: requestForConfirmation } });
// reset minimal
setForm(prev => ({ ...prev, specialRequests: '' }));
} catch (error) { } catch (error) {
setMessage({ setMessage({
type: 'error', type: 'error',
text: error.response?.data?.message || 'Failed to submit booking request' text: error.response?.data?.message || 'Failed to submit booking request'
}); });
} finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
@@ -109,7 +165,11 @@ const Booking = () => {
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : ( ) : (
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box
component="form"
onSubmit={handleSubmit}
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
>
<TextField label="First Name" value={form.firstName} onChange={handleChange('firstName')} required /> <TextField label="First Name" value={form.firstName} onChange={handleChange('firstName')} required />
<TextField label="Last Name" value={form.lastName} onChange={handleChange('lastName')} required /> <TextField label="Last Name" value={form.lastName} onChange={handleChange('lastName')} required />
<TextField label="Email" type="email" value={form.email} onChange={handleChange('email')} required /> <TextField label="Email" type="email" value={form.email} onChange={handleChange('email')} required />

View File

@@ -1,30 +1,118 @@
import React from 'react'; import React, { useEffect, useMemo } from 'react';
import { Container, Typography, Box } from '@mui/material'; import { Container, Typography, Box, Button, Paper, Divider } from '@mui/material';
import { useLocation, Link } from 'react-router-dom';
const BookingConfirmation = () => { const BookingConfirmation = () => {
const location = useLocation();
// نتوقع إنه صفحة الحجز تبعتنا رح تعمل navigate وتبعت state فيه request
const request = location.state?.request || null;
const WA_NUMBER = useMemo(() => {
// إذا عندك رقم ثابت حاليا
return '963986105010';
}, []);
const waText = useMemo(() => {
if (!request) {
return encodeURIComponent('Hello, I would like to book a room at Old Vine Hotel.');
}
const lines = [
'New booking request from website:',
`Name: ${request.fullName || ''}`,
`Phone: ${request.phone || ''}`,
request.email ? `Email: ${request.email}` : null,
request.checkInDate ? `Check-in: ${new Date(request.checkInDate).toLocaleDateString()}` : null,
request.checkOutDate ? `Check-out: ${new Date(request.checkOutDate).toLocaleDateString()}` : null,
request.adults != null ? `Adults: ${request.adults}` : null,
request.children != null ? `Children: ${request.children}` : null,
request.roomCategory ? `Category: ${request.roomCategory}` : null,
request.message ? `Message: ${request.message}` : null,
].filter(Boolean);
return encodeURIComponent(lines.join('\n'));
}, [request]);
const WA_LINK = useMemo(() => `https://wa.me/${WA_NUMBER}?text=${waText}`, [WA_NUMBER, waText]);
// خيار: تفتح واتساب أوتوماتيك بعد نجاح الإرسال
useEffect(() => {
if (request?.autoOpenWhatsApp) {
window.open(WA_LINK, '_blank', 'noopener,noreferrer');
}
}, [request, WA_LINK]);
return ( return (
<Container maxWidth="lg" sx={{ py: 8 }}> <Container maxWidth="lg" sx={{ py: 8 }}>
<Box sx={{ textAlign: 'center', mb: 6 }}> <Box sx={{ textAlign: 'center', mb: 4 }}>
<Typography variant="h3" component="h1" gutterBottom> <Typography variant="h3" component="h1" gutterBottom>
Booking Confirmation Booking Request Sent
</Typography> </Typography>
<Typography variant="h5" color="text.secondary"> <Typography variant="h5" color="text.secondary">
Your reservation has been confirmed We received your request
</Typography> </Typography>
</Box> </Box>
<Box sx={{
display: 'flex', <Box sx={{ display: 'flex', justifyContent: 'center' }}>
flexDirection: 'column', <Paper sx={{ p: 4, width: '100%', maxWidth: 720 }}>
alignItems: 'center', <Typography variant="body1" sx={{ textAlign: 'center', mb: 3 }}>
gap: 4 Thank you! Our team will contact you soon. You can also confirm faster via WhatsApp.
}}> </Typography>
<Typography variant="body1" sx={{ textAlign: 'center', maxWidth: 800 }}>
Thank you for your booking. You will receive a confirmation email shortly. {request && (
</Typography> <>
{/* Add booking confirmation details here */} <Divider sx={{ my: 2 }} />
<Typography variant="h6" sx={{ mb: 2 }}>
Request Summary
</Typography>
<Box sx={{ display: 'grid', gap: 1 }}>
<Typography><b>Name:</b> {request.fullName || ''}</Typography>
<Typography><b>Phone:</b> {request.phone || ''}</Typography>
{request.email && <Typography><b>Email:</b> {request.email}</Typography>}
{request.roomCategory && <Typography><b>Category:</b> {request.roomCategory}</Typography>}
{request.checkInDate && <Typography><b>Check-in:</b> {new Date(request.checkInDate).toLocaleDateString()}</Typography>}
{request.checkOutDate && <Typography><b>Check-out:</b> {new Date(request.checkOutDate).toLocaleDateString()}</Typography>}
{(request.adults != null || request.children != null) && (
<Typography><b>Guests:</b> {request.adults ?? 0} adults, {request.children ?? 0} children</Typography>
)}
{request.message && <Typography><b>Message:</b> {request.message}</Typography>}
</Box>
</>
)}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mt: 4, justifyContent: 'center' }}>
<Button
component="a"
href={WA_LINK}
target="_blank"
rel="noopener noreferrer"
variant="contained"
color="secondary"
sx={{ px: 4 }}
>
Open WhatsApp
</Button>
<Button component={Link} to="/rooms" variant="outlined" sx={{ px: 4 }}>
Browse Rooms
</Button>
<Button component={Link} to="/" variant="text" sx={{ px: 4 }}>
Back Home
</Button>
</Box>
{!request && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', textAlign: 'center', mt: 2 }}>
(No request details were passed to this page.)
</Typography>
)}
</Paper>
</Box> </Box>
</Container> </Container>
); );
}; };
export default BookingConfirmation; export default BookingConfirmation;

View File

@@ -13,8 +13,8 @@ import {
Hotel as HotelIcon, Hotel as HotelIcon,
CalendarMonth as CalendarIcon, CalendarMonth as CalendarIcon,
People as PeopleIcon, People as PeopleIcon,
Article as ArticleIcon, TrendingUp as TrendingUpIcon,
TrendingUp as TrendingUpIcon ContentPaste as ContentIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import api from '../../utils/api'; import api from '../../utils/api';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
@@ -73,9 +73,7 @@ const DashboardMain = () => {
const fetchStats = async () => { const fetchStats = async () => {
try { try {
setLoading(true); setLoading(true);
console.log('📊 Dashboard: Fetching stats...');
const response = await api.get('/api/admin/stats'); const response = await api.get('/api/admin/stats');
console.log('📊 Dashboard: Stats received:', response.data);
setStats(response.data.data.stats); setStats(response.data.data.stats);
} catch (err) { } catch (err) {
console.error('📊 Dashboard: Error fetching stats:', err); console.error('📊 Dashboard: Error fetching stats:', err);
@@ -93,9 +91,7 @@ const DashboardMain = () => {
); );
} }
if (error) { if (error) return <Alert severity="error">{error}</Alert>;
return <Alert severity="error">{error}</Alert>;
}
return ( return (
<Box> <Box>
@@ -109,7 +105,6 @@ const DashboardMain = () => {
</Box> </Box>
<Grid container spacing={3}> <Grid container spacing={3}>
{/* Stats Cards */}
<Grid item xs={12} sm={6} md={4}> <Grid item xs={12} sm={6} md={4}>
<StatCard <StatCard
title="Total Rooms" title="Total Rooms"
@@ -139,17 +134,7 @@ const DashboardMain = () => {
color="#7CBF9E" color="#7CBF9E"
/> />
</Grid> </Grid>
{/*
<Grid item xs={12} sm={6} md={4}>
<StatCard
title="Blog Posts"
value={stats?.blogPosts || 0}
subtitle="Published articles"
icon={<ArticleIcon />}
color="#3A635F"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}> <Grid item xs={12} sm={6} md={4}>
<StatCard <StatCard
title="Monthly Revenue" title="Monthly Revenue"
@@ -159,6 +144,7 @@ const DashboardMain = () => {
color="#0F2A26" color="#0F2A26"
/> />
</Grid> </Grid>
*/}
{/* Quick Actions */} {/* Quick Actions */}
<Grid item xs={12}> <Grid item xs={12}>
@@ -166,14 +152,12 @@ const DashboardMain = () => {
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Quick Actions Quick Actions
</Typography> </Typography>
<Grid container spacing={2} sx={{ mt: 1 }}> <Grid container spacing={2} sx={{ mt: 1 }}>
<Grid item xs={12} sm={6} md={3}> <Grid item xs={12} sm={6} md={4}>
<Card <Card
sx={{ sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
cursor: 'pointer', onClick={() => (window.location.href = '/admin/rooms')}
'&:hover': { bgcolor: 'action.hover' },
}}
onClick={() => window.location.href = '/admin/rooms'}
> >
<CardContent sx={{ textAlign: 'center', py: 3 }}> <CardContent sx={{ textAlign: 'center', py: 3 }}>
<HotelIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} /> <HotelIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
@@ -182,13 +166,10 @@ const DashboardMain = () => {
</Card> </Card>
</Grid> </Grid>
<Grid item xs={12} sm={6} md={3}> <Grid item xs={12} sm={6} md={4}>
<Card <Card
sx={{ sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
cursor: 'pointer', onClick={() => (window.location.href = '/admin/bookings')}
'&:hover': { bgcolor: 'action.hover' },
}}
onClick={() => window.location.href = '/admin/bookings'}
> >
<CardContent sx={{ textAlign: 'center', py: 3 }}> <CardContent sx={{ textAlign: 'center', py: 3 }}>
<CalendarIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} /> <CalendarIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
@@ -197,31 +178,13 @@ const DashboardMain = () => {
</Card> </Card>
</Grid> </Grid>
<Grid item xs={12} sm={6} md={3}> <Grid item xs={12} sm={6} md={4}>
<Card <Card
sx={{ sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
cursor: 'pointer', onClick={() => (window.location.href = '/admin/content')}
'&:hover': { bgcolor: 'action.hover' },
}}
onClick={() => window.location.href = '/admin/blog'}
> >
<CardContent sx={{ textAlign: 'center', py: 3 }}> <CardContent sx={{ textAlign: 'center', py: 3 }}>
<ArticleIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} /> <ContentIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
<Typography variant="h6">Write Blog Post</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card
sx={{
cursor: 'pointer',
'&:hover': { bgcolor: 'action.hover' },
}}
onClick={() => window.location.href = '/admin/content'}
>
<CardContent sx={{ textAlign: 'center', py: 3 }}>
<ArticleIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
<Typography variant="h6">Edit Content</Typography> <Typography variant="h6">Edit Content</Typography>
</CardContent> </CardContent>
</Card> </Card>
@@ -230,7 +193,6 @@ const DashboardMain = () => {
</Paper> </Paper>
</Grid> </Grid>
{/* Recent Activity - Placeholder */}
<Grid item xs={12}> <Grid item xs={12}>
<Paper sx={{ p: 3 }}> <Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
@@ -249,4 +211,3 @@ const DashboardMain = () => {
}; };
export default DashboardMain; export default DashboardMain;