import customtkinter as ctk from app.ui.base_frame import BaseAppFrame from app.utils.validators import parse_positive_float class BudgetsFrame(BaseAppFrame): """Screen for monthly category limits.""" def __init__(self, master, refresh_callback) -> None: super().__init__(master, refresh_callback) self.category_map: dict[str, int] = {} self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(0, weight=1) self._build_form() self._build_list() def _build_form(self) -> None: form_frame = ctk.CTkFrame(self, corner_radius=20) form_frame.grid(row=0, column=0, sticky="ns", padx=(8, 12), pady=8) self.title_label(form_frame, "Бюджеты").pack( anchor="w", padx=18, pady=(18, 4) ) self.subtitle_label( form_frame, "Лимиты расходов по категориям на текущий месяц.", ).pack(anchor="w", padx=18, pady=(0, 18)) self.category_menu = ctk.CTkOptionMenu( form_frame, width=320, values=["Нет категорий"], ) self.category_menu.pack(padx=18, pady=8) self.limit_entry = ctk.CTkEntry( form_frame, width=320, placeholder_text="Лимит", ) self.limit_entry.pack(padx=18, pady=8) self.add_button = self.primary_button( form_frame, "Добавить / обновить лимит", lambda: self.run_safe(self.add_budget, self.add_button), ) self.add_button.pack(fill="x", padx=18, pady=(12, 18)) def _build_list(self) -> None: self.list_frame = ctk.CTkScrollableFrame( self, label_text="Лимиты по категориям", ) self.list_frame.grid(row=0, column=1, sticky="nsew", padx=(0, 8), pady=8) def _load_categories(self) -> None: rows = self.service.list_categories("Расход") if not rows: self.category_map = {} self.category_menu.configure(values=["Нет категорий"]) self.category_menu.set("Нет категорий") return values = [] self.category_map = {} for row in rows: label = row["name"] values.append(label) self.category_map[label] = row["id"] self.category_menu.configure(values=values) if self.category_menu.get() not in values: self.category_menu.set(values[0]) def add_budget(self) -> None: category_label = self.category_menu.get() if category_label not in self.category_map: raise ValueError("Выберите расходную категорию.") limit_amount = parse_positive_float(self.limit_entry.get(), "Лимит") self.service.add_or_update_budget( self.category_map[category_label], limit_amount, ) self.limit_entry.delete(0, "end") self.refresh_callback() def delete_budget(self, budget_id: int) -> None: self.service.delete_budget(budget_id) self.refresh_callback() def refresh_data(self) -> None: self._load_categories() self.clear(self.list_frame) rows = self.service.list_budgets() if not rows: ctk.CTkLabel( self.list_frame, text="Лимитов пока нет.", text_color="gray70", ).pack(anchor="w", padx=10, pady=10) return for row in rows: self._render_budget(row) def _render_budget(self, row) -> None: card = ctk.CTkFrame(self.list_frame, corner_radius=16) card.pack(fill="x", padx=6, pady=6) card.grid_columnconfigure(0, weight=1) ctk.CTkLabel( card, text=row["category_name"], font=ctk.CTkFont(size=18, weight="bold"), ).grid(row=0, column=0, sticky="w", padx=16, pady=(12, 4)) self.danger_button( card, "Удалить", lambda budget_id=row["id"]: self.run_safe( lambda: self.delete_budget(budget_id) ), ).grid(row=0, column=1, sticky="e", padx=16, pady=(12, 4)) percent = 0 if row["limit_amount"] == 0 else row["spent"] / row["limit_amount"] percent = max(0, percent) status_color = self.status_color(percent) ctk.CTkLabel( card, text=f"Потрачено: {self.money(row['spent'])} из {self.money(row['limit_amount'])}", text_color="gray70", ).grid(row=1, column=0, columnspan=2, sticky="w", padx=16, pady=(0, 8)) progress = ctk.CTkProgressBar(card, width=460, progress_color=status_color) progress.grid(row=2, column=0, columnspan=2, sticky="w", padx=16, pady=(0, 8)) progress.set(min(percent, 1)) status_text = "Лимит превышен" if percent > 1 else f"Использовано: {percent * 100:.0f}%" ctk.CTkLabel(card, text=status_text, text_color=status_color).grid( row=3, column=0, columnspan=2, sticky="w", padx=16, pady=(0, 14), )