Добавлены главная панель, подписки, бюджеты и отчеты
This commit is contained in:
237
app/ui/budgets_frame.py
Normal file
237
app/ui/budgets_frame.py
Normal file
@@ -0,0 +1,237 @@
|
||||
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))
|
||||
Reference in New Issue
Block a user