2026-03-29 20:31:13 +08:00
|
|
|
from calendar import monthrange
|
|
|
|
|
from datetime import UTC, date, datetime
|
|
|
|
|
|
2026-04-11 08:47:39 +08:00
|
|
|
from fastapi import APIRouter, Depends, Query
|
2026-03-29 20:31:13 +08:00
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
2026-04-11 08:47:39 +08:00
|
|
|
from sqlalchemy.orm import selectinload
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
|
|
|
from app.database import get_db
|
|
|
|
|
from app.models.goal import Goal
|
|
|
|
|
from app.models.reminder import Reminder
|
2026-04-11 08:47:39 +08:00
|
|
|
from app.models.task import Task, TaskDispatchStatus, TaskPriority, TaskQuadrant, TaskStatus
|
2026-03-29 20:31:13 +08:00
|
|
|
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 (
|
2026-04-11 08:47:39 +08:00
|
|
|
ScheduleCenterCommanderSummaryOut,
|
2026-03-29 20:31:13 +08:00
|
|
|
ScheduleCenterDateOut,
|
|
|
|
|
ScheduleCenterDaySummary,
|
2026-04-11 08:47:39 +08:00
|
|
|
ScheduleCenterFocusTaskOut,
|
2026-03-29 20:31:13 +08:00
|
|
|
ScheduleCenterMonthOut,
|
2026-04-11 08:47:39 +08:00
|
|
|
ScheduleCenterQuadrantOut,
|
|
|
|
|
ScheduleCenterQuadrantTaskOut,
|
2026-03-29 20:31:13 +08:00
|
|
|
)
|
2026-04-11 08:47:39 +08:00
|
|
|
from app.schemas.task import build_task_out
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/schedule-center", tags=["调度中心"])
|
|
|
|
|
|
2026-04-11 08:47:39 +08:00
|
|
|
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": "○",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-11 08:47:39 +08:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-03-29 20:31:13 +08:00
|
|
|
@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())
|
|
|
|
|
|
2026-04-11 08:47:39 +08:00
|
|
|
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,
|
|
|
|
|
)
|
2026-03-29 20:31:13 +08:00
|
|
|
)
|
2026-04-11 08:47:39 +08:00
|
|
|
).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,
|
|
|
|
|
)
|
2026-03-29 20:31:13 +08:00
|
|
|
)
|
2026-04-11 08:47:39 +08:00
|
|
|
).scalars().all()
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
|
|
|
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()
|
2026-04-11 08:47:39 +08:00
|
|
|
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, []),
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
|
|
|
return ScheduleCenterMonthOut(month=f"{year:04d}-{month:02d}", days=days)
|
|
|
|
|
|
|
|
|
|
|
2026-04-11 08:47:39 +08:00
|
|
|
@router.get("/date", response_model=ScheduleCenterDateOut, response_model_exclude_none=True)
|
2026-03-29 20:31:13 +08:00
|
|
|
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()
|
|
|
|
|
|
2026-04-11 08:47:39 +08:00
|
|
|
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())
|
2026-03-29 20:31:13 +08:00
|
|
|
)
|
2026-04-11 08:47:39 +08:00
|
|
|
).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())
|
2026-03-29 20:31:13 +08:00
|
|
|
)
|
2026-04-11 08:47:39 +08:00
|
|
|
).scalars().all()
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
|
|
|
summary = _build_summary(date_key, todos, tasks, reminders, goals)
|
|
|
|
|
return ScheduleCenterDateOut(
|
|
|
|
|
date=date_key,
|
|
|
|
|
todos=todos,
|
2026-04-11 08:47:39 +08:00
|
|
|
tasks=[build_task_out(task) for task in tasks],
|
2026-03-29 20:31:13 +08:00
|
|
|
reminders=reminders,
|
|
|
|
|
goals=goals,
|
|
|
|
|
summary=summary,
|
2026-04-11 08:47:39 +08:00
|
|
|
focus_tasks=_build_focus_tasks(tasks),
|
|
|
|
|
quadrants=_build_quadrants(tasks),
|
|
|
|
|
commander_summary=_build_commander_summary(tasks),
|
2026-03-29 20:31:13 +08:00
|
|
|
generated_at=datetime.now(UTC),
|
|
|
|
|
)
|