Files
Finance-Control/app/ui/budgets_frame.py

148 lines
5.2 KiB
Python

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),
)