Manager + Client fixes

This commit is contained in:
helldh 2026-02-15 19:09:27 +03:00
parent c0727d7b59
commit 2fff742651
2 changed files with 641 additions and 37 deletions

255
src/db.py
View file

@ -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()

View file

@ -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")