from calendar import monthrange from datetime import UTC, date, datetime from fastapi import APIRouter, Depends, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.database import get_db from app.models.goal import Goal from app.models.reminder import Reminder from app.models.task import Task, TaskDispatchStatus, TaskPriority, TaskQuadrant, TaskStatus from app.models.todo import DailyTodo from app.models.user import User from app.routers.auth import get_current_user from app.schemas.schedule_center import ( ScheduleCenterCommanderSummaryOut, ScheduleCenterDateOut, ScheduleCenterDaySummary, ScheduleCenterFocusTaskOut, ScheduleCenterMonthOut, ScheduleCenterQuadrantOut, ScheduleCenterQuadrantTaskOut, ) from app.schemas.task import build_task_out router = APIRouter(prefix="/api/schedule-center", tags=["调度中心"]) QUADRANT_META: dict[TaskQuadrant, dict[str, str]] = { TaskQuadrant.URGENT_IMPORTANT: { "title": "重要且紧急", "subtitle": "CRITICAL", "color": "#ff4757", "glow_color": "rgba(255, 71, 87, 0.4)", "icon": "◈", }, TaskQuadrant.NOT_URGENT_IMPORTANT: { "title": "重要不紧急", "subtitle": "PLANNED", "color": "#ffd93d", "glow_color": "rgba(255, 217, 61, 0.4)", "icon": "◇", }, TaskQuadrant.URGENT_NOT_IMPORTANT: { "title": "紧急不重要", "subtitle": "DELEGATE", "color": "#00d4ff", "glow_color": "rgba(0, 212, 255, 0.4)", "icon": "◉", }, TaskQuadrant.NOT_URGENT_NOT_IMPORTANT: { "title": "不重要不紧急", "subtitle": "ELIMINATE", "color": "#6bcf7f", "glow_color": "rgba(107, 207, 127, 0.4)", "icon": "○", }, } def _build_summary( target_date: str, todos: list[DailyTodo], tasks: list[Task], reminders: list[Reminder], goals: list[Goal], ) -> ScheduleCenterDaySummary: return ScheduleCenterDaySummary( date=target_date, todo_total=len(todos), todo_completed=sum(1 for item in todos if item.is_completed), task_due_total=len(tasks), high_priority_total=sum(1 for item in tasks if item.priority in {TaskPriority.HIGH, TaskPriority.URGENT}), reminder_total=len(reminders), goal_total=len(goals), ) def _coerce_enum(value, enum_cls, default=None): if value is None: return default if isinstance(value, enum_cls): return value if isinstance(value, str): raw = value.strip() if not raw: return default for item in enum_cls: if raw == item.value or raw.lower() == item.value: return item if raw.upper() == item.name: return item return default def _derive_quadrant(task: Task) -> TaskQuadrant: quadrant = _coerce_enum(task.quadrant, TaskQuadrant, None) if quadrant is not None: return quadrant priority = _coerce_enum(task.priority, TaskPriority, TaskPriority.MEDIUM) status = _coerce_enum(task.status, TaskStatus, TaskStatus.TODO) if priority in {TaskPriority.HIGH, TaskPriority.URGENT}: return TaskQuadrant.URGENT_IMPORTANT if status == TaskStatus.IN_PROGRESS: return TaskQuadrant.NOT_URGENT_IMPORTANT if priority == TaskPriority.MEDIUM: return TaskQuadrant.URGENT_NOT_IMPORTANT return TaskQuadrant.NOT_URGENT_NOT_IMPORTANT def _enum_value(value) -> str | None: if value is None: return None if hasattr(value, "value"): return str(value.value) if isinstance(value, str): raw = value.strip() return raw or None return str(value) def _build_focus_tasks(tasks: list[Task]) -> list[ScheduleCenterFocusTaskOut]: priority_rank = { TaskPriority.URGENT: 0, TaskPriority.HIGH: 1, TaskPriority.MEDIUM: 2, TaskPriority.LOW: 3, } status_rank = { TaskStatus.IN_PROGRESS: 0, TaskStatus.TODO: 1, TaskStatus.DONE: 2, TaskStatus.CANCELLED: 3, } ordered = sorted( tasks, key=lambda item: ( status_rank.get(_coerce_enum(item.status, TaskStatus, TaskStatus.TODO), 99), priority_rank.get(_coerce_enum(item.priority, TaskPriority, TaskPriority.MEDIUM), 99), item.created_at, ), ) return [ ScheduleCenterFocusTaskOut( id=item.id, title=item.title, status=_coerce_enum(item.status, TaskStatus, TaskStatus.TODO), priority=_coerce_enum(item.priority, TaskPriority, TaskPriority.MEDIUM), quadrant=_derive_quadrant(item), assignee_type=_enum_value(item.assignee_type), assignee_id=item.assignee_id, dispatch_status=_coerce_enum(item.dispatch_status, TaskDispatchStatus, TaskDispatchStatus.IDLE), due_date=item.due_date, ) for item in ordered[:6] ] def _build_quadrants(tasks: list[Task]) -> list[ScheduleCenterQuadrantOut]: buckets: dict[TaskQuadrant, list[ScheduleCenterQuadrantTaskOut]] = { quadrant: [] for quadrant in QUADRANT_META } for task in tasks: quadrant = _derive_quadrant(task) buckets[quadrant].append( ScheduleCenterQuadrantTaskOut( id=task.id, title=task.title, status=_coerce_enum(task.status, TaskStatus, TaskStatus.TODO), priority=_coerce_enum(task.priority, TaskPriority, TaskPriority.MEDIUM), dispatch_status=_coerce_enum(task.dispatch_status, TaskDispatchStatus, TaskDispatchStatus.IDLE), assignee_type=_enum_value(task.assignee_type), assignee_id=task.assignee_id, ) ) return [ ScheduleCenterQuadrantOut( id=quadrant, title=meta["title"], subtitle=meta["subtitle"], color=meta["color"], glow_color=meta["glow_color"], icon=meta["icon"], tasks=buckets[quadrant], ) for quadrant, meta in QUADRANT_META.items() ] def _build_commander_summary(tasks: list[Task]) -> ScheduleCenterCommanderSummaryOut: counts = ScheduleCenterCommanderSummaryOut() for task in tasks: states = [task.dispatch_status, *(subtask.dispatch_status for subtask in task.subtasks)] for state in states: normalized = _coerce_enum(state, TaskDispatchStatus, TaskDispatchStatus.IDLE) if normalized == TaskDispatchStatus.IDLE: continue counts.total += 1 if normalized == TaskDispatchStatus.QUEUED: counts.queued += 1 elif normalized == TaskDispatchStatus.RUNNING: counts.running += 1 elif normalized == TaskDispatchStatus.COMPLETED: counts.completed += 1 elif normalized == TaskDispatchStatus.FAILED: counts.failed += 1 if counts.running > 0: counts.overall_status = "running" elif counts.queued > 0: counts.overall_status = "queued" elif counts.failed > 0 and counts.completed == 0: counts.overall_status = "failed" return counts @router.get("/month", response_model=ScheduleCenterMonthOut) async def get_month_schedule( year: int = Query(..., ge=2000, le=2100), month: int = Query(..., ge=1, le=12), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): month_start = date(year, month, 1) days_in_month = monthrange(month_start.year, month_start.month)[1] start_key = month_start.isoformat() end_key = month_start.replace(day=days_in_month).isoformat() start_dt = datetime.combine(month_start, datetime.min.time()) end_dt = datetime.combine(month_start.replace(day=days_in_month), datetime.max.time()) todos = ( await db.execute( select(DailyTodo).where( DailyTodo.user_id == current_user.id, DailyTodo.todo_date >= start_key, DailyTodo.todo_date <= end_key, ) ) ).scalars().all() tasks = ( await db.execute( select(Task).where( Task.user_id == current_user.id, Task.due_date.is_not(None), Task.due_date >= start_dt, Task.due_date <= end_dt, ) ) ).scalars().all() reminders = ( await db.execute( select(Reminder).where( Reminder.user_id == current_user.id, Reminder.reminder_at >= start_dt, Reminder.reminder_at <= end_dt, ) ) ).scalars().all() goals = ( await db.execute( select(Goal).where( Goal.user_id == current_user.id, Goal.goal_date >= start_key, Goal.goal_date <= end_key, ) ) ).scalars().all() todo_map: dict[str, list[DailyTodo]] = {} for item in todos: todo_map.setdefault(item.todo_date, []).append(item) task_map: dict[str, list[Task]] = {} for item in tasks: key = item.due_date.date().isoformat() task_map.setdefault(key, []).append(item) reminder_map: dict[str, list[Reminder]] = {} for item in reminders: key = item.reminder_at.date().isoformat() reminder_map.setdefault(key, []).append(item) goal_map: dict[str, list[Goal]] = {} for item in goals: goal_map.setdefault(item.goal_date, []).append(item) days = [] for day in range(1, days_in_month + 1): date_key = month_start.replace(day=day).isoformat() days.append( _build_summary( date_key, todo_map.get(date_key, []), task_map.get(date_key, []), reminder_map.get(date_key, []), goal_map.get(date_key, []), ) ) return ScheduleCenterMonthOut(month=f"{year:04d}-{month:02d}", days=days) @router.get("/date", response_model=ScheduleCenterDateOut, response_model_exclude_none=True) async def get_date_schedule( date_str: date = Query(...), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): target_date = date_str start_dt = datetime.combine(target_date, datetime.min.time()) end_dt = datetime.combine(target_date, datetime.max.time()) date_key = target_date.isoformat() todos = ( await db.execute( select(DailyTodo) .where(DailyTodo.user_id == current_user.id, DailyTodo.todo_date == date_key) .order_by(DailyTodo.created_at.desc()) ) ).scalars().all() tasks = ( await db.execute( select(Task) .options(selectinload(Task.subtasks), selectinload(Task.history)) .where( Task.user_id == current_user.id, Task.due_date.is_not(None), Task.due_date >= start_dt, Task.due_date <= end_dt, ) .order_by(Task.priority.desc(), Task.created_at.desc()) ) ).scalars().unique().all() reminders = ( await db.execute( select(Reminder) .where( Reminder.user_id == current_user.id, Reminder.reminder_at >= start_dt, Reminder.reminder_at <= end_dt, ) .order_by(Reminder.reminder_at.asc(), Reminder.created_at.asc()) ) ).scalars().all() goals = ( await db.execute( select(Goal) .where(Goal.user_id == current_user.id, Goal.goal_date == date_key) .order_by(Goal.created_at.desc()) ) ).scalars().all() summary = _build_summary(date_key, todos, tasks, reminders, goals) return ScheduleCenterDateOut( date=date_key, todos=todos, tasks=[build_task_out(task) for task in tasks], reminders=reminders, goals=goals, summary=summary, focus_tasks=_build_focus_tasks(tasks), quadrants=_build_quadrants(tasks), commander_summary=_build_commander_summary(tasks), generated_at=datetime.now(UTC), )