feat(case): add GET /case/{id} detail view and Jinja template; link from dashboard table; eager-load related data; 404 handling and logging

This commit is contained in:
HotSwapp
2025-10-06 19:21:58 -05:00
parent 6174df42b4
commit 2e49340663
2 changed files with 239 additions and 2 deletions

View File

@@ -15,7 +15,7 @@ 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, joinedload
from sqlalchemy import or_ 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
@@ -314,3 +314,58 @@ async def admin_panel(request: Request, db: Session = Depends(get_db)):
"request": request, "request": request,
"user": user "user": user
}) })
@app.get("/case/{case_id}")
async def case_detail(
request: Request,
case_id: int,
db: Session = Depends(get_db),
):
"""
Case detail view.
Displays detailed information for a single case and its related client and
associated records (transactions, documents, payments).
"""
# Check authentication
user = get_current_user_from_session(request.session)
if not user:
return RedirectResponse(url="/login", status_code=302)
# Fetch case with related entities eagerly loaded to avoid lazy-load issues
case_obj = (
db.query(Case)
.options(
joinedload(Case.client),
joinedload(Case.transactions),
joinedload(Case.documents),
joinedload(Case.payments),
)
.filter(Case.id == case_id)
.first()
)
if not case_obj:
logger.warning("Case not found: id=%s", case_id)
return templates.TemplateResponse(
"case.html",
{
"request": request,
"user": user,
"case": None,
"error": "Case not found",
},
status_code=404,
)
logger.info("Rendering case detail: id=%s, file_no='%s'", case_obj.id, case_obj.file_no)
return templates.TemplateResponse(
"case.html",
{
"request": request,
"user": user,
"case": case_obj,
},
)

View File

@@ -1 +1,183 @@
<!-- Case view/edit form --> {% extends "base.html" %}
{% block title %}
Case {{ case.file_no if case else '' }} · Delphi Database
{% endblock %}
{% block content %}
<div class="row g-3">
<div class="col-12 d-flex align-items-center">
<a class="btn btn-sm btn-outline-secondary me-2" href="/dashboard">
<i class="bi bi-arrow-left"></i>
Back
</a>
<h2 class="mb-0">Case Details</h2>
</div>
{% if error %}
<div class="col-12">
<div class="alert alert-danger" role="alert">{{ error }}</div>
</div>
{% endif %}
{% if case %}
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row mb-3">
<div class="col-md-3">
<div class="text-muted small">File #</div>
<div class="fw-semibold">{{ case.file_no }}</div>
</div>
<div class="col-md-3">
<div class="text-muted small">Status</div>
<div>
{% if case.status == 'active' %}
<span class="badge bg-success">Active</span>
{% elif case.status == 'closed' %}
<span class="badge bg-secondary">Closed</span>
{% else %}
<span class="badge bg-light text-dark">{{ case.status or 'n/a' }}</span>
{% endif %}
</div>
</div>
<div class="col-md-3">
<div class="text-muted small">Type</div>
<div>{{ case.case_type or '' }}</div>
</div>
<div class="col-md-3">
<div class="text-muted small">Opened</div>
<div>{{ case.open_date.strftime('%Y-%m-%d') if case.open_date else '' }}</div>
</div>
</div>
<div class="row mb-3">
{% set client = case.client %}
<div class="col-md-4">
<div class="text-muted small">Client</div>
<div>
{% if client %}
{{ client.last_name }}, {{ client.first_name }}
{% else %}
<span class="text-muted">Unknown</span>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="text-muted small">Company</div>
<div>{{ client.company if client else '' }}</div>
</div>
<div class="col-md-4">
<div class="text-muted small">City/State</div>
<div>
{% if client %}
{{ client.city or '' }}{% if client.state %}, {{ client.state }}{% endif %}
{% endif %}
</div>
</div>
</div>
<div class="mb-2 text-muted small">Description</div>
<p class="mb-0">{{ case.description or '' }}</p>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card h-100">
<div class="card-header">Transactions</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm mb-0 align-middle">
<thead class="table-light">
<tr>
<th style="width: 110px;">Date</th>
<th>Type</th>
<th class="text-end" style="width: 120px;">Amount</th>
</tr>
</thead>
<tbody>
{% if case.transactions and case.transactions|length > 0 %}
{% for t in case.transactions %}
<tr>
<td>{{ t.transaction_date.strftime('%Y-%m-%d') if t.transaction_date else '' }}</td>
<td>{{ t.transaction_type or '' }}</td>
<td class="text-end">{{ '%.2f'|format(t.amount) if t.amount is not none else '' }}</td>
</tr>
{% endfor %}
{% else %}
<tr><td colspan="3" class="text-center text-muted py-3">No transactions.</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card h-100">
<div class="card-header">Documents</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Type</th>
<th>File</th>
<th style="width: 120px;">Uploaded</th>
</tr>
</thead>
<tbody>
{% if case.documents and case.documents|length > 0 %}
{% for d in case.documents %}
<tr>
<td>{{ d.document_type or '' }}</td>
<td>{{ d.file_name or '' }}</td>
<td>{{ d.uploaded_date.strftime('%Y-%m-%d') if d.uploaded_date else '' }}</td>
</tr>
{% endfor %}
{% else %}
<tr><td colspan="3" class="text-center text-muted py-3">No documents.</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card h-100">
<div class="card-header">Payments</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm mb-0 align-middle">
<thead class="table-light">
<tr>
<th style="width: 110px;">Date</th>
<th>Type</th>
<th class="text-end" style="width: 120px;">Amount</th>
</tr>
</thead>
<tbody>
{% if case.payments and case.payments|length > 0 %}
{% for p in case.payments %}
<tr>
<td>{{ p.payment_date.strftime('%Y-%m-%d') if p.payment_date else '' }}</td>
<td>{{ p.payment_type or '' }}</td>
<td class="text-end">{{ '%.2f'|format(p.amount) if p.amount is not none else '' }}</td>
</tr>
{% endfor %}
{% else %}
<tr><td colspan="3" class="text-center text-muted py-3">No payments.</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}