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")