diff --git a/app.py b/app.py index 7dfba4d..0f32968 100644 --- a/app.py +++ b/app.py @@ -287,52 +287,165 @@ def save_notes(): @app.route('/user_loans') def user_loans(): """ - Show all loans grouped by user. - This demonstrates the one-to-many relationship: one user can loan multiple tablets. + 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) + result = cursor.fetchone() + total_users = result[0] if result else 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 all users - cursor.execute("SELECT * FROM users") - users = cursor.fetchall() + # Get user info + cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() - # Get all loans with tablet and user details + 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.user_id, l.loan_date, l.return_date, l.status, - t.brand, t.model, t.serial_number, - u.name, u.identification + 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 - JOIN users u ON l.user_id = u.id - ORDER BY u.name, l.loan_date DESC - ''') + WHERE l.user_id = ? + ORDER BY l.loan_date DESC + ''', (user_id,)) loans = cursor.fetchall() - # Group loans by user - user_loans_map = {} - for loan in loans: - user_id = loan['user_id'] - if user_id not in user_loans_map: - user_loans_map[user_id] = { - 'user': None, - 'loans': [] - } - user_loans_map[user_id]['loans'].append(loan) + # Get active loans count + cursor.execute(''' + SELECT COUNT(*) FROM loans + WHERE user_id = ? AND status = 'active' + ''', (user_id,)) + active_count = cursor.fetchone()[0] - # Attach user info - for user in users: - if user['id'] in user_loans_map: - user_loans_map[user['id']]['user'] = user - - # Filter out users with no loans (optional - keep all users) - # Convert to list for template - user_loans_list = [] - for user in users: - user_data = user_loans_map.get(user['id'], {'user': user, 'loans': []}) - user_loans_list.append(user_data) + # 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.html', users_with_loans=user_loans_list) + return render_template('user_loans_detail.html', + user=user, + loans=loans, + active_count=active_count, + returned_count=returned_count) + @app.route('/non_loanable_devices') diff --git a/templates/base.html b/templates/base.html index 3382ed4..a35f0b3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,328 +4,137 @@ Tablet Management System + + + + + + + + + + + - -
-

Tablet Management System

+ +
+ +
+

+ Tablet Management System +

+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + + +
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} - - - - {% block content %}{% endblock %} + +
+ {% block content %}{% endblock %} +
diff --git a/templates/components/user_loans_results.html b/templates/components/user_loans_results.html new file mode 100644 index 0000000..ffb87a3 --- /dev/null +++ b/templates/components/user_loans_results.html @@ -0,0 +1,206 @@ +{# templates/components/user_loans_results.html #} + +{%- if users %} + {# Results Table Card #} +
+ + + + + + + + + + + + {%- for user in users %} + + + + + + + + {%- endfor %} + +
+ User + + Identification + + Active Loans + + Last Loan Date + + Actions +
+
{{ user.name }}
+
+ {{ user.identification or 'N/A' }} + + + {{ user.loan_count or 0 }} + + + {{ user.last_loan_date or 'Never' }} + + + View Details + +
+
+ + {# Pagination #} + {%- if total_pages > 1 %} + + {%- endif %} + +{%- else %} + {# No Results #} +
+ + + +

No users found

+

+ {% if search %} + No users match your search for "{{ search }}" + {% elif status == 'with_loans' %} + No users have active loans + {% elif status == 'no_loans' %} + All users have at least one active loan + {% else %} + There are no users in the system yet + {% endif %} +

+ + Clear Filters + +
+{%- endif %} diff --git a/templates/index.html b/templates/index.html index f946ad8..fd904ef 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,67 +1,167 @@ {% extends "base.html" %} {% block content %} -
-

Available Tablets

+
+ {# Available Tablets Section #} +
+
+

+ + + + Available Tablets + + {{ available_tablets|length }} + +

+
+ {% if available_tablets %} -
- - +
+
+ - - - - + + + + + - + {% for tablet in available_tablets %} - - - - - - + + + + + + + {% endfor %}
BrandModelSerial NumberNotes + Brand + + Model + + Serial Number + + Notes + + Actions +
{{ tablet.brand }}{{ tablet.model }}{{ tablet.serial_number }}{{ tablet.notes or '-' }}
+
{{ tablet.brand }}
+
+ {{ tablet.model }} + + {{ tablet.serial_number }} + + {{ tablet.notes or '-' }} + + + + + + Loan + +
{% else %} -

No available tablets.

+
+ + + +

No available tablets

+ + Add Tablet + +
{% endif %}
- -
-

Active Loans

+ + {# Active Loans Section #} +
+
+

+ + + + Active Loans + + {{ active_loans|length }} + +

+
+ {% if active_loans %} -
- - +
+
+ - - - - - + + + + + - + {% for loan in active_loans %} - - - - - - - + + + + + + + {% endfor %}
TabletSerial NumberBorrowerLoan DateActions + Tablet + + Serial Number + + Borrower + + Loan Date + + Actions +
{{ loan.brand }} {{ loan.model }}{{ loan.serial_number }}{{ loan.name }}{{ loan.loan_date }} - Return -
+
{{ loan.brand }} {{ loan.model }}
+
+ {{ loan.serial_number }} + +
{{ loan.name }}
+
{{ loan.identification }}
+
+ {{ loan.loan_date[:10] if loan.loan_date else 'N/A' }} + {{ loan.loan_date[11:16] if loan.loan_date else '' }} + + + + + + Return + +
{% else %} -

No active loans.

+
+ + + +

No active loans

+
{% endif %}
-{% endblock %} \ No newline at end of file +
+{% endblock %} diff --git a/templates/user_loans.html b/templates/user_loans.html index 6fddf9c..325eb39 100644 --- a/templates/user_loans.html +++ b/templates/user_loans.html @@ -1,279 +1,124 @@ {% extends "base.html" %} {% block content %} -

User Loans

-

- Relationship: One user can loan multiple tablets (one-to-many). - This page demonstrates the many-to-many relationship between users and tablets via the loans table. -

- - -
- - +
+ +
+

User Loans

+
+ {{ total_users }} users +
-
- {% for user_data in users_with_loans %} - {% set user = user_data.user %} - {% set loans = user_data.loans %} - {% set active_loans = loans|selectattr('status', 'equalto', 'active')|list %} - {% set returned_loans = loans|selectattr('status', 'equalto', 'returned')|list %} + +
+
-
-
-

{{ user.name }} ({{ user.identification }})

- - {{ active_loans|length }} active, {{ returned_loans|length }} past - + +
+ +
+ +
+ + + + +
- {% if active_loans %} -
-

Current Loans

-
- - - - - - - - - - - {% for loan in active_loans %} - - - - - - - {% endfor %} - -
TabletSerial NumberLoan DateStatus
{{ loan.brand }} {{ loan.model }}{{ loan.serial_number }}{{ loan.loan_date }} - - {{ loan.status }} - -
-
-
- {% endif %} - - {% if returned_loans %} -
-

- -

- -
- {% endif %} - - {% if not active_loans and not returned_loans %} -

No loans recorded for this user.

- {% endif %} + +
+ + +
- {% endfor %} - - {% if users_with_loans|length == 0 %} -

No users found. Add users and loan tablets to see data here.

- {% endif %} + + + +
+ + + + + +
+ {% include 'components/user_loans_results.html' %} +
+
- + } +}); - + } +}); + {% endblock %} diff --git a/templates/user_loans_detail.html b/templates/user_loans_detail.html new file mode 100644 index 0000000..003b011 --- /dev/null +++ b/templates/user_loans_detail.html @@ -0,0 +1,126 @@ +{% extends "base.html" %} + +{% block content %} +
+ + + + +
+
+
+
+ {{ user.name[:1].upper() }} +
+
+
+

{{ user.name }}

+

+ ID: {{ user.id }} | Identification: {{ user.identification or 'N/A' }} +

+
+
+ + {{ active_count }} Active Loans +
+
+ + {{ returned_count }} Returned +
+
+
+
+ {% if user.email %} + + Email + + {% endif %} + {% if user.phone %} + + Call + + {% endif %} +
+
+
+ + +
+
+

Loan History

+
+ + {% if loans %} +
+ + + + + + + + + + + {% for loan in loans %} + + + + + + + {% endfor %} + +
+ Tablet + + Loan Date + + Return Date + + Status +
+
{{ loan.brand }} {{ loan.model }}
+
{{ loan.serial_number }}
+
+ {{ loan.loan_date[:10] if loan.loan_date else 'N/A' }} + {{ loan.loan_date[11:16] if loan.loan_date else '' }} + + {% if loan.return_date %} + {{ loan.return_date[:10] }} {{ loan.return_date[11:16] }} + {% else %} + Not returned + {% endif %} + + + {{ loan.status|title }} + +
+
+ {% else %} +
+ + + +

No loan history for this user

+
+ {% endif %} +
+
+{% endblock %}