from collections.abc import Callable from tkinter import messagebox import customtkinter as ctk from app.config import ( DANGER_COLOR, DANGER_HOVER_COLOR, PRIMARY_COLOR, PRIMARY_HOVER_COLOR, SUCCESS_COLOR, WARNING_COLOR, ) from app.services import FinanceService from app.utils.formatters import format_money class BaseAppFrame(ctk.CTkFrame): """Base class for application screens with shared UI helpers.""" def __init__(self, master, refresh_callback: Callable[[], None]) -> None: super().__init__(master) self.refresh_callback = refresh_callback self.service = FinanceService() self._busy = False def run_safe( self, action: Callable[[], None], button: ctk.CTkButton | None = None, ) -> None: """Run UI action with protection from repeated rapid clicks.""" if self._busy: return self._busy = True if button is not None: button.configure(state="disabled") try: action() except ValueError as error: messagebox.showerror("Ошибка ввода", str(error)) except Exception as error: # noqa: BLE001 - user-facing desktop app guard messagebox.showerror("Ошибка", f"Действие не выполнено: {error}") finally: if button is not None: self.after(350, lambda: button.configure(state="normal")) self.after(350, self._unlock) else: self._unlock() def _unlock(self) -> None: self._busy = False @staticmethod def clear(container: ctk.CTkBaseClass) -> None: for widget in container.winfo_children(): widget.destroy() @staticmethod def money(value: float | int | None) -> str: return format_money(value) @staticmethod def title_label(master, text: str) -> ctk.CTkLabel: return ctk.CTkLabel( master, text=text, font=ctk.CTkFont(size=28, weight="bold"), ) @staticmethod def section_label(master, text: str) -> ctk.CTkLabel: return ctk.CTkLabel( master, text=text, font=ctk.CTkFont(size=18, weight="bold"), ) @staticmethod def subtitle_label(master, text: str) -> ctk.CTkLabel: return ctk.CTkLabel(master, text=text, text_color="gray70") @staticmethod def primary_button(master, text: str, command) -> ctk.CTkButton: return ctk.CTkButton( master, text=text, height=42, fg_color=PRIMARY_COLOR, hover_color=PRIMARY_HOVER_COLOR, command=command, ) @staticmethod def danger_button(master, text: str, command, width: int = 92) -> ctk.CTkButton: return ctk.CTkButton( master, text=text, width=width, fg_color=DANGER_COLOR, hover_color=DANGER_HOVER_COLOR, command=command, ) @staticmethod def status_color(value: float) -> str: if value >= 1: return DANGER_COLOR if value >= 0.8: return WARNING_COLOR return SUCCESS_COLOR