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

172 lines
6.6 KiB
Python

import calendar
from datetime import date
import customtkinter as ctk
from app.ui.base_frame import BaseAppFrame
class DashboardFrame(BaseAppFrame):
"""Main summary page."""
def __init__(self, master, refresh_callback) -> None:
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()
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)
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,
label_text="Ближайшие списания",
)
self.upcoming_frame.grid(row=3, column=0, sticky="nsew", padx=8, pady=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=20)
card.grid(row=row, column=column, sticky="nsew", padx=8, pady=8)
card.grid_columnconfigure(0, weight=1)
ctk.CTkLabel(card, text=title, text_color="gray70").grid(
row=0,
column=0,
sticky="w",
padx=18,
pady=(16, 4),
)
value_label = ctk.CTkLabel(
card,
text="0 ₽",
font=ctk.CTkFont(size=28, weight="bold"),
)
value_label.grid(row=1, column=0, sticky="w", padx=18, pady=(0, 16))
return value_label
def refresh_data(self) -> None:
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'])} категорий")
self._render_upcoming(data["upcoming"])
self._render_recent(data["recent"])
self._render_warnings(data["warnings"])
def _render_upcoming(self, rows) -> None:
self.clear(self.upcoming_frame)
if not rows:
self._empty(self.upcoming_frame, "Нет активных подписок.")
return
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} дн."
)
self._small_card(self.upcoming_frame, row["name"], text)
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'])}"
)
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]:
today = date.today()
current_last_day = calendar.monthrange(today.year, today.month)[1]
safe_day = min(billing_day, current_last_day)
candidate = date(today.year, today.month, safe_day)
if candidate < today:
next_month = today.month + 1
next_year = today.year
if next_month == 13:
next_month = 1
next_year += 1
next_last_day = calendar.monthrange(next_year, next_month)[1]
safe_day = min(billing_day, next_last_day)
candidate = date(next_year, next_month, safe_day)
return (candidate - today).days, candidate.strftime("%d.%m.%Y")