feat: implement project status management with 'on_hold' state and associated UI updates
-feat: ability to hard delete projects, plus a soft delete with auto pruning.
This commit is contained in:
@@ -57,9 +57,11 @@ async def get_projects_list(
|
||||
"""
|
||||
query = db.query(Project)
|
||||
|
||||
# Filter by status if provided
|
||||
# Filter by status if provided; otherwise exclude soft-deleted projects
|
||||
if status:
|
||||
query = query.filter(Project.status == status)
|
||||
else:
|
||||
query = query.filter(Project.status != "deleted")
|
||||
|
||||
# Filter by project type if provided
|
||||
if project_type_id:
|
||||
@@ -118,9 +120,10 @@ async def get_projects_stats(request: Request, db: Session = Depends(get_db)):
|
||||
Get summary statistics for projects overview.
|
||||
Returns HTML partial with stat cards.
|
||||
"""
|
||||
# Count projects by status
|
||||
total_projects = db.query(func.count(Project.id)).scalar()
|
||||
# Count projects by status (exclude deleted)
|
||||
total_projects = db.query(func.count(Project.id)).filter(Project.status != "deleted").scalar()
|
||||
active_projects = db.query(func.count(Project.id)).filter_by(status="active").scalar()
|
||||
on_hold_projects = db.query(func.count(Project.id)).filter_by(status="on_hold").scalar()
|
||||
completed_projects = db.query(func.count(Project.id)).filter_by(status="completed").scalar()
|
||||
|
||||
# Count total locations across all projects
|
||||
@@ -140,6 +143,7 @@ async def get_projects_stats(request: Request, db: Session = Depends(get_db)):
|
||||
"request": request,
|
||||
"total_projects": total_projects,
|
||||
"active_projects": active_projects,
|
||||
"on_hold_projects": on_hold_projects,
|
||||
"completed_projects": completed_projects,
|
||||
"total_locations": total_locations,
|
||||
"assigned_units": assigned_units,
|
||||
@@ -178,13 +182,13 @@ async def search_projects(
|
||||
if not q.strip():
|
||||
# Return recent active projects when no search term
|
||||
projects = db.query(Project).filter(
|
||||
Project.status != "archived"
|
||||
Project.status.notin_(["archived", "deleted"])
|
||||
).order_by(Project.updated_at.desc()).limit(limit).all()
|
||||
else:
|
||||
search_term = f"%{q}%"
|
||||
projects = db.query(Project).filter(
|
||||
and_(
|
||||
Project.status != "archived",
|
||||
Project.status.notin_(["archived", "deleted"]),
|
||||
or_(
|
||||
Project.project_number.ilike(search_term),
|
||||
Project.client_name.ilike(search_term),
|
||||
@@ -223,13 +227,13 @@ async def search_projects_json(
|
||||
"""
|
||||
if not q.strip():
|
||||
projects = db.query(Project).filter(
|
||||
Project.status != "archived"
|
||||
Project.status.notin_(["archived", "deleted"])
|
||||
).order_by(Project.updated_at.desc()).limit(limit).all()
|
||||
else:
|
||||
search_term = f"%{q}%"
|
||||
projects = db.query(Project).filter(
|
||||
and_(
|
||||
Project.status != "archived",
|
||||
Project.status.notin_(["archived", "deleted"]),
|
||||
or_(
|
||||
Project.project_number.ilike(search_term),
|
||||
Project.client_name.ilike(search_term),
|
||||
@@ -359,18 +363,76 @@ async def update_project(
|
||||
@router.delete("/{project_id}")
|
||||
async def delete_project(project_id: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Delete a project (soft delete by archiving).
|
||||
Soft-delete a project. Sets status='deleted' and records deleted_at timestamp.
|
||||
Data will be permanently removed after 60 days (or via /permanent endpoint).
|
||||
"""
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
project.status = "archived"
|
||||
project.status = "deleted"
|
||||
project.deleted_at = datetime.utcnow()
|
||||
project.updated_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": "Project archived successfully"}
|
||||
return {"success": True, "message": "Project deleted. Data will be permanently removed after 60 days."}
|
||||
|
||||
|
||||
@router.delete("/{project_id}/permanent")
|
||||
async def permanently_delete_project(project_id: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Hard-delete a project and all related data. Only allowed when status='deleted'.
|
||||
Removes: locations, assignments, sessions, scheduled actions, recurring schedules.
|
||||
"""
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
if project.status != "deleted":
|
||||
raise HTTPException(status_code=400, detail="Project must be soft-deleted before permanent deletion.")
|
||||
|
||||
# Delete related data
|
||||
db.query(RecurringSchedule).filter_by(project_id=project_id).delete()
|
||||
db.query(ScheduledAction).filter_by(project_id=project_id).delete()
|
||||
db.query(RecordingSession).filter_by(project_id=project_id).delete()
|
||||
db.query(UnitAssignment).filter_by(project_id=project_id).delete()
|
||||
db.query(MonitoringLocation).filter_by(project_id=project_id).delete()
|
||||
db.delete(project)
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": "Project permanently deleted."}
|
||||
|
||||
|
||||
@router.post("/{project_id}/hold")
|
||||
async def hold_project(project_id: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Put a project on hold. Pauses without archiving; assignments and schedules remain.
|
||||
"""
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
project.status = "on_hold"
|
||||
project.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": "Project put on hold."}
|
||||
|
||||
|
||||
@router.post("/{project_id}/unhold")
|
||||
async def unhold_project(project_id: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Resume a project that was on hold.
|
||||
"""
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
project.status = "active"
|
||||
project.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": "Project resumed."}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user