facebook-tool/gui/tabs/products/product_tab.py

455 lines
15 KiB
Python

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