from dataclasses import replace from typing import List, Sequence from .todo_item import Todo from .todo_item_validate_draft import is_iso_date from .todo_list_add_todo import require_utc_now from .todo_list_update_todo import index_of_todo from .todo_next_occurrence import next_occurrence def require_today(today: str) -> None: """Refuse a today that is not a real YYYY-MM-DD date, whether or not a repeat needs it.""" if not is_iso_date(today): raise ValueError("today must be a date like 2026-09-28") def complete_todo(todo: Todo, now: str, today: str) -> Todo: """Complete one open todo; a repeating one stays open and rolls forward to its next occurrence.""" if todo.recurrence is None: return replace(todo, done=True, completed_at=now) if todo.due is None: raise ValueError(f'repeating todo "{todo.id}" has no due date') return replace(todo, done=False, completed_at=None, due=next_occurrence(todo.recurrence, todo.due, today)) def toggle_todo(todos: Sequence[Todo], id: str, now: str, today: str) -> List[Todo]: """Tick or untick one todo. Reopening clears the completion time.""" at = index_of_todo(todos, id) require_utc_now(now) require_today(today) out = list(todos) t = todos[at] out[at] = replace(t, done=False, completed_at=None) if t.done else complete_todo(t, now, today) return out