feat(dashboard): list recent cases with search and pagination\n\n- Add q, page, page_size to /dashboard route\n- Join clients and filter by file_no/name/company\n- Bootstrap table UI with search form and pagination\n- Log query params; preserve auth/session\n\nCo-authored-by: AI Assistant <ai@example.com>

This commit is contained in:
HotSwapp
2025-10-06 19:11:40 -05:00
parent 6aa4d59a25
commit 6174df42b4
2 changed files with 194 additions and 10 deletions

View File

@@ -9,18 +9,19 @@ import os
import logging import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, Request from fastapi import FastAPI, Depends, Request, Query
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import or_
from dotenv import load_dotenv from dotenv import load_dotenv
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from .database import create_tables, get_db, get_database_url from .database import create_tables, get_db, get_database_url
from .models import User from .models import User, Case, Client
from .auth import authenticate_user, get_current_user_from_session from .auth import authenticate_user, get_current_user_from_session
# Load environment variables # Load environment variables
@@ -218,21 +219,83 @@ async def logout(request: Request):
@app.get("/dashboard") @app.get("/dashboard")
async def dashboard(request: Request, db: Session = Depends(get_db)): async def dashboard(
request: Request,
q: str | None = Query(None, description="Search by file number or client name"),
page: int = Query(1, ge=1, description="Page number (1-indexed)"),
page_size: int = Query(20, ge=1, le=100, description="Results per page"),
db: Session = Depends(get_db),
):
""" """
Dashboard page - requires authentication. Dashboard page - lists recent cases with search and pagination.
Shows an overview of the system and provides navigation to main features. - Optional query param `q` filters by case file number or client name/company
- `page` and `page_size` control pagination
""" """
# Check authentication # Check authentication
user = get_current_user_from_session(request.session) user = get_current_user_from_session(request.session)
if not user: if not user:
return RedirectResponse(url="/login", status_code=302) return RedirectResponse(url="/login", status_code=302)
return templates.TemplateResponse("dashboard.html", { # Base query: join clients for name/company access
query = db.query(Case).join(Client).order_by(
Case.open_date.desc(),
Case.created_at.desc(),
)
# Apply search filter if provided
if q:
like_term = f"%{q}%"
query = query.filter(
or_(
Case.file_no.ilike(like_term),
Client.first_name.ilike(like_term),
Client.last_name.ilike(like_term),
Client.company.ilike(like_term),
)
)
# Total count for pagination
total: int = query.count()
# Clamp page to valid range when total is known
total_pages: int = (total + page_size - 1) // page_size if total > 0 else 1
if page > total_pages:
page = total_pages
# Pagination window
offset = (page - 1) * page_size
cases = query.offset(offset).limit(page_size).all()
# Page number window for UI (current +/- 2)
start_page = max(1, page - 2)
end_page = min(total_pages, page + 2)
page_numbers = list(range(start_page, end_page + 1))
logger.info(
"Rendering dashboard: q='%s', page=%s, page_size=%s, total=%s",
q,
page,
page_size,
total,
)
return templates.TemplateResponse(
"dashboard.html",
{
"request": request, "request": request,
"user": user "user": user,
}) "cases": cases,
"q": q,
"page": page,
"page_size": page_size,
"total": total,
"total_pages": total_pages,
"page_numbers": page_numbers,
"start_index": (offset + 1) if total > 0 else 0,
"end_index": min(offset + len(cases), total),
},
)
@app.get("/admin") @app.get("/admin")

View File

@@ -1 +1,122 @@
<!-- Dashboard with case listing and search --> {% extends "base.html" %}
{% block title %}Dashboard · Delphi Database{% endblock %}
{% block content %}
<div class="row g-3 align-items-center mb-3">
<div class="col-auto">
<h2 class="mb-0">Cases</h2>
</div>
<div class="col ms-auto">
<form class="d-flex" method="get" action="/dashboard">
<input
class="form-control me-2"
type="search"
name="q"
placeholder="Search file # or name/company"
aria-label="Search"
value="{{ q or '' }}"
>
<input type="hidden" name="page_size" value="{{ page_size }}">
<button class="btn btn-outline-primary" type="submit">
<i class="bi bi-search me-1"></i>Search
</button>
</form>
</div>
<div class="col-auto">
<a class="btn btn-outline-secondary" href="/dashboard">
<i class="bi bi-x-circle me-1"></i>Clear
</a>
</div>
<div class="col-12 text-muted small">
{% if total and total > 0 %}
Showing {{ start_index }}{{ end_index }} of {{ total }}
{% else %}
No results
{% endif %}
</div>
<div class="col-12">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th scope="col" style="width: 140px;">File #</th>
<th scope="col">Client</th>
<th scope="col">Company</th>
<th scope="col">Type</th>
<th scope="col">Status</th>
<th scope="col">Opened</th>
<th scope="col" class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% if cases and cases|length > 0 %}
{% for c in cases %}
<tr>
<td><span class="fw-semibold">{{ c.file_no }}</span></td>
<td>
{% set client = c.client %}
{% if client %}
{{ client.last_name }}, {{ client.first_name }}
{% else %}
<span class="text-muted">Unknown</span>
{% endif %}
</td>
<td>{{ client.company if client else '' }}</td>
<td>{{ c.case_type or '' }}</td>
<td>
{% if c.status == 'active' %}
<span class="badge bg-success">Active</span>
{% elif c.status == 'closed' %}
<span class="badge bg-secondary">Closed</span>
{% else %}
<span class="badge bg-light text-dark">{{ c.status or 'n/a' }}</span>
{% endif %}
</td>
<td>{{ c.open_date.strftime('%Y-%m-%d') if c.open_date else '' }}</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-primary" href="/case/{{ c.id }}">
<i class="bi bi-folder2-open me-1"></i>View
</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="7" class="text-center text-muted py-4">No cases found.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<div class="col-12">
{% if total_pages and total_pages > 1 %}
<nav aria-label="Cases pagination">
<ul class="pagination mb-0">
{# Previous #}
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
<a class="page-link" href="/dashboard?page={{ page - 1 if page > 1 else 1 }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{# Page numbers window #}
{% for p in page_numbers %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="/dashboard?page={{ p }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}">{{ p }}</a>
</li>
{% endfor %}
{# Next #}
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
<a class="page-link" href="/dashboard?page={{ page + 1 if page < total_pages else total_pages }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
{% endif %}
</div>
</div>
{% endblock %}