feat: implement automation module

- Event-based triggers (Phase 1):
  - Trigger/TriggerLog models with field_change type
  - TriggerService for condition evaluation and action execution
  - Trigger CRUD API endpoints
  - Task integration (status, assignee, priority changes)
  - Frontend: TriggerList, TriggerForm components

- Weekly reports (Phase 2):
  - ScheduledReport/ReportHistory models
  - ReportService for stats generation
  - APScheduler for Friday 16:00 job
  - Report preview/generate/history API
  - Frontend: WeeklyReportPreview, ReportHistory components

- Tests: 23 new tests (14 triggers + 9 reports)
- OpenSpec: add-automation change archived

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2025-12-29 22:59:00 +08:00
parent 3108fe1dff
commit 95c281d8e1
32 changed files with 3163 additions and 3 deletions

View File

@@ -14,6 +14,7 @@ from app.middleware.auth import (
)
from app.middleware.audit import get_audit_metadata
from app.services.audit_service import AuditService
from app.services.trigger_service import TriggerService
router = APIRouter(tags=["tasks"])
@@ -271,7 +272,7 @@ async def update_task(
detail="Permission denied",
)
# Capture old values for audit
# Capture old values for audit and triggers
old_values = {
"title": task.title,
"description": task.description,
@@ -289,7 +290,7 @@ async def update_task(
else:
setattr(task, field, value)
# Capture new values for audit
# Capture new values for audit and triggers
new_values = {
"title": task.title,
"description": task.description,
@@ -313,6 +314,10 @@ async def update_task(
request_metadata=get_audit_metadata(request),
)
# Evaluate triggers for priority changes
if "priority" in update_data:
TriggerService.evaluate_triggers(db, task, old_values, new_values, current_user)
db.commit()
db.refresh(task)
@@ -397,6 +402,9 @@ async def update_task_status(
detail="Status not found in this project",
)
# Capture old status for triggers
old_status_id = task.status_id
task.status_id = status_data.status_id
# Auto-set blocker_flag based on status name
@@ -405,6 +413,15 @@ async def update_task_status(
else:
task.blocker_flag = False
# Evaluate triggers for status changes
if old_status_id != status_data.status_id:
TriggerService.evaluate_triggers(
db, task,
{"status_id": old_status_id},
{"status_id": status_data.status_id},
current_user
)
db.commit()
db.refresh(task)
@@ -460,6 +477,15 @@ async def assign_task(
request_metadata=get_audit_metadata(request),
)
# Evaluate triggers for assignee changes
if old_assignee_id != assign_data.assignee_id:
TriggerService.evaluate_triggers(
db, task,
{"assignee_id": old_assignee_id},
{"assignee_id": assign_data.assignee_id},
current_user
)
db.commit()
db.refresh(task)