455 lines
15 KiB
Python
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()
|