From 13a004fa40f3922efd3b6958d70b758eaeb8968b Mon Sep 17 00:00:00 2001 From: yotakii Date: Tue, 13 Jan 2026 16:08:18 +0300 Subject: [PATCH] edit --- index.js | 10 +- models/Booking.js | 2 +- routes/blog.js | 34 +++++- routes/bookings.js | 108 ++++++++++++++++++ routes/roomCategories.js | 234 ++++++++++++++++++++++----------------- 5 files changed, 281 insertions(+), 107 deletions(-) diff --git a/index.js b/index.js index fc3717a..658a6ca 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const compression = require('compression'); const morgan = require('morgan'); +const path = require('path'); require('dotenv').config(); // Import routes @@ -33,6 +34,8 @@ const logger = require('./utils/logger'); const app = express(); const PORT = process.env.PORT || 5080; +app.set('trust proxy', 1); + // Security middleware app.use(helmet()); app.use(compression()); @@ -60,6 +63,9 @@ if (process.env.NODE_ENV !== 'test') { app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } })); } + +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + // Database connection console.log('MONGODB_URI:', process.env.MONGODB_URI); mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/oldvinehotel', { @@ -117,7 +123,7 @@ app.get('/', (req, res) => { }); }); -// Error handling middleware (must be last) +// Error handling middleware app.use(errorHandler); // 404 handler @@ -145,4 +151,4 @@ if (process.env.NODE_ENV !== 'test') { }); } -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/models/Booking.js b/models/Booking.js index 72188f7..23bb854 100644 --- a/models/Booking.js +++ b/models/Booking.js @@ -206,7 +206,7 @@ bookingSchema.virtual('duration').get(function() { }); // Pre-save middleware to generate booking number and confirmation code -bookingSchema.pre('save', function(next) { +bookingSchema.pre('validate', function(next) { if (!this.bookingNumber) { // Generate booking number: OVH + year + random 6 digits const year = new Date().getFullYear(); diff --git a/routes/blog.js b/routes/blog.js index 7653dd5..edc3e41 100644 --- a/routes/blog.js +++ b/routes/blog.js @@ -4,6 +4,23 @@ const BlogPost = require('../models/BlogPost'); const adminAuth = require('../middleware/adminAuth'); const { body, validationResult } = require('express-validator'); + +function makeSlug(input = "") { + return input + .toString() + .trim() + .toLowerCase() + .replace(/[^\p{Letter}\p{Number}]+/gu, "-") + .replace(/(^-|-$)+/g, ""); +} + +async function ensureUniqueSlug(baseSlug) { + let slug = baseSlug || `post-${Date.now()}`; + const exists = await BlogPost.findOne({ slug }).select("_id"); + if (!exists) return slug; + return `${slug}-${Date.now()}`; +} + // @route GET /api/blog // @desc Get published blog posts // @access Public @@ -158,9 +175,20 @@ router.post('/', adminAuth, [ } const postData = { - ...req.body, - author: req.admin.id - }; + ...req.body, + author: req.admin.id + }; + + if (!postData.slug || !postData.slug.trim()) { + postData.slug = makeSlug(postData.title || ""); + } + + if (!postData.slug) { + postData.slug = `post-${Date.now()}`; + } + + postData.slug = await ensureUniqueSlug(postData.slug); + const post = new BlogPost(postData); await post.save(); diff --git a/routes/bookings.js b/routes/bookings.js index b148f7e..c68b3f2 100644 --- a/routes/bookings.js +++ b/routes/bookings.js @@ -10,6 +10,114 @@ const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const sendEmail = require('../utils/sendEmail'); const logger = require('../utils/logger'); + +// @route POST /api/bookings/request +// @desc Create a booking request (no online payment) +// @access Public +router.post('/request', [ + body('guestInfo.firstName').notEmpty().withMessage('First name is required'), + body('guestInfo.lastName').notEmpty().withMessage('Last name is required'), + body('guestInfo.email').isEmail().withMessage('Valid email is required'), + body('guestInfo.phone').notEmpty().withMessage('Phone number is required'), + body('roomId').isMongoId().withMessage('Valid room ID is required'), + body('checkInDate').isISO8601().withMessage('Valid check-in date is required'), + body('checkOutDate').isISO8601().withMessage('Valid check-out date is required'), + body('numberOfGuests.adults').isInt({ min: 1 }).withMessage('At least 1 adult required'), + body('numberOfGuests.children').optional().isInt({ min: 0 }), +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Validation errors', + errors: errors.array() + }); + } + + const { guestInfo, roomId, checkInDate, checkOutDate, numberOfGuests, specialRequests } = req.body; + + const checkIn = new Date(checkInDate); + const checkOut = new Date(checkOutDate); + + if (checkIn >= checkOut) { + return res.status(400).json({ success: false, message: 'Check-out date must be after check-in date' }); + } + if (checkIn < new Date()) { + return res.status(400).json({ success: false, message: 'Check-in date cannot be in the past' }); + } + + const room = await Room.findById(roomId); + if (!room || !room.isActive) { + return res.status(404).json({ success: false, message: 'Room not found' }); + } + + const totalGuests = Number(numberOfGuests.adults) + Number(numberOfGuests.children || 0); + if (room.maxOccupancy < totalGuests) { + return res.status(400).json({ + success: false, + message: `Room can accommodate maximum ${room.maxOccupancy} guests` + }); + } + + const isAvailable = await room.isAvailable(checkIn, checkOut); + if (!isAvailable) { + return res.status(400).json({ + success: false, + message: 'Room is not available for the selected dates' + }); + } + + // Find or create guest + let guest = await Guest.findOne({ email: guestInfo.email }); + if (!guest) { + guest = new Guest({ ...guestInfo, isRegistered: false }); + await guest.save(); + } else { + Object.assign(guest, guestInfo); + await guest.save(); + } + + // Pricing + const numberOfNights = Math.ceil((checkOut - checkIn) / (1000 * 60 * 60 * 24)); + const roomRate = room.currentPrice; + const subtotal = roomRate * numberOfNights; + const taxes = subtotal * 0.12; + const totalAmount = subtotal + taxes; + + const booking = new Booking({ + guest: guest._id, + room: roomId, + checkInDate: checkIn, + checkOutDate: checkOut, + numberOfGuests, + roomRate, + numberOfNights, + subtotal, + taxes, + totalAmount, + specialRequests, + paymentStatus: 'Pending', + paymentMethod: 'Cash', + status: 'Pending', + bookingSource: 'Direct' + }); + + await booking.save(); + await booking.populate(['guest', 'room']); + + return res.status(201).json({ + success: true, + message: 'Booking request submitted successfully', + data: { booking } + }); + } catch (error) { + logger.error('Error creating booking request:', error); + return res.status(500).json({ success: false, message: 'Server error while creating booking request' }); + } +}); + + // @route POST /api/bookings // @desc Create a new booking // @access Public diff --git a/routes/roomCategories.js b/routes/roomCategories.js index e7d1ba7..c710b6e 100644 --- a/routes/roomCategories.js +++ b/routes/roomCategories.js @@ -4,105 +4,7 @@ const RoomCategory = require('../models/RoomCategory'); const Room = require('../models/Room'); const adminAuth = require('../middleware/adminAuth'); -// @route GET /api/room-categories -// @desc Get all active room categories -// @access Public -router.get('/', async (req, res) => { - try { - const categories = await RoomCategory.find({ isActive: true }) - .sort({ displayOrder: 1, name: 1 }) - .lean(); - - // Calculate room count and price range for each category - const categoriesWithStats = await Promise.all( - categories.map(async (category) => { - const rooms = await Room.find({ - category: category._id, - isActive: true - }).lean(); - - const prices = rooms.map(r => r.basePrice).filter(p => p > 0); - - return { - ...category, - roomCount: rooms.length, - priceRange: { - min: prices.length > 0 ? Math.min(...prices) : 0, - max: prices.length > 0 ? Math.max(...prices) : 0 - }, - primaryImage: category.images.find(img => img.isPrimary)?.url || - (category.images.length > 0 ? category.images[0].url : null), - imageCount: category.images.length - }; - }) - ); - - res.json({ - success: true, - data: { categories: categoriesWithStats } - }); - } catch (error) { - console.error('Error fetching room categories:', error); - res.status(500).json({ - success: false, - message: 'Server error while fetching room categories' - }); - } -}); - -// @route GET /api/room-categories/:slug -// @desc Get single room category with all images and rooms -// @access Public -router.get('/:slug', async (req, res) => { - try { - const category = await RoomCategory.findOne({ - slug: req.params.slug, - isActive: true - }); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'Room category not found' - }); - } - - // Get all rooms in this category - const rooms = await Room.find({ - category: category._id, - isActive: true - }) - .sort({ basePrice: 1 }) - .lean(); - - // Calculate price range - const prices = rooms.map(r => r.basePrice).filter(p => p > 0); - const priceRange = { - min: prices.length > 0 ? Math.min(...prices) : 0, - max: prices.length > 0 ? Math.max(...prices) : 0 - }; - - const categoryData = category.toObject(); - categoryData.rooms = rooms; - categoryData.roomCount = rooms.length; - categoryData.priceRange = priceRange; - categoryData.primaryImage = category.primaryImage; - categoryData.imageCount = category.images.length; - - res.json({ - success: true, - data: { category: categoryData } - }); - } catch (error) { - console.error('Error fetching room category:', error); - res.status(500).json({ - success: false, - message: 'Server error while fetching room category' - }); - } -}); - -// ==================== ADMIN ROUTES ==================== +// ==================== ADMIN ROUTES (put before /:slug to avoid conflicts) ==================== // @route GET /api/room-categories/admin/all // @desc Get all room categories (including inactive) - Admin only @@ -192,7 +94,7 @@ router.delete('/:id', adminAuth, async (req, res) => { try { // Check if any rooms are using this category const roomsCount = await Room.countDocuments({ category: req.params.id }); - + if (roomsCount > 0) { return res.status(400).json({ success: false, @@ -222,5 +124,135 @@ router.delete('/:id', adminAuth, async (req, res) => { } }); -module.exports = router; +// ==================== PUBLIC ROUTES ==================== +// @route GET /api/room-categories +// @desc Get all active room categories +// @access Public +router.get('/', async (req, res) => { + try { + const categories = await RoomCategory.find({ isActive: true }) + .sort({ displayOrder: 1, name: 1 }) + .lean(); + + const categoriesWithStats = await Promise.all( + categories.map(async (category) => { + const rooms = await Room.find({ + category: category._id, + isActive: true + }).lean(); + + const prices = rooms.map(r => r.basePrice).filter(p => p > 0); + + // ✅ Category primary image (if category has images) + const categoryPrimary = + (category.images || []).find(img => img.isPrimary)?.url || + ((category.images || []).length > 0 ? category.images[0].url : null); + + // ✅ Room primary image fallback (if category has no images) + const roomPrimary = + rooms + .flatMap(r => (r.images || [])) + .find(img => img?.isPrimary && img?.url)?.url || + rooms + .flatMap(r => (r.images || [])) + .find(img => img?.url)?.url || + null; + + // ✅ If category has no images, use rooms images count + const roomImagesCount = rooms.reduce((sum, r) => sum + ((r.images || []).length), 0); + + return { + ...category, + roomCount: rooms.length, + priceRange: { + min: prices.length > 0 ? Math.min(...prices) : 0, + max: prices.length > 0 ? Math.max(...prices) : 0 + }, + primaryImage: categoryPrimary || roomPrimary || null, + imageCount: (category.images || []).length > 0 ? (category.images || []).length : roomImagesCount + }; + }) + ); + + res.json({ + success: true, + data: { categories: categoriesWithStats } + }); + } catch (error) { + console.error('Error fetching room categories:', error); + res.status(500).json({ + success: false, + message: 'Server error while fetching room categories' + }); + } +}); + +// @route GET /api/room-categories/:slug +// @desc Get single room category with all images and rooms +// @access Public +router.get('/:slug', async (req, res) => { + try { + const category = await RoomCategory.findOne({ + slug: req.params.slug, + isActive: true + }); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'Room category not found' + }); + } + + const rooms = await Room.find({ + category: category._id, + isActive: true + }) + .sort({ basePrice: 1 }) + .lean(); + + const prices = rooms.map(r => r.basePrice).filter(p => p > 0); + const priceRange = { + min: prices.length > 0 ? Math.min(...prices) : 0, + max: prices.length > 0 ? Math.max(...prices) : 0 + }; + + const categoryData = category.toObject(); + categoryData.rooms = rooms; + categoryData.roomCount = rooms.length; + categoryData.priceRange = priceRange; + + // ✅ Same fallback logic for primaryImage + const categoryPrimary = + (categoryData.images || []).find(img => img.isPrimary)?.url || + ((categoryData.images || []).length > 0 ? categoryData.images[0].url : null); + + const roomPrimary = + rooms + .flatMap(r => (r.images || [])) + .find(img => img?.isPrimary && img?.url)?.url || + rooms + .flatMap(r => (r.images || [])) + .find(img => img?.url)?.url || + null; + + const roomImagesCount = rooms.reduce((sum, r) => sum + ((r.images || []).length), 0); + + categoryData.primaryImage = categoryPrimary || roomPrimary || null; + categoryData.imageCount = (categoryData.images || []).length > 0 ? (categoryData.images || []).length : roomImagesCount; + + res.json({ + success: true, + data: { category: categoryData } + }); + } catch (error) { + console.error('Error fetching room category:', error); + res.status(500).json({ + success: false, + message: 'Server error while fetching room category' + }); + } +}); + +module.exports = router;