163 lines
5.7 KiB
Python
163 lines
5.7 KiB
Python
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),
|
||
)
|