mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-24 22:45:18 +02:00
Merge pull request 'fix: improve member list defaults and highlighting' (#5) from fix/member-list-defaults-and-highlights into dev
Reviewed-on: https://git.hiabuto.net/C3MA/CCMA/pulls/5 Reviewed-by: Matcha <matcha@noreply.git.hiabuto.net>
This commit is contained in:
@@ -9,7 +9,9 @@
|
|||||||
"Forderungen können nachträglich bearbeitet werden, ohne zusätzliche Forderungspositionen oder bereits verbuchte Zahlungen zu verlieren.",
|
"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.",
|
"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.",
|
"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."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ class MainWindow(ttk.Frame):
|
|||||||
tab = MembersTab(
|
tab = MembersTab(
|
||||||
self.notebook,
|
self.notebook,
|
||||||
self.repository.list_members(),
|
self.repository.list_members(),
|
||||||
|
self.findings,
|
||||||
self.open_member,
|
self.open_member,
|
||||||
lambda: self.tabs.close(key),
|
lambda: self.tabs.close(key),
|
||||||
)
|
)
|
||||||
@@ -502,7 +503,7 @@ class MainWindow(ttk.Frame):
|
|||||||
self.dashboard.update_data(self.repository.member_count(), self.findings)
|
self.dashboard.update_data(self.repository.member_count(), self.findings)
|
||||||
members_tab = self.tabs.tabs.get("members")
|
members_tab = self.tabs.tabs.get("members")
|
||||||
if isinstance(members_tab, MembersTab) and members_tab.winfo_exists():
|
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")
|
assets_tab = self.tabs.tabs.get("assets")
|
||||||
if isinstance(assets_tab, AssetsTab) and assets_tab.winfo_exists():
|
if isinstance(assets_tab, AssetsTab) and assets_tab.winfo_exists():
|
||||||
assets_tab.refresh(self.repository.list_assets())
|
assets_tab.refresh(self.repository.list_assets())
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ from ccma.ui.sections import titled_frame
|
|||||||
|
|
||||||
MEMBER_TABLE_COLUMNS = (
|
MEMBER_TABLE_COLUMNS = (
|
||||||
("number", "Nummer", 110),
|
("number", "Nummer", 110),
|
||||||
("first_name", "Vorname", 160),
|
|
||||||
("last_name", "Nachname", 180),
|
("last_name", "Nachname", 180),
|
||||||
|
("first_name", "Vorname", 160),
|
||||||
("nickname", "Nickname", 160),
|
("nickname", "Nickname", 160),
|
||||||
("email", "E-Mail-Adresse", 270),
|
("email", "E-Mail-Adresse", 270),
|
||||||
("birth", "Geburtsdatum", 120),
|
("birth", "Geburtsdatum", 120),
|
||||||
@@ -25,6 +25,12 @@ MEMBER_TABLE_COLUMNS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
STATUS_FILTER_ALL = "Alle"
|
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_FILTER_ALL = "Alle"
|
||||||
ASSET_TABLE_COLUMNS = (
|
ASSET_TABLE_COLUMNS = (
|
||||||
("label", "Bezeichnung", 260),
|
("label", "Bezeichnung", 260),
|
||||||
@@ -75,6 +81,17 @@ def _selected_status_filter(label: str) -> str:
|
|||||||
return storage_key(MEMBERSHIP_STATUS_LABELS, label)
|
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:
|
def _filter_label_frame(parent: tk.Misc) -> ttk.LabelFrame:
|
||||||
return titled_frame(parent, "// FILTER")
|
return titled_frame(parent, "// FILTER")
|
||||||
|
|
||||||
@@ -225,11 +242,13 @@ class MembersTab(ttk.Frame):
|
|||||||
self,
|
self,
|
||||||
master: tk.Misc,
|
master: tk.Misc,
|
||||||
members: list[Member],
|
members: list[Member],
|
||||||
|
findings: list[HousekeeperFinding],
|
||||||
on_open: Callable[[str], None],
|
on_open: Callable[[str], None],
|
||||||
on_close: Callable[[], None],
|
on_close: Callable[[], None],
|
||||||
):
|
):
|
||||||
super().__init__(master, padding=12)
|
super().__init__(master, padding=12)
|
||||||
self.members = members
|
self.members = members
|
||||||
|
self.findings = findings
|
||||||
self.on_open = on_open
|
self.on_open = on_open
|
||||||
self.on_close = on_close
|
self.on_close = on_close
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
@@ -250,12 +269,12 @@ class MembersTab(ttk.Frame):
|
|||||||
filter_row.grid(row=0, column=0, sticky="w")
|
filter_row.grid(row=0, column=0, sticky="w")
|
||||||
self.tree = ttk.Treeview(
|
self.tree = ttk.Treeview(
|
||||||
self,
|
self,
|
||||||
columns=("number", "first_name", "last_name", "nickname", "email", "birth", "status"),
|
columns=tuple(column for column, _title, _width in MEMBER_TABLE_COLUMNS),
|
||||||
show="headings",
|
show="headings",
|
||||||
)
|
)
|
||||||
self.sort_column = "last_name"
|
self.sort_column = "last_name"
|
||||||
self.sort_descending = False
|
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))
|
ttk.Label(filter_row, text="Status").grid(row=0, column=0, sticky="w", padx=(0, 8))
|
||||||
self.status_filter = ttk.Combobox(
|
self.status_filter = ttk.Combobox(
|
||||||
filter_row,
|
filter_row,
|
||||||
@@ -269,13 +288,16 @@ class MembersTab(ttk.Frame):
|
|||||||
for key, title, width in MEMBER_TABLE_COLUMNS:
|
for key, title, width in MEMBER_TABLE_COLUMNS:
|
||||||
self.tree.heading(key, text=title, command=lambda column=key: self._toggle_sort(column))
|
self.tree.heading(key, text=title, command=lambda column=key: self._toggle_sort(column))
|
||||||
self.tree.column(key, width=width, anchor="w")
|
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.grid(row=2, column=0, sticky="nsew")
|
||||||
self.tree.bind("<Double-1>", lambda _event: self._open_selected())
|
self.tree.bind("<Double-1>", lambda _event: self._open_selected())
|
||||||
self.tree.bind("<Return>", 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.members = members
|
||||||
|
self.findings = findings
|
||||||
self._render_members()
|
self._render_members()
|
||||||
|
|
||||||
def _render_members(self) -> None:
|
def _render_members(self) -> None:
|
||||||
@@ -283,20 +305,23 @@ class MembersTab(ttk.Frame):
|
|||||||
status_filter = _selected_status_filter(self.status_filter_var.get())
|
status_filter = _selected_status_filter(self.status_filter_var.get())
|
||||||
filtered_members = _filter_members(self.members, status_filter)
|
filtered_members = _filter_members(self.members, status_filter)
|
||||||
sorted_members = _sort_members(filtered_members, self.sort_column, self.sort_descending)
|
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):
|
if len(filtered_members) == len(self.members):
|
||||||
self.count_var.set(f"{len(filtered_members)} Mitglieder")
|
self.count_var.set(f"{len(filtered_members)} Mitglieder")
|
||||||
else:
|
else:
|
||||||
self.count_var.set(f"{len(filtered_members)} / {len(self.members)} Mitglieder")
|
self.count_var.set(f"{len(filtered_members)} / {len(self.members)} Mitglieder")
|
||||||
self._update_tree_headings()
|
self._update_tree_headings()
|
||||||
for member in sorted_members:
|
for member in sorted_members:
|
||||||
|
severity = finding_severities.get(member.member_id)
|
||||||
self.tree.insert(
|
self.tree.insert(
|
||||||
"",
|
"",
|
||||||
"end",
|
"end",
|
||||||
iid=member.member_id,
|
iid=member.member_id,
|
||||||
|
tags=(f"finding-{severity}",) if severity else (),
|
||||||
values=(
|
values=(
|
||||||
member.member_number,
|
member.member_number,
|
||||||
member.first_name,
|
|
||||||
member.last_name,
|
member.last_name,
|
||||||
|
member.first_name,
|
||||||
member.nickname,
|
member.nickname,
|
||||||
member.email,
|
member.email,
|
||||||
format_date_for_display(member.birth_date),
|
format_date_for_display(member.birth_date),
|
||||||
|
|||||||
@@ -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"]
|
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:
|
def test_asset_table_filter_and_sort_use_status_and_holder_label() -> None:
|
||||||
from ccma.domain.models import Asset
|
from ccma.domain.models import Asset
|
||||||
from ccma.ui.work_tabs import _asset_table_value, _filter_assets, _selected_asset_filter
|
from ccma.ui.work_tabs import _asset_table_value, _filter_assets, _selected_asset_filter
|
||||||
|
|||||||
Reference in New Issue
Block a user