feat: initialize CCMA member administration

This commit is contained in:
Marcel Peterkau
2026-06-21 16:46:15 +02:00
parent 4c6a1191ee
commit dfd5b1192b
184 changed files with 5051 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
from __future__ import annotations
import threading
import tkinter as tk
from collections.abc import Callable
from dataclasses import dataclass
from queue import Empty, Queue
from tkinter import ttk
from ccma import __version__
from ccma.domain.models import HousekeeperFinding
from ccma.services.housekeeper import Housekeeper, HousekeeperSettings
from ccma.storage.repository import MemberRepository
from ccma.ui.monitors import MonitorBounds, preferred_monitor
@dataclass(slots=True)
class StartupResult:
repository: MemberRepository
validation_errors: list[str]
findings: list[HousekeeperFinding]
class SplashScreen(tk.Toplevel):
def __init__(
self,
master: tk.Tk,
repository: MemberRepository,
on_complete: Callable[[StartupResult], None],
on_error: Callable[[Exception], None],
*,
run_housekeeper: bool = True,
monitor: MonitorBounds | None = None,
housekeeper_settings: HousekeeperSettings | None = None,
):
super().__init__(master)
self.repository = repository
self.on_complete = on_complete
self.on_error = on_error
self.run_housekeeper = run_housekeeper
self.monitor = monitor
self.housekeeper_settings = housekeeper_settings
self._messages: Queue[tuple[str, object]] = Queue()
self.overrideredirect(True)
self.resizable(False, False)
self.configure(background="#0b1117")
self.canvas: tk.Canvas
self.status_item: int
self._build_ui()
self._center()
self.after(120, self._start)
def _build_ui(self) -> None:
width, height = 620, 330
self.geometry(f"{width}x{height}")
self.canvas = tk.Canvas(
self,
width=width,
height=height,
highlightthickness=0,
background="#0b1117",
)
self.canvas.pack(fill="both", expand=True)
self.canvas.create_rectangle(0, 0, width, 6, fill="#00d084", outline="")
self.canvas.create_text(
34,
58,
anchor="w",
text="CCMA",
fill="#00d084",
font=("TkFixedFont", 30, "bold"),
)
self.canvas.create_text(
34,
101,
anchor="w",
text="CHAOTIC CREATURE MEMBER ADMINISTRATION",
fill="#d7e0e8",
font=("TkFixedFont", 12, "bold"),
)
self.canvas.create_text(
34,
139,
anchor="w",
text="Chaos Computer Club Mannheim e.V.",
fill="#7f8e9b",
font=("TkDefaultFont", 10),
)
self.canvas.create_text(
width - 34, 58, anchor="e", text=f"v{__version__}", fill="#7f8e9b", font=("TkFixedFont", 10)
)
self.status_item = self.canvas.create_text(
34,
245,
anchor="w",
text="Initialisiere …",
fill="#d7e0e8",
font=("TkFixedFont", 10),
)
self.progress = ttk.Progressbar(self, mode="indeterminate")
self.canvas.create_window(34, 280, anchor="w", width=552, window=self.progress)
def _center(self) -> None:
self.update_idletasks()
width, height = self.winfo_width(), self.winfo_height()
monitor = self.monitor or preferred_monitor(self)
pointer_x, pointer_y = monitor.center
x, y = centered_position(
width=width,
height=height,
pointer_x=pointer_x,
pointer_y=pointer_y,
screen_x=monitor.x,
screen_y=monitor.y,
screen_width=monitor.width,
screen_height=monitor.height,
)
self.geometry(f"{width}x{height}+{x}+{y}")
def _start(self) -> None:
self.progress.start(10)
def worker() -> None:
try:
self._messages.put(("status", "Öffne Mitglieder-Store …"))
self.repository.initialize()
self._messages.put(("status", "Validiere Mitgliederakten …"))
errors = self.repository.validate()
self._messages.put(("status", "Baue Suchindex …"))
self.repository.list_members()
findings = []
if self.run_housekeeper:
self._messages.put(("status", "Starte Hausmeister …"))
findings = Housekeeper(self.repository, self.housekeeper_settings).run()
result = StartupResult(self.repository, errors, findings)
self._messages.put(("result", result))
except Exception as exc:
self._messages.put(("error", exc))
threading.Thread(target=worker, name="ccma-startup", daemon=True).start()
self.after(30, self._poll_messages)
def _poll_messages(self) -> None:
keep_polling = True
try:
while True:
kind, payload = self._messages.get_nowait()
if kind == "status":
self._set_status(str(payload))
elif kind == "result":
keep_polling = False
self._finish(payload) # type: ignore[arg-type]
elif kind == "error":
keep_polling = False
self._fail(payload) # type: ignore[arg-type]
except Empty:
pass
if keep_polling and self.winfo_exists():
self.after(30, self._poll_messages)
def _finish(self, result: StartupResult) -> None:
self.progress.stop()
self._set_status(f"Bereit · {len(result.findings)} Vorgänge benötigen Aufmerksamkeit")
self.after(350, lambda: self._complete(result))
def _set_status(self, text: str) -> None:
self.canvas.itemconfigure(self.status_item, text=text)
def _complete(self, result: StartupResult) -> None:
self.destroy()
self.on_complete(result)
def _fail(self, error: Exception) -> None:
self.progress.stop()
self.destroy()
self.on_error(error)
def centered_position(
*,
width: int,
height: int,
pointer_x: int,
pointer_y: int,
screen_x: int,
screen_y: int,
screen_width: int,
screen_height: int,
) -> tuple[int, int]:
"""Center around the pointer and keep the complete window on the virtual desktop."""
maximum_x = screen_x + max(0, screen_width - width)
maximum_y = screen_y + max(0, screen_height - height)
x = min(max(pointer_x - width // 2, screen_x), maximum_x)
y = min(max(pointer_y - height // 2, screen_y), maximum_y)
return x, y