- main.py: lifespan insère toujours le hash de 'FlowDeck2026!' pour admin
- dashboard.py + my_tasks.py: content_html passe via {% block content %} (Jinja)
- Avant: content_html ignoré car base.html utilise des blocs, pas des variables
- 143 tests passent
148 lines
6.7 KiB
Python
148 lines
6.7 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 jinja2 import Environment, FileSystemLoader
|
|
|
|
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 = {}
|
|
try:
|
|
props = json.loads(p["property_values_json"] or "{}")
|
|
except (json.JSONDecodeError, TypeError):
|
|
pass
|
|
due_date = props.get("due_date", "")
|
|
status = props.get("status", "—")
|
|
assignee = props.get("assignee", "")
|
|
if view != "all" and assignee and assignee != user_login:
|
|
continue
|
|
collection_tasks.append({
|
|
"id": p["id"],
|
|
"title": p["title"] or "Untitled",
|
|
"icon": p["icon"] or "📋",
|
|
"status": status,
|
|
"due_date": due_date,
|
|
})
|
|
if collection_tasks:
|
|
grouped[col["name"]] = collection_tasks
|
|
|
|
total = sum(len(t) for t in grouped.values())
|
|
|
|
# Build content HTML
|
|
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_cls = t['status'].lower().replace(' ', '-') if t.get('status') else 'none'
|
|
status_badge = f"<span class='mt-status mt-{status_cls}'>{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>"
|
|
|
|
content_html = f"""<div style="max-width:800px;margin:0 auto;padding:40px 24px;">
|
|
<h1 style="font-size:24px;margin:0 0 8px;">📋 My Tasks</h1>
|
|
<p style="color:var(--text-dim);font-size:14px;margin-bottom:24px;">{total} tasks across {len(grouped)} collections</p>
|
|
<div class="view-tabs" style="display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid var(--border);padding-bottom:12px;">
|
|
<a class="tab{' active' if view=='all' else ''}" href="/my-tasks?view=all" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">All</a>
|
|
<a class="tab{' active' if view=='today' else ''}" href="/my-tasks?view=today" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Today</a>
|
|
<a class="tab{' active' if view=='overdue' else ''}" href="/my-tasks?view=overdue" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Overdue</a>
|
|
<a class="tab{' active' if view=='upcoming' else ''}" href="/my-tasks?view=upcoming&days=7" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Next 7 days</a>
|
|
</div>
|
|
<style>
|
|
.tab.active{{background:var(--accent);color:#fff!important;}}
|
|
.tab:hover{{background:var(--bg-hover);color:var(--text);}}
|
|
.mt-section{{margin-bottom:24px;}}
|
|
.mt-section-header{{font-size:13px;font-weight:600;color:var(--text-dim);margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px;}}
|
|
.mt-count{{color:var(--text-dim);font-weight:400;opacity:.5;}}
|
|
.mt-item{{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg-card);border-radius:8px;margin-bottom:3px;border:1px solid var(--border);}}
|
|
.mt-item:hover{{border-color:var(--accent);}}
|
|
.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:var(--text-dim);}}
|
|
.mt-todo,.mt-none{{background:rgba(255,255,255,.05);color:var(--text-dim);}}
|
|
.mt-in-progress{{background:rgba(35,131,226,.15);color:#2C8CEB;}}
|
|
.mt-done,.mt-completed,.mt-complete{{background:rgba(0,200,100,.15);color:#00CC66;}}
|
|
</style>
|
|
{sections}
|
|
</div>"""
|
|
|
|
from app.routers.dashboard import _sidebar_data
|
|
env = Environment(loader=FileSystemLoader("app/templates"))
|
|
sidebar = _sidebar_data(request, [])
|
|
block_tpl = env.from_string(
|
|
'{% extends "base.html" %}{% block content %}{{ content_html|safe }}{% endblock %}'
|
|
)
|
|
return block_tpl.render(
|
|
**sidebar,
|
|
request=request,
|
|
content_html=content_html,
|
|
page_title="My Tasks",
|
|
title_prefix="My Tasks",
|
|
page_icon="📋",
|
|
)
|
|
|
|
|
|
@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())}
|