fix: improve member list defaults and highlighting

This commit is contained in:
Marcel Peterkau
2026-07-24 18:14:49 +02:00
parent 68c24219ec
commit 9942e7aa22
4 changed files with 50 additions and 8 deletions
+3 -1
View File
@@ -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."
]
},
{
+2 -1
View File
@@ -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())
+31 -6
View File
@@ -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("<Double-1>", lambda _event: self._open_selected())
self.tree.bind("<Return>", 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),
+14
View File
@@ -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