This commit is contained in:
Alvin
2025-10-21 20:54:03 +02:00
parent 97c80d7800
commit a5c73ad907
35 changed files with 4899 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
.env

View File

@@ -0,0 +1,9 @@
# Challenge 15: Magazijn App Maken
---
[<-- Terug naar hoofd-README](../../../../../README.md)
---
Dit is de projectmap voor Challenge 15.

BIN
create_user.js Normal file

Binary file not shown.

3
example-env Normal file
View File

@@ -0,0 +1,3 @@
PORT=your_webserver_port
MONGODB_URI=mongodb://username:password@ip-or-domain:27017/your_database
JWT_SECRET=your_jwt_secret_key

29
middleware/auth.js Normal file
View File

@@ -0,0 +1,29 @@
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ', '');
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findOne({ _id: decoded.userId });
if (!user) {
throw new Error();
}
req.token = token;
req.user = user;
next();
} catch (error) {
res.status(401).send({ error: 'Please authenticate' });
}
};
const adminOnly = async (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).send({ error: 'Access denied' });
}
next();
};
module.exports = { auth, adminOnly };

33
models/Item.js Normal file
View File

@@ -0,0 +1,33 @@
const mongoose = require('mongoose');
const itemSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
description: {
type: String,
default: ''
},
location: {
type: String,
enum: ['Heerlen', 'Maastricht', 'Sittard'],
required: true
},
quantity: {
type: Number,
required: true,
min: 0
},
imageUrl: {
type: String,
default: '/images/default-item.png'
},
reserved: {
type: Number,
default: 0,
min: 0
}
}, { timestamps: true });
module.exports = mongoose.model('Item', itemSchema);

31
models/Reservation.js Normal file
View File

@@ -0,0 +1,31 @@
const mongoose = require('mongoose');
const reservationSchema = new mongoose.Schema({
itemId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Item',
required: true
},
quantity: {
type: Number,
required: true,
min: 1,
default: 1
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
status: {
type: String,
enum: ['PENDING', 'APPROVED', 'REJECTED', 'RETURNED', 'ARCHIVED'],
default: 'PENDING'
},
reservedDate: {
type: Date,
default: Date.now
}
}, { timestamps: true });
module.exports = mongoose.model('Reservation', reservationSchema);

34
models/User.js Normal file
View File

@@ -0,0 +1,34 @@
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
email: {
type: String,
required: function() {
return this.role === 'student';
},
sparse: true,
unique: true,
validate: {
validator: function(email) {
return /^\d{6}@vistacollege\.nl$/.test(email);
},
message: 'Email must be in the format: 123456@vistacollege.nl'
}
},
role: {
type: String,
enum: ['admin', 'student'],
required: true
}
}, { timestamps: true });
module.exports = mongoose.model('User', userSchema);

1769
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "school-warehouse-system",
"version": "1.0.0",
"description": "School warehouse management system",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^7.6.3",
"multer": "^2.0.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

92
public/add-item.html Normal file
View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add New Item - Admin</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">School Warehouse - Admin</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="admin.html">Inventory</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="add-item.html">Add New Item</a>
</li>
<li class="nav-item">
<a class="nav-link" href="admin-reservations.html">Reservations</a>
</li>
</ul>
<div class="navbar-nav ms-auto">
<span class="nav-item nav-link text-light" id="userInfo"></span>
<a class="nav-link" href="#" id="logoutBtn">Logout</a>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3>Add New Item</h3>
</div>
<div class="card-body">
<form id="addItemForm">
<div class="mb-3">
<label for="itemName" class="form-label">Item Name</label>
<input type="text" class="form-control" id="itemName" required>
</div>
<div class="mb-3">
<label for="itemLocation" class="form-label">Location</label>
<select class="form-select" id="itemLocation" required>
<option value="Heerlen">Heerlen</option>
<option value="Maastricht">Maastricht</option>
<option value="Sittard">Sittard</option>
</select>
</div>
<div class="mb-3">
<label for="itemDescription" class="form-label">Description</label>
<textarea class="form-control" id="itemDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="itemQuantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="itemQuantity" min="1" required>
</div>
<div class="mb-3">
<label for="itemImage" class="form-label">Item Image</label>
<div class="input-group">
<input type="file" class="form-control" id="itemImage" accept="image/*">
<button class="btn btn-outline-danger" type="button" id="removeImage" style="display: none;">
<i class="bi bi-x-lg"></i> Remove
</button>
</div>
<div class="modal-image-container mt-2" id="imagePreview"></div>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary me-2" onclick="window.location.href='admin.html'">Cancel</button>
<button type="submit" class="btn btn-primary">Add Item</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/add-item.js"></script>
</body>
</html>

View File

@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Reservations - Admin</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">School Warehouse - Admin</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="admin.html">Inventory</a>
</li>
<li class="nav-item">
<a class="nav-link" href="add-item.html">Add New Item</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="admin-reservations.html">Reservations</a>
</li>
</ul>
<div class="navbar-nav ms-auto">
<span class="nav-item nav-link text-light" id="userInfo"></span>
<a class="nav-link" href="#" id="logoutBtn">Logout</a>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<h2 class="mb-4">Manage Reservations</h2>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filter Reservations</h5>
</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-md-6">
<label for="locationFilter" class="form-label">Location</label>
<select class="form-select" id="locationFilter">
<option value="all">All Locations</option>
<option value="Heerlen">Heerlen</option>
<option value="Maastricht">Maastricht</option>
<option value="Sittard">Sittard</option>
</select>
</div>
<div class="col-md-6">
<label for="statusFilter" class="form-label">Status</label>
<select class="form-select" id="statusFilter">
<option value="all">All Status</option>
<option value="PENDING">Pending</option>
<option value="APPROVED">Approved</option>
<option value="REJECTED">Rejected</option>
<option value="RETURNED">Returned</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped reservations-table mb-0">
<thead class="table-dark">
<tr>
<th>Student</th>
<th>Item Name</th>
<th>Quantity</th>
<th>Location</th>
<th>Reserved Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="reservationsList">
<!-- Reservations will be populated dynamically -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/admin-reservations.js"></script>
</body>
</html>

152
public/admin.html Normal file
View File

@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warehouse Dashboard - Admin</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">School Warehouse - Admin</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" href="admin.html">Inventory</a>
</li>
<li class="nav-item">
<a class="nav-link" href="add-item.html">Add New Item</a>
</li>
<li class="nav-item">
<a class="nav-link" href="admin-reservations.html">Reservations</a>
</li>
</ul>
<div class="navbar-nav ms-auto">
<span class="nav-item nav-link text-light" id="userInfo"></span>
<a class="nav-link" href="#" id="logoutBtn">Logout</a>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="row">
<div class="col">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-3">
<h2 class="mb-0">Inventory Management</h2>
<a href="add-item.html" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Add New Item
</a>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-primary view-mode-btn active" data-mode="grid">
<i class="bi bi-grid"></i> Grid
</button>
<button type="button" class="btn btn-outline-primary view-mode-btn" data-mode="list">
<i class="bi bi-list"></i> List
</button>
</div>
</div>
<div id="itemsGrid" class="row g-4">
<!-- Grid items will be populated dynamically -->
</div>
<div id="itemsList" class="d-none">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Image</th>
<th>Item Name</th>
<th>Description</th>
<th>Location</th>
<th>Quantity</th>
<th>Reserved</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="itemsListBody">
<!-- List items will be populated dynamically -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Item Modal -->
<div class="modal fade" id="editItemModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editItemForm">
<input type="hidden" id="editItemId">
<div class="mb-3">
<label for="editItemName" class="form-label">Item Name</label>
<input type="text" class="form-control" id="editItemName" required>
</div>
<div class="mb-3">
<label for="editItemLocation" class="form-label">Location</label>
<select class="form-select" id="editItemLocation" required>
<option value="Heerlen">Heerlen</option>
<option value="Maastricht">Maastricht</option>
<option value="Sittard">Sittard</option>
</select>
</div>
<div class="mb-3">
<label for="editItemDescription" class="form-label">Description</label>
<textarea class="form-control" id="editItemDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="editItemQuantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="editItemQuantity" min="1" required>
</div>
<div class="mb-3">
<label for="editItemImage" class="form-label">Item Image</label>
<div class="input-group">
<input type="file" class="form-control" id="editItemImage" accept="image/*">
<button class="btn btn-outline-danger" type="button" id="editRemoveImage" style="display: none;">
<i class="bi bi-x-lg"></i> Remove
</button>
</div>
<div class="modal-image-container mt-2" id="editImagePreview"></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitEditItem()">Save Changes</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Ensure Bootstrap is loaded before initializing the application
window.addEventListener('load', () => {
if (typeof bootstrap !== 'undefined') {
console.log('Bootstrap loaded successfully');
} else {
console.error('Bootstrap failed to load');
}
});
</script>
<script src="js/admin.js"></script>
</body>
</html>

444
public/css/style.css Normal file
View File

@@ -0,0 +1,444 @@
/* Vista College Colors */
:root {
--vista-white: #FEFEFE;
--vista-grey: #CCCECF;
--vista-coral: #D3705A;
--vista-peach: #CD977E;
--vista-blue: #1F4952;
}
/* Custom styles */
.navbar {
margin-bottom: 2rem;
background-color: var(--vista-blue) !important;
}
.navbar-dark .navbar-nav .nav-link {
color: var(--vista-white);
}
.navbar-dark .navbar-nav .nav-link:hover {
color: var(--vista-grey);
}
.card {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
height: 100%;
background-color: var(--vista-white);
border: none;
}
.table {
background-color: var(--vista-white);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* Grid view styles */
.item-card {
transition: transform 0.2s;
}
.item-card:hover {
transform: translateY(-5px);
}
.item-image {
width: 100%;
height: 200px;
object-fit: cover;
border-top-left-radius: calc(0.375rem - 1px);
border-top-right-radius: calc(0.375rem - 1px);
}
.item-card .card-body {
display: flex;
flex-direction: column;
}
.item-card .card-footer {
background-color: transparent;
border-top: none;
padding-top: 0;
}
/* Image preview */
#imagePreview img {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border-radius: 4px;
}
/* List view image thumbnail */
.item-thumbnail {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 4px;
}
.btn {
margin: 2px;
}
.btn-primary {
background-color: var(--vista-coral);
border-color: var(--vista-coral);
}
.btn-primary:hover {
background-color: var(--vista-peach);
border-color: var(--vista-peach);
}
.btn-outline-primary {
color: var(--vista-coral);
border-color: var(--vista-coral);
}
.btn-outline-primary:hover {
background-color: var(--vista-coral);
border-color: var(--vista-coral);
}
.btn-outline-primary.active {
background-color: var(--vista-coral) !important;
border-color: var(--vista-coral) !important;
}
.form-control:focus, .form-select:focus {
border-color: var(--vista-coral);
box-shadow: 0 0 0 0.25rem rgba(211, 112, 90, 0.25);
}
body {
background-color: var(--vista-grey);
}
.navbar-brand {
font-weight: bold;
color: var(--vista-white) !important;
}
.card-header {
background-color: var(--vista-blue);
border-bottom: none;
color: var(--vista-white);
}
.table-responsive {
border-radius: 0.25rem;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(204, 206, 207, 0.1);
}
.badge.bg-success {
background-color: var(--vista-coral) !important;
}
.badge.bg-secondary {
background-color: var(--vista-grey) !important;
}
/* Modal styling */
.modal-header {
background-color: var(--vista-blue);
color: var(--vista-white);
}
.modal-header .btn-close {
color: var(--vista-white);
opacity: 1;
}
/* Modal image styling */
.modal-image-container {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
margin-bottom: 1rem;
background-color: var(--vista-grey);
border-radius: 4px;
overflow: hidden;
}
.modal-item-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* Status badges */
.badge {
padding: 0.5em 1em;
}
.reservation-pending {
background-color: #ffc107;
color: #000;
}
.reservation-approved {
background-color: var(--vista-coral);
color: var(--vista-white);
}
.reservation-rejected {
background-color: var(--vista-grey);
color: var(--vista-blue);
}
/* Table styling */
.table-dark {
background-color: var(--vista-blue) !important;
color: var(--vista-white) !important;
}
.table-dark th {
border-color: rgba(255, 255, 255, 0.1);
font-weight: 600;
padding: 12px 8px;
font-size: 0.9rem;
background-color: var(--vista-blue) !important;
color: var(--vista-white) !important;
}
.table tbody tr:hover {
background-color: rgba(211, 112, 90, 0.1) !important;
}
.table td {
vertical-align: middle;
padding: 12px 8px;
font-size: 0.9rem;
border-color: var(--vista-grey) !important;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(204, 206, 207, 0.05) !important;
}
.table-striped tbody tr:nth-of-type(even) {
background-color: var(--vista-white) !important;
}
/* Specific column widths for reservations table */
.reservations-table th:nth-child(1), /* Student */
.reservations-table td:nth-child(1) {
width: 12%;
min-width: 90px;
}
.reservations-table th:nth-child(2), /* Item Name */
.reservations-table td:nth-child(2) {
width: 22%;
min-width: 130px;
}
.reservations-table th:nth-child(3), /* Quantity */
.reservations-table td:nth-child(3) {
width: 8%;
min-width: 60px;
text-align: center;
}
.reservations-table th:nth-child(4), /* Location */
.reservations-table td:nth-child(4) {
width: 12%;
min-width: 90px;
text-align: center;
}
.reservations-table th:nth-child(5), /* Reserved Date */
.reservations-table td:nth-child(5) {
width: 13%;
min-width: 100px;
text-align: center;
}
.reservations-table th:nth-child(6), /* Status */
.reservations-table td:nth-child(6) {
width: 10%;
min-width: 80px;
text-align: center;
}
.reservations-table th:nth-child(7), /* Actions */
.reservations-table td:nth-child(7) {
width: 23%;
min-width: 140px;
text-align: center;
}
/* Badge styling for better visibility */
.badge {
font-size: 0.75rem;
padding: 0.4em 0.8em;
font-weight: 600;
}
/* Vista-colored status badges */
.reservation-pending {
background-color: #ffc107 !important;
color: var(--vista-blue) !important;
border: 1px solid var(--vista-peach);
}
.reservation-approved {
background-color: var(--vista-coral) !important;
color: var(--vista-white) !important;
border: 1px solid var(--vista-coral);
}
.reservation-rejected {
background-color: var(--vista-grey) !important;
color: var(--vista-blue) !important;
border: 1px solid var(--vista-grey);
}
.reservation-returned {
background-color: var(--vista-peach) !important;
color: var(--vista-white) !important;
border: 1px solid var(--vista-peach);
}
.reservation-archived {
background-color: #6c757d !important;
color: var(--vista-white) !important;
border: 1px solid #6c757d;
}
/* Specific column widths for student reservations table */
.student-reservations-table th:nth-child(1), /* Item Name */
.student-reservations-table td:nth-child(1) {
width: 25%;
min-width: 150px;
}
.student-reservations-table th:nth-child(2), /* Quantity */
.student-reservations-table td:nth-child(2) {
width: 10%;
min-width: 70px;
text-align: center;
}
.student-reservations-table th:nth-child(3), /* Location */
.student-reservations-table td:nth-child(3) {
width: 15%;
min-width: 100px;
text-align: center;
}
.student-reservations-table th:nth-child(4), /* Reserved Date */
.student-reservations-table td:nth-child(4) {
width: 15%;
min-width: 110px;
text-align: center;
}
.student-reservations-table th:nth-child(5), /* Status */
.student-reservations-table td:nth-child(5) {
width: 15%;
min-width: 100px;
text-align: center;
}
.student-reservations-table th:nth-child(6), /* Actions */
.student-reservations-table td:nth-child(6) {
width: 20%;
min-width: 120px;
text-align: center;
}
/* Quantity badge styling */
.badge.bg-info {
background-color: var(--vista-blue) !important;
color: var(--vista-white) !important;
font-weight: 700;
font-size: 0.8rem;
}
/* Button group styling */
.btn-group-sm .btn {
font-size: 0.7rem;
padding: 0.2rem 0.4rem;
border-radius: 0.2rem;
}
.btn-group .btn {
white-space: nowrap;
margin: 0 1px;
}
.btn-group .btn i {
font-size: 0.8rem;
}
/* Responsive button text */
@media (max-width: 768px) {
.btn-group .btn {
font-size: 0.65rem;
padding: 0.15rem 0.3rem;
}
.btn-group .btn .btn-text {
display: none;
}
}
/* Action buttons with Vista colors */
.btn-success {
background-color: var(--vista-coral) !important;
border-color: var(--vista-coral) !important;
color: var(--vista-white) !important;
}
.btn-success:hover {
background-color: var(--vista-peach) !important;
border-color: var(--vista-peach) !important;
color: var(--vista-white) !important;
}
.btn-danger {
background-color: var(--vista-grey) !important;
border-color: var(--vista-grey) !important;
color: var(--vista-blue) !important;
}
.btn-danger:hover {
background-color: #999 !important;
border-color: #999 !important;
color: var(--vista-white) !important;
}
.btn-warning {
background-color: var(--vista-peach) !important;
border-color: var(--vista-peach) !important;
color: var(--vista-white) !important;
}
.btn-warning:hover {
background-color: var(--vista-coral) !important;
border-color: var(--vista-coral) !important;
color: var(--vista-white) !important;
}
.btn-info {
background-color: var(--vista-blue) !important;
border-color: var(--vista-blue) !important;
color: var(--vista-white) !important;
}
.btn-info:hover {
background-color: var(--vista-peach) !important;
border-color: var(--vista-peach) !important;
color: var(--vista-white) !important;
}
/* Small button spacing */
.btn-sm {
margin: 0 2px;
}

16
public/favicon.svg Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="32" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="vistaGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#D3705A" />
<stop offset="100%" style="stop-color:#1F4952" />
</linearGradient>
</defs>
<rect width="32" height="32" rx="6" fill="#1F4952"/>
<g transform="translate(4, 4)">
<!-- V letter -->
<path d="M2 2 L8 20 L12 20 L18 2 L14 2 L10 16 L6 2 Z" fill="#D3705A"/>
<!-- C letter -->
<path d="M24 6 C24 3.8 22.2 2 20 2 L20 2 C17.8 2 16 3.8 16 6 L16 16 C16 18.2 17.8 20 20 20 L20 20 C22.2 20 24 18.2 24 16 L24 14 L20 14 L20 16 C20 16.6 19.6 17 19 17 C18.4 17 18 16.6 18 16 L18 6 C18 5.4 18.4 5 19 5 C19.6 5 20 5.4 20 6 L20 8 L24 8 Z" fill="#FEFEFE"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

42
public/index.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>School Warehouse System</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="text-center">Login</h2>
</div>
<div class="card-body">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Login</button>
<p class="mt-3">New student? <a href="register.html">Register here</a></p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/auth.js"></script>
</body>
</html>

108
public/js/add-item.js Normal file
View File

@@ -0,0 +1,108 @@
// Add Item page functionality
// Check authentication
function checkAuth() {
const token = localStorage.getItem('token');
const role = localStorage.getItem('userRole');
if (!token || role !== 'admin') {
window.location.href = '/index.html';
}
}
// Initialize page
function initializePage() {
checkAuth();
setupEventListeners();
displayUserInfo();
setupImagePreview();
}
// Display user info
function displayUserInfo() {
const username = localStorage.getItem('username');
document.getElementById('userInfo').textContent = `Admin: ${username}`;
}
// Set up event listeners
function setupEventListeners() {
document.getElementById('addItemForm').addEventListener('submit', addItem);
document.getElementById('logoutBtn').addEventListener('click', logout);
}
// Handle logout
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('userRole');
localStorage.removeItem('username');
window.location.href = '/index.html';
}
// Set up image preview
function setupImagePreview() {
const imageInput = document.getElementById('itemImage');
const previewContainer = document.getElementById('imagePreview');
const removeButton = document.getElementById('removeImage');
imageInput.addEventListener('change', () => {
const file = imageInput.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
previewContainer.innerHTML = `<img src="${e.target.result}" class="img-fluid" alt="Preview">`;
removeButton.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
previewContainer.innerHTML = '';
removeButton.style.display = 'none';
}
});
// Handle remove button click
removeButton.addEventListener('click', () => {
imageInput.value = '';
previewContainer.innerHTML = '';
removeButton.style.display = 'none';
});
}
// Add new item
async function addItem(e) {
e.preventDefault();
const formData = new FormData();
formData.append('name', document.getElementById('itemName').value);
formData.append('location', document.getElementById('itemLocation').value);
formData.append('description', document.getElementById('itemDescription').value);
formData.append('quantity', document.getElementById('itemQuantity').value);
const imageFile = document.getElementById('itemImage').files[0];
if (imageFile) {
formData.append('image', imageFile);
}
try {
const response = await fetch('/api/items', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: formData
});
if (response.ok) {
alert('Item added successfully!');
window.location.href = 'admin.html';
} else {
const error = await response.json();
alert(error.message || 'Failed to add item');
}
} catch (error) {
console.error('Error adding item:', error);
alert('Failed to add item');
}
}
// Initialize the page when loaded
document.addEventListener('DOMContentLoaded', initializePage);

View File

View File

@@ -0,0 +1,188 @@
// Admin reservations page functionality
let reservations = [];
// Check authentication
function checkAuth() {
const token = localStorage.getItem('token');
const role = localStorage.getItem('userRole');
if (!token || role !== 'admin') {
window.location.href = '/index.html';
}
}
// Initialize page
async function initializePage() {
checkAuth();
await loadReservations();
setupEventListeners();
displayUserInfo();
}
// Display user info
function displayUserInfo() {
const username = localStorage.getItem('username');
document.getElementById('userInfo').textContent = `Admin: ${username}`;
}
// Load and filter reservations
async function loadReservations() {
try {
const response = await fetch('/api/reservations', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
reservations = await response.json();
filterAndDisplayReservations();
} catch (error) {
console.error('Error loading reservations:', error);
alert('Failed to load reservations');
}
}
// Filter and display reservations
function filterAndDisplayReservations() {
const locationFilter = document.getElementById('locationFilter').value;
const statusFilter = document.getElementById('statusFilter').value;
const filteredReservations = reservations.filter(reservation => {
const locationMatch = locationFilter === 'all' || reservation.location === locationFilter;
const statusMatch = statusFilter === 'all' || reservation.status === statusFilter;
return locationMatch && statusMatch;
});
const reservationsList = document.getElementById('reservationsList');
reservationsList.innerHTML = filteredReservations.map(reservation => `
<tr>
<td><strong>${reservation.studentName}</strong></td>
<td>${reservation.itemName}</td>
<td><span class="badge bg-info">${reservation.quantity || 1}</span></td>
<td>${reservation.location}</td>
<td>${new Date(reservation.reservedDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}</td>
<td><span class="badge reservation-${reservation.status.toLowerCase()}">${reservation.status}</span></td>
<td>
<div class="btn-group btn-group-sm" role="group">
${reservation.status === 'PENDING' ? `
<button class="btn btn-success" onclick="updateReservation('${reservation._id}', 'APPROVED')" title="Approve">
<i class="bi bi-check-lg"></i><span class="btn-text"> Approve</span>
</button>
<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>
</button>
` : reservation.status === 'APPROVED' ? `
<button class="btn btn-info" onclick="updateReservation('${reservation._id}', 'RETURNED')" title="Mark as Returned">
<i class="bi bi-arrow-return-left"></i><span class="btn-text"> Return</span>
</button>
` : reservation.status === 'RETURNED' ? `
<button class="btn btn-secondary" onclick="archiveReservation('${reservation._id}')" title="Archive Reservation">
<i class="bi bi-archive"></i><span class="btn-text"> Archive</span>
</button>
` : ''}
<button class="btn btn-danger" onclick="deleteReservation('${reservation._id}')" title="Delete">
<i class="bi bi-trash"></i><span class="btn-text"> Delete</span>
</button>
</div>
</td>
</tr>
`).join('');
}
// Update reservation status
async function updateReservation(reservationId, status) {
try {
const response = await fetch(`/api/reservations/${reservationId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ status })
});
if (response.ok) {
await loadReservations();
} else {
const error = await response.json();
alert(error.message || 'Failed to update reservation');
}
} catch (error) {
console.error('Error updating reservation:', error);
alert('Failed to update reservation');
}
}
// Delete reservation
async function deleteReservation(reservationId) {
if (!confirm('Are you sure you want to delete this reservation?')) return;
try {
const response = await fetch(`/api/reservations/${reservationId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
await loadReservations();
} else {
const error = await response.json();
alert(error.message || 'Failed to delete reservation');
}
} catch (error) {
console.error('Error deleting reservation:', error);
alert('Failed to delete reservation');
}
}
// Archive reservation
async function archiveReservation(reservationId) {
if (!confirm('Are you sure you want to archive this reservation? It will be hidden from the list but remain in the database.')) return;
try {
const response = await fetch(`/api/reservations/${reservationId}/archive`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
await loadReservations();
// Show success message
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show';
alert.innerHTML = `
Reservation archived successfully!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container');
container.prepend(alert);
setTimeout(() => alert.remove(), 3000);
} else {
const error = await response.json();
alert(error.message || 'Failed to archive reservation');
}
} catch (error) {
console.error('Error archiving reservation:', error);
alert('Failed to archive reservation');
}
}
// Set up event listeners
function setupEventListeners() {
document.getElementById('locationFilter').addEventListener('change', filterAndDisplayReservations);
document.getElementById('statusFilter').addEventListener('change', filterAndDisplayReservations);
document.getElementById('logoutBtn').addEventListener('click', () => {
localStorage.clear();
window.location.href = '/index.html';
});
}
// Initialize the page when DOM is loaded
document.addEventListener('DOMContentLoaded', initializePage);

436
public/js/admin.js Normal file
View File

@@ -0,0 +1,436 @@
// Admin dashboard functionality
let items = [];
let reservations = [];
let currentView = 'grid'; // Default view mode
// Check authentication
function checkAuth() {
const token = localStorage.getItem('token');
const role = localStorage.getItem('userRole');
if (!token || role !== 'admin') {
window.location.href = '/index.html';
}
}
// Initialize page
async function initializePage() {
try {
checkAuth();
await loadItems();
displayUserInfo();
setupEventListeners(); // Move this after displayUserInfo to ensure DOM is ready
console.log('Page initialization complete');
} catch (error) {
console.error('Error during page initialization:', error);
throw error; // Re-throw to be caught by the outer try-catch
}
}
// Display user info
function displayUserInfo() {
const username = localStorage.getItem('username');
document.getElementById('userInfo').textContent = `Admin: ${username}`;
}
// Load items from server
async function loadItems() {
try {
console.log('Loading items...');
const response = await fetch('/api/items', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
console.log('Received items:', data);
if (!response.ok) {
throw new Error(data.message || 'Failed to load items');
}
items = data;
displayItems();
} catch (error) {
console.error('Error loading items:', error);
alert('Failed to load items: ' + error.message);
}
}
// Display items based on current view mode
function displayItems() {
if (currentView === 'grid') {
displayGridView();
} else {
displayListView();
}
}
// Display items in grid view
function displayGridView() {
const itemsGrid = document.getElementById('itemsGrid');
document.getElementById('itemsList').classList.add('d-none');
itemsGrid.classList.remove('d-none');
itemsGrid.innerHTML = items.map(item => `
<div class="col-md-4 col-lg-3">
<div class="card item-card">
<img src="${item.imageUrl || '/images/default-item.png'}" class="item-image" alt="${item.name}">
<div class="card-body">
<h5 class="card-title">${item.name}</h5>
<p class="card-text text-muted">${item.description || 'No description available'}</p>
<p class="card-text">Location: ${item.location}</p>
<p class="card-text">Quantity: ${item.quantity}</p>
<p class="card-text">Reserved: ${item.reserved || 0}</p>
</div>
<div class="card-footer">
<button class="btn btn-sm btn-warning" onclick="editItem('${item._id}')">Edit</button>
<button class="btn btn-sm btn-danger" onclick="deleteItem('${item._id}')">Delete</button>
</div>
</div>
</div>
`).join('');
}
// Display items in list view
function displayListView() {
const itemsList = document.getElementById('itemsList');
const itemsGrid = document.getElementById('itemsGrid');
itemsGrid.classList.add('d-none');
itemsList.classList.remove('d-none');
const itemsListBody = document.getElementById('itemsListBody');
itemsListBody.innerHTML = items.map(item => `
<tr>
<td><img src="${item.imageUrl || '/images/default-item.png'}" class="item-thumbnail" alt="${item.name}"></td>
<td>${item.name}</td>
<td>${item.description || 'No description available'}</td>
<td>${item.location}</td>
<td>${item.quantity}</td>
<td>${item.reserved || 0}</td>
<td>
<button class="btn btn-sm btn-warning" onclick="editItem('${item._id}')">Edit</button>
<button class="btn btn-sm btn-danger" onclick="deleteItem('${item._id}')">Delete</button>
</td>
</tr>
`).join('');
}
// Load reservations
async function loadReservations() {
try {
const response = await fetch('/api/reservations', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
reservations = await response.json();
displayReservations();
} catch (error) {
console.error('Error loading reservations:', error);
alert('Failed to load reservations');
}
}
// Display reservations in table
function displayReservations() {
const reservationsList = document.getElementById('reservationsList');
reservationsList.innerHTML = reservations.map(reservation => `
<tr>
<td>${reservation.studentName}</td>
<td>${reservation.itemName}</td>
<td>${reservation.location}</td>
<td>${new Date(reservation.reservedDate).toLocaleDateString()}</td>
<td><span class="badge reservation-${reservation.status.toLowerCase()}">${reservation.status}</span></td>
<td>
${reservation.status === 'PENDING' ? `
<button class="btn btn-sm btn-success" onclick="updateReservation('${reservation._id}', 'APPROVED')">Approve</button>
<button class="btn btn-sm btn-danger" onclick="updateReservation('${reservation._id}', 'REJECTED')">Reject</button>
` : ''}
</td>
</tr>
`).join('');
}
// Delete item
async function deleteItem(itemId) {
if (!confirm('Are you sure you want to delete this item?')) return;
try {
const response = await fetch(`/api/items/${itemId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
await loadItems();
} else {
const error = await response.json();
alert(error.message || 'Failed to delete item');
}
} catch (error) {
console.error('Error deleting item:', error);
alert('Failed to delete item');
}
}
// Update reservation status
async function updateReservation(reservationId, status) {
try {
const response = await fetch(`/api/reservations/${reservationId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ status })
});
if (response.ok) {
await loadReservations();
await loadItems(); // Refresh items to update quantities
} else {
const error = await response.json();
alert(error.message || 'Failed to update reservation');
}
} catch (error) {
console.error('Error updating reservation:', error);
alert('Failed to update reservation');
}
}
// Edit item functionality
let editModal = null;
// Initialize Bootstrap modal when DOM is loaded
function initializeModal() {
const modalElement = document.getElementById('editItemModal');
if (!modalElement) {
console.error('Modal element not found in the DOM');
return;
}
try {
editModal = new bootstrap.Modal(modalElement);
console.log('Modal initialized successfully');
} catch (error) {
console.error('Error initializing modal:', error);
}
}
async function editItem(itemId) {
try {
console.log('Fetching item with ID:', itemId); // Debug log
const response = await fetch(`/api/items/${itemId}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
console.log('Response data:', data); // Debug log
if (!response.ok) {
console.error('Server error:', data);
throw new Error(data.message || 'Failed to fetch item');
}
if (!data || !data._id) {
throw new Error('Invalid item data received');
}
document.getElementById('editItemId').value = data._id;
document.getElementById('editItemName').value = data.name;
document.getElementById('editItemDescription').value = data.description || '';
document.getElementById('editItemLocation').value = data.location;
document.getElementById('editItemQuantity').value = data.quantity;
// Show current image if it exists
const imagePreview = document.getElementById('editImagePreview');
const removeButton = document.getElementById('editRemoveImage');
imagePreview.innerHTML = '';
if (data.imageUrl) {
const img = document.createElement('img');
img.src = data.imageUrl;
img.classList.add('modal-item-image');
imagePreview.appendChild(img);
removeButton.style.display = 'block';
imagePreview.dataset.currentImageUrl = data.imageUrl;
} else {
removeButton.style.display = 'none';
imagePreview.dataset.currentImageUrl = '';
}
// Show the modal
if (!editModal) {
console.log('Modal not initialized, attempting to initialize now');
initializeModal();
}
if (editModal) {
editModal.show();
} else {
throw new Error('Could not initialize modal. Please try again.');
}
} catch (error) {
console.error('Error fetching item:', error);
alert('Error loading item details: ' + error.message);
}
}
async function submitEditItem() {
const itemId = document.getElementById('editItemId').value;
const formData = new FormData();
formData.append('name', document.getElementById('editItemName').value);
formData.append('description', document.getElementById('editItemDescription').value);
formData.append('location', document.getElementById('editItemLocation').value);
formData.append('quantity', document.getElementById('editItemQuantity').value);
const imagePreview = document.getElementById('editImagePreview');
const imageFile = document.getElementById('editItemImage').files[0];
const currentImageUrl = imagePreview.dataset.currentImageUrl;
try {
// Handle image update
if (imageFile) {
// Upload new image
const imageFormData = new FormData();
imageFormData.append('image', imageFile);
const uploadResponse = await fetch('/api/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: imageFormData
});
if (!uploadResponse.ok) {
throw new Error('Failed to upload image');
}
const { imageUrl } = await uploadResponse.json();
formData.append('imageUrl', imageUrl);
} else if (!currentImageUrl) {
// If no new image and no current image, explicitly set imageUrl to null
formData.append('imageUrl', '');
}
} catch (error) {
console.error('Error uploading image:', error);
alert('Failed to upload image');
return;
}
try {
const response = await fetch(`/api/items/${itemId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(Object.fromEntries(formData))
});
if (!response.ok) {
throw new Error('Update failed');
}
await loadItems(); // Refresh the items list
bootstrap.Modal.getInstance(document.getElementById('editItemModal')).hide();
alert('Item updated successfully');
} catch (error) {
console.error('Error updating item:', error);
alert('Error updating item');
}
}
// Set up event listeners
function setupEventListeners() {
// Common elements
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
localStorage.clear();
window.location.href = '/index.html';
});
}
// View mode toggle
const viewModeBtns = document.querySelectorAll('.view-mode-btn');
if (viewModeBtns.length > 0) {
viewModeBtns.forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.view-mode-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentView = this.dataset.mode;
displayItems();
});
});
}
// Edit form elements
const editItemImage = document.getElementById('editItemImage');
if (editItemImage) {
editItemImage.addEventListener('change', function(e) {
const file = e.target.files[0];
const imagePreview = document.getElementById('editImagePreview');
const removeButton = document.getElementById('editRemoveImage');
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
imagePreview.innerHTML = '';
const img = document.createElement('img');
img.src = e.target.result;
img.classList.add('modal-item-image');
imagePreview.appendChild(img);
removeButton.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
imagePreview.innerHTML = '';
if (!imagePreview.dataset.currentImageUrl) {
removeButton.style.display = 'none';
}
}
});
}
// Remove image button
const editRemoveImage = document.getElementById('editRemoveImage');
if (editRemoveImage) {
editRemoveImage.addEventListener('click', function() {
const imagePreview = document.getElementById('editImagePreview');
const imageInput = document.getElementById('editItemImage');
const removeButton = document.getElementById('editRemoveImage');
imagePreview.innerHTML = '';
imageInput.value = '';
imagePreview.dataset.currentImageUrl = '';
removeButton.style.display = 'none';
});
}
}
// Initialize the page when DOM is loaded
document.addEventListener('DOMContentLoaded', async () => {
try {
console.log('Initializing page...');
await initializePage();
console.log('Page initialized');
// Ensure modal is initialized after Bootstrap is loaded
setTimeout(() => {
console.log('Initializing modal...');
initializeModal();
console.log('Modal initialized');
}, 100);
} catch (error) {
console.error('Error during page initialization:', error);
alert('Error initializing page: ' + error.message);
}
});

36
public/js/auth.js Normal file
View File

@@ -0,0 +1,36 @@
// Authentication related functions
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value.toLowerCase(); // Convert to lowercase
const password = document.getElementById('password').value;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token);
localStorage.setItem('userRole', data.role);
localStorage.setItem('username', data.username.toLowerCase());
// Redirect based on role
if (data.role === 'admin') {
window.location.href = '/admin.html';
} else {
window.location.href = '/student.html';
}
} else {
alert(data.message || 'Login failed');
}
} catch (error) {
console.error('Login error:', error);
alert('An error occurred during login');
}
});

60
public/js/register.js Normal file
View File

@@ -0,0 +1,60 @@
// Registration form handling
document.getElementById('registrationForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value.toLowerCase(); // Convert to lowercase
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
// Validate username (only allow alphanumeric and underscores)
const usernameRegex = /^[a-z0-9_]+$/;
if (!usernameRegex.test(username)) {
alert('Username can only contain letters, numbers, and underscores');
return;
}
// Validate password match
if (password !== confirmPassword) {
alert('Passwords do not match!');
return;
}
// Validate email format
const emailRegex = /^\d+@vistacollege\.nl$/;
if (!emailRegex.test(email)) {
alert('Email must be in the format: studentnumber@vistacollege.nl');
return;
}
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
email,
password
})
});
const data = await response.json();
if (response.ok) {
// Store the token and user info
localStorage.setItem('token', data.token);
localStorage.setItem('userRole', data.role);
localStorage.setItem('username', data.username.toLowerCase());
// Redirect to student dashboard
window.location.href = '/student.html';
} else {
alert(data.message || 'Registration failed');
}
} catch (error) {
console.error('Registration error:', error);
alert('An error occurred during registration');
}
});

View File

@@ -0,0 +1,175 @@
// Student reservations page functionality
let reservations = [];
// Check authentication
function checkAuth() {
const token = localStorage.getItem('token');
const role = localStorage.getItem('userRole');
if (!token || role !== 'student') {
window.location.href = '/index.html';
}
}
// Initialize page
async function initializePage() {
checkAuth();
await loadReservations();
setupEventListeners();
displayUserInfo();
}
// Display user info
function displayUserInfo() {
const username = localStorage.getItem('username');
document.getElementById('userInfo').textContent = `Student: ${username}`;
}
// Load and filter reservations
async function loadReservations() {
try {
const response = await fetch('/api/reservations/my', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP ${response.status}`);
}
reservations = await response.json();
filterAndDisplayReservations();
} catch (error) {
console.error('Error loading reservations:', error);
// Display a user-friendly error message
const reservationsList = document.getElementById('reservationsList');
reservationsList.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted">
<i class="bi bi-exclamation-triangle"></i>
Failed to load reservations: ${error.message}
<br><small>Please try refreshing the page</small>
</td>
</tr>
`;
}
}
// Filter and display reservations
function filterAndDisplayReservations() {
const locationFilter = document.getElementById('locationFilter').value;
const statusFilter = document.getElementById('statusFilter').value;
const filteredReservations = reservations.filter(reservation => {
const locationMatch = locationFilter === 'all' || reservation.location === locationFilter;
const statusMatch = statusFilter === 'all' || reservation.status === statusFilter;
return locationMatch && statusMatch;
});
const reservationsList = document.getElementById('reservationsList');
reservationsList.innerHTML = filteredReservations.map(reservation => `
<tr>
<td><strong>${reservation.itemName}</strong></td>
<td><span class="badge bg-info">${reservation.quantity || 1}</span></td>
<td>${reservation.location}</td>
<td>${new Date(reservation.reservedDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}</td>
<td><span class="badge reservation-${reservation.status.toLowerCase()}">${reservation.status}</span></td>
<td>
${reservation.status === 'PENDING' ? `
<button class="btn btn-sm btn-danger" onclick="deleteReservation('${reservation._id}')" title="Cancel Reservation">
<i class="bi bi-x-circle"></i> Cancel
</button>
` : reservation.status === 'APPROVED' ? `
<button class="btn btn-sm btn-info" onclick="returnReservation('${reservation._id}')" title="Return Item">
<i class="bi bi-arrow-return-left"></i> Return
</button>
` : reservation.status === 'REJECTED' ? `
<span class="text-muted"><i class="bi bi-x-circle"></i> Rejected</span>
` : reservation.status === 'RETURNED' ? `
<span class="text-success"><i class="bi bi-check-circle"></i> Returned</span>
` : ''}
</td>
</tr>
`).join('');
}
// Delete (cancel) reservation
async function deleteReservation(reservationId) {
if (!confirm('Are you sure you want to cancel this reservation?')) return;
try {
const response = await fetch(`/api/reservations/${reservationId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
await loadReservations();
} else {
const error = await response.json();
alert(error.message || 'Failed to cancel reservation');
}
} catch (error) {
console.error('Error canceling reservation:', error);
alert('Failed to cancel reservation');
}
}
// Return reservation (mark as returned)
async function returnReservation(reservationId) {
if (!confirm('Are you sure you want to return this item?')) return;
try {
const response = await fetch(`/api/reservations/${reservationId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ status: 'RETURNED' })
});
if (response.ok) {
// Successfully returned, reload reservations
await loadReservations();
// Show success message
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show mt-3';
alert.innerHTML = `
Item returned successfully!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container');
const h2Element = container.querySelector('h2');
h2Element.insertAdjacentElement('afterend', alert);
setTimeout(() => alert.remove(), 3000);
} else {
const error = await response.json();
throw new Error(error.message || 'Failed to return item');
}
} catch (error) {
console.error('Error returning item:', error);
alert(`Failed to return item: ${error.message}`);
}
}
// Set up event listeners
function setupEventListeners() {
document.getElementById('locationFilter').addEventListener('change', filterAndDisplayReservations);
document.getElementById('statusFilter').addEventListener('change', filterAndDisplayReservations);
document.getElementById('logoutBtn').addEventListener('click', () => {
localStorage.clear();
window.location.href = '/index.html';
});
}
// Initialize the page when DOM is loaded
document.addEventListener('DOMContentLoaded', initializePage);

240
public/js/student.js Normal file
View File

@@ -0,0 +1,240 @@
// Student dashboard functionality
let items = [];
// Check authentication
function checkAuth() {
const token = localStorage.getItem('token');
const role = localStorage.getItem('userRole');
if (!token || role !== 'student') {
window.location.href = '/index.html';
}
}
// Initialize page
async function initializePage() {
checkAuth();
await loadItems();
setupEventListeners();
displayUserInfo();
initializeModal();
}
// Display user info
function displayUserInfo() {
const username = localStorage.getItem('username');
document.getElementById('userInfo').textContent = `Student: ${username}`;
}
// Load items from server
async function loadItems() {
try {
const response = await fetch('/api/items', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
items = await response.json();
displayItems();
} catch (error) {
console.error('Error loading items:', error);
alert('Failed to load items');
}
}
let itemDetailsModal = null;
let currentItem = null;
// Initialize Bootstrap modal
function initializeModal() {
const modalElement = document.getElementById('itemDetailsModal');
itemDetailsModal = new bootstrap.Modal(modalElement);
// Set up modal reserve button
document.getElementById('modalReserveButton').addEventListener('click', () => {
if (currentItem) {
const quantity = parseInt(document.getElementById('reserveQuantity').value);
reserveItem(currentItem._id, quantity);
}
});
}
// Show item details in modal
function showItemDetails(item) {
currentItem = item;
const availableQuantity = item.quantity - (item.reserved || 0);
// Set modal content
document.getElementById('modalItemImage').src = item.imageUrl || '/images/default-item.png';
document.getElementById('modalItemName').textContent = item.name;
document.getElementById('modalItemDescription').textContent = item.description || 'No description available';
document.getElementById('modalItemLocation').textContent = item.location;
document.getElementById('modalItemQuantity').textContent = availableQuantity;
// Populate quantity select
const quantitySelect = document.getElementById('reserveQuantity');
quantitySelect.innerHTML = '';
for (let i = 1; i <= availableQuantity; i++) {
const option = document.createElement('option');
option.value = i;
option.textContent = i;
quantitySelect.appendChild(option);
}
// Show/hide reserve button and quantity select based on availability
const reserveButton = document.getElementById('modalReserveButton');
const quantityGroup = document.getElementById('quantitySelectGroup');
if (availableQuantity > 0) {
reserveButton.style.display = 'block';
quantityGroup.style.display = 'block';
reserveButton.disabled = false;
} else {
reserveButton.style.display = 'none';
quantityGroup.style.display = 'none';
}
itemDetailsModal.show();
}
// Track current view mode
let currentViewMode = localStorage.getItem('studentViewMode') || 'grid';
// Display items based on current view mode
function displayItems() {
const locationFilter = document.getElementById('locationFilter').value;
const filteredItems = locationFilter === 'all'
? items
: items.filter(item => item.location === locationFilter);
// Update view mode buttons active state
const viewButtons = document.querySelectorAll('.view-mode-btn');
viewButtons.forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-mode') === currentViewMode);
});
// Show/hide appropriate containers
document.getElementById('itemsGrid').classList.toggle('d-none', currentViewMode !== 'grid');
document.getElementById('itemsList').classList.toggle('d-none', currentViewMode !== 'list');
if (currentViewMode === 'grid') {
// Display items in grid view
const gridContainer = document.getElementById('itemsGrid');
gridContainer.innerHTML = filteredItems.map(item => `
<div class="col-md-4 col-lg-3 mb-4">
<div class="card h-100" style="cursor: pointer" onclick='showItemDetails(${JSON.stringify(item).replace(/"/g, '&quot;')})'>
<img src="${item.imageUrl || '/images/default-item.png'}" class="card-img-top" alt="${item.name}" style="height: 200px; object-fit: cover;">
<div class="card-body">
<h5 class="card-title">${item.name}</h5>
<p class="card-text small text-muted">${item.description || 'No description available'}</p>
<p class="card-text">
<small class="text-muted">Location: ${item.location}</small><br>
<small class="text-muted">Available: ${item.quantity - (item.reserved || 0)}</small>
</p>
${item.quantity - (item.reserved || 0) > 0 ?
'<span class="badge bg-success">Available</span>' :
'<span class="badge bg-secondary">Not Available</span>'
}
</div>
</div>
</div>
`).join('');
} else {
// Display items in list view
const itemsListBody = document.getElementById('itemsListBody');
itemsListBody.innerHTML = filteredItems.map(item => `
<tr class="item-row" style="cursor: pointer" onclick="showItemDetails(${JSON.stringify(item).replace(/"/g, '&quot;')})">
<td><img src="${item.imageUrl || '/images/default-item.png'}" class="item-thumbnail" alt="${item.name}"></td>
<td>${item.name}</td>
<td>${item.description || 'No description available'}</td>
<td>${item.location}</td>
<td>${item.quantity - (item.reserved || 0)}</td>
<td>
${item.quantity - (item.reserved || 0) > 0 ?
'<span class="badge bg-success">Available</span>' :
'<span class="badge bg-secondary">Not Available</span>'
}
</td>
</tr>
`).join('');
}
}
// Load user's reservations
async function loadMyReservations() {
try {
const response = await fetch('/api/reservations/my', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
myReservations = await response.json();
displayMyReservations();
} catch (error) {
console.error('Error loading reservations:', error);
alert('Failed to load reservations');
}
}
// Display user's reservations
function displayMyReservations() {
const reservationsList = document.getElementById('reservationsList');
reservationsList.innerHTML = myReservations.map(reservation => `
<tr>
<td>${reservation.itemName}</td>
<td>${reservation.location}</td>
<td>${new Date(reservation.reservedDate).toLocaleDateString()}</td>
<td><span class="badge reservation-${reservation.status.toLowerCase()}">${reservation.status}</span></td>
</tr>
`).join('');
}
// Reserve an item
async function reserveItem(itemId, quantity = 1) {
try {
const response = await fetch('/api/reservations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ itemId, quantity })
});
if (response.ok) {
await loadItems();
itemDetailsModal.hide();
// Redirect to reservations page after successful reservation
window.location.href = '/student-reservations.html';
} else {
const error = await response.json();
alert(error.message || 'Failed to reserve item');
}
} catch (error) {
console.error('Error reserving item:', error);
alert('Failed to reserve item');
}
}
// Switch view mode
function switchViewMode(mode) {
currentViewMode = mode;
localStorage.setItem('studentViewMode', mode);
displayItems();
}
// Set up event listeners
function setupEventListeners() {
document.getElementById('locationFilter').addEventListener('change', displayItems);
document.getElementById('logoutBtn').addEventListener('click', () => {
localStorage.clear();
window.location.href = '/index.html';
});
// View mode toggle listeners
document.querySelectorAll('.view-mode-btn').forEach(btn => {
btn.addEventListener('click', () => switchViewMode(btn.getAttribute('data-mode')));
});
}
// Initialize the page when DOM is loaded
document.addEventListener('DOMContentLoaded', initializePage);

54
public/register.html Normal file
View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Student Registration - School Warehouse System</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="text-center">Student Registration</h2>
</div>
<div class="card-body">
<form id="registrationForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Vista College Email</label>
<input type="email" class="form-control" id="email"
placeholder="123456@vistacollege.nl"
pattern="^\d+@vistacollege\.nl$"
title="Email must be in the format: studentnumber@vistacollege.nl"
required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" required>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Register</button>
<p class="mt-3">Already have an account? <a href="index.html">Login here</a></p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/register.js"></script>
</body>
</html>

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Reservations - Student</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">School Warehouse</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="student.html">Available Items</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="student-reservations.html">My Reservations</a>
</li>
</ul>
<div class="navbar-nav ms-auto">
<span class="nav-item nav-link text-light" id="userInfo"></span>
<a class="nav-link" href="#" id="logoutBtn">Logout</a>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<h2 class="mb-4">My Reservations</h2>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filter Reservations</h5>
</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-md-6">
<label for="locationFilter" class="form-label">Location</label>
<select class="form-select" id="locationFilter">
<option value="all">All Locations</option>
<option value="Heerlen">Heerlen</option>
<option value="Maastricht">Maastricht</option>
<option value="Sittard">Sittard</option>
</select>
</div>
<div class="col-md-6">
<label for="statusFilter" class="form-label">Status</label>
<select class="form-select" id="statusFilter">
<option value="all">All Status</option>
<option value="PENDING">Pending</option>
<option value="APPROVED">Approved</option>
<option value="REJECTED">Rejected</option>
<option value="RETURNED">Returned</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped student-reservations-table mb-0">
<thead class="table-dark">
<tr>
<th>Item Name</th>
<th>Quantity</th>
<th>Location</th>
<th>Reserved Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="reservationsList">
<!-- Reservations will be populated dynamically -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/student-reservations.js"></script>
</body>
</html>

129
public/student.html Normal file
View File

@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warehouse Dashboard - Student</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">School Warehouse</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" href="student.html">Available Items</a>
</li>
<li class="nav-item">
<a class="nav-link" href="student-reservations.html">My Reservations</a>
</li>
</ul>
<div class="navbar-nav ms-auto">
<span class="nav-item nav-link text-light" id="userInfo"></span>
<a class="nav-link" href="#" id="logoutBtn">Logout</a>
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="row mb-4">
<div class="col-auto">
<h2>Available Items</h2>
</div>
<div class="col-auto ms-auto">
<select class="form-select me-2" id="locationFilter">
<option value="all">All Locations</option>
<option value="Heerlen">Heerlen</option>
<option value="Maastricht">Maastricht</option>
<option value="Sittard">Sittard</option>
</select>
</div>
<div class="col-auto">
<div class="btn-group">
<button type="button" class="btn btn-outline-primary view-mode-btn active" data-mode="grid">
<i class="bi bi-grid"></i> Grid
</button>
<button type="button" class="btn btn-outline-primary view-mode-btn" data-mode="list">
<i class="bi bi-list"></i> List
</button>
</div>
</div>
</div>
<!-- Grid View -->
<div id="itemsGrid" class="row g-4">
<!-- Grid items will be populated dynamically -->
</div>
<!-- List View -->
<div id="itemsList" class="d-none">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Image</th>
<th>Item Name</th>
<th>Description</th>
<th>Location</th>
<th>Quantity Available</th>
<th>Action</th>
</tr>
</thead>
<tbody id="itemsListBody">
<!-- List items will be populated dynamically -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Item Details Modal -->
<div class="modal fade" id="itemDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Item Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="modal-image-container">
<img id="modalItemImage" src="" alt="Item Image" class="modal-item-image">
</div>
</div>
<div class="col-md-6">
<h4 id="modalItemName"></h4>
<p class="text-muted" id="modalItemDescription"></p>
<p><strong>Location:</strong> <span id="modalItemLocation"></span></p>
<p><strong>Available Quantity:</strong> <span id="modalItemQuantity"></span></p>
<div class="mb-3" id="quantitySelectGroup">
<label for="reserveQuantity" class="form-label">Quantity to Reserve:</label>
<select class="form-select" id="reserveQuantity">
<!-- Options will be populated dynamically -->
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="modalReserveButton">Reserve</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/student.js"></script>
</body>
</html>

86
routes/auth.js Normal file
View File

@@ -0,0 +1,86 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
// Register new student
router.post('/register', async (req, res) => {
try {
const { username, password, email } = req.body;
// Check if email already exists
const existingEmail = await User.findOne({ email });
if (existingEmail) {
return res.status(400).json({ message: 'Email already registered' });
}
// Check if username already exists
const existingUsername = await User.findOne({ username });
if (existingUsername) {
return res.status(400).json({ message: 'Username already taken' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new student user
const user = new User({
username,
password: hashedPassword,
email,
role: 'student' // Force role to be student
});
await user.save();
// Create and send token
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
res.status(201).json({
message: 'Student account created successfully',
token,
role: user.role,
username: user.username
});
} catch (error) {
if (error.name === 'ValidationError') {
return res.status(400).json({
message: 'Invalid email format. Email must be in the format: 123456@vistacollege.nl'
});
}
res.status(500).json({ message: 'Server error' });
}
});
// Login
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username: username.toLowerCase() });
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
res.json({
token,
role: user.role,
username: user.username
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error' });
}
});
module.exports = router;

117
routes/items.js Normal file
View File

@@ -0,0 +1,117 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const { auth, adminOnly } = require('../middleware/auth');
const Item = require('../models/Item');
const router = express.Router();
// Configure multer for image upload
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/images/items/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'item-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Not an image! Please upload an image.'), false);
}
}
});
// Get all items
router.get('/', auth, async (req, res) => {
try {
const items = await Item.find();
res.json(items);
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
// Get single item by ID
router.get('/:id', auth, async (req, res) => {
try {
const item = await Item.findById(req.params.id);
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
res.json(item);
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
// Add new item (admin only)
router.post('/', auth, adminOnly, upload.single('image'), async (req, res) => {
try {
const itemData = {
name: req.body.name,
description: req.body.description,
location: req.body.location,
quantity: parseInt(req.body.quantity)
};
// If an image was uploaded, set the imageUrl
if (req.file) {
itemData.imageUrl = `/images/items/${req.file.filename}`;
}
const item = new Item(itemData);
await item.save();
res.status(201).json(item);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// Update item (admin only)
router.put('/:id', auth, adminOnly, upload.single('image'), async (req, res) => {
try {
const updateData = {
name: req.body.name,
description: req.body.description,
location: req.body.location,
quantity: parseInt(req.body.quantity)
};
// If an image was uploaded, set the new imageUrl
if (req.file) {
updateData.imageUrl = `/images/items/${req.file.filename}`;
} else if (req.body.imageUrl !== undefined) {
// If imageUrl is explicitly provided (including empty string for removal)
updateData.imageUrl = req.body.imageUrl || '/images/default-item.png';
}
const item = await Item.findByIdAndUpdate(req.params.id, updateData, { new: true });
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
res.json(item);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// Delete item (admin only)
router.delete('/:id', auth, adminOnly, async (req, res) => {
try {
const item = await Item.findByIdAndDelete(req.params.id);
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
res.json({ message: 'Item deleted successfully' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;

208
routes/reservations.js Normal file
View File

@@ -0,0 +1,208 @@
const express = require('express');
const { auth, adminOnly } = require('../middleware/auth');
const Reservation = require('../models/Reservation');
const Item = require('../models/Item');
const router = express.Router();
// Get all reservations (admin only)
router.get('/', auth, adminOnly, async (req, res) => {
try {
const reservations = await Reservation.find({ status: { $ne: 'ARCHIVED' } })
.populate('userId', 'username')
.populate('itemId')
.exec();
if (!reservations) {
return res.json([]); // Return empty array if no reservations
}
const formattedReservations = reservations
.filter(reservation => reservation.userId && reservation.itemId) // Filter out any invalid references
.map(reservation => ({
_id: reservation._id,
studentName: reservation.userId.username,
itemName: reservation.itemId.name,
location: reservation.itemId.location,
status: reservation.status,
quantity: reservation.quantity || 1,
reservedDate: reservation.reservedDate
}));
res.json(formattedReservations);
} catch (error) {
console.error('Error fetching reservations:', error);
res.status(500).json({
message: 'Failed to load reservations',
error: error.message
});
}
});
// Get user's reservations
router.get('/my', auth, async (req, res) => {
try {
const reservations = await Reservation.find({
userId: req.user._id,
status: { $ne: 'ARCHIVED' }
}).populate('itemId');
// Filter out reservations with deleted items and format the response
const formattedReservations = reservations
.filter(reservation => reservation.itemId) // Only include reservations with valid items
.map(reservation => ({
_id: reservation._id,
itemName: reservation.itemId.name,
location: reservation.itemId.location,
status: reservation.status,
quantity: reservation.quantity || 1,
reservedDate: reservation.reservedDate
}));
res.json(formattedReservations);
} catch (error) {
console.error('Error fetching user reservations:', error);
res.status(500).json({
message: 'Failed to load reservations',
error: error.message
});
}
});
// Create reservation
router.post('/', auth, async (req, res) => {
try {
const item = await Item.findById(req.body.itemId);
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
const requestedQuantity = parseInt(req.body.quantity) || 1;
if (item.quantity < item.reserved + requestedQuantity) {
return res.status(400).json({ message: 'Requested quantity is not available' });
}
const reservation = new Reservation({
itemId: req.body.itemId,
userId: req.user._id,
quantity: requestedQuantity
});
await reservation.save();
item.reserved += requestedQuantity;
await item.save();
res.status(201).json(reservation);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// Update reservation status (admin can update any, students can only mark as returned)
router.patch('/:id', auth, async (req, res) => {
try {
const reservation = await Reservation.findById(req.params.id).populate('userId');
if (!reservation) {
return res.status(404).json({ message: 'Reservation not found' });
}
// Check authorization
const isAdmin = req.user.role === 'admin';
const isOwner = reservation.userId._id.toString() === req.user._id.toString();
const isReturning = req.body.status === 'RETURNED';
if (!isAdmin && (!isOwner || !isReturning)) {
return res.status(403).json({
message: 'Not authorized. Students can only return their own items.'
});
}
// Additional validation for students
if (!isAdmin && isReturning && reservation.status !== 'APPROVED') {
return res.status(400).json({
message: 'Can only return approved items'
});
}
const item = await Item.findById(reservation.itemId);
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
const oldStatus = reservation.status;
const newStatus = req.body.status;
reservation.status = newStatus;
// Include quantity in the update if provided
if (req.body.quantity !== undefined) {
reservation.quantity = req.body.quantity;
}
await reservation.save();
// Update item reserved count based on status change
if (oldStatus === 'PENDING' && newStatus === 'REJECTED') {
item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
await item.save();
} else if (oldStatus === 'APPROVED' && newStatus === 'RETURNED') {
item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
await item.save();
}
res.json(reservation);
} catch (error) {
console.error('Error updating reservation:', error);
res.status(400).json({ message: error.message });
}
});
// Delete reservation (admin can delete any, students can only delete their own)
router.delete('/:id', auth, async (req, res) => {
try {
const reservation = await Reservation.findById(req.params.id).populate('userId');
if (!reservation) {
return res.status(404).json({ message: 'Reservation not found' });
}
// Check if user is authorized to delete this reservation
if (req.user.role !== 'admin' && reservation.userId._id.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Not authorized to delete this reservation' });
}
// Update item's reserved count
const item = await Item.findById(reservation.itemId);
if (item) {
item.reserved = Math.max(0, item.reserved - 1); // Ensure it doesn't go below 0
await item.save();
}
await Reservation.findByIdAndDelete(req.params.id);
res.json({ message: 'Reservation deleted successfully' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Archive reservation (admin only)
router.patch('/:id/archive', auth, adminOnly, async (req, res) => {
try {
const reservation = await Reservation.findById(req.params.id);
if (!reservation) {
return res.status(404).json({ message: 'Reservation not found' });
}
// Only allow archiving of returned reservations
if (reservation.status !== 'RETURNED') {
return res.status(400).json({ message: 'Can only archive returned reservations' });
}
reservation.status = 'ARCHIVED';
await reservation.save();
res.json({ message: 'Reservation archived successfully' });
} catch (error) {
console.error('Error archiving reservation:', error);
res.status(500).json({ message: error.message });
}
});
module.exports = router;

43
routes/upload.js Normal file
View File

@@ -0,0 +1,43 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const { auth, adminOnly } = require('../middleware/auth');
const router = express.Router();
// Configure multer for image upload
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/images/items/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'item-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Not an image! Please upload an image.'), false);
}
}
});
// Upload image route
router.post('/', auth, adminOnly, upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'No file uploaded' });
}
const imageUrl = `/images/items/${req.file.filename}`;
res.json({ imageUrl });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;

73
seed.js Normal file
View File

@@ -0,0 +1,73 @@
const bcrypt = require('bcryptjs');
const mongoose = require('mongoose');
require('dotenv').config();
const User = require('./models/User');
const Item = require('./models/Item');
async function seedDatabase() {
try {
// Connect to MongoDB
await mongoose.connect(process.env.MONGODB_URI);
console.log('Connected to MongoDB');
// Clear existing data
await User.deleteMany({});
await Item.deleteMany({});
// Create admin user
const adminPassword = await bcrypt.hash('admin123', 10);
await User.create({
username: 'admin',
password: adminPassword,
role: 'admin'
});
// Create test student
const studentPassword = await bcrypt.hash('student123', 10);
await User.create({
username: 'student',
password: studentPassword,
role: 'student'
});
// Create some test items
const items = [
{
name: 'Laptop',
location: 'Heerlen',
quantity: 5
},
{
name: 'Projector',
location: 'Maastricht',
quantity: 3
},
{
name: 'Microscope',
location: 'Sittard',
quantity: 4
},
{
name: 'Tablet',
location: 'Heerlen',
quantity: 10
},
{
name: 'Camera',
location: 'Maastricht',
quantity: 2
}
];
await Item.insertMany(items);
console.log('Database seeded successfully');
process.exit(0);
} catch (error) {
console.error('Error seeding database:', error);
process.exit(1);
}
}
seedDatabase();

86
server.js Normal file
View File

@@ -0,0 +1,86 @@
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const path = require('path');
const os = require('os');
require('dotenv').config();
const app = express();
// Load models first
const User = require('./models/User');
const Item = require('./models/Item');
const Reservation = require('./models/Reservation');
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
.then(async () => {
console.log('Database connected and models initialized successfully');
})
.catch(err => {
console.error('MongoDB connection error:', err);
// Don't exit, just log the error
});
// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/items', require('./routes/items'));
app.use('/api/reservations', require('./routes/reservations'));
app.use('/api/upload', require('./routes/upload'));
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
// Serve static files
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
const PORT = process.env.PORT || 3000;
// Get local IP addresses
function getLocalIPs() {
const interfaces = os.networkInterfaces();
const addresses = [];
for (const interfaceName in interfaces) {
const interface = interfaces[interfaceName];
for (const address of interface) {
// Skip internal and non-IPv4 addresses
if (address.family === 'IPv4' && !address.internal) {
addresses.push(address.address);
}
}
}
return addresses;
}
// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
app.listen(PORT, () => {
const localIPs = getLocalIPs();
console.log(`Server is running on:`);
console.log(`- Local: http://localhost:${PORT}`);
localIPs.forEach(ip => {
console.log(`- Network: http://${ip}:${PORT}`);
});
});