feat: My Tasks dans base.html (sidebar visible) + Help page complète

- /my-tasks: wrappé dans base.html pour garder le sidebar visible
- /help: page d'aide complète avec 5 sections (démarrage, pages, workspaces, intégrations, raccourcis)
- Style élégant Notion dark avec cartes, grille 2 colonnes, badges
- 143 tests passent
This commit is contained in:
2026-07-20 10:46:24 -04:00
parent d5ba29714c
commit 0d9c1bf9ee
+58 -91
View File
@@ -6,6 +6,7 @@ import logging
from fastapi import APIRouter, Request, HTTPException, Query from fastapi import APIRouter, Request, HTTPException, Query
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from jinja2 import Environment, FileSystemLoader
from app.db import get_conn from app.db import get_conn
from app.auth.session import SessionManager from app.auth.session import SessionManager
@@ -38,114 +39,80 @@ async def my_tasks_dashboard(request: Request, view: str = "all", days: int = 7)
collection_tasks = [] collection_tasks = []
for p in pages: for p in pages:
props = json.loads(p["property_values_json"]) props = {}
# Check if assigned to current user try:
assigned = False props = json.loads(p["property_values_json"] or "{}")
for v in props.values(): except (json.JSONDecodeError, TypeError):
if isinstance(v, list): pass
for item in v: due_date = props.get("due_date", "")
if isinstance(item, dict) and item.get("login") == user_login: status = props.get("status", "—")
assigned = True assignee = props.get("assignee", "")
break if view != "all" and assignee and assignee != user_login:
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 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({ collection_tasks.append({
"id": p["id"], "title": p["title"], "icon": p.get("icon", "📄"), "id": p["id"],
"status": status, "due_date": due_date, "props": props, "title": p["title"] or "Untitled",
"icon": p["icon"] or "📋",
"status": status,
"due_date": due_date,
}) })
if collection_tasks: if collection_tasks:
grouped[col["name"]] = collection_tasks grouped[col["name"]] = collection_tasks
total = sum(len(t) for t in grouped.values()) total = sum(len(t) for t in grouped.values())
# Render # Build content HTML
sections = "" sections = ""
for col_name, tasks in grouped.items(): for col_name, tasks in grouped.items():
items = "" items = ""
for t in tasks: for t in tasks:
due_badge = f"<span class='mt-due'>{t['due_date']}</span>" if t.get("due_date") else "" 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>" status_cls = t['status'].lower().replace(' ', '-') if t.get('status') else 'none'
items += f"""<div class='mt-item'> status_badge = f"<span class='mt-status mt-{status_cls}'>{t.get('status', '—')}</span>"
<span class='mt-icon'>{t['icon']}</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>"
<span class='mt-title'>{t['title']}</span>
{status_badge}{due_badge}
</div>"""
sections += f"""<div class='mt-section'> sections += f"<div class='mt-section'><div class='mt-section-header'>{col_name} <span class='mt-count'>{len(tasks)}</span></div>{items}</div>"
<div class='mt-section-header'>{col_name} <span class='mt-count'>{len(tasks)}</span></div>
{items}
</div>"""
return HTMLResponse(f"""<!DOCTYPE html> content_html = f"""<div style="max-width:800px;margin:0 auto;padding:40px 24px;">
<html><head><meta charset="utf-8"><title>My Tasks — FlowDeck</title> <h1 style="font-size:24px;margin:0 0 8px;">📋 My Tasks</h1>
<style> <p style="color:var(--text-dim);font-size:14px;margin-bottom:24px;">{total} tasks across {len(grouped)} collections</p>
body{{font-family:system-ui,sans-serif;background:#191919;color:#fff;margin:0;padding:20px}} <div class="view-tabs" style="display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid var(--border);padding-bottom:12px;">
h1{{font-size:24px;margin:0 0 16px}} <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>
.view-tabs{{display:flex;gap:4px;margin-bottom:20px;border-bottom:1px solid #333;padding-bottom:8px}} <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>
.tab{{padding:6px 14px;border-radius:6px;cursor:pointer;color:#A0A0A0;font-size:13px;background:none;border:none;text-decoration:none}} <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>
.tab:hover,.tab.active{{background:#333;color:#fff}} <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>
.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> </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} {sections}
<p class="p_desc">{total} tasks across {len(grouped)} collections</p> </div>"""
</body></html>""")
from app.routers.dashboard import _sidebar_data
env = Environment(loader=FileSystemLoader("app/templates"))
sidebar = _sidebar_data(request, [])
template = env.get_template("base.html")
return template.render(
**sidebar,
request=request,
content_html=content_html,
page_title="My Tasks",
title_prefix="My Tasks",
page_icon="📋",
)
@router.get("/api") @router.get("/api")