From 2fff742651f059310bd4419715f3e1150880c25a Mon Sep 17 00:00:00 2001 From: helldh Date: Sun, 15 Feb 2026 19:09:27 +0300 Subject: [PATCH] Manager + Client fixes --- src/db.py | 255 +++++++++++++++++++++++++++-- src/windows.py | 423 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 641 insertions(+), 37 deletions(-) diff --git a/src/db.py b/src/db.py index 6fab82b..45488b4 100644 --- a/src/db.py +++ b/src/db.py @@ -118,31 +118,260 @@ def get_items(*, cursor): @do_request(autocommit=True) -def create_request(item_id: int, user: User, **kwargs): +def create_request(item_id: int, quantity: int, user: User, + date_from: str = None, date_to: str = None, + notes: str = None, *, cursor): """ - Создать заявку/заказ + Создать новую заявку/заказ - TODO: Адаптировать под конкретную предметную область Args: - item_id: ID элемента (товар, номер, услуга) - user: Пользователь создающий заявку - **kwargs: Дополнительные параметры (даты, количество, etc.) + item_id: ID товара/услуги + quantity: Количество + user: Объект пользователя + date_from: Дата начала (YYYY-MM-DD) или None + date_to: Дата окончания (YYYY-MM-DD) или None + notes: Дополнительные заметки + cursor: Курсор БД (автоматически через декоратор) + + Returns: + True если успешно, False если ошибка """ - # TODO: Реализовать вашу логику создания заявки - pass + try: + # 1. Проверяем существование товара и его доступность + cursor.execute(""" + SELECT id, name, price, quantity, status + FROM items + WHERE id = %s; + """, (item_id,)) + + item = cursor.fetchone() + + if not item: + print(f"Error: Item with id={item_id} not found") + return False + + item_db_id, item_name, item_price, item_quantity, item_status = item + + # 2. Проверяем статус товара + if item_status not in ['available', 'reserved']: + print(f"Error: Item '{item_name}' is not available (status: {item_status})") + return False + + # 3. Проверяем количество (если товар физический) + if item_quantity < quantity: + print(f"Error: Insufficient quantity. Available: {item_quantity}, requested: {quantity}") + return False + + # 4. Вычисляем total_price + total_price = float(item_price) * quantity + + # 5. Создаём заявку + cursor.execute(""" + INSERT INTO requests (user_id, item_id, quantity, date_from, date_to, + status, total_price, notes) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id; + """, (user.id, item_id, quantity, date_from, date_to, + 'pending', total_price, notes)) + + request_id = cursor.fetchone()[0] + + print(f"Success: Request #{request_id} created for user {user.name}") + return True + + except Exception as e: + print(f"Error creating request: {e}") + return False @do_request() def get_user_requests(user_id: int, *, cursor): """ - Получить заявки/заказы пользователя + Получить все заявки пользователя - TODO: Заменить на вашу логику + Args: + user_id: ID пользователя + + Returns: + List of tuples или None """ cursor.execute(""" - SELECT * - FROM requests - WHERE user_id = %s; + SELECT + r.id, + r.created_at, + i.name AS item_name, + r.quantity, + r.total_price, + r.date_from, + r.date_to, + r.status, + r.notes + FROM requests r + JOIN items i ON r.item_id = i.id + WHERE r.user_id = %s + ORDER BY r.created_at DESC; """, (user_id,)) + return cursor.fetchall() + + +@do_request(autocommit=True) +def update_request_status(request_id: int, new_status: str, *, cursor): + """ + Обновить статус заявки (для менеджера/администратора) + + Args: + request_id: ID заявки + new_status: Новый статус ('pending', 'approved', 'rejected', 'completed', 'cancelled') + + Returns: + True если успешно, False если ошибка + """ + valid_statuses = ['pending', 'approved', 'rejected', 'completed', 'cancelled'] + + if new_status not in valid_statuses: + print(f"Error: Invalid status '{new_status}'. Must be one of: {valid_statuses}") + return False + + try: + cursor.execute(""" + UPDATE requests + SET status = %s, updated_at = CURRENT_TIMESTAMP + WHERE id = %s; + """, (new_status, request_id)) + + if cursor.rowcount == 0: + print(f"Error: Request #{request_id} not found") + return False + + print(f"Success: Request #{request_id} status updated to '{new_status}'") + return True + + except Exception as e: + print(f"Error updating request status: {e}") + return False + + +@do_request() +def get_all_requests(status_filter: str = None, *, cursor): + """ + Получить все заявки (для администратора/менеджера) + + Args: + status_filter: Фильтр по статусу (опционально) + + Returns: + List of tuples + """ + if status_filter: + cursor.execute(""" + SELECT + r.id, + r.created_at, + u.name AS user_name, + u.email AS user_email, + i.name AS item_name, + r.quantity, + r.total_price, + r.date_from, + r.date_to, + r.status + FROM requests r + JOIN users u ON r.user_id = u.id + JOIN items i ON r.item_id = i.id + WHERE r.status = %s + ORDER BY r.created_at DESC; + """, (status_filter,)) + else: + cursor.execute(""" + SELECT + r.id, + r.created_at, + u.name AS user_name, + u.email AS user_email, + i.name AS item_name, + r.quantity, + r.total_price, + r.date_from, + r.date_to, + r.status + FROM requests r + JOIN users u ON r.user_id = u.id + JOIN items i ON r.item_id = i.id + ORDER BY r.created_at DESC; + """) + + return cursor.fetchall() + + +@do_request() +def search_items(search_term: str, *, cursor): + """ + Поиск товаров по названию или описанию + + Args: + search_term: Поисковый запрос + + Returns: + List of tuples + """ + cursor.execute(""" + SELECT + i.id, + i.name, + i.description, + i.price, + i.quantity, + i.status, + c.name AS category_name + FROM items i + LEFT JOIN categories c ON i.category_id = c.id + WHERE i.name ILIKE %s OR i.description ILIKE %s + ORDER BY i.name; + """, (f'%{search_term}%', f'%{search_term}%')) + + return cursor.fetchall() + + +@do_request() +def get_items_by_category(category_id: int, *, cursor): + """ + Получить товары по категории + + Args: + category_id: ID категории + + Returns: + List of tuples + """ + cursor.execute(""" + SELECT + i.id, + i.name, + i.description, + i.price, + i.quantity, + i.status + FROM items i + WHERE i.category_id = %s AND i.status = 'available' + ORDER BY i.name; + """, (category_id,)) + + return cursor.fetchall() + + +@do_request() +def get_all_categories(*, cursor): + """ + Получить все категории + + Returns: + List of tuples (id, name) + """ + cursor.execute(""" + SELECT id, name, description + FROM categories + ORDER BY name; + """) + return cursor.fetchall() \ No newline at end of file diff --git a/src/windows.py b/src/windows.py index a0bd9ac..7b52703 100644 --- a/src/windows.py +++ b/src/windows.py @@ -1,5 +1,5 @@ import csv -from .db import auth, get_items, get_user_requests +from .db import auth, get_items, create_request from .objects import User, SignalCode from .utils import TabWidgetCustom from PyQt6.QtWidgets import ( @@ -9,12 +9,14 @@ from PyQt6.QtWidgets import ( QGroupBox, QFormLayout, QVBoxLayout, + QHBoxLayout, QMessageBox, QTabWidget, QMainWindow, QComboBox, QDateEdit, - QTableView + QTableView, + QLabel ) from PyQt6.QtGui import QStandardItemModel, QStandardItem from PyQt6.QtSql import QSqlTableModel @@ -146,7 +148,7 @@ class LoginWindow(BaseWindow): def _apply_window_settings(self): self.setWindowTitle("Login") - self.setFixedSize(300, 200) + self.setFixedSize(260, 150) class AdminWindow(BaseWindow): @@ -210,7 +212,7 @@ class AdminWindow(BaseWindow): date_from = tab.from_date.date().toString("yyyy-MM-dd") date_to = tab.to_date.date().toString("yyyy-MM-dd") - # TODO: Изменить название поля (checkin/checkout → order_date/delivery_date) + # TODO: Изменить название поля (checkin/checkout -> order_date/delivery_date) if hasattr(tab, '_name'): # Пример фильтра tab.model.setFilter( @@ -324,17 +326,343 @@ class ManagerWindow(BaseWindow): """ Панель менеджера - TODO: Реализовать если требуется в задании - Обычно: просмотр данных + поиск/фильтрация (без удаления) + Функционал: + - Просмотр всех заявок + - Фильтрация заявок по статусу + - Одобрение/отклонение заявок + - Поиск товаров + - Просмотр товаров (без редактирования) """ def _define_widgets(self): - # TODO: Похож на AdminWindow, но с ограниченными правами - pass + self.root = QWidget() + self.tabs = QTabWidget() + + # Таб 1: Управление заявками + self._define_requests_tab() + + # Таб 2: Просмотр товаров + self._define_items_tab() + + def _define_requests_tab(self): + """Таб управления заявками""" + self.requests_widget = QWidget() + + # Фильтр по статусу + self.filter_layout = QHBoxLayout() + + self.filter_label = QLabel("Filter by status:") + self.status_filter = QComboBox() + self.status_filter.addItems([ + "All", + "Pending", + "Approved", + "Rejected", + "Completed", + "Cancelled" + ]) + + self.filter_button = QPushButton("Apply Filter") + self.refresh_button = QPushButton("Refresh") + + self.filter_layout.addWidget(self.filter_label) + self.filter_layout.addWidget(self.status_filter) + self.filter_layout.addWidget(self.filter_button) + self.filter_layout.addWidget(self.refresh_button) + self.filter_layout.addStretch() + + # Таблица заявок + self.requests_table = QTableView() + self.requests_model = QStandardItemModel() + self.requests_model.setHorizontalHeaderLabels([ + "ID", "Date", "User", "Email", "Item", + "Qty", "Price", "From", "To", "Status" + ]) + self.requests_table.setModel(self.requests_model) + + # Кнопки управления + self.requests_buttons = QHBoxLayout() + + self.approve_button = QPushButton("✓ Approve") + self.approve_button.setStyleSheet("background-color: #90EE90;") + + self.reject_button = QPushButton("✗ Reject") + self.reject_button.setStyleSheet("background-color: #FFB6C6;") + + self.complete_button = QPushButton("✓ Complete") + self.complete_button.setStyleSheet("background-color: #87CEEB;") + + self.view_details_button = QPushButton("View Details") + + self.requests_buttons.addWidget(self.approve_button) + self.requests_buttons.addWidget(self.reject_button) + self.requests_buttons.addWidget(self.complete_button) + self.requests_buttons.addWidget(self.view_details_button) + self.requests_buttons.addStretch() + + def _define_items_tab(self): + """Таб просмотра товаров""" + self.items_widget = QWidget() + + # Поиск + self.search_layout = QHBoxLayout() + + self.search_label = QLabel("Search:") + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Enter item name...") + self.search_button = QPushButton("Search") + self.show_all_button = QPushButton("Show All") + + self.search_layout.addWidget(self.search_label) + self.search_layout.addWidget(self.search_input) + self.search_layout.addWidget(self.search_button) + self.search_layout.addWidget(self.show_all_button) + self.search_layout.addStretch() + + # Таблица товаров + self.items_table = QTableView() + self.items_model = QSqlTableModel(db=self._db) + self.items_model.setTable("items") + self.items_model.setEditStrategy(QSqlTableModel.EditStrategy.OnManualSubmit) + self.items_model.select() + self.items_table.setModel(self.items_model) + + # Только для чтения + self.items_table.setEditTriggers(QTableView.EditTrigger.NoEditTriggers) + + def _tune_layouts(self): + """Компоновка""" + # Requests tab layout + requests_layout = QVBoxLayout() + requests_layout.addLayout(self.filter_layout) + requests_layout.addWidget(self.requests_table) + requests_layout.addLayout(self.requests_buttons) + self.requests_widget.setLayout(requests_layout) + + # Items tab layout + items_layout = QVBoxLayout() + items_layout.addLayout(self.search_layout) + items_layout.addWidget(self.items_table) + self.items_widget.setLayout(items_layout) + + # Add tabs + self.tabs.addTab(self.requests_widget, "Manage Requests") + self.tabs.addTab(self.items_widget, "View Items") + + # Root layout + root_layout = QVBoxLayout() + root_layout.addWidget(self.tabs) + self.root.setLayout(root_layout) + + self.setCentralWidget(self.root) + + def _connect_slots(self): + """Подключение сигналов""" + # Requests tab + self.filter_button.clicked.connect(self._on_filter_requests) + self.refresh_button.clicked.connect(self._on_refresh_requests) + self.approve_button.clicked.connect(self._on_approve_request) + self.reject_button.clicked.connect(self._on_reject_request) + self.complete_button.clicked.connect(self._on_complete_request) + self.view_details_button.clicked.connect(self._on_view_details) + + # Items tab + self.search_button.clicked.connect(self._on_search_items) + self.show_all_button.clicked.connect(self._on_show_all_items) + self.search_input.returnPressed.connect(self._on_search_items) + + def _load_requests(self, status_filter=None): + """Загрузить заявки из БД""" + from .db import get_all_requests + + # Конвертируем фильтр в формат БД + status_map = { + "All": None, + "Pending": "pending", + "Approved": "approved", + "Rejected": "rejected", + "Completed": "completed", + "Cancelled": "cancelled" + } + + db_status = status_map.get(status_filter, None) + requests = get_all_requests(status_filter=db_status) + + # Очистить модель + self.requests_model.removeRows(0, self.requests_model.rowCount()) + + if not requests: + return + + # Заполнить модель + for req in requests: + row = [QStandardItem(str(field) if field else "") for field in req] + self.requests_model.appendRow(row) + + # Автоширина колонок + self.requests_table.resizeColumnsToContents() + + def _on_filter_requests(self): + """Применить фильтр""" + status = self.status_filter.currentText() + self._load_requests(status_filter=status) + + def _on_refresh_requests(self): + """Обновить список заявок""" + self._load_requests() + + def _get_selected_request_id(self): + """Получить ID выбранной заявки""" + index = self.requests_table.currentIndex() + + if not index.isValid(): + QMessageBox.warning(self, "No Selection", + "Please select a request first") + return None + + # ID в первой колонке (индекс 0) + request_id = self.requests_model.item(index.row(), 0).text() + return int(request_id) if request_id else None + + def _on_approve_request(self): + """Одобрить заявку""" + from .db import update_request_status + + request_id = self._get_selected_request_id() + if not request_id: + return + + confirm = QMessageBox.question( + self, + "Confirm Approval", + f"Approve request #{request_id}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if confirm == QMessageBox.StandardButton.Yes: + success = update_request_status(request_id, 'approved') + + if success: + QMessageBox.information(self, "Success", + f"Request #{request_id} approved!") + self._on_refresh_requests() + else: + QMessageBox.critical(self, "Error", + "Failed to approve request") + + def _on_reject_request(self): + """Отклонить заявку""" + from .db import update_request_status + + request_id = self._get_selected_request_id() + if not request_id: + return + + confirm = QMessageBox.question( + self, + "Confirm Rejection", + f"Reject request #{request_id}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if confirm == QMessageBox.StandardButton.Yes: + success = update_request_status(request_id, 'rejected') + + if success: + QMessageBox.information(self, "Success", + f"Request #{request_id} rejected") + self._on_refresh_requests() + else: + QMessageBox.critical(self, "Error", + "Failed to reject request") + + def _on_complete_request(self): + """Завершить заявку""" + from .db import update_request_status + + request_id = self._get_selected_request_id() + if not request_id: + return + + confirm = QMessageBox.question( + self, + "Confirm Completion", + f"Mark request #{request_id} as completed?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if confirm == QMessageBox.StandardButton.Yes: + success = update_request_status(request_id, 'completed') + + if success: + QMessageBox.information(self, "Success", + f"Request #{request_id} completed!") + self._on_refresh_requests() + else: + QMessageBox.critical(self, "Error", + "Failed to complete request") + + def _on_view_details(self): + """Просмотр деталей заявки""" + index = self.requests_table.currentIndex() + + if not index.isValid(): + QMessageBox.warning(self, "No Selection", + "Please select a request first") + return + + # Собираем данные из строки + row = index.row() + details = [] + + for col in range(self.requests_model.columnCount()): + header = self.requests_model.horizontalHeaderItem(col).text() + value = self.requests_model.item(row, col).text() + details.append(f"{header}: {value}") + + # Показываем диалог с деталями + QMessageBox.information( + self, + "Request Details", + "\n".join(details) + ) + + def _on_search_items(self): + """Поиск товаров""" + from .db import search_items + + search_term = self.search_input.text().strip() + + if not search_term: + QMessageBox.warning(self, "Empty Search", + "Please enter a search term") + return + + results = search_items(search_term) + + if not results: + QMessageBox.information(self, "No Results", + f"No items found matching '{search_term}'") + return + + # Применяем фильтр к модели + filter_str = f"name ILIKE '%{search_term}%' OR description ILIKE '%{search_term}%'" + self.items_model.setFilter(filter_str) + self.items_model.select() + + def _on_show_all_items(self): + """Показать все товары""" + self.search_input.clear() + self.items_model.setFilter("") + self.items_model.select() def _apply_window_settings(self): + """Настройки окна""" self.setWindowTitle("Manager Panel") - self.setFixedSize(1000, 600) + self.setFixedSize(1100, 700) + + # Загрузить заявки при открытии + self._load_requests() class ClientWindow(BaseWindow): @@ -481,48 +809,95 @@ class ClientWindow(BaseWindow): def _on_submit_request(self): """ - Обработка создания заявки - - TODO: Реализовать логику создания заявки + Обработка создания заявки (исправленная версия) """ - item = self.item_combo.currentText() + from .db import create_request, get_items + + item_text = self.item_combo.currentText() date_from = self.date_from.date().toString("yyyy-MM-dd") date_to = self.date_to.date().toString("yyyy-MM-dd") - # Валидация - if item == "No items available": + # Валидация: проверка наличия товаров + if item_text == "No items available": QMessageBox.warning(self, "No Items", "There are no items available at this moment") return + # Валидация: даты if not date_from or not date_to: QMessageBox.critical(self, "Input Error", "Please select valid dates") return - # Проверка дат + # Валидация: даты не в прошлом if self.date_from.date() < QDate.currentDate() or \ self.date_to.date() < QDate.currentDate(): QMessageBox.warning(self, "Invalid Date", "Cannot select past dates") return - # TODO: Вызвать функцию создания заявки из db.py - # success = create_request(item_id, self._user, date_from=date_from, date_to=date_to) + # Валидация: date_to >= date_from + if self.date_to.date() < self.date_from.date(): + QMessageBox.warning(self, "Invalid Date Range", + "End date must be after start date") + return - # Заглушка - success = True + # Получить item_id из выбранного товара + items = get_items() + if not items: + QMessageBox.critical(self, "Error", "Failed to load items") + return + + # Найти item_id по названию + item_id = None + for item in items: + if str(item[1]) == item_text: # item[1] = name + item_id = item[0] # item[0] = id + break + + # КРИТИЧНО: Проверка ДО использования + if item_id is None: + QMessageBox.critical(self, "Error", + "Failed to identify selected item") + return # ← ВЫХОД, дальше item_id гарантированно НЕ None + + # Теперь item_id точно не None, безопасно использовать + quantity = 1 # TODO: Добавить поле для quantity в форме + + # Создать заявку + success = create_request( + item_id=item_id, # ← Теперь это безопасно + quantity=quantity, + user=self._user, + date_from=date_from, + date_to=date_to, + notes=None + ) if success: QMessageBox.information( self, "Success", - f"Request created successfully!\nItem: {item}\nPeriod: {date_from} - {date_to}" + f"Request created successfully!\n" + f"Item: {item_text}\n" + f"Period: {date_from} → {date_to}\n" + f"Status: Pending approval" ) - self.history_model.select() # Обновить историю + self.history_model.select() + + # Очистить форму + self.date_from.setDate(QDate.currentDate()) + self.date_to.setDate(QDate.currentDate().addDays(1)) else: - QMessageBox.critical(self, "Error", - "Failed to create request. Please try again later.") + QMessageBox.critical( + self, + "Error", + "Failed to create request.\n" + "Possible reasons:\n" + "- Item is unavailable\n" + "- Insufficient quantity\n" + "- Database error" + ) def _apply_window_settings(self): self.setWindowTitle("Client Panel")