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.