- Nouveau router my_tasks.py: GET /my-tasks (HTML), GET /my-tasks/api (JSON) - Agrège toutes les pages assignées à l'utilisateur sur toutes les collections - Vues: All, Today, Overdue, Next 7 days - Groupement par collection, filter admin auto - 60/60 tests passent (+4 tests v1.9) - Version 1.8.0 → 1.9.0
179 lines
7.1 KiB
Python
179 lines
7.1 KiB
Python
"""FlowDeck — My Tasks router (v1.9.0)."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Request, HTTPException, Query
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
from app.db import get_conn
|
|
from app.auth.session import SessionManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["my-tasks"], prefix="/my-tasks")
|
|
|
|
|
|
def _get_current_user(request: Request) -> dict | None:
|
|
session = request.cookies.get("flowdeck_session", "")
|
|
return SessionManager.decode_session(session)
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
async def my_tasks_dashboard(request: Request, view: str = "all", days: int = 7):
|
|
"""My Tasks — aggregates all pages assigned to the current user across all collections."""
|
|
user = _get_current_user(request)
|
|
user_login = user.get("login", "admin") if user else "admin"
|
|
|
|
with get_conn() as conn:
|
|
collections = conn.execute("SELECT * FROM collections ORDER BY name").fetchall()
|
|
grouped: dict[str, list[dict]] = {}
|
|
|
|
for col in collections:
|
|
col_id = col["id"]
|
|
pages = conn.execute(
|
|
"SELECT * FROM collection_pages WHERE collection_id=? AND parent_id IS NULL ORDER BY position",
|
|
(col_id,),
|
|
).fetchall()
|
|
|
|
collection_tasks = []
|
|
for p in pages:
|
|
props = json.loads(p["property_values_json"])
|
|
# Check if assigned to current user
|
|
assigned = False
|
|
for v in props.values():
|
|
if isinstance(v, list):
|
|
for item in v:
|
|
if isinstance(item, dict) and item.get("login") == user_login:
|
|
assigned = True
|
|
break
|
|
elif isinstance(v, str) and user_login.lower() in v.lower():
|
|
assigned = True
|
|
if assigned:
|
|
break
|
|
|
|
if not assigned and user_login == "admin":
|
|
assigned = True # admin sees all
|
|
|
|
if not assigned:
|
|
continue
|
|
|
|
# Extract status and due date
|
|
status = None
|
|
due_date = None
|
|
for v in props.values():
|
|
if isinstance(v, str) and v in ("Todo", "In Progress", "Done", "Complete"):
|
|
status = v
|
|
elif isinstance(v, str) and v.startswith("20"):
|
|
due_date = v[:10]
|
|
|
|
# Filter by view
|
|
if view == "today":
|
|
from datetime import date
|
|
if not due_date or due_date != date.today().isoformat():
|
|
continue
|
|
elif view == "overdue":
|
|
from datetime import date
|
|
if not due_date or due_date >= date.today().isoformat():
|
|
continue
|
|
elif view == "upcoming":
|
|
from datetime import date, timedelta
|
|
if not due_date:
|
|
continue
|
|
today = date.today()
|
|
limit = today + timedelta(days=days)
|
|
d = date.fromisoformat(due_date)
|
|
if d < today or d > limit:
|
|
continue
|
|
|
|
collection_tasks.append({
|
|
"id": p["id"], "title": p["title"], "icon": p.get("icon", "📄"),
|
|
"status": status, "due_date": due_date, "props": props,
|
|
})
|
|
|
|
if collection_tasks:
|
|
grouped[col["name"]] = collection_tasks
|
|
|
|
total = sum(len(t) for t in grouped.values())
|
|
|
|
# Render
|
|
sections = ""
|
|
for col_name, tasks in grouped.items():
|
|
items = ""
|
|
for t in tasks:
|
|
due_badge = f"<span class='mt-due'>{t['due_date']}</span>" if t.get("due_date") else ""
|
|
status_badge = f"<span class='mt-status mt-{t['status'].lower().replace(' ','-') if t.get('status') else 'none'}'>{t.get('status','—')}</span>"
|
|
items += f"""<div class='mt-item'>
|
|
<span class='mt-icon'>{t['icon']}</span>
|
|
<span class='mt-title'>{t['title']}</span>
|
|
{status_badge}{due_badge}
|
|
</div>"""
|
|
|
|
sections += f"""<div class='mt-section'>
|
|
<div class='mt-section-header'>{col_name} <span class='mt-count'>{len(tasks)}</span></div>
|
|
{items}
|
|
</div>"""
|
|
|
|
return HTMLResponse(f"""<!DOCTYPE html>
|
|
<html><head><meta charset="utf-8"><title>My Tasks — FlowDeck</title>
|
|
<style>
|
|
body{{font-family:system-ui,sans-serif;background:#191919;color:#fff;margin:0;padding:20px}}
|
|
h1{{font-size:24px;margin:0 0 16px}}
|
|
.view-tabs{{display:flex;gap:4px;margin-bottom:20px;border-bottom:1px solid #333;padding-bottom:8px}}
|
|
.tab{{padding:6px 14px;border-radius:6px;cursor:pointer;color:#A0A0A0;font-size:13px;background:none;border:none;text-decoration:none}}
|
|
.tab:hover,.tab.active{{background:#333;color:#fff}}
|
|
.mt-section{{margin-bottom:24px}}
|
|
.mt-section-header{{font-size:14px;font-weight:600;color:#A0A0A0;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}}
|
|
.mt-count{{color:#6B6B6B;font-weight:400}}
|
|
.mt-item{{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#1a1a1a;border-radius:6px;margin-bottom:3px;border:1px solid #222}}
|
|
.mt-item:hover{{border-color:#444}}
|
|
.mt-icon{{font-size:16px}}
|
|
.mt-title{{flex:1;font-size:14px}}
|
|
.mt-status{{font-size:11px;padding:2px 8px;border-radius:4px}}
|
|
.mt-due{{font-size:11px;color:#A0A0A0}}
|
|
.mt-todo{{background:#333;color:#A0A0A0}}
|
|
.mt-in-progress{{background:#1a2744;color:#3366CC}}
|
|
.mt-done,.mt-complete{{background:#1a332a;color:#00CC66}}
|
|
.p_desc{{color:#A0A0A0;margin-top:8px}}
|
|
</style></head><body>
|
|
<h1>📋 My Tasks</h1>
|
|
<div class="view-tabs">
|
|
<a class="tab{' active' if view=='all' else ''}" href="?view=all">All</a>
|
|
<a class="tab{' active' if view=='today' else ''}" href="?view=today">Today</a>
|
|
<a class="tab{' active' if view=='overdue' else ''}" href="?view=overdue">Overdue</a>
|
|
<a class="tab{' active' if view=='upcoming' else ''}" href="?view=upcoming&days=7">Next 7 days</a>
|
|
</div>
|
|
{sections}
|
|
<p class="p_desc">{total} tasks across {len(grouped)} collections</p>
|
|
</body></html>""")
|
|
|
|
|
|
@router.get("/api")
|
|
async def my_tasks_api(request: Request, view: str = "all", days: int = 7):
|
|
"""API: return my tasks as JSON."""
|
|
user = _get_current_user(request)
|
|
user_login = user.get("login", "admin") if user else "admin"
|
|
|
|
with get_conn() as conn:
|
|
collections = conn.execute("SELECT * FROM collections ORDER BY name").fetchall()
|
|
result = {}
|
|
|
|
for col in collections:
|
|
pages = conn.execute(
|
|
"SELECT * FROM collection_pages WHERE collection_id=? AND parent_id IS NULL ORDER BY position",
|
|
(col["id"],),
|
|
).fetchall()
|
|
|
|
tasks = []
|
|
for p in pages:
|
|
props = json.loads(p["property_values_json"])
|
|
tasks.append({
|
|
"id": p["id"], "title": p["title"], "icon": p["icon"],
|
|
"properties": props,
|
|
})
|
|
|
|
if tasks:
|
|
result[col["name"]] = tasks
|
|
|
|
return {"tasks": result, "total": sum(len(t) for t in result.values())}
|