feat(case): enable editing and close/reopen actions on case detail

- Add POST /case/{id}/update route for editing case fields (status, case_type, description, open_date, close_date)
- Add POST /case/{id}/close route to set status='closed' and close_date=current date
- Add POST /case/{id}/reopen route to set status='active' and clear close_date
- Update case.html template with edit form, success/error messaging, and action buttons
- Include comprehensive validation for dates and status values
- Add proper error handling with session-based error storage
- Preserve existing view content and styling consistency
This commit is contained in:
HotSwapp
2025-10-06 19:43:21 -05:00
parent 2e49340663
commit 728d26ad17
2 changed files with 290 additions and 1 deletions

View File

@@ -8,8 +8,10 @@ and provides the main application instance.
import os
import logging
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Optional
from fastapi import FastAPI, Depends, Request, Query
from fastapi import FastAPI, Depends, Request, Query, HTTPException
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from fastapi.middleware.cors import CORSMiddleware
@@ -320,6 +322,7 @@ async def admin_panel(request: Request, db: Session = Depends(get_db)):
async def case_detail(
request: Request,
case_id: int,
saved: bool = Query(False, description="Whether to show success message"),
db: Session = Depends(get_db),
):
"""
@@ -348,6 +351,9 @@ async def case_detail(
if not case_obj:
logger.warning("Case not found: id=%s", case_id)
# Get any errors from session and clear them
errors = request.session.pop("case_update_errors", None)
return templates.TemplateResponse(
"case.html",
{
@@ -355,17 +361,203 @@ async def case_detail(
"user": user,
"case": None,
"error": "Case not found",
"saved": False,
"errors": errors or [],
},
status_code=404,
)
logger.info("Rendering case detail: id=%s, file_no='%s'", case_obj.id, case_obj.file_no)
# Get any errors from session and clear them
errors = request.session.pop("case_update_errors", None)
return templates.TemplateResponse(
"case.html",
{
"request": request,
"user": user,
"case": case_obj,
"saved": saved,
"errors": errors or [],
},
)
@app.post("/case/{case_id}/update")
async def case_update(
request: Request,
case_id: int,
status: str = None,
case_type: str = None,
description: str = None,
open_date: str = None,
close_date: str = None,
db: Session = Depends(get_db),
) -> RedirectResponse:
"""
Update case details.
Updates the specified fields on a case and redirects back to the case detail view.
"""
# Check authentication
user = get_current_user_from_session(request.session)
if not user:
return RedirectResponse(url="/login", status_code=302)
# Fetch the case
case_obj = db.query(Case).filter(Case.id == case_id).first()
if not case_obj:
logger.warning("Case not found for update: id=%s", case_id)
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
# Validate and process fields
errors = []
update_data = {}
# Status validation
if status is not None:
if status not in ["active", "closed"]:
errors.append("Status must be 'active' or 'closed'")
else:
update_data["status"] = status
# Case type and description (optional)
if case_type is not None:
update_data["case_type"] = case_type.strip() if case_type.strip() else None
if description is not None:
update_data["description"] = description.strip() if description.strip() else None
# Date validation and parsing
if open_date is not None:
if open_date.strip():
try:
update_data["open_date"] = datetime.strptime(open_date.strip(), "%Y-%m-%d")
except ValueError:
errors.append("Open date must be in YYYY-MM-DD format")
else:
update_data["open_date"] = None
if close_date is not None:
if close_date.strip():
try:
update_data["close_date"] = datetime.strptime(close_date.strip(), "%Y-%m-%d")
except ValueError:
errors.append("Close date must be in YYYY-MM-DD format")
else:
update_data["close_date"] = None
# If there are validation errors, redirect back with errors
if errors:
# Store errors in session for display on the case page
request.session["case_update_errors"] = errors
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
# Apply updates
try:
for field, value in update_data.items():
setattr(case_obj, field, value)
db.commit()
logger.info("Case updated successfully: id=%s, fields=%s", case_id, list(update_data.keys()))
# Clear any previous errors from session
request.session.pop("case_update_errors", None)
return RedirectResponse(url=f"/case/{case_id}?saved=1", status_code=302)
except Exception as e:
db.rollback()
logger.error("Failed to update case id=%s: %s", case_id, str(e))
# Store error in session for display
request.session["case_update_errors"] = ["Failed to save changes. Please try again."]
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
@app.post("/case/{case_id}/close")
async def case_close(
request: Request,
case_id: int,
db: Session = Depends(get_db),
) -> RedirectResponse:
"""
Close a case.
Sets the case status to 'closed' and sets close_date to current date if not already set.
"""
# Check authentication
user = get_current_user_from_session(request.session)
if not user:
return RedirectResponse(url="/login", status_code=302)
# Fetch the case
case_obj = db.query(Case).filter(Case.id == case_id).first()
if not case_obj:
logger.warning("Case not found for close: id=%s", case_id)
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
# Update case
try:
case_obj.status = "closed"
# Only set close_date if it's not already set
if not case_obj.close_date:
case_obj.close_date = datetime.now()
db.commit()
logger.info("Case closed: id=%s, close_date=%s", case_id, case_obj.close_date)
return RedirectResponse(url=f"/case/{case_id}?saved=1", status_code=302)
except Exception as e:
db.rollback()
logger.error("Failed to close case id=%s: %s", case_id, str(e))
# Store error in session for display
request.session["case_update_errors"] = ["Failed to close case. Please try again."]
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
@app.post("/case/{case_id}/reopen")
async def case_reopen(
request: Request,
case_id: int,
db: Session = Depends(get_db),
) -> RedirectResponse:
"""
Reopen a case.
Sets the case status to 'active' and clears the close_date.
"""
# Check authentication
user = get_current_user_from_session(request.session)
if not user:
return RedirectResponse(url="/login", status_code=302)
# Fetch the case
case_obj = db.query(Case).filter(Case.id == case_id).first()
if not case_obj:
logger.warning("Case not found for reopen: id=%s", case_id)
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
# Update case
try:
case_obj.status = "active"
case_obj.close_date = None
db.commit()
logger.info("Case reopened: id=%s", case_id)
return RedirectResponse(url=f"/case/{case_id}?saved=1", status_code=302)
except Exception as e:
db.rollback()
logger.error("Failed to reopen case id=%s: %s", case_id, str(e))
# Store error in session for display
request.session["case_update_errors"] = ["Failed to reopen case. Please try again."]
return RedirectResponse(url=f"/case/{case_id}", status_code=302)

View File

@@ -20,6 +20,31 @@ Case {{ case.file_no if case else '' }} · Delphi Database
</div>
{% endif %}
{% if saved %}
<div class="col-12">
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i>
Case updated successfully!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
</div>
{% endif %}
{% if errors %}
<div class="col-12">
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Please fix the following errors:</strong>
<ul class="mb-0 mt-2">
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
</div>
{% endif %}
{% if case %}
<div class="col-12">
<div class="card">
@@ -83,6 +108,78 @@ Case {{ case.file_no if case else '' }} · Delphi Database
</div>
</div>
<!-- Edit Case Form -->
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Edit Case</h5>
<div>
{% if case.status == 'active' %}
<form method="post" action="/case/{{ case.id }}/close" class="d-inline me-2">
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('Are you sure you want to close this case?')">
<i class="bi bi-x-circle me-1"></i>Close Case
</button>
</form>
{% endif %}
{% if case.status == 'closed' %}
<form method="post" action="/case/{{ case.id }}/reopen" class="d-inline me-2">
<button type="submit" class="btn btn-sm btn-outline-success"
onclick="return confirm('Are you sure you want to reopen this case?')">
<i class="bi bi-check-circle me-1"></i>Reopen Case
</button>
</form>
{% endif %}
</div>
</div>
<div class="card-body">
<form method="post" action="/case/{{ case.id }}/update">
<div class="row g-3">
<div class="col-md-6">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="active" {% if case.status == 'active' %}selected{% endif %}>Active</option>
<option value="closed" {% if case.status == 'closed' %}selected{% endif %}>Closed</option>
</select>
</div>
<div class="col-md-6">
<label for="case_type" class="form-label">Case Type</label>
<input type="text" class="form-control" id="case_type" name="case_type"
value="{{ case.case_type or '' }}">
</div>
<div class="col-12">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ case.description or '' }}</textarea>
</div>
<div class="col-md-6">
<label for="open_date" class="form-label">Open Date</label>
<input type="date" class="form-control" id="open_date" name="open_date"
value="{{ case.open_date.strftime('%Y-%m-%d') if case.open_date else '' }}">
</div>
<div class="col-md-6">
<label for="close_date" class="form-label">Close Date</label>
<input type="date" class="form-control" id="close_date" name="close_date"
value="{{ case.close_date.strftime('%Y-%m-%d') if case.close_date else '' }}">
</div>
<div class="col-12">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>Save Changes
</button>
<a href="/case/{{ case.id }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card h-100">
<div class="card-header">Transactions</div>