from tkinter import filedialog, messagebox import customtkinter as ctk from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure from app.database import get_connection class ReportsFrame(ctk.CTkFrame): def __init__(self, master, refresh_callback) -> None: super().__init__(master) self.refresh_callback = refresh_callback self.last_report_text = "" self.grid_columnconfigure((0, 1), weight=1) self.grid_rowconfigure(1, weight=1) self._build_header() self._build_left_panel() self._build_right_panel() def _build_header(self) -> None: header_frame = ctk.CTkFrame(self, fg_color="transparent") header_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=8, pady=(8, 12)) header_frame.grid_columnconfigure(0, weight=1) title = ctk.CTkLabel( header_frame, text="Аналитика и отчёты", font=ctk.CTkFont(size=28, weight="bold"), ) title.grid(row=0, column=0, sticky="w") export_button = ctk.CTkButton( header_frame, text="Сохранить отчёт в TXT", command=self.export_report, ) export_button.grid(row=0, column=1, padx=(12, 0)) def _build_left_panel(self) -> None: left_frame = ctk.CTkFrame(self, corner_radius=18) left_frame.grid(row=1, column=0, sticky="nsew", padx=(8, 12), pady=8) left_frame.grid_rowconfigure(1, weight=1) left_frame.grid_columnconfigure(0, weight=1) info_label = ctk.CTkLabel( left_frame, text="Текстовый отчёт за текущий месяц", font=ctk.CTkFont(size=20, weight="bold"), ) info_label.grid(row=0, column=0, sticky="w", padx=18, pady=(18, 8)) self.report_box = ctk.CTkTextbox(left_frame) self.report_box.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) def _build_right_panel(self) -> None: right_frame = ctk.CTkFrame(self, corner_radius=18) right_frame.grid(row=1, column=1, sticky="nsew", padx=(0, 8), pady=8) right_frame.grid_rowconfigure(1, weight=1) right_frame.grid_columnconfigure(0, weight=1) chart_label = ctk.CTkLabel( right_frame, text="Структура расходов по категориям", font=ctk.CTkFont(size=20, weight="bold"), ) chart_label.grid(row=0, column=0, sticky="w", padx=18, pady=(18, 8)) self.chart_container = ctk.CTkFrame(right_frame, fg_color="transparent") self.chart_container.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) self.figure = Figure(figsize=(5, 4), dpi=100) self.canvas = FigureCanvasTkAgg(self.figure, master=self.chart_container) self.canvas.get_tk_widget().pack(fill="both", expand=True) def refresh_data(self) -> None: with get_connection() as connection: cursor = connection.cursor() summary = cursor.execute( """ SELECT COALESCE(SUM(CASE WHEN transaction_type = 'Доход' THEN amount END), 0) AS income, COALESCE(SUM(CASE WHEN transaction_type = 'Расход' THEN amount END), 0) AS expense FROM transactions WHERE strftime('%Y-%m', created_at) = strftime('%Y-%m', 'now') """ ).fetchone() balance = cursor.execute( "SELECT COALESCE(SUM(balance), 0) AS total FROM accounts" ).fetchone()["total"] subscription_total = cursor.execute( """ SELECT COALESCE( SUM( CASE WHEN period = 'Год' THEN amount / 12.0 ELSE amount END ), 0 ) AS total FROM subscriptions WHERE status = 'Активна' """ ).fetchone()["total"] category_rows = cursor.execute( """ SELECT c.name, SUM(t.amount) AS total FROM transactions AS t JOIN categories AS c ON c.id = t.category_id WHERE t.transaction_type = 'Расход' AND strftime('%Y-%m', t.created_at) = strftime('%Y-%m', 'now') GROUP BY c.name HAVING total > 0 ORDER BY total DESC """ ).fetchall() net_result = summary["income"] - summary["expense"] top_category = category_rows[0]["name"] if category_rows else "Нет данных" top_category_value = category_rows[0]["total"] if category_rows else 0 report_text = ( "ОТЧЁТ ЗА ТЕКУЩИЙ МЕСЯЦ\n\n" f"Доходы: {summary['income']:,.2f} ₽\n" f"Расходы: {summary['expense']:,.2f} ₽\n" f"Финансовый результат: {net_result:,.2f} ₽\n" f"Общий баланс по всем счетам: {balance:,.2f} ₽\n" f"Активные подписки в пересчёте на месяц: {subscription_total:,.2f} ₽\n" f"Самая затратная категория: {top_category} ({top_category_value:,.2f} ₽)\n" ).replace(",", " ") self.last_report_text = report_text self.report_box.delete("1.0", "end") self.report_box.insert("1.0", report_text) self.figure.clear() axis = self.figure.add_subplot(111) axis.set_title("Расходы по категориям") if category_rows: labels = [row["name"] for row in category_rows] values = [row["total"] for row in category_rows] axis.pie(values, labels=labels, autopct="%1.1f%%") else: axis.text(0.5, 0.5, "Нет данных для диаграммы", ha="center", va="center") axis.axis("off") self.canvas.draw() def export_report(self) -> None: if not self.last_report_text: messagebox.showinfo("Информация", "Сначала обновите отчёт.") return file_path = filedialog.asksaveasfilename( defaultextension=".txt", filetypes=[("Text files", "*.txt")], title="Сохранить отчёт", ) if not file_path: return with open(file_path, "w", encoding="utf-8") as file: file.write(self.last_report_text) messagebox.showinfo("Готово", "Отчёт сохранён.")