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:
194
app/main.py
194
app/main.py
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user