feat(ui): one CountBadge for all counters; counters on tab corners
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m30s

- CountBadge component (accent / muted, inline / corner, 99+, showZero);
  used in the nav bar, task list and list tabs, admin organizations and
  numeric badges of the mobile menu; old badge-count removed
- Counters sit on the top right corner of tabs; "Alle Aufgaben" gets one
  (desktop tab and mobile menu)
- Open/done task counters include tasks in shared lists, matching what
  "Alle Aufgaben" shows (nav counter was too low)
- Theme template derives --accent-dim/--accent-border from the accent
- Docs: ui-patterns counters section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 22:54:15 +01:00
1 parent a354f3d008
commit a62b204e14
12 files changed
+167 -55

No files matched your search

+4 -2
View File
@@ -4080,8 +4080,10 @@ func (s *Store) ValidateAPIKeyAndGetUser(rawKey string) (*domain.User, error) {
func (s *Store) GetDashboardStats(userID string) (*domain.DashboardStats, error) {
st := &domain.DashboardStats{}
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE user_id = ? AND status = 'inbox'`, userID).Scan(&st.InboxCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE user_id = ? AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task')) AND is_completed = 0`, userID).Scan(&st.TasksOpenCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE user_id = ? AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task')) AND is_completed = 1`, userID).Scan(&st.TasksDoneCount)
// Same tasks as GetTasks without a list ("Alle Aufgaben"): own ones plus those in shared task lists
taskScope := `(user_id = ? OR task_list_id IN (` + s.accessibleTaskListIDsQuery() + `)) AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))`
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE `+taskScope+` AND is_completed = 0`, userID, userID, userID, userID).Scan(&st.TasksOpenCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE `+taskScope+` AND is_completed = 1`, userID, userID, userID, userID).Scan(&st.TasksDoneCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE user_id = ? AND is_archived = 0`, userID).Scan(&st.DocsCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE user_id = ? AND status = 'discarded'`, userID).Scan(&st.DiscardedCount)
_ = s.db.QueryRow(`
+42
View File
@@ -120,3 +120,45 @@ func assertAssignees(t *testing.T, store *Store, viewerID, taskID string, want .
}
}
}
// The open-task counter (nav badge, "Alle Aufgaben") counts what "Alle Aufgaben" shows,
// including tasks others created in a shared list
func TestDashboardCountsTasksOfSharedLists(t *testing.T) {
store, err := NewStore(filepath.Join(t.TempDir(), "stats.db"))
if err != nil {
t.Fatalf("NewStore failed: %v", err)
}
defer store.Close()
openSharing(t, store)
owner, _ := store.CreateUser("owner@example.com", "hash", "Owner", "member")
member, _ := store.CreateUser("member@example.com", "hash", "Member", "member")
tl, _ := store.CreateTaskList(owner.ID, "", "", "Haushalt", "🏠", "")
if _, err := store.AddTaskListShare(tl.ID, "user", member.ID, "editor", owner.ID); err != nil {
t.Fatalf("share: %v", err)
}
for i, title := range []string{"Bad putzen", "Müll raus"} {
th, _ := store.CreateThoughtWithDetails(owner.ID, title, "normal", "manual", "", tl.ID, "todo", "")
_, _ = store.UpdateThoughtTriage(owner.ID, th.ID, "keep", "task", "", "", tl.ID)
if i == 1 {
done := true
_, _ = store.UpdateThoughtDetails(owner.ID, th.ID, nil, &done, nil, nil, nil, nil, nil, nil, nil)
}
}
own, _ := store.CreateThoughtWithDetails(member.ID, "Eigene Aufgabe", "normal", "manual", "", "", "todo", "")
_, _ = store.UpdateThoughtTriage(member.ID, own.ID, "keep", "task", "", "", "")
st, err := store.GetDashboardStats(member.ID)
if err != nil {
t.Fatalf("GetDashboardStats: %v", err)
}
tasks, _ := store.GetTasks(member.ID, "")
open := 0
for _, task := range tasks {
if !task.IsCompleted {
open++
}
}
if st.TasksOpenCount != 2 || st.TasksOpenCount != open || st.TasksDoneCount != 1 {
t.Fatalf("expected 2 open / 1 done matching GetTasks (%d open), got %d / %d", open, st.TasksOpenCount, st.TasksDoneCount)
}
}