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 @@
Fällig
{{ task.due_date|date_de }}
-
-
Zuständig
-
{{ task.assignee_label }}
-
-
-
Rhythmus
-
{{ task.task_template.recurrence_label }}
-
{% if task.completed_at %}
-
Erledigt von
-
{{ task.completed_by_user.name if task.completed_by_user else '—' }}
+
Erledigt
+
{{ task.completed_at|datetime_de }}
{% endif %} diff --git a/app/templates/tasks/all_tasks.html b/app/templates/tasks/all_tasks.html index 33eb4d4..648c8f9 100644 --- a/app/templates/tasks/all_tasks.html +++ b/app/templates/tasks/all_tasks.html @@ -15,7 +15,6 @@ -
@@ -43,11 +42,69 @@ -
- {% for task in tasks %} - {{ task_card(task, current_user) }} - {% else %} -
Für diese Filter gibt es gerade keine Aufgaben.
- {% endfor %} -
+ {% if filters.status == 'all' %} +
+
+

Überfällig

+ {{ sections.overdue|length }} +
+
+ {% for task in sections.overdue %} + {{ task_card(task, current_user) }} + {% else %} +
Für diese Auswahl ist nichts überfällig.
+ {% endfor %} +
+
+ +
+
+

Heute

+ {{ sections.today|length }} +
+
+ {% for task in sections.today %} + {{ task_card(task, current_user) }} + {% else %} +
Heute ist hier gerade nichts offen.
+ {% endfor %} +
+
+ +
+
+

Bald fällig

+ {{ sections.soon|length }} +
+
+ {% for task in sections.soon %} + {{ task_card(task, current_user) }} + {% else %} +
In den nächsten Tagen ist hier gerade nichts fällig.
+ {% endfor %} +
+
+ +
+
+

Offen

+ {{ sections.open|length }} +
+
+ {% for task in sections.open %} + {{ task_card(task, current_user) }} + {% else %} +
Keine weiteren offenen Aufgaben für diese Auswahl.
+ {% endfor %} +
+
+ {% else %} +
+ {% for task in tasks %} + {{ task_card(task, current_user) }} + {% else %} +
Für diese Filter gibt es gerade keine Aufgaben.
+ {% endfor %} +
+ {% endif %} {% endblock %} diff --git a/app/templates/tasks/archive.html b/app/templates/tasks/archive.html new file mode 100644 index 0000000..10ea9dc --- /dev/null +++ b/app/templates/tasks/archive.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% from "partials/macros.html" import avatar, task_card %} +{% block title %}Archiv · Putzliga{% endblock %} +{% block page_title %}Archiv{% endblock %} +{% block content %} +
+
+

Erledigte Aufgaben

+

Was schon geschafft wurde

+

Hier landen alle erledigten Aufgaben. Du kannst pro Person sehen, was heute, gestern, vorgestern und davor erledigt wurde.

+
+
+

Ansicht

+

{{ selected_user.name }}

+

Wechsle über die Tabs direkt zwischen den erledigten Aufgaben der einzelnen Nutzer.

+
+
+ +
+ {% for user in archive_users %} + + {{ avatar(user) }} + {{ user.name }} + + {% endfor %} +
+ + {% for section in archive_sections %} +
+
+

{{ section.label }}

+ {{ section.tasks|length }} +
+
+ {% for task in section.tasks %} + {{ task_card(task, current_user, compact=true) }} + {% endfor %} +
+
+ {% else %} +
+
Für {{ selected_user.name }} gibt es im Archiv noch keine erledigten Aufgaben.
+
+ {% endfor %} +{% endblock %} diff --git a/app/templates/tasks/my_tasks.html b/app/templates/tasks/my_tasks.html index bc2a619..49990be 100644 --- a/app/templates/tasks/my_tasks.html +++ b/app/templates/tasks/my_tasks.html @@ -22,6 +22,10 @@ {{ nav_icon('plus') }} Neue Aufgabe anlegen + + {{ nav_icon('check-double') }} + Archiv + {{ nav_icon('trophy') }} Zum aktuellen Highscore @@ -45,14 +49,28 @@
-

Bald fällig

- {{ soon_tasks|length }} +

Heute

+ {{ sections.today|length }}
- {% for task in soon_tasks %} + {% for task in sections.today %} {{ task_card(task, current_user) }} {% else %} -
Gerade ist nichts bald fällig. Sehr stark.
+
Heute ist gerade nichts mehr offen. Sehr stark.
+ {% endfor %} +
+
+ +
+
+

Bald fällig

+ {{ sections.soon|length }} +
+
+ {% for task in sections.soon %} + {{ task_card(task, current_user) }} + {% else %} +
Gerade ist nichts in den nächsten Tagen fällig.
{% endfor %}
@@ -70,18 +88,4 @@ {% endfor %}
- -
-
-

Erledigt

- {{ sections.completed|length }} -
-
- {% for task in sections.completed %} - {{ task_card(task, current_user, compact=true) }} - {% else %} -
Noch keine erledigten Aufgaben in deiner Liste.
- {% endfor %} -
-
{% endblock %}