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 os
|
||||||
import logging
|
import logging
|
||||||
from contextlib import asynccontextmanager
|
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 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
|
||||||
@@ -320,6 +322,7 @@ async def admin_panel(request: Request, db: Session = Depends(get_db)):
|
|||||||
async def case_detail(
|
async def case_detail(
|
||||||
request: Request,
|
request: Request,
|
||||||
case_id: int,
|
case_id: int,
|
||||||
|
saved: bool = Query(False, description="Whether to show success message"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -348,6 +351,9 @@ async def case_detail(
|
|||||||
|
|
||||||
if not case_obj:
|
if not case_obj:
|
||||||
logger.warning("Case not found: id=%s", case_id)
|
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(
|
return templates.TemplateResponse(
|
||||||
"case.html",
|
"case.html",
|
||||||
{
|
{
|
||||||
@@ -355,17 +361,203 @@ async def case_detail(
|
|||||||
"user": user,
|
"user": user,
|
||||||
"case": None,
|
"case": None,
|
||||||
"error": "Case not found",
|
"error": "Case not found",
|
||||||
|
"saved": False,
|
||||||
|
"errors": errors or [],
|
||||||
},
|
},
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Rendering case detail: id=%s, file_no='%s'", case_obj.id, case_obj.file_no)
|
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(
|
return templates.TemplateResponse(
|
||||||
"case.html",
|
"case.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"user": user,
|
"user": user,
|
||||||
"case": case_obj,
|
"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>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if case %}
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -83,6 +108,78 @@ Case {{ case.file_no if case else '' }} · Delphi Database
|
|||||||
</div>
|
</div>
|
||||||
</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="col-xl-4">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header">Transactions</div>
|
<div class="card-header">Transactions</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user