178 lines
6.3 KiB
Python
178 lines
6.3 KiB
Python
import calendar
|
|
from datetime import date
|
|
|
|
import customtkinter as ctk
|
|
|
|
from app.database import get_connection
|
|
|
|
|
|
class DashboardFrame(ctk.CTkFrame):
|
|
def __init__(self, master, refresh_callback) -> None:
|
|
super().__init__(master)
|
|
self.refresh_callback = refresh_callback
|
|
|
|
self.grid_columnconfigure((0, 1), weight=1)
|
|
self.grid_rowconfigure(2, weight=1)
|
|
|
|
header = ctk.CTkLabel(
|
|
self,
|
|
text="Главная панель",
|
|
font=ctk.CTkFont(size=28, weight="bold"),
|
|
)
|
|
header.grid(row=0, column=0, columnspan=2, sticky="w", padx=8, pady=(8, 16))
|
|
|
|
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),
|
|
)
|
|
|
|
def _create_card(self, row: int, column: int, title: str) -> ctk.CTkLabel:
|
|
card = ctk.CTkFrame(self, corner_radius=18)
|
|
card.grid(row=row, column=column, sticky="nsew", padx=8, pady=8)
|
|
|
|
title_label = ctk.CTkLabel(
|
|
card,
|
|
text=title,
|
|
text_color="gray75",
|
|
font=ctk.CTkFont(size=15),
|
|
)
|
|
title_label.pack(anchor="w", padx=18, pady=(16, 8))
|
|
|
|
value_label = ctk.CTkLabel(
|
|
card,
|
|
text="0 ₽",
|
|
font=ctk.CTkFont(size=30, weight="bold"),
|
|
)
|
|
value_label.pack(anchor="w", padx=18, pady=(0, 16))
|
|
return value_label
|
|
|
|
def refresh_data(self) -> None:
|
|
with get_connection() as connection:
|
|
cursor = connection.cursor()
|
|
|
|
balance = cursor.execute(
|
|
"SELECT COALESCE(SUM(balance), 0) AS total FROM accounts"
|
|
).fetchone()["total"]
|
|
|
|
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)
|
|
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"),
|
|
)
|
|
title.pack(anchor="w", padx=14, pady=(12, 4))
|
|
|
|
info = ctk.CTkLabel(
|
|
card,
|
|
text=(
|
|
f"{item['amount']:,.2f} ₽ | {item['period']} | "
|
|
f"следующее списание: {next_date_text} | через {days_left} дн."
|
|
).replace(",", " "),
|
|
text_color="gray75",
|
|
)
|
|
info.pack(anchor="w", padx=14, pady=(0, 12))
|
|
|
|
@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)
|
|
|
|
days_left = (candidate - today).days
|
|
return days_left, candidate.strftime("%d.%m.%Y")
|