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

View File

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

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Container,
@@ -24,11 +24,74 @@ import {
import { Link as RouterLink } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
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 { t } = useTranslation();
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 = [
{ label: t('nav.home'), path: '/' },
{ label: t('nav.about'), path: '/about' },
@@ -38,13 +101,6 @@ const Footer = () => {
{ 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 (
<Box
component="footer"
@@ -58,7 +114,6 @@ const Footer = () => {
>
<Container maxWidth="lg">
<Grid container spacing={4}>
{/* Hotel Information */}
<Grid item xs={12} md={4}>
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -75,19 +130,19 @@ const Footer = () => {
color: theme.palette.secondary.main,
}}
>
The Old Vine Hotel
{hotelName}
</Typography>
<Typography variant="body2" sx={{ mb: 3, lineHeight: 1.7 }}>
{t('footer.description')}
</Typography>
{/* Social Media Links */}
<Box sx={{ display: 'flex', gap: 1, mb: 3 }}>
{socialLinks.map((social) => (
{socialLinks.map((s) => (
<IconButton
key={social.label}
key={s.label}
component="a"
href={social.url}
href={s.url}
target="_blank"
rel="noopener noreferrer"
sx={{
@@ -100,14 +155,13 @@ const Footer = () => {
transition: 'all 0.3s ease',
}}
>
{social.icon}
{s.icon}
</IconButton>
))}
</Box>
</motion.div>
</Grid>
{/* Quick Links */}
<Grid item xs={12} sm={6} md={2}>
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -122,31 +176,29 @@ const Footer = () => {
>
{t('footer.quickLinks')}
</Typography>
<Box component="nav">
{quickLinks.map((link) => (
{quickLinks.map((l) => (
<Link
key={link.path}
key={l.path}
component={RouterLink}
to={link.path}
to={l.path}
sx={{
display: 'block',
color: 'rgba(255, 255, 255, 0.8)',
textDecoration: 'none',
mb: 1,
transition: 'color 0.3s ease',
'&:hover': {
color: theme.palette.secondary.main,
},
'&:hover': { color: theme.palette.secondary.main },
}}
>
{link.label}
{l.label}
</Link>
))}
</Box>
</motion.div>
</Grid>
{/* Contact Information */}
<Grid item xs={12} sm={6} md={3}>
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -164,35 +216,26 @@ const Footer = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<LocationOn sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2">
Old Damascus City
</Typography>
<Typography variant="body2">{address}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Phone sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2">
0112241609
</Typography>
<Typography variant="body2">{phone}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Email sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2">
reservations@oldvinehotel.com
</Typography>
<Typography variant="body2">{email}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<WhatsApp sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2">
+963986105010
</Typography>
<Typography variant="body2">{whatsapp}</Typography>
</Box>
</motion.div>
</Grid>
{/* Newsletter Signup */}
<Grid item xs={12} md={3}>
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -220,15 +263,9 @@ const Footer = () => {
'& .MuiOutlinedInput-root': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'white',
'& fieldset': {
borderColor: 'rgba(255, 255, 255, 0.3)',
},
'&:hover fieldset': {
borderColor: 'rgba(255, 255, 255, 0.5)',
},
'&.Mui-focused fieldset': {
borderColor: theme.palette.secondary.main,
},
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' },
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' },
'&.Mui-focused fieldset': { borderColor: theme.palette.secondary.main },
},
'& .MuiInputBase-input::placeholder': {
color: 'rgba(255, 255, 255, 0.7)',
@@ -236,13 +273,7 @@ const Footer = () => {
},
}}
/>
<Button
variant="contained"
color="secondary"
sx={{
fontWeight: 600,
}}
>
<Button variant="contained" color="secondary" sx={{ fontWeight: 600 }}>
{t('footer.subscribe')}
</Button>
</Box>
@@ -252,7 +283,6 @@ const Footer = () => {
<Divider sx={{ my: 4, borderColor: 'rgba(255, 255, 255, 0.2)' }} />
{/* Bottom Section */}
<Box
sx={{
display: 'flex',
@@ -263,7 +293,7 @@ const Footer = () => {
}}
>
<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>
<Box sx={{ display: 'flex', gap: 3 }}>
@@ -273,9 +303,7 @@ const Footer = () => {
color: 'rgba(255, 255, 255, 0.8)',
textDecoration: 'none',
fontSize: '0.875rem',
'&:hover': {
color: theme.palette.secondary.main,
},
'&:hover': { color: theme.palette.secondary.main },
}}
>
{t('footer.privacy')}
@@ -286,9 +314,7 @@ const Footer = () => {
color: 'rgba(255, 255, 255, 0.8)',
textDecoration: 'none',
fontSize: '0.875rem',
'&:hover': {
color: theme.palette.secondary.main,
},
'&:hover': { color: theme.palette.secondary.main },
}}
>
{t('footer.terms')}

View File

@@ -36,12 +36,6 @@ const Header = () => {
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 = [
{ label: t('nav.home'), path: '/' },
{ label: t('nav.about'), path: '/about' },
@@ -125,12 +119,11 @@ const Header = () => {
</ListItem>
))}
{/* ✅ بدل واتساب: يروح على صفحة /booking */}
<ListItem disablePadding>
<ListItemButton
component="a"
href={WA_LINK}
target="_blank"
rel="noopener noreferrer"
component={Link}
to="/booking"
sx={{
textAlign: 'center',
backgroundColor: 'secondary.main',
@@ -235,16 +228,15 @@ const Header = () => {
<LanguageIcon />
</IconButton>
{/* ✅ بدل واتساب: يروح على صفحة /booking */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<Button
component="a"
href={WA_LINK}
target="_blank"
rel="noopener noreferrer"
component={Link}
to="/booking"
variant="contained"
color="secondary"
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 {
Container, Typography, Box, TextField, Button,
MenuItem, Alert, CircularProgress
Container,
Typography,
Box,
TextField,
Button,
MenuItem,
Alert,
CircularProgress
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import api from '../utils/api';
const Booking = () => {
const navigate = useNavigate();
const [rooms, setRooms] = useState([]);
const [loadingRooms, setLoadingRooms] = useState(true);
const [submitting, setSubmitting] = useState(false);
@@ -25,35 +34,69 @@ const Booking = () => {
});
useEffect(() => {
let mounted = true;
const loadRooms = async () => {
try {
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) {
setMessage({ type: 'error', text: 'Failed to load rooms' });
if (mounted) setMessage({ type: 'error', text: 'Failed to load rooms' });
} finally {
setLoadingRooms(false);
if (mounted) setLoadingRooms(false);
}
};
loadRooms();
return () => { mounted = false; };
}, []);
const handleChange = (key) => (e) => {
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) => {
e.preventDefault();
setSubmitting(true);
setMessage(null);
if (!validateBeforeSubmit()) return;
setSubmitting(true);
try {
const payload = {
guestInfo: {
firstName: form.firstName,
lastName: form.lastName,
email: form.email,
phone: form.phone
firstName: form.firstName.trim(),
lastName: form.lastName.trim(),
email: form.email.trim(),
phone: form.phone.trim()
},
roomId: form.roomId,
checkInDate: form.checkInDate,
@@ -65,24 +108,37 @@ const Booking = () => {
specialRequests: form.specialRequests
};
// ✅ هذا لازم يكون موجود بالـ CMS: POST /api/bookings/request
const res = await api.post('/api/bookings/request', payload);
const bookingNumber = res?.data?.data?.booking?.bookingNumber;
setMessage({
type: 'success',
text: bookingNumber
? `Booking request submitted! Booking #: ${bookingNumber}`
: 'Booking request submitted successfully!'
});
const booking = res?.data?.data?.booking || null;
const bookingNumber = booking?.bookingNumber || res?.data?.data?.bookingNumber || null;
const selectedRoom = rooms.find(r => r._id === form.roomId);
// ✅ معلومات نبعثها لصفحة 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) {
setMessage({
type: 'error',
text: error.response?.data?.message || 'Failed to submit booking request'
});
} finally {
setSubmitting(false);
}
};
@@ -109,7 +165,11 @@ const Booking = () => {
<CircularProgress />
</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="Last Name" value={form.lastName} onChange={handleChange('lastName')} required />
<TextField label="Email" type="email" value={form.email} onChange={handleChange('email')} required />

View File

@@ -1,27 +1,115 @@
import React from 'react';
import { Container, Typography, Box } from '@mui/material';
import React, { useEffect, useMemo } from 'react';
import { Container, Typography, Box, Button, Paper, Divider } from '@mui/material';
import { useLocation, Link } from 'react-router-dom';
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 (
<Container maxWidth="lg" sx={{ py: 8 }}>
<Box sx={{ textAlign: 'center', mb: 6 }}>
<Box sx={{ textAlign: 'center', mb: 4 }}>
<Typography variant="h3" component="h1" gutterBottom>
Booking Confirmation
Booking Request Sent
</Typography>
<Typography variant="h5" color="text.secondary">
Your reservation has been confirmed
We received your request
</Typography>
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 4
}}>
<Typography variant="body1" sx={{ textAlign: 'center', maxWidth: 800 }}>
Thank you for your booking. You will receive a confirmation email shortly.
</Typography>
{/* Add booking confirmation details here */}
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Paper sx={{ p: 4, width: '100%', maxWidth: 720 }}>
<Typography variant="body1" sx={{ textAlign: 'center', mb: 3 }}>
Thank you! Our team will contact you soon. You can also confirm faster via WhatsApp.
</Typography>
{request && (
<>
<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>
</Container>
);

View File

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