финальная версия Finance Control
This commit is contained in:
@@ -3,158 +3,153 @@ from datetime import date
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
from app.database import get_connection
|
||||
from app.ui.base_frame import BaseAppFrame
|
||||
|
||||
|
||||
class DashboardFrame(ctk.CTkFrame):
|
||||
class DashboardFrame(BaseAppFrame):
|
||||
"""Main summary page."""
|
||||
|
||||
def __init__(self, master, refresh_callback) -> None:
|
||||
super().__init__(master)
|
||||
self.refresh_callback = refresh_callback
|
||||
super().__init__(master, refresh_callback)
|
||||
self.grid_columnconfigure((0, 1, 2), weight=1)
|
||||
self.grid_rowconfigure(3, weight=1)
|
||||
self.cards: dict[str, ctk.CTkLabel] = {}
|
||||
self._build_ui()
|
||||
|
||||
self.grid_columnconfigure((0, 1), weight=1)
|
||||
self.grid_rowconfigure(2, weight=1)
|
||||
def _build_ui(self) -> None:
|
||||
header = ctk.CTkFrame(self, fg_color="transparent")
|
||||
header.grid(row=0, column=0, columnspan=3, sticky="ew", padx=8, pady=(8, 12))
|
||||
header.grid_columnconfigure(0, weight=1)
|
||||
|
||||
header = ctk.CTkLabel(
|
||||
self.title_label(header, "Главная панель").grid(row=0, column=0, sticky="w")
|
||||
self.subtitle_label(
|
||||
header,
|
||||
"Краткая финансовая сводка, ближайшие списания и последние операции.",
|
||||
).grid(row=1, column=0, sticky="w", pady=(4, 0))
|
||||
|
||||
card_data = [
|
||||
("balance", "Общий баланс"),
|
||||
("income", "Доходы за месяц"),
|
||||
("expense", "Расходы за месяц"),
|
||||
("net", "Финансовый результат"),
|
||||
("subscriptions", "Активные подписки"),
|
||||
("warnings", "Превышенные лимиты"),
|
||||
]
|
||||
for index, (key, title) in enumerate(card_data):
|
||||
row = 1 + index // 3
|
||||
column = index % 3
|
||||
self.cards[key] = self._create_card(row, column, title)
|
||||
|
||||
self.upcoming_frame = ctk.CTkScrollableFrame(
|
||||
self,
|
||||
text="Главная панель",
|
||||
font=ctk.CTkFont(size=28, weight="bold"),
|
||||
label_text="Ближайшие списания",
|
||||
)
|
||||
header.grid(row=0, column=0, columnspan=2, sticky="w", padx=8, pady=(8, 16))
|
||||
self.upcoming_frame.grid(row=3, column=0, sticky="nsew", padx=8, pady=8)
|
||||
|
||||
self.balance_card = self._create_card(1, 0, "Общий баланс")
|
||||
self.month_expense_card = self._create_card(1, 1, "Расходы за месяц")
|
||||
self.month_income_card = self._create_card(2, 0, "Доходы за месяц")
|
||||
self.subscriptions_card = self._create_card(2, 1, "Активные подписки")
|
||||
|
||||
self.upcoming_frame = ctk.CTkScrollableFrame(self, label_text="Ближайшие списания")
|
||||
self.upcoming_frame.grid(
|
||||
row=3,
|
||||
column=0,
|
||||
columnspan=2,
|
||||
sticky="nsew",
|
||||
padx=8,
|
||||
pady=(16, 8),
|
||||
self.recent_frame = ctk.CTkScrollableFrame(
|
||||
self,
|
||||
label_text="Последние операции",
|
||||
)
|
||||
self.recent_frame.grid(row=3, column=1, sticky="nsew", padx=8, pady=8)
|
||||
|
||||
self.warning_frame = ctk.CTkScrollableFrame(
|
||||
self,
|
||||
label_text="Контроль лимитов",
|
||||
)
|
||||
self.warning_frame.grid(row=3, column=2, sticky="nsew", padx=8, pady=8)
|
||||
|
||||
def _create_card(self, row: int, column: int, title: str) -> ctk.CTkLabel:
|
||||
card = ctk.CTkFrame(self, corner_radius=18)
|
||||
card = ctk.CTkFrame(self, corner_radius=20)
|
||||
card.grid(row=row, column=column, sticky="nsew", padx=8, pady=8)
|
||||
card.grid_columnconfigure(0, weight=1)
|
||||
|
||||
title_label = ctk.CTkLabel(
|
||||
card,
|
||||
text=title,
|
||||
text_color="gray75",
|
||||
font=ctk.CTkFont(size=15),
|
||||
ctk.CTkLabel(card, text=title, text_color="gray70").grid(
|
||||
row=0,
|
||||
column=0,
|
||||
sticky="w",
|
||||
padx=18,
|
||||
pady=(16, 4),
|
||||
)
|
||||
title_label.pack(anchor="w", padx=18, pady=(16, 8))
|
||||
|
||||
value_label = ctk.CTkLabel(
|
||||
card,
|
||||
text="0 ₽",
|
||||
font=ctk.CTkFont(size=30, weight="bold"),
|
||||
font=ctk.CTkFont(size=28, weight="bold"),
|
||||
)
|
||||
value_label.pack(anchor="w", padx=18, pady=(0, 16))
|
||||
value_label.grid(row=1, column=0, sticky="w", padx=18, pady=(0, 16))
|
||||
return value_label
|
||||
|
||||
def refresh_data(self) -> None:
|
||||
with get_connection() as connection:
|
||||
cursor = connection.cursor()
|
||||
data = self.service.dashboard_summary()
|
||||
self.cards["balance"].configure(text=self.money(data["balance"]))
|
||||
self.cards["income"].configure(text=self.money(data["income"]))
|
||||
self.cards["expense"].configure(text=self.money(data["expense"]))
|
||||
self.cards["net"].configure(text=self.money(data["net"]))
|
||||
self.cards["subscriptions"].configure(
|
||||
text=f"{data['subscriptions_count']} шт. / {self.money(data['subscriptions_total'])}"
|
||||
)
|
||||
self.cards["warnings"].configure(text=f"{len(data['warnings'])} категорий")
|
||||
|
||||
balance = cursor.execute(
|
||||
"SELECT COALESCE(SUM(balance), 0) AS total FROM accounts"
|
||||
).fetchone()["total"]
|
||||
self._render_upcoming(data["upcoming"])
|
||||
self._render_recent(data["recent"])
|
||||
self._render_warnings(data["warnings"])
|
||||
|
||||
month_expense = cursor.execute(
|
||||
"""
|
||||
SELECT COALESCE(SUM(amount), 0) AS total
|
||||
FROM transactions
|
||||
WHERE transaction_type = 'Расход'
|
||||
AND strftime('%Y-%m', created_at) = strftime('%Y-%m', 'now')
|
||||
"""
|
||||
).fetchone()["total"]
|
||||
|
||||
month_income = cursor.execute(
|
||||
"""
|
||||
SELECT COALESCE(SUM(amount), 0) AS total
|
||||
FROM transactions
|
||||
WHERE transaction_type = 'Доход'
|
||||
AND strftime('%Y-%m', created_at) = strftime('%Y-%m', 'now')
|
||||
"""
|
||||
).fetchone()["total"]
|
||||
|
||||
subscriptions = cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS total_count,
|
||||
COALESCE(
|
||||
SUM(
|
||||
CASE
|
||||
WHEN period = 'Год' THEN amount / 12.0
|
||||
ELSE amount
|
||||
END
|
||||
),
|
||||
0
|
||||
) AS monthly_total
|
||||
FROM subscriptions
|
||||
WHERE status = 'Активна'
|
||||
"""
|
||||
).fetchone()
|
||||
|
||||
self.balance_card.configure(text=f"{balance:,.2f} ₽".replace(",", " "))
|
||||
self.month_expense_card.configure(
|
||||
text=f"{month_expense:,.2f} ₽".replace(",", " ")
|
||||
)
|
||||
self.month_income_card.configure(
|
||||
text=f"{month_income:,.2f} ₽".replace(",", " ")
|
||||
)
|
||||
self.subscriptions_card.configure(
|
||||
text=(
|
||||
f"{subscriptions['total_count']} шт. / "
|
||||
f"{subscriptions['monthly_total']:,.2f} ₽"
|
||||
).replace(",", " ")
|
||||
)
|
||||
|
||||
items = cursor.execute(
|
||||
"""
|
||||
SELECT id, name, amount, billing_day, period
|
||||
FROM subscriptions
|
||||
WHERE status = 'Активна'
|
||||
ORDER BY billing_day
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
for widget in self.upcoming_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
if not items:
|
||||
empty_label = ctk.CTkLabel(
|
||||
self.upcoming_frame,
|
||||
text="Нет активных подписок.",
|
||||
text_color="gray70",
|
||||
)
|
||||
empty_label.pack(anchor="w", padx=10, pady=10)
|
||||
def _render_upcoming(self, rows) -> None:
|
||||
self.clear(self.upcoming_frame)
|
||||
if not rows:
|
||||
self._empty(self.upcoming_frame, "Нет активных подписок.")
|
||||
return
|
||||
|
||||
for item in items:
|
||||
days_left, next_date_text = self._get_next_payment_info(item["billing_day"])
|
||||
card = ctk.CTkFrame(self.upcoming_frame, corner_radius=14)
|
||||
card.pack(fill="x", padx=6, pady=6)
|
||||
|
||||
title = ctk.CTkLabel(
|
||||
card,
|
||||
text=item["name"],
|
||||
font=ctk.CTkFont(size=18, weight="bold"),
|
||||
for row in rows:
|
||||
days_left, next_date_text = self._get_next_payment_info(row["billing_day"])
|
||||
text = (
|
||||
f"{self.money(row['amount'])} | {row['period']} | "
|
||||
f"{next_date_text} | через {days_left} дн."
|
||||
)
|
||||
title.pack(anchor="w", padx=14, pady=(12, 4))
|
||||
self._small_card(self.upcoming_frame, row["name"], text)
|
||||
|
||||
info = ctk.CTkLabel(
|
||||
card,
|
||||
text=(
|
||||
f"{item['amount']:,.2f} ₽ | {item['period']} | "
|
||||
f"следующее списание: {next_date_text} | через {days_left} дн."
|
||||
).replace(",", " "),
|
||||
text_color="gray75",
|
||||
def _render_recent(self, rows) -> None:
|
||||
self.clear(self.recent_frame)
|
||||
if not rows:
|
||||
self._empty(self.recent_frame, "Операций пока нет.")
|
||||
return
|
||||
for row in rows:
|
||||
sign = "+" if row["transaction_type"] == "Доход" else "-"
|
||||
text = (
|
||||
f"{row['created_at'][:10]} | {row['account_name']} | "
|
||||
f"{row['category_name']} | {sign}{self.money(row['amount'])}"
|
||||
)
|
||||
info.pack(anchor="w", padx=14, pady=(0, 12))
|
||||
self._small_card(self.recent_frame, row["title"], text)
|
||||
|
||||
def _render_warnings(self, rows) -> None:
|
||||
self.clear(self.warning_frame)
|
||||
if not rows:
|
||||
self._empty(self.warning_frame, "Превышенных лимитов нет.")
|
||||
return
|
||||
for row in rows:
|
||||
text = f"Потрачено {self.money(row['spent'])} из {self.money(row['limit_amount'])}"
|
||||
self._small_card(self.warning_frame, row["category_name"], text)
|
||||
|
||||
@staticmethod
|
||||
def _small_card(master, title: str, text: str) -> None:
|
||||
card = ctk.CTkFrame(master, corner_radius=14)
|
||||
card.pack(fill="x", padx=6, pady=6)
|
||||
ctk.CTkLabel(
|
||||
card,
|
||||
text=title,
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
).pack(anchor="w", padx=14, pady=(10, 2))
|
||||
ctk.CTkLabel(card, text=text, text_color="gray70").pack(
|
||||
anchor="w",
|
||||
padx=14,
|
||||
pady=(0, 10),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _empty(master, text: str) -> None:
|
||||
ctk.CTkLabel(master, text=text, text_color="gray70").pack(
|
||||
anchor="w",
|
||||
padx=10,
|
||||
pady=10,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_next_payment_info(billing_day: int) -> tuple[int, str]:
|
||||
@@ -173,5 +168,4 @@ class DashboardFrame(ctk.CTkFrame):
|
||||
safe_day = min(billing_day, next_last_day)
|
||||
candidate = date(next_year, next_month, safe_day)
|
||||
|
||||
days_left = (candidate - today).days
|
||||
return days_left, candidate.strftime("%d.%m.%Y")
|
||||
return (candidate - today).days, candidate.strftime("%d.%m.%Y")
|
||||
|
||||
Reference in New Issue
Block a user