feat: enhance project management by canceling pending actions for archived and on_hold projects
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user