mirror of
https://github.com/Alvin-Zilverstand/Challenge_15_Magazijn_App_Maken.git
synced 2026-03-06 11:06:34 +01:00
init
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
@@ -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
BIN
create_user.js
Normal file
Binary file not shown.
3
example-env
Normal file
3
example-env
Normal 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
29
middleware/auth.js
Normal 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
33
models/Item.js
Normal 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
31
models/Reservation.js
Normal 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
34
models/User.js
Normal 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
1769
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal 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
92
public/add-item.html
Normal 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>
|
||||||
93
public/admin-reservations.html
Normal file
93
public/admin-reservations.html
Normal 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
152
public/admin.html
Normal 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
444
public/css/style.css
Normal 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
16
public/favicon.svg
Normal 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 |
BIN
public/images/default-item.png
Normal file
BIN
public/images/default-item.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
public/images/items/item-1761070418185-318281261.jpg
Normal file
BIN
public/images/items/item-1761070418185-318281261.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
42
public/index.html
Normal file
42
public/index.html
Normal 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
108
public/js/add-item.js
Normal 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);
|
||||||
0
public/js/admin-add-item.js
Normal file
0
public/js/admin-add-item.js
Normal file
188
public/js/admin-reservations.js
Normal file
188
public/js/admin-reservations.js
Normal 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
436
public/js/admin.js
Normal 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
36
public/js/auth.js
Normal 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
60
public/js/register.js
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
175
public/js/student-reservations.js
Normal file
175
public/js/student-reservations.js
Normal 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
240
public/js/student.js
Normal 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, '"')})'>
|
||||||
|
<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, '"')})">
|
||||||
|
<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
54
public/register.html
Normal 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>
|
||||||
89
public/student-reservations.html
Normal file
89
public/student-reservations.html
Normal 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
129
public/student.html
Normal 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
86
routes/auth.js
Normal 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
117
routes/items.js
Normal 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
208
routes/reservations.js
Normal 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
43
routes/upload.js
Normal 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
73
seed.js
Normal 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
86
server.js
Normal 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}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user