Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
422 lines
13 KiB
Python
422 lines
13 KiB
Python
"""
|
|
Inbox Task Service (Arbeitsvorrat)
|
|
|
|
Manages tasks extracted from emails and manual task creation.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime, timedelta
|
|
|
|
from .models import (
|
|
TaskStatus,
|
|
TaskPriority,
|
|
InboxTask,
|
|
TaskCreate,
|
|
TaskUpdate,
|
|
TaskDashboardStats,
|
|
SenderType,
|
|
DeadlineExtraction,
|
|
)
|
|
from .mail_db import (
|
|
create_task as db_create_task,
|
|
get_tasks as db_get_tasks,
|
|
get_task as db_get_task,
|
|
update_task as db_update_task,
|
|
get_task_dashboard_stats as db_get_dashboard_stats,
|
|
get_email,
|
|
log_mail_audit,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TaskService:
|
|
"""
|
|
Service for managing inbox tasks (Arbeitsvorrat).
|
|
|
|
Features:
|
|
- Create tasks from emails (auto or manual)
|
|
- Track deadlines and priorities
|
|
- Dashboard statistics
|
|
- Reminders (to be integrated with notification service)
|
|
"""
|
|
|
|
async def create_task_from_email(
|
|
self,
|
|
user_id: str,
|
|
tenant_id: str,
|
|
email_id: str,
|
|
deadlines: List[DeadlineExtraction],
|
|
sender_type: Optional[SenderType] = None,
|
|
auto_created: bool = False,
|
|
) -> Optional[str]:
|
|
"""
|
|
Create a task from an analyzed email.
|
|
|
|
Args:
|
|
user_id: The user ID
|
|
tenant_id: The tenant ID
|
|
email_id: The source email ID
|
|
deadlines: Extracted deadlines
|
|
sender_type: Classified sender type
|
|
auto_created: Whether this was auto-created by AI
|
|
|
|
Returns:
|
|
Task ID if created successfully
|
|
"""
|
|
# Get email details
|
|
email_data = await get_email(email_id, user_id)
|
|
if not email_data:
|
|
logger.warning(f"Email not found: {email_id}")
|
|
return None
|
|
|
|
# Determine priority
|
|
priority = TaskPriority.MEDIUM
|
|
if sender_type:
|
|
priority = self._get_priority_from_sender(sender_type)
|
|
|
|
# Get earliest deadline
|
|
deadline = None
|
|
if deadlines:
|
|
deadline = min(d.deadline_date for d in deadlines)
|
|
|
|
# Adjust priority based on deadline
|
|
priority = self._adjust_priority_for_deadline(priority, deadline)
|
|
|
|
# Create task title from email subject
|
|
subject = email_data.get("subject", "Keine Betreffzeile")
|
|
title = f"Bearbeiten: {subject[:100]}"
|
|
|
|
# Create description from deadlines
|
|
description = self._build_task_description(deadlines, email_data)
|
|
|
|
# Create the task
|
|
task_id = await db_create_task(
|
|
user_id=user_id,
|
|
tenant_id=tenant_id,
|
|
title=title,
|
|
description=description,
|
|
priority=priority.value,
|
|
deadline=deadline,
|
|
email_id=email_id,
|
|
account_id=email_data.get("account_id"),
|
|
source_email_subject=subject,
|
|
source_sender=email_data.get("sender_email"),
|
|
source_sender_type=sender_type.value if sender_type else None,
|
|
ai_extracted=auto_created,
|
|
confidence_score=deadlines[0].confidence if deadlines else None,
|
|
)
|
|
|
|
if task_id:
|
|
# Log audit event
|
|
await log_mail_audit(
|
|
user_id=user_id,
|
|
action="task_created",
|
|
entity_type="task",
|
|
entity_id=task_id,
|
|
details={
|
|
"email_id": email_id,
|
|
"auto_created": auto_created,
|
|
"deadline": deadline.isoformat() if deadline else None,
|
|
},
|
|
tenant_id=tenant_id,
|
|
)
|
|
|
|
logger.info(f"Created task {task_id} from email {email_id}")
|
|
|
|
return task_id
|
|
|
|
async def create_manual_task(
|
|
self,
|
|
user_id: str,
|
|
tenant_id: str,
|
|
task_data: TaskCreate,
|
|
) -> Optional[str]:
|
|
"""
|
|
Create a task manually (not from email).
|
|
|
|
Args:
|
|
user_id: The user ID
|
|
tenant_id: The tenant ID
|
|
task_data: Task creation data
|
|
|
|
Returns:
|
|
Task ID if created successfully
|
|
"""
|
|
# Get email details if linked
|
|
source_subject = None
|
|
source_sender = None
|
|
account_id = None
|
|
|
|
if task_data.email_id:
|
|
email_data = await get_email(task_data.email_id, user_id)
|
|
if email_data:
|
|
source_subject = email_data.get("subject")
|
|
source_sender = email_data.get("sender_email")
|
|
account_id = email_data.get("account_id")
|
|
|
|
task_id = await db_create_task(
|
|
user_id=user_id,
|
|
tenant_id=tenant_id,
|
|
title=task_data.title,
|
|
description=task_data.description,
|
|
priority=task_data.priority.value,
|
|
deadline=task_data.deadline,
|
|
email_id=task_data.email_id,
|
|
account_id=account_id,
|
|
source_email_subject=source_subject,
|
|
source_sender=source_sender,
|
|
ai_extracted=False,
|
|
)
|
|
|
|
if task_id:
|
|
await log_mail_audit(
|
|
user_id=user_id,
|
|
action="task_created_manual",
|
|
entity_type="task",
|
|
entity_id=task_id,
|
|
details={"title": task_data.title},
|
|
tenant_id=tenant_id,
|
|
)
|
|
|
|
return task_id
|
|
|
|
async def get_user_tasks(
|
|
self,
|
|
user_id: str,
|
|
status: Optional[TaskStatus] = None,
|
|
priority: Optional[TaskPriority] = None,
|
|
include_completed: bool = False,
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
) -> List[Dict]:
|
|
"""
|
|
Get tasks for a user with filtering.
|
|
|
|
Args:
|
|
user_id: The user ID
|
|
status: Filter by status
|
|
priority: Filter by priority
|
|
include_completed: Include completed tasks
|
|
limit: Maximum results
|
|
offset: Pagination offset
|
|
|
|
Returns:
|
|
List of task dictionaries
|
|
"""
|
|
return await db_get_tasks(
|
|
user_id=user_id,
|
|
status=status.value if status else None,
|
|
priority=priority.value if priority else None,
|
|
include_completed=include_completed,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
async def get_task(self, task_id: str, user_id: str) -> Optional[Dict]:
|
|
"""Get a single task by ID."""
|
|
return await db_get_task(task_id, user_id)
|
|
|
|
async def update_task(
|
|
self,
|
|
task_id: str,
|
|
user_id: str,
|
|
updates: TaskUpdate,
|
|
) -> bool:
|
|
"""
|
|
Update a task.
|
|
|
|
Args:
|
|
task_id: The task ID
|
|
user_id: The user ID
|
|
updates: Fields to update
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
update_dict = {}
|
|
|
|
if updates.title is not None:
|
|
update_dict["title"] = updates.title
|
|
if updates.description is not None:
|
|
update_dict["description"] = updates.description
|
|
if updates.priority is not None:
|
|
update_dict["priority"] = updates.priority.value
|
|
if updates.status is not None:
|
|
update_dict["status"] = updates.status.value
|
|
if updates.deadline is not None:
|
|
update_dict["deadline"] = updates.deadline
|
|
|
|
success = await db_update_task(task_id, user_id, **update_dict)
|
|
|
|
if success:
|
|
await log_mail_audit(
|
|
user_id=user_id,
|
|
action="task_updated",
|
|
entity_type="task",
|
|
entity_id=task_id,
|
|
details={"updates": update_dict},
|
|
)
|
|
|
|
return success
|
|
|
|
async def mark_completed(self, task_id: str, user_id: str) -> bool:
|
|
"""Mark a task as completed."""
|
|
success = await db_update_task(
|
|
task_id, user_id, status=TaskStatus.COMPLETED.value
|
|
)
|
|
|
|
if success:
|
|
await log_mail_audit(
|
|
user_id=user_id,
|
|
action="task_completed",
|
|
entity_type="task",
|
|
entity_id=task_id,
|
|
)
|
|
|
|
return success
|
|
|
|
async def mark_in_progress(self, task_id: str, user_id: str) -> bool:
|
|
"""Mark a task as in progress."""
|
|
return await db_update_task(
|
|
task_id, user_id, status=TaskStatus.IN_PROGRESS.value
|
|
)
|
|
|
|
async def get_dashboard_stats(self, user_id: str) -> TaskDashboardStats:
|
|
"""
|
|
Get dashboard statistics for a user.
|
|
|
|
Returns:
|
|
TaskDashboardStats with all metrics
|
|
"""
|
|
stats = await db_get_dashboard_stats(user_id)
|
|
|
|
return TaskDashboardStats(
|
|
total_tasks=stats.get("total_tasks", 0),
|
|
pending_tasks=stats.get("pending_tasks", 0),
|
|
in_progress_tasks=stats.get("in_progress_tasks", 0),
|
|
completed_tasks=stats.get("completed_tasks", 0),
|
|
overdue_tasks=stats.get("overdue_tasks", 0),
|
|
due_today=stats.get("due_today", 0),
|
|
due_this_week=stats.get("due_this_week", 0),
|
|
by_priority=stats.get("by_priority", {}),
|
|
by_sender_type=stats.get("by_sender_type", {}),
|
|
)
|
|
|
|
async def get_overdue_tasks(self, user_id: str) -> List[Dict]:
|
|
"""Get all overdue tasks for a user."""
|
|
all_tasks = await db_get_tasks(user_id, include_completed=False, limit=500)
|
|
|
|
now = datetime.now()
|
|
overdue = [
|
|
task for task in all_tasks
|
|
if task.get("deadline") and task["deadline"] < now
|
|
]
|
|
|
|
return overdue
|
|
|
|
async def get_tasks_due_soon(
|
|
self,
|
|
user_id: str,
|
|
days: int = 3,
|
|
) -> List[Dict]:
|
|
"""Get tasks due within the specified number of days."""
|
|
all_tasks = await db_get_tasks(user_id, include_completed=False, limit=500)
|
|
|
|
now = datetime.now()
|
|
deadline_cutoff = now + timedelta(days=days)
|
|
|
|
due_soon = [
|
|
task for task in all_tasks
|
|
if task.get("deadline") and now <= task["deadline"] <= deadline_cutoff
|
|
]
|
|
|
|
return sorted(due_soon, key=lambda t: t["deadline"])
|
|
|
|
# =========================================================================
|
|
# Helper Methods
|
|
# =========================================================================
|
|
|
|
def _get_priority_from_sender(self, sender_type: SenderType) -> TaskPriority:
|
|
"""Determine priority based on sender type."""
|
|
high_priority_senders = {
|
|
SenderType.KULTUSMINISTERIUM,
|
|
SenderType.LANDESSCHULBEHOERDE,
|
|
SenderType.RLSB,
|
|
SenderType.SCHULAMT,
|
|
}
|
|
|
|
medium_priority_senders = {
|
|
SenderType.NIBIS,
|
|
SenderType.SCHULTRAEGER,
|
|
SenderType.ELTERNVERTRETER,
|
|
}
|
|
|
|
if sender_type in high_priority_senders:
|
|
return TaskPriority.HIGH
|
|
elif sender_type in medium_priority_senders:
|
|
return TaskPriority.MEDIUM
|
|
else:
|
|
return TaskPriority.LOW
|
|
|
|
def _adjust_priority_for_deadline(
|
|
self,
|
|
current_priority: TaskPriority,
|
|
deadline: datetime,
|
|
) -> TaskPriority:
|
|
"""Adjust priority based on deadline proximity."""
|
|
now = datetime.now()
|
|
days_until = (deadline - now).days
|
|
|
|
if days_until <= 1:
|
|
return TaskPriority.URGENT
|
|
elif days_until <= 3:
|
|
return max(current_priority, TaskPriority.HIGH)
|
|
elif days_until <= 7:
|
|
return max(current_priority, TaskPriority.MEDIUM)
|
|
else:
|
|
return current_priority
|
|
|
|
def _build_task_description(
|
|
self,
|
|
deadlines: List[DeadlineExtraction],
|
|
email_data: Dict,
|
|
) -> str:
|
|
"""Build a task description from deadlines and email data."""
|
|
parts = []
|
|
|
|
# Add deadlines
|
|
if deadlines:
|
|
parts.append("**Fristen:**")
|
|
for d in deadlines:
|
|
date_str = d.deadline_date.strftime("%d.%m.%Y")
|
|
firm_str = " (verbindlich)" if d.is_firm else ""
|
|
parts.append(f"- {date_str}: {d.description}{firm_str}")
|
|
parts.append("")
|
|
|
|
# Add email info
|
|
sender = email_data.get("sender_email", "Unbekannt")
|
|
parts.append(f"**Von:** {sender}")
|
|
|
|
# Add preview
|
|
preview = email_data.get("body_preview", "")
|
|
if preview:
|
|
parts.append(f"\n**Vorschau:**\n{preview[:300]}...")
|
|
|
|
return "\n".join(parts)
|
|
|
|
|
|
# Global instance
|
|
_task_service: Optional[TaskService] = None
|
|
|
|
|
|
def get_task_service() -> TaskService:
|
|
"""Get or create the global TaskService instance."""
|
|
global _task_service
|
|
|
|
if _task_service is None:
|
|
_task_service = TaskService()
|
|
|
|
return _task_service
|