#!/usr/bin/env python3 """ Simple Tablet Lending and Return Management Web Application with SQLite backend """ from flask import Flask, render_template, request, redirect, url_for, flash import sqlite3 import os from datetime import datetime app = Flask(__name__) app.secret_key = 'your_secret_key_here' # Database setup DATABASE = 'tablets.db' def get_db(): """Get database connection""" conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row return conn def init_db(): """Initialize database with required tables""" with get_db() as conn: cursor = conn.cursor() # Create tablets table cursor.execute(''' CREATE TABLE IF NOT EXISTS tablets ( id INTEGER PRIMARY KEY AUTOINCREMENT, brand TEXT NOT NULL, model TEXT NOT NULL, serial_number TEXT UNIQUE NOT NULL, status TEXT DEFAULT 'available', notes TEXT ) ''') # Create users table cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT, phone TEXT, identification TEXT UNIQUE ) ''') # Create loans table cursor.execute(''' CREATE TABLE IF NOT EXISTS loans ( id INTEGER PRIMARY KEY AUTOINCREMENT, tablet_id INTEGER NOT NULL, user_id INTEGER NOT NULL, loan_date TEXT NOT NULL, return_date TEXT, status TEXT DEFAULT 'active', FOREIGN KEY (tablet_id) REFERENCES tablets (id), FOREIGN KEY (user_id) REFERENCES users (id) ) ''') # Create non_loanable_devices table for devices that cannot be loaned cursor.execute(''' CREATE TABLE IF NOT EXISTS non_loanable_devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, brand TEXT NOT NULL, model TEXT NOT NULL, serial_number TEXT UNIQUE NOT NULL, device_type TEXT NOT NULL, location TEXT, status TEXT DEFAULT 'available', notes TEXT, purchase_date TEXT, purchase_cost REAL ) ''') conn.commit() @app.route('/') def index(): """Main page showing available tablets and active loans""" with get_db() as conn: cursor = conn.cursor() # Get available tablets cursor.execute("SELECT * FROM tablets WHERE status = 'available'") available_tablets = cursor.fetchall() # Get active loans cursor.execute(''' SELECT l.id, t.brand, t.model, t.serial_number, u.name, l.loan_date FROM loans l JOIN tablets t ON l.tablet_id = t.id JOIN users u ON l.user_id = u.id WHERE l.status = 'active' ''') active_loans = cursor.fetchall() return render_template('index.html', available_tablets=available_tablets, active_loans=active_loans) @app.route('/add_tablet', methods=['GET', 'POST']) def add_tablet(): """Add a new tablet to inventory""" if request.method == 'POST': brand = request.form['brand'] model = request.form['model'] serial_number = request.form['serial_number'] notes = request.form['notes'] try: with get_db() as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO tablets (brand, model, serial_number, status, notes) VALUES (?, ?, ?, 'available', ?) ''', (brand, model, serial_number, notes)) conn.commit() flash('Tablet added successfully!', 'success') except sqlite3.IntegrityError: flash('Error: Serial number already exists!', 'error') return redirect(url_for('index')) return render_template('add_tablet.html') @app.route('/add_user', methods=['GET', 'POST']) def add_user(): """Add a new user""" if request.method == 'POST': name = request.form['name'] email = request.form['email'] phone = request.form['phone'] identification = request.form['identification'] try: with get_db() as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO users (name, email, phone, identification) VALUES (?, ?, ?, ?) ''', (name, email, phone, identification)) conn.commit() flash('User added successfully!', 'success') except sqlite3.IntegrityError: flash('Error: Identification already exists!', 'error') return redirect(url_for('index')) return render_template('add_user.html') @app.route('/loan_tablet', methods=['GET', 'POST']) def loan_tablet(): """Loan a tablet to a user""" if request.method == 'POST': tablet_id = request.form['tablet_id'] user_id = request.form['user_id'] with get_db() as conn: cursor = conn.cursor() # Update tablet status cursor.execute("UPDATE tablets SET status = 'loaned' WHERE id = ?", (tablet_id,)) # Create loan record loan_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') cursor.execute(''' INSERT INTO loans (tablet_id, user_id, loan_date, status) VALUES (?, ?, ?, 'active') ''', (tablet_id, user_id, loan_date)) conn.commit() flash('Tablet loaned successfully!', 'success') return redirect(url_for('index')) # Get data for form with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM tablets WHERE status = 'available'") available_tablets = cursor.fetchall() cursor.execute("SELECT * FROM users") users = cursor.fetchall() return render_template('loan_tablet.html', available_tablets=available_tablets, users=users) @app.route('/return_tablet/') def return_tablet(loan_id): """Return a loaned tablet""" with get_db() as conn: cursor = conn.cursor() # Get loan information cursor.execute("SELECT tablet_id FROM loans WHERE id = ?", (loan_id,)) loan = cursor.fetchone() if loan: tablet_id = loan['tablet_id'] # Update loan status and return date return_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') cursor.execute(''' UPDATE loans SET status = 'returned', return_date = ? WHERE id = ? ''', (return_date, loan_id)) # Update tablet status cursor.execute("UPDATE tablets SET status = 'available' WHERE id = ?", (tablet_id,)) conn.commit() flash('Tablet returned successfully!', 'success') else: flash('Error: Loan not found!', 'error') return redirect(url_for('index')) @app.route('/history') def history(): """Show loan history""" with get_db() as conn: cursor = conn.cursor() cursor.execute(''' SELECT l.id, t.brand, t.model, t.serial_number, u.name, l.loan_date, l.return_date, l.status FROM loans l JOIN tablets t ON l.tablet_id = t.id JOIN users u ON l.user_id = u.id ORDER BY l.loan_date DESC ''') loans = cursor.fetchall() return render_template('history.html', loans=loans) @app.route('/project_management') def project_management(): """Project management page with markdown editor for development notes""" return render_template('project_management.html') @app.route('/get_notes/') def get_notes(filename): """Serve notes file content""" try: with open(filename, 'r', encoding='utf-8') as f: content = f.read() return content, 200, { 'Content-Type': 'text/markdown; charset=utf-8', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' } except FileNotFoundError: return '', 404 except Exception as e: return str(e), 500 @app.route('/save_notes', methods=['POST']) def save_notes(): """Save markdown notes to file""" data = request.get_json() content = data.get('content', '') filename = data.get('filename', 'notes_development/project_notes.md') try: # Ensure directory exists os.makedirs(os.path.dirname(filename), exist_ok=True) # Write content to file with open(filename, 'w', encoding='utf-8') as f: f.write(content) return {'status': 'success', 'message': 'Notes saved successfully'} except Exception as e: return {'status': 'error', 'message': str(e)}, 500 @app.route('/user_loans') def user_loans(): """ Show all users with their loan information. Supports search, filtering, and pagination via HTMX. """ from flask import request # Get query parameters page = request.args.get('page', 1, type=int) search = request.args.get('search', '').strip() status_filter = request.args.get('status', '') # Pagination settings per_page = 20 offset = (page - 1) * per_page with get_db() as conn: cursor = conn.cursor() # Build base query for users with loan counts query = """ SELECT u.id, u.name, u.identification, u.email, u.phone, COUNT(l.id) as loan_count, MAX(l.loan_date) as last_loan_date FROM users u LEFT JOIN loans l ON u.id = l.user_id AND l.status = 'active' """ conditions = [] params = [] having_conditions = [] # Add search filter if search: conditions.append("(u.name LIKE ? OR u.identification LIKE ? OR u.email LIKE ?)") search_param = f"%{search}%" params.extend([search_param, search_param, search_param]) # Add status filter (use HAVING for aggregate functions) if status_filter == 'with_loans': having_conditions.append("COUNT(l.id) > 0") elif status_filter == 'no_loans': having_conditions.append("COUNT(l.id) = 0") # Combine conditions for WHERE clause where_clause = "" if conditions: where_clause = " WHERE " + " AND ".join(conditions) # Combine HAVING conditions having_clause = "" if having_conditions: having_clause = " HAVING " + " AND ".join(having_conditions) # Group by and order group_by = " GROUP BY u.id, u.name, u.identification, u.email, u.phone" order_by = " ORDER BY u.name COLLATE NOCASE" # Get total count for pagination # We need to count distinct users matching the criteria count_query = f"SELECT COUNT(DISTINCT u.id) FROM users u LEFT JOIN loans l ON u.id = l.user_id AND l.status = 'active'{where_clause}{having_clause}" cursor.execute(count_query, params) total_users = cursor.fetchone()[0] total_pages = (total_users + per_page - 1) // per_page # Build final query with pagination final_query = query + where_clause + group_by + having_clause + order_by + f" LIMIT {per_page} OFFSET {offset}" # Execute main query cursor.execute(final_query, params) users = cursor.fetchall() # Convert to list of dicts for template users_list = [] for user in users: users_list.append({ 'id': user[0], 'name': user[1], 'identification': user[2], 'email': user[3], 'phone': user[4], 'loan_count': user[5], 'last_loan_date': user[6] }) # Check if this is an HTMX request is_htmx = request.headers.get('HX-Request') == 'true' if is_htmx: # Return just the results partial for HTMX swap return render_template('components/user_loans_results.html', users=users_list, page=page, total_pages=total_pages, total_users=total_users, search=search, status=status_filter, per_page=per_page) else: # Full page render return render_template('user_loans.html', users=users_list, page=page, total_pages=total_pages, total_users=total_users, search=search, status=status_filter, per_page=per_page) @app.route('/user_loans/') def user_loans_detail(user_id): """ Show detailed loan history for a specific user. """ with get_db() as conn: cursor = conn.cursor() # Get user info cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user: flash('User not found', 'error') return redirect(url_for('user_loans')) # Get all loans for this user cursor.execute(''' SELECT l.id, l.tablet_id, l.loan_date, l.return_date, l.status, t.brand, t.model, t.serial_number, t.status as tablet_status FROM loans l JOIN tablets t ON l.tablet_id = t.id WHERE l.user_id = ? ORDER BY l.loan_date DESC ''', (user_id,)) loans = cursor.fetchall() # Get active loans count cursor.execute(''' SELECT COUNT(*) FROM loans WHERE user_id = ? AND status = 'active' ''', (user_id,)) active_count = cursor.fetchone()[0] # Get returned loans count cursor.execute(''' SELECT COUNT(*) FROM loans WHERE user_id = ? AND status = 'returned' ''', (user_id,)) returned_count = cursor.fetchone()[0] return render_template('user_loans_detail.html', user=user, loans=loans, active_count=active_count, returned_count=returned_count) @app.route('/non_loanable_devices') def non_loanable_devices(): """Show all non-loanable devices""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM non_loanable_devices ORDER BY device_type, brand, model") devices = cursor.fetchall() return render_template('non_loanable_devices.html', devices=devices) @app.route('/add_non_loanable_device', methods=['GET', 'POST']) def add_non_loanable_device(): """Add a new non-loanable device to inventory""" if request.method == 'POST': brand = request.form['brand'] model = request.form['model'] serial_number = request.form['serial_number'] device_type = request.form['device_type'] location = request.form['location'] notes = request.form['notes'] purchase_date = request.form.get('purchase_date', '') purchase_cost = request.form.get('purchase_cost', '') try: with get_db() as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO non_loanable_devices (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost) VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?) ''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost)) conn.commit() flash('Non-loanable device added successfully!', 'success') except sqlite3.IntegrityError: flash('Error: Serial number already exists!', 'error') return redirect(url_for('non_loanable_devices')) return render_template('add_non_loanable_device.html') @app.route('/edit_non_loanable_device/', methods=['GET', 'POST']) def edit_non_loanable_device(device_id): """Edit a non-loanable device""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM non_loanable_devices WHERE id = ?", (device_id,)) device = cursor.fetchone() if not device: flash('Error: Device not found!', 'error') return redirect(url_for('non_loanable_devices')) if request.method == 'POST': brand = request.form['brand'] model = request.form['model'] serial_number = request.form['serial_number'] device_type = request.form['device_type'] location = request.form['location'] status = request.form['status'] notes = request.form['notes'] purchase_date = request.form.get('purchase_date', '') purchase_cost = request.form.get('purchase_cost', '') try: cursor.execute(''' UPDATE non_loanable_devices SET brand = ?, model = ?, serial_number = ?, device_type = ?, location = ?, status = ?, notes = ?, purchase_date = ?, purchase_cost = ? WHERE id = ? ''', (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost, device_id)) conn.commit() flash('Device updated successfully!', 'success') return redirect(url_for('non_loanable_devices')) except sqlite3.IntegrityError: flash('Error: Serial number already exists!', 'error') return render_template('edit_non_loanable_device.html', device=device) @app.route('/delete_non_loanable_device/') def delete_non_loanable_device(device_id): """Delete a non-loanable device""" with get_db() as conn: cursor = conn.cursor() cursor.execute("DELETE FROM non_loanable_devices WHERE id = ?", (device_id,)) conn.commit() flash('Device deleted successfully!', 'success') return redirect(url_for('non_loanable_devices')) if __name__ == '__main__': # Initialize database init_db() # Create templates directory if it doesn't exist if not os.path.exists('templates'): os.makedirs('templates') app.run(debug=True, host='0.0.0.0', port=5000)