from tkinter import messagebox import customtkinter as ctk from app.ui.base_frame import BaseAppFrame from app.utils.validators import ( normalize_text, parse_billing_day, parse_positive_float, ) class SubscriptionsFrame(BaseAppFrame): """Screen for recurring payments and subscriptions.""" def __init__(self, master, refresh_callback) -> None: super().__init__(master, refresh_callback) 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=20) form_frame.grid(row=0, column=0, sticky="ns", padx=(8, 12), pady=8) self.title_label(form_frame, "Подписки").pack( anchor="w", padx=18, pady=(18, 4) ) self.subtitle_label( form_frame, "Контроль регулярных списаний и платных сервисов.", ).pack(anchor="w", padx=18, pady=(0, 18)) self.name_entry = ctk.CTkEntry( form_frame, width=320, placeholder_text="Название сервиса", ) self.name_entry.pack(padx=18, pady=7) self.amount_entry = ctk.CTkEntry( form_frame, width=320, placeholder_text="Стоимость", ) self.amount_entry.pack(padx=18, pady=7) self.day_entry = ctk.CTkEntry( form_frame, width=320, placeholder_text="День списания: 1–31", ) self.day_entry.pack(padx=18, pady=7) self.period_menu = ctk.CTkOptionMenu( form_frame, width=320, values=["Месяц", "Год"], ) self.period_menu.set("Месяц") self.period_menu.pack(padx=18, pady=7) self.status_menu = ctk.CTkOptionMenu( form_frame, width=320, values=["Активна", "Пауза"], ) self.status_menu.set("Активна") self.status_menu.pack(padx=18, pady=7) self.add_button = self.primary_button( form_frame, "Добавить подписку", lambda: self.run_safe(self.add_subscription, self.add_button), ) self.add_button.pack(fill="x", 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 add_subscription(self) -> None: name = normalize_text(self.name_entry.get(), "Название") amount = parse_positive_float(self.amount_entry.get(), "Стоимость") day = parse_billing_day(self.day_entry.get()) self.service.add_subscription( name=name, amount=amount, billing_day=day, period=self.period_menu.get(), status=self.status_menu.get(), ) self.name_entry.delete(0, "end") self.amount_entry.delete(0, "end") self.day_entry.delete(0, "end") self.refresh_callback() def delete_subscription(self, subscription_id: int) -> None: if not messagebox.askyesno("Подтверждение", "Удалить выбранную подписку?"): return self.service.delete_subscription(subscription_id) self.refresh_callback() def toggle_status(self, subscription_id: int) -> None: self.service.toggle_subscription_status(subscription_id) self.refresh_callback() def refresh_data(self) -> None: self.clear(self.list_frame) rows = self.service.list_subscriptions() if not rows: ctk.CTkLabel( self.list_frame, text="Подписок пока нет.", text_color="gray70", ).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) card.grid_columnconfigure(0, weight=1) ctk.CTkLabel( card, text=row["name"], font=ctk.CTkFont(size=18, weight="bold"), ).grid(row=0, column=0, sticky="w", padx=16, pady=(12, 4)) toggle_text = "Пауза" if row["status"] == "Активна" else "Активировать" ctk.CTkButton( card, text=toggle_text, width=110, command=lambda sub_id=row["id"]: self.run_safe( lambda: self.toggle_status(sub_id) ), ).grid(row=0, column=1, sticky="e", padx=(8, 4), pady=(12, 4)) self.danger_button( card, "Удалить", lambda sub_id=row["id"]: self.run_safe( lambda: self.delete_subscription(sub_id) ), ).grid(row=0, column=2, sticky="e", padx=(4, 16), pady=(12, 4)) monthly = row["amount"] / 12 if row["period"] == "Год" else row["amount"] info = ( f"{self.money(row['amount'])} | {row['period']} | " f"списание {row['billing_day']} числа | {row['status']} | " f"≈ {self.money(monthly)} в месяц" ) ctk.CTkLabel(card, text=info, text_color="gray70").grid( row=1, column=0, columnspan=3, sticky="w", padx=16, pady=(0, 14), )