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

417 lines
14 KiB
Python

import json
from functools import partial
from services.core.loading_service import run_with_progress
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView,
QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton, QStyle, QStylePainter, QLineEdit
)
from PyQt5.QtCore import Qt, QRect, pyqtSignal
from database.models.product import Product
from database.models.listed import Listed
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)
option.state = QStyle.State_Enabled | (QStyle.State_On if self.isOn else QStyle.State_Off)
painter2 = QStylePainter(self.viewport())
painter2.drawControl(QStyle.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.Fixed, QSizePolicy.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.NoEditTriggers)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
header = CheckBoxHeader(Qt.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_():
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.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.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.Fixed)
self.table.setColumnWidth(1, 60)
for idx in range(2, 8):
header.setSectionResizeMode(idx, QHeaderView.Stretch)
header.setSectionResizeMode(8, QHeaderView.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.Yes | QMessageBox.No
)
if confirm != QMessageBox.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_():
self.current_page = 0
self.load_data()
def edit_product(self, product):
form = ProductForm(self, product)
if form.exec_():
self.load_data()
def delete_product(self, product_id):
confirm = QMessageBox.question(
self, "Confirm Delete", f"Delete product ID {product_id}?", QMessageBox.Yes | QMessageBox.No
)
if confirm != QMessageBox.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_()
# --- 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_()
# --- 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()