commit 3c2137da2b82fa3aeb11d13ec8b20a454eaf26cc Author: helldh Date: Thu Dec 25 21:44:30 2025 +0300 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..129c041 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv +__pycache__ +*\.pyc diff --git a/composer.py b/composer.py new file mode 100644 index 0000000..e88f7e1 --- /dev/null +++ b/composer.py @@ -0,0 +1,72 @@ +from src.windows import LoginWindow, AdminWindow, ClientWindow +from src.objects import User, Rights +from src.db import DB_AUTH_HARDCODED as config + +from PyQt6.QtWidgets import QApplication +from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal +from PyQt6.QtSql import QSqlDatabase + +class Composer(QObject): + render_request = pyqtSignal(User) + + def __init__(self): + super().__init__() + self._current = None + self._app = QApplication([]) + self._init_db() + self.render_request.connect(self._render) + + def _init_db(self): + self._db = QSqlDatabase("QPSQL") + self._db.setDatabaseName(config['dbname']) + self._db.setPort(config['port']) + self._db.setHostName(config['host']) + self._db.setUserName(config['user']) + self._db.setPassword(config['password']) + self._db.open() + + @pyqtSlot(User) + def _render(self, user: User): + match user.rights: + case Rights.ADMIN: + self._admin_fabric() + case Rights.MANAGER: + pass + case Rights.CLIENT: + self._client_fabric(user) + + def _login_fabric(self): + self.wlogin = LoginWindow(self, self._db) + + if self._current: + self._current.close() + + self.wlogin.show() + + self._current = self.wlogin + + def _admin_fabric(self): + self.wadmin = AdminWindow(self, self._db) + + if self._current: + self._current.close() + + self.wadmin.show() + + self._current = self.wadmin + + def _client_fabric(self, user: User): + self.wclient = ClientWindow(self, self._db, user) + + if self._current: + self._current.close() + + self.wclient.show() + + self._current = self.wclient + + def run(self): + import sys + self._login_fabric() + self._current.show() + sys.exit(self._app.exec()) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..a7ddfea --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ +from composer import Composer + +def main(): + composer = Composer() + composer.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scrapper.sh b/scrapper.sh new file mode 100755 index 0000000..7c70281 --- /dev/null +++ b/scrapper.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +OUTPUT="all_code.txt" +ROOT="." + +> "$OUTPUT" + +find "$ROOT" \ + -type d -name "__pycache__" -prune -o \ + -type f -name "*.py" ! -name "*.pyc" \ + -print | sort | while read -r file; do + echo "########################################" >> "$OUTPUT" + echo "# FILE: $file" >> "$OUTPUT" + echo "########################################" >> "$OUTPUT" + echo >> "$OUTPUT" + cat "$file" >> "$OUTPUT" + echo -e "\n\n" >> "$OUTPUT" +done diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/db.py b/src/db.py new file mode 100644 index 0000000..150b108 --- /dev/null +++ b/src/db.py @@ -0,0 +1,153 @@ +import psycopg2 as pg + +from .objects import User, Rights + +DB_AUTH_HARDCODED = { + "host": "127.0.0.1", + "port": 5432, + "dbname": "examdb", + "user": "postgres", + "password": "213k2010###" +} + +def get_connection(): + return pg.connect(**DB_AUTH_HARDCODED) + +def do_request(autocommit=False): + def upper_wrapper(func): + def wrapper(*args, **kwargs): + conn = get_connection() + cursor = conn.cursor() + + try: + kwargs['cursor'] = cursor + + result = func(*args, **kwargs) + + if autocommit: + conn.commit() + + return result + + except Exception as e: + print(f"Error: Can't request query to DB: {e}") + return None + + finally: + cursor.close() + conn.close() + + return wrapper + + return upper_wrapper + +@do_request() +def auth(login: str, password: str, *, cursor) -> User | None: + cursor.execute(""" + SELECT id, name, rights + FROM users + WHERE name = %s + AND password = %s; + """, (login, password)) + + user = cursor.fetchone() + + if not user: + print("Warning: Login Forbidden: Can't find such user!") + return None + + rights = None + + match user[2]: + case "admin": + rights = Rights.ADMIN + case "customer": + rights = Rights.CLIENT + case "manager": + rights = Rights.MANAGER + case _: + return None + + return User( + id=user[0], + name=user[1], + rights=rights + ) + +@do_request() +def get_free_numbers(*, cursor): + cursor.execute(""" + SELECT * + FROM rooms + WHERE status = 'free'; + """) + + free = cursor.fetchall() + + if not free: + return None + + return free + +@do_request(autocommit=True) +def update_number_status(number: str, checkin: str, + checkout: str, user: User, + *, cursor): + cursor.execute(""" + SELECT password + FROM users + WHERE id = %s + """, (user.id,)) + + password = cursor.fetchone() + + 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 \ No newline at end of file diff --git a/src/objects.py b/src/objects.py new file mode 100644 index 0000000..ad11e58 --- /dev/null +++ b/src/objects.py @@ -0,0 +1,17 @@ +from enum import Enum, auto +from dataclasses import dataclass + +class SignalCode(Enum): + SIGFALSE = auto() + SIGERR = auto() + +class Rights(Enum): + ADMIN = auto() + CLIENT = auto() + MANAGER = auto() + +@dataclass +class User: + id: int + name: str + rights: Rights \ No newline at end of file diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..b1d81e7 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,70 @@ +from PyQt6.QtWidgets import ( + QWidget, + QTableView, + QHBoxLayout, + QVBoxLayout, + QDateEdit, + QPushButton, + QLabel +) +from PyQt6.QtSql import QSqlTableModel + +class TabWidgetCustom(QWidget): + def __init__(self, name: str, db): + super().__init__() + self._name = name + self._db = db + self._setup() + + def _setup(self): + self.root = QVBoxLayout(self) + + if self._name in ("bookings", "payments"): + self.header = QHBoxLayout() + + self.from_date = QDateEdit() + self.from_date.setCalendarPopup(True) + + self.to_date = QDateEdit() + self.to_date.setCalendarPopup(True) + + self.button_filter = QPushButton("Фильтровать") + self.button_all = QPushButton("Показать всё") + + self.header.addWidget(QLabel("С:")) + self.header.addWidget(self.from_date) + self.header.addWidget(QLabel("По:")) + self.header.addWidget(self.to_date) + self.header.addWidget(self.button_filter) + self.header.addWidget(self.button_all) + + self.root.addLayout(self.header) + + self.view = QTableView() + + self.btoolbar = QHBoxLayout() + + self.button_add = QPushButton("+ Добавить") + self.button_del = QPushButton("- Удалить") + self.button_ok = QPushButton("\\_/ Применить") + self.button_deny = QPushButton("-- Отменить") + self.button_csv = QPushButton("Выгрузить отчёт в CSV") + + self.btoolbar.addWidget(self.button_add) + self.btoolbar.addWidget(self.button_del) + self.btoolbar.addWidget(self.button_ok) + self.btoolbar.addWidget(self.button_deny) + self.btoolbar.addWidget(self.button_csv) + + self.root.addWidget(self.view) + self.root.addLayout(self.btoolbar) + + self._setup_db() + + def _setup_db(self): + self.model = QSqlTableModel(db=self._db) + self.model.setTable(self._name) + self.model.setEditStrategy(QSqlTableModel.EditStrategy.OnManualSubmit) + self.model.select() + + self.view.setModel(self.model) \ No newline at end of file diff --git a/src/windows.py b/src/windows.py new file mode 100644 index 0000000..f168f19 --- /dev/null +++ b/src/windows.py @@ -0,0 +1,396 @@ +import csv +from .db import auth, get_free_numbers, update_number_status +from .objects import User, SignalCode +from .utils import TabWidgetCustom +from PyQt6.QtWidgets import ( + QWidget, + QLineEdit, + QPushButton, + QGroupBox, + QFormLayout, + QVBoxLayout, + QMessageBox, + QTabWidget, + QMainWindow, + QComboBox, + QDateEdit, + QTableView +) +from PyQt6.QtGui import QStandardItemModel, QStandardItem +from PyQt6.QtSql import QSqlTableModel +from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot, QDate + +FIW = 160 # Fixed Input Width +NO_NUMBERS_CONST = "Нет свободных номеров" + +class BaseWindow(QMainWindow): + def __init__(self, composer, db, user=None): + super().__init__() + self._composer = composer + self._db = db + self._user = user + self._define_widgets() + self._tune_layouts() + self._connect_slots() + self._apply_window_settings() + + def _define_widgets(self): + pass + + def _tune_layouts(self): + pass + + def _connect_slots(self): + pass + + def _apply_window_settings(self): + pass + +class LoginWindow(BaseWindow): + login_success = pyqtSignal(User) + login_forbidden = pyqtSignal(SignalCode) + + def _define_widgets(self): + self.root = QWidget() + + self.auth_form = QGroupBox("Логин") + self.auth_form.setAlignment(Qt.AlignmentFlag.AlignHCenter | + Qt.AlignmentFlag.AlignVCenter) + + self.login_line = QLineEdit() + self.login_line.setFixedWidth(FIW) + + self.password_line = QLineEdit() + self.password_line.setFixedWidth(FIW) + self.password_line.setEchoMode(QLineEdit.EchoMode.Password) + + self.auth_button = QPushButton("Авторизоваться") + + def _tune_layouts(self): + self.root_l = QVBoxLayout() + + self.form = QFormLayout() + self.form.addRow("Логин:", self.login_line) + self.form.addRow("Пароль:", self.password_line) + + self.auth_form.setLayout(self.form) + + self.root_l.addWidget(self.auth_form) + self.root_l.addWidget(self.auth_button) + + self.root.setLayout(self.root_l) + + self.setCentralWidget(self.root) + + def _connect_slots(self): + self.auth_button.clicked.connect(self._on_auth_button_clicked) + self.login_success.connect(self._on_login_success) + self.login_forbidden.connect(self._on_login_forbidden) + + def _on_auth_button_clicked(self): + login = self.login_line.text() + password = self.password_line.text() + + if not login or not password: + self.login_forbidden.emit(SignalCode.SIGERR) + return + + user = auth(login, password) + + if not user: + self.login_forbidden.emit(SignalCode.SIGFALSE) + return + + print(f"Login OK: {user.name} {user.rights}") + + self.login_success.emit(user) + + @pyqtSlot(User) + def _on_login_success(self, user: User): + self._composer.render_request.emit(user) + + @pyqtSlot(SignalCode) + def _on_login_forbidden(self, code: SignalCode): + match code: + case SignalCode.SIGFALSE: + QMessageBox().information(self, + "Предупреждение!", + "Вы ввели некорректные данные") + case SignalCode.SIGERR: + QMessageBox().information(self, + "Ошибка!", + "Вы ввели неприемлемые данные") + + def _apply_window_settings(self): + self.setWindowTitle("Авторизация") + self.setFixedSize(260,180) + +class AdminWindow(BaseWindow): + def _define_widgets(self): + self.root = QWidget() + + self.tabs = QTabWidget() + + self.users_tab = TabWidgetCustom("users", self._db) + self.guests_tab = TabWidgetCustom("guests", self._db) + self.rooms_tab = TabWidgetCustom("rooms", self._db) + self.bookings_tab = TabWidgetCustom("bookings", self._db) + self.staff_tab = TabWidgetCustom("staff", self._db) + self.payments_tab = TabWidgetCustom("payments", self._db) + + self.tabs.addTab(self.users_tab, "Пользователи") + self.tabs.addTab(self.guests_tab, "Постояльцы") + self.tabs.addTab(self.rooms_tab, "Номера") + self.tabs.addTab(self.bookings_tab, "Бронирования") + self.tabs.addTab(self.staff_tab, "Персонал") + self.tabs.addTab(self.payments_tab, "Платежи") + + def _tune_layouts(self): + self.root_l = QVBoxLayout() + self.root_l.addWidget(self.tabs) + self.root.setLayout(self.root_l) + self.setCentralWidget(self.root) + + def _connect_slots(self): + for i in range(self.tabs.count()): + tab = self.tabs.widget(i) + + try: + tab.button_all.clicked.connect(lambda _, t=tab: self._select_all(t)) + tab.button_filter.clicked.connect(lambda _, t=tab: self._select_filter(t)) + except Exception as e: + pass + + 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_csv.clicked.connect(lambda _, t=tab: self._csv_row(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)) + + def _select_all(self, tab): + tab.model.setFilter("") + tab.model.select() + + def _select_filter(self, tab): + date_from = tab.from_date.date().toString("yyyy-MM-dd") + date_to = tab.to_date.date().toString("yyyy-MM-dd") + + if tab._name == "bookings": + tab.model.setFilter(f"DATE(checkin) >= \'{date_from}\' AND DATE(checkout) <= \'{date_to}\'") + tab.model.select() + elif tab._name == "payments": + tab.model.setFilter(f"date <= \'{date_from}\' AND date <= \'{date_to}\'") + tab.model.select() + + def _add_row(self, tab): + row = tab.model.rowCount() + tab.model.insertRow(row) + tab.view.selectRow(row) + tab.view.edit(tab.model.index(row, 0)) + + def _del_row(self, tab): + idx = tab.view.currentIndex() + + if not idx.isValid(): + print("Error: Fatal: row index is not valid!") + return + + msg = QMessageBox.question(self, "Подтверждение", + "Вы уверены что хотите удалить выбранную строку ?" + "Изменения будут необратимы") + + if msg == QMessageBox.StandardButton.Yes: + tab.model.removeRow(idx.row()) + tab.model.submitAll() + + def _apply_changes(self, tab): + if not tab.model.isDirty(): + return + + msg = QMessageBox.question(self, "Подтверждение", + "Применить изменения ?") + + if msg == QMessageBox.StandardButton.Yes: + if not tab.model.submitAll(): + QMessageBox.critical(self, "Ошибка БД", + "Не получилось применить изменения," + f"\n{tab.model.lastError().text()}") + tab.model.revertAll() + + def _revert_changes(self, tab): + if not tab.model.isDirty(): + return + + tab.model.revertAll() + + def _csv_row(self, tab): + path = f"{tab._name}.csv" + + with open(path, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + header = [tab.model.headerData(i, Qt.Orientation.Horizontal) for i in range(tab.model.columnCount())] + + writer.writerow(header) + + for row in range(tab.model.rowCount()): + row_data = [] + for col in range(tab.model.columnCount()): + value = tab.model.data(tab.model.index(row, col)) + + if hasattr(value, "toString"): + value = value.toString() + + row_data.append(value) + + writer.writerow(row_data) + + QMessageBox.information(self, "Информация", + "Отчёт был сохранён в корневую директорию приложения") + + def _apply_window_settings(self): + self.setWindowTitle("Панель Администратора") + self.setFixedSize(1200,800) + +class ClientWindow(BaseWindow): + def _define_widgets(self): + self.root = QTabWidget() + + self._define_request_box() + self._define_free_rooms() + self._define_client_bookings() + + def _tune_layouts(self): + self.root_l = QVBoxLayout() + + self._tune_request_box() + self._tune_free_rooms() + self._tune_client_bookings() + + self.root.addTab(self.rb_widget, "Окно заявки") + self.root.addTab(self.fr_widget, "Свободные номера") + self.root.addTab(self.cl_widget, "Ваши заявки") + + self.root.setLayout(self.root_l) + + self.setCentralWidget(self.root) + + def _define_request_box(self): + self.rb_widget = QWidget() + self.req_box = QGroupBox("Создание заявки") + self.req_box.setAlignment(Qt.AlignmentFlag.AlignVCenter | + Qt.AlignmentFlag.AlignHCenter) + + self.room_combo = QComboBox() + + free_rooms = get_free_numbers() + + if free_rooms: + for room in free_rooms: + number = room[1] + self.room_combo.addItem(str(number)) + else: + self.room_combo.addItem(NO_NUMBERS_CONST) + + self.checkin = QDateEdit() + self.checkin.setCalendarPopup(True) + + self.checkout = QDateEdit() + self.checkout.setCalendarPopup(True) + + self.book_button = QPushButton("Забронировать") + + def _tune_request_box(self): + self.req_l = QVBoxLayout() + self.form_l = QFormLayout() + + self.form_l.addRow("Комната:", self.room_combo) + self.form_l.addRow("Заезд:", self.checkin) + self.form_l.addRow("Выезд:", self.checkout) + + self.req_box.setLayout(self.form_l) + + self.req_l.addWidget(self.req_box) + self.req_l.addWidget(self.book_button) + + self.rb_widget.setLayout(self.req_l) + + def _define_free_rooms(self): + self.fr_widget = QWidget() + self.fr_table = QTableView() + + free_rooms = get_free_numbers() + + self.fr_model = QStandardItemModel() + self.fr_model.setHorizontalHeaderLabels(["Номер Комнаты"]) + + if free_rooms: + for room in free_rooms: + item = room[1] + self.fr_model.appendRow(QStandardItem(str(item))) + + self.fr_table.setModel(self.fr_model) + + def _tune_free_rooms(self): + self.f_rooms_l = QVBoxLayout() + self.f_rooms_l.addWidget(self.fr_table) + + self.fr_widget.setLayout(self.f_rooms_l) + + def _define_client_bookings(self): + self.cl_widget = QWidget() + self.cl_table = QTableView() + + self.cl_model = QSqlTableModel(db=self._db) + self.cl_model.setTable("bookings") + self.cl_model.select() + + self.cl_table.setModel(self.cl_model) + + def _tune_client_bookings(self): + self.c_bookings_l = QVBoxLayout() + self.c_bookings_l.addWidget(self.cl_table) + + self.cl_widget.setLayout(self.c_bookings_l) + + def _connect_slots(self): + self.book_button.clicked.connect(self._book_button_handler) + + def _book_button_handler(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: + QMessageBox.information(self, "Информация", + "На данный момент у нас нет свободных номеров," + "приносим свои извинения за доставленные неудобства") + return + + if not checkin or not checkout: + QMessageBox.critical(self, "Информация", + "Пожалуйста, введите корректную дату прибытия и отбытия") + return + + current_date = QDate.currentDate() + + if self.checkin.date() < current_date or self.checkout.date() < current_date: + QMessageBox.critical(self, "Предупреждение", + "Вы не можете выбрать прошедшую дату!") + return + + status = update_number_status(room, checkin, checkout, self._user) + + if not status: + QMessageBox.critical(self, "Ошибка!", + "Не получилось забронировать ваш номер, попробуйте позже") + return + else: + QMessageBox.information(self, "Успешно!", + f"Вы забронировали номер {room} на даты" + f"\nС {checkin} по {checkout}, будем рады вас видеть!") + + self.cl_model.select() + + def _apply_window_settings(self): + self.setWindowTitle("Панель Пользователя") + self.setFixedSize(800,400) \ No newline at end of file