Add return pending status and related functionality for reservations

This commit is contained in:
Alvin
2025-10-22 14:34:43 +02:00
parent 56ebf599ea
commit 68b0f9c557
7 changed files with 34 additions and 18 deletions

View File

@@ -19,7 +19,7 @@ const reservationSchema = new mongoose.Schema({
}, },
status: { status: {
type: String, type: String,
enum: ['PENDING', 'APPROVED', 'REJECTED', 'RETURNED', 'ARCHIVED'], enum: ['PENDING', 'APPROVED', 'REJECTED', 'RETURN_PENDING', 'RETURNED', 'ARCHIVED'],
default: 'PENDING' default: 'PENDING'
}, },
reservedDate: { reservedDate: {

View File

@@ -61,6 +61,7 @@
<option value="PENDING">Pending</option> <option value="PENDING">Pending</option>
<option value="APPROVED">Approved</option> <option value="APPROVED">Approved</option>
<option value="REJECTED">Rejected</option> <option value="REJECTED">Rejected</option>
<option value="RETURN_PENDING">Return Pending</option>
<option value="RETURNED">Returned</option> <option value="RETURNED">Returned</option>
</select> </select>
</div> </div>

View File

@@ -299,6 +299,12 @@ body {
border: 1px solid var(--vista-grey); border: 1px solid var(--vista-grey);
} }
.reservation-return_pending {
background-color: #ffc107 !important;
color: var(--vista-blue) !important;
border: 1px solid var(--vista-peach);
}
.reservation-returned { .reservation-returned {
background-color: var(--vista-peach) !important; background-color: var(--vista-peach) !important;
color: var(--vista-white) !important; color: var(--vista-white) !important;

View File

@@ -75,10 +75,15 @@ function filterAndDisplayReservations() {
<button class="btn btn-warning" onclick="updateReservation('${reservation._id}', 'REJECTED')" title="Reject"> <button class="btn btn-warning" onclick="updateReservation('${reservation._id}', 'REJECTED')" title="Reject">
<i class="bi bi-x-lg"></i><span class="btn-text"> Reject</span> <i class="bi bi-x-lg"></i><span class="btn-text"> Reject</span>
</button> </button>
` : reservation.status === 'APPROVED' ? ` ` : reservation.status === 'RETURN_PENDING' ? `
<button class="btn btn-info" onclick="updateReservation('${reservation._id}', 'RETURNED')" title="Mark as Returned"> <button class="btn btn-success" onclick="updateReservation('${reservation._id}', 'RETURNED')" title="Approve Return">
<i class="bi bi-arrow-return-left"></i><span class="btn-text"> Return</span> <i class="bi bi-check-lg"></i><span class="btn-text"> Approve Return</span>
</button> </button>
<button class="btn btn-warning" onclick="updateReservation('${reservation._id}', 'APPROVED')" title="Reject Return">
<i class="bi bi-x-lg"></i><span class="btn-text"> Reject Return</span>
</button>
` : reservation.status === 'APPROVED' ? `
<span class="text-info"><i class="bi bi-check-circle"></i><span class="btn-text"> Item Loaned</span></span>
` : reservation.status === 'RETURNED' ? ` ` : reservation.status === 'RETURNED' ? `
<button class="btn btn-secondary" onclick="archiveReservation('${reservation._id}')" title="Archive Reservation"> <button class="btn btn-secondary" onclick="archiveReservation('${reservation._id}')" title="Archive Reservation">
<i class="bi bi-archive"></i><span class="btn-text"> Archive</span> <i class="bi bi-archive"></i><span class="btn-text"> Archive</span>

View File

@@ -87,11 +87,13 @@ function filterAndDisplayReservations() {
<i class="bi bi-x-circle"></i> Cancel <i class="bi bi-x-circle"></i> Cancel
</button> </button>
` : reservation.status === 'APPROVED' ? ` ` : reservation.status === 'APPROVED' ? `
<button class="btn btn-sm btn-info" onclick="returnReservation('${reservation._id}')" title="Return Item"> <button class="btn btn-sm btn-info" onclick="returnReservation('${reservation._id}')" title="Request Return">
<i class="bi bi-arrow-return-left"></i> Return <i class="bi bi-arrow-return-left"></i> Request Return
</button> </button>
` : reservation.status === 'REJECTED' ? ` ` : reservation.status === 'REJECTED' ? `
<span class="text-muted"><i class="bi bi-x-circle"></i> Rejected</span> <span class="text-muted"><i class="bi bi-x-circle"></i> Rejected</span>
` : reservation.status === 'RETURN_PENDING' ? `
<span class="text-warning"><i class="bi bi-clock"></i> Return Pending Approval</span>
` : reservation.status === 'RETURNED' ? ` ` : reservation.status === 'RETURNED' ? `
<span class="text-success"><i class="bi bi-check-circle"></i> Returned</span> <span class="text-success"><i class="bi bi-check-circle"></i> Returned</span>
` : ''} ` : ''}
@@ -126,7 +128,7 @@ async function deleteReservation(reservationId) {
// Return reservation (mark as returned) // Return reservation (mark as returned)
async function returnReservation(reservationId) { async function returnReservation(reservationId) {
if (!confirm('Are you sure you want to return this item?')) return; if (!confirm('Are you sure you want to request return for this item? An admin will need to approve the return.')) return;
try { try {
const response = await fetch(`/api/reservations/${reservationId}`, { const response = await fetch(`/api/reservations/${reservationId}`, {
@@ -135,29 +137,29 @@ async function returnReservation(reservationId) {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}` 'Authorization': `Bearer ${localStorage.getItem('token')}`
}, },
body: JSON.stringify({ status: 'RETURNED' }) body: JSON.stringify({ status: 'RETURN_PENDING' })
}); });
if (response.ok) { if (response.ok) {
// Successfully returned, reload reservations // Successfully requested return, reload reservations
await loadReservations(); await loadReservations();
// Show success message // Show success message
const alert = document.createElement('div'); const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show'; alert.className = 'alert alert-success alert-dismissible fade show';
alert.innerHTML = ` alert.innerHTML = `
Item returned successfully! Return requested successfully! An admin will review your request.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`; `;
const container = document.querySelector('.container'); const container = document.querySelector('.container');
container.prepend(alert); container.prepend(alert);
setTimeout(() => alert.remove(), 3000); setTimeout(() => alert.remove(), 5000);
} else { } else {
const error = await response.json(); const error = await response.json();
throw new Error(error.message || 'Failed to return item'); throw new Error(error.message || 'Failed to request return');
} }
} catch (error) { } catch (error) {
console.error('Error returning item:', error); console.error('Error requesting return:', error);
alert(`Failed to return item: ${error.message}`); alert(`Failed to request return: ${error.message}`);
} }
} }

View File

@@ -58,6 +58,7 @@
<option value="PENDING">Pending</option> <option value="PENDING">Pending</option>
<option value="APPROVED">Approved</option> <option value="APPROVED">Approved</option>
<option value="REJECTED">Rejected</option> <option value="REJECTED">Rejected</option>
<option value="RETURN_PENDING">Return Pending</option>
<option value="RETURNED">Returned</option> <option value="RETURNED">Returned</option>
</select> </select>
</div> </div>

View File

@@ -108,18 +108,18 @@ router.patch('/:id', auth, async (req, res) => {
// Check authorization // Check authorization
const isAdmin = req.user.role === 'admin'; const isAdmin = req.user.role === 'admin';
const isOwner = reservation.userId._id.toString() === req.user._id.toString(); const isOwner = reservation.userId._id.toString() === req.user._id.toString();
const isReturning = req.body.status === 'RETURNED'; const isReturning = req.body.status === 'RETURN_PENDING';
if (!isAdmin && (!isOwner || !isReturning)) { if (!isAdmin && (!isOwner || !isReturning)) {
return res.status(403).json({ return res.status(403).json({
message: 'Not authorized. Students can only return their own items.' message: 'Not authorized. Students can only request return of their own items.'
}); });
} }
// Additional validation for students // Additional validation for students
if (!isAdmin && isReturning && reservation.status !== 'APPROVED') { if (!isAdmin && isReturning && reservation.status !== 'APPROVED') {
return res.status(400).json({ return res.status(400).json({
message: 'Can only return approved items' message: 'Can only request return for approved items'
}); });
} }
@@ -143,7 +143,8 @@ router.patch('/:id', auth, async (req, res) => {
if (oldStatus === 'PENDING' && newStatus === 'REJECTED') { if (oldStatus === 'PENDING' && newStatus === 'REJECTED') {
item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1)); item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
await item.save(); await item.save();
} else if (oldStatus === 'APPROVED' && newStatus === 'RETURNED') { } else if (oldStatus === 'RETURN_PENDING' && newStatus === 'RETURNED') {
// Admin approved the return
item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1)); item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
await item.save(); await item.save();
} }