Erledigte Aufgaben
+Was schon geschafft wurde
+Hier landen alle erledigten Aufgaben. Du kannst pro Person sehen, was heute, gestern, vorgestern und davor erledigt wurde.
+diff --git a/app/__init__.py b/app/__init__.py index ead05df..57483f0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -90,6 +90,7 @@ def create_app(config_class: type[Config] = Config) -> Flask: "nav_items": [ ("tasks.my_tasks", "Meine Aufgaben", "house"), ("tasks.all_tasks", "Alle", "list"), + ("tasks.archive_view", "Archiv", "check-double"), ("tasks.create", "Neu", "plus"), ("tasks.calendar_view", "Kalender", "calendar"), ("scoreboard.index", "Highscore", "trophy"), diff --git a/app/routes/tasks.py b/app/routes/tasks.py index 857ca2c..4129189 100644 --- a/app/routes/tasks.py +++ b/app/routes/tasks.py @@ -2,7 +2,7 @@ from __future__ import annotations import calendar from collections import defaultdict -from datetime import date +from datetime import date, timedelta from flask import Blueprint, flash, redirect, render_template, request, url_for from flask_login import current_user, login_required @@ -37,15 +37,72 @@ def _my_tasks_soon_priority(task: TaskInstance) -> int: order = { "due_tomorrow": 0, "due_day_after_tomorrow": 1, - "due_today": 2, + "open": 2, } return order.get(task.status, 99) +def _task_sort_key(task: TaskInstance, sort: str) -> tuple: + if sort == "points": + return (-task.points_awarded, task.due_date, task.title.lower()) + if sort == "user": + return (task.assignee_label.lower(), task.due_date, task.title.lower()) + return (task.due_date, task.title.lower()) + + +def _group_active_tasks(tasks: list[TaskInstance], *, sort: str = "due") -> dict[str, list[TaskInstance]]: + sections = { + "overdue": [], + "today": [], + "soon": [], + "open": [], + } + + for task in tasks: + if task.is_completed: + continue + if task.status == "overdue": + sections["overdue"].append(task) + elif task.status == "due_today": + sections["today"].append(task) + elif task.status in {"due_tomorrow", "due_day_after_tomorrow"}: + sections["soon"].append(task) + else: + sections["open"].append(task) + + sections["overdue"].sort(key=lambda task: _task_sort_key(task, sort)) + sections["today"].sort(key=lambda task: _task_sort_key(task, sort)) + sections["soon"].sort( + key=lambda task: (_my_tasks_soon_priority(task),) + _task_sort_key(task, sort) + ) + sections["open"].sort(key=lambda task: _task_sort_key(task, sort)) + return sections + + +def _archive_day_priority(day: date, today: date) -> tuple[int, int]: + if day == today: + return (0, 0) + if day == today - timedelta(days=1): + return (1, 0) + if day == today - timedelta(days=2): + return (2, 0) + return (3, -day.toordinal()) + + +def _archive_day_label(day: date, today: date) -> str: + if day == today: + return "Heute" + if day == today - timedelta(days=1): + return "Gestern" + if day == today - timedelta(days=2): + return "Vorgestern" + return day.strftime("%d.%m.%Y") + + @bp.route("/my-tasks") @login_required def my_tasks(): - tasks = ( + all_tasks = ( TaskInstance.query.filter( or_( TaskInstance.assigned_user_id == current_user.id, @@ -55,38 +112,21 @@ def my_tasks(): .order_by(TaskInstance.completed_at.is_(None).desc(), TaskInstance.due_date.asc()) .all() ) - refresh_task_statuses(tasks) + refresh_task_statuses(all_tasks) + sections = _group_active_tasks(all_tasks) - sections = { - "open": [], - "due_today": [], - "due_tomorrow": [], - "due_day_after_tomorrow": [], - "overdue": [], - "completed": [], - } - for task in tasks: - sections[task.status].append(task) - - soon_tasks = sorted( - sections["due_tomorrow"] + sections["due_day_after_tomorrow"] + sections["due_today"], - key=lambda task: (_my_tasks_soon_priority(task), task.due_date, task.title.lower()), - ) - - completed_count = len(sections["completed"]) + completed_count = len([task for task in all_tasks if task.is_completed]) active_count = ( - len(sections["open"]) - + len(sections["due_today"]) - + len(sections["due_tomorrow"]) - + len(sections["due_day_after_tomorrow"]) - + len(sections["overdue"]) + len(sections["overdue"]) + + len(sections["today"]) + + len(sections["soon"]) + + len(sections["open"]) ) completion_ratio = 0 if completed_count + active_count == 0 else round(completed_count / (completed_count + active_count) * 100) return render_template( "tasks/my_tasks.html", sections=sections, - soon_tasks=soon_tasks, completion_ratio=completion_ratio, today=today_local(), ) @@ -128,10 +168,9 @@ def all_tasks(): if status != "all": if status == "soon": - tasks = [task for task in tasks if task.status in {"due_today", "due_tomorrow", "due_day_after_tomorrow"}] + tasks = [task for task in tasks if task.status in {"due_tomorrow", "due_day_after_tomorrow"}] else: status_map = { - "completed": "completed", "overdue": "overdue", "open": "open", "today": "due_today", @@ -141,15 +180,58 @@ def all_tasks(): selected = status_map.get(status) if selected: tasks = [task for task in tasks if task.status == selected] + tasks = [task for task in tasks if not task.is_completed] + else: + tasks = [task for task in tasks if not task.is_completed] + + sections = _group_active_tasks(tasks, sort=sort) return render_template( "tasks/all_tasks.html", tasks=tasks, + sections=sections, users=User.query.order_by(User.name.asc()).all(), filters={"status": status, "mine": mine, "user_id": user_filter, "sort": sort}, ) +@bp.route("/archive") +@login_required +def archive_view(): + selected_user_id = request.args.get("user_id", type=int) or current_user.id + archive_users = User.query.order_by(User.name.asc()).all() + selected_user = next((user for user in archive_users if user.id == selected_user_id), current_user) + ordered_users = sorted(archive_users, key=lambda user: (user.id != current_user.id, user.name.lower())) + + completed_tasks = ( + TaskInstance.query.filter_by(completed_by_user_id=selected_user.id) + .filter(TaskInstance.completed_at.isnot(None)) + .order_by(TaskInstance.completed_at.desc(), TaskInstance.updated_at.desc()) + .all() + ) + + today = today_local() + grouped: dict[date, list[TaskInstance]] = defaultdict(list) + for task in completed_tasks: + grouped[task.completed_at.date()].append(task) + + archive_sections = [ + { + "label": _archive_day_label(day, today), + "day": day, + "tasks": grouped[day], + } + for day in sorted(grouped.keys(), key=lambda value: _archive_day_priority(value, today)) + ] + + return render_template( + "tasks/archive.html", + archive_users=ordered_users, + selected_user=selected_user, + archive_sections=archive_sections, + ) + + @bp.route("/tasks/new", methods=["GET", "POST"]) @login_required def create(): diff --git a/app/static/css/style.css b/app/static/css/style.css index e7b0c7b..a9f4c55 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -474,8 +474,8 @@ p { .task-card { display: grid; - gap: 16px; - padding: 20px; + gap: 14px; + padding: 18px; } .task-card__top { @@ -505,7 +505,7 @@ p { } .task-card h3 { - font-size: 1.35rem; + font-size: 1.24rem; line-height: 1.08; overflow-wrap: break-word; hyphens: auto; @@ -577,8 +577,8 @@ p { .task-meta { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 10px 14px; } .task-meta dt { @@ -594,11 +594,16 @@ p { font-weight: 600; } +.task-card--compact .task-meta { + grid-template-columns: 1fr; +} + .task-assignee { display: inline-flex; align-items: center; gap: 10px; color: var(--muted); + min-width: 0; } .task-assignee__avatars { @@ -611,6 +616,10 @@ p { border: 2px solid var(--surface-strong); } +.task-assignee span:last-child { + overflow-wrap: break-word; +} + .avatar { width: 34px; height: 34px; @@ -757,6 +766,41 @@ p { gap: 14px; } +.archive-user-tabs { + display: flex; + gap: 12px; + overflow-x: auto; + padding-bottom: 4px; + margin-bottom: 8px; +} + +.archive-user-tab { + flex: 0 0 auto; + min-width: 108px; + display: grid; + justify-items: center; + gap: 8px; + padding: 14px 16px; + border-radius: 20px; + border: 1px solid var(--border); + background: var(--surface); + box-shadow: var(--shadow); + color: var(--muted); + text-align: center; +} + +.archive-user-tab span { + font-size: 0.92rem; + font-weight: 700; + line-height: 1.15; +} + +.archive-user-tab.is-active { + color: var(--text); + border-color: rgba(37, 99, 235, 0.22); + background: linear-gradient(135deg, rgba(37, 99, 235, 0.14), rgba(52, 211, 153, 0.1)); +} + .panel--toolbar { display: flex; justify-content: space-between; @@ -1345,6 +1389,22 @@ p { border-radius: 28px; } + .archive-user-tabs { + gap: 10px; + margin-bottom: 4px; + } + + .archive-user-tab { + min-width: 92px; + padding: 12px 12px; + gap: 6px; + border-radius: 18px; + } + + .archive-user-tab span { + font-size: 0.82rem; + } + .task-card { gap: 14px; padding: 18px; diff --git a/app/templates/partials/macros.html b/app/templates/partials/macros.html index cd6041a..a66b6fe 100644 --- a/app/templates/partials/macros.html +++ b/app/templates/partials/macros.html @@ -66,18 +66,10 @@
Erledigte Aufgaben
+Hier landen alle erledigten Aufgaben. Du kannst pro Person sehen, was heute, gestern, vorgestern und davor erledigt wurde.
+Ansicht
+Wechsle über die Tabs direkt zwischen den erledigten Aufgaben der einzelnen Nutzer.
+