From f1f3f8ee2250dc2aad983a8e2531821d187a1456 Mon Sep 17 00:00:00 2001 From: Egor Deev <67710823+IGlek@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:26:58 +0300 Subject: [PATCH] v. 1.1 --- scripts/exe/mobile_devices.py | 938 ++++++++++++++++++++++++++++++++++ 1 file changed, 938 insertions(+) create mode 100644 scripts/exe/mobile_devices.py diff --git a/scripts/exe/mobile_devices.py b/scripts/exe/mobile_devices.py new file mode 100644 index 0000000..5c6b88a --- /dev/null +++ b/scripts/exe/mobile_devices.py @@ -0,0 +1,938 @@ +# main.py +import sys +import logging +from PyQt6.QtWidgets import QApplication +from PyQt6.QtCore import Qt + +# ui/main_window.py +import sys +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QTableWidget, QTableWidgetItem, QPushButton, QTabWidget, + QLabel, QLineEdit, QComboBox, QSpinBox, QMessageBox, + QDialog, QFormLayout, QDialogButtonBox, QHeaderView, + QToolBar, QStatusBar, QGroupBox, QTextEdit, QInputDialog +) +from PyQt6.QtCore import Qt, QTimer, pyqtSignal +from PyQt6.QtGui import QAction, QIcon, QFont +from typing import Optional, Dict, Any +import logging + +# db/database.py +import psycopg2 +from psycopg2.extras import RealDictCursor +from typing import List, Dict, Any, Optional +import logging +from contextlib import contextmanager + +logger = logging.getLogger(__name__) + +CURRENCY_MAP = { + 'Pakistan': ('PKR', '₨'), + 'India': ('INR', '₹'), + 'China': ('CNY', '¥'), + 'USA': ('USD', '$'), + 'Dubai': ('AED', 'د.إ') +} + +class Database: + """ + Класс для управления подключением к PostgreSQL и выполнения операций + Использует паттерн Singleton для единственного экземпляра подключения + """ + + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, host='localhost', port=5432, database='mobile_devices_db', + user='admin', password='password'): + if not hasattr(self, 'initialized'): + self.connection_params = { + 'host': host, + 'port': port, + 'database': database, + 'user': user, + 'password': password + } + self.connection = None + self.initialized = True + + def connect(self): + """Установка соединения с БД""" + try: + self.connection = psycopg2.connect(**self.connection_params) + logger.info("✅ Подключение к БД установлено") + return True + except Exception as e: + logger.error(f"❌ Ошибка подключения к БД: {e}") + return False + + def disconnect(self): + """Закрытие соединения с БД""" + if self.connection: + self.connection.close() + logger.info("🔒 Соединение с БД закрыто") + + @contextmanager + def get_cursor(self, dict_cursor=True): + """ + Контекстный менеджер для безопасной работы с курсором + """ + cursor_factory = RealDictCursor if dict_cursor else None + cursor = self.connection.cursor(cursor_factory=cursor_factory) + try: + yield cursor + self.connection.commit() + except Exception as e: + self.connection.rollback() + logger.error(f"❌ Ошибка выполнения запроса: {e}") + raise + finally: + cursor.close() + + # === CRUD операции для Companies === + + def get_all_companies(self) -> List[Dict[str, Any]]: + """Получение всех компаний""" + with self.get_cursor() as cursor: + cursor.execute(""" + SELECT c.company_id, c.company_name, COUNT(m.model_id) as models_count + FROM companies c + LEFT JOIN models m ON c.company_id = m.company_id + GROUP BY c.company_id, c.company_name + ORDER BY c.company_name + """) + return cursor.fetchall() + + def add_company(self, company_name: str) -> int: + """Добавление новой компании""" + with self.get_cursor() as cursor: + cursor.execute( + "INSERT INTO companies (company_name) VALUES (%s) RETURNING company_id", + (company_name,) + ) + return cursor.fetchone()['company_id'] + + def update_company(self, company_id: int, company_name: str): + """Обновление названия компании""" + with self.get_cursor() as cursor: + cursor.execute( + "UPDATE companies SET company_name = %s WHERE company_id = %s", + (company_name, company_id) + ) + + def delete_company(self, company_id: int): + """Удаление компании (каскадно удалит все модели)""" + with self.get_cursor() as cursor: + cursor.execute("DELETE FROM companies WHERE company_id = %s", (company_id,)) + + # === CRUD операции для Models === + + def get_all_models(self, company_id: Optional[int] = None) -> List[Dict[str, Any]]: + """Получение всех моделей (опционально по компании)""" + query = """ + SELECT + m.model_id, m.model_name, c.company_name, + m.mobile_weight, m.ram, m.front_camera, + m.back_camera, pr.processor_name, m.battery_capacity, + m.screen_size, m.launched_year, + COUNT(DISTINCT p.region_id) as price_regions + FROM models m + JOIN companies c ON m.company_id = c.company_id + LEFT JOIN processors pr ON m.processor_id = pr.processor_id + LEFT JOIN prices p ON m.model_id = p.model_id + """ + + params = [] + if company_id: + query += " WHERE m.company_id = %s" + params.append(company_id) + + query += " GROUP BY m.model_id, c.company_name, pr.processor_name ORDER BY c.company_name, m.model_name" + + with self.get_cursor() as cursor: + cursor.execute(query, params) + return cursor.fetchall() + + def get_model_by_id(self, model_id: int) -> Dict[str, Any]: + """Получение модели по ID""" + with self.get_cursor() as cursor: + cursor.execute(""" + SELECT m.*, c.company_name, pr.processor_name + FROM models m + JOIN companies c ON m.company_id = c.company_id + LEFT JOIN processors pr ON m.processor_id = pr.processor_id + WHERE m.model_id = %s + """, (model_id,)) + return cursor.fetchone() + + def add_model(self, model_data: Dict[str, Any]) -> int: + """Добавление новой модели""" + # Сначала получаем или создаем процессор + processor_id = None + if model_data.get('processor_name'): + processor_id = self.get_or_create_processor(model_data['processor_name']) + + with self.get_cursor() as cursor: + cursor.execute(""" + INSERT INTO models + (model_name, company_id, processor_id, mobile_weight, + ram, front_camera, back_camera, battery_capacity, + screen_size, launched_year) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING model_id + """, ( + model_data['model_name'], + model_data['company_id'], + processor_id, + model_data.get('mobile_weight'), + model_data.get('ram'), + model_data.get('front_camera'), + model_data.get('back_camera'), + model_data.get('battery_capacity'), + model_data.get('screen_size'), + model_data.get('launched_year') + )) + return cursor.fetchone()['model_id'] + + def update_model(self, model_id: int, model_data: Dict[str, Any]): + """Обновление модели""" + # Получаем или создаем процессор + processor_id = None + if model_data.get('processor_name'): + processor_id = self.get_or_create_processor(model_data['processor_name']) + + with self.get_cursor() as cursor: + cursor.execute(""" + UPDATE models SET + model_name = %s, + company_id = %s, + processor_id = %s, + mobile_weight = %s, + ram = %s, + front_camera = %s, + back_camera = %s, + battery_capacity = %s, + screen_size = %s, + launched_year = %s + WHERE model_id = %s + """, ( + model_data['model_name'], + model_data['company_id'], + processor_id, + model_data.get('mobile_weight'), + model_data.get('ram'), + model_data.get('front_camera'), + model_data.get('back_camera'), + model_data.get('battery_capacity'), + model_data.get('screen_size'), + model_data.get('launched_year'), + model_id + )) + + def delete_model(self, model_id: int): + """Удаление модели""" + with self.get_cursor() as cursor: + cursor.execute("DELETE FROM models WHERE model_id = %s", (model_id,)) + + # === CRUD операции для Prices === + + def get_model_prices(self, model_id: int) -> List[Dict[str, Any]]: + """Получение всех цен для модели""" + with self.get_cursor() as cursor: + cursor.execute(""" + SELECT p.price_id, p.model_id, p.region_id, + r.region_name, p.price, p.currency + FROM prices p + JOIN regions r ON p.region_id = r.region_id + WHERE p.model_id = %s + ORDER BY r.region_name + """, (model_id,)) + return cursor.fetchall() + + def add_or_update_price(self, model_id: int, region_id: int, price: float): + """Добавление или обновление цены""" + with self.get_cursor() as cursor: + cursor.execute(""" + INSERT INTO prices (model_id, region_id, price) + VALUES (%s, %s, %s) + ON CONFLICT (model_id, region_id) + DO UPDATE SET price = EXCLUDED.price + """, (model_id, region_id, price)) + + def delete_price(self, price_id: int): + """Удаление цены""" + with self.get_cursor() as cursor: + cursor.execute("DELETE FROM prices WHERE price_id = %s", (price_id,)) + + # === Вспомогательные методы === + + def get_or_create_processor(self, processor_name: str) -> int: + """Получение или создание процессора""" + with self.get_cursor() as cursor: + cursor.execute( + "SELECT processor_id FROM processors WHERE processor_name = %s", + (processor_name,) + ) + result = cursor.fetchone() + + if result: + return result['processor_id'] + else: + cursor.execute( + "INSERT INTO processors (processor_name) VALUES (%s) RETURNING processor_id", + (processor_name,) + ) + return cursor.fetchone()['processor_id'] + + def get_all_regions(self) -> List[Dict[str, Any]]: + """Получение всех регионов""" + with self.get_cursor() as cursor: + cursor.execute("SELECT * FROM regions ORDER BY region_name") + return cursor.fetchall() + + def get_all_processors(self) -> List[Dict[str, Any]]: + """Получение всех процессоров""" + with self.get_cursor() as cursor: + cursor.execute("SELECT * FROM processors ORDER BY processor_name") + return cursor.fetchall() + + # === Методы для аналитики === + + def get_price_statistics(self) -> List[Dict[str, Any]]: + """Получение статистики цен по регионам""" + with self.get_cursor() as cursor: + cursor.execute(""" + SELECT + r.region_name, + COUNT(p.price_id) as models_count, + AVG(p.price) as avg_price, + MIN(p.price) as min_price, + MAX(p.price) as max_price + FROM prices p + JOIN regions r ON p.region_id = r.region_id + GROUP BY r.region_name + ORDER BY avg_price DESC + """) + return cursor.fetchall() + + def search_models(self, search_text: str) -> List[Dict[str, Any]]: + """Поиск моделей по тексту""" + search_pattern = f"%{search_text}%" + with self.get_cursor() as cursor: + cursor.execute(""" + SELECT DISTINCT + m.model_id, m.model_name, c.company_name, + m.ram, m.battery_capacity, m.launched_year + FROM models m + JOIN companies c ON m.company_id = c.company_id + WHERE + m.model_name ILIKE %s OR + c.company_name ILIKE %s OR + m.ram ILIKE %s OR + m.battery_capacity ILIKE %s + ORDER BY c.company_name, m.model_name + LIMIT 100 + """, (search_pattern, search_pattern, search_pattern, search_pattern)) + return cursor.fetchall() + +class ModelDialog(QDialog): + """Диалог для добавления/редактирования модели""" + + def __init__(self, parent=None, model_data=None): + super().__init__(parent) + self.model_data = model_data + self.db = Database() + self.init_ui() + + def init_ui(self): + self.setWindowTitle("Добавить модель" if not self.model_data else "Редактировать модель") + self.setModal(True) + self.setMinimumWidth(500) + + layout = QFormLayout() + + # Поля формы + self.company_combo = QComboBox() + companies = self.db.get_all_companies() + for company in companies: + self.company_combo.addItem(company['company_name'], company['company_id']) + + self.model_name_edit = QLineEdit() + self.weight_edit = QLineEdit() + self.ram_edit = QLineEdit() + self.front_camera_edit = QLineEdit() + self.back_camera_edit = QLineEdit() + self.processor_edit = QLineEdit() + self.battery_edit = QLineEdit() + self.screen_edit = QLineEdit() + self.year_spin = QSpinBox() + self.year_spin.setRange(2000, 2030) + self.year_spin.setValue(2024) + + # Добавляем поля в форму + layout.addRow("Компания:", self.company_combo) + layout.addRow("Название модели:", self.model_name_edit) + layout.addRow("Вес:", self.weight_edit) + layout.addRow("RAM:", self.ram_edit) + layout.addRow("Фронтальная камера:", self.front_camera_edit) + layout.addRow("Основная камера:", self.back_camera_edit) + layout.addRow("Процессор:", self.processor_edit) + layout.addRow("Батарея:", self.battery_edit) + layout.addRow("Размер экрана:", self.screen_edit) + layout.addRow("Год выпуска:", self.year_spin) + + # Заполняем данные при редактировании + if self.model_data: + self.model_name_edit.setText(self.model_data.get('model_name', '')) + self.weight_edit.setText(self.model_data.get('mobile_weight', '')) + self.ram_edit.setText(self.model_data.get('ram', '')) + self.front_camera_edit.setText(self.model_data.get('front_camera', '')) + self.back_camera_edit.setText(self.model_data.get('back_camera', '')) + self.processor_edit.setText(self.model_data.get('processor_name', '')) + self.battery_edit.setText(self.model_data.get('battery_capacity', '')) + self.screen_edit.setText(self.model_data.get('screen_size', '')) + if self.model_data.get('launched_year'): + self.year_spin.setValue(self.model_data['launched_year']) + + # Устанавливаем компанию + for i in range(self.company_combo.count()): + if self.company_combo.itemData(i) == self.model_data.get('company_id'): + self.company_combo.setCurrentIndex(i) + break + + # Кнопки + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | + QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + layout.addRow(buttons) + self.setLayout(layout) + + def get_data(self) -> Dict[str, Any]: + """Получение данных из формы""" + return { + 'company_id': self.company_combo.currentData(), + 'model_name': self.model_name_edit.text(), + 'mobile_weight': self.weight_edit.text() or None, + 'ram': self.ram_edit.text() or None, + 'front_camera': self.front_camera_edit.text() or None, + 'back_camera': self.back_camera_edit.text() or None, + 'processor_name': self.processor_edit.text() or None, + 'battery_capacity': self.battery_edit.text() or None, + 'screen_size': self.screen_edit.text() or None, + 'launched_year': self.year_spin.value() + } + +class PriceDialog(QDialog): + """Диалог для управления ценами модели""" + + def __init__(self, parent=None, model_id=None, model_name=""): + super().__init__(parent) + self.model_id = model_id + self.model_name = model_name + self.db = Database() + self.init_ui() + self.load_prices() + + def init_ui(self): + self.setWindowTitle(f"Цены для: {self.model_name}") + self.setModal(True) + self.setMinimumSize(600, 400) + + layout = QVBoxLayout() + + # Таблица цен + self.prices_table = QTableWidget() + self.prices_table.setColumnCount(4) + self.prices_table.setHorizontalHeaderLabels(["Регион", "Цена", "Валюта", "Действия"]) + self.prices_table.setSortingEnabled(True) + + # Настройка размеров столбцов + header = self.prices_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) + + layout.addWidget(self.prices_table) + + # Форма добавления цены + add_group = QGroupBox("Добавить/Обновить цену") + add_layout = QHBoxLayout() + + self.region_combo = QComboBox() + regions = self.db.get_all_regions() + for region in regions: + self.region_combo.addItem(region['region_name'], region['region_id']) + + self.price_edit = QLineEdit() + self.price_edit.setPlaceholderText("Цена") + + add_btn = QPushButton("Добавить/Обновить") + add_btn.clicked.connect(self.add_update_price) + + add_layout.addWidget(QLabel("Регион:")) + add_layout.addWidget(self.region_combo) + add_layout.addWidget(QLabel("Цена:")) + add_layout.addWidget(self.price_edit) + add_layout.addWidget(add_btn) + + add_group.setLayout(add_layout) + layout.addWidget(add_group) + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(self.close) + layout.addWidget(close_btn) + + self.setLayout(layout) + + def format_price(self, price: float, region_name: str) -> str: + """Форматирование цены с правильным символом валюты""" + currency_code, currency_symbol = CURRENCY_MAP.get(region_name, ('USD', '$')) + return f"{currency_symbol}{price:,.2f}" + + def load_prices(self): + """Загрузка цен из БД""" + prices = self.db.get_model_prices(self.model_id) + self.prices_table.setRowCount(len(prices)) + + for row, price_data in enumerate(prices): + # Регион + region_item = QTableWidgetItem(price_data['region_name']) + region_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.prices_table.setItem(row, 0, region_item) + + # Цена с правильной валютой + price_str = self.format_price(price_data['price'], price_data['region_name']) + price_item = QTableWidgetItem(price_str) + price_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.prices_table.setItem(row, 1, price_item) + + # Код валюты + currency_code, _ = CURRENCY_MAP.get(price_data['region_name'], ('USD', '$')) + currency_item = QTableWidgetItem(currency_code) + currency_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.prices_table.setItem(row, 2, currency_item) + + # Кнопка удаления + delete_btn = QPushButton("Удалить") + delete_btn.clicked.connect(lambda checked, pid=price_data['price_id']: self.delete_price(pid)) + self.prices_table.setCellWidget(row, 3, delete_btn) + + def add_update_price(self): + """Добавление или обновление цены""" + try: + region_id = self.region_combo.currentData() + price = float(self.price_edit.text()) + + self.db.add_or_update_price(self.model_id, region_id, price) + self.load_prices() + self.price_edit.clear() + + QMessageBox.information(self, "Успех", "Цена успешно обновлена!") + except ValueError: + QMessageBox.warning(self, "Ошибка", "Введите корректную цену!") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка при обновлении цены: {str(e)}") + + def delete_price(self, price_id): + """Удаление цены""" + reply = QMessageBox.question( + self, "Подтверждение", + "Вы уверены, что хотите удалить эту цену?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + try: + self.db.delete_price(price_id) + self.load_prices() + QMessageBox.information(self, "Успех", "Цена удалена!") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка при удалении: {str(e)}") + +class MainWindow(QMainWindow): + """Главное окно приложения""" + + def __init__(self): + super().__init__() + self.db = Database() + self.init_ui() + self.connect_to_db() + + def init_ui(self): + self.setWindowTitle("Датасет мобильных устройств") + self.setGeometry(100, 100, 1200, 600) + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Основной layout + layout = QVBoxLayout(central_widget) + + # Создаем панель инструментов + self.create_toolbar() + + # Создаем вкладки + self.tabs = QTabWidget() + + # Вкладка компаний + self.companies_tab = self.create_companies_tab() + self.tabs.addTab(self.companies_tab, "🏢 Компании") + + # Вкладка моделей + self.models_tab = self.create_models_tab() + self.tabs.addTab(self.models_tab, "📱 Модели") + + # Вкладка аналитики + self.analytics_tab = self.create_analytics_tab() + self.tabs.addTab(self.analytics_tab, "📊 Аналитика") + + layout.addWidget(self.tabs) + + # Статусная строка + self.status_bar = QStatusBar() + self.setStatusBar(self.status_bar) + self.status_bar.showMessage("Готов к работе") + + def create_toolbar(self): + """Создание панели инструментов""" + toolbar = QToolBar() + self.addToolBar(toolbar) + + # Действия + refresh_action = QAction("🔄 Обновить", self) + refresh_action.triggered.connect(self.refresh_data) + toolbar.addAction(refresh_action) + + toolbar.addSeparator() + + add_company_action = QAction("➕ Добавить компанию", self) + add_company_action.triggered.connect(self.add_company) + toolbar.addAction(add_company_action) + + toolbar.addSeparator() + + add_model_action = QAction("➕ Добавить модель", self) + add_model_action.triggered.connect(self.add_model) + toolbar.addAction(add_model_action) + + def create_companies_tab(self) -> QWidget: + """Создание вкладки компаний""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # Панель управления + control_panel = QHBoxLayout() + control_panel.addStretch() + layout.addLayout(control_panel) + + # Таблица компаний + self.companies_table = QTableWidget() + self.companies_table.setColumnCount(3) + self.companies_table.setHorizontalHeaderLabels(["ID", "Название компании", "Кол-во моделей"]) + self.companies_table.setSortingEnabled(True) + + # Настройка размеров столбцов + header = self.companies_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + + layout.addWidget(self.companies_table) + + return widget + + def create_models_tab(self) -> QWidget: + """Создание вкладки моделей""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # Панель поиска + search_panel = QHBoxLayout() + + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("🔍 Поиск по названию, компании, RAM...") + self.search_edit.textChanged.connect(self.search_models) + search_panel.addWidget(self.search_edit) + + layout.addLayout(search_panel) + + # Таблица моделей + self.models_table = QTableWidget() + self.models_table.setColumnCount(11) + self.models_table.setHorizontalHeaderLabels([ + "ID", "Компания", "Модель", "RAM", "Батарея", + "Экран", "Год", "Регионов с ценами", "Действия", "", "" + ]) + self.models_table.setSortingEnabled(True) + + # Настройка размеров столбцов + header = self.models_table.horizontalHeader() + # ID - минимальный размер + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + # Компания и Модель - расширяемые + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + # Характеристики - по содержимому + for i in range(3, 8): + header.setSectionResizeMode(i, QHeaderView.ResizeMode.ResizeToContents) + # Кнопки действий - фиксированные + for i in range(8, 11): + header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed) + header.resizeSection(i, 120) + + layout.addWidget(self.models_table) + + return widget + + def create_analytics_tab(self) -> QWidget: + """Создание вкладки аналитики""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # Статистика по регионам + stats_group = QGroupBox("📊 Статистика цен по регионам") + stats_layout = QVBoxLayout() + + self.stats_text = QTextEdit() + self.stats_text.setReadOnly(True) + self.stats_text.setFont(QFont("Consolas", 10)) + stats_layout.addWidget(self.stats_text) + + refresh_stats_btn = QPushButton("🔄 Обновить статистику") + refresh_stats_btn.clicked.connect(self.update_statistics) + stats_layout.addWidget(refresh_stats_btn) + + stats_group.setLayout(stats_layout) + layout.addWidget(stats_group) + + return widget + + def connect_to_db(self): + """Подключение к БД""" + if self.db.connect(): + self.status_bar.showMessage("✅ Подключено к БД") + self.refresh_data() + else: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к БД!") + + def refresh_data(self): + """Обновление всех данных""" + self.load_companies() + self.load_models() + self.update_statistics() + self.status_bar.showMessage("✅ Данные обновлены") + + def load_companies(self): + """Загрузка списка компаний""" + companies = self.db.get_all_companies() + self.companies_table.setRowCount(len(companies)) + + for row, company in enumerate(companies): + # ID + id_item = QTableWidgetItem(str(company['company_id'])) + id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.companies_table.setItem(row, 0, id_item) + + # Название + name_item = QTableWidgetItem(company['company_name']) + name_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + self.companies_table.setItem(row, 1, name_item) + + # Количество моделей + count_item = QTableWidgetItem(str(company['models_count'])) + count_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.companies_table.setItem(row, 2, count_item) + + def load_models(self, search_text=""): + """Загрузка списка моделей""" + if search_text: + models = self.db.search_models(search_text) + else: + models = self.db.get_all_models() + + self.models_table.setRowCount(len(models)) + + for row, model in enumerate(models): + # ID + id_item = QTableWidgetItem(str(model['model_id'])) + id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.models_table.setItem(row, 0, id_item) + + # Компания + company_item = QTableWidgetItem(model['company_name']) + company_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + self.models_table.setItem(row, 1, company_item) + + # Модель + model_item = QTableWidgetItem(model['model_name']) + model_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + self.models_table.setItem(row, 2, model_item) + + # RAM + ram_item = QTableWidgetItem(model.get('ram', '')) + ram_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.models_table.setItem(row, 3, ram_item) + + # Батарея + battery_item = QTableWidgetItem(model.get('battery_capacity', '')) + battery_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.models_table.setItem(row, 4, battery_item) + + # Экран + screen_item = QTableWidgetItem(model.get('screen_size', '')) + screen_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.models_table.setItem(row, 5, screen_item) + + # Год + year_item = QTableWidgetItem(str(model.get('launched_year', ''))) + year_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.models_table.setItem(row, 6, year_item) + + # Регионов с ценами + regions_item = QTableWidgetItem(str(model.get('price_regions', 0))) + regions_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.models_table.setItem(row, 7, regions_item) + + # Кнопки действий + edit_btn = QPushButton("✏️ Редакт.") + edit_btn.clicked.connect(lambda checked, mid=model['model_id']: self.edit_model(mid)) + self.models_table.setCellWidget(row, 8, edit_btn) + + price_btn = QPushButton("💰 Цены") + price_btn.clicked.connect( + lambda checked, mid=model['model_id'], name=model['model_name']: + self.manage_prices(mid, name) + ) + self.models_table.setCellWidget(row, 9, price_btn) + + delete_btn = QPushButton("🗑️ Удалить") + delete_btn.clicked.connect(lambda checked, mid=model['model_id']: self.delete_model(mid)) + self.models_table.setCellWidget(row, 10, delete_btn) + + def add_company(self): + """Добавление новой компании""" + name, ok = QInputDialog.getText(self, "Новая компания", "Введите название компании:") + if ok and name: + try: + self.db.add_company(name) + self.refresh_data() + QMessageBox.information(self, "Успех", f"Компания '{name}' добавлена!") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка при добавлении: {str(e)}") + + def add_model(self): + """Добавление новой модели""" + dialog = ModelDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + try: + model_data = dialog.get_data() + self.db.add_model(model_data) + self.refresh_data() + QMessageBox.information(self, "Успех", "Модель добавлена!") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка при добавлении: {str(e)}") + + def edit_model(self, model_id): + """Редактирование модели""" + model_data = self.db.get_model_by_id(model_id) + dialog = ModelDialog(self, model_data) + + if dialog.exec() == QDialog.DialogCode.Accepted: + try: + updated_data = dialog.get_data() + self.db.update_model(model_id, updated_data) + self.refresh_data() + QMessageBox.information(self, "Успех", "Модель обновлена!") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка при обновлении: {str(e)}") + + def delete_model(self, model_id): + """Удаление модели""" + reply = QMessageBox.question( + self, "Подтверждение", + "Вы уверены, что хотите удалить эту модель?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + try: + self.db.delete_model(model_id) + self.refresh_data() + QMessageBox.information(self, "Успех", "Модель удалена!") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка при удалении: {str(e)}") + + def manage_prices(self, model_id, model_name): + """Управление ценами модели""" + dialog = PriceDialog(self, model_id, model_name) + dialog.exec() + self.refresh_data() + + def search_models(self, text): + """Поиск моделей""" + self.load_models(text) + + def update_statistics(self): + """Обновление статистики""" + try: + stats = self.db.get_price_statistics() + + stats_text = "📊 СТАТИСТИКА ЦЕН ПО РЕГИОНАМ\n" + "="*60 + "\n\n" + + for stat in stats: + region_name = stat['region_name'] + currency_code, currency_symbol = CURRENCY_MAP.get(region_name, ('USD', '$')) + + stats_text += f"🌍 {region_name} ({currency_code}):\n" + stats_text += f" • Моделей с ценами: {stat['models_count']}\n" + stats_text += f" • Средняя цена: {currency_symbol}{stat['avg_price']:,.2f}\n" + stats_text += f" • Минимальная цена: {currency_symbol}{stat['min_price']:,.2f}\n" + stats_text += f" • Максимальная цена: {currency_symbol}{stat['max_price']:,.2f}\n\n" + + self.stats_text.setText(stats_text) + except Exception as e: + self.stats_text.setText(f"Ошибка при загрузке статистики: {str(e)}") + + def closeEvent(self, event): + """Обработка закрытия окна""" + self.db.disconnect() + event.accept() + + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +def main(): + app = QApplication(sys.argv) + + # Настройки приложения + app.setApplicationName("Mobile Devices Manager") + app.setOrganizationName("Moscow Polytech") + + # Устанавливаем стиль + app.setStyle('Fusion') + + # Создаем и показываем главное окно + window = MainWindow() + window.show() + + # Запускаем цикл обработки событий + sys.exit(app.exec()) + +if __name__ == "__main__": + main() \ No newline at end of file