import json from functools import partial from services.core.loading_service import run_with_progress from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QHBoxLayout, QMenu, QHeaderView, QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton, QStyle, QStylePainter, QLineEdit, ) from PyQt6.QtGui import QAction from PyQt6.QtCore import Qt, QRect, pyqtSignal from database.models.product import Product from services.image_service import ImageService from gui.tabs.products.forms.product_form import ProductForm from gui.tabs.products.dialogs.product_filter_dialog import FilterDialog from gui.tabs.products.dialogs.add_listed_dialog import AddListedDialog PAGE_SIZE = 10 # --- Header Checkbox --- class CheckBoxHeader(QHeaderView): select_all_changed = pyqtSignal(bool) def __init__(self, orientation, parent=None): super().__init__(orientation, parent) self.isOn = False self.setSectionsClickable(True) self.sectionPressed.connect(self.handle_section_pressed) def paintSection(self, painter, rect, logicalIndex): super().paintSection(painter, rect, logicalIndex) if logicalIndex == 0: option = QStyleOptionButton() size = 20 x = rect.x() + (rect.width() - size) // 2 y = rect.y() + (rect.height() - size) // 2 option.rect = QRect(x, y, size, size) # PyQt6: use QStyle.StateFlag and QStyle.ControlElement state = QStyle.StateFlag.State_Enabled state = state | ( QStyle.StateFlag.State_On if self.isOn else QStyle.StateFlag.State_Off ) option.state = state painter2 = QStylePainter(self.viewport()) painter2.drawControl(QStyle.ControlElement.CE_CheckBox, option) def handle_section_pressed(self, logicalIndex): if logicalIndex == 0: self.isOn = not self.isOn self.select_all_changed.emit(self.isOn) self.viewport().update() # --- ProductTab --- class ProductTab(QWidget): SORTABLE_COLUMNS = {1: "id", 3: "name", 4: "price", 7: "created_at"} def __init__(self): super().__init__() self.current_page = 0 self.total_count = 0 self.total_pages = 0 self.filters = {} self.sort_by = "id" self.sort_order = "ASC" layout = QVBoxLayout() # Top menu top_layout = QHBoxLayout() self.add_btn = QPushButton("Add Product") self.add_btn.clicked.connect(self.add_product) top_layout.addWidget(self.add_btn) self.options_btn = QPushButton("Action") self.options_btn.setSizePolicy( QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed ) self.options_btn.setMinimumWidth(50) self.options_btn.setMaximumWidth(120) top_layout.addWidget(self.options_btn) layout.addLayout(top_layout) # Table self.table = QTableWidget() self.table.verticalHeader().setDefaultSectionSize(60) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) header = CheckBoxHeader(Qt.Orientation.Horizontal, self.table) self.table.setHorizontalHeader(header) header.select_all_changed.connect(self.select_all_rows) header.sectionClicked.connect(self.handle_header_click) layout.addWidget(self.table) # Pagination pag_layout = QHBoxLayout() self.prev_btn = QPushButton("Previous") self.prev_btn.clicked.connect(self.prev_page) pag_layout.addWidget(self.prev_btn) self.page_info_label = QLabel("Page 1 / 1 (0 items)") pag_layout.addWidget(self.page_info_label) self.page_input = QLineEdit() self.page_input.setFixedWidth(50) self.page_input.setPlaceholderText("Page") self.page_input.returnPressed.connect(self.go_to_page) pag_layout.addWidget(self.page_input) self.next_btn = QPushButton("Next") self.next_btn.clicked.connect(self.next_page) pag_layout.addWidget(self.next_btn) layout.addLayout(pag_layout) self.setLayout(layout) # self.load_data() # --- Handle Header Click for Sorting --- def handle_header_click(self, logicalIndex): if logicalIndex in self.SORTABLE_COLUMNS: col = self.SORTABLE_COLUMNS[logicalIndex] if self.sort_by == col: self.sort_order = "DESC" if self.sort_order == "ASC" else "ASC" else: self.sort_by = col self.sort_order = "ASC" self.load_data() # --- Options Menu --- def update_options_menu(self): menu = QMenu() action_reload = QAction("Reload", menu) action_reload.triggered.connect(lambda: self.load_data(show_progress=True)) menu.addAction(action_reload) action_filter = QAction("Filter", menu) action_filter.triggered.connect(self.open_filter_dialog) menu.addAction(action_filter) if self.filters: action_clear = QAction("Clear Filter", menu) action_clear.triggered.connect(self.clear_filters) menu.addAction(action_clear) # --- Thêm Add Listed Selected --- if any( isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked() for i in range(self.table.rowCount()) ): action_add_listed_selected = QAction("Add Listed Selected", menu) action_add_listed_selected.triggered.connect(self.add_listed_selected) menu.addAction(action_add_listed_selected) if any( isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked() for i in range(self.table.rowCount()) ): action_delete_selected = QAction("Delete Selected", menu) action_delete_selected.triggered.connect(self.delete_selected) menu.addAction(action_delete_selected) self.options_btn.setMenu(menu) # --- Filter --- def open_filter_dialog(self): dialog = FilterDialog(self) if dialog.exec(): # PyQt6: exec() self.filters = dialog.get_filters() self.current_page = 0 self.load_data() def clear_filters(self): self.filters = {} self.current_page = 0 self.load_data() # --- Load Data --- def load_data(self, show_progress=True): """ Load products vào table. - show_progress: nếu True thì hiển thị progress dialog (user trigger), False khi mở app. """ self.table.clearContents() self.table.setRowCount(0) offset = self.current_page * PAGE_SIZE # Lấy toàn bộ dữ liệu cần load page_items, total_count = Product.get_paginated( offset, PAGE_SIZE, self.filters, sort_by=self.sort_by, sort_order=self.sort_order, ) self.total_count = total_count self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE) self.table.setColumnCount(9) columns = [ "", "Image", "SKU", "Name", "Price", "Condition", "Brand", "Created At", "Actions", ] self.table.setHorizontalHeaderLabels(columns) self.table.setRowCount(len(page_items)) def handler(p, i_row): product_id = p.get("id") # Checkbox cb = QCheckBox() cb.setProperty("product_id", product_id) self.table.setCellWidget(i_row, 0, cb) cb.stateChanged.connect(self.update_options_menu) # Image images = json.loads(p.get("images") or "[]") if images: pixmap = ImageService.get_display_pixmap(images[0], size=60) if pixmap: lbl = QLabel() lbl.setPixmap(pixmap) lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setCellWidget(i_row, 1, lbl) else: self.table.setItem(i_row, 1, QTableWidgetItem("None")) else: self.table.setItem(i_row, 1, QTableWidgetItem("None")) # Các cột text self.table.setItem(i_row, 2, QTableWidgetItem(p.get("sku") or "")) self.table.setItem(i_row, 3, QTableWidgetItem(p.get("name") or "")) self.table.setItem(i_row, 4, QTableWidgetItem(str(p.get("price") or ""))) self.table.setItem(i_row, 5, QTableWidgetItem(p.get("condition") or "")) self.table.setItem(i_row, 6, QTableWidgetItem(p.get("brand") or "")) created_str = "" created_ts = p.get("created_at") if created_ts: from datetime import datetime try: created_str = datetime.fromtimestamp(int(created_ts)).strftime( "%Y-%m-%d %H:%M" ) except Exception: created_str = str(created_ts) self.table.setItem(i_row, 7, QTableWidgetItem(created_str)) # Actions btn_menu = QPushButton("Actions") menu = QMenu() act_edit = QAction("Edit", btn_menu) act_edit.triggered.connect(partial(self.edit_product, p)) menu.addAction(act_edit) act_add_listed = QAction("Add Listed", btn_menu) # <-- thêm action này act_add_listed.triggered.connect(partial(self.add_listed_row, product_id)) menu.addAction(act_add_listed) act_del = QAction("Delete", btn_menu) act_del.triggered.connect(partial(self.delete_product, product_id)) menu.addAction(act_del) btn_menu.setMenu(menu) self.table.setCellWidget(i_row, 8, btn_menu) # --- Hiển thị progress nếu cần --- items_with_index = [(p, i) for i, p in enumerate(page_items)] if show_progress: run_with_progress( items_with_index, handler=lambda x: handler(*x), message="Loading products...", parent=self, ) else: for item in items_with_index: handler(*item) # Header sizing header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) self.table.setColumnWidth(1, 60) for idx in range(2, 8): header.setSectionResizeMode(idx, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed) self.table.setColumnWidth(8, 100) # Pagination self.prev_btn.setEnabled(self.current_page > 0) self.next_btn.setEnabled(self.current_page < self.total_pages - 1) self.page_info_label.setText( f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)" ) # Reset header checkbox if isinstance(header, CheckBoxHeader): header.isOn = False header.viewport().update() self.update_options_menu() # --- Go to page --- def go_to_page(self): try: page = int(self.page_input.text()) - 1 if 0 <= page < self.total_pages: self.current_page = page self.load_data() except ValueError: pass # --- Select All --- def select_all_rows(self, checked): for i in range(self.table.rowCount()): cb = self.table.cellWidget(i, 0) if isinstance(cb, QCheckBox): cb.setChecked(checked) # --- Delete Selected --- def delete_selected(self): ids = [ int(cb.property("product_id")) for i in range(self.table.rowCount()) if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked() ] if not ids: QMessageBox.information(self, "Info", "No product selected") return confirm = QMessageBox.question( self, "Confirm Delete", f"Delete {len(ids)} selected products?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if confirm != QMessageBox.StandardButton.Yes: return # --- dùng run_with_progress --- run_with_progress( ids, handler=Product.delete, message="Deleting products...", parent=self ) self.current_page = 0 self.load_data() # --- Product Actions --- def add_product(self): form = ProductForm(self) if form.exec(): # PyQt6: exec() self.current_page = 0 self.load_data() def edit_product(self, product): form = ProductForm(self, product) if form.exec(): # PyQt6: exec() self.load_data() def delete_product(self, product_id): confirm = QMessageBox.question( self, "Confirm Delete", f"Delete product ID {product_id}?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if confirm != QMessageBox.StandardButton.Yes: return run_with_progress( [product_id], handler=Product.delete, message="Deleting product...", parent=self, ) self.load_data() def add_listed_selected(self): selected_ids = [ int(cb.property("product_id")) for i in range(self.table.rowCount()) if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked() ] if not selected_ids: QMessageBox.information(self, "Info", "No products selected") return dialog = AddListedDialog(selected_ids, parent=self) dialog.exec() # PyQt6 # --- Clear row checkboxes --- for i in range(self.table.rowCount()): cb = self.table.cellWidget(i, 0) if isinstance(cb, QCheckBox): cb.setChecked(False) # --- Reset header checkbox --- self.reset_header_checkbox() # --- Update menu --- self.update_options_menu() def reset_header_checkbox(self): header = self.table.horizontalHeader() if isinstance(header, CheckBoxHeader): header.isOn = False header.viewport().update() # --- Add Listed for single row (row action) --- def add_listed_row(self, product_id): if not product_id: QMessageBox.warning(self, "Warning", "Invalid product") return dialog = AddListedDialog([product_id], parent=self) dialog.exec() # PyQt6 # --- Pagination --- def next_page(self): if self.current_page < self.total_pages - 1: self.current_page += 1 self.load_data() def prev_page(self): if self.current_page > 0: self.current_page -= 1 self.load_data()