template creation

This commit is contained in:
helldh 2026-02-15 15:52:10 +03:00
parent 3c2137da2b
commit c0727d7b59
6 changed files with 464 additions and 293 deletions

View file

@ -1,12 +1,15 @@
from src.windows import LoginWindow, AdminWindow, ClientWindow from src.windows import LoginWindow, AdminWindow, ClientWindow, ManagerWindow
from src.objects import User, Rights from src.objects import User, Rights
from src.db import DB_AUTH_HARDCODED as config from src.db import DB_CONFIG
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal
from PyQt6.QtSql import QSqlDatabase from PyQt6.QtSql import QSqlDatabase
class Composer(QObject): class Composer(QObject):
"""Управляет навигацией между окнами приложения"""
render_request = pyqtSignal(User) render_request = pyqtSignal(User)
def __init__(self): def __init__(self):
@ -17,56 +20,64 @@ class Composer(QObject):
self.render_request.connect(self._render) self.render_request.connect(self._render)
def _init_db(self): def _init_db(self):
"""Инициализация подключения к базе данных"""
self._db = QSqlDatabase("QPSQL") self._db = QSqlDatabase("QPSQL")
self._db.setDatabaseName(config['dbname']) self._db.setDatabaseName(DB_CONFIG['dbname'])
self._db.setPort(config['port']) self._db.setPort(DB_CONFIG['port'])
self._db.setHostName(config['host']) self._db.setHostName(DB_CONFIG['host'])
self._db.setUserName(config['user']) self._db.setUserName(DB_CONFIG['user'])
self._db.setPassword(config['password']) self._db.setPassword(DB_CONFIG['password'])
self._db.open()
if not self._db.open():
raise Exception(f"Не получилось подключиться к базе данных: {self._db.lastError().text()}")
@pyqtSlot(User) @pyqtSlot(User)
def _render(self, user: User): def _render(self, user: User):
"""Маршрутизация пользователя на основе его роли"""
match user.rights: match user.rights:
case Rights.ADMIN: case Rights.ADMIN:
self._admin_fabric() self._admin_fabric()
case Rights.MANAGER: case Rights.MANAGER:
pass self._manager_fabric()
case Rights.CLIENT: case Rights.CLIENT:
self._client_fabric(user) self._client_fabric(user)
case Rights.GUEST:
self._guest_fabric()
def _login_fabric(self): def _login_fabric(self):
"""Создание и отображение окна входа"""
self.wlogin = LoginWindow(self, self._db) self.wlogin = LoginWindow(self, self._db)
self._switch_window(self.wlogin)
if self._current:
self._current.close()
self.wlogin.show()
self._current = self.wlogin
def _admin_fabric(self): def _admin_fabric(self):
"""Создание и отображение панели администратора"""
self.wadmin = AdminWindow(self, self._db) self.wadmin = AdminWindow(self, self._db)
self._switch_window(self.wadmin)
if self._current: def _manager_fabric(self):
self._current.close() """Создание и отображение панели менеджера"""
self.wmanager = ManagerWindow(self, self._db)
self.wadmin.show() self._switch_window(self.wmanager)
self._current = self.wadmin
def _client_fabric(self, user: User): def _client_fabric(self, user: User):
"""Создание и отображение панели клиента"""
self.wclient = ClientWindow(self, self._db, user) self.wclient = ClientWindow(self, self._db, user)
self._switch_window(self.wclient)
def _guest_fabric(self):
"""Создание и отображение панели гостя"""
# TODO: Реализовать GuestWindow если нужен режим гостя
pass
def _switch_window(self, new_window):
"""Переключение между окнами"""
if self._current: if self._current:
self._current.close() self._current.close()
new_window.show()
self.wclient.show() self._current = new_window
self._current = self.wclient
def run(self): def run(self):
"""Запуск приложения"""
import sys import sys
self._login_fabric() self._login_fabric()
self._current.show()
sys.exit(self._app.exec()) sys.exit(self._app.exec())

View file

@ -1,5 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Этот скрипт я написал для быстрой сборки кода в 1 файл.
# Это очень удобно что бы отправлять код на анализ ИИ
# Работает только в оболочке bash для *nix систем
OUTPUT="all_code.txt" OUTPUT="all_code.txt"
ROOT="." ROOT="."

173
src/db.py
View file

@ -1,19 +1,27 @@
import psycopg2 as pg import psycopg2 as pg
from .objects import User, Rights from .objects import User, Rights
DB_AUTH_HARDCODED = { DB_CONFIG = {
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 5432, "port": 5432,
"dbname": "examdb", "dbname": "example", # TODO: Заменить на название из задания
"user": "postgres", "user": "example_user",
"password": "213k2010###" "password": "example_password123" # TODO: Заменить на реальный пароль
} }
def get_connection(): def get_connection():
return pg.connect(**DB_AUTH_HARDCODED) """Получить подключение к базе данных"""
return pg.connect(**DB_CONFIG)
def do_request(autocommit=False): def do_request(autocommit=False):
"""
Декоратор для выполнения SQL запросов с автоматическим управлением соединением
Args:
autocommit: Если True, автоматически фиксирует транзакцию
"""
def upper_wrapper(func): def upper_wrapper(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
conn = get_connection() conn = get_connection()
@ -21,7 +29,6 @@ def do_request(autocommit=False):
try: try:
kwargs['cursor'] = cursor kwargs['cursor'] = cursor
result = func(*args, **kwargs) result = func(*args, **kwargs)
if autocommit: if autocommit:
@ -30,43 +37,57 @@ def do_request(autocommit=False):
return result return result
except Exception as e: except Exception as e:
print(f"Error: Can't request query to DB: {e}") print(f"Error: Database request failed: {e}")
conn.rollback()
return None return None
finally: finally:
cursor.close() cursor.close()
conn.close() conn.close()
return wrapper return wrapper
return upper_wrapper return upper_wrapper
@do_request() @do_request()
def auth(login: str, password: str, *, cursor) -> User | None: def auth(login: str, password: str, *, cursor) -> User | None:
"""
Аутентификация пользователя
Args:
login: Имя пользователя
password: Пароль
Returns:
User объект или None если аутентификация не удалась
"""
cursor.execute(""" cursor.execute("""
SELECT id, name, rights SELECT id, name, rights
FROM users FROM users
WHERE name = %s WHERE name = %s AND password = %s;
AND password = %s;
""", (login, password)) """, (login, password))
user = cursor.fetchone() user = cursor.fetchone()
if not user: if not user:
print("Warning: Login Forbidden: Can't find such user!") print("Warning: Authentication failed - user not found")
return None return None
rights = None # Маппинг строки роли на Enum
rights_mapping = {
"admin": Rights.ADMIN,
"manager": Rights.MANAGER,
"client": Rights.CLIENT,
"customer": Rights.CLIENT, # Альтернативное название
"guest": Rights.GUEST,
}
match user[2]: rights = rights_mapping.get(user[2])
case "admin":
rights = Rights.ADMIN if not rights:
case "customer": print(f"Warning: Unknown user role: {user[2]}")
rights = Rights.CLIENT return None
case "manager":
rights = Rights.MANAGER
case _:
return None
return User( return User(
id=user[0], id=user[0],
@ -74,80 +95,54 @@ def auth(login: str, password: str, *, cursor) -> User | None:
rights=rights rights=rights
) )
# TODO: Ниже функции для работы с предметной областью
# Заменить на ваши функции (например: get_products, add_product, etc.)
@do_request() @do_request()
def get_free_numbers(*, cursor): def get_items(*, cursor):
"""
Получить список доступных элементов
Например: get_products(), get_available_rooms(), etc.
"""
# Пример запроса - заменить на реальный
cursor.execute(""" cursor.execute("""
SELECT * SELECT *
FROM rooms FROM items
WHERE status = 'free'; WHERE status = 'available';
""") """)
free = cursor.fetchall() items = cursor.fetchall()
return items if items else None
if not free:
return None
return free
@do_request(autocommit=True) @do_request(autocommit=True)
def update_number_status(number: str, checkin: str, def create_request(item_id: int, user: User, **kwargs):
checkout: str, user: User, """
*, cursor): Создать заявку/заказ
TODO: Адаптировать под конкретную предметную область
Args:
item_id: ID элемента (товар, номер, услуга)
user: Пользователь создающий заявку
**kwargs: Дополнительные параметры (даты, количество, etc.)
"""
# TODO: Реализовать вашу логику создания заявки
pass
@do_request()
def get_user_requests(user_id: int, *, cursor):
"""
Получить заявки/заказы пользователя
TODO: Заменить на вашу логику
"""
cursor.execute(""" cursor.execute("""
SELECT password SELECT *
FROM users FROM requests
WHERE id = %s WHERE user_id = %s;
""", (user.id,)) """, (user_id,))
password = cursor.fetchone() return cursor.fetchall()
if not password:
return False
cursor.execute("""
SELECT id
FROM guests
WHERE name = %s
AND PHONE = %s
""", (user.name, password[0]))
guest = cursor.fetchone()
if not guest:
return False
cursor.execute("""
SELECT id
FROM rooms
WHERE number = %s;
""", (number,))
number_id = cursor.fetchone()
if not number_id:
return False
cursor.execute("""
SELECT guest, room
FROM bookings
WHERE guest = %s
AND room = %s;
""", (guest[0], number_id[0]))
request_exists = cursor.fetchone()
if request_exists:
return False
cursor.execute("""
INSERT INTO bookings(guest, room, checkin, checkout, status)
VALUES (%s, %s, %s, %s, 'active');
""", (guest[0], number_id[0], checkin, checkout))
cursor.execute("""
UPDATE rooms
SET status = 'booked'
WHERE number = %s;
""", (number,))
return True

View file

@ -2,16 +2,20 @@ from enum import Enum, auto
from dataclasses import dataclass from dataclasses import dataclass
class SignalCode(Enum): class SignalCode(Enum):
SIGFALSE = auto() """Коды сигналов для обработки ошибок"""
SIGERR = auto() SIGFALSE = auto() # Неверные данные
SIGERR = auto() # Ошибка валидации
class Rights(Enum): class Rights(Enum):
ADMIN = auto() """Роли пользователей в системе"""
CLIENT = auto() ADMIN = auto() # Полный доступ
MANAGER = auto() MANAGER = auto() # Просмотр + частичное редактирование
CLIENT = auto() # Ограниченный доступ
GUEST = auto() # Только просмотр
@dataclass @dataclass
class User: class User:
"""Модель пользователя"""
id: int id: int
name: str name: str
rights: Rights rights: Rights

View file

@ -9,17 +9,38 @@ from PyQt6.QtWidgets import (
) )
from PyQt6.QtSql import QSqlTableModel from PyQt6.QtSql import QSqlTableModel
class TabWidgetCustom(QWidget): class TabWidgetCustom(QWidget):
def __init__(self, name: str, db): """
Универсальный виджет для CRUD операций с таблицей БД
Функционал:
- Отображение данных в QTableView
- Добавление/удаление строк
- Применение/отмена изменений
- Экспорт в CSV
- Опциональная фильтрация по датам
"""
def __init__(self, table_name: str, db, show_date_filter=False):
"""
Args:
table_name: Название таблицы в БД
db: QSqlDatabase объект
show_date_filter: Показывать ли фильтр по датам
"""
super().__init__() super().__init__()
self._name = name self._name = table_name
self._db = db self._db = db
self._show_date_filter = show_date_filter
self._setup() self._setup()
def _setup(self): def _setup(self):
"""Настройка UI виджета"""
self.root = QVBoxLayout(self) self.root = QVBoxLayout(self)
if self._name in ("bookings", "payments"): # Опциональный фильтр по датам
if self._show_date_filter:
self.header = QHBoxLayout() self.header = QHBoxLayout()
self.from_date = QDateEdit() self.from_date = QDateEdit()
@ -28,27 +49,29 @@ class TabWidgetCustom(QWidget):
self.to_date = QDateEdit() self.to_date = QDateEdit()
self.to_date.setCalendarPopup(True) self.to_date.setCalendarPopup(True)
self.button_filter = QPushButton("Фильтровать") self.button_filter = QPushButton("Filter")
self.button_all = QPushButton("Показать всё") self.button_all = QPushButton("Show All")
self.header.addWidget(QLabel("С:")) self.header.addWidget(QLabel("From:"))
self.header.addWidget(self.from_date) self.header.addWidget(self.from_date)
self.header.addWidget(QLabel("По:")) self.header.addWidget(QLabel("To:"))
self.header.addWidget(self.to_date) self.header.addWidget(self.to_date)
self.header.addWidget(self.button_filter) self.header.addWidget(self.button_filter)
self.header.addWidget(self.button_all) self.header.addWidget(self.button_all)
self.root.addLayout(self.header) self.root.addLayout(self.header)
# Таблица
self.view = QTableView() self.view = QTableView()
# Панель кнопок
self.btoolbar = QHBoxLayout() self.btoolbar = QHBoxLayout()
self.button_add = QPushButton("+ Добавить") self.button_add = QPushButton("+ Add")
self.button_del = QPushButton("- Удалить") self.button_del = QPushButton("- Delete")
self.button_ok = QPushButton("\\_/ Применить") self.button_ok = QPushButton("✓ Apply")
self.button_deny = QPushButton("-- Отменить") self.button_deny = QPushButton("✗ Cancel")
self.button_csv = QPushButton("Выгрузить отчёт в CSV") self.button_csv = QPushButton("Export CSV")
self.btoolbar.addWidget(self.button_add) self.btoolbar.addWidget(self.button_add)
self.btoolbar.addWidget(self.button_del) self.btoolbar.addWidget(self.button_del)
@ -62,6 +85,7 @@ class TabWidgetCustom(QWidget):
self._setup_db() self._setup_db()
def _setup_db(self): def _setup_db(self):
"""Настройка модели БД"""
self.model = QSqlTableModel(db=self._db) self.model = QSqlTableModel(db=self._db)
self.model.setTable(self._name) self.model.setTable(self._name)
self.model.setEditStrategy(QSqlTableModel.EditStrategy.OnManualSubmit) self.model.setEditStrategy(QSqlTableModel.EditStrategy.OnManualSubmit)

View file

@ -1,5 +1,5 @@
import csv import csv
from .db import auth, get_free_numbers, update_number_status from .db import auth, get_items, get_user_requests
from .objects import User, SignalCode from .objects import User, SignalCode
from .utils import TabWidgetCustom from .utils import TabWidgetCustom
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
@ -20,10 +20,20 @@ from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtSql import QSqlTableModel from PyQt6.QtSql import QSqlTableModel
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot, QDate from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot, QDate
FIW = 160 # Fixed Input Width FIW = 160 # Fixed Input Width
NO_NUMBERS_CONST = "Нет свободных номеров"
class BaseWindow(QMainWindow): class BaseWindow(QMainWindow):
"""
Базовый класс для всех окон приложения
Определяет общую структуру окна:
1. _define_widgets() - создание виджетов
2. _tune_layouts() - настройка layout'ов
3. _connect_slots() - подключение сигналов
4. _apply_window_settings() - настройка окна (заголовок, размер)
"""
def __init__(self, composer, db, user=None): def __init__(self, composer, db, user=None):
super().__init__() super().__init__()
self._composer = composer self._composer = composer
@ -35,59 +45,71 @@ class BaseWindow(QMainWindow):
self._apply_window_settings() self._apply_window_settings()
def _define_widgets(self): def _define_widgets(self):
"""Определение виджетов (переопределить в наследниках)"""
pass pass
def _tune_layouts(self): def _tune_layouts(self):
"""Настройка компоновки (переопределить в наследниках)"""
pass pass
def _connect_slots(self): def _connect_slots(self):
"""Подключение сигналов к слотам (переопределить в наследниках)"""
pass pass
def _apply_window_settings(self): def _apply_window_settings(self):
"""Настройка параметров окна (переопределить в наследниках)"""
pass pass
class LoginWindow(BaseWindow): class LoginWindow(BaseWindow):
"""Окно авторизации"""
login_success = pyqtSignal(User) login_success = pyqtSignal(User)
login_forbidden = pyqtSignal(SignalCode) login_forbidden = pyqtSignal(SignalCode)
def _define_widgets(self): def _define_widgets(self):
self.root = QWidget() self.root = QWidget()
self.auth_form = QGroupBox("Логин") self.auth_form = QGroupBox("Authorization")
self.auth_form.setAlignment(Qt.AlignmentFlag.AlignHCenter | self.auth_form.setAlignment(Qt.AlignmentFlag.AlignHCenter |
Qt.AlignmentFlag.AlignVCenter) Qt.AlignmentFlag.AlignVCenter)
self.login_line = QLineEdit() self.login_line = QLineEdit()
self.login_line.setFixedWidth(FIW) self.login_line.setFixedWidth(FIW)
self.login_line.setPlaceholderText("Username")
self.password_line = QLineEdit() self.password_line = QLineEdit()
self.password_line.setFixedWidth(FIW) self.password_line.setFixedWidth(FIW)
self.password_line.setEchoMode(QLineEdit.EchoMode.Password) self.password_line.setEchoMode(QLineEdit.EchoMode.Password)
self.password_line.setPlaceholderText("Password")
self.auth_button = QPushButton("Авторизоваться") self.auth_button = QPushButton("Login")
self.guest_button = QPushButton("Continue as Guest") # Опционально
def _tune_layouts(self): def _tune_layouts(self):
self.root_l = QVBoxLayout() self.root_l = QVBoxLayout()
self.form = QFormLayout() self.form = QFormLayout()
self.form.addRow("Логин:", self.login_line) self.form.addRow("Login:", self.login_line)
self.form.addRow("Пароль:", self.password_line) self.form.addRow("Password:", self.password_line)
self.auth_form.setLayout(self.form) self.auth_form.setLayout(self.form)
self.root_l.addWidget(self.auth_form) self.root_l.addWidget(self.auth_form)
self.root_l.addWidget(self.auth_button) self.root_l.addWidget(self.auth_button)
# self.root_l.addWidget(self.guest_button) # Опционально
self.root.setLayout(self.root_l) self.root.setLayout(self.root_l)
self.setCentralWidget(self.root) self.setCentralWidget(self.root)
def _connect_slots(self): def _connect_slots(self):
self.auth_button.clicked.connect(self._on_auth_button_clicked) self.auth_button.clicked.connect(self._on_auth_button_clicked)
self.login_success.connect(self._on_login_success) self.login_success.connect(self._on_login_success)
self.login_forbidden.connect(self._on_login_forbidden) self.login_forbidden.connect(self._on_login_forbidden)
# self.guest_button.clicked.connect(self._on_guest_button_clicked) # Опционально
def _on_auth_button_clicked(self): def _on_auth_button_clicked(self):
"""Обработка нажатия кнопки входа"""
login = self.login_line.text() login = self.login_line.text()
password = self.password_line.text() password = self.password_line.text()
@ -101,49 +123,53 @@ class LoginWindow(BaseWindow):
self.login_forbidden.emit(SignalCode.SIGFALSE) self.login_forbidden.emit(SignalCode.SIGFALSE)
return return
print(f"Login OK: {user.name} {user.rights}") print(f"Login successful: {user.name} ({user.rights.name})")
self.login_success.emit(user) self.login_success.emit(user)
@pyqtSlot(User) @pyqtSlot(User)
def _on_login_success(self, user: User): def _on_login_success(self, user: User):
"""Успешная авторизация - переход к соответствующей панели"""
self._composer.render_request.emit(user) self._composer.render_request.emit(user)
@pyqtSlot(SignalCode) @pyqtSlot(SignalCode)
def _on_login_forbidden(self, code: SignalCode): def _on_login_forbidden(self, code: SignalCode):
"""Обработка ошибок авторизации"""
match code: match code:
case SignalCode.SIGFALSE: case SignalCode.SIGFALSE:
QMessageBox().information(self, QMessageBox.warning(self,
"Предупреждение!", "Authentication Failed",
"Вы ввели некорректные данные") "Invalid username or password")
case SignalCode.SIGERR: case SignalCode.SIGERR:
QMessageBox().information(self, QMessageBox.critical(self,
"Ошибка!", "Input Error",
"Вы ввели неприемлемые данные") "Please enter both username and password")
def _apply_window_settings(self): def _apply_window_settings(self):
self.setWindowTitle("Авторизация") self.setWindowTitle("Login")
self.setFixedSize(260,180) self.setFixedSize(300, 200)
class AdminWindow(BaseWindow): class AdminWindow(BaseWindow):
"""
Панель администратора
TODO: Добавить табы для каждой таблицы из задания
Например: products, orders, users, etc.
"""
def _define_widgets(self): def _define_widgets(self):
self.root = QWidget() self.root = QWidget()
self.tabs = QTabWidget() self.tabs = QTabWidget()
self.users_tab = TabWidgetCustom("users", self._db) # TODO: Заменить на ваши таблицы
self.guests_tab = TabWidgetCustom("guests", self._db) # Пример:
self.rooms_tab = TabWidgetCustom("rooms", self._db) # self.products_tab = TabWidgetCustom("products", self._db)
self.bookings_tab = TabWidgetCustom("bookings", self._db) # self.orders_tab = TabWidgetCustom("orders", self._db, show_date_filter=True)
self.staff_tab = TabWidgetCustom("staff", self._db) # self.users_tab = TabWidgetCustom("users", self._db)
self.payments_tab = TabWidgetCustom("payments", self._db)
self.tabs.addTab(self.users_tab, "Пользователи") self.example_tab = TabWidgetCustom("items", self._db) # TODO: Заменить
self.tabs.addTab(self.guests_tab, "Постояльцы")
self.tabs.addTab(self.rooms_tab, "Номера") self.tabs.addTab(self.example_tab, "Items") # TODO: Адаптировать названия
self.tabs.addTab(self.bookings_tab, "Бронирования")
self.tabs.addTab(self.staff_tab, "Персонал")
self.tabs.addTab(self.payments_tab, "Платежи")
def _tune_layouts(self): def _tune_layouts(self):
self.root_l = QVBoxLayout() self.root_l = QVBoxLayout()
@ -152,245 +178,352 @@ class AdminWindow(BaseWindow):
self.setCentralWidget(self.root) self.setCentralWidget(self.root)
def _connect_slots(self): def _connect_slots(self):
"""Подключение обработчиков для каждого таба"""
for i in range(self.tabs.count()): for i in range(self.tabs.count()):
tab = self.tabs.widget(i) tab = self.tabs.widget(i)
# Фильтрация по датам (если доступна)
try: try:
tab.button_all.clicked.connect(lambda _, t=tab: self._select_all(t)) tab.button_all.clicked.connect(lambda _, t=tab: self._select_all(t))
tab.button_filter.clicked.connect(lambda _, t=tab: self._select_filter(t)) tab.button_filter.clicked.connect(lambda _, t=tab: self._select_filter(t))
except Exception as e: except AttributeError:
pass pass # Нет фильтра по датам
# CRUD операции
tab.button_add.clicked.connect(lambda _, t=tab: self._add_row(t)) tab.button_add.clicked.connect(lambda _, t=tab: self._add_row(t))
tab.button_del.clicked.connect(lambda _, t=tab: self._del_row(t)) tab.button_del.clicked.connect(lambda _, t=tab: self._del_row(t))
tab.button_csv.clicked.connect(lambda _, t=tab: self._csv_row(t)) tab.button_csv.clicked.connect(lambda _, t=tab: self._csv_export(t))
tab.button_ok.clicked.connect(lambda _, t=tab: self._apply_changes(t)) tab.button_ok.clicked.connect(lambda _, t=tab: self._apply_changes(t))
tab.button_deny.clicked.connect(lambda _, t=tab: self._revert_changes(t)) tab.button_deny.clicked.connect(lambda _, t=tab: self._revert_changes(t))
def _select_all(self, tab): def _select_all(self, tab):
"""Сброс фильтра - показать все записи"""
tab.model.setFilter("") tab.model.setFilter("")
tab.model.select() tab.model.select()
def _select_filter(self, tab): def _select_filter(self, tab):
"""
Фильтрация по датам
TODO: Адаптировать под поля из задания
"""
date_from = tab.from_date.date().toString("yyyy-MM-dd") date_from = tab.from_date.date().toString("yyyy-MM-dd")
date_to = tab.to_date.date().toString("yyyy-MM-dd") date_to = tab.to_date.date().toString("yyyy-MM-dd")
if tab._name == "bookings": # TODO: Изменить название поля (checkin/checkout → order_date/delivery_date)
tab.model.setFilter(f"DATE(checkin) >= \'{date_from}\' AND DATE(checkout) <= \'{date_to}\'") if hasattr(tab, '_name'):
tab.model.select() # Пример фильтра
elif tab._name == "payments": tab.model.setFilter(
tab.model.setFilter(f"date <= \'{date_from}\' AND date <= \'{date_to}\'") f"DATE(date_field) >= '{date_from}' AND DATE(date_field) <= '{date_to}'"
)
tab.model.select() tab.model.select()
def _add_row(self, tab): def _add_row(self, tab):
"""Добавление новой строки"""
row = tab.model.rowCount() row = tab.model.rowCount()
tab.model.insertRow(row) tab.model.insertRow(row)
tab.view.selectRow(row) tab.view.selectRow(row)
tab.view.edit(tab.model.index(row, 0)) tab.view.edit(tab.model.index(row, 0))
def _del_row(self, tab): def _del_row(self, tab):
"""Удаление выбранной строки"""
idx = tab.view.currentIndex() idx = tab.view.currentIndex()
if not idx.isValid(): if not idx.isValid():
print("Error: Fatal: row index is not valid!") QMessageBox.warning(self, "Selection Error",
"Please select a row to delete")
return return
msg = QMessageBox.question(self, "Подтверждение", confirm = QMessageBox.question(
"Вы уверены что хотите удалить выбранную строку ?" self,
"Изменения будут необратимы") "Confirm Deletion",
"Are you sure you want to delete this record?\nThis action cannot be undone.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if msg == QMessageBox.StandardButton.Yes: if confirm == QMessageBox.StandardButton.Yes:
tab.model.removeRow(idx.row()) tab.model.removeRow(idx.row())
tab.model.submitAll() tab.model.submitAll()
def _apply_changes(self, tab): def _apply_changes(self, tab):
"""Применить изменения к БД"""
if not tab.model.isDirty(): if not tab.model.isDirty():
return return
msg = QMessageBox.question(self, "Подтверждение", confirm = QMessageBox.question(
"Применить изменения ?") self,
"Apply Changes",
"Do you want to save all changes?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if msg == QMessageBox.StandardButton.Yes: if confirm == QMessageBox.StandardButton.Yes:
if not tab.model.submitAll(): if not tab.model.submitAll():
QMessageBox.critical(self, "Ошибка БД", QMessageBox.critical(
"Не получилось применить изменения," self,
f"\n{tab.model.lastError().text()}") "Database Error",
f"Failed to save changes:\n{tab.model.lastError().text()}"
)
tab.model.revertAll() tab.model.revertAll()
def _revert_changes(self, tab): def _revert_changes(self, tab):
"""Отменить несохранённые изменения"""
if not tab.model.isDirty(): if not tab.model.isDirty():
return return
tab.model.revertAll() tab.model.revertAll()
QMessageBox.information(self, "Changes Reverted",
"All unsaved changes have been discarded")
def _csv_row(self, tab): def _csv_export(self, tab):
path = f"{tab._name}.csv" """Экспорт таблицы в CSV"""
filename = f"{tab._name}.csv"
with open(path, "w", newline="", encoding="utf-8") as f: try:
writer = csv.writer(f) with open(filename, "w", newline="", encoding="utf-8") as f:
header = [tab.model.headerData(i, Qt.Orientation.Horizontal) for i in range(tab.model.columnCount())] writer = csv.writer(f)
writer.writerow(header) # Заголовки
headers = [
tab.model.headerData(i, Qt.Orientation.Horizontal)
for i in range(tab.model.columnCount())
]
writer.writerow(headers)
for row in range(tab.model.rowCount()): # Данные
row_data = [] for row in range(tab.model.rowCount()):
for col in range(tab.model.columnCount()): row_data = []
value = tab.model.data(tab.model.index(row, col)) for col in range(tab.model.columnCount()):
value = tab.model.data(tab.model.index(row, col))
if hasattr(value, "toString"): if hasattr(value, "toString"):
value = value.toString() value = value.toString()
row_data.append(value) row_data.append(value)
writer.writerow(row_data) writer.writerow(row_data)
QMessageBox.information(self, "Информация", QMessageBox.information(
"Отчёт был сохранён в корневую директорию приложения") self,
"Export Successful",
f"Data exported to:\n{filename}"
)
except Exception as e:
QMessageBox.critical(
self,
"Export Failed",
f"Failed to export data:\n{str(e)}"
)
def _apply_window_settings(self): def _apply_window_settings(self):
self.setWindowTitle("Панель Администратора") self.setWindowTitle("Admin Panel")
self.setFixedSize(1200,800) self.setFixedSize(1200, 800)
class ManagerWindow(BaseWindow):
"""
Панель менеджера
TODO: Реализовать если требуется в задании
Обычно: просмотр данных + поиск/фильтрация (без удаления)
"""
def _define_widgets(self):
# TODO: Похож на AdminWindow, но с ограниченными правами
pass
def _apply_window_settings(self):
self.setWindowTitle("Manager Panel")
self.setFixedSize(1000, 600)
class ClientWindow(BaseWindow): class ClientWindow(BaseWindow):
"""
Панель клиента
TODO: Адаптировать под задание
Обычная структура:
- Таб 1: Создание заявки/заказа
- Таб 2: Просмотр доступных товаров/услуг
- Таб 3: История заявок/заказов пользователя
"""
def _define_widgets(self): def _define_widgets(self):
self.root = QTabWidget() self.root = QTabWidget()
self._define_request_box() self._define_request_form()
self._define_free_rooms() self._define_available_items()
self._define_client_bookings() self._define_user_requests()
def _tune_layouts(self): def _tune_layouts(self):
self.root_l = QVBoxLayout() self.root_l = QVBoxLayout()
self._tune_request_box() self._tune_request_form()
self._tune_free_rooms() self._tune_available_items()
self._tune_client_bookings() self._tune_user_requests()
self.root.addTab(self.rb_widget, "Окно заявки") # TODO: Адаптировать названия табов под задание
self.root.addTab(self.fr_widget, "Свободные номера") self.root.addTab(self.request_widget, "New Request")
self.root.addTab(self.cl_widget, "Ваши заявки") self.root.addTab(self.items_widget, "Available Items")
self.root.addTab(self.history_widget, "My Requests")
self.root.setLayout(self.root_l) self.root.setLayout(self.root_l)
self.setCentralWidget(self.root) self.setCentralWidget(self.root)
def _define_request_box(self): def _define_request_form(self):
self.rb_widget = QWidget() """
self.req_box = QGroupBox("Создание заявки") Форма создания заявки
self.req_box.setAlignment(Qt.AlignmentFlag.AlignVCenter |
Qt.AlignmentFlag.AlignHCenter)
self.room_combo = QComboBox() TODO: Адаптировать под задание
"""
self.request_widget = QWidget()
self.request_box = QGroupBox("Create Request")
self.request_box.setAlignment(Qt.AlignmentFlag.AlignVCenter |
Qt.AlignmentFlag.AlignHCenter)
free_rooms = get_free_numbers() # TODO: Заменить на реальные поля из задания
self.item_combo = QComboBox()
if free_rooms: # Загрузка доступных элементов
for room in free_rooms: items = get_items()
number = room[1] if items:
self.room_combo.addItem(str(number)) for item in items:
# TODO: Адаптировать под структуру данных
self.item_combo.addItem(str(item[1])) # Предполагаем что item[1] - название
else: else:
self.room_combo.addItem(NO_NUMBERS_CONST) self.item_combo.addItem("No items available")
self.checkin = QDateEdit() # TODO: Добавить дополнительные поля (даты, количество, etc.)
self.checkin.setCalendarPopup(True) self.date_from = QDateEdit()
self.date_from.setCalendarPopup(True)
self.checkout = QDateEdit() self.date_to = QDateEdit()
self.checkout.setCalendarPopup(True) self.date_to.setCalendarPopup(True)
self.book_button = QPushButton("Забронировать") self.submit_button = QPushButton("Submit Request")
def _tune_request_box(self): def _tune_request_form(self):
self.req_l = QVBoxLayout() """Компоновка формы заявки"""
self.form_l = QFormLayout() req_layout = QVBoxLayout()
form_layout = QFormLayout()
self.form_l.addRow("Комната:", self.room_combo) # TODO: Адаптировать названия полей
self.form_l.addRow("Заезд:", self.checkin) form_layout.addRow("Item:", self.item_combo)
self.form_l.addRow("Выезд:", self.checkout) form_layout.addRow("From:", self.date_from)
form_layout.addRow("To:", self.date_to)
self.req_box.setLayout(self.form_l) self.request_box.setLayout(form_layout)
self.req_l.addWidget(self.req_box) req_layout.addWidget(self.request_box)
self.req_l.addWidget(self.book_button) req_layout.addWidget(self.submit_button)
self.rb_widget.setLayout(self.req_l) self.request_widget.setLayout(req_layout)
def _define_free_rooms(self): def _define_available_items(self):
self.fr_widget = QWidget() """
self.fr_table = QTableView() Таб со списком доступных элементов
free_rooms = get_free_numbers() TODO: Адаптировать под задание
"""
self.items_widget = QWidget()
self.items_table = QTableView()
self.fr_model = QStandardItemModel() items = get_items()
self.fr_model.setHorizontalHeaderLabels(["Номер Комнаты"])
if free_rooms: self.items_model = QStandardItemModel()
for room in free_rooms: # TODO: Адаптировать заголовки
item = room[1] self.items_model.setHorizontalHeaderLabels(["ID", "Name", "Status"])
self.fr_model.appendRow(QStandardItem(str(item)))
self.fr_table.setModel(self.fr_model) if items:
for item in items:
# TODO: Адаптировать под структуру данных
row = [QStandardItem(str(field)) for field in item]
self.items_model.appendRow(row)
def _tune_free_rooms(self): self.items_table.setModel(self.items_model)
self.f_rooms_l = QVBoxLayout()
self.f_rooms_l.addWidget(self.fr_table)
self.fr_widget.setLayout(self.f_rooms_l) def _tune_available_items(self):
"""Компоновка таба доступных элементов"""
layout = QVBoxLayout()
layout.addWidget(self.items_table)
self.items_widget.setLayout(layout)
def _define_client_bookings(self): def _define_user_requests(self):
self.cl_widget = QWidget() """
self.cl_table = QTableView() Таб с историей заявок пользователя
self.cl_model = QSqlTableModel(db=self._db) TODO: Адаптировать под задание
self.cl_model.setTable("bookings") """
self.cl_model.select() self.history_widget = QWidget()
self.history_table = QTableView()
self.cl_table.setModel(self.cl_model) self.history_model = QSqlTableModel(db=self._db)
# TODO: Заменить на реальную таблицу
self.history_model.setTable("requests")
def _tune_client_bookings(self): # TODO: Добавить фильтр по user_id
self.c_bookings_l = QVBoxLayout() if self._user:
self.c_bookings_l.addWidget(self.cl_table) self.history_model.setFilter(f"user_id = {self._user.id}")
self.cl_widget.setLayout(self.c_bookings_l) self.history_model.select()
self.history_table.setModel(self.history_model)
def _tune_user_requests(self):
"""Компоновка таба истории"""
layout = QVBoxLayout()
layout.addWidget(self.history_table)
self.history_widget.setLayout(layout)
def _connect_slots(self): def _connect_slots(self):
self.book_button.clicked.connect(self._book_button_handler) """Подключение обработчиков"""
self.submit_button.clicked.connect(self._on_submit_request)
def _book_button_handler(self): def _on_submit_request(self):
room = self.room_combo.currentText() """
checkin = self.checkin.date().toString("yyyy-MM-dd") Обработка создания заявки
checkout = self.checkout.date().toString("yyyy-MM-dd")
if room == NO_NUMBERS_CONST: TODO: Реализовать логику создания заявки
QMessageBox.information(self, "Информация", """
"На данный момент у нас нет свободных номеров," item = 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":
QMessageBox.warning(self, "No Items",
"There are no items available at this moment")
return return
if not checkin or not checkout: if not date_from or not date_to:
QMessageBox.critical(self, "Информация", QMessageBox.critical(self, "Input Error",
"Пожалуйста, введите корректную дату прибытия и отбытия") "Please select valid dates")
return return
current_date = QDate.currentDate() # Проверка дат
if self.date_from.date() < QDate.currentDate() or \
if self.checkin.date() < current_date or self.checkout.date() < current_date: self.date_to.date() < QDate.currentDate():
QMessageBox.critical(self, "Предупреждение", QMessageBox.warning(self, "Invalid Date",
"Вы не можете выбрать прошедшую дату!") "Cannot select past dates")
return return
status = update_number_status(room, checkin, checkout, self._user) # TODO: Вызвать функцию создания заявки из db.py
# success = create_request(item_id, self._user, date_from=date_from, date_to=date_to)
if not status: # Заглушка
QMessageBox.critical(self, "Ошибка!", success = True
"Не получилось забронировать ваш номер, попробуйте позже")
return if success:
QMessageBox.information(
self,
"Success",
f"Request created successfully!\nItem: {item}\nPeriod: {date_from} - {date_to}"
)
self.history_model.select() # Обновить историю
else: else:
QMessageBox.information(self, "Успешно!", QMessageBox.critical(self, "Error",
f"Вы забронировали номер {room} на даты" "Failed to create request. Please try again later.")
f"\nС {checkin} по {checkout}, будем рады вас видеть!")
self.cl_model.select()
def _apply_window_settings(self): def _apply_window_settings(self):
self.setWindowTitle("Панель Пользователя") self.setWindowTitle("Client Panel")
self.setFixedSize(800,400) self.setFixedSize(900, 600)