import sqlite3 from tkinter import messagebox import customtkinter as ctk from app.database import get_connection class BudgetsFrame(ctk.CTkFrame): def __init__(self, master, refresh_callback) -> None: super().__init__(master) self.refresh_callback = 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=18) form_frame.grid(row=0, column=0, sticky="ns", padx=(8, 12), pady=8) title = ctk.CTkLabel( form_frame, text="Бюджеты и лимиты", font=ctk.CTkFont(size=26, weight="bold"), ) title.pack(anchor="w", padx=18, pady=(18, 6)) subtitle = ctk.CTkLabel( form_frame, text="Ограничения по категориям на месяц.", text_color="gray75", ) subtitle.pack(anchor="w", padx=18, pady=(0, 18)) self.category_menu = ctk.CTkOptionMenu( form_frame, width=300, values=["Нет категорий"], ) self.category_menu.pack(padx=18, pady=8) self.limit_entry = ctk.CTkEntry( form_frame, width=300, placeholder_text="Лимит", ) self.limit_entry.pack(padx=18, pady=8) add_button = ctk.CTkButton( form_frame, text="Добавить лимит", width=300, height=42, command=self.add_budget, ) add_button.pack(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: with get_connection() as connection: rows = connection.execute( """ SELECT id, name FROM categories WHERE category_type = 'Расход' ORDER BY name """ ).fetchall() 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 = f"{row['name']} (ID {row['id']})" values.append(label) self.category_map[label] = row["id"] self.category_menu.configure(values=values) self.category_menu.set(values[0]) def add_budget(self) -> None: category_label = self.category_menu.get() limit_text = self.limit_entry.get().strip().replace(",", ".") if category_label not in self.category_map or not limit_text: messagebox.showerror("Ошибка", "Выберите категорию и введите лимит.") return try: limit_amount = float(limit_text) except ValueError: messagebox.showerror("Ошибка", "Лимит должен быть числом.") return if limit_amount <= 0: messagebox.showerror("Ошибка", "Лимит должен быть больше нуля.") return category_id = self.category_map[category_label] try: with get_connection() as connection: connection.execute( """ INSERT INTO budgets (category_id, limit_amount) VALUES (?, ?) """, (category_id, limit_amount), ) connection.commit() except sqlite3.IntegrityError: messagebox.showerror( "Ошибка", "Для этой категории лимит уже создан.", ) return self.limit_entry.delete(0, "end") self.refresh_callback() def delete_budget(self, budget_id: int) -> None: with get_connection() as connection: connection.execute( "DELETE FROM budgets WHERE id = ?", (budget_id,), ) connection.commit() self.refresh_callback() def refresh_data(self) -> None: self._load_categories() for widget in self.list_frame.winfo_children(): widget.destroy() with get_connection() as connection: rows = connection.execute( """ SELECT b.id, b.limit_amount, c.name AS category_name, COALESCE( SUM( CASE WHEN strftime('%Y-%m', t.created_at) = strftime('%Y-%m', 'now') THEN t.amount ELSE 0 END ), 0 ) AS spent FROM budgets AS b JOIN categories AS c ON c.id = b.category_id LEFT JOIN transactions AS t ON t.category_id = b.category_id AND t.transaction_type = 'Расход' GROUP BY b.id, b.limit_amount, c.name ORDER BY c.name """ ).fetchall() if not rows: empty_label = ctk.CTkLabel( self.list_frame, text="Лимитов пока нет.", text_color="gray70", ) empty_label.pack(anchor="w", padx=10, pady=10) return for row in rows: card = ctk.CTkFrame(self.list_frame, corner_radius=16) card.pack(fill="x", padx=6, pady=6) title = ctk.CTkLabel( card, text=row["category_name"], font=ctk.CTkFont(size=18, weight="bold"), ) title.grid(row=0, column=0, sticky="w", padx=16, pady=(12, 4)) delete_button = ctk.CTkButton( card, text="Удалить", width=90, fg_color="#b33939", hover_color="#932f2f", command=lambda budget_id=row["id"]: self.delete_budget(budget_id), ) delete_button.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) info = ctk.CTkLabel( card, text=( f"Потрачено: {row['spent']:,.2f} ₽ " f"из {row['limit_amount']:,.2f} ₽" ).replace(",", " "), text_color="gray75", ) info.grid(row=1, column=0, columnspan=2, sticky="w", padx=16, pady=(0, 10)) progress = ctk.CTkProgressBar(card, width=360) 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}%" ) status = ctk.CTkLabel( card, text=status_text, text_color="gray75", ) status.grid(row=3, column=0, columnspan=2, sticky="w", padx=16, pady=(0, 14))