mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 03:04:52 +02:00
Refine member and asset detail layouts
This commit is contained in:
+143
-30
@@ -12,8 +12,11 @@ from ccma.domain.models import ASSET_STATUS_LABELS, MEMBERSHIP_STATUS_LABELS as
|
||||
from ccma.domain.models import Event
|
||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||
from ccma.ui.document_dialog import DocumentTemplateDialog
|
||||
from ccma.ui.dialogs import IntegrityWarningDialog
|
||||
from ccma.ui.file_open import open_path
|
||||
from ccma.ui.labels import display_label, storage_key
|
||||
from ccma.ui.messages import MessageAction, MessageBannerList, TabMessage
|
||||
from ccma.ui.scrolling import ScrollableFrame
|
||||
|
||||
|
||||
CLAIM_TABLE_COLUMNS = (
|
||||
@@ -61,30 +64,46 @@ class MemberTab(ttk.Frame):
|
||||
self.on_return_asset = on_return_asset
|
||||
self.member = repository.get_member(member_id)
|
||||
self.variables: dict[str, tk.Variable] = {}
|
||||
self._field_sections: dict[str, str] = {}
|
||||
self._dirty_sections: set[str] = set()
|
||||
self._form_tabs: dict[str, str] = {}
|
||||
self._form_tab_titles: dict[str, str] = {}
|
||||
self._loading = False
|
||||
self.notes_text: tk.Text | None = None
|
||||
self._build_ui()
|
||||
self.refresh()
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(1, weight=1)
|
||||
self.rowconfigure(2, weight=1)
|
||||
header = ttk.Frame(self)
|
||||
header.grid(row=0, column=0, sticky="ew", pady=(0, 10))
|
||||
header.columnconfigure(0, weight=1)
|
||||
header.columnconfigure(1, weight=0)
|
||||
self.title_var = tk.StringVar()
|
||||
self.status_var = tk.StringVar()
|
||||
ttk.Label(header, textvariable=self.title_var, style="TabTitle.TLabel").grid(
|
||||
self.id_var = tk.StringVar()
|
||||
title_column = ttk.Frame(header)
|
||||
title_column.grid(row=0, column=0, sticky="ew")
|
||||
title_column.columnconfigure(0, weight=1)
|
||||
ttk.Label(title_column, textvariable=self.title_var, style="TabTitle.TLabel").grid(
|
||||
row=0, column=0, sticky="w"
|
||||
)
|
||||
ttk.Label(header, textvariable=self.status_var, style="Mono.TLabel").grid(
|
||||
ttk.Label(title_column, textvariable=self.status_var, style="Mono.TLabel").grid(
|
||||
row=1, column=0, sticky="w", pady=(3, 0)
|
||||
)
|
||||
ttk.Button(header, text="Tab schließen", command=self.on_close).grid(
|
||||
row=0, column=1, rowspan=2, sticky="e"
|
||||
ttk.Label(title_column, textvariable=self.id_var, style="Mono.TLabel").grid(
|
||||
row=2, column=0, sticky="w", pady=(3, 0)
|
||||
)
|
||||
ttk.Button(header, text="Tab schließen", command=self._close).grid(
|
||||
row=0, column=1, sticky="ne", padx=(12, 0)
|
||||
)
|
||||
self.messages = MessageBannerList(self)
|
||||
self.messages.grid(row=1, column=0, sticky="ew", pady=(0, 10))
|
||||
self.messages.grid_remove()
|
||||
|
||||
self.pane = ttk.Panedwindow(self, orient="horizontal")
|
||||
self.pane.grid(row=1, column=0, sticky="nsew")
|
||||
self.pane.grid(row=2, column=0, sticky="nsew")
|
||||
self.details_pane = ttk.Frame(self.pane, padding=(0, 0, 10, 0))
|
||||
self.timeline_pane = ttk.Frame(self.pane, padding=(10, 0, 0, 0))
|
||||
self.pane.add(self.details_pane, weight=2)
|
||||
@@ -107,23 +126,34 @@ class MemberTab(ttk.Frame):
|
||||
|
||||
def _build_details(self, parent: ttk.Frame) -> None:
|
||||
parent.columnconfigure(0, weight=1)
|
||||
parent.rowconfigure(0, weight=1)
|
||||
parent.rowconfigure(1, weight=1)
|
||||
ttk.Label(parent, text="// MITGLIED", style="TimelineHeader.TLabel").grid(
|
||||
row=0, column=0, sticky="w", pady=(0, 8)
|
||||
)
|
||||
notebook = ttk.Notebook(parent)
|
||||
notebook.grid(row=0, column=0, sticky="nsew")
|
||||
data_tab = ttk.Frame(notebook, padding=16)
|
||||
address_tab = ttk.Frame(notebook, padding=16)
|
||||
banking_tab = ttk.Frame(notebook, padding=16)
|
||||
self.details_notebook = notebook
|
||||
notebook.grid(row=1, column=0, sticky="nsew")
|
||||
data_tab = self._create_form_tab(notebook, "data", "Stammdaten")
|
||||
address_tab = self._create_form_tab(notebook, "address", "Anschrift")
|
||||
banking_tab = self._create_form_tab(notebook, "banking", "Bank / SEPA")
|
||||
actions = ttk.Frame(parent)
|
||||
actions.grid(row=2, column=0, sticky="ew", pady=(10, 0))
|
||||
actions.columnconfigure(0, weight=1)
|
||||
ttk.Button(
|
||||
actions,
|
||||
text="Mitgliedsdaten speichern",
|
||||
style="Accent.TButton",
|
||||
command=self._save,
|
||||
).grid(row=0, column=0, sticky="e")
|
||||
contribution_tab = ttk.Frame(notebook, padding=16)
|
||||
assets_tab = ttk.Frame(notebook, padding=16)
|
||||
documents_tab = ttk.Frame(notebook, padding=16)
|
||||
notebook.add(data_tab, text="Stammdaten")
|
||||
notebook.add(address_tab, text="Anschrift")
|
||||
notebook.add(banking_tab, text="Bank / SEPA")
|
||||
notebook.add(contribution_tab, text="Forderungen")
|
||||
notebook.add(assets_tab, text="Assets")
|
||||
notebook.add(documents_tab, text="Dokumente")
|
||||
|
||||
fields = [
|
||||
("UUID / Ordner-ID", "member_id"),
|
||||
("Mitgliedsnummer", "member_number"),
|
||||
("Vorname", "first_name"),
|
||||
("Nachname", "last_name"),
|
||||
@@ -136,7 +166,7 @@ class MemberTab(ttk.Frame):
|
||||
]
|
||||
for row, (label, key) in enumerate(fields):
|
||||
variable = tk.StringVar()
|
||||
self.variables[key] = variable
|
||||
self._add_variable(key, variable, "data")
|
||||
ttk.Label(data_tab, text=label).grid(row=row, column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
if key == "birth_date":
|
||||
birth_row = ttk.Frame(data_tab)
|
||||
@@ -151,11 +181,11 @@ class MemberTab(ttk.Frame):
|
||||
"write", lambda *_args, source=variable: self.age_var.set(age_label(source.get()))
|
||||
)
|
||||
else:
|
||||
entry_state = "readonly" if key == "member_number" else "normal"
|
||||
entry_state = "readonly" if key in {"member_id", "member_number"} else "normal"
|
||||
ttk.Entry(data_tab, textvariable=variable, width=42, state=entry_state).grid(
|
||||
row=row, column=1, sticky="ew", pady=5
|
||||
)
|
||||
self.variables["status"] = tk.StringVar()
|
||||
self._add_variable("status", tk.StringVar(), "data")
|
||||
ttk.Label(data_tab, text="Status").grid(row=len(fields), column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
ttk.Combobox(
|
||||
data_tab,
|
||||
@@ -169,10 +199,8 @@ class MemberTab(ttk.Frame):
|
||||
)
|
||||
self.notes_text = tk.Text(data_tab, width=42, height=6, wrap="word")
|
||||
self.notes_text.grid(row=len(fields) + 1, column=1, sticky="ew", pady=5)
|
||||
self.notes_text.bind("<<Modified>>", lambda _event: self._mark_dirty_from_text("data"), add="+")
|
||||
data_tab.columnconfigure(1, weight=1)
|
||||
ttk.Button(data_tab, text="Änderungen speichern", style="Accent.TButton", command=self._save).grid(
|
||||
row=len(fields) + 2, column=1, sticky="e", pady=(18, 0)
|
||||
)
|
||||
|
||||
address_fields = (
|
||||
("Straße und Hausnummer", "street"),
|
||||
@@ -183,14 +211,11 @@ class MemberTab(ttk.Frame):
|
||||
)
|
||||
address_tab.columnconfigure(1, weight=1)
|
||||
for row, (label, key) in enumerate(address_fields):
|
||||
self.variables[key] = tk.StringVar()
|
||||
self._add_variable(key, tk.StringVar(), "address")
|
||||
ttk.Label(address_tab, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5)
|
||||
ttk.Entry(address_tab, textvariable=self.variables[key], width=42).grid(
|
||||
row=row, column=1, sticky="ew", pady=5
|
||||
)
|
||||
ttk.Button(
|
||||
address_tab, text="Anschrift speichern", style="Accent.TButton", command=self._save
|
||||
).grid(row=len(address_fields), column=1, sticky="e", pady=(18, 0))
|
||||
|
||||
banking_fields = (
|
||||
("Kontoinhaber", "account_holder"),
|
||||
@@ -202,22 +227,18 @@ class MemberTab(ttk.Frame):
|
||||
)
|
||||
banking_tab.columnconfigure(1, weight=1)
|
||||
for row, (label, key) in enumerate(banking_fields):
|
||||
self.variables[key] = tk.StringVar()
|
||||
self._add_variable(key, tk.StringVar(), "banking")
|
||||
ttk.Label(banking_tab, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5)
|
||||
ttk.Entry(banking_tab, textvariable=self.variables[key], width=42).grid(
|
||||
row=row, column=1, sticky="ew", pady=5
|
||||
)
|
||||
self.variables["mandate_active"] = tk.BooleanVar()
|
||||
self._add_variable("mandate_active", tk.BooleanVar(), "banking")
|
||||
ttk.Checkbutton(
|
||||
banking_tab,
|
||||
text="SEPA-Lastschriftmandat ist aktiv",
|
||||
variable=self.variables["mandate_active"],
|
||||
style="Switch",
|
||||
).grid(row=len(banking_fields), column=0, columnspan=2, sticky="w", pady=(12, 5))
|
||||
ttk.Button(
|
||||
banking_tab, text="Bankdaten speichern", style="Accent.TButton", command=self._save
|
||||
).grid(row=len(banking_fields) + 1, column=1, sticky="e", pady=(18, 0))
|
||||
|
||||
contribution_tab.columnconfigure(0, weight=1)
|
||||
contribution_tab.rowconfigure(1, weight=1)
|
||||
self.contribution_summary = tk.StringVar()
|
||||
@@ -308,6 +329,53 @@ class MemberTab(ttk.Frame):
|
||||
self.documents.bind("<Return>", lambda _event: self._open_selected_document())
|
||||
self.document_paths: dict[str, Path] = {}
|
||||
|
||||
def _create_form_tab(self, notebook: ttk.Notebook, section: str, title: str) -> ttk.Frame:
|
||||
tab = ttk.Frame(notebook)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
tab.rowconfigure(0, weight=1)
|
||||
scroller = ScrollableFrame(tab, padding=16)
|
||||
scroller.grid(row=0, column=0, sticky="nsew")
|
||||
notebook.add(tab, text=title)
|
||||
self._form_tabs[section] = str(tab)
|
||||
self._form_tab_titles[section] = title
|
||||
return scroller.content
|
||||
|
||||
def _add_variable(self, key: str, variable: tk.Variable, section: str) -> None:
|
||||
self.variables[key] = variable
|
||||
self._field_sections[key] = section
|
||||
variable.trace_add("write", lambda *_args, target=section: self._mark_dirty(target))
|
||||
|
||||
def _mark_dirty(self, section: str) -> None:
|
||||
if self._loading:
|
||||
return
|
||||
self._dirty_sections.add(section)
|
||||
self._refresh_dirty_tabs()
|
||||
|
||||
def _mark_dirty_from_text(self, section: str) -> None:
|
||||
if self.notes_text is None or not self.notes_text.edit_modified():
|
||||
return
|
||||
self.notes_text.edit_modified(False)
|
||||
self._mark_dirty(section)
|
||||
|
||||
def _refresh_dirty_tabs(self) -> None:
|
||||
for section, tab_id in self._form_tabs.items():
|
||||
title = self._form_tab_titles[section]
|
||||
suffix = " *" if section in self._dirty_sections else ""
|
||||
self.details_notebook.tab(tab_id, text=f"{title}{suffix}")
|
||||
|
||||
def _clear_dirty(self) -> None:
|
||||
self._dirty_sections.clear()
|
||||
self._refresh_dirty_tabs()
|
||||
|
||||
def _close(self) -> None:
|
||||
if self._dirty_sections and not messagebox.askokcancel(
|
||||
"Ungespeicherte Änderungen",
|
||||
"Es gibt ungespeicherte Änderungen an den Mitgliedsdaten. Tab trotzdem schließen?",
|
||||
parent=self,
|
||||
):
|
||||
return
|
||||
self.on_close()
|
||||
|
||||
def _build_timeline(self, parent: ttk.Frame) -> None:
|
||||
parent.columnconfigure(0, weight=1)
|
||||
parent.rowconfigure(1, weight=1)
|
||||
@@ -332,9 +400,23 @@ class MemberTab(ttk.Frame):
|
||||
ttk.Button(compose, text="Kommentar", command=self._add_comment).grid(row=0, column=1)
|
||||
|
||||
def refresh(self) -> None:
|
||||
self._loading = True
|
||||
self.member = self.repository.get_member(self.member_id)
|
||||
self.title_var.set(f"{self.member.member_number or '—'} · {self.member.display_name}")
|
||||
self.status_var.set(STATUS_LABELS.get(self.member.status, self.member.status.upper()))
|
||||
self.id_var.set(f"UUID: {self.member.member_id}")
|
||||
warnings = self.repository.member_hash_warnings(self.member_id)
|
||||
self.messages.set_messages(
|
||||
[
|
||||
TabMessage(
|
||||
"warning",
|
||||
"WARNUNG: " + " | ".join(warnings),
|
||||
MessageAction("Überprüft, bestätigen", self._confirm_integrity_banner),
|
||||
)
|
||||
]
|
||||
if warnings
|
||||
else []
|
||||
)
|
||||
date_fields = {
|
||||
"birth_date",
|
||||
"accepted_at",
|
||||
@@ -351,6 +433,9 @@ class MemberTab(ttk.Frame):
|
||||
if self.notes_text is not None:
|
||||
self.notes_text.delete("1.0", "end")
|
||||
self.notes_text.insert("1.0", self.member.notes)
|
||||
self.notes_text.edit_modified(False)
|
||||
self._loading = False
|
||||
self._clear_dirty()
|
||||
self._refresh_events()
|
||||
self._refresh_contributions()
|
||||
self._refresh_assets()
|
||||
@@ -460,6 +545,13 @@ class MemberTab(ttk.Frame):
|
||||
)
|
||||
|
||||
def _save(self) -> None:
|
||||
warnings = self.repository.member_hash_warnings(self.member_id)
|
||||
if warnings:
|
||||
self._confirm_integrity_and_then(self._save_confirmed)
|
||||
return
|
||||
self._save_confirmed()
|
||||
|
||||
def _save_confirmed(self) -> None:
|
||||
for key, variable in self.variables.items():
|
||||
raw_value = variable.get()
|
||||
value = raw_value.strip() if isinstance(raw_value, str) else raw_value
|
||||
@@ -470,12 +562,33 @@ class MemberTab(ttk.Frame):
|
||||
self.member.notes = self.notes_text.get("1.0", "end-1c").strip()
|
||||
try:
|
||||
self.repository.save_member(self.member)
|
||||
self.repository.refresh_member_record_hashes(self.member_id)
|
||||
except RepositoryError as exc:
|
||||
messagebox.showerror("Speichern fehlgeschlagen", str(exc), parent=self)
|
||||
return
|
||||
self.refresh()
|
||||
self.on_changed()
|
||||
|
||||
def _confirm_integrity_banner(self) -> None:
|
||||
self._confirm_integrity_and_then(self._refresh_hashes_only)
|
||||
|
||||
def _refresh_hashes_only(self) -> None:
|
||||
self.repository.refresh_member_record_hashes(self.member_id)
|
||||
self.refresh()
|
||||
self.on_changed()
|
||||
|
||||
def _confirm_integrity_and_then(self, callback: Callable[[], None]) -> None:
|
||||
warnings = self.repository.member_hash_warnings(self.member_id)
|
||||
if not warnings:
|
||||
callback()
|
||||
return
|
||||
IntegrityWarningDialog(
|
||||
self,
|
||||
title="Externe Änderungen bestätigen",
|
||||
warnings=warnings,
|
||||
on_confirm=callback,
|
||||
)
|
||||
|
||||
def _add_comment(self) -> None:
|
||||
text = self.comment_var.get().strip()
|
||||
if not text:
|
||||
|
||||
Reference in New Issue
Block a user