feat(ui): implement HTMX + Tailwind frontend revamp

- Add Tailwind CSS via CDN for modern styling
- Add HTMX for AJAX functionality without full page reloads
- Revamp user_loans interface with:
  - Real-time search (debounced 500ms)
  - Status filtering (all users, with loans, no loans)
  - Pagination (20 users per page)
  - Responsive table design
  - Clean, modern UI with Tailwind classes
- Create user_loans_detail.html for individual user loan history
- Update index.html with Tailwind styling
- Update base.html with Tailwind + HTMX setup
- Add components/user_loans_results.html for HTMX partial updates
- Update app.py user_loans route to support search, filter, pagination
- Add user_loans_detail route for detailed user view

This addresses the issues:
- Navigation overflow (fixed with responsive Tailwind classes)
- Basic look (modern, professional UI)
- User loans UX (fast search, filtering, pagination for 500+ users)
This commit is contained in:
ijuanes 2026-06-20 08:38:42 +01:00
parent e70738c792
commit c9f3382c7c
6 changed files with 837 additions and 649 deletions

170
app.py
View file

@ -287,52 +287,154 @@ 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 = []
# 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
if status_filter == 'with_loans':
conditions.append("COUNT(l.id) > 0")
elif status_filter == 'no_loans':
conditions.append("COUNT(l.id) = 0")
# Combine conditions
if conditions:
query += " WHERE " + " AND ".join(conditions)
# Group by and order
query += " GROUP BY u.id, u.name, u.identification, u.email, u.phone"
query += " ORDER BY u.name COLLATE NOCASE"
# Get total count for pagination
count_query = f"SELECT COUNT(*) FROM ({query})"
cursor.execute(count_query, params)
total_users = cursor.fetchone()[0]
total_pages = (total_users + per_page - 1) // per_page
# Add pagination to query
query += f" LIMIT {per_page} OFFSET {offset}"
# Execute main query
cursor.execute(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/<int:user_id>')
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')