mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
535 lines
22 KiB
Python
535 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
import tkinter as tk
|
|
from collections.abc import Callable
|
|
from datetime import datetime
|
|
from tkinter import messagebox, ttk
|
|
|
|
from ccma.domain.models import ASSET_STATUS_LABELS, Event
|
|
from ccma.storage.repository import MemberRepository, RepositoryError
|
|
from ccma.ui.dialogs import AssetClaimDialog, IntegrityWarningDialog
|
|
from ccma.ui.messages import MessageAction, MessageBannerList, TabMessage
|
|
from ccma.ui.scrolling import ScrollableFrame
|
|
|
|
|
|
class AssetTab(ttk.Frame):
|
|
def __init__(
|
|
self,
|
|
master: tk.Misc,
|
|
repository: MemberRepository,
|
|
asset_id: str,
|
|
on_close: Callable[[], None],
|
|
on_changed: Callable[[], None],
|
|
on_open_member: Callable[[str], None],
|
|
on_issue_asset: Callable[[str], None],
|
|
on_return_asset: Callable[[str], None],
|
|
on_open_claim: Callable[[str, str], None],
|
|
):
|
|
super().__init__(master, padding=12)
|
|
self.repository = repository
|
|
self.asset_id = asset_id
|
|
self.on_close = on_close
|
|
self.on_changed = on_changed
|
|
self.on_open_member = on_open_member
|
|
self.on_issue_asset = on_issue_asset
|
|
self.on_return_asset = on_return_asset
|
|
self.on_open_claim = on_open_claim
|
|
self.asset = repository.get_asset(asset_id)
|
|
self.variables: dict[str, tk.StringVar] = {}
|
|
self.notes_text: tk.Text | None = None
|
|
self._build_ui()
|
|
self.refresh()
|
|
|
|
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)
|
|
header.columnconfigure(1, weight=0)
|
|
self.title_var = tk.StringVar()
|
|
self.subtitle_var = tk.StringVar()
|
|
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(title_column, textvariable=self.subtitle_var, style="Mono.TLabel").grid(
|
|
row=1, column=0, sticky="w"
|
|
)
|
|
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.on_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=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)
|
|
self.pane.add(self.timeline_pane, weight=3)
|
|
self._build_details(self.details_pane)
|
|
self._build_timeline(self.timeline_pane)
|
|
self._pane_position_initialized = False
|
|
self.pane.bind("<Configure>", self._set_initial_pane_position, add="+")
|
|
|
|
def _set_initial_pane_position(self, event: tk.Event | None = None) -> None:
|
|
if self._pane_position_initialized:
|
|
return
|
|
try:
|
|
width = int(getattr(event, "width", 0)) or self.pane.winfo_width()
|
|
if width > 1:
|
|
self.pane.sashpos(0, max(360, int(width * 0.4)))
|
|
self._pane_position_initialized = True
|
|
except tk.TclError:
|
|
return
|
|
|
|
def _build_details(self, parent: ttk.Frame) -> None:
|
|
parent.columnconfigure(0, weight=1)
|
|
parent.rowconfigure(1, weight=1)
|
|
ttk.Label(parent, text="// ASSET", style="TimelineHeader.TLabel").grid(
|
|
row=0, column=0, sticky="w", pady=(0, 8)
|
|
)
|
|
notebook = ttk.Notebook(parent)
|
|
notebook.grid(row=1, column=0, sticky="nsew")
|
|
data_tab = self._create_form_tab(notebook, "Stammdaten")
|
|
actions = ttk.Frame(parent)
|
|
actions.grid(row=2, column=0, sticky="ew", pady=(10, 0))
|
|
actions.columnconfigure(0, weight=1)
|
|
action_buttons = ttk.Frame(actions)
|
|
action_buttons.grid(row=0, column=0, sticky="e")
|
|
finance_tab = ttk.Frame(notebook, padding=16)
|
|
notebook.add(finance_tab, text="Forderungen")
|
|
|
|
fields = [
|
|
("UUID / Ordner-ID", "asset_id"),
|
|
("Bezeichnung", "label"),
|
|
("Kategorie", "category"),
|
|
("Inventarnummer", "inventory_number"),
|
|
("Seriennummer", "serial_number"),
|
|
("Kaution (EUR)", "deposit_amount_default"),
|
|
("Standort", "location"),
|
|
("Übergabedatum", "handed_over_at"),
|
|
("Zustand bei Übergabe", "condition"),
|
|
("Wert / Kaufpreis", "estimated_value"),
|
|
]
|
|
for row, (label, key) in enumerate(fields):
|
|
self.variables[key] = tk.StringVar()
|
|
ttk.Label(data_tab, text=label).grid(row=row, column=0, sticky="w", pady=5, padx=(0, 12))
|
|
entry = ttk.Entry(data_tab, textvariable=self.variables[key], width=42)
|
|
entry.grid(row=row, column=1, sticky="ew", pady=5)
|
|
if key == "asset_id":
|
|
entry.configure(state="readonly")
|
|
if key == "deposit_amount_default":
|
|
self.deposit_entry = entry
|
|
self.variables["status"] = tk.StringVar()
|
|
holder_row = len(fields)
|
|
ttk.Label(data_tab, text="Status").grid(row=holder_row, column=0, sticky="w", pady=5, padx=(0, 12))
|
|
self.status_box = ttk.Combobox(
|
|
data_tab,
|
|
textvariable=self.variables["status"],
|
|
values=["VERFUEGBAR", "AUSGEGEBEN", "VERLOREN", "AUSGEMUSTERT"],
|
|
state="readonly",
|
|
width=39,
|
|
)
|
|
self.status_box.grid(row=holder_row, column=1, sticky="ew", pady=5)
|
|
self.holder_var = tk.StringVar()
|
|
ttk.Label(data_tab, text="Aktueller Halter").grid(
|
|
row=holder_row + 1,
|
|
column=0,
|
|
sticky="w",
|
|
pady=5,
|
|
padx=(0, 12),
|
|
)
|
|
self.holder_label = ttk.Label(
|
|
data_tab,
|
|
textvariable=self.holder_var,
|
|
style="TimelineHeader.TLabel",
|
|
)
|
|
self.holder_label.grid(row=holder_row + 1, column=1, sticky="w", pady=5)
|
|
self.holder_label.bind("<Button-1>", lambda _event: self._open_holder_member(), add="+")
|
|
self.owner_var = tk.StringVar()
|
|
ttk.Label(data_tab, text="Eigentümer").grid(
|
|
row=holder_row + 2, column=0, sticky="w", pady=5, padx=(0, 12)
|
|
)
|
|
self.owner_label = ttk.Label(data_tab, textvariable=self.owner_var, style="TimelineHeader.TLabel")
|
|
self.owner_label.grid(row=holder_row + 2, column=1, sticky="w", pady=5)
|
|
self.owner_label.bind("<Button-1>", lambda _event: self._open_owner_member(), add="+")
|
|
ttk.Label(data_tab, text="Interne Notiz").grid(
|
|
row=holder_row + 3,
|
|
column=0,
|
|
sticky="nw",
|
|
pady=5,
|
|
padx=(0, 12),
|
|
)
|
|
self.notes_text = tk.Text(data_tab, width=42, height=6, wrap="word")
|
|
self.notes_text.grid(row=holder_row + 3, column=1, sticky="ew", pady=5)
|
|
data_tab.columnconfigure(1, weight=1)
|
|
self.issue_button = ttk.Button(
|
|
action_buttons,
|
|
text="Ausgeben",
|
|
command=lambda: self.on_issue_asset(self.asset_id),
|
|
)
|
|
self.issue_button.pack(side="left", padx=(0, 8))
|
|
self.return_button = ttk.Button(
|
|
action_buttons,
|
|
text="Zurücknehmen",
|
|
command=lambda: self.on_return_asset(self.asset_id),
|
|
)
|
|
self.return_button.pack(side="left", padx=(0, 8))
|
|
ttk.Button(
|
|
action_buttons,
|
|
text="Speichern",
|
|
style="Accent.TButton",
|
|
command=self._save,
|
|
).pack(side="left")
|
|
|
|
finance_tab.columnconfigure(0, weight=1)
|
|
self.finance_summary_var = tk.StringVar()
|
|
ttk.Label(finance_tab, textvariable=self.finance_summary_var, style="Mono.TLabel").grid(
|
|
row=0, column=0, sticky="w", pady=(0, 10)
|
|
)
|
|
finance_actions = ttk.Frame(finance_tab)
|
|
finance_actions.grid(row=1, column=0, sticky="ew")
|
|
ttk.Button(finance_actions, text="Kautionsforderung", command=self._create_deposit_claim).pack(
|
|
side="left", padx=(0, 8)
|
|
)
|
|
ttk.Button(finance_actions, text="Kautionsrückzahlung", command=self._create_refund_claim).pack(
|
|
side="left", padx=(0, 8)
|
|
)
|
|
ttk.Button(finance_actions, text="Verlustforderung", command=self._create_loss_claim).pack(
|
|
side="left", padx=(0, 8)
|
|
)
|
|
ttk.Button(
|
|
finance_actions,
|
|
text="Reparaturforderung",
|
|
command=self._create_repair_claim,
|
|
).pack(side="left")
|
|
self.asset_claims = ttk.Treeview(
|
|
finance_tab,
|
|
columns=("title", "due", "amount", "member"),
|
|
show="headings",
|
|
)
|
|
for key, title, width in (
|
|
("title", "Forderung", 240),
|
|
("due", "Fällig", 110),
|
|
("amount", "Betrag", 90),
|
|
("member", "Mitglied", 220),
|
|
):
|
|
self.asset_claims.heading(key, text=title)
|
|
self.asset_claims.column(key, width=width, anchor="w")
|
|
self.asset_claims.grid(row=2, column=0, sticky="nsew", pady=(10, 0))
|
|
finance_tab.rowconfigure(2, weight=1)
|
|
self.asset_claims.bind("<Double-1>", lambda _event: self._open_selected_asset_claim())
|
|
self.asset_claims.bind("<Return>", lambda _event: self._open_selected_asset_claim())
|
|
|
|
def _create_form_tab(self, notebook: ttk.Notebook, 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)
|
|
return scroller.content
|
|
|
|
def _build_timeline(self, parent: ttk.Frame) -> None:
|
|
parent.columnconfigure(0, weight=1)
|
|
parent.rowconfigure(1, weight=1)
|
|
ttk.Label(parent, text="// CHRONIK", style="TimelineHeader.TLabel").grid(
|
|
row=0, column=0, sticky="w", pady=(0, 8)
|
|
)
|
|
self.timeline = ttk.Treeview(
|
|
parent, columns=("time", "summary"), show="headings", style="Timeline.Treeview"
|
|
)
|
|
self.timeline.heading("time", text="Zeit")
|
|
self.timeline.heading("summary", text="Ereignis")
|
|
self.timeline.column("time", width=135, stretch=False)
|
|
self.timeline.column("summary", width=320, stretch=True)
|
|
self.timeline.grid(row=1, column=0, sticky="nsew")
|
|
compose = ttk.Frame(parent)
|
|
compose.grid(row=2, column=0, sticky="ew", pady=(10, 0))
|
|
compose.columnconfigure(0, weight=1)
|
|
self.comment_var = tk.StringVar()
|
|
comment = ttk.Entry(compose, textvariable=self.comment_var)
|
|
comment.grid(row=0, column=0, sticky="ew", padx=(0, 6))
|
|
comment.bind("<Return>", lambda _event: self._add_comment())
|
|
ttk.Button(compose, text="Kommentar", command=self._add_comment).grid(row=0, column=1)
|
|
|
|
def refresh(self) -> None:
|
|
self.asset = self.repository.get_asset(self.asset_id)
|
|
self.title_var.set(self.asset.label or self.asset.asset_id)
|
|
holder = self._holder_label()
|
|
status = ASSET_STATUS_LABELS.get(self.asset.status, self.asset.status.upper())
|
|
self.subtitle_var.set(f"{self.asset.inventory_number or '—'} · {status} · {holder}")
|
|
self.id_var.set(f"UUID: {self.asset.asset_id}")
|
|
warnings = self.repository.asset_hash_warnings(self.asset_id)
|
|
self.messages.set_messages(
|
|
[
|
|
TabMessage(
|
|
"warning",
|
|
"WARNUNG: " + " | ".join(warnings),
|
|
MessageAction("Überprüft, bestätigen", self._confirm_integrity_banner),
|
|
)
|
|
]
|
|
if warnings
|
|
else []
|
|
)
|
|
self.variables["asset_id"].set(self.asset.asset_id)
|
|
self.variables["label"].set(self.asset.label)
|
|
self.variables["category"].set(self.asset.category)
|
|
self.variables["inventory_number"].set(self.asset.inventory_number)
|
|
self.variables["serial_number"].set(self.asset.serial_number)
|
|
self.variables["deposit_amount_default"].set(self.asset.deposit_amount_default)
|
|
self.variables["location"].set(self.asset.location)
|
|
self.variables["handed_over_at"].set(self.asset.handed_over_at)
|
|
self.variables["condition"].set(self.asset.condition)
|
|
self.variables["estimated_value"].set(self.asset.estimated_value)
|
|
self.variables["status"].set(ASSET_STATUS_LABELS.get(self.asset.status, self.asset.status.upper()))
|
|
if self.notes_text is not None:
|
|
self.notes_text.delete("1.0", "end")
|
|
self.notes_text.insert("1.0", self.asset.notes)
|
|
self.holder_var.set(holder)
|
|
self.owner_var.set(self._owner_label())
|
|
issued = bool(self.asset.current_holder_member_id)
|
|
self.holder_label.configure(cursor="hand2" if issued else "")
|
|
self.owner_label.configure(cursor="hand2" if self.asset.owner_type == "member" else "")
|
|
self.issue_button.configure(state="normal" if self.asset.status == "available" else "disabled")
|
|
self.return_button.configure(state="normal" if issued else "disabled")
|
|
self.status_box.configure(state="disabled" if issued else "readonly")
|
|
deposit_state = "readonly" if issued else "normal"
|
|
self.deposit_entry.configure(state=deposit_state)
|
|
self._refresh_events()
|
|
self._refresh_finance()
|
|
|
|
def _holder_label(self) -> str:
|
|
if not self.asset.current_holder_member_id:
|
|
return "—"
|
|
try:
|
|
member = self.repository.get_member(self.asset.current_holder_member_id)
|
|
except RepositoryError:
|
|
return self.asset.current_holder_member_id
|
|
return f"{member.member_number or member.member_id} · {member.display_name}"
|
|
|
|
def _owner_label(self) -> str:
|
|
if self.asset.owner_type == "club":
|
|
return "Verein"
|
|
if self.asset.owner_type == "external":
|
|
return self.asset.owner_name or "Extern"
|
|
try:
|
|
member = self.repository.get_member(self.asset.owner_member_id)
|
|
except RepositoryError:
|
|
return self.asset.owner_member_id or "—"
|
|
return f"{member.member_number or member.member_id} · {member.display_name}"
|
|
|
|
def _open_owner_member(self) -> None:
|
|
if self.asset.owner_type == "member" and self.asset.owner_member_id:
|
|
self.on_open_member(self.asset.owner_member_id)
|
|
|
|
def _refresh_events(self) -> None:
|
|
self.timeline.delete(*self.timeline.get_children())
|
|
try:
|
|
events = self.repository.get_asset_events(self.asset_id)
|
|
except RepositoryError as exc:
|
|
messagebox.showerror("Chronik beschädigt", str(exc), parent=self)
|
|
return
|
|
for event in reversed(events):
|
|
self.timeline.insert("", "end", values=(_format_timestamp(event), _event_label(event)))
|
|
|
|
def _refresh_finance(self) -> None:
|
|
self.asset_claims.delete(*self.asset_claims.get_children())
|
|
linked_claims: list[tuple[str, str, dict]] = []
|
|
for member in self.repository.list_members():
|
|
try:
|
|
data = self.repository.get_contributions(member.member_id)
|
|
except RepositoryError:
|
|
continue
|
|
for claim in data.claims:
|
|
origin = claim.get("origin") or {}
|
|
if isinstance(origin, dict) and str(origin.get("asset_id", "")) == self.asset_id:
|
|
linked_claims.append((member.member_id, member.display_name, claim))
|
|
for member_id, member_name, claim in linked_claims:
|
|
claim_id = str(claim.get("claim_id", ""))
|
|
self.asset_claims.insert(
|
|
"",
|
|
"end",
|
|
iid=f"{member_id}:{claim_id}",
|
|
values=(
|
|
str(claim.get("title", "")),
|
|
str(claim.get("due_date", "")),
|
|
str(claim.get("amount", "")),
|
|
member_name,
|
|
),
|
|
)
|
|
self.finance_summary_var.set(
|
|
"Forderungen für dieses Asset. Negative Betraege dokumentieren Gutschriften/Rueckzahlungen."
|
|
)
|
|
|
|
def _save(self) -> None:
|
|
warnings = self.repository.asset_hash_warnings(self.asset_id)
|
|
if warnings:
|
|
self._confirm_integrity_and_then(self._save_confirmed)
|
|
return
|
|
self._save_confirmed()
|
|
|
|
def _save_confirmed(self) -> None:
|
|
self.asset.label = self.variables["label"].get().strip()
|
|
self.asset.category = self.variables["category"].get().strip()
|
|
self.asset.inventory_number = self.variables["inventory_number"].get().strip()
|
|
self.asset.serial_number = self.variables["serial_number"].get().strip()
|
|
self.asset.notes = self.notes_text.get("1.0", "end-1c").strip() if self.notes_text else ""
|
|
self.asset.location = self.variables["location"].get().strip()
|
|
self.asset.handed_over_at = self.variables["handed_over_at"].get().strip()
|
|
self.asset.condition = self.variables["condition"].get().strip()
|
|
self.asset.estimated_value = self.variables["estimated_value"].get().strip()
|
|
if not self.asset.current_holder_member_id:
|
|
self.asset.deposit_amount_default = self.variables["deposit_amount_default"].get().strip()
|
|
self.asset.status = _asset_status_key(self.variables["status"].get())
|
|
try:
|
|
self.repository.save_asset(self.asset)
|
|
self.repository.refresh_asset_record_hashes(self.asset_id)
|
|
except RepositoryError as exc:
|
|
messagebox.showerror("Asset konnte nicht gespeichert werden", 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_asset_record_hashes(self.asset_id)
|
|
self.refresh()
|
|
self.on_changed()
|
|
|
|
def _confirm_integrity_and_then(self, callback: Callable[[], None]) -> None:
|
|
warnings = self.repository.asset_hash_warnings(self.asset_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:
|
|
return
|
|
self.repository.append_asset_event(
|
|
self.asset_id,
|
|
event_type="board_comment",
|
|
summary=text,
|
|
actor_type="user",
|
|
actor_name="Vorstand",
|
|
)
|
|
self.comment_var.set("")
|
|
self._refresh_events()
|
|
|
|
def _open_holder_member(self) -> None:
|
|
if self.asset.current_holder_member_id:
|
|
self.on_open_member(self.asset.current_holder_member_id)
|
|
|
|
def _create_deposit_claim(self) -> None:
|
|
self._open_claim_dialog(
|
|
preset_title=f"Kaution: {self.asset.label}",
|
|
preset_amount=self.asset.deposit_amount_default or "0.00",
|
|
preset_description=f"Kaution für Asset {self.asset.label}",
|
|
claim_type="asset_deposit",
|
|
)
|
|
|
|
def _create_refund_claim(self) -> None:
|
|
amount = self.asset.deposit_amount_default or "0.00"
|
|
if not str(amount).startswith("-"):
|
|
amount = f"-{amount}"
|
|
self._open_claim_dialog(
|
|
preset_title=f"Kautionsrückzahlung: {self.asset.label}",
|
|
preset_amount=amount,
|
|
preset_description=f"Gutschrift / Rückzahlung der Kaution für Asset {self.asset.label}",
|
|
claim_type="asset_refund",
|
|
)
|
|
|
|
def _create_loss_claim(self) -> None:
|
|
self._open_claim_dialog(
|
|
preset_title=f"Verlust: {self.asset.label}",
|
|
preset_amount=self.asset.deposit_amount_default or "0.00",
|
|
preset_description=f"Forderung wegen Verlust von Asset {self.asset.label}",
|
|
claim_type="asset_loss",
|
|
)
|
|
|
|
def _create_repair_claim(self) -> None:
|
|
self._open_claim_dialog(
|
|
preset_title=f"Reparatur: {self.asset.label}",
|
|
preset_amount="0.00",
|
|
preset_description=f"Reparaturkosten für Asset {self.asset.label}",
|
|
claim_type="asset_repair",
|
|
)
|
|
|
|
def _open_claim_dialog(
|
|
self,
|
|
*,
|
|
preset_title: str,
|
|
preset_amount: str,
|
|
preset_description: str,
|
|
claim_type: str,
|
|
) -> None:
|
|
member_id = self.asset.current_holder_member_id
|
|
if not member_id:
|
|
messagebox.showerror(
|
|
"Forderung nicht möglich",
|
|
"Dieses Asset ist aktuell keinem Mitglied zugeordnet.",
|
|
parent=self,
|
|
)
|
|
return
|
|
AssetClaimDialog(
|
|
self,
|
|
self.repository,
|
|
self.asset_id,
|
|
member_id,
|
|
preset_title=preset_title,
|
|
preset_amount=preset_amount,
|
|
preset_description=preset_description,
|
|
claim_type=claim_type,
|
|
on_created=lambda claim_id: self._claim_created(member_id, claim_id),
|
|
)
|
|
|
|
def _claim_created(self, member_id: str, claim_id: str) -> None:
|
|
self.refresh()
|
|
self.on_changed()
|
|
self.on_open_claim(member_id, claim_id)
|
|
|
|
def _open_selected_asset_claim(self) -> None:
|
|
selected = self.asset_claims.selection()
|
|
if not selected:
|
|
return
|
|
member_id, claim_id = selected[0].split(":", 1)
|
|
self.on_open_claim(member_id, claim_id)
|
|
|
|
|
|
def _format_timestamp(event: Event) -> str:
|
|
try:
|
|
return datetime.fromisoformat(event.timestamp).strftime("%d.%m.%Y %H:%M")
|
|
except ValueError:
|
|
return event.timestamp[:16]
|
|
|
|
|
|
def _event_label(event: Event) -> str:
|
|
if event.actor_type == "system":
|
|
return f"[AUTO] {event.summary}"
|
|
return event.summary
|
|
|
|
|
|
def _asset_status_key(label: str) -> str:
|
|
for key, value in ASSET_STATUS_LABELS.items():
|
|
if value == label:
|
|
return key
|
|
return label.casefold().strip()
|