148 lines
5.2 KiB
Python
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),
|
|
)
|