diff --git a/app.py b/app.py index 0f32968..7dfba4d 100644 --- a/app.py +++ b/app.py @@ -287,165 +287,52 @@ def save_notes(): @app.route('/user_loans') def user_loans(): """ - Show all users with their loan information. - Supports search, filtering, and pagination via HTMX. + Show all loans grouped by user. + This demonstrates the one-to-many relationship: one user can loan multiple tablets. """ - 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) + # Get all users + cursor.execute("SELECT * FROM users") 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 + # Get all loans with tablet and user details 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 + 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 FROM loans l JOIN tablets t ON l.tablet_id = t.id - WHERE l.user_id = ? - ORDER BY l.loan_date DESC - ''', (user_id,)) + JOIN users u ON l.user_id = u.id + ORDER BY u.name, l.loan_date DESC + ''') 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] + # 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 returned loans count - cursor.execute(''' - SELECT COUNT(*) FROM loans - WHERE user_id = ? AND status = 'returned' - ''', (user_id,)) - returned_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) - return render_template('user_loans_detail.html', - user=user, - loans=loans, - active_count=active_count, - returned_count=returned_count) - + return render_template('user_loans.html', users_with_loans=user_loans_list) @app.route('/non_loanable_devices') diff --git a/templates/base.html b/templates/base.html index a35f0b3..3382ed4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,137 +4,328 @@ Tablet Management System - - - - - - - - - - - + + /* Medium devices (tablets, 768px and up) */ + @media (min-width: 768px) { + body { + padding: 1rem; + } + .container { + max-width: 720px; + padding: 1.5rem; + } + table { + min-width: auto; + } + .table-container { + overflow-x: visible; + } + } + + /* Large devices (desktops, 992px and up) */ + @media (min-width: 992px) { + .container { + max-width: 960px; + } + .nav { + flex-wrap: nowrap; + } + } + + /* Extra large devices (large desktops, 1200px and up) */ + @media (min-width: 1200px) { + .container { + max-width: 1140px; + } + } + + /* Print styles */ + @media print { + .nav, + .btn, + .flash-message { + display: none !important; + } + body { + background: white; + padding: 0; + } + .container { + box-shadow: none; + border: none; + max-width: 100%; + padding: 0; + } + } + - -
- -
-

- Tablet Management System -

- - - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for category, message in messages %} -
- {{ message }} -
- {% endfor %} -
- {% endif %} - {% endwith %} - - - -
+ +
+

Tablet Management System

- -
- {% block content %}{% endblock %} -
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + + + + {% block content %}{% endblock %}
diff --git a/templates/components/user_loans_results.html b/templates/components/user_loans_results.html deleted file mode 100644 index ffb87a3..0000000 --- a/templates/components/user_loans_results.html +++ /dev/null @@ -1,206 +0,0 @@ -{# 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 fd904ef..f946ad8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,167 +1,67 @@ {% extends "base.html" %} {% block content %} -
- {# Available Tablets Section #} -
-
-

- - - - Available Tablets - - {{ available_tablets|length }} - -

-
- +
+

Available Tablets

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

No available tablets

- - Add Tablet - -
+

No available tablets.

{% endif %}
- - {# Active Loans Section #} -
-
-

- - - - Active Loans - - {{ active_loans|length }} - -

-
- + +
+

Active Loans

{% if active_loans %} -
- - +
+
+ - - - - - + + + + + - + {% for loan in active_loans %} - - - - - - - + + + + + + + {% endfor %}
- Tablet - - Serial Number - - Borrower - - Loan Date - - Actions - TabletSerial NumberBorrowerLoan DateActions
-
{{ 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 - -
{{ loan.brand }} {{ loan.model }}{{ loan.serial_number }}{{ loan.name }}{{ loan.loan_date }} + Return +
{% else %} -
- - - -

No active loans

-
+

No active loans.

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

User Loans

-
- {{ total_users }} users -
+

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. +

+ + +
+ +
- -
-
+
+ {% 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 %} +
+ + + + function togglePastLoans(button) { + const content = button.closest('.loans-section').querySelector('.past-loans-content'); + const isExpanded = content.style.display !== 'none'; + + content.style.display = isExpanded ? 'none' : 'block'; + const count = button.textContent.match(/\d+/)[0]; + button.textContent = isExpanded ? '▶ Past Loans (' + count + ')' : '▼ Past Loans (' + count + ') '; + } + + + {% endblock %} diff --git a/templates/user_loans_detail.html b/templates/user_loans_detail.html deleted file mode 100644 index 003b011..0000000 --- a/templates/user_loans_detail.html +++ /dev/null @@ -1,126 +0,0 @@ -{% 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 %}