Spaces:
No application file
No application file
Faizur Rahman Zunayed
commited on
Commit
Β·
58668c3
0
Parent(s):
Initial commit: Library Management System
Browse files- .gitignore +14 -0
- Dockerfile +26 -0
- README.md +26 -0
- Start Library System.command +3 -0
- app.py +420 -0
- data/books.json +44 -0
- data/fines.json +11 -0
- data/issues.json +38 -0
- data/users.json +42 -0
- database.py +273 -0
- models.py +129 -0
- requirements.txt +4 -0
- run.sh +32 -0
- templates/add_book.html +20 -0
- templates/add_student.html +22 -0
- templates/base.html +70 -0
- templates/books.html +72 -0
- templates/dashboard.html +63 -0
- templates/edit_book.html +18 -0
- templates/edit_student.html +20 -0
- templates/issue_book.html +18 -0
- templates/login.html +21 -0
- templates/reports.html +85 -0
- templates/return_book.html +15 -0
- templates/student_dashboard.html +60 -0
- templates/students.html +78 -0
- templates/view_student.html +205 -0
.gitignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
*.pyd
|
| 5 |
+
.Python
|
| 6 |
+
*.so
|
| 7 |
+
*.egg
|
| 8 |
+
*.egg-info/
|
| 9 |
+
dist/
|
| 10 |
+
build/
|
| 11 |
+
venv/
|
| 12 |
+
.venv/
|
| 13 |
+
*.log
|
| 14 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
# Copy requirements and install Python dependencies
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Copy application files
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Create data directory
|
| 17 |
+
RUN mkdir -p data
|
| 18 |
+
|
| 19 |
+
# Initialize database
|
| 20 |
+
RUN python3 -c "import database as db; db.init_db()"
|
| 21 |
+
|
| 22 |
+
# Expose port
|
| 23 |
+
EXPOSE 7860
|
| 24 |
+
|
| 25 |
+
# Run the application
|
| 26 |
+
CMD ["python3", "app.py"]
|
README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Library Management System
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Library Management System
|
| 11 |
+
|
| 12 |
+
A comprehensive Flask-based Library Management System with role-based access control.
|
| 13 |
+
|
| 14 |
+
## Features
|
| 15 |
+
|
| 16 |
+
- **User Roles**: Admin, Librarian, Student
|
| 17 |
+
- **Book Management**: Add, edit, delete, search books
|
| 18 |
+
- **Student Management**: Manage student records
|
| 19 |
+
- **Issue/Return System**: Track book borrowing and returns
|
| 20 |
+
- **Overdue Tracking**: Monitor overdue books
|
| 21 |
+
- **Reports**: System statistics and analytics
|
| 22 |
+
|
| 23 |
+
## Login Credentials
|
| 24 |
+
|
| 25 |
+
- **Admin**: admin@lms.com / admin123
|
| 26 |
+
- **Student**: student@test.com / student123
|
Start Library System.command
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
cd "$(dirname "$0")"
|
| 3 |
+
./run.sh
|
app.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Library Management System - Main Application
|
| 3 |
+
"""
|
| 4 |
+
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
| 5 |
+
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
| 6 |
+
from functools import wraps
|
| 7 |
+
import bcrypt
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
from models import User, Book, Issue
|
| 12 |
+
import database as db
|
| 13 |
+
|
| 14 |
+
app = Flask(__name__)
|
| 15 |
+
app.secret_key = 'your-secret-key-here-change-in-production'
|
| 16 |
+
|
| 17 |
+
# Initialize Flask-Login
|
| 18 |
+
login_manager = LoginManager()
|
| 19 |
+
login_manager.init_app(app)
|
| 20 |
+
login_manager.login_view = 'login'
|
| 21 |
+
|
| 22 |
+
# Initialize database
|
| 23 |
+
db.init_db()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@login_manager.user_loader
|
| 27 |
+
def load_user(user_id):
|
| 28 |
+
return db.get_user(user_id)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Role-based access decorators
|
| 32 |
+
def admin_required(f):
|
| 33 |
+
@wraps(f)
|
| 34 |
+
@login_required
|
| 35 |
+
def decorated_function(*args, **kwargs):
|
| 36 |
+
if not current_user.is_admin():
|
| 37 |
+
flash('Access denied. Admin privileges required.', 'danger')
|
| 38 |
+
return redirect(url_for('dashboard'))
|
| 39 |
+
return f(*args, **kwargs)
|
| 40 |
+
return decorated_function
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def librarian_required(f):
|
| 44 |
+
@wraps(f)
|
| 45 |
+
@login_required
|
| 46 |
+
def decorated_function(*args, **kwargs):
|
| 47 |
+
if not current_user.can_manage_books():
|
| 48 |
+
flash('Access denied. Librarian or Admin privileges required.', 'danger')
|
| 49 |
+
return redirect(url_for('dashboard'))
|
| 50 |
+
return f(*args, **kwargs)
|
| 51 |
+
return decorated_function
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# Authentication routes
|
| 55 |
+
@app.route('/login', methods=['GET', 'POST'])
|
| 56 |
+
def login():
|
| 57 |
+
if current_user.is_authenticated:
|
| 58 |
+
return redirect(url_for('dashboard'))
|
| 59 |
+
|
| 60 |
+
if request.method == 'POST':
|
| 61 |
+
email = request.form.get('email')
|
| 62 |
+
password = request.form.get('password')
|
| 63 |
+
|
| 64 |
+
user = db.get_user_by_email(email)
|
| 65 |
+
if user and bcrypt.checkpw(password.encode(), user.password.encode()):
|
| 66 |
+
login_user(user)
|
| 67 |
+
flash(f'Welcome back, {user.name}!', 'success')
|
| 68 |
+
return redirect(url_for('dashboard'))
|
| 69 |
+
|
| 70 |
+
flash('Invalid email or password', 'danger')
|
| 71 |
+
|
| 72 |
+
return render_template('login.html')
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@app.route('/logout')
|
| 76 |
+
@login_required
|
| 77 |
+
def logout():
|
| 78 |
+
logout_user()
|
| 79 |
+
flash('You have been logged out successfully', 'success')
|
| 80 |
+
return redirect(url_for('login'))
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# Dashboard
|
| 84 |
+
@app.route('/')
|
| 85 |
+
@app.route('/dashboard')
|
| 86 |
+
@login_required
|
| 87 |
+
def dashboard():
|
| 88 |
+
stats = db.get_stats()
|
| 89 |
+
|
| 90 |
+
if current_user.is_student():
|
| 91 |
+
# Student dashboard
|
| 92 |
+
books = db.get_all_books()
|
| 93 |
+
my_issues = db.get_user_issues(current_user.id)
|
| 94 |
+
return render_template('student_dashboard.html',
|
| 95 |
+
stats=stats, books=books,
|
| 96 |
+
my_issues=my_issues)
|
| 97 |
+
else:
|
| 98 |
+
# Admin/Librarian dashboard
|
| 99 |
+
recent_issues = db.get_active_issues()[:10]
|
| 100 |
+
overdue = db.get_overdue_issues()
|
| 101 |
+
return render_template('dashboard.html',
|
| 102 |
+
stats=stats, recent_issues=recent_issues,
|
| 103 |
+
overdue=overdue)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# Student Management
|
| 107 |
+
@app.route('/students')
|
| 108 |
+
@librarian_required
|
| 109 |
+
def students():
|
| 110 |
+
# Get search query
|
| 111 |
+
search_query = request.args.get('q', '').strip().lower()
|
| 112 |
+
|
| 113 |
+
# Get all students
|
| 114 |
+
all_students = db.get_students()
|
| 115 |
+
|
| 116 |
+
# Filter by search query if provided
|
| 117 |
+
if search_query:
|
| 118 |
+
students = [s for s in all_students if
|
| 119 |
+
search_query in s.id.lower() or
|
| 120 |
+
search_query in s.name.lower()]
|
| 121 |
+
else:
|
| 122 |
+
students = all_students
|
| 123 |
+
|
| 124 |
+
# Get ONLY ACTIVE issued books for each student
|
| 125 |
+
student_active_issues = {}
|
| 126 |
+
for student in students:
|
| 127 |
+
all_issues = db.get_user_issues(student.id)
|
| 128 |
+
# Filter only active (not returned)
|
| 129 |
+
active_issues = [issue for issue in all_issues if issue.status == 'issued']
|
| 130 |
+
student_active_issues[student.id] = active_issues
|
| 131 |
+
|
| 132 |
+
return render_template('students.html', students=students,
|
| 133 |
+
student_issues=student_active_issues, search_query=search_query)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@app.route('/students/add', methods=['GET', 'POST'])
|
| 137 |
+
@librarian_required
|
| 138 |
+
def add_student():
|
| 139 |
+
if request.method == 'POST':
|
| 140 |
+
student_id = request.form.get('student_id')
|
| 141 |
+
name = request.form.get('name')
|
| 142 |
+
email = request.form.get('email')
|
| 143 |
+
role = request.form.get('role', 'student') # Get role from form
|
| 144 |
+
department = request.form.get('department')
|
| 145 |
+
contact = request.form.get('contact')
|
| 146 |
+
password = request.form.get('password', 'student123')
|
| 147 |
+
|
| 148 |
+
# Check if student ID already exists
|
| 149 |
+
if db.get_user(student_id):
|
| 150 |
+
flash('User ID already exists', 'danger')
|
| 151 |
+
return redirect(url_for('add_student'))
|
| 152 |
+
|
| 153 |
+
# Hash password
|
| 154 |
+
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
|
| 155 |
+
|
| 156 |
+
student = User(
|
| 157 |
+
id=student_id,
|
| 158 |
+
name=name,
|
| 159 |
+
email=email,
|
| 160 |
+
password=hashed_password.decode(),
|
| 161 |
+
role=role, # Use role from form
|
| 162 |
+
department=department,
|
| 163 |
+
contact=contact
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
db.add_user(student)
|
| 167 |
+
flash(f'{role.capitalize()} {name} added successfully', 'success')
|
| 168 |
+
return redirect(url_for('students'))
|
| 169 |
+
|
| 170 |
+
return render_template('add_student.html')
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
@app.route('/students/edit/<student_id>', methods=['GET', 'POST'])
|
| 174 |
+
@librarian_required
|
| 175 |
+
def edit_student(student_id):
|
| 176 |
+
student = db.get_user(student_id)
|
| 177 |
+
if not student:
|
| 178 |
+
flash('User not found', 'danger')
|
| 179 |
+
return redirect(url_for('students'))
|
| 180 |
+
|
| 181 |
+
if request.method == 'POST':
|
| 182 |
+
student.name = request.form.get('name')
|
| 183 |
+
student.email = request.form.get('email')
|
| 184 |
+
student.role = request.form.get('role', 'student') # Get role from form
|
| 185 |
+
student.department = request.form.get('department')
|
| 186 |
+
student.contact = request.form.get('contact')
|
| 187 |
+
|
| 188 |
+
db.update_user(student)
|
| 189 |
+
flash(f'{student.role.capitalize()} {student.name} updated successfully', 'success')
|
| 190 |
+
return redirect(url_for('students'))
|
| 191 |
+
|
| 192 |
+
return render_template('edit_student.html', student=student)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@app.route('/students/delete/<student_id>', methods=['POST'])
|
| 196 |
+
@librarian_required
|
| 197 |
+
def delete_student(student_id):
|
| 198 |
+
db.delete_user(student_id)
|
| 199 |
+
flash('Student deleted successfully', 'success')
|
| 200 |
+
return redirect(url_for('students'))
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@app.route('/students/view/<student_id>')
|
| 204 |
+
@librarian_required
|
| 205 |
+
def view_student(student_id):
|
| 206 |
+
student = db.get_user(student_id)
|
| 207 |
+
if not student:
|
| 208 |
+
flash('Student not found', 'danger')
|
| 209 |
+
return redirect(url_for('students'))
|
| 210 |
+
|
| 211 |
+
# Get all issues for this student
|
| 212 |
+
all_issues = db.get_user_issues(student_id)
|
| 213 |
+
|
| 214 |
+
# Categorize issues
|
| 215 |
+
active_issues = [i for i in all_issues if i.status == 'issued']
|
| 216 |
+
returned_issues = [i for i in all_issues if i.status == 'returned']
|
| 217 |
+
current_overdue = len([i for i in active_issues if i.is_overdue()])
|
| 218 |
+
|
| 219 |
+
# Calculate total times student has been overdue (including past returned books)
|
| 220 |
+
total_overdue_times = len([i for i in all_issues if i.is_overdue() or (i.status == 'returned' and i.days_overdue() > 0)])
|
| 221 |
+
|
| 222 |
+
# Get books info for issues
|
| 223 |
+
issues_with_books = []
|
| 224 |
+
for issue in all_issues:
|
| 225 |
+
book = db.get_book(issue.book_id)
|
| 226 |
+
issues_with_books.append({
|
| 227 |
+
'issue': issue,
|
| 228 |
+
'book': book
|
| 229 |
+
})
|
| 230 |
+
|
| 231 |
+
return render_template('view_student.html',
|
| 232 |
+
student=student,
|
| 233 |
+
issues_with_books=issues_with_books,
|
| 234 |
+
active_count=len(active_issues),
|
| 235 |
+
returned_count=len(returned_issues),
|
| 236 |
+
overdue_count=current_overdue,
|
| 237 |
+
total_overdue_times=total_overdue_times)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
# Book Management
|
| 241 |
+
@app.route('/books')
|
| 242 |
+
@login_required
|
| 243 |
+
def books():
|
| 244 |
+
query = request.args.get('q', '')
|
| 245 |
+
category = request.args.get('category', '')
|
| 246 |
+
available_only = request.args.get('available') == 'true'
|
| 247 |
+
|
| 248 |
+
books = db.search_books(query, category, available_only)
|
| 249 |
+
categories = list(set(b.category for b in db.get_all_books()))
|
| 250 |
+
|
| 251 |
+
return render_template('books.html', books=books, categories=categories,
|
| 252 |
+
query=query, selected_category=category,
|
| 253 |
+
available_only=available_only)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
@app.route('/books/add', methods=['GET', 'POST'])
|
| 257 |
+
@librarian_required
|
| 258 |
+
def add_book():
|
| 259 |
+
if request.method == 'POST':
|
| 260 |
+
book = Book(
|
| 261 |
+
book_id=request.form.get('book_id'),
|
| 262 |
+
title=request.form.get('title'),
|
| 263 |
+
author=request.form.get('author'),
|
| 264 |
+
isbn=request.form.get('isbn'),
|
| 265 |
+
category=request.form.get('category'),
|
| 266 |
+
publisher=request.form.get('publisher'),
|
| 267 |
+
year=int(request.form.get('year')),
|
| 268 |
+
total_copies=int(request.form.get('total_copies')),
|
| 269 |
+
shelf_no=request.form.get('shelf_no'),
|
| 270 |
+
cover_image=request.form.get('cover_image')
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
db.add_book(book)
|
| 274 |
+
flash(f'Book "{book.title}" added successfully', 'success')
|
| 275 |
+
return redirect(url_for('books'))
|
| 276 |
+
|
| 277 |
+
return render_template('add_book.html')
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
@app.route('/books/edit/<book_id>', methods=['GET', 'POST'])
|
| 281 |
+
@librarian_required
|
| 282 |
+
def edit_book(book_id):
|
| 283 |
+
book = db.get_book(book_id)
|
| 284 |
+
if not book:
|
| 285 |
+
flash('Book not found', 'danger')
|
| 286 |
+
return redirect(url_for('books'))
|
| 287 |
+
|
| 288 |
+
if request.method == 'POST':
|
| 289 |
+
book.title = request.form.get('title')
|
| 290 |
+
book.author = request.form.get('author')
|
| 291 |
+
book.isbn = request.form.get('isbn')
|
| 292 |
+
book.category = request.form.get('category')
|
| 293 |
+
book.publisher = request.form.get('publisher')
|
| 294 |
+
book.year = int(request.form.get('year'))
|
| 295 |
+
book.total_copies = int(request.form.get('total_copies'))
|
| 296 |
+
book.shelf_no = request.form.get('shelf_no')
|
| 297 |
+
book.cover_image = request.form.get('cover_image')
|
| 298 |
+
|
| 299 |
+
db.update_book(book)
|
| 300 |
+
flash(f'Book "{book.title}" updated successfully', 'success')
|
| 301 |
+
return redirect(url_for('books'))
|
| 302 |
+
|
| 303 |
+
return render_template('edit_book.html', book=book)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
@app.route('/books/delete/<book_id>', methods=['POST'])
|
| 307 |
+
@librarian_required
|
| 308 |
+
def delete_book(book_id):
|
| 309 |
+
book = db.get_book(book_id)
|
| 310 |
+
if book:
|
| 311 |
+
db.delete_book(book_id)
|
| 312 |
+
flash(f'Book "{book.title}" deleted successfully', 'success')
|
| 313 |
+
return redirect(url_for('books'))
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
# Issue/Borrow System
|
| 317 |
+
@app.route('/issue', methods=['GET', 'POST'])
|
| 318 |
+
@librarian_required
|
| 319 |
+
def issue_book():
|
| 320 |
+
if request.method == 'POST':
|
| 321 |
+
student_id = request.form.get('student_id')
|
| 322 |
+
book_id = request.form.get('book_id')
|
| 323 |
+
|
| 324 |
+
student = db.get_user(student_id)
|
| 325 |
+
book = db.get_book(book_id)
|
| 326 |
+
|
| 327 |
+
if not student:
|
| 328 |
+
flash('Student not found', 'danger')
|
| 329 |
+
return redirect(url_for('issue_book'))
|
| 330 |
+
|
| 331 |
+
if not book:
|
| 332 |
+
flash('Book not found', 'danger')
|
| 333 |
+
return redirect(url_for('issue_book'))
|
| 334 |
+
|
| 335 |
+
if not book.is_available():
|
| 336 |
+
flash('No copies available for this book', 'danger')
|
| 337 |
+
return redirect(url_for('issue_book'))
|
| 338 |
+
|
| 339 |
+
# Create issue record
|
| 340 |
+
issue = Issue(
|
| 341 |
+
issue_id=str(uuid.uuid4())[:8],
|
| 342 |
+
student_id=student_id,
|
| 343 |
+
book_id=book_id
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
db.add_issue(issue)
|
| 347 |
+
|
| 348 |
+
# Update book availability
|
| 349 |
+
db.update_book_copies(book_id, book.available_copies - 1)
|
| 350 |
+
|
| 351 |
+
flash(f'Book "{book.title}" issued to {student.name}', 'success')
|
| 352 |
+
return redirect(url_for('dashboard'))
|
| 353 |
+
|
| 354 |
+
students = db.get_students()
|
| 355 |
+
books = db.search_books(available_only=True)
|
| 356 |
+
return render_template('issue_book.html', students=students, books=books)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
# Return System
|
| 360 |
+
@app.route('/return', methods=['GET', 'POST'])
|
| 361 |
+
@librarian_required
|
| 362 |
+
def return_book():
|
| 363 |
+
if request.method == 'POST':
|
| 364 |
+
student_id = request.form.get('student_id')
|
| 365 |
+
book_id = request.form.get('book_id')
|
| 366 |
+
|
| 367 |
+
# Find the active issue
|
| 368 |
+
issues = [i for i in db.get_user_issues(student_id)
|
| 369 |
+
if i.book_id == book_id and i.status == 'issued']
|
| 370 |
+
|
| 371 |
+
if not issues:
|
| 372 |
+
flash('No active issue found for this book and student', 'danger')
|
| 373 |
+
return redirect(url_for('return_book'))
|
| 374 |
+
|
| 375 |
+
issue = issues[0]
|
| 376 |
+
issue.return_date = datetime.now()
|
| 377 |
+
issue.status = 'returned'
|
| 378 |
+
|
| 379 |
+
# Calculate fine if overdue
|
| 380 |
+
if issue.is_overdue():
|
| 381 |
+
fine_amount = issue.calculate_fine()
|
| 382 |
+
fine = Fine(
|
| 383 |
+
fine_id=str(uuid.uuid4())[:8],
|
| 384 |
+
issue_id=issue.issue_id,
|
| 385 |
+
student_id=student_id,
|
| 386 |
+
amount=fine_amount
|
| 387 |
+
)
|
| 388 |
+
db.add_fine(fine)
|
| 389 |
+
flash(f'Book returned. Fine: {fine_amount} BDT for {issue.days_overdue()} days overdue', 'warning')
|
| 390 |
+
else:
|
| 391 |
+
flash('Book returned successfully', 'success')
|
| 392 |
+
|
| 393 |
+
db.update_issue(issue)
|
| 394 |
+
|
| 395 |
+
# Update book availability
|
| 396 |
+
book = db.get_book(book_id)
|
| 397 |
+
db.update_book_copies(book_id, book.available_copies + 1)
|
| 398 |
+
|
| 399 |
+
return redirect(url_for('dashboard'))
|
| 400 |
+
|
| 401 |
+
active_issues = db.get_active_issues()
|
| 402 |
+
return render_template('return_book.html', issues=active_issues)
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
# Reports
|
| 406 |
+
@app.route('/reports')
|
| 407 |
+
@librarian_required
|
| 408 |
+
def reports():
|
| 409 |
+
stats = db.get_stats()
|
| 410 |
+
all_issues = db.get_all_issues()
|
| 411 |
+
overdue = db.get_overdue_issues()
|
| 412 |
+
|
| 413 |
+
return render_template('reports.html', stats=stats, issues=all_issues,
|
| 414 |
+
overdue=overdue)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
if __name__ == '__main__':
|
| 418 |
+
import os
|
| 419 |
+
port = int(os.environ.get('PORT', 7860))
|
| 420 |
+
app.run(debug=False, host='0.0.0.0', port=port)
|
data/books.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"3332": {
|
| 3 |
+
"book_id": "3332",
|
| 4 |
+
"title": "dds",
|
| 5 |
+
"author": "sdf",
|
| 6 |
+
"isbn": "333w",
|
| 7 |
+
"category": "wrrw",
|
| 8 |
+
"publisher": "wrrw",
|
| 9 |
+
"year": 2324,
|
| 10 |
+
"total_copies": 24,
|
| 11 |
+
"available_copies": 23,
|
| 12 |
+
"shelf_no": "24",
|
| 13 |
+
"cover_image": "",
|
| 14 |
+
"created_at": "2025-11-13T19:02:12.227703"
|
| 15 |
+
},
|
| 16 |
+
"BK001": {
|
| 17 |
+
"book_id": "BK001",
|
| 18 |
+
"title": "Python Programming",
|
| 19 |
+
"author": "John Doe",
|
| 20 |
+
"isbn": "978-123",
|
| 21 |
+
"category": "Programming",
|
| 22 |
+
"publisher": "Tech Books",
|
| 23 |
+
"year": 2023,
|
| 24 |
+
"total_copies": 5,
|
| 25 |
+
"available_copies": 4,
|
| 26 |
+
"shelf_no": "A1",
|
| 27 |
+
"cover_image": null,
|
| 28 |
+
"created_at": "2025-11-13T18:50:02.162456"
|
| 29 |
+
},
|
| 30 |
+
"250001": {
|
| 31 |
+
"book_id": "250001",
|
| 32 |
+
"title": "T/O",
|
| 33 |
+
"author": "Osmani",
|
| 34 |
+
"isbn": "876543",
|
| 35 |
+
"category": "Tafsir",
|
| 36 |
+
"publisher": "Thanvi Publ.",
|
| 37 |
+
"year": 2017,
|
| 38 |
+
"total_copies": 5,
|
| 39 |
+
"available_copies": 5,
|
| 40 |
+
"shelf_no": "1B",
|
| 41 |
+
"cover_image": "",
|
| 42 |
+
"created_at": "2025-11-15T20:47:34.767088"
|
| 43 |
+
}
|
| 44 |
+
}
|
data/fines.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"45bcfbaf": {
|
| 3 |
+
"fine_id": "45bcfbaf",
|
| 4 |
+
"issue_id": "974e6573",
|
| 5 |
+
"student_id": "STU001",
|
| 6 |
+
"amount": 50,
|
| 7 |
+
"paid": false,
|
| 8 |
+
"paid_date": null,
|
| 9 |
+
"created_at": "2025-11-13T18:41:36.870469"
|
| 10 |
+
}
|
| 11 |
+
}
|
data/issues.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"974e6573": {
|
| 3 |
+
"issue_id": "974e6573",
|
| 4 |
+
"student_id": "STU001",
|
| 5 |
+
"book_id": "BK001",
|
| 6 |
+
"issue_date": "2025-11-13T18:41:36.869792",
|
| 7 |
+
"due_date": "2025-11-08T18:41:36.870443",
|
| 8 |
+
"return_date": "2025-11-13T18:41:36.870753",
|
| 9 |
+
"status": "returned"
|
| 10 |
+
},
|
| 11 |
+
"69e000b1": {
|
| 12 |
+
"issue_id": "69e000b1",
|
| 13 |
+
"student_id": "123",
|
| 14 |
+
"book_id": "3332",
|
| 15 |
+
"issue_date": "2025-11-13T19:02:12.226945",
|
| 16 |
+
"due_date": "2025-11-27T19:02:12.226947",
|
| 17 |
+
"return_date": null,
|
| 18 |
+
"status": "issued"
|
| 19 |
+
},
|
| 20 |
+
"fa74d48a": {
|
| 21 |
+
"issue_id": "fa74d48a",
|
| 22 |
+
"student_id": "STU001",
|
| 23 |
+
"book_id": "BK001",
|
| 24 |
+
"issue_date": "2025-11-13T19:02:59.338752",
|
| 25 |
+
"due_date": "2025-11-27T19:02:59.338773",
|
| 26 |
+
"return_date": null,
|
| 27 |
+
"status": "issued"
|
| 28 |
+
},
|
| 29 |
+
"894a4b97": {
|
| 30 |
+
"issue_id": "894a4b97",
|
| 31 |
+
"student_id": "250001",
|
| 32 |
+
"book_id": "250001",
|
| 33 |
+
"issue_date": "2025-11-15T20:49:47.878206",
|
| 34 |
+
"due_date": "2025-11-29T20:49:47.878228",
|
| 35 |
+
"return_date": "2025-11-15T20:50:31.959295",
|
| 36 |
+
"status": "returned"
|
| 37 |
+
}
|
| 38 |
+
}
|
data/users.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"ADMIN001": {
|
| 3 |
+
"id": "ADMIN001",
|
| 4 |
+
"name": "System Admin",
|
| 5 |
+
"email": "admin@lms.com",
|
| 6 |
+
"password": "$2b$12$fHhvbTH/kuz5ORiJlVDj6.38QOQa.6Eyr0Qp9zpImoHpUtXcSuXme",
|
| 7 |
+
"role": "admin",
|
| 8 |
+
"department": null,
|
| 9 |
+
"contact": null,
|
| 10 |
+
"created_at": "2025-11-11T20:59:30.219380"
|
| 11 |
+
},
|
| 12 |
+
"123": {
|
| 13 |
+
"id": "123",
|
| 14 |
+
"name": "abc",
|
| 15 |
+
"email": "gghg@gmail.com",
|
| 16 |
+
"password": "$2b$12$HmLWaXVJ0rVca6WgTi0qneD1ln2WIcW9Hp1rRLxbWACaEA55h6FG2",
|
| 17 |
+
"role": "student",
|
| 18 |
+
"department": "asd",
|
| 19 |
+
"contact": "asds",
|
| 20 |
+
"created_at": "2025-11-13T03:30:41.405388"
|
| 21 |
+
},
|
| 22 |
+
"STU001": {
|
| 23 |
+
"id": "STU001",
|
| 24 |
+
"name": "Test Student",
|
| 25 |
+
"email": "student@test.com",
|
| 26 |
+
"password": "$2b$12$f4nnfEFbtzBIg7CYisfz9eY6lBwjhFVMa4m1m536.HC5DyH8qEHoC",
|
| 27 |
+
"role": "student",
|
| 28 |
+
"department": "Computer Science",
|
| 29 |
+
"contact": "01712345678",
|
| 30 |
+
"created_at": "2025-11-13T19:58:12.293294"
|
| 31 |
+
},
|
| 32 |
+
"250001": {
|
| 33 |
+
"id": "250001",
|
| 34 |
+
"name": "Abdullah",
|
| 35 |
+
"email": "abdullah@lms.com",
|
| 36 |
+
"password": "$2b$12$YYK8u2F5m/No6Vaybgb7qOCzgqtEfz0xq3oPt//gJg.f1KzMdEbL6",
|
| 37 |
+
"role": "student",
|
| 38 |
+
"department": "Sharyiah",
|
| 39 |
+
"contact": "01711111111",
|
| 40 |
+
"created_at": "2025-11-15T20:40:22.624008"
|
| 41 |
+
}
|
| 42 |
+
}
|
database.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database layer - JSON file-based storage
|
| 3 |
+
"""
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from models import User, Book, Issue
|
| 8 |
+
|
| 9 |
+
DATA_DIR = 'data'
|
| 10 |
+
USERS_FILE = os.path.join(DATA_DIR, 'users.json')
|
| 11 |
+
BOOKS_FILE = os.path.join(DATA_DIR, 'books.json')
|
| 12 |
+
ISSUES_FILE = os.path.join(DATA_DIR, 'issues.json')
|
| 13 |
+
|
| 14 |
+
# Ensure data directory exists
|
| 15 |
+
os.makedirs(DATA_DIR, exist_ok=True)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def init_db():
|
| 19 |
+
"""Initialize database with default admin user"""
|
| 20 |
+
import bcrypt
|
| 21 |
+
|
| 22 |
+
if not os.path.exists(USERS_FILE):
|
| 23 |
+
admin_password = bcrypt.hashpw('admin123'.encode(), bcrypt.gensalt())
|
| 24 |
+
admin = User(
|
| 25 |
+
id='ADMIN001',
|
| 26 |
+
name='System Admin',
|
| 27 |
+
email='admin@lms.com',
|
| 28 |
+
password=admin_password.decode(),
|
| 29 |
+
role='admin'
|
| 30 |
+
)
|
| 31 |
+
save_data(USERS_FILE, {'ADMIN001': admin.to_dict()})
|
| 32 |
+
|
| 33 |
+
for file in [BOOKS_FILE, ISSUES_FILE]:
|
| 34 |
+
if not os.path.exists(file):
|
| 35 |
+
save_data(file, {})
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def load_data(filename):
|
| 39 |
+
"""Load data from JSON file"""
|
| 40 |
+
try:
|
| 41 |
+
with open(filename, 'r') as f:
|
| 42 |
+
return json.load(f)
|
| 43 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
| 44 |
+
return {}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def save_data(filename, data):
|
| 48 |
+
"""Save data to JSON file"""
|
| 49 |
+
with open(filename, 'w') as f:
|
| 50 |
+
json.dump(data, f, indent=2, default=str)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# User operations
|
| 54 |
+
def get_user(user_id):
|
| 55 |
+
"""Get user by ID"""
|
| 56 |
+
users = load_data(USERS_FILE)
|
| 57 |
+
user_data = users.get(user_id)
|
| 58 |
+
if user_data:
|
| 59 |
+
return User(**user_data)
|
| 60 |
+
return None
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def get_user_by_email(email):
|
| 64 |
+
"""Get user by email"""
|
| 65 |
+
users = load_data(USERS_FILE)
|
| 66 |
+
for user_data in users.values():
|
| 67 |
+
if user_data['email'] == email:
|
| 68 |
+
return User(**user_data)
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def get_all_users():
|
| 73 |
+
"""Get all users"""
|
| 74 |
+
users = load_data(USERS_FILE)
|
| 75 |
+
return [User(**data) for data in users.values()]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def get_students():
|
| 79 |
+
"""Get all students"""
|
| 80 |
+
users = load_data(USERS_FILE)
|
| 81 |
+
return [User(**data) for data in users.values() if data['role'] == 'student']
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def add_user(user):
|
| 85 |
+
"""Add new user"""
|
| 86 |
+
users = load_data(USERS_FILE)
|
| 87 |
+
users[user.id] = user.to_dict()
|
| 88 |
+
save_data(USERS_FILE, users)
|
| 89 |
+
return user
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def update_user(user):
|
| 93 |
+
"""Update existing user"""
|
| 94 |
+
users = load_data(USERS_FILE)
|
| 95 |
+
if user.id in users:
|
| 96 |
+
users[user.id] = user.to_dict()
|
| 97 |
+
save_data(USERS_FILE, users)
|
| 98 |
+
return True
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def delete_user(user_id):
|
| 103 |
+
"""Delete user"""
|
| 104 |
+
users = load_data(USERS_FILE)
|
| 105 |
+
if user_id in users:
|
| 106 |
+
del users[user_id]
|
| 107 |
+
save_data(USERS_FILE, users)
|
| 108 |
+
return True
|
| 109 |
+
return False
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# Book operations
|
| 113 |
+
def get_book(book_id):
|
| 114 |
+
"""Get book by ID"""
|
| 115 |
+
books = load_data(BOOKS_FILE)
|
| 116 |
+
book_data = books.get(book_id)
|
| 117 |
+
if book_data:
|
| 118 |
+
# Remove computed fields that shouldn't be in __init__
|
| 119 |
+
book_data.pop('is_available', None)
|
| 120 |
+
return Book(**book_data)
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def get_all_books():
|
| 125 |
+
"""Get all books"""
|
| 126 |
+
books = load_data(BOOKS_FILE)
|
| 127 |
+
result = []
|
| 128 |
+
for data in books.values():
|
| 129 |
+
# Remove computed fields that shouldn't be in __init__
|
| 130 |
+
data.pop('is_available', None)
|
| 131 |
+
result.append(Book(**data))
|
| 132 |
+
return result
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def search_books(query=None, category=None, available_only=False):
|
| 136 |
+
"""Search books by title, author, ISBN or category"""
|
| 137 |
+
books = get_all_books()
|
| 138 |
+
|
| 139 |
+
if query:
|
| 140 |
+
query = query.lower()
|
| 141 |
+
books = [b for b in books if
|
| 142 |
+
query in b.title.lower() or
|
| 143 |
+
query in b.author.lower() or
|
| 144 |
+
query in b.isbn.lower()]
|
| 145 |
+
|
| 146 |
+
if category:
|
| 147 |
+
books = [b for b in books if b.category == category]
|
| 148 |
+
|
| 149 |
+
if available_only:
|
| 150 |
+
books = [b for b in books if b.is_available()]
|
| 151 |
+
|
| 152 |
+
return books
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def add_book(book):
|
| 156 |
+
"""Add new book"""
|
| 157 |
+
books = load_data(BOOKS_FILE)
|
| 158 |
+
books[book.book_id] = book.to_dict()
|
| 159 |
+
save_data(BOOKS_FILE, books)
|
| 160 |
+
return book
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def update_book(book):
|
| 164 |
+
"""Update existing book"""
|
| 165 |
+
books = load_data(BOOKS_FILE)
|
| 166 |
+
if book.book_id in books:
|
| 167 |
+
books[book.book_id] = book.to_dict()
|
| 168 |
+
save_data(BOOKS_FILE, books)
|
| 169 |
+
return True
|
| 170 |
+
return False
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def delete_book(book_id):
|
| 174 |
+
"""Delete book"""
|
| 175 |
+
books = load_data(BOOKS_FILE)
|
| 176 |
+
if book_id in books:
|
| 177 |
+
del books[book_id]
|
| 178 |
+
save_data(BOOKS_FILE, books)
|
| 179 |
+
return True
|
| 180 |
+
return False
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def update_book_copies(book_id, available_copies):
|
| 184 |
+
"""Update available copies count"""
|
| 185 |
+
book = get_book(book_id)
|
| 186 |
+
if book:
|
| 187 |
+
book.available_copies = available_copies
|
| 188 |
+
update_book(book)
|
| 189 |
+
return True
|
| 190 |
+
return False
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# Issue operations
|
| 194 |
+
def get_issue(issue_id):
|
| 195 |
+
"""Get issue by ID"""
|
| 196 |
+
issues = load_data(ISSUES_FILE)
|
| 197 |
+
issue_data = issues.get(issue_id)
|
| 198 |
+
if issue_data:
|
| 199 |
+
# Convert date strings back to datetime
|
| 200 |
+
if 'issue_date' in issue_data and issue_data['issue_date']:
|
| 201 |
+
issue_data['issue_date'] = datetime.fromisoformat(issue_data['issue_date'])
|
| 202 |
+
if 'due_date' in issue_data and issue_data['due_date']:
|
| 203 |
+
issue_data['due_date'] = datetime.fromisoformat(issue_data['due_date'])
|
| 204 |
+
if 'return_date' in issue_data and issue_data['return_date']:
|
| 205 |
+
issue_data['return_date'] = datetime.fromisoformat(issue_data['return_date'])
|
| 206 |
+
return Issue(**issue_data)
|
| 207 |
+
return None
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def get_all_issues():
|
| 211 |
+
"""Get all issues"""
|
| 212 |
+
issues = load_data(ISSUES_FILE)
|
| 213 |
+
result = []
|
| 214 |
+
for data in issues.values():
|
| 215 |
+
if 'issue_date' in data and data['issue_date']:
|
| 216 |
+
data['issue_date'] = datetime.fromisoformat(data['issue_date'])
|
| 217 |
+
if 'due_date' in data and data['due_date']:
|
| 218 |
+
data['due_date'] = datetime.fromisoformat(data['due_date'])
|
| 219 |
+
if 'return_date' in data and data['return_date']:
|
| 220 |
+
data['return_date'] = datetime.fromisoformat(data['return_date'])
|
| 221 |
+
result.append(Issue(**data))
|
| 222 |
+
return result
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def get_active_issues():
|
| 226 |
+
"""Get all currently issued books (not returned)"""
|
| 227 |
+
return [i for i in get_all_issues() if i.status == 'issued']
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def get_overdue_issues():
|
| 231 |
+
"""Get all overdue issues"""
|
| 232 |
+
return [i for i in get_active_issues() if i.is_overdue()]
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def get_user_issues(user_id):
|
| 236 |
+
"""Get all issues for a specific user"""
|
| 237 |
+
return [i for i in get_all_issues() if i.student_id == user_id]
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def add_issue(issue):
|
| 241 |
+
"""Add new issue"""
|
| 242 |
+
issues = load_data(ISSUES_FILE)
|
| 243 |
+
issues[issue.issue_id] = issue.to_dict()
|
| 244 |
+
save_data(ISSUES_FILE, issues)
|
| 245 |
+
return issue
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def update_issue(issue):
|
| 249 |
+
"""Update existing issue"""
|
| 250 |
+
issues = load_data(ISSUES_FILE)
|
| 251 |
+
if issue.issue_id in issues:
|
| 252 |
+
issues[issue.issue_id] = issue.to_dict()
|
| 253 |
+
save_data(ISSUES_FILE, issues)
|
| 254 |
+
return True
|
| 255 |
+
return False
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
# Statistics
|
| 259 |
+
def get_stats():
|
| 260 |
+
"""Get system statistics"""
|
| 261 |
+
books = get_all_books()
|
| 262 |
+
users = get_students()
|
| 263 |
+
issues = get_active_issues()
|
| 264 |
+
overdue = get_overdue_issues()
|
| 265 |
+
|
| 266 |
+
return {
|
| 267 |
+
'total_books': len(books),
|
| 268 |
+
'total_copies': sum(b.total_copies for b in books),
|
| 269 |
+
'available_copies': sum(b.available_copies for b in books),
|
| 270 |
+
'total_students': len(users),
|
| 271 |
+
'books_issued': len(issues),
|
| 272 |
+
'overdue_books': len(overdue)
|
| 273 |
+
}
|
models.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database models for Library Management System
|
| 3 |
+
"""
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from flask_login import UserMixin
|
| 6 |
+
|
| 7 |
+
class User(UserMixin):
|
| 8 |
+
"""User model for authentication and role management"""
|
| 9 |
+
|
| 10 |
+
ROLES = {
|
| 11 |
+
'admin': 'Admin',
|
| 12 |
+
'librarian': 'Librarian',
|
| 13 |
+
'student': 'Student'
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
def __init__(self, id, name, email, password, role='student', department=None, contact=None, created_at=None):
|
| 17 |
+
self.id = id
|
| 18 |
+
self.name = name
|
| 19 |
+
self.email = email
|
| 20 |
+
self.password = password # Should be hashed
|
| 21 |
+
self.role = role
|
| 22 |
+
self.department = department
|
| 23 |
+
self.contact = contact
|
| 24 |
+
self.created_at = created_at if created_at else datetime.now()
|
| 25 |
+
|
| 26 |
+
def is_admin(self):
|
| 27 |
+
return self.role == 'admin'
|
| 28 |
+
|
| 29 |
+
def is_librarian(self):
|
| 30 |
+
return self.role == 'librarian'
|
| 31 |
+
|
| 32 |
+
def is_student(self):
|
| 33 |
+
return self.role == 'student'
|
| 34 |
+
|
| 35 |
+
def can_manage_books(self):
|
| 36 |
+
return self.role in ['admin', 'librarian']
|
| 37 |
+
|
| 38 |
+
def can_manage_users(self):
|
| 39 |
+
return self.role in ['admin', 'librarian']
|
| 40 |
+
|
| 41 |
+
def to_dict(self):
|
| 42 |
+
return {
|
| 43 |
+
'id': self.id,
|
| 44 |
+
'name': self.name,
|
| 45 |
+
'email': self.email,
|
| 46 |
+
'password': self.password,
|
| 47 |
+
'role': self.role,
|
| 48 |
+
'department': self.department,
|
| 49 |
+
'contact': self.contact,
|
| 50 |
+
'created_at': self.created_at.isoformat() if isinstance(self.created_at, datetime) else self.created_at
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class Book:
|
| 55 |
+
"""Book model"""
|
| 56 |
+
|
| 57 |
+
def __init__(self, book_id, title, author, isbn, category, publisher,
|
| 58 |
+
year, total_copies, shelf_no, cover_image=None, available_copies=None, created_at=None):
|
| 59 |
+
self.book_id = book_id
|
| 60 |
+
self.title = title
|
| 61 |
+
self.author = author
|
| 62 |
+
self.isbn = isbn
|
| 63 |
+
self.category = category
|
| 64 |
+
self.publisher = publisher
|
| 65 |
+
self.year = year
|
| 66 |
+
self.total_copies = total_copies
|
| 67 |
+
self.available_copies = available_copies if available_copies is not None else total_copies
|
| 68 |
+
self.shelf_no = shelf_no
|
| 69 |
+
self.cover_image = cover_image
|
| 70 |
+
self.created_at = created_at if created_at else datetime.now()
|
| 71 |
+
|
| 72 |
+
def is_available(self):
|
| 73 |
+
return self.available_copies > 0
|
| 74 |
+
|
| 75 |
+
def to_dict(self):
|
| 76 |
+
return {
|
| 77 |
+
'book_id': self.book_id,
|
| 78 |
+
'title': self.title,
|
| 79 |
+
'author': self.author,
|
| 80 |
+
'isbn': self.isbn,
|
| 81 |
+
'category': self.category,
|
| 82 |
+
'publisher': self.publisher,
|
| 83 |
+
'year': self.year,
|
| 84 |
+
'total_copies': self.total_copies,
|
| 85 |
+
'available_copies': self.available_copies,
|
| 86 |
+
'shelf_no': self.shelf_no,
|
| 87 |
+
'cover_image': self.cover_image,
|
| 88 |
+
'created_at': self.created_at.isoformat() if isinstance(self.created_at, datetime) else self.created_at
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class Issue:
|
| 93 |
+
"""Book issue/borrowing record"""
|
| 94 |
+
|
| 95 |
+
def __init__(self, issue_id, student_id, book_id, issue_date=None, due_date=None,
|
| 96 |
+
return_date=None, status='issued'):
|
| 97 |
+
self.issue_id = issue_id
|
| 98 |
+
self.student_id = student_id
|
| 99 |
+
self.book_id = book_id
|
| 100 |
+
self.issue_date = issue_date or datetime.now()
|
| 101 |
+
self.due_date = due_date or (datetime.now() + timedelta(days=14)) # 14 days default
|
| 102 |
+
self.return_date = return_date
|
| 103 |
+
self.status = status # 'issued', 'returned', 'overdue'
|
| 104 |
+
|
| 105 |
+
def is_overdue(self):
|
| 106 |
+
if self.status == 'returned':
|
| 107 |
+
return False
|
| 108 |
+
return datetime.now() > self.due_date
|
| 109 |
+
|
| 110 |
+
def days_overdue(self):
|
| 111 |
+
if not self.is_overdue():
|
| 112 |
+
return 0
|
| 113 |
+
if self.return_date:
|
| 114 |
+
return (self.return_date - self.due_date).days
|
| 115 |
+
return (datetime.now() - self.due_date).days
|
| 116 |
+
|
| 117 |
+
def to_dict(self):
|
| 118 |
+
return {
|
| 119 |
+
'issue_id': self.issue_id,
|
| 120 |
+
'student_id': self.student_id,
|
| 121 |
+
'book_id': self.book_id,
|
| 122 |
+
'issue_date': self.issue_date.isoformat() if isinstance(self.issue_date, datetime) else self.issue_date,
|
| 123 |
+
'due_date': self.due_date.isoformat() if isinstance(self.due_date, datetime) else self.due_date,
|
| 124 |
+
'return_date': self.return_date.isoformat() if isinstance(self.return_date, datetime) else self.return_date,
|
| 125 |
+
'status': self.status
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==3.1.2
|
| 2 |
+
flask-login==0.6.3
|
| 3 |
+
bcrypt==4.1.2
|
| 4 |
+
werkzeug==3.1.3
|
run.sh
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Library Management System Launcher
|
| 4 |
+
echo "π Starting Library Management System..."
|
| 5 |
+
echo ""
|
| 6 |
+
|
| 7 |
+
# Navigate to LMS directory
|
| 8 |
+
cd "/Users/faizur.zunayed/Gwtf/LMS"
|
| 9 |
+
|
| 10 |
+
# Check if virtual environment exists, create if not
|
| 11 |
+
if [ ! -d "venv" ]; then
|
| 12 |
+
echo "π¦ Setting up environment for first time..."
|
| 13 |
+
python3.12 -m venv venv
|
| 14 |
+
source venv/bin/activate
|
| 15 |
+
pip install -r requirements.txt
|
| 16 |
+
else
|
| 17 |
+
source venv/bin/activate
|
| 18 |
+
fi
|
| 19 |
+
|
| 20 |
+
# Install dependencies if needed
|
| 21 |
+
echo "β
Environment ready!"
|
| 22 |
+
echo ""
|
| 23 |
+
|
| 24 |
+
# Run the application
|
| 25 |
+
echo "π Starting server on http://127.0.0.1:5001"
|
| 26 |
+
echo "π§ Login: admin@lms.com / admin123"
|
| 27 |
+
echo ""
|
| 28 |
+
echo "Press Ctrl+C to stop the server"
|
| 29 |
+
echo "================================================"
|
| 30 |
+
echo ""
|
| 31 |
+
|
| 32 |
+
python3.12 app.py
|
templates/add_book.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>Add Book</h2>
|
| 4 |
+
<div class="card">
|
| 5 |
+
<form method="POST">
|
| 6 |
+
<label>Book ID:</label><input name="book_id" required>
|
| 7 |
+
<label>Title:</label><input name="title" required>
|
| 8 |
+
<label>Author:</label><input name="author" required>
|
| 9 |
+
<label>ISBN:</label><input name="isbn" required>
|
| 10 |
+
<label>Category:</label><input name="category" required>
|
| 11 |
+
<label>Publisher:</label><input name="publisher" required>
|
| 12 |
+
<label>Year:</label><input type="number" name="year" required>
|
| 13 |
+
<label>Total Copies:</label><input type="number" name="total_copies" required>
|
| 14 |
+
<label>Shelf No:</label><input name="shelf_no" required>
|
| 15 |
+
<label>Cover Image URL (optional):</label><input name="cover_image">
|
| 16 |
+
<button type="submit" class="btn">Add Book</button>
|
| 17 |
+
<a href="{{ url_for('books') }}" class="btn btn-danger">Cancel</a>
|
| 18 |
+
</form>
|
| 19 |
+
</div>
|
| 20 |
+
{% endblock %}
|
templates/add_student.html
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>Add User</h2>
|
| 4 |
+
<div class="card">
|
| 5 |
+
<form method="POST">
|
| 6 |
+
<label>User ID:</label><input name="student_id" required>
|
| 7 |
+
<label>Name:</label><input name="name" required>
|
| 8 |
+
<label>Email:</label><input type="email" name="email" required>
|
| 9 |
+
<label>Role:</label>
|
| 10 |
+
<select name="role" required style="padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 4px; width: 100%; margin-bottom: 1rem;">
|
| 11 |
+
<option value="student">Student</option>
|
| 12 |
+
<option value="librarian">Librarian</option>
|
| 13 |
+
<option value="admin">Admin</option>
|
| 14 |
+
</select>
|
| 15 |
+
<label>Department:</label><input name="department">
|
| 16 |
+
<label>Contact:</label><input name="contact">
|
| 17 |
+
<label>Password (default: student123):</label><input type="password" name="password">
|
| 18 |
+
<button type="submit" class="btn">Add User</button>
|
| 19 |
+
<a href="{{ url_for('students') }}" class="btn btn-danger">Cancel</a>
|
| 20 |
+
</form>
|
| 21 |
+
</div>
|
| 22 |
+
{% endblock %}
|
templates/base.html
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}Library Management System{% endblock %}</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
+
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; }
|
| 10 |
+
nav { background: #2c3e50; color: white; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; }
|
| 11 |
+
nav h1 { font-size: 1.5rem; }
|
| 12 |
+
nav ul { list-style: none; display: flex; gap: 2rem; }
|
| 13 |
+
nav a { color: white; text-decoration: none; padding: 0.5rem 1rem; border-radius: 4px; }
|
| 14 |
+
nav a:hover { background: #34495e; }
|
| 15 |
+
.container { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; }
|
| 16 |
+
.flash { padding: 1rem; margin-bottom: 1rem; border-radius: 4px; }
|
| 17 |
+
.flash.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
| 18 |
+
.flash.danger { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
| 19 |
+
.flash.warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
|
| 20 |
+
.card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 2rem; }
|
| 21 |
+
.btn { display: inline-block; padding: 0.75rem 1.5rem; background: #3498db; color: white; text-decoration: none; border-radius: 4px; border: none; cursor: pointer; }
|
| 22 |
+
.btn:hover { background: #2980b9; }
|
| 23 |
+
.btn-danger { background: #e74c3c; }
|
| 24 |
+
.btn-danger:hover { background: #c0392b; }
|
| 25 |
+
.btn-success { background: #27ae60; }
|
| 26 |
+
.btn-success:hover { background: #229954; }
|
| 27 |
+
table { width: 100%; border-collapse: collapse; }
|
| 28 |
+
table th, table td { padding: 1rem; text-align: left; border-bottom: 1px solid #ddd; }
|
| 29 |
+
table th { background: #ecf0f1; font-weight: 600; }
|
| 30 |
+
form label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
|
| 31 |
+
form input, form select { width: 100%; padding: 0.75rem; margin-bottom: 1rem; border: 1px solid #ddd; border-radius: 4px; }
|
| 32 |
+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
| 33 |
+
.stat-card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
| 34 |
+
.stat-card h3 { color: #7f8c8d; font-size: 0.9rem; margin-bottom: 0.5rem; }
|
| 35 |
+
.stat-card p { font-size: 2rem; font-weight: bold; color: #2c3e50; }
|
| 36 |
+
</style>
|
| 37 |
+
</head>
|
| 38 |
+
<body>
|
| 39 |
+
{% if current_user.is_authenticated %}
|
| 40 |
+
<nav>
|
| 41 |
+
<h1>π Library Management System</h1>
|
| 42 |
+
<ul>
|
| 43 |
+
<li><a href="{{ url_for('dashboard') }}">Dashboard</a></li>
|
| 44 |
+
{% if current_user.can_manage_books() %}
|
| 45 |
+
<li><a href="{{ url_for('students') }}">Students</a></li>
|
| 46 |
+
<li><a href="{{ url_for('books') }}">Books</a></li>
|
| 47 |
+
<li><a href="{{ url_for('issue_book') }}">Issue</a></li>
|
| 48 |
+
<li><a href="{{ url_for('return_book') }}">Return</a></li>
|
| 49 |
+
<li><a href="{{ url_for('reports') }}">Reports</a></li>
|
| 50 |
+
{% else %}
|
| 51 |
+
<li><a href="{{ url_for('books') }}">Browse Books</a></li>
|
| 52 |
+
{% endif %}
|
| 53 |
+
<li><a href="{{ url_for('logout') }}">Logout ({{ current_user.name }})</a></li>
|
| 54 |
+
</ul>
|
| 55 |
+
</nav>
|
| 56 |
+
{% endif %}
|
| 57 |
+
|
| 58 |
+
<div class="container">
|
| 59 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 60 |
+
{% if messages %}
|
| 61 |
+
{% for category, message in messages %}
|
| 62 |
+
<div class="flash {{ category }}">{{ message }}</div>
|
| 63 |
+
{% endfor %}
|
| 64 |
+
{% endif %}
|
| 65 |
+
{% endwith %}
|
| 66 |
+
|
| 67 |
+
{% block content %}{% endblock %}
|
| 68 |
+
</div>
|
| 69 |
+
</body>
|
| 70 |
+
</html>
|
templates/books.html
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
| 4 |
+
<h2>π Books Library</h2>
|
| 5 |
+
{% if current_user.can_manage_books() %}
|
| 6 |
+
<a href="{{ url_for('add_book') }}" class="btn btn-success">+ Add New Book</a>
|
| 7 |
+
{% endif %}
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+
<div class="card">
|
| 11 |
+
<form method="GET" style="display: grid; grid-template-columns: 1fr 200px 200px auto; gap: 1rem;">
|
| 12 |
+
<input type="text" name="q" placeholder="Search by title, author, or ISBN..." value="{{ query }}">
|
| 13 |
+
<select name="category">
|
| 14 |
+
<option value="">All Categories</option>
|
| 15 |
+
{% for cat in categories %}
|
| 16 |
+
<option value="{{ cat }}" {% if cat == selected_category %}selected{% endif %}>{{ cat }}</option>
|
| 17 |
+
{% endfor %}
|
| 18 |
+
</select>
|
| 19 |
+
<select name="available">
|
| 20 |
+
<option value="">All Books</option>
|
| 21 |
+
<option value="true" {% if available_only %}selected{% endif %}>Available Only</option>
|
| 22 |
+
</select>
|
| 23 |
+
<button type="submit" class="btn">Search</button>
|
| 24 |
+
</form>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div class="card">
|
| 28 |
+
<table>
|
| 29 |
+
<tr>
|
| 30 |
+
<th>Book ID</th>
|
| 31 |
+
<th>Title</th>
|
| 32 |
+
<th>Author</th>
|
| 33 |
+
<th>Category</th>
|
| 34 |
+
<th>Shelf No</th>
|
| 35 |
+
<th>Total</th>
|
| 36 |
+
<th>Available</th>
|
| 37 |
+
<th>Status</th>
|
| 38 |
+
{% if current_user.can_manage_books() %}
|
| 39 |
+
<th>Actions</th>
|
| 40 |
+
{% endif %}
|
| 41 |
+
</tr>
|
| 42 |
+
{% for book in books %}
|
| 43 |
+
<tr>
|
| 44 |
+
<td>{{ book.book_id }}</td>
|
| 45 |
+
<td><strong>{{ book.title }}</strong></td>
|
| 46 |
+
<td>{{ book.author }}</td>
|
| 47 |
+
<td>{{ book.category }}</td>
|
| 48 |
+
<td>ποΈ {{ book.shelf_no }}</td>
|
| 49 |
+
<td>{{ book.total_copies }}</td>
|
| 50 |
+
<td>{{ book.available_copies }}</td>
|
| 51 |
+
<td>
|
| 52 |
+
{% if book.is_available() %}
|
| 53 |
+
<span style="color: #27ae60;">β Available</span>
|
| 54 |
+
{% else %}
|
| 55 |
+
<span style="color: #e74c3c;">β Not Available</span>
|
| 56 |
+
{% endif %}
|
| 57 |
+
</td>
|
| 58 |
+
{% if current_user.can_manage_books() %}
|
| 59 |
+
<td>
|
| 60 |
+
<a href="{{ url_for('edit_book', book_id=book.book_id) }}" class="btn" style="padding: 0.5rem 1rem;">Edit</a>
|
| 61 |
+
<form method="POST" action="{{ url_for('delete_book', book_id=book.book_id) }}" style="display: inline;">
|
| 62 |
+
<button type="submit" class="btn btn-danger" style="padding: 0.5rem 1rem;" onclick="return confirm('Delete this book?')">Delete</button>
|
| 63 |
+
</form>
|
| 64 |
+
</td>
|
| 65 |
+
{% endif %}
|
| 66 |
+
</tr>
|
| 67 |
+
{% else %}
|
| 68 |
+
<tr><td colspan="9" style="text-align: center;">No books found</td></tr>
|
| 69 |
+
{% endfor %}
|
| 70 |
+
</table>
|
| 71 |
+
</div>
|
| 72 |
+
{% endblock %}
|
templates/dashboard.html
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>π Dashboard</h2>
|
| 4 |
+
|
| 5 |
+
<div class="stats">
|
| 6 |
+
<div class="stat-card">
|
| 7 |
+
<h3>Total Books</h3>
|
| 8 |
+
<p>{{ stats.total_books }}</p>
|
| 9 |
+
</div>
|
| 10 |
+
<div class="stat-card">
|
| 11 |
+
<h3>Total Copies</h3>
|
| 12 |
+
<p>{{ stats.total_copies }}</p>
|
| 13 |
+
</div>
|
| 14 |
+
<div class="stat-card">
|
| 15 |
+
<h3>Available Copies</h3>
|
| 16 |
+
<p>{{ stats.available_copies }}</p>
|
| 17 |
+
</div>
|
| 18 |
+
<div class="stat-card">
|
| 19 |
+
<h3>Total Students</h3>
|
| 20 |
+
<p>{{ stats.total_students }}</p>
|
| 21 |
+
</div>
|
| 22 |
+
<div class="stat-card">
|
| 23 |
+
<h3>Books Issued</h3>
|
| 24 |
+
<p>{{ stats.books_issued }}</p>
|
| 25 |
+
</div>
|
| 26 |
+
<div class="stat-card">
|
| 27 |
+
<h3>Overdue Books</h3>
|
| 28 |
+
<p style="color: #e74c3c;">{{ stats.overdue_books }}</p>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="stat-card">
|
| 31 |
+
<h3>Total Fines</h3>
|
| 32 |
+
<p>{{ stats.total_fines }} BDT</p>
|
| 33 |
+
</div>
|
| 34 |
+
<div class="stat-card">
|
| 35 |
+
<h3>Collected Fines</h3>
|
| 36 |
+
<p style="color: #27ae60;">{{ stats.collected_fines }} BDT</p>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
{% if overdue %}
|
| 41 |
+
<div class="card">
|
| 42 |
+
<h3 style="color: #e74c3c;">β οΈ Overdue Books ({{ overdue|length }})</h3>
|
| 43 |
+
<table>
|
| 44 |
+
<tr>
|
| 45 |
+
<th>Student ID</th>
|
| 46 |
+
<th>Book ID</th>
|
| 47 |
+
<th>Due Date</th>
|
| 48 |
+
<th>Days Overdue</th>
|
| 49 |
+
<th>Fine</th>
|
| 50 |
+
</tr>
|
| 51 |
+
{% for issue in overdue %}
|
| 52 |
+
<tr>
|
| 53 |
+
<td>{{ issue.student_id }}</td>
|
| 54 |
+
<td>{{ issue.book_id }}</td>
|
| 55 |
+
<td>{{ issue.due_date.strftime('%Y-%m-%d') }}</td>
|
| 56 |
+
<td>{{ issue.days_overdue() }}</td>
|
| 57 |
+
<td>{{ issue.calculate_fine() }} BDT</td>
|
| 58 |
+
</tr>
|
| 59 |
+
{% endfor %}
|
| 60 |
+
</table>
|
| 61 |
+
</div>
|
| 62 |
+
{% endif %}
|
| 63 |
+
{% endblock %}
|
templates/edit_book.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>Edit Book</h2>
|
| 4 |
+
<div class="card">
|
| 5 |
+
<form method="POST">
|
| 6 |
+
<label>Title:</label><input name="title" value="{{ book.title }}" required>
|
| 7 |
+
<label>Author:</label><input name="author" value="{{ book.author }}" required>
|
| 8 |
+
<label>ISBN:</label><input name="isbn" value="{{ book.isbn }}" required>
|
| 9 |
+
<label>Category:</label><input name="category" value="{{ book.category }}" required>
|
| 10 |
+
<label>Publisher:</label><input name="publisher" value="{{ book.publisher }}" required>
|
| 11 |
+
<label>Year:</label><input type="number" name="year" value="{{ book.year }}" required>
|
| 12 |
+
<label>Total Copies:</label><input type="number" name="total_copies" value="{{ book.total_copies }}" required>
|
| 13 |
+
<label>Shelf No:</label><input name="shelf_no" value="{{ book.shelf_no }}" required>
|
| 14 |
+
<button type="submit" class="btn">Update</button>
|
| 15 |
+
<a href="{{ url_for('books') }}" class="btn btn-danger">Cancel</a>
|
| 16 |
+
</form>
|
| 17 |
+
</div>
|
| 18 |
+
{% endblock %}
|
templates/edit_student.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>Edit User</h2>
|
| 4 |
+
<div class="card">
|
| 5 |
+
<form method="POST">
|
| 6 |
+
<label>Name:</label><input name="name" value="{{ student.name }}" required>
|
| 7 |
+
<label>Email:</label><input type="email" name="email" value="{{ student.email }}" required>
|
| 8 |
+
<label>Role:</label>
|
| 9 |
+
<select name="role" required style="padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 4px; width: 100%; margin-bottom: 1rem;">
|
| 10 |
+
<option value="student" {% if student.role == 'student' %}selected{% endif %}>Student</option>
|
| 11 |
+
<option value="librarian" {% if student.role == 'librarian' %}selected{% endif %}>Librarian</option>
|
| 12 |
+
<option value="admin" {% if student.role == 'admin' %}selected{% endif %}>Admin</option>
|
| 13 |
+
</select>
|
| 14 |
+
<label>Department:</label><input name="department" value="{{ student.department }}">
|
| 15 |
+
<label>Contact:</label><input name="contact" value="{{ student.contact }}">
|
| 16 |
+
<button type="submit" class="btn">Update</button>
|
| 17 |
+
<a href="{{ url_for('students') }}" class="btn btn-danger">Cancel</a>
|
| 18 |
+
</form>
|
| 19 |
+
</div>
|
| 20 |
+
{% endblock %}
|
templates/issue_book.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>Issue Book</h2>
|
| 4 |
+
<div class="card">
|
| 5 |
+
<form method="POST">
|
| 6 |
+
<label>Student ID:</label>
|
| 7 |
+
<select name="student_id" required>
|
| 8 |
+
{% for s in students %}<option value="{{ s.id }}">{{ s.id }} - {{ s.name }}</option>{% endfor %}
|
| 9 |
+
</select>
|
| 10 |
+
<label>Book ID:</label>
|
| 11 |
+
<select name="book_id" required>
|
| 12 |
+
{% for b in books %}<option value="{{ b.book_id }}">{{ b.book_id }} - {{ b.title }} ({{ b.available_copies }} available)</option>{% endfor %}
|
| 13 |
+
</select>
|
| 14 |
+
<button type="submit" class="btn">Issue Book</button>
|
| 15 |
+
<a href="{{ url_for('dashboard') }}" class="btn btn-danger">Cancel</a>
|
| 16 |
+
</form>
|
| 17 |
+
</div>
|
| 18 |
+
{% endblock %}
|
templates/login.html
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Login - LMS{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="card" style="max-width: 400px; margin: 4rem auto;">
|
| 7 |
+
<h2 style="text-align: center; margin-bottom: 2rem;">π Library Login</h2>
|
| 8 |
+
<form method="POST">
|
| 9 |
+
<label>Email:</label>
|
| 10 |
+
<input type="email" name="email" required>
|
| 11 |
+
|
| 12 |
+
<label>Password:</label>
|
| 13 |
+
<input type="password" name="password" required>
|
| 14 |
+
|
| 15 |
+
<button type="submit" class="btn" style="width: 100%;">Login</button>
|
| 16 |
+
</form>
|
| 17 |
+
<p style="margin-top: 1rem; text-align: center; color: #7f8c8d;">
|
| 18 |
+
Default: admin@lms.com / admin123
|
| 19 |
+
</p>
|
| 20 |
+
</div>
|
| 21 |
+
{% endblock %}
|
templates/reports.html
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>π Reports</h2>
|
| 4 |
+
|
| 5 |
+
<div class="card" style="margin-bottom: 2rem;">
|
| 6 |
+
<h3>System Statistics</h3>
|
| 7 |
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; margin-top: 1rem;">
|
| 8 |
+
<div style="text-align: center; padding: 1rem; background: #f3f4f6; border-radius: 8px;">
|
| 9 |
+
<div style="font-size: 2rem; font-weight: bold; color: #2563eb;">{{ stats.total_books }}</div>
|
| 10 |
+
<div style="color: #6b7280;">Total Books</div>
|
| 11 |
+
</div>
|
| 12 |
+
<div style="text-align: center; padding: 1rem; background: #f3f4f6; border-radius: 8px;">
|
| 13 |
+
<div style="font-size: 2rem; font-weight: bold; color: #f59e0b;">{{ stats.books_issued }}</div>
|
| 14 |
+
<div style="color: #6b7280;">Books Issued</div>
|
| 15 |
+
</div>
|
| 16 |
+
<div style="text-align: center; padding: 1rem; background: #f3f4f6; border-radius: 8px;">
|
| 17 |
+
<div style="font-size: 2rem; font-weight: bold; color: #ef4444;">{{ stats.overdue_books }}</div>
|
| 18 |
+
<div style="color: #6b7280;">Overdue Books</div>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div class="card" style="margin-bottom: 2rem;">
|
| 24 |
+
<h3>π All Issues</h3>
|
| 25 |
+
{% if issues %}
|
| 26 |
+
<table>
|
| 27 |
+
<tr>
|
| 28 |
+
<th>Issue ID</th>
|
| 29 |
+
<th>Student ID</th>
|
| 30 |
+
<th>Book ID</th>
|
| 31 |
+
<th>Issue Date</th>
|
| 32 |
+
<th>Due Date</th>
|
| 33 |
+
<th>Status</th>
|
| 34 |
+
</tr>
|
| 35 |
+
{% for issue in issues %}
|
| 36 |
+
<tr>
|
| 37 |
+
<td>{{ issue.issue_id }}</td>
|
| 38 |
+
<td>{{ issue.student_id }}</td>
|
| 39 |
+
<td>{{ issue.book_id }}</td>
|
| 40 |
+
<td>{{ issue.issue_date.strftime('%Y-%m-%d') }}</td>
|
| 41 |
+
<td>{{ issue.due_date.strftime('%Y-%m-%d') }}</td>
|
| 42 |
+
<td>
|
| 43 |
+
{% if issue.status == 'returned' %}
|
| 44 |
+
<span style="background: #d1fae5; color: #065f46; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">β Returned</span>
|
| 45 |
+
{% elif issue.is_overdue() %}
|
| 46 |
+
<span style="background: #fee2e2; color: #991b1b; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">β Overdue</span>
|
| 47 |
+
{% else %}
|
| 48 |
+
<span style="background: #fef3c7; color: #92400e; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">π Active</span>
|
| 49 |
+
{% endif %}
|
| 50 |
+
</td>
|
| 51 |
+
</tr>
|
| 52 |
+
{% endfor %}
|
| 53 |
+
</table>
|
| 54 |
+
{% else %}
|
| 55 |
+
<p style="text-align: center; color: #9ca3af; padding: 2rem;">No issues found</p>
|
| 56 |
+
{% endif %}
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div class="card">
|
| 60 |
+
<h3>β Overdue Books</h3>
|
| 61 |
+
{% if overdue %}
|
| 62 |
+
<table>
|
| 63 |
+
<tr>
|
| 64 |
+
<th>Issue ID</th>
|
| 65 |
+
<th>Student ID</th>
|
| 66 |
+
<th>Book ID</th>
|
| 67 |
+
<th>Due Date</th>
|
| 68 |
+
<th>Days Overdue</th>
|
| 69 |
+
</tr>
|
| 70 |
+
{% for issue in overdue %}
|
| 71 |
+
<tr>
|
| 72 |
+
<td>{{ issue.issue_id }}</td>
|
| 73 |
+
<td>{{ issue.student_id }}</td>
|
| 74 |
+
<td>{{ issue.book_id }}</td>
|
| 75 |
+
<td>{{ issue.due_date.strftime('%Y-%m-%d') }}</td>
|
| 76 |
+
<td><strong style="color: #ef4444;">{{ issue.days_overdue() }} days</strong></td>
|
| 77 |
+
</tr>
|
| 78 |
+
{% endfor %}
|
| 79 |
+
</table>
|
| 80 |
+
{% else %}
|
| 81 |
+
<p style="text-align: center; color: #9ca3af; padding: 2rem;">No overdue books</p>
|
| 82 |
+
{% endif %}
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
{% endblock %}
|
templates/return_book.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>Return Book</h2>
|
| 4 |
+
<div class="card">
|
| 5 |
+
<form method="POST">
|
| 6 |
+
<label>Select Issue to Return:</label>
|
| 7 |
+
<select name="student_id" required onchange="this.form.book_id.value=this.options[this.selectedIndex].dataset.bookid">
|
| 8 |
+
{% for i in issues %}<option value="{{ i.student_id }}" data-bookid="{{ i.book_id }}">{{ i.student_id }} - {{ i.book_id }}</option>{% endfor %}
|
| 9 |
+
</select>
|
| 10 |
+
<input type="hidden" name="book_id">
|
| 11 |
+
<button type="submit" class="btn">Return Book</button>
|
| 12 |
+
<a href="{{ url_for('dashboard') }}" class="btn btn-danger">Cancel</a>
|
| 13 |
+
</form>
|
| 14 |
+
</div>
|
| 15 |
+
{% endblock %}
|
templates/student_dashboard.html
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<h2>π Welcome, {{ current_user.name }}!</h2>
|
| 4 |
+
|
| 5 |
+
<div class="stats">
|
| 6 |
+
<div class="stat-card">
|
| 7 |
+
<h3>Total Books in Library</h3>
|
| 8 |
+
<p>{{ stats.total_books }}</p>
|
| 9 |
+
</div>
|
| 10 |
+
<div class="stat-card">
|
| 11 |
+
<h3>Available Books</h3>
|
| 12 |
+
<p>{{ stats.available_copies }}</p>
|
| 13 |
+
</div>
|
| 14 |
+
<div class="stat-card">
|
| 15 |
+
<h3>Books I Borrowed</h3>
|
| 16 |
+
<p>{{ my_issues|length }}</p>
|
| 17 |
+
</div>
|
| 18 |
+
<div class="stat-card">
|
| 19 |
+
<h3>My Fines</h3>
|
| 20 |
+
<p style="color: {% if my_fines %}#e74c3c{% else %}#27ae60{% endif %};">
|
| 21 |
+
{{ my_fines|sum(attribute='amount')|default(0) }} BDT
|
| 22 |
+
</p>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
<div class="card">
|
| 27 |
+
<h3>π Browse All Books</h3>
|
| 28 |
+
<p><a href="{{ url_for('books') }}" class="btn">View All Books β</a></p>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
{% if my_issues %}
|
| 32 |
+
<div class="card">
|
| 33 |
+
<h3>π My Borrowed Books</h3>
|
| 34 |
+
<table>
|
| 35 |
+
<tr>
|
| 36 |
+
<th>Book ID</th>
|
| 37 |
+
<th>Issue Date</th>
|
| 38 |
+
<th>Due Date</th>
|
| 39 |
+
<th>Status</th>
|
| 40 |
+
</tr>
|
| 41 |
+
{% for issue in my_issues %}
|
| 42 |
+
<tr>
|
| 43 |
+
<td>{{ issue.book_id }}</td>
|
| 44 |
+
<td>{{ issue.issue_date.strftime('%Y-%m-%d') }}</td>
|
| 45 |
+
<td>{{ issue.due_date.strftime('%Y-%m-%d') }}</td>
|
| 46 |
+
<td>
|
| 47 |
+
{% if issue.status == 'returned' %}
|
| 48 |
+
<span style="color: #27ae60;">β Returned</span>
|
| 49 |
+
{% elif issue.is_overdue() %}
|
| 50 |
+
<span style="color: #e74c3c;">β οΈ Overdue ({{ issue.days_overdue() }} days)</span>
|
| 51 |
+
{% else %}
|
| 52 |
+
<span style="color: #3498db;">π Issued</span>
|
| 53 |
+
{% endif %}
|
| 54 |
+
</td>
|
| 55 |
+
</tr>
|
| 56 |
+
{% endfor %}
|
| 57 |
+
</table>
|
| 58 |
+
</div>
|
| 59 |
+
{% endif %}
|
| 60 |
+
{% endblock %}
|
templates/students.html
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
| 4 |
+
<h2>π¨βπ Students</h2>
|
| 5 |
+
<a href="{{ url_for('add_student') }}" class="btn btn-success">+ Add Student</a>
|
| 6 |
+
</div>
|
| 7 |
+
|
| 8 |
+
<!-- Search Bar -->
|
| 9 |
+
<div class="card" style="margin-bottom: 1.5rem; padding: 1rem;">
|
| 10 |
+
<form method="GET" action="{{ url_for('students') }}" style="display: flex; gap: 1rem; align-items: center;">
|
| 11 |
+
<input type="text"
|
| 12 |
+
name="q"
|
| 13 |
+
value="{{ search_query or '' }}"
|
| 14 |
+
placeholder="π Search by Student ID or Name..."
|
| 15 |
+
style="flex: 1; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 1rem;">
|
| 16 |
+
<button type="submit" class="btn" style="padding: 0.75rem 1.5rem;">Search</button>
|
| 17 |
+
{% if search_query %}
|
| 18 |
+
<a href="{{ url_for('students') }}" class="btn" style="padding: 0.75rem 1.5rem; background: #6b7280;">Clear</a>
|
| 19 |
+
{% endif %}
|
| 20 |
+
</form>
|
| 21 |
+
{% if search_query %}
|
| 22 |
+
<div style="margin-top: 0.5rem; color: #6b7280; font-size: 0.9rem;">
|
| 23 |
+
Found {{ students|length }} student(s) matching "{{ search_query }}"
|
| 24 |
+
</div>
|
| 25 |
+
{% endif %}
|
| 26 |
+
</div>
|
| 27 |
+
<div class="card">
|
| 28 |
+
<table>
|
| 29 |
+
<tr>
|
| 30 |
+
<th>Student ID</th>
|
| 31 |
+
<th>Name</th>
|
| 32 |
+
<th>Email</th>
|
| 33 |
+
<th>Department</th>
|
| 34 |
+
<th>Contact</th>
|
| 35 |
+
<th>Issued Books</th>
|
| 36 |
+
<th>Actions</th>
|
| 37 |
+
</tr>
|
| 38 |
+
{% for student in students %}
|
| 39 |
+
<tr>
|
| 40 |
+
<td>{{ student.id }}</td>
|
| 41 |
+
<td>
|
| 42 |
+
<a href="{{ url_for('view_student', student_id=student.id) }}"
|
| 43 |
+
style="color: #2563eb; text-decoration: none; font-weight: 600;">
|
| 44 |
+
{{ student.name }}
|
| 45 |
+
</a>
|
| 46 |
+
</td>
|
| 47 |
+
<td>{{ student.email }}</td>
|
| 48 |
+
<td>{{ student.department }}</td>
|
| 49 |
+
<td>{{ student.contact }}</td>
|
| 50 |
+
<td>
|
| 51 |
+
{% set issues = student_issues.get(student.id, []) %}
|
| 52 |
+
{% if issues %}
|
| 53 |
+
<span style="color: #2563eb; font-weight: 600;">{{ issues|length }} active book(s)</span>
|
| 54 |
+
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: #6b7280;">
|
| 55 |
+
{% for issue in issues %}
|
| 56 |
+
<div style="padding: 0.25rem 0;">
|
| 57 |
+
π {{ issue.book_id }}
|
| 58 |
+
{% if issue.is_overdue() %}
|
| 59 |
+
<span style="color: red; font-weight: bold;">β Overdue</span>
|
| 60 |
+
{% endif %}
|
| 61 |
+
</div>
|
| 62 |
+
{% endfor %}
|
| 63 |
+
</div>
|
| 64 |
+
{% else %}
|
| 65 |
+
<span style="color: #9ca3af;">No active books</span>
|
| 66 |
+
{% endif %}
|
| 67 |
+
</td>
|
| 68 |
+
<td>
|
| 69 |
+
<a href="{{ url_for('edit_student', student_id=student.id) }}" class="btn">Edit</a>
|
| 70 |
+
<form method="POST" action="{{ url_for('delete_student', student_id=student.id) }}" style="display: inline;">
|
| 71 |
+
<button type="submit" class="btn btn-danger" onclick="return confirm('Delete?')">Delete</button>
|
| 72 |
+
</form>
|
| 73 |
+
</td>
|
| 74 |
+
</tr>
|
| 75 |
+
{% endfor %}
|
| 76 |
+
</table>
|
| 77 |
+
</div>
|
| 78 |
+
{% endblock %}
|
templates/view_student.html
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<div style="margin-bottom: 2rem;">
|
| 4 |
+
<a href="{{ url_for('students') }}" style="color: #2563eb; text-decoration: none;">← Back to Students</a>
|
| 5 |
+
</div>
|
| 6 |
+
|
| 7 |
+
<div class="card" style="margin-bottom: 2rem;">
|
| 8 |
+
<h2 style="margin-bottom: 1.5rem;">π¨βπ Student Details</h2>
|
| 9 |
+
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;">
|
| 10 |
+
<div>
|
| 11 |
+
<strong>Student ID:</strong> {{ student.id }}
|
| 12 |
+
</div>
|
| 13 |
+
<div>
|
| 14 |
+
<strong>Name:</strong> {{ student.name }}
|
| 15 |
+
</div>
|
| 16 |
+
<div>
|
| 17 |
+
<strong>Email:</strong> {{ student.email }}
|
| 18 |
+
</div>
|
| 19 |
+
<div>
|
| 20 |
+
<strong>Department:</strong> {{ student.department or 'N/A' }}
|
| 21 |
+
</div>
|
| 22 |
+
<div>
|
| 23 |
+
<strong>Contact:</strong> {{ student.contact or 'N/A' }}
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem;">
|
| 29 |
+
<a href="#all-section" style="text-decoration: none;">
|
| 30 |
+
<div class="card" style="text-align: center; padding: 1.5rem; cursor: pointer; transition: transform 0.2s;"
|
| 31 |
+
onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
| 32 |
+
<div style="font-size: 2rem; font-weight: bold; color: #2563eb;">{{ issues_with_books|length }}</div>
|
| 33 |
+
<div style="color: #6b7280;">Total Issues</div>
|
| 34 |
+
<small style="color: #9ca3af; font-size: 0.8rem;">Click to view all</small>
|
| 35 |
+
</div>
|
| 36 |
+
</a>
|
| 37 |
+
<a href="#active-section" style="text-decoration: none;">
|
| 38 |
+
<div class="card" style="text-align: center; padding: 1.5rem; cursor: pointer; transition: transform 0.2s;"
|
| 39 |
+
onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
| 40 |
+
<div style="font-size: 2rem; font-weight: bold; color: #f59e0b;">{{ active_count }}</div>
|
| 41 |
+
<div style="color: #6b7280;">Active Books</div>
|
| 42 |
+
<small style="color: #9ca3af; font-size: 0.8rem;">Click to view</small>
|
| 43 |
+
</div>
|
| 44 |
+
</a>
|
| 45 |
+
<a href="#returned-section" style="text-decoration: none;">
|
| 46 |
+
<div class="card" style="text-align: center; padding: 1.5rem; cursor: pointer; transition: transform 0.2s;"
|
| 47 |
+
onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
| 48 |
+
<div style="font-size: 2rem; font-weight: bold; color: #10b981;">{{ returned_count }}</div>
|
| 49 |
+
<div style="color: #6b7280;">Returned</div>
|
| 50 |
+
<small style="color: #9ca3af; font-size: 0.8rem;">Click to view</small>
|
| 51 |
+
</div>
|
| 52 |
+
</a>
|
| 53 |
+
<div class="card" style="text-align: center; padding: 1.5rem;">
|
| 54 |
+
<div style="font-size: 2rem; font-weight: bold; color: #ef4444;">{{ overdue_count }}</div>
|
| 55 |
+
<div style="color: #6b7280;">Currently Overdue</div>
|
| 56 |
+
<small style="color: #ef4444; font-weight: 600; font-size: 0.9rem; margin-top: 0.5rem; display: block;">
|
| 57 |
+
Total overdue times: {{ total_overdue_times }}
|
| 58 |
+
</small>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div class="card" id="all-section" style="margin-bottom: 2rem;">
|
| 63 |
+
<h3 style="margin-bottom: 1rem;">π All Issue History</h3>
|
| 64 |
+
{% if issues_with_books %}
|
| 65 |
+
<table>
|
| 66 |
+
<tr>
|
| 67 |
+
<th>Book</th>
|
| 68 |
+
<th>Issue Date</th>
|
| 69 |
+
<th>Due Date</th>
|
| 70 |
+
<th>Return Date</th>
|
| 71 |
+
<th>Status</th>
|
| 72 |
+
</tr>
|
| 73 |
+
{% for item in issues_with_books %}
|
| 74 |
+
{% set issue = item.issue %}
|
| 75 |
+
{% set book = item.book %}
|
| 76 |
+
{% set is_overdue = issue.is_overdue() %}
|
| 77 |
+
{% set was_overdue = issue.status == 'returned' and issue.days_overdue() > 0 %}
|
| 78 |
+
<tr>
|
| 79 |
+
<td>
|
| 80 |
+
<strong>{{ book.title if book else 'Unknown' }}</strong><br>
|
| 81 |
+
<small style="color: #6b7280;">ID: {{ issue.book_id }}</small>
|
| 82 |
+
</td>
|
| 83 |
+
<td>{{ issue.issue_date.strftime('%Y-%m-%d %H:%M') }}</td>
|
| 84 |
+
<td>{{ issue.due_date.strftime('%Y-%m-%d') }}</td>
|
| 85 |
+
<td>
|
| 86 |
+
{% if issue.return_date %}
|
| 87 |
+
{{ issue.return_date.strftime('%Y-%m-%d %H:%M') }}
|
| 88 |
+
{% else %}
|
| 89 |
+
<span style="color: #9ca3af;">Not returned</span>
|
| 90 |
+
{% endif %}
|
| 91 |
+
</td>
|
| 92 |
+
<td>
|
| 93 |
+
{% if issue.status == 'returned' %}
|
| 94 |
+
{% if was_overdue %}
|
| 95 |
+
<span style="background: #fee2e2; color: #991b1b; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">
|
| 96 |
+
β Returned Late ({{ issue.days_overdue() }} days)
|
| 97 |
+
</span>
|
| 98 |
+
{% else %}
|
| 99 |
+
<span style="background: #d1fae5; color: #065f46; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">
|
| 100 |
+
β Returned On Time
|
| 101 |
+
</span>
|
| 102 |
+
{% endif %}
|
| 103 |
+
{% elif is_overdue %}
|
| 104 |
+
<span style="background: #fee2e2; color: #991b1b; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem; font-weight: bold;">
|
| 105 |
+
β OVERDUE ({{ issue.days_overdue() }} days)
|
| 106 |
+
</span>
|
| 107 |
+
{% else %}
|
| 108 |
+
<span style="background: #fef3c7; color: #92400e; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">
|
| 109 |
+
π Active
|
| 110 |
+
</span>
|
| 111 |
+
{% endif %}
|
| 112 |
+
</td>
|
| 113 |
+
</tr>
|
| 114 |
+
{% endfor %}
|
| 115 |
+
</table>
|
| 116 |
+
{% else %}
|
| 117 |
+
<p style="text-align: center; color: #9ca3af; padding: 2rem;">No issue history found</p>
|
| 118 |
+
{% endif %}
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div class="card" id="active-section" style="margin-bottom: 2rem;">
|
| 122 |
+
<h3 style="margin-bottom: 1rem;">π Active Books</h3>
|
| 123 |
+
{% set active_books = issues_with_books|selectattr('issue.status', 'equalto', 'issued')|list %}
|
| 124 |
+
{% if active_books %}
|
| 125 |
+
<table>
|
| 126 |
+
<tr>
|
| 127 |
+
<th>Book</th>
|
| 128 |
+
<th>Issue Date</th>
|
| 129 |
+
<th>Due Date</th>
|
| 130 |
+
<th>Status</th>
|
| 131 |
+
</tr>
|
| 132 |
+
{% for item in active_books %}
|
| 133 |
+
{% set issue = item.issue %}
|
| 134 |
+
{% set book = item.book %}
|
| 135 |
+
{% set is_overdue = issue.is_overdue() %}
|
| 136 |
+
<tr>
|
| 137 |
+
<td>
|
| 138 |
+
<strong>{{ book.title if book else 'Unknown' }}</strong><br>
|
| 139 |
+
<small style="color: #6b7280;">ID: {{ issue.book_id }}</small>
|
| 140 |
+
</td>
|
| 141 |
+
<td>{{ issue.issue_date.strftime('%Y-%m-%d %H:%M') }}</td>
|
| 142 |
+
<td>{{ issue.due_date.strftime('%Y-%m-%d') }}</td>
|
| 143 |
+
<td>
|
| 144 |
+
{% if is_overdue %}
|
| 145 |
+
<span style="background: #fee2e2; color: #991b1b; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem; font-weight: bold;">
|
| 146 |
+
β OVERDUE ({{ issue.days_overdue() }} days)
|
| 147 |
+
</span>
|
| 148 |
+
{% else %}
|
| 149 |
+
<span style="background: #fef3c7; color: #92400e; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">
|
| 150 |
+
π Active
|
| 151 |
+
</span>
|
| 152 |
+
{% endif %}
|
| 153 |
+
</td>
|
| 154 |
+
</tr>
|
| 155 |
+
{% endfor %}
|
| 156 |
+
</table>
|
| 157 |
+
{% else %}
|
| 158 |
+
<p style="text-align: center; color: #9ca3af; padding: 2rem;">No active books</p>
|
| 159 |
+
{% endif %}
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<div class="card" id="returned-section">
|
| 163 |
+
<h3 style="margin-bottom: 1rem;">β Returned Books</h3>
|
| 164 |
+
{% set returned_books = issues_with_books|selectattr('issue.status', 'equalto', 'returned')|list %}
|
| 165 |
+
{% if returned_books %}
|
| 166 |
+
<table>
|
| 167 |
+
<tr>
|
| 168 |
+
<th>Book</th>
|
| 169 |
+
<th>Issue Date</th>
|
| 170 |
+
<th>Due Date</th>
|
| 171 |
+
<th>Return Date</th>
|
| 172 |
+
<th>Status</th>
|
| 173 |
+
</tr>
|
| 174 |
+
{% for item in returned_books %}
|
| 175 |
+
{% set issue = item.issue %}
|
| 176 |
+
{% set book = item.book %}
|
| 177 |
+
{% set was_overdue = issue.days_overdue() > 0 %}
|
| 178 |
+
<tr>
|
| 179 |
+
<td>
|
| 180 |
+
<strong>{{ book.title if book else 'Unknown' }}</strong><br>
|
| 181 |
+
<small style="color: #6b7280;">ID: {{ issue.book_id }}</small>
|
| 182 |
+
</td>
|
| 183 |
+
<td>{{ issue.issue_date.strftime('%Y-%m-%d %H:%M') }}</td>
|
| 184 |
+
<td>{{ issue.due_date.strftime('%Y-%m-%d') }}</td>
|
| 185 |
+
<td>{{ issue.return_date.strftime('%Y-%m-%d %H:%M') }}</td>
|
| 186 |
+
<td>
|
| 187 |
+
{% if was_overdue %}
|
| 188 |
+
<span style="background: #fee2e2; color: #991b1b; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">
|
| 189 |
+
β Returned Late ({{ issue.days_overdue() }} days)
|
| 190 |
+
</span>
|
| 191 |
+
{% else %}
|
| 192 |
+
<span style="background: #d1fae5; color: #065f46; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem;">
|
| 193 |
+
β Returned On Time
|
| 194 |
+
</span>
|
| 195 |
+
{% endif %}
|
| 196 |
+
</td>
|
| 197 |
+
</tr>
|
| 198 |
+
{% endfor %}
|
| 199 |
+
</table>
|
| 200 |
+
{% else %}
|
| 201 |
+
<p style="text-align: center; color: #9ca3af; padding: 2rem;">No returned books</p>
|
| 202 |
+
{% endif %}
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
{% endblock %}
|