From 85d313d45b94f6b0577ec504a0f5b53dca20c1a6 Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Tue, 14 Jan 2025 17:22:35 +0100 Subject: [PATCH] [main] Started working on member list --- src/database/__init__.py | 0 src/database/member.py | 14 ++ src/models/__init__.py | 5 +- src/ui/book_editor/book_editor.py | 20 ++- src/ui/dashboard/book_card.py | 18 ++- src/ui/dashboard/dashboard.py | 5 +- src/ui/main_window_tabs/__init__.py | 0 .../main_window_tabs/member_list/__init__.py | 0 .../member_list/member_card.py | 143 ++++++++++++++++++ .../member_list/member_list.py | 114 ++++++++++++++ src/ui/member_editor/member_editor.py | 82 ++++++++-- src/ui/window.py | 7 +- 12 files changed, 384 insertions(+), 24 deletions(-) create mode 100644 src/database/__init__.py create mode 100644 src/database/member.py create mode 100644 src/ui/main_window_tabs/__init__.py create mode 100644 src/ui/main_window_tabs/member_list/__init__.py create mode 100644 src/ui/main_window_tabs/member_list/member_card.py create mode 100644 src/ui/main_window_tabs/member_list/member_list.py diff --git a/src/database/__init__.py b/src/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/database/member.py b/src/database/member.py new file mode 100644 index 0000000..8f84bb2 --- /dev/null +++ b/src/database/member.py @@ -0,0 +1,14 @@ +from sqlalchemy.exc import IntegrityError + +from models.member import Member + +from utils.database import DatabaseManager + +def create_new_member(new_member: Member): + try: + with DatabaseManager.get_session() as session: + session.add(new_member) + session.commit() + except IntegrityError as e: + print(e) + session.rollback() diff --git a/src/models/__init__.py b/src/models/__init__.py index 49b7b9c..dc362da 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -2,11 +2,10 @@ from .base import Base from .author import Author from .book import Book from .book_category import BookCategory +from .book_overview import BooksOverview from .book_category_link import BookCategoryLink from .member import Member from .librarian import Librarian from .loan import Loan -from .book_overview import BooksOverview - -__all__ = ["Author", "Book", "BookCategory", "BookCategoryLink", "Member", "Librarian", "Loan", "BookOverviewView"] +__all__ = ["Author", "Book", "BookCategory", "BookCategoryLink", "Member", "Librarian", "Loan", "BooksOverview"] diff --git a/src/ui/book_editor/book_editor.py b/src/ui/book_editor/book_editor.py index 908c555..180bad5 100644 --- a/src/ui/book_editor/book_editor.py +++ b/src/ui/book_editor/book_editor.py @@ -5,6 +5,10 @@ from PySide6.QtGui import QRegularExpressionValidator from PySide6.QtCore import QRegularExpression from models.book import Book, BookStatusEnum +from utils.database import DatabaseManager + +from sqlalchemy import update + class BookEditor(QDialog): def __init__(self, book: Book, parent=None): super().__init__(parent) @@ -47,12 +51,6 @@ class BookEditor(QDialog): self.categories_input = QLineEdit(all_categories) form_layout.addRow("Categories: ", self.categories_input) - # Status dropdown - self.status_input = QComboBox() - self.status_input.addItems([status.value for status in BookStatusEnum]) - self.status_input.setCurrentText(self.book.status.value) - form_layout.addRow("Status:", self.status_input) - layout.addLayout(form_layout) # Buttons @@ -76,5 +74,15 @@ class BookEditor(QDialog): self.book.isbn = self.isbn_input.text() self.book.status = BookStatusEnum(self.status_input.currentText()) + + with DatabaseManager.get_session() as session: + existing_book = session.query(Book).get(self.book.id) + + existing_book.title = self.book.title + existing_book.description = self.book.description + existing_book.year_published = self.book.year_published + existing_book.isbn = self.book.isbn + + session.commit() # Accept the dialog and close self.accept() diff --git a/src/ui/dashboard/book_card.py b/src/ui/dashboard/book_card.py index a12b7da..579b110 100644 --- a/src/ui/dashboard/book_card.py +++ b/src/ui/dashboard/book_card.py @@ -129,7 +129,9 @@ class BookCard(QWidget): print("Remove reservation selected") def delete_book(self): - print("Delete") + if not self.make_sure(): + return + with DatabaseManager.get_session() as session: try: stmt = delete(Book).where(Book.id == self.book_overview.id) @@ -139,3 +141,17 @@ class BookCard(QWidget): except Exception as e: session.rollback print(e) + + def make_sure(self) -> bool: + are_you_sure_box = QMessageBox() + are_you_sure_box.setIcon(QMessageBox.Question) + are_you_sure_box.setWindowTitle("Are you sure?") + are_you_sure_box.setText(f"Are you sure you want to delete {self.book_overview.title}?") + are_you_sure_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + are_you_sure_box.setDefaultButton(QMessageBox.No) + + # Show the message box and capture the user's response + response = are_you_sure_box.exec() + + # Handle the response + return response == QMessageBox.Yes \ No newline at end of file diff --git a/src/ui/dashboard/dashboard.py b/src/ui/dashboard/dashboard.py index 72a96a0..1631b0e 100644 --- a/src/ui/dashboard/dashboard.py +++ b/src/ui/dashboard/dashboard.py @@ -10,6 +10,8 @@ from models.book_overview import BooksOverview from utils.database import DatabaseManager +from ui.member_editor.member_editor import MemberEditor + class LibraryDashboard(QWidget): def __init__(self): @@ -74,8 +76,7 @@ class LibraryDashboard(QWidget): card.setVisible(title_contains_text or author_name_contains_text or isbn_contains_text) def register_member(self): - QMessageBox.information( - self, "Add Member", "Open dialog to register a new member.") + MemberEditor().exec() def add_borrow_record(self): QMessageBox.information(self, "Add Borrow Record", diff --git a/src/ui/main_window_tabs/__init__.py b/src/ui/main_window_tabs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/main_window_tabs/member_list/__init__.py b/src/ui/main_window_tabs/member_list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/main_window_tabs/member_list/member_card.py b/src/ui/main_window_tabs/member_list/member_card.py new file mode 100644 index 0000000..9c32fbc --- /dev/null +++ b/src/ui/main_window_tabs/member_list/member_card.py @@ -0,0 +1,143 @@ +from PySide6.QtWidgets import ( + QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout, QMessageBox +) +from PySide6.QtGui import Qt +from PySide6.QtCore import qDebug + +from models.member import Member, MemberStatusEnum +from utils.database import DatabaseManager +from sqlalchemy import delete + +STATUS_TO_COLOR_MAP = { + MemberStatusEnum.active: "#3c702e", + MemberStatusEnum.inactive: "#702525" +} + + +class MemberCard(QWidget): + def __init__(self, member: Member): + super().__init__() + + self.member = member + + self.setAttribute(Qt.WidgetAttribute.WA_Hover, True) + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + + # Set initial stylesheet with hover behavior + self.setStyleSheet(""" + MemberCard:hover { + background-color: palette(highlight); + } + """) + + # Layout setup + layout = QHBoxLayout(self) + layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize) + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Left-side content + left_side = QVBoxLayout() + layout.addLayout(left_side) + name_label = QLabel(f"{member.first_name} {member.last_name}") + name_label.setStyleSheet("font-size: 20px; font-weight: bold;") + email_label = QLabel(f"Email: {member.email}") + phone_label = QLabel(f"Phone: {member.phone or 'Not Available'}") + left_side.addWidget(name_label) + left_side.addWidget(email_label) + left_side.addWidget(phone_label) + + # Right-side content + right_side = QVBoxLayout() + layout.addLayout(right_side) + + status_label = QLabel(str(member.status.value.capitalize())) + status_label.setStyleSheet(f"color: {STATUS_TO_COLOR_MAP[member.status]}; font-size: 20px; font-weight: bold;") + status_label.setAlignment(Qt.AlignmentFlag.AlignRight) + + register_date_label = QLabel(f"Registered: {member.register_date}") + register_date_label.setAlignment(Qt.AlignmentFlag.AlignRight) + + right_side.addWidget(status_label) + right_side.addWidget(register_date_label) + + self.setLayout(layout) + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.contextMenuEvent(event) + else: + super().mousePressEvent(event) + + def contextMenuEvent(self, event): + context_menu = QMenu(self) + + action_edit_member = context_menu.addAction("Edit Member") + action_deactivate_member = context_menu.addAction("Deactivate Member") + action_activate_member = context_menu.addAction("Activate Member") + context_menu.addSeparator() + delete_member_action = context_menu.addAction("Delete Member") + delete_member_action.triggered.connect(self.delete_member) + + if self.member.status == MemberStatusEnum.active: + action_activate_member.setVisible(False) + elif self.member.status == MemberStatusEnum.inactive: + action_deactivate_member.setVisible(False) + + action = context_menu.exec_(self.mapToGlobal(event.pos())) + + if action == action_edit_member: + print("Edit Member selected") # Implement editor logic here + elif action == action_deactivate_member: + self.update_member_status(MemberStatusEnum.inactive) + elif action == action_activate_member: + self.update_member_status(MemberStatusEnum.active) + + def delete_member(self): + self.make_sure() + # if not self.make_sure(): + # return + + # with DatabaseManager.get_session() as session: + # try: + # stmt = delete(Member).where(Member.id == self.member.id) + # session.execute(stmt) + # session.commit() + # self.setVisible(False) + # except Exception as e: + # session.rollback() + # print(e) + + def update_member_status(self, new_status): + with DatabaseManager.get_session() as session: + try: + member = session.query(Member).filter(Member.id == self.member.id).one_or_none() + if member: + member.status = new_status + session.commit() + QMessageBox.information(self, "Status Updated", f"Member status updated to {new_status.value.capitalize()}.") + self.member.status = new_status + self.update_status_label() + else: + QMessageBox.critical(self, "Error", "The member you requested could not be found.", QMessageBox.StandardButton.Ok) + except Exception as e: + session.rollback() + print(e) + + def update_status_label(self): + self.findChild(QLabel, self.member.status.value).setStyleSheet( + f"color: {STATUS_TO_COLOR_MAP[self.member.status]}; font-size: 20px; font-weight: bold;" + ) + + def make_sure(self) -> bool: + are_you_sure_box = QMessageBox() + are_you_sure_box.setIcon(QMessageBox.Question) + are_you_sure_box.setWindowTitle("Are you sure?") + are_you_sure_box.setText(f"Are you sure you want to delete {self.member.first_name} {self.member.last_name}?") + are_you_sure_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + are_you_sure_box.setDefaultButton(QMessageBox.No) + + response = are_you_sure_box.exec() + return response == QMessageBox.Yes diff --git a/src/ui/main_window_tabs/member_list/member_list.py b/src/ui/main_window_tabs/member_list/member_list.py new file mode 100644 index 0000000..38dc64a --- /dev/null +++ b/src/ui/main_window_tabs/member_list/member_list.py @@ -0,0 +1,114 @@ +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, + QPushButton, QMessageBox, QVBoxLayout +) +from PySide6.QtCore import Qt + +from .member_card import MemberCard +from models.member import Member +from utils.database import DatabaseManager + +from ui.member_editor.member_editor import MemberEditor + + +class MemberDashboard(QWidget): + def __init__(self): + super().__init__() + + # Central widget and layout + main_layout = QVBoxLayout(self) + + # Title label + title_label = QLabel("Member Dashboard", self) + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet( + "font-size: 20px; font-weight: bold; color: #0078D4;") + main_layout.addWidget(title_label) + + # Search bar + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Search members...") + self.search_input.textChanged.connect(self.filter_members) + main_layout.addWidget(self.search_input) + + # Scrollable area for cards + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + + # Container widget for the scroll area + self.scroll_widget = QWidget() + self.scroll_layout = QVBoxLayout(self.scroll_widget) + self.scroll_layout.setSpacing(5) # Set gap between individual cards + self.scroll_layout.setContentsMargins(0, 0, 0, 0) # Remove default spacing + + # Align the cards to the top + self.scroll_layout.setAlignment(Qt.AlignTop) + self.members = [] + self.member_cards = [] + self.redraw_cards() + + self.scroll_widget.setLayout(self.scroll_layout) + self.scroll_area.setWidget(self.scroll_widget) + main_layout.addWidget(self.scroll_area) + + # Buttons for actions + button_layout = QHBoxLayout() + register_member_button = QPushButton("Add New Member") + register_member_button.clicked.connect(self.register_member) + button_layout.addWidget(register_member_button) + + delete_member_button = QPushButton("Delete Member") + delete_member_button.clicked.connect(self.delete_member) + button_layout.addWidget(delete_member_button) + + main_layout.addLayout(button_layout) + + def filter_members(self, text): + """Filter the cards based on the search input.""" + for card, member in zip(self.member_cards, self.members): + name_contains_text = text.lower() in member.name.lower() + id_contains_text = text.lower() in str(member.id) + + card.setVisible(name_contains_text or id_contains_text) + + def register_member(self): + MemberEditor().exec() + + def delete_member(self): + QMessageBox.information(self, "Delete Member", + "Open dialog to delete a member.") + + def clear_layout(self, layout): + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.deleteLater() + else: + sub_layout = item.layout() + if sub_layout is not None: + self.clear_layout(sub_layout) + del item + + def redraw_cards(self): + self.clear_layout(self.scroll_layout) + self.member_cards = [] + + self.members = self.fetch_members_from_db() + + for member in self.members: + card = MemberCard(member) + + self.scroll_layout.addWidget(card) + self.member_cards.append(card) + + def fetch_members_from_db(self): + """Fetch all members from the database.""" + try: + with DatabaseManager.get_session() as session: + members = session.query(Member).all() + return members + except Exception as e: + QMessageBox.critical(self, "Database Error", + f"Failed to fetch members: {e}") + return [] diff --git a/src/ui/member_editor/member_editor.py b/src/ui/member_editor/member_editor.py index 75b68d1..53089c0 100644 --- a/src/ui/member_editor/member_editor.py +++ b/src/ui/member_editor/member_editor.py @@ -1,19 +1,81 @@ from PySide6.QtGui import QGuiApplication, QAction from PySide6.QtQml import QQmlApplicationEngine from PySide6 import QtWidgets, QtCore +from PySide6.QtWidgets import QVBoxLayout, QFormLayout, QLineEdit, QHBoxLayout, QPushButton, QDialog -class MemberEditor(QtWidgets.QWidget): - def __init__(self): +from models.member import Member + +from database.member import create_new_member + + +class MemberEditor(QDialog): + def __init__(self, member: Member = None): super().__init__() - # Central widget and layout - main_layout = QtWidgets.QVBoxLayout(self) + self.create_layout() - # Title label - title_label = QtWidgets.QLabel("Members", self) - title_label.setAlignment(QtCore.Qt.AlignCenter) - title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #0078D4;") - main_layout.addWidget(title_label) + if member: + self.member = member + self.fill_with_existing_data() + self.create_new = False + else: + self.member = Member() + self.create_new = True - self.setLayout(main_layout) + def create_layout(self): + self.setWindowTitle("Members") + self.setMinimumSize(400, 300) + # Create main layout + self.layout = QVBoxLayout(self) + + # Form layout for member fields + self.form_layout = QFormLayout() + + # First name field + self.first_name_input = QLineEdit() + self.form_layout.addRow("First name:", self.first_name_input) + + # Last name field + self.last_name_input = QLineEdit() + self.form_layout.addRow("Last name: ", self.last_name_input) + + # E-mail field + self.email_input = QLineEdit() + self.form_layout.addRow("E-mail:", self.email_input) + + # Phone number + self.phone_number_input = QLineEdit() + self.form_layout.addRow("Phone number:", self.phone_number_input) + + self.layout.addLayout(self.form_layout) + + # Buttons + self.button_layout = QHBoxLayout() + + self.save_button = QPushButton("Save") + self.save_button.clicked.connect(self.save_member) + self.button_layout.addWidget(self.save_button) + + self.cancel_button = QPushButton("Discard") + self.cancel_button.clicked.connect(self.reject) + self.button_layout.addWidget(self.cancel_button) + + self.layout.addLayout(self.button_layout) + + def fill_with_existing_data(self): + self.first_name_input.setText(self.member.first_name) + self.last_name_input.setText(self.member.last_name) + self.email_input.setText(self.member.email) + self.phone_number_input.setText(self.member.phone) + + def save_member(self): + self.member.first_name = self.first_name_input.text() + self.member.last_name = self.last_name_input.text() + self.member.email = self.email_input.text() + self.member.phone = self.phone_number_input.text() + + if self.create_new: + create_new_member(self.member) + + self.accept() \ No newline at end of file diff --git a/src/ui/window.py b/src/ui/window.py index b530831..02c7554 100644 --- a/src/ui/window.py +++ b/src/ui/window.py @@ -5,6 +5,7 @@ from PySide6.QtWidgets import QMessageBox, QFileDialog from PySide6.QtCore import QStandardPaths from ui.dashboard.dashboard import LibraryDashboard +from ui.main_window_tabs.member_list.member_list import MemberDashboard from ui.book_editor.book_editor import BookEditor from ui.member_editor.member_editor import MemberEditor @@ -39,8 +40,9 @@ class LibraryWindow(QtWidgets.QMainWindow): self.dashboard = LibraryDashboard() + self.member_list = MemberDashboard() central_widget.addTab(self.dashboard, "Dashboard") - central_widget.addTab(MemberEditor(), "Members") + central_widget.addTab(self.member_list, "Members") self.file_types = { "XML files (*.xml)": ".xml", @@ -211,7 +213,8 @@ class LibraryWindow(QtWidgets.QMainWindow): pass def new_member(self): - pass + MemberEditor().exec() + def import_data(self): pass