diff --git a/PROJECTS.md b/PROJECTS.md index 5e401c2..d0a0785 100644 --- a/PROJECTS.md +++ b/PROJECTS.md @@ -1,12 +1,12 @@ # 📋 Projekte - Payments System **Status**: Production-ready -**Last Updated**: 2026-07-24 +**Last Updated**: 2026-07-30 ## 🔍 Recent Activity +- **2026-07-30 — Focus Queue: real undo, "due today," and `is_focus` retirement (issue #281)**: three related fixes to `/focus/`. (1) The complete checkbox only ever called `mark_completed()` — an accidental click had no way back; it now toggles complete/incomplete and swaps just that row so a just-completed task stays visible (struck through, still checked) as an immediate undo. (2) `due_date` used to only decorate a row; a new "📅 Today" button sets it, and due-today-or-overdue tasks now always sort ahead of the rest of the queue regardless of priority. (3) `is_focus` (P-028, week-scoped, dashboard-only) is retired — its only "add to focus" UI path was lost back in P-050 phase 4, leaving no way to set it at all. Rather than restoring a second star toggle, the dashboard's weekly widget now lists `due_date <= today` tasks, sharing the Focus Queue's own signal (migration `0018_remove_practicetodo_is_focus`); resolves the overlap issue #281 flagged. - **2026-07-24 — Configurable overdue-invoice threshold (issue #195)**: the "sent invoice counts as overdue" cutoff was hardcoded to 30 days in three separate places (`InvoiceActionsWidgetBuilder.get_overdue_invoices()`, feeding both the dashboard and the Focus Queue's `invoice_unpaid` task; `ClientDetailContextBuilder`'s payment-reminder urgency, which also had a hardcoded 14-day "medium" tier). Added `Practice.overdue_after_days` (default 30, admin-editable in the existing "Payment Terms" fieldset) and wired all three call sites to it; the medium tier now reuses the existing `payment_terms_days` field instead of a second hardcoded 14. The `bank_import_reminder_days` half of #195 turned out to be moot — that widget (`BankImportReminderWidgetBuilder`) was already deleted as dead code during P-050 phase 4. Also fixed a migration-graph conflict (duplicate `0015` leaves) left behind when PRs #270 and #271 merged independently. -- **2026-07-23 — Configurable bank-import CSV format (issue #11)**: `utils/bank_import.py`'s CSV parser was hardcoded to GLS Bank's column names and semicolon delimiter. Added `csv_delimiter`/`csv_column_*` fields to `Practice` (admin-editable, GLS defaults preserved), so self-hosters on other banks can point the parser at their own export's delimiter and column headers without touching code. UI strings no longer imply GLS-only support. > Ältere Einträge: [docs/CHANGELOG.md](docs/CHANGELOG.md) diff --git a/app/locale/de/LC_MESSAGES/django.po b/app/locale/de/LC_MESSAGES/django.po index 7b5598b..512b29f 100644 --- a/app/locale/de/LC_MESSAGES/django.po +++ b/app/locale/de/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: my-practice\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 22:30+0200\n" +"POT-Creation-Date: 2026-07-29 23:11+0200\n" "PO-Revision-Date: 2026-06-22 23:01+0200\n" "Language-Team: German\n" "Language: de\n" @@ -525,7 +525,7 @@ msgstr "Fokus-Warteschlange (P-050)" msgid "Set automatically for materialized tasks — not manually editable." msgstr "Wird für materialisierte Aufgaben automatisch gesetzt — nicht manuell editierbar." -#: my_practice/admin.py:1038 my_practice/models/todo.py:155 +#: my_practice/admin.py:1038 my_practice/models/todo.py:149 msgid "Task" msgstr "Aufgabe" @@ -2876,35 +2876,27 @@ msgstr "Aufgabenkategorie zur Organisation" msgid "Task priority level" msgstr "Aufgabenpriorität" -#: my_practice/models/todo.py:115 -msgid "Mark as a focus task for the current week" -msgstr "Als Fokus-Aufgabe für die aktuelle Woche markieren" - -#: my_practice/models/todo.py:116 -msgid "Focus task" -msgstr "Fokus-Aufgabe" - -#: my_practice/models/todo.py:120 +#: my_practice/models/todo.py:114 msgid "Optional due date" msgstr "Optionales Fälligkeitsdatum" -#: my_practice/models/todo.py:122 +#: my_practice/models/todo.py:116 msgid "When the task was completed" msgstr "Wann die Aufgabe abgeschlossen wurde" -#: my_practice/models/todo.py:127 +#: my_practice/models/todo.py:121 msgid "Hide from the focus queue until this date" msgstr "Bis zu diesem Datum aus der Fokus-Warteschlange ausblenden" -#: my_practice/models/todo.py:137 +#: my_practice/models/todo.py:131 msgid "Manual entry, or a materialized system signal (unpaid invoice, missing session log, etc.)" msgstr "Manueller Eintrag oder ein materialisiertes Systemsignal (unbezahlte Rechnung, fehlendes Sitzungsprotokoll usw.)" -#: my_practice/models/todo.py:146 +#: my_practice/models/todo.py:140 msgid "Related object type, for materialized tasks (e.g. Client, Invoice)" msgstr "Typ des verknüpften Objekts, für materialisierte Aufgaben (z. B. Klient, Rechnung)" -#: my_practice/models/todo.py:156 +#: my_practice/models/todo.py:150 msgid "Tasks" msgstr "Aufgaben" @@ -3084,11 +3076,12 @@ msgid_plural "%(count)s sessions" msgstr[0] "%(count)s Sitzung" msgstr[1] "%(count)s Sitzungen" -#: my_practice/utils/dashboard_context_builder.py:130 -msgid "Focus" -msgstr "Fokus" +#: my_practice/utils/dashboard_context_builder.py:131 +#: templates/includes/weekly_focus_widget_content.html:33 +msgid "Due today" +msgstr "Heute fällig" -#: my_practice/utils/dashboard_context_builder.py:140 +#: my_practice/utils/dashboard_context_builder.py:142 #: templates/my_practice/analytics.html:107 #: templates/my_practice/analytics.html:731 msgid "Capacity" @@ -4621,31 +4614,23 @@ msgstr "Quartalsübersicht anzeigen" msgid "Tax prepayment recorded for Q%(quarter)s" msgstr "Steuervorauszahlung für Q%(quarter)s erfasst" -#: templates/includes/weekly_focus_widget_content.html:11 +#: templates/includes/weekly_focus_widget_content.html:13 msgid "Sessions this week" msgstr "Sitzungen diese Woche" -#: templates/includes/weekly_focus_widget_content.html:25 +#: templates/includes/weekly_focus_widget_content.html:27 msgid "No sessions this week." msgstr "Keine Sitzungen diese Woche." -#: templates/includes/weekly_focus_widget_content.html:31 -msgid "Focus tasks" -msgstr "Fokus-Aufgaben" - -#: templates/includes/weekly_focus_widget_content.html:41 +#: templates/includes/weekly_focus_widget_content.html:43 #: templates/my_practice/calendar_approval_queue.html:38 msgid "Mark as done" msgstr "Als erledigt markieren" -#: templates/includes/weekly_focus_widget_content.html:46 -msgid "Remove focus" -msgstr "Fokus entfernen" - -#: templates/includes/weekly_focus_widget_content.html:60 +#: templates/includes/weekly_focus_widget_content.html:57 #, python-format -msgid "No focus tasks. Click ⭐ on a task in the focus queue." -msgstr "Keine Fokus-Aufgaben. Klicke ⭐ auf einer Aufgabe im Fokus." +msgid "Nothing due today. Set a due date from the focus queue." +msgstr "Nichts ist heute fällig. Setze ein Fälligkeitsdatum im Fokus." #: templates/includes/widget_base.html:39 msgid "Toggle widget" @@ -8810,6 +8795,21 @@ msgstr "Benutzername" msgid "Password" msgstr "Passwort" +#~ msgid "Mark as a focus task for the current week" +#~ msgstr "Als Fokus-Aufgabe für die aktuelle Woche markieren" + +#~ msgid "Focus task" +#~ msgstr "Fokus-Aufgabe" + +#~ msgid "Focus" +#~ msgstr "Fokus" + +#~ msgid "Focus tasks" +#~ msgstr "Fokus-Aufgaben" + +#~ msgid "Remove focus" +#~ msgstr "Fokus entfernen" + #~ msgid "No sessions for 60+ days" #~ msgstr "Keine Sitzungen seit 60+ Tagen" @@ -8833,9 +8833,6 @@ msgstr "Passwort" #~ msgid "⚠️ Overdue since %(date)s" #~ msgstr "⚠️ Überfällig seit %(date)s" -#~ msgid "📅 Due today" -#~ msgstr "📅 Heute fällig" - #, python-format #~ msgid "📅 Due in %(days)s day" #~ msgid_plural "📅 Due in %(days)s days" diff --git a/app/locale/en/LC_MESSAGES/django.po b/app/locale/en/LC_MESSAGES/django.po index 79761ad..01ae9fb 100644 --- a/app/locale/en/LC_MESSAGES/django.po +++ b/app/locale/en/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: my-practice\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 22:30+0200\n" +"POT-Creation-Date: 2026-07-29 23:11+0200\n" "PO-Revision-Date: 2026-06-22 23:01+0200\n" "Language-Team: English\n" "Language: en\n" @@ -525,7 +525,7 @@ msgstr "" msgid "Set automatically for materialized tasks — not manually editable." msgstr "" -#: my_practice/admin.py:1038 my_practice/models/todo.py:155 +#: my_practice/admin.py:1038 my_practice/models/todo.py:149 msgid "Task" msgstr "" @@ -2876,35 +2876,27 @@ msgstr "" msgid "Task priority level" msgstr "" -#: my_practice/models/todo.py:115 -msgid "Mark as a focus task for the current week" -msgstr "" - -#: my_practice/models/todo.py:116 -msgid "Focus task" -msgstr "" - -#: my_practice/models/todo.py:120 +#: my_practice/models/todo.py:114 msgid "Optional due date" msgstr "" -#: my_practice/models/todo.py:122 +#: my_practice/models/todo.py:116 msgid "When the task was completed" msgstr "" -#: my_practice/models/todo.py:127 +#: my_practice/models/todo.py:121 msgid "Hide from the focus queue until this date" msgstr "" -#: my_practice/models/todo.py:137 +#: my_practice/models/todo.py:131 msgid "Manual entry, or a materialized system signal (unpaid invoice, missing session log, etc.)" msgstr "" -#: my_practice/models/todo.py:146 +#: my_practice/models/todo.py:140 msgid "Related object type, for materialized tasks (e.g. Client, Invoice)" msgstr "" -#: my_practice/models/todo.py:156 +#: my_practice/models/todo.py:150 msgid "Tasks" msgstr "" @@ -3084,11 +3076,12 @@ msgid_plural "%(count)s sessions" msgstr[0] "" msgstr[1] "" -#: my_practice/utils/dashboard_context_builder.py:130 -msgid "Focus" +#: my_practice/utils/dashboard_context_builder.py:131 +#: templates/includes/weekly_focus_widget_content.html:33 +msgid "Due today" msgstr "" -#: my_practice/utils/dashboard_context_builder.py:140 +#: my_practice/utils/dashboard_context_builder.py:142 #: templates/my_practice/analytics.html:107 #: templates/my_practice/analytics.html:731 msgid "Capacity" @@ -4621,30 +4614,22 @@ msgstr "" msgid "Tax prepayment recorded for Q%(quarter)s" msgstr "" -#: templates/includes/weekly_focus_widget_content.html:11 +#: templates/includes/weekly_focus_widget_content.html:13 msgid "Sessions this week" msgstr "" -#: templates/includes/weekly_focus_widget_content.html:25 +#: templates/includes/weekly_focus_widget_content.html:27 msgid "No sessions this week." msgstr "No sessions this week." -#: templates/includes/weekly_focus_widget_content.html:31 -msgid "Focus tasks" -msgstr "" - -#: templates/includes/weekly_focus_widget_content.html:41 +#: templates/includes/weekly_focus_widget_content.html:43 #: templates/my_practice/calendar_approval_queue.html:38 msgid "Mark as done" msgstr "" -#: templates/includes/weekly_focus_widget_content.html:46 -msgid "Remove focus" -msgstr "" - -#: templates/includes/weekly_focus_widget_content.html:60 +#: templates/includes/weekly_focus_widget_content.html:57 #, python-format -msgid "No focus tasks. Click ⭐ on a task in the focus queue." +msgid "Nothing due today. Set a due date from the focus queue." msgstr "" #: templates/includes/widget_base.html:39 diff --git a/app/my_practice/migrations/0018_remove_practicetodo_is_focus.py b/app/my_practice/migrations/0018_remove_practicetodo_is_focus.py new file mode 100644 index 0000000..e9920b0 --- /dev/null +++ b/app/my_practice/migrations/0018_remove_practicetodo_is_focus.py @@ -0,0 +1,16 @@ +# Generated by Django 6.0.7 on 2026-07-29 20:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("my_practice", "0017_practice_overdue_after_days"), + ] + + operations = [ + migrations.RemoveField( + model_name="practicetodo", + name="is_focus", + ), + ] diff --git a/app/my_practice/models/todo.py b/app/my_practice/models/todo.py index 17b8bbe..86de334 100644 --- a/app/my_practice/models/todo.py +++ b/app/my_practice/models/todo.py @@ -110,12 +110,6 @@ class TaskType(StrEnum): default="medium", help_text=_("Task priority level"), ) - is_focus = models.BooleanField( - default=False, - help_text=_("Mark as a focus task for the current week"), - verbose_name=_("Focus task"), - ) - # Dates due_date = models.DateField(null=True, blank=True, help_text=_("Optional due date")) completed_at = models.DateTimeField( diff --git a/app/my_practice/tests/test_views_todo.py b/app/my_practice/tests/test_views_todo.py index d525c45..f55805f 100644 --- a/app/my_practice/tests/test_views_todo.py +++ b/app/my_practice/tests/test_views_todo.py @@ -184,33 +184,3 @@ def test_htmx_post_returns_content(self): ) # HTMX response: either partial HTML or redirect self.assertIn(response.status_code, [200, 302]) - - -class TodoToggleFocusTest(TestCase): - """Tests for todo_toggle_focus view.""" - - def setUp(self): - self.practice = _make_practice("todo-focus-1") - self.user = User.objects.create_user(username="todouser6", password="testpass123") - link_user_to_practice(self.user, self.practice) - self.tc = _setup_client(self.user, self.practice) - self.todo = PracticeTodo.objects.create( - practice=self.practice, title="Focus Me", priority="medium", is_focus=False - ) - - def test_non_htmx_redirects(self): - response = self.tc.post(reverse("todo_toggle_focus", args=[self.todo.pk])) - self.assertEqual(response.status_code, 302) - self.todo.refresh_from_db() - self.assertTrue(self.todo.is_focus) - - def test_htmx_post_returns_200(self): - response = self.tc.post( - reverse("todo_toggle_focus", args=[self.todo.pk]), - HTTP_HX_REQUEST="true", - ) - self.assertEqual(response.status_code, 200) - - def test_nonexistent_returns_404(self): - response = self.tc.post(reverse("todo_toggle_focus", args=[99999])) - self.assertEqual(response.status_code, 404) diff --git a/app/my_practice/tests/test_weekly_focus_widget.py b/app/my_practice/tests/test_weekly_focus_widget.py new file mode 100644 index 0000000..b82f813 --- /dev/null +++ b/app/my_practice/tests/test_weekly_focus_widget.py @@ -0,0 +1,69 @@ +""" +Tests for WeeklyFocusWidgetBuilder (P-028), specifically the "due today or +overdue" task list — merged onto due_date instead of the retired is_focus +flag so it shares one signal with the Focus Queue (P-050). +""" + +from datetime import date, timedelta + +from django.test import TestCase + +from ..models import Practice, PracticeTodo +from ..utils.weekly_focus_widget import WeeklyFocusWidgetBuilder + + +def _make_practice(slug): + return Practice.objects.create( + name="Test Practice", + slug=slug, + title="Test Practitioner", + email="test@practice.example", + city="Berlin", + ) + + +class WeeklyFocusWidgetDueTodayTasksTest(TestCase): + def setUp(self): + self.practice = _make_practice("weekly-focus-widget-1") + self.today = date(2026, 7, 30) + + def _build(self): + return WeeklyFocusWidgetBuilder(self.practice, today=self.today).build_context() + + def test_includes_task_due_today(self): + PracticeTodo.objects.create(practice=self.practice, title="Due today", due_date=self.today) + context = self._build() + titles = [t.title for t in context["due_today_tasks"]] + self.assertIn("Due today", titles) + self.assertEqual(context["due_today_count"], 1) + + def test_includes_overdue_task(self): + PracticeTodo.objects.create( + practice=self.practice, title="Overdue", due_date=self.today - timedelta(days=3) + ) + context = self._build() + titles = [t.title for t in context["due_today_tasks"]] + self.assertIn("Overdue", titles) + + def test_excludes_task_due_in_future(self): + PracticeTodo.objects.create( + practice=self.practice, title="Later", due_date=self.today + timedelta(days=1) + ) + context = self._build() + titles = [t.title for t in context["due_today_tasks"]] + self.assertNotIn("Later", titles) + + def test_excludes_task_without_due_date(self): + PracticeTodo.objects.create(practice=self.practice, title="No due date") + context = self._build() + titles = [t.title for t in context["due_today_tasks"]] + self.assertNotIn("No due date", titles) + + def test_excludes_completed_task(self): + task = PracticeTodo.objects.create( + practice=self.practice, title="Done already", due_date=self.today + ) + task.mark_completed() + context = self._build() + titles = [t.title for t in context["due_today_tasks"]] + self.assertNotIn("Done already", titles) diff --git a/app/my_practice/urls.py b/app/my_practice/urls.py index 61e1efa..05e2278 100644 --- a/app/my_practice/urls.py +++ b/app/my_practice/urls.py @@ -279,7 +279,6 @@ path("todos//edit/", views.TodoUpdateView.as_view(), name="todo_edit"), path("todos//delete/", views.TodoDeleteView.as_view(), name="todo_delete"), path("todos//toggle/", views.todo_toggle_complete, name="todo_toggle"), - path("todos//toggle-focus/", views.todo_toggle_focus, name="todo_toggle_focus"), # Focus Queue (P-050) path("focus/", views.FocusQueueView.as_view(), name="focus_queue"), path( diff --git a/app/my_practice/utils/dashboard_context_builder.py b/app/my_practice/utils/dashboard_context_builder.py index 7f35d94..b21669d 100644 --- a/app/my_practice/utils/dashboard_context_builder.py +++ b/app/my_practice/utils/dashboard_context_builder.py @@ -119,15 +119,17 @@ def _build_widgets(self) -> dict: cap_ctx = CapacityMonitoringWidgetBuilder(practice).build_context() wf_ctx = WeeklyFocusWidgetBuilder(practice).build_context() - focus_count = wf_ctx["focus_count"] + due_today_count = wf_ctx["due_today_count"] wf_session_count = wf_ctx["session_count"] wf_badge_parts = [ f'' f"{ngettext('%(count)s session', '%(count)s sessions', wf_session_count) % {'count': wf_session_count}}" f"" ] - if focus_count: - wf_badge_parts.append(f'{focus_count} {_("Focus")}') + if due_today_count: + wf_badge_parts.append( + f'{due_today_count} {_("Due today")}' + ) def _html(template: str, ctx: dict) -> SafeString: return mark_safe(render_to_string(template, ctx)) diff --git a/app/my_practice/utils/weekly_focus_widget.py b/app/my_practice/utils/weekly_focus_widget.py index 7262248..e821e3e 100644 --- a/app/my_practice/utils/weekly_focus_widget.py +++ b/app/my_practice/utils/weekly_focus_widget.py @@ -1,6 +1,8 @@ """ -Weekly Focus Widget for dashboard – shows Mon–Sun sessions plus focus tasks. -Part of P-028 Dashboard Redesign. +Weekly Focus Widget for dashboard – shows Mon–Sun sessions plus tasks due +today or overdue. Part of P-028 Dashboard Redesign; the task half now shares +its "due today" signal with the Focus Queue (P-050) instead of the retired +is_focus flag — see docs/projects/done/P-028_DASHBOARD_WEEKLY_FOCUS.md. """ from datetime import date, timedelta @@ -16,7 +18,7 @@ class WeeklyFocusWidgetBuilder: Combines: - All sessions scheduled for the current calendar week (Mon–Sun) - - All incomplete focus tasks (is_focus=True) + - All incomplete tasks due today or overdue (due_date <= today) Usage: builder = WeeklyFocusWidgetBuilder(practice) @@ -53,11 +55,11 @@ def _get_week_sessions(self) -> list[dict]: for s in sessions ] - def _get_focus_tasks(self) -> QuerySet: - """Get active focus tasks for the current practice.""" + def _get_due_today_tasks(self) -> QuerySet: + """Get incomplete tasks due today or overdue for the current practice.""" return PracticeTodo.objects.filter( practice=self.practice, - is_focus=True, + due_date__lte=self.today, completed_at__isnull=True, ).order_by("due_date", "-created_at") @@ -67,22 +69,21 @@ def build_context(self) -> dict: Returns dict with: week_sessions: list of session dicts - focus_tasks: QuerySet of PracticeTodo + due_today_tasks: QuerySet of PracticeTodo week_start: date (Monday) week_end: date (Sunday) session_count: int - focus_count: int + due_today_count: int """ week_sessions = self._get_week_sessions() - focus_tasks = self._get_focus_tasks() + due_today_tasks = self._get_due_today_tasks() return { "week_sessions": week_sessions, - "focus_tasks": focus_tasks, + "due_today_tasks": due_today_tasks, "week_start": self.week_start, "week_end": self.week_end, "session_count": len(week_sessions), - "focus_count": focus_tasks.count(), + "due_today_count": due_today_tasks.count(), "today": self.today, - "todo_toggle_focus_url": "todo_toggle_focus", } diff --git a/app/my_practice/views/__init__.py b/app/my_practice/views/__init__.py index 7137018..698a073 100644 --- a/app/my_practice/views/__init__.py +++ b/app/my_practice/views/__init__.py @@ -167,7 +167,6 @@ TodoDeleteView, TodoUpdateView, todo_toggle_complete, - todo_toggle_focus, ) # Time-off views @@ -236,7 +235,6 @@ "TodoUpdateView", "TodoDeleteView", "todo_toggle_complete", - "todo_toggle_focus", # Focus Queue views (P-050) "FocusQueueView", "focus_queue_toggle_complete", diff --git a/app/my_practice/views/todo_views.py b/app/my_practice/views/todo_views.py index 5889d29..8ff3661 100644 --- a/app/my_practice/views/todo_views.py +++ b/app/my_practice/views/todo_views.py @@ -4,7 +4,7 @@ The standalone /todos/ list page was retired in favour of the Focus Queue page (P-050 phase 4), which shows manual and materialized tasks together. These CRUD views remain — Focus Queue reuses them for creating/editing a -manual task — as do the two toggle endpoints, still used inline by the +manual task — as does todo_toggle_complete, still used inline by the dashboard's WeeklyFocus widget. """ @@ -128,23 +128,3 @@ def todo_toggle_complete(request: HttpRequest, pk: int) -> HttpResponse | JsonRe # Redirect to referrer if available, otherwise to dashboard referrer_path = urlparse(request.META.get("HTTP_REFERER", "")).path return redirect(referrer_path or reverse("dashboard")) - - -def todo_toggle_focus(request: HttpRequest, pk: int) -> HttpResponse: - """ - Toggle is_focus flag on a PracticeTodo. - - POST only, used by the dashboard's WeeklyFocus widget. Returns an HTMX - partial on HTMX requests, otherwise redirects back to referrer. - """ - todo = get_object_or_404(PracticeTodo.objects.for_current_practice(request), pk=pk) - - todo.is_focus = not todo.is_focus - todo.save(update_fields=["is_focus"]) - - if request.headers.get("HX-Request"): - builder = WeeklyFocusWidgetBuilder(request.current_practice) - return render(request, "includes/weekly_focus_widget_content.html", builder.build_context()) - - referrer_path = urlparse(request.META.get("HTTP_REFERER", "")).path - return redirect(referrer_path or reverse("focus_queue")) diff --git a/app/static/css/tailwind.css b/app/static/css/tailwind.css index f29cece..484ff43 100644 --- a/app/static/css/tailwind.css +++ b/app/static/css/tailwind.css @@ -2131,17 +2131,6 @@ transition: background 0.15s; } .btn-due-today:hover { background: var(--color-link); color: white; } - .btn-focus-toggle { - background: none; - border: none; - cursor: pointer; - font-size: 1rem; - padding: 0 0.25rem; - opacity: 0.4; - transition: opacity 0.15s; - } - .btn-focus-toggle:hover { opacity: 1; } - .btn-focus-toggle.is-focused { opacity: 1; } /* Invoice create/edit form formset — .formset-item/.deleted also driven by JS */ .form-row { diff --git a/app/templates/includes/weekly_focus_widget_content.html b/app/templates/includes/weekly_focus_widget_content.html index 8cebb40..7003dd7 100644 --- a/app/templates/includes/weekly_focus_widget_content.html +++ b/app/templates/includes/weekly_focus_widget_content.html @@ -1,6 +1,8 @@ {% comment %} Weekly Focus Widget Content – P-028 -Shows Mon–Sun sessions and focus tasks (is_focus=True). +Shows Mon–Sun sessions and tasks due today or overdue (due_date <= today) — +same "due today" signal the Focus Queue (P-050) sorts to the top, set there +via the "Today" quick action. Read-only here; manage due dates in the queue. {% endcomment %} {% load static i18n %} @@ -26,12 +28,12 @@ {% endif %} - +
-
⭐ {% trans "Focus tasks" %}
- {% if focus_tasks %} +
📅 {% trans "Due today" %}
+ {% if due_today_tasks %}
- {% for task in focus_tasks %} + {% for task in due_today_tasks %}
- {{ task.title }}
{% if task.due_date %} @@ -57,7 +54,7 @@ {% else %} {% url 'focus_queue' as focus_queue_url %}
- {% blocktrans %}No focus tasks. Click ⭐ on a task in the focus queue.{% endblocktrans %} + {% blocktrans %}Nothing due today. Set a due date from the focus queue.{% endblocktrans %}
{% endif %}
diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 20321b8..68e219e 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -96,7 +96,7 @@ Complete feature list for the Therapy Practice Management System. - ✅ Status breakdown (Draft/Sent/Paid/Cancelled) — all-time overview - ✅ Recent invoices overview - ✅ Dark mode + Privacy mode -- ✅ **Fokus-Aufgaben Widget** — ⭐ toggle on todos, `WeeklyFocusWidgetBuilder` shows focus tasks in dashboard (P-028) +- ✅ **Weekly focus widget** — `WeeklyFocusWidgetBuilder` shows this week's sessions plus tasks due today or overdue, sharing the Focus Queue's `due_date` signal instead of a separate star toggle (P-028, merged into `due_date` P-050) ### Analytics Dashboard - ✅ Time period filters (All/Month/Quarter/Year/Custom) @@ -347,6 +347,14 @@ Complete feature list for the Therapy Practice Management System. - Zero hardcoded hex colours in non-PDF templates; new semantic classes: `.callout-warning/danger/success/primary`, `.btn-gradient` - New UI features require zero new CSS files +## 🚀 Recent Additions (Juli 2026) + +### Focus Queue improvements (30. Juli) + +- **Checkbox now really toggles** — previously only ever marked a task complete; an accidental click had no way back short of Django admin. It now toggles complete/incomplete, and the just-completed row stays visible in place (struck through, still checked) as an immediate undo +- **"📅 Today" quick action** — sets a task's due date to today in one click; due-today-or-overdue tasks now always sort ahead of the rest of the queue regardless of priority +- **`is_focus` retired, merged into `due_date`** — the dashboard's weekly widget star toggle had no working "add" path left (only "remove"); rather than restoring it, the widget now lists tasks due today or overdue, sharing the same signal the Focus Queue sorts on + ## 🚀 Recent Additions (April 2026) ### P-040 Sample Data + Bank Import Cleanup (28. April) diff --git a/docs/architecture/CODE_STRUCTURE.md b/docs/architecture/CODE_STRUCTURE.md index 95cce05..769f62a 100644 --- a/docs/architecture/CODE_STRUCTURE.md +++ b/docs/architecture/CODE_STRUCTURE.md @@ -143,7 +143,7 @@ context = AnalyticsDashboardBuilder(start_date, end_date).build_context() `dashboard_views.py` is a thin dispatcher (22 lines). All data preparation lives in: - `DashboardContextAssembler` (`dashboard_context_builder.py`) — orchestrates widget builders -- Eleven widget builders: eight in `dashboard_widgets.py` (`InvoiceActionsWidgetBuilder`, `ClientAttentionWidgetBuilder`, `SessionImportWidgetBuilder`, `PendingCalendarWidgetBuilder`, `ChecklistWidgetBuilder`, `CapacityMonitoringWidgetBuilder`, `TaxQuarterWidgetBuilder`, `BankImportReminderWidgetBuilder`), plus `AgendaWidgetBuilder` (`agenda_helpers.py`), `WeeklyFocusWidgetBuilder` (`weekly_focus_widget.py`), and `ActionQueueBuilder` (`action_queue_builder.py`) +- Six widget builders: five in `dashboard_widgets.py` (`InvoiceActionsWidgetBuilder`, `SessionImportWidgetBuilder`, `PendingCalendarWidgetBuilder`, `ChecklistWidgetBuilder`, `CapacityMonitoringWidgetBuilder`), plus `WeeklyFocusWidgetBuilder` (`weekly_focus_widget.py`) — `ClientAttentionWidgetBuilder`, `TaxQuarterWidgetBuilder`, `BankImportReminderWidgetBuilder`, `AgendaWidgetBuilder`, and `ActionQueueBuilder` were removed with the P-050 phase-4 "Needs Action" pane retirement; this line was stale until now ### Session billing helpers (`utils/billing_helpers.py`) diff --git a/docs/projects/done/P-028_DASHBOARD_WEEKLY_FOCUS.md b/docs/projects/done/P-028_DASHBOARD_WEEKLY_FOCUS.md index 0dfdb8a..92e17d2 100644 --- a/docs/projects/done/P-028_DASHBOARD_WEEKLY_FOCUS.md +++ b/docs/projects/done/P-028_DASHBOARD_WEEKLY_FOCUS.md @@ -1,34 +1,54 @@ # P-028: Dashboard Redesign — Weekly Focus Widget **Status**: DONE -**Abgeschlossen**: April 2026 +**Completed**: April 2026 +**Superseded**: 2026-07-30 — see "Update" below; the `is_focus` field described here has been removed. -## Ziel +## Goal -Dashboard-Widget für fokussierte Aufgaben der aktuellen Woche: Aufgaben als Fokus -markieren (⭐), direkt im Widget abhaken (☐), ohne die Seite zu wechseln. +Dashboard widget for the current week's tasks: mark a task as focus (⭐), +check it off (☐) directly in the widget, without leaving the page. -## Umgesetzt +## Implemented ### Phase 1 -- `is_focus` BooleanField auf `PracticeTodo` -- HTMX-Toggle `/todos//toggle-focus/` → sofortiger Stern-Button im Todo-Widget -- `WeeklyFocusWidgetBuilder` in `utils/dashboard_widgets.py` -- 2-column Dashboard-Grid (Tages-Agenda links, Fokus-Woche rechts) -- ⭐-Button in `todo_content.html` und neuem `weekly_focus_widget_content.html` +- `is_focus` BooleanField on `PracticeTodo` +- HTMX toggle `/todos//toggle-focus/` → star button in the todo widget +- `WeeklyFocusWidgetBuilder` in `utils/weekly_focus_widget.py` +- 2-column dashboard grid (daily agenda left, weekly focus right) +- ⭐ button in `todo_content.html` and the new `weekly_focus_widget_content.html` ### Phase 2 (follow-up) -- ☐ Complete-Button direkt im Weekly-Focus-Widget -- Beide Buttons (`☐`, `⭐`) ersetzen das gesamte Widget-Fragment via +- ☐ complete button directly in the weekly focus widget +- Both buttons (`☐`, `⭐`) replace the whole widget fragment via `hx-target="#weekly-focus-content"` + `hx-swap="outerHTML"` -- `ctx=weekly_focus` Query-Parameter in `todo_toggle_complete` und - `todo_toggle_focus` (`WeeklyFocusWidgetBuilder` wird aufgerufen) -- CSS: `.weekly-focus-task-item`, `.btn-focus-complete` in `dashboard.css` - -## Dateien - -- `app/my_practice/models/todo.py` — `is_focus` BooleanField -- `app/my_practice/views/todo_views.py` — `todo_toggle_focus`, `todo_toggle_complete` -- `app/my_practice/utils/dashboard_widgets.py` — `WeeklyFocusWidgetBuilder` -- `app/templates/includes/weekly_focus_widget_content.html` — Widget-Partial -- `app/static/css/dashboard.css` — Widget-Styles +- `ctx=weekly_focus` query parameter on `todo_toggle_complete` and + `todo_toggle_focus` (calls `WeeklyFocusWidgetBuilder`) +- CSS: `.weekly-focus-task-item`, `.btn-focus-complete` + +## Update (2026-07-30): `is_focus` retired, merged into `due_date` + +The `/todos/` list page (P-050 phase 4) had already removed the only UI path +to *set* `is_focus=True` — the star only survived here as a "remove focus" +action, leaving no way to add a task to this widget at all. Rather than +restoring a second star toggle, `is_focus` was retired outright and this +widget now lists tasks with `due_date <= today` — the same "due today or +overdue" signal the Focus Queue (P-050) already used to sort its own queue, +set via that page's "📅 Today" quick action. See +[P-050_FOCUS_QUEUE.md](P-050_FOCUS_QUEUE.md), which had flagged `is_focus` +as likely-redundant once the queue existed. + +- `todo_toggle_focus` view, its URL, and the `is_focus` field are removed + (migration `0018_remove_practicetodo_is_focus`). +- `WeeklyFocusWidgetBuilder._get_due_today_tasks()` (renamed from + `_get_focus_tasks`) filters `due_date__lte=today` instead of `is_focus=True`. +- The widget is now read-only for tasks — due dates are managed from the + Focus Queue, not from this widget's star button (which no longer exists). + +## Files + +- `app/my_practice/models/todo.py` — `PracticeTodo` (`is_focus` removed) +- `app/my_practice/views/todo_views.py` — `todo_toggle_complete` +- `app/my_practice/utils/weekly_focus_widget.py` — `WeeklyFocusWidgetBuilder` +- `app/templates/includes/weekly_focus_widget_content.html` — widget partial +- `app/static/css/tailwind.css` — widget styles (`@layer components`) diff --git a/docs/projects/done/P-050_FOCUS_QUEUE.md b/docs/projects/done/P-050_FOCUS_QUEUE.md index 9ade7ff..69cf256 100644 --- a/docs/projects/done/P-050_FOCUS_QUEUE.md +++ b/docs/projects/done/P-050_FOCUS_QUEUE.md @@ -193,3 +193,25 @@ Shipped after the 4 core phases above, in response to real usage: `scripts/my-practice-sync-focus-queue.timer`/`.service`, documented in `docs/operations/SCRIPTS.md` — closes the one gap this doc had flagged as unfinished. Materialized tasks now stay fresh without a manual run. +- **Checkbox in the queue now really toggles** (2026-07-29): it previously + only ever called `mark_completed()`, so an accidental click had no way + back except Django admin. The endpoint now toggles complete/incomplete and + swaps just the one row (`includes/focus_queue_row.html`), so a + just-completed task stays visible — struck through, checkbox still + checked — as an immediate undo affordance. +- **"📅 Today" quick action + due-date-aware sort** (2026-07-30): `due_date` + previously only decorated a row; it now also drives sort order — tasks due + today or overdue always rank above the rest of the queue regardless of + priority tier. A new button next to the snooze presets sets `due_date` to + today in one click. +- **`is_focus` retired, folded into `due_date`** (2026-07-30): resolves the + overlap flagged in line 56 above and in issue #281. The `/todos/` list + page's retirement (phase 4) had already removed the only UI path to *set* + `is_focus` — the dashboard's weekly widget star only ever did "remove + focus" from then on, with no way to add a task to it. Rather than + restoring a second star toggle, `is_focus` was removed outright + (migration `0018_remove_practicetodo_is_focus`) and the dashboard widget + now lists `due_date <= today` tasks — the same signal the Focus Queue + itself sorts on. `todo_toggle_focus` and its URL are gone; see + [P-028_DASHBOARD_WEEKLY_FOCUS.md](P-028_DASHBOARD_WEEKLY_FOCUS.md) for the + widget-side detail.