from datetime import date from quart import Blueprint, jsonify, request from fabledassistant.auth import login_required, get_current_user_id from fabledassistant.models.note import TaskPriority, TaskStatus from fabledassistant.services.notes import ( create_note, delete_note, get_note, list_notes, update_note, ) from fabledassistant.utils.tags import extract_tags tasks_bp = Blueprint("tasks", __name__, url_prefix="/api/tasks") @tasks_bp.route("", methods=["GET"]) @login_required async def list_tasks_route(): uid = get_current_user_id() q = request.args.get("q") tag = request.args.getlist("tag") status = request.args.get("status") priority = request.args.get("priority") sort = request.args.get("sort", "updated_at") order = request.args.get("order", "desc") limit = request.args.get("limit", 50, type=int) offset = request.args.get("offset", 0, type=int) due_before_str = request.args.get("due_before") due_after_str = request.args.get("due_after") due_before = date.fromisoformat(due_before_str) if due_before_str else None due_after = date.fromisoformat(due_after_str) if due_after_str else None tasks, total = await list_notes( uid, q=q, tags=tag or None, is_task=True, status=status, priority=priority, due_before=due_before, due_after=due_after, sort=sort, order=order, limit=limit, offset=offset, ) return jsonify({"tasks": [t.to_dict() for t in tasks], "total": total}) @tasks_bp.route("", methods=["POST"]) @login_required async def create_task_route(): uid = get_current_user_id() data = await request.get_json() body = data.get("body", "") or data.get("description", "") tags = extract_tags(body) due_date = None if data.get("due_date"): due_date = date.fromisoformat(data["due_date"]) status = TaskStatus(data["status"]).value if "status" in data else TaskStatus.todo.value priority = ( TaskPriority(data["priority"]).value if "priority" in data else TaskPriority.none.value ) task = await create_note( uid, title=data.get("title", ""), body=body, status=status, priority=priority, due_date=due_date, tags=tags, ) return jsonify(task.to_dict()), 201 @tasks_bp.route("/", methods=["GET"]) @login_required async def get_task_route(task_id: int): uid = get_current_user_id() task = await get_note(uid, task_id) if task is None: return jsonify({"error": "Task not found"}), 404 return jsonify(task.to_dict()) @tasks_bp.route("/", methods=["PUT"]) @login_required async def update_task_route(task_id: int): uid = get_current_user_id() data = await request.get_json() fields = {} for key in ("title", "status", "priority"): if key in data: fields[key] = data[key] # Accept both "body" and "description" (prefer body) if "body" in data: fields["body"] = data["body"] elif "description" in data: fields["body"] = data["description"] if "due_date" in data: fields["due_date"] = ( date.fromisoformat(data["due_date"]) if data["due_date"] else None ) if "body" in fields: fields["tags"] = extract_tags(fields["body"]) task = await update_note(uid, task_id, **fields) if task is None: return jsonify({"error": "Task not found"}), 404 return jsonify(task.to_dict()) @tasks_bp.route("//status", methods=["PATCH"]) @login_required async def patch_task_status(task_id: int): uid = get_current_user_id() data = await request.get_json() status_val = data.get("status") if not status_val: return jsonify({"error": "status is required"}), 400 try: TaskStatus(status_val) except ValueError: return jsonify({"error": f"Invalid status: {status_val}"}), 400 task = await update_note(uid, task_id, status=status_val) if task is None: return jsonify({"error": "Task not found"}), 404 return jsonify(task.to_dict()) @tasks_bp.route("/", methods=["DELETE"]) @login_required async def delete_task_route(task_id: int): uid = get_current_user_id() deleted = await delete_note(uid, task_id) if not deleted: return jsonify({"error": "Task not found"}), 404 return "", 204