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:
parent
e70738c792
commit
c9f3382c7c
6 changed files with 837 additions and 649 deletions
170
app.py
170
app.py
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue