From 9942e7aa221bc9a66d24dd1d66613dc957b04c4d Mon Sep 17 00:00:00 2001 From: Marcel Peterkau Date: Fri, 24 Jul 2026 18:14:49 +0200 Subject: [PATCH] fix: improve member list defaults and highlighting --- src/ccma/assets/CHANGELOG.json | 4 +++- src/ccma/ui/main_window.py | 3 ++- src/ccma/ui/work_tabs.py | 37 ++++++++++++++++++++++++++++------ tests/test_ui_imports.py | 14 +++++++++++++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/ccma/assets/CHANGELOG.json b/src/ccma/assets/CHANGELOG.json index 89101d4..47896bb 100644 --- a/src/ccma/assets/CHANGELOG.json +++ b/src/ccma/assets/CHANGELOG.json @@ -9,7 +9,9 @@ "Forderungen können nachträglich bearbeitet werden, ohne zusätzliche Forderungspositionen oder bereits verbuchte Zahlungen zu verlieren.", "Zahlungen können bearbeitet, neu auf mehrere Forderungen verteilt oder einschließlich ihrer Zuordnungen gelöscht werden.", "Das Austrittsdatum eines Mitglieds wird gespeichert und validiert; automatische Beitragsforderungen werden nicht mehr für Fälligkeiten nach dem Mitgliedschaftsende erzeugt.", - "Anteilig berechnete Mitgliedsbeiträge weisen den abgerechneten Zeitraum verständlicher in ihrer Beschreibung aus." + "Anteilig berechnete Mitgliedsbeiträge weisen den abgerechneten Zeitraum verständlicher in ihrer Beschreibung aus.", + "Die Mitgliederliste zeigt standardmäßig aktive Mitglieder, ordnet Nachname vor Vorname an und sortiert aufsteigend nach Nachnamen.", + "Mitglieder mit offenen Hausmeister-Vorgängen werden in der Liste je nach höchster Dringlichkeit blau, gelb oder rot hervorgehoben." ] }, { diff --git a/src/ccma/ui/main_window.py b/src/ccma/ui/main_window.py index 51b2145..742d5fe 100644 --- a/src/ccma/ui/main_window.py +++ b/src/ccma/ui/main_window.py @@ -368,6 +368,7 @@ class MainWindow(ttk.Frame): tab = MembersTab( self.notebook, self.repository.list_members(), + self.findings, self.open_member, lambda: self.tabs.close(key), ) @@ -502,7 +503,7 @@ class MainWindow(ttk.Frame): self.dashboard.update_data(self.repository.member_count(), self.findings) members_tab = self.tabs.tabs.get("members") if isinstance(members_tab, MembersTab) and members_tab.winfo_exists(): - members_tab.refresh(self.repository.list_members()) + members_tab.refresh(self.repository.list_members(), self.findings) assets_tab = self.tabs.tabs.get("assets") if isinstance(assets_tab, AssetsTab) and assets_tab.winfo_exists(): assets_tab.refresh(self.repository.list_assets()) diff --git a/src/ccma/ui/work_tabs.py b/src/ccma/ui/work_tabs.py index 3029d1a..9c8b3f6 100644 --- a/src/ccma/ui/work_tabs.py +++ b/src/ccma/ui/work_tabs.py @@ -16,8 +16,8 @@ from ccma.ui.sections import titled_frame MEMBER_TABLE_COLUMNS = ( ("number", "Nummer", 110), - ("first_name", "Vorname", 160), ("last_name", "Nachname", 180), + ("first_name", "Vorname", 160), ("nickname", "Nickname", 160), ("email", "E-Mail-Adresse", 270), ("birth", "Geburtsdatum", 120), @@ -25,6 +25,12 @@ MEMBER_TABLE_COLUMNS = ( ) STATUS_FILTER_ALL = "Alle" +MEMBER_FINDING_COLORS = { + "info": "#d9efff", + "warning": "#fff3b0", + "error": "#ffd0d0", +} +MEMBER_FINDING_PRIORITY = {"info": 1, "warning": 2, "error": 3} ASSET_FILTER_ALL = "Alle" ASSET_TABLE_COLUMNS = ( ("label", "Bezeichnung", 260), @@ -75,6 +81,17 @@ def _selected_status_filter(label: str) -> str: return storage_key(MEMBERSHIP_STATUS_LABELS, label) +def _member_finding_severities(findings: list[HousekeeperFinding]) -> dict[str, str]: + severities: dict[str, str] = {} + for finding in findings: + if not finding.member_id or finding.severity not in MEMBER_FINDING_PRIORITY: + continue + current = severities.get(finding.member_id) + if current is None or MEMBER_FINDING_PRIORITY[finding.severity] > MEMBER_FINDING_PRIORITY[current]: + severities[finding.member_id] = finding.severity + return severities + + def _filter_label_frame(parent: tk.Misc) -> ttk.LabelFrame: return titled_frame(parent, "// FILTER") @@ -225,11 +242,13 @@ class MembersTab(ttk.Frame): self, master: tk.Misc, members: list[Member], + findings: list[HousekeeperFinding], on_open: Callable[[str], None], on_close: Callable[[], None], ): super().__init__(master, padding=12) self.members = members + self.findings = findings self.on_open = on_open self.on_close = on_close self._build_ui() @@ -250,12 +269,12 @@ class MembersTab(ttk.Frame): filter_row.grid(row=0, column=0, sticky="w") self.tree = ttk.Treeview( self, - columns=("number", "first_name", "last_name", "nickname", "email", "birth", "status"), + columns=tuple(column for column, _title, _width in MEMBER_TABLE_COLUMNS), show="headings", ) self.sort_column = "last_name" self.sort_descending = False - self.status_filter_var = tk.StringVar(value=STATUS_FILTER_ALL) + self.status_filter_var = tk.StringVar(value=MEMBERSHIP_STATUS_LABELS["active"]) ttk.Label(filter_row, text="Status").grid(row=0, column=0, sticky="w", padx=(0, 8)) self.status_filter = ttk.Combobox( filter_row, @@ -269,13 +288,16 @@ class MembersTab(ttk.Frame): for key, title, width in MEMBER_TABLE_COLUMNS: self.tree.heading(key, text=title, command=lambda column=key: self._toggle_sort(column)) self.tree.column(key, width=width, anchor="w") + for severity, color in MEMBER_FINDING_COLORS.items(): + self.tree.tag_configure(f"finding-{severity}", background=color, foreground="#202020") self.tree.grid(row=2, column=0, sticky="nsew") self.tree.bind("", lambda _event: self._open_selected()) self.tree.bind("", lambda _event: self._open_selected()) - self.refresh(self.members) + self.refresh(self.members, self.findings) - def refresh(self, members: list[Member]) -> None: + def refresh(self, members: list[Member], findings: list[HousekeeperFinding]) -> None: self.members = members + self.findings = findings self._render_members() def _render_members(self) -> None: @@ -283,20 +305,23 @@ class MembersTab(ttk.Frame): status_filter = _selected_status_filter(self.status_filter_var.get()) filtered_members = _filter_members(self.members, status_filter) sorted_members = _sort_members(filtered_members, self.sort_column, self.sort_descending) + finding_severities = _member_finding_severities(self.findings) if len(filtered_members) == len(self.members): self.count_var.set(f"{len(filtered_members)} Mitglieder") else: self.count_var.set(f"{len(filtered_members)} / {len(self.members)} Mitglieder") self._update_tree_headings() for member in sorted_members: + severity = finding_severities.get(member.member_id) self.tree.insert( "", "end", iid=member.member_id, + tags=(f"finding-{severity}",) if severity else (), values=( member.member_number, - member.first_name, member.last_name, + member.first_name, member.nickname, member.email, format_date_for_display(member.birth_date), diff --git a/tests/test_ui_imports.py b/tests/test_ui_imports.py index c3a8f21..0101aa7 100644 --- a/tests/test_ui_imports.py +++ b/tests/test_ui_imports.py @@ -131,6 +131,20 @@ def test_member_table_sort_uses_display_values() -> None: assert [member.member_id for member in _sort_members(members, "status", False)] == ["2", "1", "3"] +def test_member_finding_severity_uses_highest_level_per_member() -> None: + from ccma.domain.models import HousekeeperFinding + from ccma.ui.work_tabs import _member_finding_severities + + findings = [ + HousekeeperFinding("info", "info", "Info", "", member_id="1"), + HousekeeperFinding("warning", "warning", "Warning", "", member_id="1"), + HousekeeperFinding("error", "error", "Error", "", member_id="2"), + HousekeeperFinding("info", "global", "Global", ""), + ] + + assert _member_finding_severities(findings) == {"1": "warning", "2": "error"} + + def test_asset_table_filter_and_sort_use_status_and_holder_label() -> None: from ccma.domain.models import Asset from ccma.ui.work_tabs import _asset_table_value, _filter_assets, _selected_asset_filter