-
Notifications
You must be signed in to change notification settings - Fork 55
Feature/room booking frontend #235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' }); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
| }, | ||
|
Comment on lines
+697
to
+704
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify whether any temporal guard already exists in model/controller paths.
rg -nP -C3 'startTime|endTime|validate|bookRoom|getAvailability' backend/models/schema.js backend/controllers/roomBookingController.jsRepository: OpenLake/Student_Database_COSA Length of output: 3022 Add schema validation: Invalid booking windows (e.g., 💡 Proposed fix const roomBookingSchema = new mongoose.Schema({
@@
endTime: {
type: Date,
required: true,
},
@@
});
+roomBookingSchema.path("endTime").validate(function (value) {
+ return this.startTime instanceof Date && value instanceof Date && value > this.startTime;
+}, "endTime must be later than startTime");
+
roomBookingSchema.index({ room: 1, date: 1, startTime: 1, endTime: 1 });🤖 Prompt for AI Agents |
||
| 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, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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!"); | ||
| }; | ||
|
Comment on lines
+26
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 Proposed fix async function seedDB() {
try {
@@
await clearData();
await seedOrganizationalUnits();
+ await seedRooms();
await seedUsers();
await seedPositions();🤖 Prompt for AI Agents |
||
|
|
||
| // --- Data for Seeding --- | ||
|
|
||
| // Original club/committee data. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Date matching may fail due to time component differences.
The exact date match
{ date: new Date(date) }is fragile. If the stored date has a different time component (e.g., due to timezone handling), the query won't match. Consider using a date range query.🛡️ Proposed fix using date range
exports.getAvailability = async (req, res) => { try { const { date, roomId } = req.query; - const query = { date: new Date(date) }; + const queryDate = new Date(date); + const nextDate = new Date(queryDate); + nextDate.setDate(nextDate.getDate() + 1); + const query = { date: { $gte: queryDate, $lt: nextDate } }; if (roomId) query.room = roomId; const bookings = await RoomBooking.find(query).populate('room event bookedBy'); res.json(bookings);📝 Committable suggestion
🤖 Prompt for AI Agents