diff --git a/backend/controllers/roomBookingController.js b/backend/controllers/roomBookingController.js new file mode 100644 index 00000000..d19c11ba --- /dev/null +++ b/backend/controllers/roomBookingController.js @@ -0,0 +1,126 @@ + +const { Room, RoomBooking, Event, User } = require('../models/schema'); + +exports.createRoom = async (req, res) => { + try { + const { name, capacity, location, amenities } = req.body; + const room = new Room({ name, capacity, location, amenities }); + await room.save(); + res.status(201).json({ message: 'Room created', room }); + } catch (err) { + if (err.code === 11000) { + return res.status(409).json({ message: 'Room name already exists' }); + } + res.status(500).json({ message: 'Error creating room', error: err.message }); + } +}; + + +exports.getAllRooms = async (_req, res) => { + try { + const rooms = await Room.find({ is_active: true }); + res.json(rooms); + } catch (err) { + res.status(500).json({ message: 'Error fetching rooms' }); + } +}; + + +exports.bookRoom = async (req, res) => { + try { + const { roomId, eventId, date, startTime, endTime, purpose } = req.body; + // Check for clash + const clash = await RoomBooking.findOne({ + room: roomId, + status: { $in: ['Pending', 'Approved'] }, + $or: [ + { startTime: { $lt: endTime }, endTime: { $gt: startTime } }, + ], + }); + if (clash) { + return res.status(409).json({ message: 'Room clash detected', conflictingBooking: clash }); + } + const booking = new RoomBooking({ + room: roomId, + event: eventId, + date, + startTime, + endTime, + purpose, + bookedBy: req.user._id, + }); + await booking.save(); + res.status(201).json({ message: 'Room booked (pending approval)', booking }); + } catch (err) { + res.status(500).json({ message: 'Error booking room', error: err.message }); + } +}; + + +exports.getAvailability = async (req, res) => { + try { + const { date, roomId } = req.query; + const query = { date: new Date(date) }; + if (roomId) query.room = roomId; + const bookings = await RoomBooking.find(query).populate('room event bookedBy'); + res.json(bookings); + } catch (err) { + res.status(500).json({ message: 'Error fetching availability' }); + } +}; + + +exports.updateBookingStatus = async (req, res) => { + try { + const { id } = req.params; + const { status } = req.body; + if (!['Approved', 'Rejected'].includes(status)) { + return res.status(400).json({ message: 'Invalid status' }); + } + const booking = await RoomBooking.findByIdAndUpdate( + id, + { status, reviewedBy: req.user._id, updated_at: new Date() }, + { new: true } + ); + if (!booking) return res.status(404).json({ message: 'Booking not found' }); + res.json({ message: 'Booking status updated', booking }); + } catch (err) { + res.status(500).json({ message: 'Error updating booking status' }); + } +}; + + +exports.cancelBooking = async (req, res) => { + try { + const { id } = req.params; + const booking = await RoomBooking.findById(id); + if (!booking) return res.status(404).json({ message: 'Booking not found' }); + + if ( + String(booking.bookedBy) !== String(req.user._id) && + !['PRESIDENT', 'GENSEC_SCITECH', 'GENSEC_ACADEMIC', 'GENSEC_CULTURAL', 'GENSEC_SPORTS', 'CLUB_COORDINATOR'].includes(req.user.role) + ) { + return res.status(403).json({ message: 'Forbidden' }); + } + booking.status = 'Cancelled'; + booking.updated_at = new Date(); + await booking.save(); + res.json({ message: 'Booking cancelled', booking }); + } catch (err) { + res.status(500).json({ message: 'Error cancelling booking' }); + } +}; + +exports.getBookings = async (req, res) => { + try { + const { roomId, date, status } = req.query; + const query = {}; + if (roomId) query.room = roomId; + if (date) query.date = new Date(date); + if (status) query.status = status; + const bookings = await RoomBooking.find(query).populate('room event bookedBy'); + res.json(bookings); + } catch (err) { + res.status(500).json({ message: 'Error fetching bookings' }); + } +}; diff --git a/backend/index.js b/backend/index.js index 4ad6d419..78e133d7 100644 --- a/backend/index.js +++ b/backend/index.js @@ -21,6 +21,7 @@ const dashboardRoutes = require("./routes/dashboard.js"); const analyticsRoutes = require("./routes/analytics.js"); const porRoutes = require("./routes/por.js"); +const roomBookingRoutes = require("./routes/roomBooking.js"); const app = express(); if (process.env.NODE_ENV === "production") { @@ -68,6 +69,7 @@ app.use("/api/announcements", announcementRoutes); app.use("/api/dashboard", dashboardRoutes); app.use("/api/announcements", announcementRoutes); app.use("/api/analytics", analyticsRoutes); +app.use("/api/rooms", roomBookingRoutes); app.use("/api/por", porRoutes); // Start the server diff --git a/backend/models/schema.js b/backend/models/schema.js index 400bc856..0b85f5d6 100644 --- a/backend/models/schema.js +++ b/backend/models/schema.js @@ -645,6 +645,94 @@ const OrganizationalUnit = mongoose.model( ); const Announcement = mongoose.model("Announcement", announcementSchema); +const roomSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + unique: true, + }, + capacity: { + type: Number, + required: true, + }, + location: { + type: String, + required: true, + }, + amenities: [ + { + type: String, + }, + ], + is_active: { + type: Boolean, + default: true, + }, + created_at: { + type: Date, + default: Date.now, + }, + updated_at: { + type: Date, + default: Date.now, + }, +}); + +const Room = mongoose.model("Room", roomSchema); + +const roomBookingSchema = new mongoose.Schema({ + room: { + type: mongoose.Schema.Types.ObjectId, + ref: "Room", + required: true, + }, + event: { + type: mongoose.Schema.Types.ObjectId, + ref: "Event", + }, + date: { + type: Date, + required: true, + }, + startTime: { + type: Date, + required: true, + }, + endTime: { + type: Date, + required: true, + }, + purpose: { + type: String, + }, + bookedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + status: { + type: String, + enum: ["Pending", "Approved", "Rejected", "Cancelled"], + default: "Pending", + }, + reviewedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + created_at: { + type: Date, + default: Date.now, + }, + updated_at: { + type: Date, + default: Date.now, + }, +}); + +roomBookingSchema.index({ room: 1, date: 1, startTime: 1, endTime: 1 }); + +const RoomBooking = mongoose.model("RoomBooking", roomBookingSchema); + module.exports = { User, Feedback, @@ -656,4 +744,6 @@ module.exports = { Position, OrganizationalUnit, Announcement, + Room, + RoomBooking, }; diff --git a/backend/routes/roomBooking.js b/backend/routes/roomBooking.js new file mode 100644 index 00000000..7ff148fc --- /dev/null +++ b/backend/routes/roomBooking.js @@ -0,0 +1,35 @@ +const express = require('express'); +const router = express.Router(); +const isAuthenticated = require('../middlewares/isAuthenticated'); +const authorizeRole = require('../middlewares/authorizeRole'); +const { ROLE_GROUPS, ROLES } = require('../utils/roles'); +const roomBookingController = require('../controllers/roomBookingController'); + +// Create a new room (admin only) +router.post('/create-room', isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), roomBookingController.createRoom); + +// Get all rooms +router.get('/rooms', isAuthenticated, roomBookingController.getAllRooms); + +// Book a room (admin only) +router.post('/book', isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), roomBookingController.bookRoom); + +// Get room availability +router.get('/availability', isAuthenticated, roomBookingController.getAvailability); + +// Get bookings (filterable) +router.get('/bookings', isAuthenticated, roomBookingController.getBookings); + +// Update booking status (approve/reject) +router.put('/bookings/:id/status', isAuthenticated, authorizeRole([ + ROLES.PRESIDENT, + ROLES.GENSEC_SCITECH, + ROLES.GENSEC_ACADEMIC, + ROLES.GENSEC_CULTURAL, + ROLES.GENSEC_SPORTS, +]), roomBookingController.updateBookingStatus); + +// Cancel a booking +router.delete('/bookings/:id', isAuthenticated, roomBookingController.cancelBooking); + +module.exports = router; diff --git a/backend/seed.js b/backend/seed.js index 61a65273..3656e6cf 100644 --- a/backend/seed.js +++ b/backend/seed.js @@ -2,17 +2,34 @@ require("dotenv").config(); const mongoose = require("mongoose"); const { - User, - Feedback, - Achievement, - UserSkill, - Skill, - Event, - PositionHolder, - Position, - OrganizationalUnit, + User, + Feedback, + Achievement, + UserSkill, + Skill, + Event, + PositionHolder, + Position, + OrganizationalUnit, + Room, } = require("./models/schema"); +// Sample Rooms for Seeding +const sampleRooms = [ + { name: "LH-101", capacity: 60, location: "Academic Block 1, Ground Floor", amenities: ["Projector", "AC", "Whiteboard"] }, + { name: "LH-102", capacity: 60, location: "Academic Block 1, Ground Floor", amenities: ["Projector", "AC"] }, + { name: "Seminar Hall", capacity: 120, location: "Admin Block, 1st Floor", amenities: ["Projector", "Sound System", "AC"] }, +]; + +// Seeds sample rooms for testing room booking features. + +const seedRooms = async () => { + console.log("Seeding sample rooms..."); + await Room.deleteMany({}); + await Room.insertMany(sampleRooms); + console.log("Sample rooms seeded!"); +}; + // --- Data for Seeding --- // Original club/committee data.