Загрузка данных


import customtkinter as ctk
from tkinter import ttk, messagebox
import datetime
import os

# Для календаря
try:
    from tkcalendar import DateEntry
except ImportError:
    DateEntry = None

# Для экспорта в Word
try:
    import docx
except ImportError:
    docx = None

ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")

class ProjectFillerApp(ctk.CTk):
    def __init__(self):
        super().__init__()

        self.title("Наполнение проекта")
        
        # --- Настройка динамического окна ---
        screen_height = self.winfo_screenheight()
        window_width = 650
        
        # Начальное состояние: только заголовки + отступы
        initial_height = 220 
        
        x_cord = int((self.winfo_screenwidth() / 2) - (window_width / 2))
        y_cord = int((screen_height / 2) - (initial_height / 2))
        
        self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
        self.resizable(True, False) # Менять можно только высоту для чистоты анимации

        main_container = ctk.CTkFrame(self, corner_radius=10)
        main_container.pack(padx=10, pady=10, fill="both", expand=True)

        self._build_project_selector(main_container)
        
        accordion_frame = ctk.CTkFrame(main_container)
        accordion_frame.pack(fill="both", expand=True, pady=(10, 0))

        self.accordion_data = {}
        self.currently_open_button = None

        # Создание секций
        events_item = self._create_accordion_header(accordion_frame, "Открыть для ввода событий")
        self._fill_events_section(events_item["content"])

        charts_item = self._create_accordion_header(accordion_frame, "Показать графики")
        self._fill_charts_section(charts_item["content"])

        print_item = self._create_accordion_header(accordion_frame, "Настройка печати")
        self._fill_print_section(print_item["content"])

        # Привязка команд переключения
        for item in [events_item, charts_item, print_item]:
            btn = item["button"]
            content = item["content"]
            self.accordion_data[btn] = {"content": content}
            btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))

        button_frame = ctk.CTkFrame(main_container)
        button_frame.pack(fill="x", pady=(10, 0))
        
        save_btn = ctk.CTkButton(button_frame, text="Сохранить изменения", width=150)
        save_btn.pack(side="right", padx=10)
        
        cancel_btn = ctk.CTkButton(button_frame, text="Отмена", width=100, fg_color="transparent", border_color="#666666", text_color="#666666")
        cancel_btn.pack(side="right")

        # ПРИВЯЗКА ОБРАБОТЧИКА ЗАКРЫТИЯ ОКНА:
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

    # --- УПРАВЛЕНИЕ ОКНОМ И АККОРДЕОНОМ ---
    def toggle_section(self, button, content):
        is_currently_open = (self.currently_open_button == button)

        if self.currently_open_button and not is_currently_open:
            prev_content = self.accordion_data[self.currently_open_button]["content"]
            prev_content.pack_forget()
            self.currently_open_button.configure(fg_color="#34495e")

        if is_currently_open:
            content.pack_forget()
            button.configure(fg_color="#34495e")
            self.currently_open_button = None
            new_geo = self._calculate_window_geometry(collapsed=True)
        else:
            content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
            button.configure(fg_color="#2c3e50")
            self.currently_open_button = button
            new_geo = self._calculate_window_geometry(collapsed=False)

        # Используем after_idle для корректного расчета размеров после отрисовки
        self.after_idle(lambda g=new_geo: self.geometry(g))


    def _calculate_window_geometry(self, collapsed=False):
        """Вычисляет ПОЛНУЮ геометрию окна (Ширина x Высота)"""
        
        # Получаем текущую ширину окна, чтобы она не прыгала
        current_width = self.winfo_width()
        if current_width == 1: # Если окно еще не нарисовалось, берем заданную из __init__
            current_width = 650

        header_height = 50 * 3 + 30 
        
        if collapsed or not self.currently_open_button:
            target_height = header_height + 80
        else:
            data = self.accordion_data[self.currently_open_button]
            content_widget = data["content"]
            
            # Обновляем дерево виджетов перед расчетом высоты
            self.update_idletasks()
            open_content_height = content_widget.winfo_reqheight()
            target_height = header_height + open_content_height + 120

        max_h = self.winfo_screenheight() - 50
        final_height = min(target_height, max_h)
        
        # ВОЗВРАЩАЕМ СТРОКУ "ШИРИНА x ВЫСОТА"
        return f"{current_width}x{final_height}"

    # --- БЛОК 1: СЕЛЕКТОР ПРОЕКТА СВЕРХУ ---
    def _build_project_selector(self, container):
        frame = ctk.CTkFrame(container)
        frame.pack(fill="x", pady=(0, 10))

        label_project = ctk.CTkLabel(frame, text="Выбрать проект:", anchor="w")
        label_project.pack(side="left", padx=(0, 10))

        projects_list = ["Проект Альфа", "Бета-версия 2.0", "Гамма-отчет", "Тестовый стенд"]
        
        self.project_selector = ctk.CTkComboBox(
            frame,
            values=projects_list,
            state="readonly",
            width=250
        )
        self.project_selector.set(projects_list[0])
        self.project_selector.pack(side="left")

    # --- УТИЛИТА ЗАГОЛОВКА ---
    def _create_accordion_header(self, parent, title):
        item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
        item_container.pack(fill="x", pady=5)

        header_btn = ctk.CTkButton(
            item_container,
            text=title,
            height=45,
            font=ctk.CTkFont(weight="bold", size=16),
            anchor="w",
            fg_color="#34495e",
            hover_color="#3c5a72",
        )
        header_btn.pack(fill="x")

        content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
        
        return {"button": header_btn, "content": content_frame}

    # --- БЛОК 2: ОТКРЫТЬ ДЛЯ ВВОДА СОБЫТИЙ ---
    def _fill_events_section(self, frame):
        form = ctk.CTkFrame(frame, fg_color="transparent")
        form.pack(fill="x", expand=True, padx=10, pady=10)
        form.grid_columnconfigure(1, weight=1)

        # Группа
        lbl_group = ctk.CTkLabel(form, text="Выбрать группу:", anchor="w")
        lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        cmb_group = ctk.CTkComboBox(form, values=["Группа А", "Группа Б", "Складской учет"], width=200)
        cmb_group.set("Группа А")
        cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 10))

        # Показатель
        lbl_metric = ctk.CTkLabel(form, text="Выбрать показатель:", anchor="w")
        lbl_metric.grid(row=1, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        cmb_metric = ctk.CTkComboBox(form, values=["Тип ошибки", "Время простоя", "Кол-во операций"])
        cmb_metric.set("Тип ошибки")
        cmb_metric.grid(row=1, column=1, sticky="ew", pady=(0, 15))

        # Дата и Направленность
        meta_frame = ctk.CTkFrame(form, fg_color="transparent")
        meta_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(0, 15))
        meta_frame.grid_columnconfigure(1, weight=1)

        date_label = ctk.CTkLabel(meta_frame, text="Дата события:", anchor="w")
        date_label.grid(row=0, column=0, sticky="w", padx=(0, 10))
        date_entry = ctk.CTkEntry(meta_frame, placeholder_text="ДД.ММ.ГГГГ", width=120)
        date_entry.grid(row=0, column=1, sticky="w")

        dir_label = ctk.CTkLabel(meta_frame, text="Направленность:", anchor="w")
        dir_label.grid(row=0, column=2, sticky="w", padx=(15, 0))
        dir_entry = ctk.CTkEntry(meta_frame, placeholder_text="Входящее", width=120)
        dir_entry.grid(row=0, column=3, sticky="w")

        # Название
        name_label = ctk.CTkLabel(form, text="Название события:", anchor="w")
        name_label.grid(row=3, column=0, sticky="w", padx=(0, 10))
        event_name_entry = ctk.CTkEntry(form, height=30)
        event_name_entry.grid(row=3, column=1, sticky="ew", pady=(0, 15))

        # Кнопки действий
        btns_frame = ctk.CTkFrame(form, fg_color="transparent")
        btns_frame.grid(row=4, column=0, columnspan=2, sticky="ew")
        
        insert_btn = ctk.CTkButton(btns_frame, text="Вставить", width=80)
        insert_btn.pack(side="left", padx=5)
        add_btn = ctk.CTkButton(btns_frame, text="Добавить", width=80)
        add_btn.pack(side="left", padx=5)
        edit_btn = ctk.CTkButton(btns_frame, text="Редактировать", width=100)
        edit_btn.pack(side="left", padx=5)
        del_btn = ctk.CTkButton(btns_frame, text="Удалить", width=80, fg_color="#D32F2F", hover_color="#C62828")
        del_btn.pack(side="left", padx=5)

        # Таблица
        table_frame = ctk.CTkFrame(form)
        table_frame.grid(row=5, column=0, columnspan=2, sticky="nsew", pady=(10, 0))
        table_frame.grid_rowconfigure(0, weight=1)
        table_frame.grid_columnconfigure(0, weight=1)

        columns = ("#", "code", "date", "name") 
        tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=5)
        
        tree.heading("#", text="№ п/п"); tree.column("#", width=60, stretch=False, anchor="center")
        tree.heading("code", text="Код"); tree.column("code", width=80, stretch=False, anchor="center")
        tree.heading("date", text="Дата"); tree.column("date", width=100, stretch=False, anchor="center")
        tree.heading("name", text="Название"); tree.column("name", minwidth=200, anchor="w")

        scrollbar = ctk.CTkScrollbar(table_frame, orientation="vertical", command=tree.yview)
        tree.configure(yscrollcommand=scrollbar.set)
        
        tree.grid(row=0, column=0, sticky="nsew")
        scrollbar.grid(row=0, column=1, sticky="ns")

        for i in range(1, 11):
            tree.insert("", "end", values=(i, f"C{i:03}", f"01.07.2026", f"Эталонное название строки {i}"))

        # --- Поиск ---
        search_frame = ctk.CTkFrame(form, fg_color="transparent")
        search_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        
        # Настраиваем колонки ВНУТРИ search_frame
        search_frame.grid_columnconfigure(0, weight=0) # Лейбл
        search_frame.grid_columnconfigure(1, weight=1) # Поле ввода (растягивается)
        search_frame.grid_columnconfigure(2, weight=0) # Кнопка

        code_search_label = ctk.CTkLabel(search_frame, text="Код события:", anchor="w")
        code_search_label.grid(row=0, column=0, sticky="w", padx=(0, 5), pady=5)
        
        code_search_entry = ctk.CTkEntry(search_frame, placeholder_text="Например: INV-2026-001")
        code_search_entry.grid(row=0, column=1, sticky="ew", padx=(0, 5), pady=5)
        
        find_btn = ctk.CTkButton(search_frame, text="Найти", width=80)
        find_btn.grid(row=0, column=2, sticky="e", pady=5)

    # --- БЛОК 3: ПОКАЗАТЬ ГРАФИКИ ---
    def _fill_charts_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return

        form = ctk.CTkFrame(frame, fg_color="transparent")
        form.pack(fill="x", expand=True, padx=10, pady=10)
        form.grid_columnconfigure(1, weight=1)

        lbl_group = ctk.CTkLabel(form, text="Выбрать группу:", anchor="w")
        lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        cmb_group = ctk.CTkComboBox(form, values=["Группа А", "Группа Б", "Все данные"])
        cmb_group.set("Все данные")
        cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 10))

        lbl_date = ctk.CTkLabel(form, text="Дата начала:", anchor="w")
        lbl_date.grid(row=1, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        date_entry = DateEntry(
            master=form,
            width=18,
            background='darkblue',
            foreground='white', 
            locale='ru_RU',
            date_pattern='dd.MM.yyyy'
        )
        date_entry.delete(0, "end")
        date_entry.insert(0, datetime.date.today().strftime("%d.%m.%Y"))
        date_entry.grid(row=1, column=1, sticky="w", pady=(0, 10))

        lbl_step_event = ctk.CTkLabel(form, text="Шаг событий:", anchor="w")
        lbl_step_event.grid(row=2, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        cmb_step_event = ctk.CTkComboBox(form, values=["Авто", "Час", "Смена", "День"])
        cmb_step_event.set("Авто")
        cmb_step_event.grid(row=2, column=1, sticky="ew", pady=(0, 10))

        lbl_step_days = ctk.CTkLabel(form, text="Шаг по дням:", anchor="w")
        lbl_step_days.grid(row=3, column=0, sticky="w", pady=(0, 20), padx=(0, 10))
        cmb_step_days = ctk.CTkComboBox(form, values=["1 день", "3 дня", "Неделя", "Месяц"])
        cmb_step_days.set("1 день")
        cmb_step_days.grid(row=3, column=1, sticky="ew", pady=(0, 20))

        lbl_chart = ctk.CTkLabel(frame, text="Образец графика:", anchor="w")
        lbl_chart.pack(anchor="w", padx=10, pady=(10, 5))
        
        chart_placeholder = ctk.CTkFrame(frame, height=250, fg_color="transparent")
        chart_placeholder.pack(fill="x", padx=10, pady=(0, 15))

        try:
            import matplotlib.pyplot as plt
            from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
            
            fig, ax = plt.subplots(figsize=(6, 3), dpi=100)
            ax.plot([0, 1, 2, 3], [10, 40, 20, 50], marker='o', color='#3b82f6')
            ax.fill_between([0, 1, 2, 3], [10, 40, 20, 50], alpha=0.2, color='#3b82f6')
            ax.set_title('Динамика показателя', fontsize=10)
            ax.tick_params(axis='x', labelrotation=45)
            ax.grid(True, linestyle='--', alpha=0.6)
            plt.tight_layout()

            canvas = FigureCanvasTkAgg(fig, master=chart_placeholder)
            canvas.draw()
            canvas.get_tk_widget().pack(fill="both", expand=True)
            
        except ImportError:
            fallback_label = ctk.CTkLabel(chart_placeholder, text="[График]\nУстановите matplotlib", justify="center")
            fallback_label.pack(expand=True)

        btns_frame = ctk.CTkFrame(frame, fg_color="transparent")
        btns_frame.pack(fill="x", padx=10, pady=(0, 15))

        show_btn = ctk.CTkButton(btns_frame, text="Показать", width=120)
        show_btn.pack(side="left", expand=True, padx=(0, 5))
        
        copy_btn = ctk.CTkButton(btns_frame, text="Копировать", width=120, fg_color="#1F8A5B", hover_color="#165C3E")
        copy_btn.pack(side="left", expand=True, padx=(5, 0))

    # --- БЛОК 4: НАСТРОЙКА ПЕЧАТИ ---
    def _fill_print_section(self, frame):
        if DateEntry is None:
            error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
            error_lbl.pack(expand=True)
            return

        form = ctk.CTkFrame(frame, fg_color="transparent")
        form.pack(fill="x", expand=True, padx=10, pady=10)
        form.grid_columnconfigure(1, weight=1)

        lbl_group = ctk.CTkLabel(form, text="Выбрать группу:", anchor="w")
        lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        cmb_group = ctk.CTkComboBox(form, values=["Группа А", "Группа Б", "Складской учет"])
        cmb_group.set("Группа А")
        cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 10))

        lbl_date = ctk.CTkLabel(form, text="Выбрать дату:", anchor="w")
        lbl_date.grid(row=1, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        date_entry = DateEntry(
            master=form,
            width=18,
            background='darkblue',
            foreground='white',
            locale='ru_RU',
            date_pattern='dd.MM.yyyy'
        )
        date_entry.delete(0, "end")
        date_entry.insert(0, datetime.date.today().strftime("%d.%m.%Y"))
        date_entry.grid(row=1, column=1, sticky="w", pady=(0, 10))

        lbl_orientation = ctk.CTkLabel(form, text="Ориентация страницы:", anchor="w")
        lbl_orientation.grid(row=2, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
        cmb_orientation = ctk.CTkComboBox(form, values=["Книжная", "Альбомная"])
        cmb_orientation.set("Книжная")
        cmb_orientation.grid(row=2, column=1, sticky="ew", pady=(0, 20))

        btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
        btn_frame.pack(fill="x", padx=10, pady=(0, 15))

        word_btn = ctk.CTkButton(
            btn_frame, 
            text="Печать в Word", 
            width=160,
            fg_color="#1E87F0",
            hover_color="#1869C0",
            command=lambda: self._generate_word_report(
                cmb_group.get(),
                date_entry.get(),
                cmb_orientation.get()
            )
        )
        word_btn.pack(side="left", expand=True)

    # --- ЛОГИКА ЭКСПОРТА В WORD ---
    def _generate_word_report(self, group, report_date, orientation):
        if docx is None:
            messagebox.showerror("Библиотека не найдена", "Для экспорта нужна библиотека 'python-docx'.\nВыполните:\npip install python-docx")
            return

        safe_group = "".join(x for x in group if x.isalnum() or x in " _-")
        filename = f"Отчет_{safe_group}_{report_date}.docx"

        document = docx.Document()
        document.add_heading(f'Отчет по группе: {group}', level=1)
        
        p = document.add_paragraph('Дата создания: ')
        p.add_run(datetime.datetime.now().strftime('%d.%m.%Y %H:%M')).italic = True
        
        document.add_heading('Параметры отчета:', level=2)
        document.add_paragraph(f"• Выбранная группа: {group}")
        document.add_paragraph(f"• Дата данных: {report_date}")
        document.add_paragraph(f"• Ориентация: {orientation}")
        
        try:
            document.save(filename)
            messagebox.showinfo("Успех", f"Файл успешно создан в папке скрипта:\n{filename}")
        except Exception as e:
            messagebox.showerror("Ошибка записи", f"Не удалось сохранить файл:\n{e}")

    def on_closing(self):
        """Жесткое завершение процесса"""
        
        # 1. Пытаемся мягко очистить очередь задач
        try:
            self.after_cancel("all")
        except Exception:
            pass

        # 2. Уничтожаем виджеты вручную (помогает сборщику мусора)
        for item in list(self.accordion_data.values()):
            try:
                content = item.get("content")
                if content:
                    content.destroy()
            except Exception:
                pass
        
        # 3. Разрушаем само окно
        try:
            self.destroy()
        except Exception:
            pass
        
        # === ГЛАВНОЕ ИСПРАВЛЕНИЕ ===
        # 4. Принудительно выходим из Python с кодом 0 (успех)
        # Это игнорирует все висящие потоки и мгновенно возвращает управление консоли.
        import sys
        os._exit(0) 

import sys
import os

class HiddenStderr:
    """Перенаправляет stderr в null-устройство (пустоту)"""
    def __enter__(self):
        # Сохраняем старый дескриптор потока ошибок (обычно 2)
        self.old_stderr_fd = os.dup(2)
        
        # Создаем ссылку на "черную дыру" системы
        devnull = os.open(os.devnull, os.O_WRONLY)
        
        # Заменяем стандартный поток ошибок (дескриптор 2) на /dev/null
        os.dup2(devnull, 2)
        
        # Закрываем временный файловый дескриптор
        os.close(devnull)
        
        # Также подменяем объект sys.stderr для чистого вывода питоновских print()
        self.old_stderr = sys.stderr
        sys.stderr = open(os.devnull, 'w')

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Возвращаем оригинальный дескриптор обратно
        os.dup2(self.old_stderr_fd, 2)
        os.close(self.old_stderr_fd)
        
        # Возвращаем объект sys.stderr
        if self.old_stderr:
            sys.stderr.close() # закрываем заглушку
            sys.stderr = self.old_stderr

if __name__ == "__main__":
    ctk.set_widget_scaling(1.0)
    ctk.set_window_scaling(1.0)
    
    app = ProjectFillerApp()
    app.protocol("WM_DELETE_WINDOW", app.on_closing)

    # Запускаем приложение внутри "тихого" режима
    with HiddenStderr():
        app.mainloop()