""" 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