- Refactored TimerPage component to improve layout and user experience. - Updated theme configuration to utilize design tokens for better maintainability. - Introduced new tokens for colors and typography to align with design principles. - Simplified state management and improved timer functionality. - Enhanced accessibility and responsiveness of UI elements.
120 lines
4.3 KiB
Python
120 lines
4.3 KiB
Python
"""Task endpoints: the aggregated list and the refresh that keeps it honest.
|
|
|
|
There is no "mark done" here on purpose — a task is closed where it lives, and
|
|
the refresh notices. See `app.services.tasks_service`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
from app.api.deps import DbSession, Settings
|
|
from app.api.schemas import TaskCreateRequest, TaskUpdateRequest
|
|
from app.models import TaskRecord
|
|
from app.services import tasks_service
|
|
from app.sources.registry import SOURCE_LABELS, check_source
|
|
from app.timeutil import ensure_utc, local_date
|
|
|
|
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
|
|
|
|
|
|
def task_dict(record: TaskRecord, today) -> dict:
|
|
return {
|
|
"id": record.id,
|
|
"source": record.source,
|
|
"source_label": SOURCE_LABELS.get(record.source, record.source),
|
|
"external_id": record.external_id,
|
|
"title": record.title,
|
|
"description": record.description,
|
|
"url": record.url,
|
|
"project": record.project,
|
|
"status": record.status,
|
|
"priority": record.priority,
|
|
"due_date": record.due_date.isoformat() if record.due_date else None,
|
|
"start_date": record.start_date.isoformat() if record.start_date else None,
|
|
"labels": record.labels or [],
|
|
"is_done": record.is_done,
|
|
"completed_at": ensure_utc(record.completed_at).isoformat() if record.completed_at else None,
|
|
"pinned": record.pinned,
|
|
"stale": record.stale,
|
|
"estimated_pomodoros": record.estimated_pomodoros,
|
|
"days_overdue": (today - record.due_date).days if record.due_date and record.due_date < today else 0,
|
|
"bucket": _bucket_of(record, today),
|
|
}
|
|
|
|
|
|
def _bucket_of(record: TaskRecord, today) -> str:
|
|
if record.due_date is not None and record.due_date < today:
|
|
return "overdue"
|
|
if record.due_date == today:
|
|
return "today"
|
|
if record.start_date is not None and record.start_date <= today:
|
|
# Started, but not due today — in progress rather than "upcoming".
|
|
return "active"
|
|
if record.due_date is None:
|
|
return "someday"
|
|
return "upcoming"
|
|
|
|
|
|
@router.get("")
|
|
def list_tasks(
|
|
session: DbSession,
|
|
settings: Settings,
|
|
include_done: bool = Query(default=False),
|
|
include_stale: bool = Query(default=False),
|
|
source: list[str] | None = Query(default=None),
|
|
) -> dict:
|
|
today = local_date(settings.timezone)
|
|
records = tasks_service.list_tasks(
|
|
session, include_done=include_done, include_stale=include_stale, sources=source
|
|
)
|
|
buckets = tasks_service.bucket_tasks(records, today)
|
|
return {
|
|
"date": today.isoformat(),
|
|
"tasks": [task_dict(record, today) for record in records],
|
|
"counts": {name: len(items) for name, items in buckets.items()},
|
|
"sources": sorted({record.source for record in records}),
|
|
}
|
|
|
|
|
|
@router.post("/refresh")
|
|
def refresh(session: DbSession, settings: Settings) -> dict:
|
|
"""Pull every enabled source now. Failures come back per source, not as a 500."""
|
|
return tasks_service.refresh_tasks(session, settings)
|
|
|
|
|
|
@router.post("")
|
|
def create(body: TaskCreateRequest, session: DbSession, settings: Settings) -> dict:
|
|
record = tasks_service.create_local_task(
|
|
session,
|
|
title=body.title,
|
|
description=body.description,
|
|
due_date=body.due_date,
|
|
priority=body.priority,
|
|
project=body.project,
|
|
estimated_pomodoros=body.estimated_pomodoros,
|
|
)
|
|
return task_dict(record, local_date(settings.timezone))
|
|
|
|
|
|
@router.patch("/{task_id}")
|
|
def update(task_id: int, body: TaskUpdateRequest, session: DbSession, settings: Settings) -> dict:
|
|
try:
|
|
record = tasks_service.update_task(session, task_id, **body.model_dump(exclude_unset=True))
|
|
except LookupError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return task_dict(record, local_date(settings.timezone))
|
|
|
|
|
|
@router.delete("/{task_id}")
|
|
def delete(task_id: int, session: DbSession) -> dict:
|
|
tasks_service.delete_task(session, task_id)
|
|
return {"deleted": task_id}
|
|
|
|
|
|
@router.post("/sources/{name}/test")
|
|
def test_source(name: str, settings: Settings) -> dict:
|
|
"""What the settings page's "Test connection" button calls."""
|
|
ok, detail = check_source(settings, name)
|
|
return {"source": name, "ok": ok, "detail": detail}
|