mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-03 12:10:05 +02:00
Add asset records, claims, and credit workflows
This commit is contained in:
+206
-1
@@ -4,7 +4,7 @@ from collections.abc import Callable
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
from ccma.domain.dates import format_date_for_display
|
||||
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS, HousekeeperFinding, Member
|
||||
from ccma.domain.models import ASSET_STATUS_LABELS, MEMBERSHIP_STATUS_LABELS, Asset, HousekeeperFinding, Member
|
||||
from ccma.ui.labels import storage_key
|
||||
|
||||
|
||||
@@ -19,6 +19,14 @@ MEMBER_TABLE_COLUMNS = (
|
||||
)
|
||||
|
||||
STATUS_FILTER_ALL = "Alle"
|
||||
ASSET_FILTER_ALL = "Alle"
|
||||
ASSET_TABLE_COLUMNS = (
|
||||
("label", "Bezeichnung", 260),
|
||||
("category", "Kategorie", 140),
|
||||
("inventory_number", "Inventarnummer", 140),
|
||||
("status", "Status", 140),
|
||||
("holder", "Mitglied", 240),
|
||||
)
|
||||
|
||||
|
||||
def _member_table_value(member: Member, column: str) -> str:
|
||||
@@ -59,6 +67,26 @@ def _selected_status_filter(label: str) -> str:
|
||||
return storage_key(MEMBERSHIP_STATUS_LABELS, label)
|
||||
|
||||
|
||||
def _asset_table_value(asset: Asset, column: str, holder_label: str) -> str:
|
||||
if column == "label":
|
||||
return asset.label
|
||||
if column == "category":
|
||||
return asset.category
|
||||
if column == "inventory_number":
|
||||
return asset.inventory_number
|
||||
if column == "status":
|
||||
return ASSET_STATUS_LABELS.get(asset.status, asset.status)
|
||||
if column == "holder":
|
||||
return holder_label
|
||||
return ""
|
||||
|
||||
|
||||
def _filter_assets(assets: list[Asset], status_filter: str) -> list[Asset]:
|
||||
if status_filter == "all":
|
||||
return list(assets)
|
||||
return [asset for asset in assets if asset.status == status_filter]
|
||||
|
||||
|
||||
class DashboardTab(ttk.Frame):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -281,6 +309,177 @@ class MembersTab(ttk.Frame):
|
||||
self.on_open(selected[0])
|
||||
|
||||
|
||||
class AssetsTab(ttk.Frame):
|
||||
def __init__(
|
||||
self,
|
||||
master: tk.Misc,
|
||||
assets: list[Asset],
|
||||
resolve_holder_label: Callable[[str], str],
|
||||
on_new: Callable[[], None],
|
||||
on_open: Callable[[str], None],
|
||||
on_edit: Callable[[str], None],
|
||||
on_issue: Callable[[str], None],
|
||||
on_return: Callable[[str], None],
|
||||
on_close: Callable[[], None],
|
||||
):
|
||||
super().__init__(master, padding=12)
|
||||
self.assets = assets
|
||||
self.resolve_holder_label = resolve_holder_label
|
||||
self.on_new = on_new
|
||||
self.on_open = on_open
|
||||
self.on_edit = on_edit
|
||||
self.on_issue = on_issue
|
||||
self.on_return = on_return
|
||||
self.on_close = on_close
|
||||
self.sort_column = "label"
|
||||
self.sort_descending = False
|
||||
self.status_filter_var = tk.StringVar(value=ASSET_FILTER_ALL)
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
self.columnconfigure(0, 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)
|
||||
ttk.Label(header, text="INVENTAR", style="TabTitle.TLabel").grid(row=0, column=0, sticky="w")
|
||||
self.count_var = tk.StringVar()
|
||||
ttk.Label(header, textvariable=self.count_var, style="Mono.TLabel").grid(row=1, column=0, sticky="w")
|
||||
button_row = ttk.Frame(header)
|
||||
button_row.grid(row=0, column=1, rowspan=2, sticky="e")
|
||||
ttk.Button(button_row, text="Neues Asset", command=self.on_new).pack(side="left", padx=(0, 8))
|
||||
ttk.Button(button_row, text="Tab schließen", command=self.on_close).pack(side="left")
|
||||
filters = ttk.Frame(self, padding=(12, 10))
|
||||
filters.grid(row=1, column=0, sticky="ew", pady=(0, 10))
|
||||
ttk.Label(filters, text="Status").grid(row=0, column=0, sticky="w", padx=(0, 8))
|
||||
self.status_filter = ttk.Combobox(
|
||||
filters,
|
||||
textvariable=self.status_filter_var,
|
||||
state="readonly",
|
||||
values=[ASSET_FILTER_ALL, *ASSET_STATUS_LABELS.values()],
|
||||
width=22,
|
||||
)
|
||||
self.status_filter.grid(row=0, column=1, sticky="w")
|
||||
self.status_filter.bind("<<ComboboxSelected>>", lambda _event: self._render_assets())
|
||||
self.tree = ttk.Treeview(self, columns=tuple(key for key, _title, _width in ASSET_TABLE_COLUMNS), show="headings")
|
||||
for key, title, width in ASSET_TABLE_COLUMNS:
|
||||
self.tree.heading(key, text=title, command=lambda column=key: self._toggle_sort(column))
|
||||
self.tree.column(key, width=width, anchor="w")
|
||||
self.tree.grid(row=2, column=0, sticky="nsew")
|
||||
self.tree.bind("<Double-1>", self._open_selected)
|
||||
self.tree.bind("<<TreeviewSelect>>", lambda _event: self._update_actions())
|
||||
actions = ttk.Frame(self)
|
||||
actions.grid(row=3, column=0, sticky="e", pady=(10, 0))
|
||||
self.edit_button = ttk.Button(actions, text="Bearbeiten", command=self._edit_selected, state="disabled")
|
||||
self.edit_button.pack(side="left", padx=(0, 8))
|
||||
self.open_button = ttk.Button(actions, text="Öffnen", command=self._open_selected, state="disabled")
|
||||
self.open_button.pack(side="left", padx=(0, 8))
|
||||
self.issue_button = ttk.Button(actions, text="Ausgeben", command=self._issue_selected, state="disabled")
|
||||
self.issue_button.pack(side="left", padx=(0, 8))
|
||||
self.return_button = ttk.Button(actions, text="Zurücknehmen", command=self._return_selected, state="disabled")
|
||||
self.return_button.pack(side="left")
|
||||
self.refresh(self.assets)
|
||||
|
||||
def refresh(self, assets: list[Asset]) -> None:
|
||||
self.assets = assets
|
||||
self._render_assets()
|
||||
|
||||
def _render_assets(self) -> None:
|
||||
self.tree.delete(*self.tree.get_children())
|
||||
status_filter = _selected_asset_filter(self.status_filter_var.get())
|
||||
filtered_assets = _filter_assets(self.assets, status_filter)
|
||||
sorted_assets = sorted(
|
||||
filtered_assets,
|
||||
key=lambda asset: _asset_table_value(
|
||||
asset,
|
||||
self.sort_column,
|
||||
self.resolve_holder_label(asset.current_holder_member_id),
|
||||
).casefold(),
|
||||
reverse=self.sort_descending,
|
||||
)
|
||||
if len(filtered_assets) == len(self.assets):
|
||||
self.count_var.set(f"{len(filtered_assets)} Assets")
|
||||
else:
|
||||
self.count_var.set(f"{len(filtered_assets)} / {len(self.assets)} Assets")
|
||||
self._update_tree_headings()
|
||||
for asset in sorted_assets:
|
||||
self.tree.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=asset.asset_id,
|
||||
values=(
|
||||
asset.label,
|
||||
asset.category,
|
||||
asset.inventory_number,
|
||||
ASSET_STATUS_LABELS.get(asset.status, asset.status),
|
||||
self.resolve_holder_label(asset.current_holder_member_id),
|
||||
),
|
||||
)
|
||||
self._update_actions()
|
||||
|
||||
def _toggle_sort(self, column: str) -> None:
|
||||
if self.sort_column == column:
|
||||
self.sort_descending = not self.sort_descending
|
||||
else:
|
||||
self.sort_column = column
|
||||
self.sort_descending = False
|
||||
self._render_assets()
|
||||
|
||||
def _update_tree_headings(self) -> None:
|
||||
for key, title, _width in ASSET_TABLE_COLUMNS:
|
||||
suffix = ""
|
||||
if key == self.sort_column:
|
||||
suffix = " v" if self.sort_descending else " ^"
|
||||
self.tree.heading(key, text=f"{title}{suffix}", command=lambda column=key: self._toggle_sort(column))
|
||||
|
||||
def _selected_asset_id(self) -> str:
|
||||
selected = self.tree.selection()
|
||||
return selected[0] if selected else ""
|
||||
|
||||
def _selected_asset(self) -> Asset | None:
|
||||
selected_asset_id = self._selected_asset_id()
|
||||
return next((asset for asset in self.assets if asset.asset_id == selected_asset_id), None)
|
||||
|
||||
def _update_actions(self) -> None:
|
||||
asset = self._selected_asset()
|
||||
if asset is None:
|
||||
self.edit_button.configure(state="disabled")
|
||||
self.open_button.configure(state="disabled")
|
||||
self.issue_button.configure(state="disabled")
|
||||
self.return_button.configure(state="disabled")
|
||||
return
|
||||
self.edit_button.configure(state="normal")
|
||||
self.open_button.configure(state="normal")
|
||||
self.issue_button.configure(state="normal" if asset.status == "available" else "disabled")
|
||||
self.return_button.configure(state="normal" if asset.current_holder_member_id else "disabled")
|
||||
|
||||
def _open_selected(self, _event: tk.Event | None = None) -> None:
|
||||
asset_id = self._selected_asset_id()
|
||||
if not asset_id:
|
||||
asset_id = self.tree.focus()
|
||||
if asset_id:
|
||||
self.on_open(asset_id)
|
||||
|
||||
def _edit_selected(self, _event: tk.Event | None = None) -> None:
|
||||
asset_id = self._selected_asset_id()
|
||||
if not asset_id:
|
||||
asset_id = self.tree.focus()
|
||||
if asset_id:
|
||||
self.on_edit(asset_id)
|
||||
|
||||
def _issue_selected(self, _event: tk.Event | None = None) -> None:
|
||||
asset_id = self._selected_asset_id()
|
||||
if not asset_id:
|
||||
asset_id = self.tree.focus()
|
||||
if asset_id:
|
||||
self.on_issue(asset_id)
|
||||
|
||||
def _return_selected(self) -> None:
|
||||
asset_id = self._selected_asset_id()
|
||||
if asset_id:
|
||||
self.on_return(asset_id)
|
||||
|
||||
|
||||
class HousekeeperTab(ttk.Frame):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -415,3 +614,9 @@ def _finding_details(finding: HousekeeperFinding) -> str:
|
||||
if finding.detail:
|
||||
lines.extend(("", finding.detail))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _selected_asset_filter(label: str) -> str:
|
||||
if label == ASSET_FILTER_ALL:
|
||||
return "all"
|
||||
return storage_key(ASSET_STATUS_LABELS, label)
|
||||
|
||||
Reference in New Issue
Block a user