Skip to content

Instantly share code, notes, and snippets.

@Madhawa97
Last active July 6, 2025 15:48
Show Gist options
  • Save Madhawa97/8707007771f058a201d9764865580095 to your computer and use it in GitHub Desktop.
Save Madhawa97/8707007771f058a201d9764865580095 to your computer and use it in GitHub Desktop.

How to Encrypt Your Home Directory on Ubuntu 22.04

Encryption is a process of transforming data into an unreadable form that can only be accessed by authorized parties. Encryption can protect your personal and sensitive information from unauthorized access, theft, or tampering. One of the ways to encrypt your data on Ubuntu is to encrypt your home directory, which is where your personal files and settings are stored.

This gist will describe how to encrypt your home directory on Ubuntu 22.04 after installation. This method is useful if you have already installed Ubuntu without encryption and want to add it later. It will also work on other versions of Ubuntu that use the ecryptfs-utils package for encryption.

Backup Your Data

Please make sure you have created a backup of your system/files before proceeding with the encryption process. This process may involve significant changes to your system, and having a backup ensures that you can restore your data in case of any unexpected issues during the encryption.

Install Necessary Packages

Before we start, we need to install some packages that are required for encryption. Open a terminal and run the following command:

$ sudo apt install ecryptfs-utils cryptsetup

This will install the ecryptfs-utils package, which provides tools for managing encrypted file systems, and the cryptsetup package, which provides tools for setting up encrypted devices.

Create a New User and Grant Privileges

Next, we need to create a new user account that will be used to perform the encryption process. This is because we cannot encrypt the home directory of the user that is currently logged in. We will also grant this user sudo privileges so that they can run commands as root.

To create a new user account, run the following command:

$ sudo adduser backup_user

You will be prompted to enter a password and some optional information for the new user. After that, run the following command to add the new user to the sudo group:

$ sudo usermod -aG sudo backup_user

Log Out and Log In as the New User

Now that we have created a new user account, we need to log out of our current session and log in as the new user. To log out, click on the power icon on the top right corner of the screen and select Log Out. Do NOT reboot when logging out. This is very important because rebooting will cause problems with the encryption process.

After logging out, select the new user from the login screen and enter their password. You should now be logged in as the new user.

Encrypt the Home Folder

We are now ready to encrypt the home folder of our original user account. To do this, we will use a command called ecryptfs-migrate-home, which will migrate the existing home folder to an encrypted one.

First, we need to confirm the username of our original user account. Run the following command, replacing your_original_account_username with your actual username:

$ sudo ls -l ~your_original_account_username

This should display the contents of the home directory of the original user account. If you see an error message or an empty directory, make sure you have entered the correct username.

Next, run the following command to start the encryption process:

$ sudo ecryptfs-migrate-home -u your_original_account_username

This will create a temporary folder with a random name in /home and copy all the files from the original home folder to it. Then, it will mount an encrypted file system over the original home folder and move all the files back to it. Finally, it will unmount and delete the temporary folder.

The encryption process may take some time depending on how much data you have in your home folder. During this time, do not interrupt or close the terminal window.

Once done, you will see some important notes displayed on the screen. These notes contain information about how to access your encrypted data and how to recover it in case of emergency. Capture a photograph of these notes and keep them in a safe place.

20230726_101429

Log Out and Log Back In as the Original User

After encrypting the home folder, we need to log out of our current session and log back in as our original user. To log out, click on the power icon on the top right corner of the screen and select Log Out. Make sure to NOT restart the machine. This is really important because restarting will cause problems with accessing your encrypted data.

After logging out, select your original user from the login screen and enter their password. You should now be logged in as your original user with an encrypted home folder.

Confirm Home Folder Encryption

To confirm that your home folder is encrypted, you can try to create a text file with some content in it and see if you can access it normally. For example, run the following commands in a terminal:

$ echo "Hello, world!" > ~/test.txt
$ cat ~/test.txt

You should see the output "Hello, world!" on the screen. This means that you have access to write and read data in your home folder.

Record Your Encryption Passphrase

One of the most important things to do after encrypting your home folder is to record your encryption passphrase. This is a secret key that is used to unlock your encrypted data. Without it, you will not be able to access your data if you forget your password or if your system fails.

To view your encryption passphrase, run the following command in a terminal:

$ sudo ecryptfs-unwrap-passphrase ~/.ecryptfs/wrapped-passphrase

You will be asked to enter your login password. After that, you will see a long string of characters on the screen. This is your encryption passphrase. Remember to save it in a safe location such as a USB drive, a cloud storage service, or a piece of paper.

Encrypt Swap Space

If you have a swap partition set up on your system, it can also be encrypted using the ecryptfs-setup-swap command. Swap is a space on your disk that is used to store temporary data when your system runs out of memory. Encrypting swap can prevent someone from recovering sensitive data from it.

To encrypt swap, run the following command in a terminal:

$ sudo ecryptfs-setup-swap

This will disable the existing swap partition, create an encrypted swap file, and enable it. You may need to reboot your system for the changes to take effect.

Clean Up

With the home folder and swap space successfully encrypted, we can remove the user and extra files we created for the encryption process.

To remove the user account we created earlier, run the following command:

$ sudo deluser --remove-home backup_user

This will delete the user and their home folder from the system.

Next, delete the temporary folder that was created when we originally ran the migration command. The folder location should be displayed after the encryption process. For example, it may look something like this:

/home/your_original_account_username.sikwr0Wp

To delete this folder, run the following command, replacing the folder name with yours:

$ sudo rm -Rf /home/your_original_account_username.sikwr0Wp

This will delete the folder and all its contents.

Congratulations!

You have successfully encrypted your home directory on Ubuntu 22.04. You can now enjoy the benefits of having more privacy and security for your personal data. Remember to keep your encryption passphrase safe and backup your data regularly.

I hope you found this blog post helpful and informative. If you have any questions or feedback,

@Strel1988k
Copy link

import sys
import os
import sqlite3
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QFileDialog, QTableWidget, QTableWidgetItem, QComboBox, QMessageBox,
QTabWidget, QProgressBar, QHeaderView, QAbstractItemView, QSizePolicy,
QMenu, QAction, QInputDialog, QColorDialog, QStyleFactory, QStyle,
QDialog, QDialogButtonBox, QFormLayout, QGroupBox, QRadioButton, QCheckBox,
QListWidget, QListWidgetItem, QSplitter, QLineEdit, QGridLayout)
from PyQt5.QtCore import Qt, QTimer, QDate
from PyQt5.QtGui import QIcon, QColor, QFont, QBrush, QPixmap

class ColumnFormatDialog(QDialog):
"""Диалог для форматирования целого столбца"""
def init(self, parent=None):
super().init(parent)
self.setWindowTitle("Форматирование столбца")
self.setWindowIcon(parent.style().standardIcon(QStyle.SP_FileDialogDetailedView))
self.setMinimumWidth(400)

    # Основной layout
    layout = QVBoxLayout()
    
    # Группа форматов данных
    format_group = QGroupBox("Формат данных")
    format_layout = QVBoxLayout()
    
    self.format_text = QRadioButton("Текст")
    self.format_number = QRadioButton("Число")
    self.format_percent = QRadioButton("Процент")
    self.format_currency = QRadioButton("Денежный")
    self.format_date = QRadioButton("Дата")
    self.format_datetime = QRadioButton("Дата и время")
    
    self.format_text.setChecked(True)
    
    format_layout.addWidget(self.format_text)
    format_layout.addWidget(self.format_number)
    format_layout.addWidget(self.format_percent)
    format_layout.addWidget(self.format_currency)
    format_layout.addWidget(self.format_date)
    format_layout.addWidget(self.format_datetime)
    format_group.setLayout(format_layout)
    
    # Группа внешнего вида
    appearance_group = QGroupBox("Внешний вид")
    appearance_layout = QFormLayout()
    
    self.font_btn = QPushButton("Выбрать шрифт...")
    self.font_btn.clicked.connect(self.select_font)
    appearance_layout.addRow("Шрифт:", self.font_btn)
    
    self.text_color_btn = QPushButton("Выбрать цвет текста...")
    self.text_color_btn.clicked.connect(self.select_text_color)
    appearance_layout.addRow("Цвет текста:", self.text_color_btn)
    
    self.bg_color_btn = QPushButton("Выбрать цвет фона...")
    self.bg_color_btn.clicked.connect(self.select_bg_color)
    appearance_layout.addRow("Цвет фона:", self.bg_color_btn)
    
    self.alignment_combo = QComboBox()
    self.alignment_combo.addItems(["По левому краю", "По центру", "По правому краю"])
    appearance_layout.addRow("Выравнивание:", self.alignment_combo)
    
    appearance_group.setLayout(appearance_layout)
    
    # Кнопки
    button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
    button_box.accepted.connect(self.accept)
    button_box.rejected.connect(self.reject)
    
    layout.addWidget(format_group)
    layout.addWidget(appearance_group)
    layout.addWidget(button_box)
    
    self.setLayout(layout)
    
    # Инициализация значений по умолчанию
    self.selected_font = QFont()
    self.text_color = QColor(Qt.black)
    self.bg_color = QColor(Qt.white)
    self.alignment = Qt.AlignLeft
    
    # Обновление кнопок
    self.update_button_colors()

def update_button_colors(self):
    """Обновление цвета кнопок в соответствии с выбранными цветами"""
    self.text_color_btn.setStyleSheet(f"background-color: {self.text_color.name()}; color: {'white' if self.text_color.lightness() < 128 else 'black'};")
    self.bg_color_btn.setStyleSheet(f"background-color: {self.bg_color.name()}; color: {'white' if self.bg_color.lightness() < 128 else 'black'};")

def select_font(self):
    """Выбор шрифта"""
    font, ok = QFontDialog.getFont(self.selected_font, self)
    if ok:
        self.selected_font = font

def select_text_color(self):
    """Выбор цвета текста"""
    color = QColorDialog.getColor(self.text_color, self, "Выберите цвет текста")
    if color.isValid():
        self.text_color = color
        self.update_button_colors()

def select_bg_color(self):
    """Выбор цвета фона"""
    color = QColorDialog.getColor(self.bg_color, self, "Выберите цвет фона")
    if color.isValid():
        self.bg_color = color
        self.update_button_colors()

def get_format_settings(self):
    """Получение настроек форматирования"""
    # Определение формата данных
    if self.format_number.isChecked():
        data_format = "number"
    elif self.format_percent.isChecked():
        data_format = "percent"
    elif self.format_currency.isChecked():
        data_format = "currency"
    elif self.format_date.isChecked():
        data_format = "date"
    elif self.format_datetime.isChecked():
        data_format = "datetime"
    else:
        data_format = "text"
    
    # Определение выравнивания
    alignment_text = self.alignment_combo.currentText()
    if alignment_text == "По центру":
        alignment = Qt.AlignCenter
    elif alignment_text == "По правому краю":
        alignment = Qt.AlignRight
    else:
        alignment = Qt.AlignLeft
    
    return {
        'data_format': data_format,
        'font': self.selected_font,
        'text_color': self.text_color,
        'bg_color': self.bg_color,
        'alignment': alignment
    }

class ReportPreviewDialog(QDialog):
"""Диалог предпросмотра отчета"""
def init(self, report_df, parent=None):
super().init(parent)
self.setWindowTitle("Предпросмотр отчета")
self.setWindowIcon(parent.style().standardIcon(QStyle.SP_FileDialogInfoView))
self.setGeometry(100, 100, 800, 600)

    layout = QVBoxLayout()
    
    # Таблица для предпросмотра
    self.preview_table = QTableWidget()
    self.preview_table.setRowCount(min(50, len(report_df)))
    self.preview_table.setColumnCount(len(report_df.columns))
    
    # Устанавливаем заголовки
    self.preview_table.setHorizontalHeaderLabels(report_df.columns)
    
    # Заполняем данными
    for row in range(self.preview_table.rowCount()):
        for col in range(self.preview_table.columnCount()):
            value = str(report_df.iloc[row, col])
            item = QTableWidgetItem(value)
            item.setFlags(item.flags() ^ Qt.ItemIsEditable)
            self.preview_table.setItem(row, col, item)
    
    # Автоподгонка ширины столбцов
    self.preview_table.resizeColumnsToContents()
    
    # Информация о отчете
    info_label = QLabel(f"Отображается {self.preview_table.rowCount()} из {len(report_df)} строк")
    
    # Кнопки
    btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
    btn_box.accepted.connect(self.accept)
    btn_box.rejected.connect(self.reject)
    
    layout.addWidget(QLabel("Предпросмотр отчета:"))
    layout.addWidget(self.preview_table)
    layout.addWidget(info_label)
    layout.addWidget(btn_box)
    
    self.setLayout(layout)

class ExcelDatabaseApp(QMainWindow):
def init(self):
super().init()
self.db_name = 'excel_database.db'
self.current_table = None
self.check_dependencies()
self.init_ui()
self.init_db()

def check_dependencies(self):
    """Проверка необходимых зависимостей"""
    self.missing_deps = []
    try:
        import openpyxl
    except ImportError:
        self.missing_deps.append('openpyxl')
    
    try:
        import matplotlib
    except ImportError:
        self.missing_deps.append('matplotlib')
    
    if self.missing_deps:
        msg = "Отсутствуют необходимые зависимости:\n"
        msg += "\n".join([f"- {dep}" for dep in self.missing_deps])
        msg += "\n\nУстановите их командой:\npip install " + " ".join(self.missing_deps)
        QMessageBox.critical(None, "Критическая ошибка", msg)
        sys.exit(1)

def init_db(self):
    """Инициализация базы данных"""
    with sqlite3.connect(self.db_name) as conn:
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS uploads (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                filename TEXT NOT NULL,
                upload_date DATETIME NOT NULL,
                table_name TEXT NOT NULL UNIQUE
            )
        ''')
        conn.commit()

def init_ui(self):
    """Инициализация интерфейса"""
    self.setWindowTitle('Excel Database Manager')
    self.setGeometry(100, 100, 1200, 800)
    
    # Создаем вкладки
    self.tabs = QTabWidget()
    self.setCentralWidget(self.tabs)
    
    # Вкладка загрузки
    self.upload_tab = QWidget()
    self.tabs.addTab(self.upload_tab, "Загрузка данных")
    self.setup_upload_tab()
    
    # Вкладка просмотра
    self.view_tab = QWidget()
    self.tabs.addTab(self.view_tab, "Просмотр данных")
    self.setup_view_tab()
    
    # Вкладка отчетов
    self.report_tab = QWidget()
    self.tabs.addTab(self.report_tab, "Генерация отчетов")
    self.setup_report_tab()
    
    # Статус бар
    self.status_bar = self.statusBar()
    self.progress = QProgressBar()
    self.status_bar.addPermanentWidget(self.progress)
    self.progress.setVisible(False)
    
    self.show()
    self.update_tables_list()
    self.load_uploads_history()

def setup_upload_tab(self):
    """Настройка вкладки загрузки с возможностью удаления"""
    layout = QVBoxLayout()
    
    # Верхняя панель для загрузки
    upload_layout = QHBoxLayout()
    
    # Кнопка выбора файла
    self.btn_select = QPushButton("Выбрать Excel-файл")
    self.btn_select.clicked.connect(self.select_file)
    upload_layout.addWidget(self.btn_select)
    
    # Кнопка загрузки
    self.btn_upload = QPushButton("Загрузить в базу данных")
    self.btn_upload.clicked.connect(self.upload_file)
    self.btn_upload.setEnabled(False)
    upload_layout.addWidget(self.btn_upload)
    
    layout.addLayout(upload_layout)
    
    # Информация о файле
    self.file_info = QLabel("Файл не выбран")
    layout.addWidget(self.file_info)
    
    # Панель для управления историей
    history_control_layout = QHBoxLayout()
    
    # Кнопка обновления истории
    self.btn_refresh_history = QPushButton("Обновить историю")
    self.btn_refresh_history.setIcon(self.style().standardIcon(QStyle.SP_BrowserReload))
    self.btn_refresh_history.clicked.connect(self.load_uploads_history)
    history_control_layout.addWidget(self.btn_refresh_history)
    
    # Кнопка удаления
    self.btn_delete = QPushButton("Удалить выбранный файл")
    self.btn_delete.setIcon(self.style().standardIcon(QStyle.SP_TrashIcon))
    self.btn_delete.clicked.connect(self.delete_selected_upload)
    self.btn_delete.setEnabled(False)
    history_control_layout.addWidget(self.btn_delete)
    
    # Растягивающийся элемент
    history_control_layout.addStretch()
    
    layout.addLayout(history_control_layout)
    
    # История загрузок
    layout.addWidget(QLabel("История загрузок:"))
    
    self.history_table = QTableWidget()
    self.history_table.setColumnCount(4)
    self.history_table.setHorizontalHeaderLabels(["ID", "Имя файла", "Дата загрузки", "Таблица в БД"])
    self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
    self.history_table.setSelectionBehavior(QAbstractItemView.SelectRows)
    self.history_table.setSelectionMode(QAbstractItemView.SingleSelection)
    self.history_table.itemSelectionChanged.connect(self.update_delete_button_state)
    
    layout.addWidget(self.history_table)
    
    self.upload_tab.setLayout(layout)

def update_delete_button_state(self):
    """Обновление состояния кнопки удаления в зависимости от выбора"""
    selected = self.history_table.selectedItems()
    self.btn_delete.setEnabled(len(selected) > 0)

def delete_selected_upload(self):
    """Удаление выбранного загруженного файла из базы данных"""
    selected_items = self.history_table.selectedItems()
    if not selected_items:
        return
        
    # Получаем данные из выбранной строки
    row = selected_items[0].row()
    upload_id = self.history_table.item(row, 0).text()
    filename = self.history_table.item(row, 1).text()
    table_name = self.history_table.item(row, 3).text()
    
    # Запрос подтверждения
    reply = QMessageBox.question(
        self, "Подтверждение удаления",
        f"Вы уверены, что хотите удалить файл '{filename}'?\n"
        f"Это приведет к удалению таблицы '{table_name}' из базы данных.\n"
        "Действие нельзя будет отменить!",
        QMessageBox.Yes | QMessageBox.No, QMessageBox.No
    )
    
    if reply == QMessageBox.No:
        return
        
    try:
        self.show_progress(True)
        QApplication.processEvents()
        
        # Удаляем таблицу данных и запись из истории
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            
            # Удаляем таблицу с данными
            try:
                cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
            except sqlite3.OperationalError as e:
                print(f"Ошибка удаления таблицы {table_name}: {str(e)}")
            
            # Удаляем запись из истории
            cursor.execute("DELETE FROM uploads WHERE id = ?", (upload_id,))
            conn.commit()
        
        # Обновляем интерфейс
        self.show_progress(False)
        QMessageBox.information(self, "Успех", f"Файл '{filename}' и таблица '{table_name}' успешно удалены!")
        
        # Обновляем списки
        self.update_tables_list()
        self.load_uploads_history()
        
        # Если удаленная таблица была открыта - очищаем просмотр
        if self.current_table == table_name:
            self.current_table = None
            self.data_table.clearContents()
            self.data_table.setRowCount(0)
            self.data_table.setColumnCount(0)
            self.change_status.setText("Нет данных")
        
    except Exception as e:
        self.show_progress(False)
        QMessageBox.critical(self, "Ошибка", f"Ошибка при удалении: {str(e)}")

def setup_view_tab(self):
    """Настройка вкладки просмотра с расширенными возможностями редактирования и поиска"""
    layout = QVBoxLayout()
    
    # Верхняя панель с элементами управления
    control_layout = QHBoxLayout()
    
    # Выбор таблицы
    control_layout.addWidget(QLabel("Выберите таблицу:"))
    self.table_selector = QComboBox()
    self.table_selector.setMinimumWidth(250)
    self.table_selector.currentIndexChanged.connect(self.load_table_data)
    control_layout.addWidget(self.table_selector)
    
    # Кнопка обновления данных
    self.btn_refresh = QPushButton("Обновить")
    self.btn_refresh.setIcon(self.style().standardIcon(QStyle.SP_BrowserReload))
    self.btn_refresh.clicked.connect(self.load_table_data)
    control_layout.addWidget(self.btn_refresh)
    
    # Кнопка авто-ширины
    self.btn_auto_width = QPushButton("Авто-ширина")
    self.btn_auto_width.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView))
    self.btn_auto_width.clicked.connect(self.auto_resize_columns)
    control_layout.addWidget(self.btn_auto_width)
    
    # Кнопка форматирования столбца
    self.btn_format_column = QPushButton("Формат столбца")
    self.btn_format_column.setIcon(self.style().standardIcon(QStyle.SP_FileDialogContentsView))
    self.btn_format_column.clicked.connect(self.format_current_column)
    control_layout.addWidget(self.btn_format_column)
    
    # Кнопка добавления строки
    self.btn_add_row = QPushButton("Добавить строку")
    self.btn_add_row.setIcon(self.style().standardIcon(QStyle.SP_FileDialogNewFolder))
    self.btn_add_row.clicked.connect(self.add_row)
    control_layout.addWidget(self.btn_add_row)
    
    # Кнопка удаления строк
    self.btn_delete_rows = QPushButton("Удалить строки")
    self.btn_delete_rows.setIcon(self.style().standardIcon(QStyle.SP_TrashIcon))
    self.btn_delete_rows.clicked.connect(self.delete_selected_rows)
    control_layout.addWidget(self.btn_delete_rows)
    
    # Кнопка сохранения изменений
    self.btn_save = QPushButton("Сохранить изменения")
    self.btn_save.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
    self.btn_save.clicked.connect(self.save_table_changes)
    self.btn_save.setEnabled(False)
    control_layout.addWidget(self.btn_save)
    
    # Кнопка отмены изменений
    self.btn_revert = QPushButton("Отменить изменения")
    self.btn_revert.setIcon(self.style().standardIcon(QStyle.SP_DialogResetButton))
    self.btn_revert.clicked.connect(self.revert_table_changes)
    self.btn_revert.setEnabled(False)
    control_layout.addWidget(self.btn_revert)
    
    # Растягивающийся элемент для выравнивания
    control_layout.addStretch()
    
    layout.addLayout(control_layout)
    
    # Панель поиска
    search_layout = QGridLayout()
    search_layout.setColumnStretch(1, 1)  # Растягиваем поле ввода
    
    # Поиск по всем столбцам
    search_layout.addWidget(QLabel("Поиск:"), 0, 0)
    self.search_input = QLineEdit()
    self.search_input.setPlaceholderText("Введите текст для поиска...")
    self.search_input.textChanged.connect(self.search_table)
    search_layout.addWidget(self.search_input, 0, 1)
    
    # Выбор столбца для поиска
    search_layout.addWidget(QLabel("В столбце:"), 0, 2)
    self.search_column_combo = QComboBox()
    self.search_column_combo.addItem("Все столбцы", -1)
    self.search_column_combo.currentIndexChanged.connect(self.search_table)
    search_layout.addWidget(self.search_column_combo, 0, 3)
    
    # Кнопки навигации по результатам
    self.btn_prev = QPushButton("← Назад")
    self.btn_prev.setEnabled(False)
    self.btn_prev.clicked.connect(self.prev_search_result)
    search_layout.addWidget(self.btn_prev, 0, 4)
    
    self.btn_next = QPushButton("Вперед →")
    self.btn_next.setEnabled(False)
    self.btn_next.clicked.connect(self.next_search_result)
    search_layout.addWidget(self.btn_next, 0, 5)
    
    # Статус поиска
    self.search_status = QLabel("")
    search_layout.addWidget(self.search_status, 1, 0, 1, 6)
    
    layout.addLayout(search_layout)
    
    # Отображение данных с возможностью редактирования
    self.data_table = QTableWidget()
    self.data_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
    
    # Настройки для редактирования
    self.data_table.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
    self.data_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
    self.data_table.setSelectionBehavior(QAbstractItemView.SelectRows)
    
    # Настройки для улучшения производительности
    self.data_table.setAlternatingRowColors(True)
    self.data_table.setSortingEnabled(True)
    self.data_table.verticalHeader().setDefaultSectionSize(24)
    self.data_table.verticalHeader().setVisible(True)
    
    # Включаем контекстное меню
    self.data_table.setContextMenuPolicy(Qt.CustomContextMenu)
    self.data_table.customContextMenuRequested.connect(self.show_context_menu)
    
    # Отслеживаем изменения
    self.data_table.itemChanged.connect(self.table_item_changed)
    
    # Двойной клик на заголовке столбца для форматирования
    self.data_table.horizontalHeader().sectionDoubleClicked.connect(self.format_column_by_index)
    
    # Добавляем таблицу в layout
    layout.addWidget(self.data_table)
    
    # Статус изменений
    self.change_status = QLabel("Нет изменений")
    layout.addWidget(self.change_status)
    
    self.view_tab.setLayout(layout)
    
    # Переменные для отслеживания изменений
    self.changes = {}
    self.has_unsaved_changes = False
    
    # Переменные для поиска
    self.search_results = []
    self.current_search_index = -1

def setup_report_tab(self):
    """Настройка вкладки отчетов с различными типами отчетов"""
    layout = QVBoxLayout()
    
    # Верхняя часть: выбор таблицы и формата
    top_layout = QHBoxLayout()
    
    # Выбор таблицы
    top_layout.addWidget(QLabel("Выберите таблицу:"))
    self.report_table_selector = QComboBox()
    self.report_table_selector.setMinimumWidth(200)
    top_layout.addWidget(self.report_table_selector)
    
    # Выбор формата
    top_layout.addWidget(QLabel("Формат отчета:"))
    self.format_selector = QComboBox()
    self.format_selector.addItems(["Excel (.xlsx)", "CSV (.csv)", "HTML (.html)", "PDF (.pdf)"])
    top_layout.addWidget(self.format_selector)
    
    layout.addLayout(top_layout)
    
    # Разделитель
    layout.addWidget(QLabel("Тип отчета:"))
    
    # Выбор типа отчета
    self.report_type_combo = QComboBox()
    self.report_type_combo.addItems([
        "Полный экспорт данных",
        "Сводная статистика",
        "Топ-10 значений",
        "Анализ по категориям",
        "Тренды по датам",
        "Корреляционный анализ",
        "Фильтрованный отчет",
        "Сводная таблица"
    ])
    self.report_type_combo.currentIndexChanged.connect(self.update_report_parameters)
    layout.addWidget(self.report_type_combo)
    
    # Контейнер для параметров отчета
    self.report_params_container = QWidget()
    self.report_params_layout = QVBoxLayout()
    self.report_params_container.setLayout(self.report_params_layout)
    layout.addWidget(self.report_params_container)
    
    # Кнопка предпросмотра
    self.btn_preview = QPushButton("Предпросмотр отчета")
    self.btn_preview.setIcon(self.style().standardIcon(QStyle.SP_FileDialogContentsView))
    self.btn_preview.clicked.connect(self.preview_report)
    layout.addWidget(self.btn_preview)
    
    # Кнопка генерации
    self.btn_generate = QPushButton("Сгенерировать отчет")
    self.btn_generate.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView))
    self.btn_generate.clicked.connect(self.generate_report)
    layout.addWidget(self.btn_generate)
    
    # Информация
    self.report_info = QLabel("")
    layout.addWidget(self.report_info)
    
    # Инициализируем параметры для первого отчета
    self.update_report_parameters()
    
    self.report_tab.setLayout(layout)

def update_report_parameters(self):
    """Обновление параметров отчета в зависимости от выбранного типа"""
    # Очищаем контейнер
    while self.report_params_layout.count():
        item = self.report_params_layout.takeAt(0)
        widget = item.widget()
        if widget:
            widget.deleteLater()
    
    report_type = self.report_type_combo.currentText()
    
    if report_type == "Полный экспорт данных":
        # Нет дополнительных параметров
        pass
        
    elif report_type == "Сводная статистика":
        layout = QHBoxLayout()
        layout.addWidget(QLabel("Статистика для числовых столбцов:"))
        
        self.stats_checkbox = QCheckBox("Включить статистику")
        self.stats_checkbox.setChecked(True)
        layout.addWidget(self.stats_checkbox)
        
        container = QWidget()
        container.setLayout(layout)
        self.report_params_layout.addWidget(container)
        
    elif report_type == "Топ-10 значений":
        layout = QFormLayout()
        
        self.top_column_combo = QComboBox()
        layout.addRow("Столбец для анализа:", self.top_column_combo)
        
        self.top_order_combo = QComboBox()
        self.top_order_combo.addItems(["Наибольшие значения", "Наименьшие значения"])
        layout.addRow("Порядок:", self.top_order_combo)
        
        container = QWidget()
        container.setLayout(layout)
        self.report_params_layout.addWidget(container)
        
        # Обновим список столбцов при изменении таблицы
        self.report_table_selector.currentIndexChanged.connect(self.update_top_columns)
        self.update_top_columns()
        
    elif report_type == "Анализ по категориям":
        layout = QFormLayout()
        
        self.category_column_combo = QComboBox()
        layout.addRow("Категорийный столбец:", self.category_column_combo)
        
        self.value_column_combo = QComboBox()
        layout.addRow("Столбец значений:", self.value_column_combo)
        
        self.agg_function_combo = QComboBox()
        self.agg_function_combo.addItems(["Сумма", "Среднее", "Количество", "Максимум", "Минимум"])
        layout.addRow("Функция агрегации:", self.agg_function_combo)
        
        container = QWidget()
        container.setLayout(layout)
        self.report_params_layout.addWidget(container)
        
        # Обновим списки столбцов
        self.report_table_selector.currentIndexChanged.connect(self.update_category_columns)
        self.update_category_columns()
        
    elif report_type == "Тренды по датам":
        layout = QFormLayout()
        
        self.date_column_combo = QComboBox()
        layout.addRow("Столбец с датой:", self.date_column_combo)
        
        self.value_trend_combo = QComboBox()
        layout.addRow("Столбец значений:", self.value_trend_combo)
        
        self.trend_period_combo = QComboBox()
        self.trend_period_combo.addItems(["День", "Неделя", "Месяц", "Квартал", "Год"])
        layout.addRow("Период группировки:", self.trend_period_combo)
        
        container = QWidget()
        container.setLayout(layout)
        self.report_params_layout.addWidget(container)
        
        # Обновим списки столбцов
        self.report_table_selector.currentIndexChanged.connect(self.update_trend_columns)
        self.update_trend_columns()
        
    elif report_type == "Корреляционный анализ":
        layout = QVBoxLayout()
        layout.addWidget(QLabel("Выберите столбцы для анализа корреляции:"))
        
        self.corr_columns_list = QListWidget()
        self.corr_columns_list.setSelectionMode(QListWidget.MultiSelection)
        layout.addWidget(self.corr_columns_list)
        
        container = QWidget()
        container.setLayout(layout)
        self.report_params_layout.addWidget(container)
        
        # Обновим список столбцов
        self.report_table_selector.currentIndexChanged.connect(self.update_corr_columns)
        self.update_corr_columns()
        
    elif report_type == "Фильтрованный отчет":
        layout = QFormLayout()
        
        self.filter_column_combo = QComboBox()
        layout.addRow("Столбец для фильтра:", self.filter_column_combo)
        
        self.filter_operator_combo = QComboBox()
        self.filter_operator_combo.addItems(["=", "!=", ">", ">=", "<", "<=", "содержит", "начинается с"])
        layout.addRow("Оператор:", self.filter_operator_combo)
        
        self.filter_value_edit = QLineEdit()
        layout.addRow("Значение:", self.filter_value_edit)
        
        container = QWidget()
        container.setLayout(layout)
        self.report_params_layout.addWidget(container)
        
        # Обновим список столбцов
        self.report_table_selector.currentIndexChanged.connect(self.update_filter_columns)
        self.update_filter_columns()
        
    elif report_type == "Сводная таблица":
        layout = QFormLayout()
        
        # Выбор столбцов для строк
        self.pivot_rows_combo = QComboBox()
        layout.addRow("Столбцы для строк:", self.pivot_rows_combo)
        
        # Выбор столбцов для колонок
        self.pivot_columns_combo = QComboBox()
        layout.addRow("Столбцы для колонок:", self.pivot_columns_combo)
        
        # Выбор столбца значений
        self.pivot_values_combo = QComboBox()
        layout.addRow("Столбец значений:", self.pivot_values_combo)
        
        # Выбор агрегирующей функции
        self.pivot_agg_combo = QComboBox()
        self.pivot_agg_combo.addItems(["Сумма", "Среднее", "Количество", "Максимум", "Минимум"])
        layout.addRow("Агрегирующая функция:", self.pivot_agg_combo)
        
        # Чекбокс для отображения итогов
        self.pivot_margins = QCheckBox("Показывать итоги")
        self.pivot_margins.setChecked(True)
        layout.addRow(self.pivot_margins)
        
        container = QWidget()
        container.setLayout(layout)
        self.report_params_layout.addWidget(container)
        
        # Обновляем списки столбцов
        self.report_table_selector.currentIndexChanged.connect(self.update_pivot_columns)
        self.update_pivot_columns()

def update_pivot_columns(self):
    """Обновление списков столбцов для сводной таблицы"""
    table_name = self.report_table_selector.currentText()
    if not table_name:
        return
        
    try:
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute(f"PRAGMA table_info({table_name})")
            columns = [col[1] for col in cursor.fetchall()]
            
        # Обновляем все комбобоксы
        for combo in [self.pivot_rows_combo, self.pivot_columns_combo, self.pivot_values_combo]:
            combo.clear()
            combo.addItems(columns)
        
    except Exception as e:
        print(f"Ошибка обновления столбцов для сводной таблицы: {str(e)}")

def update_top_columns(self):
    """Обновление списка столбцов для отчета Топ-10"""
    table_name = self.report_table_selector.currentText()
    if not table_name:
        return
        
    try:
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute(f"PRAGMA table_info({table_name})")
            columns = [col[1] for col in cursor.fetchall()]
            
        self.top_column_combo.clear()
        self.top_column_combo.addItems(columns)
        
    except Exception as e:
        print(f"Ошибка обновления столбцов для Топ-10: {str(e)}")

def update_category_columns(self):
    """Обновление списков столбцов для анализа по категориям"""
    table_name = self.report_table_selector.currentText()
    if not table_name:
        return
        
    try:
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute(f"PRAGMA table_info({table_name})")
            columns = [col[1] for col in cursor.fetchall()]
            
        self.category_column_combo.clear()
        self.category_column_combo.addItems(columns)
        
        self.value_column_combo.clear()
        self.value_column_combo.addItems(columns)
        
    except Exception as e:
        print(f"Ошибка обновления столбцов для анализа по категориям: {str(e)}")

def update_trend_columns(self):
    """Обновление списков столбцов для анализа трендов"""
    table_name = self.report_table_selector.currentText()
    if not table_name:
        return
        
    try:
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute(f"PRAGMA table_info({table_name})")
            columns = [col[1] for col in cursor.fetchall()]
            
        self.date_column_combo.clear()
        self.date_column_combo.addItems(columns)
        
        self.value_trend_combo.clear()
        self.value_trend_combo.addItems(columns)
        
    except Exception as e:
        print(f"Ошибка обновления столбцов для анализа трендов: {str(e)}")

def update_corr_columns(self):
    """Обновление списка столбцов для корреляционного анализа"""
    table_name = self.report_table_selector.currentText()
    if not table_name:
        return
        
    try:
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute(f"PRAGMA table_info({table_name})")
            columns = [col[1] for col in cursor.fetchall()]
            
        self.corr_columns_list.clear()
        for column in columns:
            item = QListWidgetItem(column)
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Unchecked)
            self.corr_columns_list.addItem(item)
        
    except Exception as e:
        print(f"Ошибка обновления столбцов для корреляционного анализа: {str(e)}")

def update_filter_columns(self):
    """Обновление списка столбцов для фильтрованного отчета"""
    table_name = self.report_table_selector.currentText()
    if not table_name:
        return
        
    try:
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute(f"PRAGMA table_info({table_name})")
            columns = [col[1] for col in cursor.fetchall()]
            
        self.filter_column_combo.clear()
        self.filter_column_combo.addItems(columns)
        
    except Exception as e:
        print(f"Ошибка обновления столбцов для фильтрованного отчета: {str(e)}")

def select_file(self):
    """Выбор файла для загрузки"""
    file_path, _ = QFileDialog.getOpenFileName(
        self, "Выберите Excel файл", "", "Excel Files (*.xlsx *.xls)"
    )
    if file_path:
        self.current_file = file_path
        self.file_info.setText(f"Выбран файл: {os.path.basename(file_path)}")
        self.btn_upload.setEnabled(True)

def upload_file(self):
    """Загрузка файла в базу данных"""
    if not hasattr(self, 'current_file'):
        return
        
    try:
        self.show_progress(True)
        QApplication.processEvents()  # Обновляем GUI
        
        # Чтение Excel
        df = pd.read_excel(self.current_file)
        if df.empty:
            raise ValueError("Файл не содержит данных")
            
        # Генерация имени таблицы
        table_name = f"data_{datetime.now().strftime('%Y%m%d%H%M%S')}"
        filename = os.path.basename(self.current_file)
        upload_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        
        # Сохранение в SQLite
        with sqlite3.connect(self.db_name) as conn:
            df.to_sql(table_name, conn, index=False, if_exists='fail')
            cursor = conn.cursor()
            cursor.execute('''
                INSERT INTO uploads (filename, upload_date, table_name)
                VALUES (?, ?, ?)
            ''', (filename, upload_date, table_name))
            conn.commit()
            
        # Обновление интерфейса
        self.show_progress(False)
        QMessageBox.information(self, "Успех", f"Файл успешно загружен в таблицу: {table_name}")
        self.update_tables_list()
        self.load_uploads_history()
        self.file_info.setText("Файл не выбран")
        self.btn_upload.setEnabled(False)
        delattr(self, 'current_file')
        
    except Exception as e:
        self.show_progress(False)
        error_msg = str(e)
        if "No engine for filetype" in error_msg or "openpyxl" in error_msg:
            error_msg += "\n\nУбедитесь, что установлены все зависимости:\npip install openpyxl xlrd"
        QMessageBox.critical(self, "Ошибка", error_msg)

def load_uploads_history(self):
    """Загрузка истории загрузок"""
    try:
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT * FROM uploads ORDER BY upload_date DESC")
            data = cursor.fetchall()
            
        self.history_table.setRowCount(len(data))
        for row_idx, row_data in enumerate(data):
            for col_idx, col_data in enumerate(row_data):
                item = QTableWidgetItem(str(col_data))
                item.setFlags(item.flags() ^ Qt.ItemIsEditable)
                self.history_table.setItem(row_idx, col_idx, item)
                
        # Автоподгонка ширины столбцов для истории
        self.history_table.resizeColumnsToContents()
        
        # Обновляем состояние кнопки удаления
        self.update_delete_button_state()
                
    except Exception as e:
        print(f"Ошибка загрузки истории: {str(e)}")

def update_tables_list(self):
    """Обновление списка таблиц в комбо-боксах"""
    try:
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT table_name FROM uploads ORDER BY upload_date DESC")
            tables = [row[0] for row in cursor.fetchall()]
            
        self.table_selector.clear()
        self.table_selector.addItems(tables)
        
        self.report_table_selector.clear()
        self.report_table_selector.addItems(tables)
        
    except Exception as e:
        print(f"Ошибка обновления списка таблиц: {str(e)}")

def load_table_data(self):
    """Загрузка данных выбранной таблицы"""
    table_name = self.table_selector.currentText()
    if not table_name:
        return
        
    try:
        self.current_table = table_name
        self.show_progress(True)
        QApplication.processEvents()  # Обновляем GUI
        
        with sqlite3.connect(self.db_name) as conn:
            # Получаем данные
            df = pd.read_sql(f"SELECT * FROM {table_name}", conn)
            
            # Получаем названия столбцов
            cursor = conn.cursor()
            cursor.execute(f"PRAGMA table_info({table_name})")
            columns = [col[1] for col in cursor.fetchall()]
            
        # Настраиваем таблицу
        self.data_table.setRowCount(df.shape[0])
        self.data_table.setColumnCount(df.shape[1])
        self.data_table.setHorizontalHeaderLabels(columns)
        
        # Оптимизация заполнения данных
        self.data_table.setUpdatesEnabled(False)  # Отключаем обновления для ускорения
        
        # Заполняем данными
        for row in range(df.shape[0]):
            for col in range(df.shape[1]):
                value = df.iat[row, col]
                
                # Обработка NaN значений
                if pd.isna(value):
                    item = QTableWidgetItem("")
                else:
                    item = QTableWidgetItem(str(value))
                
                # Сохраняем исходное значение для отслеживания изменений
                item.setData(Qt.UserRole, value)
                
                # Устанавливаем флаги для редактирования
                item.setFlags(item.flags() | Qt.ItemIsEditable)
                
                self.data_table.setItem(row, col, item)
        
        self.data_table.setUpdatesEnabled(True)  # Включаем обновления обратно
        
        # Автоматическая подгонка ширины столбцов после небольшой задержки
        QTimer.singleShot(100, self.auto_resize_columns)
        
        # Сбрасываем отслеживание изменений
        self.reset_changes_tracking()
        
        # Обновляем выпадающий список для поиска по столбцам
        self.search_column_combo.clear()
        self.search_column_combo.addItem("Все столбцы", -1)
        for i, col_name in enumerate(columns):
            self.search_column_combo.addItem(col_name, i)
        
        # Сбрасываем поиск
        self.search_input.clear()
        self.search_results = []
        self.current_search_index = -1
        self.btn_prev.setEnabled(False)
        self.btn_next.setEnabled(False)
        self.search_status.setText("")
        
    except Exception as e:
        QMessageBox.critical(self, "Ошибка", f"Ошибка загрузки данных: {str(e)}")
    finally:
        self.show_progress(False)

def auto_resize_columns(self):
    """Автоматическая подгонка ширины столбцов"""
    # Подгоняем ширину по содержимому
    self.data_table.resizeColumnsToContents()
    
    # Устанавливаем минимальную ширину для столбцов
    for col in range(self.data_table.columnCount()):
        current_width = self.data_table.columnWidth(col)
        header_width = self.data_table.horizontalHeader().sectionSizeHint(col)
        min_width = max(100, header_width, current_width)
        self.data_table.setColumnWidth(col, min_width)
    
    # Убедимся, что таблица занимает все доступное пространство
    self.data_table.horizontalHeader().setStretchLastSection(True)

def show_context_menu(self, position):
    """Показ контекстного меню для редактирования форматов и управления строками"""
    menu = QMenu()
    
    # Получаем выбранные элементы
    selected_items = self.data_table.selectedItems()
    selected_indexes = self.data_table.selectedIndexes()
    
    if not selected_items:
        return
    
    # Определяем, выделена ли строка, столбец или ячейки
    rows = set()
    columns = set()
    for index in selected_indexes:
        rows.add(index.row())
        columns.add(index.column())
    
    is_row_selected = len(rows) == 1
    is_column_selected = len(columns) == 1
    is_cell_selected = len(rows) == 1 and len(columns) == 1
    
    # Создаем действия
    font_action = menu.addAction("Изменить шрифт...")
    bg_color_action = menu.addAction("Изменить цвет фона...")
    text_color_action = menu.addAction("Изменить цвет текста...")
    menu.addSeparator()
    
    # Действия для управления строками
    add_row_action = menu.addAction("Добавить строку")
    delete_row_action = menu.addAction("Удалить строку")
    menu.addSeparator()
    
    if is_cell_selected:
        format_cell_action = menu.addAction("Формат ячейки...")
    if is_row_selected:
        format_row_action = menu.addAction("Формат строки...")
    if is_column_selected:
        format_col_action = menu.addAction("Формат столбца...")
    
    menu.addSeparator()
    clear_format_action = menu.addAction("Очистить форматирование")
    
    # Выполняем действие
    action = menu.exec_(self.data_table.viewport().mapToGlobal(position))
    
    if action == font_action:
        self.change_font(selected_indexes)
    elif action == bg_color_action:
        self.change_background_color(selected_indexes)
    elif action == text_color_action:
        self.change_text_color(selected_indexes)
    elif action == clear_format_action:
        self.clear_formatting(selected_indexes)
    elif action == add_row_action:
        self.add_row()
    elif action == delete_row_action:
        self.delete_selected_rows()
    elif action == format_cell_action and is_cell_selected:
        self.format_cell(selected_indexes[0])
    elif action == format_row_action and is_row_selected:
        self.format_row(list(rows)[0])
    elif action == format_col_action and is_column_selected:
        self.format_column(list(columns)[0])

def add_row(self):
    """Добавление новой строки в таблицу"""
    if not self.current_table:
        QMessageBox.warning(self, "Внимание", "Сначала загрузите таблицу")
        return
        
    # Добавляем строку в конец таблицы
    row_count = self.data_table.rowCount()
    self.data_table.insertRow(row_count)
    
    # Заполняем ячейки пустыми значениями
    for col in range(self.data_table.columnCount()):
        item = QTableWidgetItem("")
        item.setFlags(item.flags() | Qt.ItemIsEditable)
        self.data_table.setItem(row_count, col, item)
        
    # Помечаем таблицу как измененную
    self.has_unsaved_changes = True
    self.change_status.setText("Несохраненные изменения: добавлена новая строка")
    self.btn_save.setEnabled(True)
    self.btn_revert.setEnabled(True)

def delete_selected_rows(self):
    """Удаление выбранных строк из таблицы"""
    if not self.current_table:
        QMessageBox.warning(self, "Внимание", "Сначала загрузите таблицу")
        return
        
    selected_rows = set()
    for index in self.data_table.selectedIndexes():
        selected_rows.add(index.row())
        
    if not selected_rows:
        QMessageBox.warning(self, "Внимание", "Выберите строки для удаления")
        return
        
    # Преобразуем в список и сортируем в обратном порядке
    selected_rows = sorted(selected_rows, reverse=True)
    
    # Запрос подтверждения
    reply = QMessageBox.question(
        self, "Подтверждение удаления",
        f"Вы уверены, что хотите удалить {len(selected_rows)} строк?",
        QMessageBox.Yes | QMessageBox.No, QMessageBox.No
    )
    
    if reply == QMessageBox.No:
        return
        
    # Удаляем строки
    for row in selected_rows:
        self.data_table.removeRow(row)
        
    # Помечаем таблицу как измененную
    self.has_unsaved_changes = True
    self.change_status.setText(f"Несохраненные изменения: удалено {len(selected_rows)} строк")
    self.btn_save.setEnabled(True)
    self.btn_revert.setEnabled(True)

def change_font(self, indexes):
    """Изменение шрифта для выбранных ячеек"""
    # Получаем текущий шрифт
    current_font = self.data_table.item(indexes[0].row(), indexes[0].column()).font()
    
    # Запрашиваем новый шрифт
    font, ok = QFontDialog.getFont(current_font, self, "Выберите шрифт")
    if ok:
        for index in indexes:
            item = self.data_table.item(index.row(), index.column())
            if item:
                item.setFont(font)

def change_background_color(self, indexes):
    """Изменение цвета фона для выбранных ячеек"""
    color = QColorDialog.getColor(initial=Qt.white, parent=self, title="Выберите цвет фона")
    if color.isValid():
        for index in indexes:
            item = self.data_table.item(index.row(), index.column())
            if item:
                item.setBackground(color)

def change_text_color(self, indexes):
    """Изменение цвета текста для выбранных ячеек"""
    color = QColorDialog.getColor(initial=Qt.black, parent=self, title="Выберите цвет текста")
    if color.isValid():
        for index in indexes:
            item = self.data_table.item(index.row(), index.column())
            if item:
                item.setForeground(color)

def clear_formatting(self, indexes):
    """Очистка форматирования для выбранных ячеек"""
    for index in indexes:
        item = self.data_table.item(index.row(), index.column())
        if item:
            item.setBackground(QBrush(Qt.NoBrush))
            item.setForeground(QBrush(Qt.NoBrush))
            item.setFont(QFont())

def format_cell(self, index):
    """Расширенное форматирование одной ячейки"""
    row, col = index.row(), index.column()
    item = self.data_table.item(row, col)
    if not item:
        return
        
    # Диалог форматирования
    formats = ["Текст", "Число", "Процент", "Дата", "Денежный"]
    format_name, ok = QInputDialog.getItem(
        self, "Формат ячейки", "Выберите формат:", formats, 0, False
    )
    
    if ok:
        try:
            value = item.data(Qt.UserRole)
            
            if format_name == "Текст":
                item.setText(str(value))
            elif format_name == "Число":
                num_value = float(value) if value and not pd.isna(value) else 0
                item.setText(f"{num_value:,.2f}".rstrip('0').rstrip('.'))
            elif format_name == "Процент":
                num_value = float(value) * 100 if value and not pd.isna(value) else 0
                item.setText(f"{num_value:.2f}%")
            elif format_name == "Дата":
                if isinstance(value, (int, float)):
                    date_value = QDate.fromJulianDay(int(value))
                    item.setText(date_value.toString("dd.MM.yyyy"))
                else:
                    item.setText(str(value))
            elif format_name == "Денежный":
                num_value = float(value) if value and not pd.isna(value) else 0
                item.setText(f"${num_value:,.2f}")
        except:
            item.setText(str(value))

def format_row(self, row):
    """Форматирование всей строки"""
    # Запрашиваем цвет фона для строки
    color = QColorDialog.getColor(initial=Qt.white, parent=self, title="Выберите цвет фона для строки")
    if color.isValid():
        for col in range(self.data_table.columnCount()):
            item = self.data_table.item(row, col)
            if item:
                item.setBackground(color)

def format_current_column(self):
    """Форматирование текущего столбца"""
    selected_cols = set()
    for index in self.data_table.selectedIndexes():
        selected_cols.add(index.column())
    
    if len(selected_cols) != 1:
        QMessageBox.warning(self, "Внимание", "Выберите один столбец для форматирования")
        return
        
    self.format_column(list(selected_cols)[0])

def format_column_by_index(self, col_index):
    """Форматирование столбца по индексу (из заголовка)"""
    self.format_column(col_index)

def format_column(self, col):
    """Форматирование всего столбца с помощью диалога"""
    dialog = ColumnFormatDialog(self)
    if dialog.exec_() == QDialog.Accepted:
        format_settings = dialog.get_format_settings()
        self.apply_column_format(col, format_settings)

def apply_column_format(self, col, format_settings):
    """Применение формата ко всему столбцу"""
    for row in range(self.data_table.rowCount()):
        item = self.data_table.item(row, col)
        if item:
            value = item.data(Qt.UserRole)
            
            # Применяем формат данных
            try:
                if format_settings['data_format'] == "number":
                    num_value = float(value) if value and not pd.isna(value) else 0
                    item.setText(f"{num_value:,.2f}".rstrip('0').rstrip('.'))
                elif format_settings['data_format'] == "percent":
                    num_value = float(value) * 100 if value and not pd.isna(value) else 0
                    item.setText(f"{num_value:.2f}%")
                elif format_settings['data_format'] == "currency":
                    num_value = float(value) if value and not pd.isna(value) else 0
                    item.setText(f"${num_value:,.2f}")
                elif format_settings['data_format'] == "date":
                    if isinstance(value, (int, float)):
                        date_value = QDate.fromJulianDay(int(value))
                        item.setText(date_value.toString("dd.MM.yyyy"))
                    else:
                        item.setText(str(value))
                elif format_settings['data_format'] == "datetime":
                    if isinstance(value, (int, float)):
                        # Преобразование в дату и время
                        from datetime import datetime
                        dt = datetime.fromtimestamp(value)
                        item.setText(dt.strftime("%d.%m.%Y %H:%M"))
                    else:
                        item.setText(str(value))
                else:
                    item.setText(str(value))
            except:
                item.setText(str(value))
            
            # Применяем стили
            item.setFont(format_settings['font'])
            item.setForeground(format_settings['text_color'])
            item.setBackground(format_settings['bg_color'])
            item.setTextAlignment(format_settings['alignment'] | Qt.AlignVCenter)

def table_item_changed(self, item):
    """Обработка изменения значения в таблице"""
    if not self.current_table:
        return
        
    # Получаем координаты ячейки
    row = item.row()
    col = item.column()
    
    # Получаем новое и старое значение
    new_value = item.text()
    old_value = item.data(Qt.UserRole)
    
    # Игнорируем изменения, если значения совпадают
    if str(old_value) == new_value:
        return
        
    # Сохраняем изменение
    key = (row, col)
    self.changes[key] = {
        'old_value': old_value,
        'new_value': new_value,
        'item': item
    }
    
    # Помечаем ячейку как измененную
    item.setBackground(QColor(255, 255, 200))  # Светло-желтый фон
    
    # Обновляем статус
    self.has_unsaved_changes = True
    self.change_status.setText(f"Несохраненные изменения: {len(self.changes)}")
    self.btn_save.setEnabled(True)
    self.btn_revert.setEnabled(True)

def reset_changes_tracking(self):
    """Сброс отслеживания изменений"""
    self.changes = {}
    self.has_unsaved_changes = False
    self.change_status.setText("Нет изменений")
    self.btn_save.setEnabled(False)
    self.btn_revert.setEnabled(False)
    
    # Сбрасываем подсветку изменений
    for row in range(self.data_table.rowCount()):
        for col in range(self.data_table.columnCount()):
            item = self.data_table.item(row, col)
            if item:
                item.setBackground(QBrush(Qt.NoBrush))

def save_table_changes(self):
    """Сохранение изменений в базе данных"""
    if not self.current_table or not self.has_unsaved_changes:
        return
        
    try:
        self.show_progress(True)
        QApplication.processEvents()
        
        # Получаем имена столбцов
        column_names = []
        for col in range(self.data_table.columnCount()):
            column_names.append(self.data_table.horizontalHeaderItem(col).text())
        
        # Собираем все данные из таблицы
        data = []
        for row in range(self.data_table.rowCount()):
            row_data = []
            for col in range(self.data_table.columnCount()):
                item = self.data_table.item(row, col)
                row_data.append(item.text() if item else "")
            data.append(row_data)
        
        # Создаем DataFrame
        df = pd.DataFrame(data, columns=column_names)
        
        # Сохраняем в базу данных
        with sqlite3.connect(self.db_name) as conn:
            # Удаляем старую таблицу
            cursor = conn.cursor()
            cursor.execute(f"DROP TABLE IF EXISTS {self.current_table}")
            
            # Создаем новую таблицу с данными
            df.to_sql(self.current_table, conn, index=False)
            
            # Обновляем запись в истории загрузок
            cursor.execute("UPDATE uploads SET upload_date = ? WHERE table_name = ?", 
                          (datetime.now().strftime('%Y-%m-%d %H:%M:%S'), self.current_table))
            conn.commit()
        
        # Сбрасываем отслеживание изменений
        self.reset_changes_tracking()
        
        self.show_progress(False)
        QMessageBox.information(self, "Успех", "Изменения успешно сохранены в базе данных!")
        
    except Exception as e:
        self.show_progress(False)
        QMessageBox.critical(self, "Ошибка", f"Ошибка при сохранении изменений: {str(e)}")

def revert_table_changes(self):
    """Отмена изменений в таблице"""
    if not self.has_unsaved_changes:
        return
        
    reply = QMessageBox.question(
        self, "Подтверждение", 
        "Вы уверены, что хотите отменить все изменения?",
        QMessageBox.Yes | QMessageBox.No, QMessageBox.No
    )
    
    if reply == QMessageBox.Yes:
        # Перезагружаем таблицу
        self.load_table_data()

def search_table(self):
    """Поиск данных в таблице"""
    # Очищаем предыдущие результаты
    self.search_results = []
    self.current_search_index = -1
    
    # Получаем текст для поиска
    search_text = self.search_input.text().strip().lower()
    if not search_text:
        self.btn_prev.setEnabled(False)
        self.btn_next.setEnabled(False)
        self.search_status.setText("")
        # Снимаем выделение
        self.data_table.clearSelection()
        return
        
    # Получаем выбранный столбец для поиска
    col_index = self.search_column_combo.currentData()
    
    # Определяем диапазон столбцов для поиска
    if col_index == -1:  # Все столбцы
        cols = range(self.data_table.columnCount())
    else:  # Конкретный столбец
        cols = [col_index]
    
    # Проходим по всем строкам и выбранным столбцам
    for row in range(self.data_table.rowCount()):
        for col in cols:
            item = self.data_table.item(row, col)
            if item and search_text in item.text().lower():
                # Найдено совпадение, добавляем строку
                self.search_results.append(row)
                break  # Прерываем внутренний цикл, чтобы строка не добавлялась несколько раз
    
    if self.search_results:
        # Убираем дубликаты и сортируем
        self.search_results = sorted(set(self.search_results))
        self.current_search_index = 0
        self.highlight_search_result(self.current_search_index)
        self.btn_prev.setEnabled(True)
        self.btn_next.setEnabled(True)
        self.search_status.setText(f"Найдено: {len(self.search_results)} совпадений. Текущее: 1/{len(self.search_results)}")
    else:
        self.btn_prev.setEnabled(False)
        self.btn_next.setEnabled(False)
        self.search_status.setText("Совпадений не найдено")
        QMessageBox.information(self, "Поиск", "Совпадений не найдено")

def highlight_search_result(self, index):
    """Выделяет строку с найденным результатом"""
    if not self.search_results or index < 0 or index >= len(self.search_results):
        return
        
    row = self.search_results[index]
    # Выделяем всю строку
    self.data_table.selectRow(row)
    # Прокручиваем таблицу к найденной строке
    self.data_table.scrollToItem(self.data_table.item(row, 0))
    # Обновляем статус
    self.search_status.setText(f"Найдено: {len(self.search_results)} совпадений. Текущее: {index+1}/{len(self.search_results)}")

def next_search_result(self):
    """Переход к следующему результату поиска"""
    if not self.search_results:
        return
        
    self.current_search_index = (self.current_search_index + 1) % len(self.search_results)
    self.highlight_search_result(self.current_search_index)

def prev_search_result(self):
    """Переход к предыдущему результату поиска"""
    if not self.search_results:
        return
        
    self.current_search_index = (self.current_search_index - 1) % len(self.search_results)
    self.highlight_search_result(self.current_search_index)

def preview_report(self):
    """Предпросмотр отчета"""
    try:
        report_df = self.generate_report_df(preview=True)
        if report_df is None:
            return
            
        # Показываем диалог предпросмотра
        preview_dialog = ReportPreviewDialog(report_df, self)
        preview_dialog.exec_()
        
    except Exception as e:
        QMessageBox.critical(self, "Ошибка", f"Ошибка при создании предпросмотра: {str(e)}")

def generate_report_df(self, preview=False):
    """Генерация DataFrame для отчета"""
    table_name = self.report_table_selector.currentText()
    if not table_name:
        QMessageBox.warning(self, "Внимание", "Выберите таблицу для отчета")
        return None
        
    try:
        self.show_progress(True)
        QApplication.processEvents()  # Обновляем GUI
        
        with sqlite3.connect(self.db_name) as conn:
            df = pd.read_sql(f"SELECT * FROM {table_name}", conn)
        
        report_type = self.report_type_combo.currentText()
        report_df = df
        
        if report_type == "Сводная статистика":
            # Добавляем статистику для числовых столбцов
            if self.stats_checkbox.isChecked():
                num_cols = df.select_dtypes(include=np.number).columns
                if len(num_cols) > 0:
                    stats_df = df[num_cols].describe().transpose().reset_index()
                    stats_df.columns = ['Столбец', 'Количество', 'Среднее', 'Стд', 'Мин', '25%', '50%', '75%', 'Макс']
                    stats_df = stats_df.round(2)
                    
                    if preview:
                        report_df = stats_df
                    else:
                        # Для финального отчета сохраняем оба
                        report_df = pd.concat([df, pd.DataFrame([['-'*20]*len(df.columns)], columns=df.columns), 
                                            stats_df], ignore_index=True)
        
        elif report_type == "Топ-10 значений":
            column = self.top_column_combo.currentText()
            order = self.top_order_combo.currentText()
            
            if column:
                ascending = (order == "Наименьшие значения")
                report_df = df.sort_values(by=column, ascending=ascending).head(10)
        
        elif report_type == "Анализ по категориям":
            category_col = self.category_column_combo.currentText()
            value_col = self.value_column_combo.currentText()
            agg_func = self.agg_function_combo.currentText()
            
            if category_col and value_col:
                # Преобразуем названия функций
                agg_map = {
                    "Сумма": "sum",
                    "Среднее": "mean",
                    "Количество": "count",
                    "Максимум": "max",
                    "Минимум": "min"
                }
                
                func = agg_map.get(agg_func, "sum")
                
                report_df = df.groupby(category_col)[value_col].agg(func).reset_index()
                report_df.columns = [category_col, f"{agg_func} ({value_col})"]
        
        elif report_type == "Тренды по датам":
            date_col = self.date_column_combo.currentText()
            value_col = self.value_trend_combo.currentText()
            period = self.trend_period_combo.currentText()
            
            if date_col and value_col:
                # Преобразуем в дату, если возможно
                try:
                    df[date_col] = pd.to_datetime(df[date_col])
                    
                    # Группировка по периоду
                    period_map = {
                        "День": "D",
                        "Неделя": "W",
                        "Месяц": "M",
                        "Квартал": "Q",
                        "Год": "Y"
                    }
                    
                    freq = period_map.get(period, "M")
                    
                    # Группируем и агрегируем
                    report_df = df.set_index(date_col).resample(freq)[value_col].sum().reset_index()
                    report_df.columns = [f"Период ({period})", f"Сумма {value_col}"]
                
                except Exception as e:
                    QMessageBox.warning(self, "Ошибка", f"Не удалось преобразовать столбец '{date_col}' в дату: {str(e)}")
                    return None
        
        elif report_type == "Корреляционный анализ":
            # Получаем выбранные столбцы
            selected_cols = []
            for i in range(self.corr_columns_list.count()):
                item = self.corr_columns_list.item(i)
                if item.checkState() == Qt.Checked:
                    selected_cols.append(item.text())
            
            if len(selected_cols) < 2:
                QMessageBox.warning(self, "Внимание", "Выберите хотя бы 2 столбца для анализа корреляции")
                return None
            
            # Фильтруем только числовые столбцы
            num_df = df[selected_cols].select_dtypes(include=np.number)
            if len(num_df.columns) < 2:
                QMessageBox.warning(self, "Ошибка", "Выбранные столбцы не содержат достаточно числовых данных")
                return None
            
            # Рассчитываем корреляцию
            corr_matrix = num_df.corr().reset_index()
            corr_matrix.columns = ["Столбец"] + list(corr_matrix.columns[1:])
            report_df = corr_matrix
        
        elif report_type == "Фильтрованный отчет":
            column = self.filter_column_combo.currentText()
            operator = self.filter_operator_combo.currentText()
            value = self.filter_value_edit.text()
            
            if column and operator and value:
                try:
                    # Пытаемся преобразовать значение в число
                    try:
                        value_num = float(value)
                        is_numeric = True
                    except:
                        is_numeric = False
                    
                    # Применяем фильтр
                    if operator == "=":
                        if is_numeric:
                            report_df = df[df[column] == value_num]
                        else:
                            report_df = df[df[column].astype(str) == value]
                    elif operator == "!=":
                        if is_numeric:
                            report_df = df[df[column] != value_num]
                        else:
                            report_df = df[df[column].astype(str) != value]
                    elif operator == ">":
                        report_df = df[df[column] > float(value)]
                    elif operator == ">=":
                        report_df = df[df[column] >= float(value)]
                    elif operator == "<":
                        report_df = df[df[column] < float(value)]
                    elif operator == "<=":
                        report_df = df[df[column] <= float(value)]
                    elif operator == "содержит":
                        report_df = df[df[column].astype(str).str.contains(value, case=False)]
                    elif operator == "начинается с":
                        report_df = df[df[column].astype(str).str.startswith(value)]
                except Exception as e:
                    QMessageBox.warning(self, "Ошибка", f"Ошибка при фильтрации данных: {str(e)}")
                    return None
        
        elif report_type == "Сводная таблица":
            rows = self.pivot_rows_combo.currentText()
            columns = self.pivot_columns_combo.currentText()
            values = self.pivot_values_combo.currentText()
            agg_func = self.pivot_agg_combo.currentText()
            margins = self.pivot_margins.isChecked()
            
            if not values:
                QMessageBox.warning(self, "Внимание", "Выберите столбец значений для сводной таблицы")
                return None
                
            # Преобразуем названия функций
            agg_map = {
                "Сумма": "sum",
                "Среднее": "mean",
                "Количество": "count",
                "Максимум": "max",
                "Минимум": "min"
            }
            
            func = agg_map.get(agg_func, "sum")
            
            # Проверяем, что столбец значений числовой
            if values and not np.issubdtype(df[values].dtype, np.number):
                try:
                    df[values] = pd.to_numeric(df[values], errors='coerce')
                except:
                    QMessageBox.warning(self, "Ошибка", 
                        f"Столбец '{values}' не может быть преобразован в числовой формат")
                    return None
            
            # Создаем сводную таблицу
            try:
                pivot_df = df.pivot_table(
                    index=rows if rows else None,
                    columns=columns if columns else None,
                    values=values,
                    aggfunc=func,
                    margins=margins,
                    margins_name="Итог",
                    fill_value=0
                )
                
                # Для лучшего отображения в отчетах
                pivot_df = pivot_df.reset_index()
                
                report_df = pivot_df
            except Exception as e:
                QMessageBox.critical(self, "Ошибка", f"Ошибка создания сводной таблица: {str(e)}")
                return None
        
        return report_df
        
    except Exception as e:
        QMessageBox.critical(self, "Ошибка", f"Ошибка при подготовке отчета: {str(e)}")
        return None
    finally:
        if not preview:
            self.show_progress(False)

def generate_report(self):
    """Генерация и сохранение отчета"""
    report_df = self.generate_report_df()
    if report_df is None:
        return
        
    format_type = self.format_selector.currentText()
    try:
        # Выбор места сохранения
        options = QFileDialog.Options()
        report_type_name = self.report_type_combo.currentText().replace(" ", "_")
        default_name = f"report_{report_type_name}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
        
        if "Excel" in format_type:
            report_path, _ = QFileDialog.getSaveFileName(
                self, "Сохранить отчет Excel", default_name + ".xlsx", 
                "Excel Files (*.xlsx)", options=options
            )
            if report_path:
                if not report_path.endswith('.xlsx'):
                    report_path += '.xlsx'
                report_df.to_excel(report_path, index=False)
        elif "CSV" in format_type:
            report_path, _ = QFileDialog.getSaveFileName(
                self, "Сохранить отчет CSV", default_name + ".csv", 
                "CSV Files (*.csv)", options=options
            )
            if report_path:
                if not report_path.endswith('.csv'):
                    report_path += '.csv'
                report_df.to_csv(report_path, index=False)
        elif "HTML" in format_type:
            report_path, _ = QFileDialog.getSaveFileName(
                self, "Сохранить отчет HTML", default_name + ".html", 
                "HTML Files (*.html)", options=options
            )
            if report_path:
                if not report_path.endswith('.html'):
                    report_path += '.html'
                report_df.to_html(report_path, index=False)
        elif "PDF" in format_type:
            report_path, _ = QFileDialog.getSaveFileName(
                self, "Сохранить отчет PDF", default_name + ".pdf", 
                "PDF Files (*.pdf)", options=options
            )
            if report_path:
                if not report_path.endswith('.pdf'):
                    report_path += '.pdf'
                
                # Для PDF создаем графическое представление таблицы
                fig, ax = plt.subplots(figsize=(12, 8))
                ax.axis('tight')
                ax.axis('off')
                
                # Создаем таблицу
                table = ax.table(
                    cellText=report_df.values,
                    colLabels=report_df.columns,
                    cellLoc='center',
                    loc='center'
                )
                
                # Настраиваем стиль
                table.auto_set_font_size(False)
                table.set_fontsize(8)
                table.scale(1.2, 1.2)
                
                plt.savefig(report_path, bbox_inches='tight')
                plt.close()
            
        if report_path:
            self.report_info.setText(f"Отчет сохранен: {os.path.abspath(report_path)}")
            QMessageBox.information(self, "Успех", f"Отчет успешно сохранен:\n{os.path.abspath(report_path)}")
        else:
            self.report_info.setText("Сохранение отчета отменено")
        
    except Exception as e:
        error_msg = str(e)
        if "No engine for filetype" in error_msg or "openpyxl" in error_msg:
            error_msg += "\n\nУбедитесь, что установлены все зависимости:\npip install openpyxl"
        QMessageBox.critical(self, "Ошибка", error_msg)
    finally:
        self.show_progress(False)

def show_progress(self, visible):
    """Показать/скрыть прогресс бар"""
    self.progress.setVisible(visible)
    self.progress.setRange(0, 0 if visible else 1)  # Анимированный индикатор

if name == "main":
app = QApplication(sys.argv)
window = ExcelDatabaseApp()
sys.exit(app.exec_())`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment