feat: enhance project management by canceling pending actions for archived and on_hold projects

This commit is contained in:
serversdwn
2026-02-19 18:57:59 +00:00
parent 65362bab21
commit 0f17841218
7 changed files with 85 additions and 8 deletions

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter, Request, Depends from fastapi import APIRouter, Request, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import and_
from datetime import datetime, timedelta from datetime import datetime, timedelta
from backend.database import get_db from backend.database import get_db
@@ -48,10 +49,18 @@ def dashboard_todays_actions(request: Request, db: Session = Depends(get_db)):
today_start_utc = today_start_local.astimezone(ZoneInfo("UTC")).replace(tzinfo=None) today_start_utc = today_start_local.astimezone(ZoneInfo("UTC")).replace(tzinfo=None)
today_end_utc = today_end_local.astimezone(ZoneInfo("UTC")).replace(tzinfo=None) today_end_utc = today_end_local.astimezone(ZoneInfo("UTC")).replace(tzinfo=None)
# Exclude actions from paused/removed projects
paused_project_ids = [
p.id for p in db.query(Project.id).filter(
Project.status.in_(["on_hold", "archived", "deleted"])
).all()
]
# Query today's actions # Query today's actions
actions = db.query(ScheduledAction).filter( actions = db.query(ScheduledAction).filter(
ScheduledAction.scheduled_time >= today_start_utc, ScheduledAction.scheduled_time >= today_start_utc,
ScheduledAction.scheduled_time < today_end_utc, ScheduledAction.scheduled_time < today_end_utc,
ScheduledAction.project_id.notin_(paused_project_ids),
).order_by(ScheduledAction.scheduled_time.asc()).all() ).order_by(ScheduledAction.scheduled_time.asc()).all()
# Enrich with location/project info and parse results # Enrich with location/project info and parse results

View File

@@ -342,6 +342,14 @@ async def update_project(
project.description = data["description"] project.description = data["description"]
if "status" in data: if "status" in data:
project.status = data["status"] project.status = data["status"]
# Cancel pending scheduled actions when archiving
if data["status"] == "archived":
db.query(ScheduledAction).filter(
and_(
ScheduledAction.project_id == project_id,
ScheduledAction.execution_status == "pending",
)
).update({"execution_status": "cancelled"})
if "client_name" in data: if "client_name" in data:
project.client_name = data["client_name"] project.client_name = data["client_name"]
if "site_address" in data: if "site_address" in data:
@@ -374,6 +382,14 @@ async def delete_project(project_id: str, db: Session = Depends(get_db)):
project.deleted_at = datetime.utcnow() project.deleted_at = datetime.utcnow()
project.updated_at = datetime.utcnow() project.updated_at = datetime.utcnow()
# Cancel all pending scheduled actions
db.query(ScheduledAction).filter(
and_(
ScheduledAction.project_id == project_id,
ScheduledAction.execution_status == "pending",
)
).update({"execution_status": "cancelled"})
db.commit() db.commit()
return {"success": True, "message": "Project deleted. Data will be permanently removed after 60 days."} return {"success": True, "message": "Project deleted. Data will be permanently removed after 60 days."}
@@ -414,6 +430,15 @@ async def hold_project(project_id: str, db: Session = Depends(get_db)):
project.status = "on_hold" project.status = "on_hold"
project.updated_at = datetime.utcnow() project.updated_at = datetime.utcnow()
# Cancel pending scheduled actions so they don't appear in dashboards or fire
db.query(ScheduledAction).filter(
and_(
ScheduledAction.project_id == project_id,
ScheduledAction.execution_status == "pending",
)
).update({"execution_status": "cancelled"})
db.commit() db.commit()
return {"success": True, "message": "Project put on hold."} return {"success": True, "message": "Project put on hold."}
@@ -672,10 +697,14 @@ async def get_project_schedules(
"result": result_data, "result": result_data,
}) })
project = db.query(Project).filter_by(id=project_id).first()
project_status = project.status if project else "active"
return templates.TemplateResponse("partials/projects/schedule_list.html", { return templates.TemplateResponse("partials/projects/schedule_list.html", {
"request": request, "request": request,
"project_id": project_id, "project_id": project_id,
"schedules_by_date": schedules_by_date, "schedules_by_date": schedules_by_date,
"project_status": project_status,
}) })

View File

@@ -497,6 +497,9 @@ async def get_schedule_list_partial(
""" """
Return HTML partial for schedule list. Return HTML partial for schedule list.
""" """
project = db.query(Project).filter_by(id=project_id).first()
project_status = project.status if project else "active"
schedules = db.query(RecurringSchedule).filter_by( schedules = db.query(RecurringSchedule).filter_by(
project_id=project_id project_id=project_id
).order_by(RecurringSchedule.created_at.desc()).all() ).order_by(RecurringSchedule.created_at.desc()).all()
@@ -515,4 +518,5 @@ async def get_schedule_list_partial(
"request": request, "request": request,
"project_id": project_id, "project_id": project_id,
"schedules": schedule_data, "schedules": schedule_data,
"project_status": project_status,
}) })

View File

@@ -15,7 +15,7 @@ from zoneinfo import ZoneInfo
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import and_ from sqlalchemy import and_
from backend.models import RecurringSchedule, ScheduledAction, MonitoringLocation, UnitAssignment from backend.models import RecurringSchedule, ScheduledAction, MonitoringLocation, UnitAssignment, Project
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -594,8 +594,16 @@ class RecurringScheduleService:
return self.db.query(RecurringSchedule).filter_by(project_id=project_id).all() return self.db.query(RecurringSchedule).filter_by(project_id=project_id).all()
def get_enabled_schedules(self) -> List[RecurringSchedule]: def get_enabled_schedules(self) -> List[RecurringSchedule]:
"""Get all enabled recurring schedules.""" """Get all enabled recurring schedules for projects that are not on hold or deleted."""
return self.db.query(RecurringSchedule).filter_by(enabled=True).all() active_project_ids = [
p.id for p in self.db.query(Project.id).filter(
Project.status.notin_(["on_hold", "archived", "deleted"])
).all()
]
return self.db.query(RecurringSchedule).filter(
RecurringSchedule.enabled == True,
RecurringSchedule.project_id.in_(active_project_ids),
).all()
def get_recurring_schedule_service(db: Session) -> RecurringScheduleService: def get_recurring_schedule_service(db: Session) -> RecurringScheduleService:

View File

@@ -107,10 +107,19 @@ class SchedulerService:
try: try:
# Find pending actions that are due # Find pending actions that are due
now = datetime.utcnow() now = datetime.utcnow()
# Only execute actions for active/completed projects (not on_hold, archived, or deleted)
active_project_ids = [
p.id for p in db.query(Project.id).filter(
Project.status.notin_(["on_hold", "archived", "deleted"])
).all()
]
pending_actions = db.query(ScheduledAction).filter( pending_actions = db.query(ScheduledAction).filter(
and_( and_(
ScheduledAction.execution_status == "pending", ScheduledAction.execution_status == "pending",
ScheduledAction.scheduled_time <= now, ScheduledAction.scheduled_time <= now,
ScheduledAction.project_id.in_(active_project_ids),
) )
).order_by(ScheduledAction.scheduled_time).all() ).order_by(ScheduledAction.scheduled_time).all()

View File

@@ -5,7 +5,7 @@
{% if schedules %} {% if schedules %}
{% for item in schedules %} {% for item in schedules %}
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4
{% if not item.schedule.enabled %}opacity-60{% endif %}"> {% if project_status == 'on_hold' or not item.schedule.enabled %}opacity-60{% endif %}">
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-2"> <div class="flex items-center gap-3 mb-2">
@@ -29,7 +29,15 @@
{% endif %} {% endif %}
<!-- Status badge --> <!-- Status badge -->
{% if item.schedule.enabled %} {% if project_status == 'on_hold' %}
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
On Hold
</span>
{% elif project_status == 'archived' %}
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
Archived
</span>
{% elif item.schedule.enabled %}
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300"> <span class="px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
Active Active
</span> </span>
@@ -98,7 +106,8 @@
</div> </div>
</div> </div>
<!-- Actions --> <!-- Actions (hidden when project is on hold or archived) -->
{% if project_status not in ('on_hold', 'archived') %}
<div class="flex items-center gap-2 flex-shrink-0"> <div class="flex items-center gap-2 flex-shrink-0">
{% if item.schedule.enabled %} {% if item.schedule.enabled %}
<button hx-post="/api/projects/{{ project_id }}/recurring-schedules/{{ item.schedule.id }}/disable" <button hx-post="/api/projects/{{ project_id }}/recurring-schedules/{{ item.schedule.id }}/disable"
@@ -131,6 +140,7 @@
</svg> </svg>
</button> </button>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -19,7 +19,8 @@
<!-- Actions for this date --> <!-- Actions for this date -->
<div class="space-y-3 ml-13 pl-3 border-l-2 border-gray-200 dark:border-gray-700"> <div class="space-y-3 ml-13 pl-3 border-l-2 border-gray-200 dark:border-gray-700">
{% for item in date_group.actions %} {% for item in date_group.actions %}
<div class="bg-white dark:bg-slate-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow"> <div class="bg-white dark:bg-slate-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow
{% if project_status == 'on_hold' and item.schedule.execution_status == 'pending' %}opacity-60{% endif %}">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-3 mb-2"> <div class="flex items-center gap-3 mb-2">
@@ -54,6 +55,11 @@
<span class="px-2 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 rounded-full"> <span class="px-2 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 rounded-full">
Pending Pending
</span> </span>
{% if project_status == 'on_hold' %}
<span class="px-2 py-0.5 text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 rounded-full">
On Hold
</span>
{% endif %}
{% elif item.schedule.execution_status == 'completed' %} {% elif item.schedule.execution_status == 'completed' %}
<span class="px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full"> <span class="px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full">
Completed Completed
@@ -157,7 +163,8 @@
{% endif %} {% endif %}
</div> </div>
<!-- Actions --> <!-- Actions (hidden when project is on hold or archived) -->
{% if project_status not in ('on_hold', 'archived') %}
<div class="flex items-center gap-2 flex-shrink-0"> <div class="flex items-center gap-2 flex-shrink-0">
{% if item.schedule.execution_status == 'pending' %} {% if item.schedule.execution_status == 'pending' %}
<button onclick="executeSchedule('{{ item.schedule.id }}')" <button onclick="executeSchedule('{{ item.schedule.id }}')"
@@ -177,6 +184,7 @@
</button> </button>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}