setting page
This commit is contained in:
parent
ccd85a03fc
commit
2355a7de55
|
|
@ -55,6 +55,33 @@ def create_tables():
|
||||||
FOREIGN KEY(product_id) REFERENCES products(id) ON DELETE CASCADE
|
FOREIGN KEY(product_id) REFERENCES products(id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Tạo bảng settings với thêm cột type
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
key TEXT NOT NULL UNIQUE,
|
||||||
|
value TEXT,
|
||||||
|
type TEXT DEFAULT 'text', -- NEW: loại giá trị (text, boolean, number)
|
||||||
|
created_at INTEGER DEFAULT (strftime('%s','now')),
|
||||||
|
updated_at INTEGER DEFAULT (strftime('%s','now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# --- Default settings với type ---
|
||||||
|
default_settings = [
|
||||||
|
("AUTO_LISTING", "true", "boolean"), # Bật/tắt tự động listing
|
||||||
|
("MAX_CONCURRENT_LISTING", "2", "number"), # Số lượng listing tối đa chạy song song
|
||||||
|
("LISTING_INTERVAL_SECONDS", "5", "number"), # Thời gian nghỉ giữa các listing
|
||||||
|
("MAX_CONCURRENT_LISTING", "2", "number"), # Số lượng worker chạy đồng thời
|
||||||
|
]
|
||||||
|
|
||||||
|
for key, value, typ in default_settings:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR IGNORE INTO settings (key, value, type) VALUES (?, ?, ?)",
|
||||||
|
(key, value, typ)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
|
||||||
|
|
@ -56,11 +56,6 @@ class Listed:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_paginated(offset, limit, filters=None, sort_by="id", sort_order="ASC"):
|
def get_paginated(offset, limit, filters=None, sort_by="id", sort_order="ASC"):
|
||||||
"""
|
|
||||||
Trả về:
|
|
||||||
- danh sách dict: mỗi dict gồm id, sku, product_name, account_email, listed_at, condition, status, images
|
|
||||||
- total_count: tổng số bản ghi
|
|
||||||
"""
|
|
||||||
filters = filters or {}
|
filters = filters or {}
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
|
|
@ -79,41 +74,65 @@ class Listed:
|
||||||
JOIN accounts a ON l.account_id = a.id
|
JOIN accounts a ON l.account_id = a.id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# --- Thêm filter nếu có ---
|
|
||||||
where_clauses = []
|
where_clauses = []
|
||||||
if "status" in filters:
|
|
||||||
where_clauses.append("l.status = ?")
|
# --- Filters ---
|
||||||
params.append(filters["status"])
|
if "status" in filters and filters["status"]:
|
||||||
if "account_id" in filters:
|
status_val = filters["status"]
|
||||||
|
if isinstance(status_val, list):
|
||||||
|
placeholders = ",".join(["?"] * len(status_val))
|
||||||
|
where_clauses.append(f"l.status IN ({placeholders})")
|
||||||
|
params.extend(status_val)
|
||||||
|
else:
|
||||||
|
where_clauses.append("l.status = ?")
|
||||||
|
params.append(status_val)
|
||||||
|
|
||||||
|
if "account_id" in filters and filters["account_id"]:
|
||||||
where_clauses.append("l.account_id = ?")
|
where_clauses.append("l.account_id = ?")
|
||||||
params.append(filters["account_id"])
|
params.append(filters["account_id"])
|
||||||
|
|
||||||
|
if "sku" in filters and filters["sku"].strip():
|
||||||
|
where_clauses.append("p.sku LIKE ?")
|
||||||
|
params.append(f"%{filters['sku'].strip()}%")
|
||||||
|
|
||||||
|
if "product_name" in filters and filters["product_name"].strip():
|
||||||
|
where_clauses.append("p.name LIKE ?")
|
||||||
|
params.append(f"%{filters['product_name'].strip()}%")
|
||||||
|
|
||||||
|
if "account_email" in filters and filters["account_email"].strip():
|
||||||
|
where_clauses.append("a.email LIKE ?")
|
||||||
|
params.append(f"%{filters['account_email'].strip()}%")
|
||||||
|
|
||||||
if where_clauses:
|
if where_clauses:
|
||||||
base_query += " WHERE " + " AND ".join(where_clauses)
|
base_query += " WHERE " + " AND ".join(where_clauses)
|
||||||
|
|
||||||
# --- Đếm tổng ---
|
# --- Count ---
|
||||||
total_query = f"SELECT COUNT(*) FROM ({base_query})"
|
total_query = f"SELECT COUNT(*) FROM ({base_query}) AS total"
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute(total_query, params)
|
cur.execute(total_query, params)
|
||||||
total_count = cur.fetchone()[0]
|
total_count = cur.fetchone()[0]
|
||||||
|
|
||||||
# --- Thêm sort + limit + offset ---
|
# --- Sort + Paginate ---
|
||||||
sort_column_map = {
|
sort_column_map = {
|
||||||
"id": "l.id",
|
"id": "l.id",
|
||||||
|
"sku": "p.sku",
|
||||||
"product_name": "p.name",
|
"product_name": "p.name",
|
||||||
"listed_at": "l.listed_at"
|
"account_email": "a.email",
|
||||||
|
"listed_at": "l.listed_at",
|
||||||
|
"status": "l.status"
|
||||||
}
|
}
|
||||||
sort_col = sort_column_map.get(sort_by, "l.id")
|
sort_col = sort_column_map.get(sort_by, "l.id")
|
||||||
|
sort_order = "DESC" if sort_order.upper() == "DESC" else "ASC"
|
||||||
|
|
||||||
base_query += f" ORDER BY {sort_col} {sort_order} LIMIT ? OFFSET ?"
|
base_query += f" ORDER BY {sort_col} {sort_order} LIMIT ? OFFSET ?"
|
||||||
params.extend([limit, offset])
|
params.extend([limit, offset])
|
||||||
|
|
||||||
cur.execute(base_query, params)
|
cur.execute(base_query, params)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
# Chuyển row thành dict
|
items = [
|
||||||
items = []
|
{
|
||||||
for r in rows:
|
|
||||||
items.append({
|
|
||||||
"id": r[0],
|
"id": r[0],
|
||||||
"sku": r[1],
|
"sku": r[1],
|
||||||
"product_name": r[2],
|
"product_name": r[2],
|
||||||
|
|
@ -121,12 +140,13 @@ class Listed:
|
||||||
"listed_at": r[4],
|
"listed_at": r[4],
|
||||||
"status": r[5],
|
"status": r[5],
|
||||||
"condition": r[6],
|
"condition": r[6],
|
||||||
"images": r[7] # giả sử lưu dạng JSON string
|
"images": r[7]
|
||||||
})
|
}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
conn.close()
|
||||||
return items, total_count
|
return items, total_count
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bulk_create(items):
|
def bulk_create(items):
|
||||||
|
|
@ -207,3 +227,21 @@ class Listed:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return deleted_count
|
return deleted_count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def count_all():
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM listed")
|
||||||
|
total = cursor.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
return total
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def count_by_status(status):
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM listed WHERE status=?", (status,))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
return count
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
# database/models/setting.py
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
from database.db import get_connection
|
||||||
|
|
||||||
|
class Setting:
|
||||||
|
|
||||||
|
# --- Constants for setting keys ---
|
||||||
|
AUTO_LISTING = "AUTO_LISTING"
|
||||||
|
MAX_CONCURRENT_LISTING = "MAX_CONCURRENT_LISTING"
|
||||||
|
LISTING_INTERVAL_SECONDS = "LISTING_INTERVAL_SECONDS"
|
||||||
|
MAX_CONCURRENT_LISTING = "MAX_CONCURRENT_LISTING"
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def all():
|
||||||
|
conn = get_connection()
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT * FROM settings")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id(setting_id):
|
||||||
|
conn = get_connection()
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT * FROM settings WHERE id = ?", (setting_id,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_key(key):
|
||||||
|
conn = get_connection()
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM settings WHERE key = ?",
|
||||||
|
(key,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
return dict(row)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(key, value):
|
||||||
|
existing = Setting.get_by_key(key)
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
created_at = updated_at = int(time.time())
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)",
|
||||||
|
(key, value, created_at, updated_at)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return Setting.get_by_key(key)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete(setting_id):
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("DELETE FROM settings WHERE id = ?", (setting_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_paginated(offset, limit, filters=None, sort_by="id", sort_order="ASC"):
|
||||||
|
filters = filters or {}
|
||||||
|
params = []
|
||||||
|
|
||||||
|
# Lấy luôn type
|
||||||
|
base_query = "SELECT id, key, value, type, created_at, updated_at FROM settings"
|
||||||
|
where_clauses = []
|
||||||
|
|
||||||
|
# --- Filters ---
|
||||||
|
if "key" in filters and filters["key"].strip():
|
||||||
|
where_clauses.append("key LIKE ?")
|
||||||
|
params.append(f"%{filters['key'].strip()}%")
|
||||||
|
|
||||||
|
if "value" in filters and filters["value"].strip():
|
||||||
|
where_clauses.append("value LIKE ?")
|
||||||
|
params.append(f"%{filters['value'].strip()}%")
|
||||||
|
|
||||||
|
if where_clauses:
|
||||||
|
base_query += " WHERE " + " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
# --- Count ---
|
||||||
|
total_query = f"SELECT COUNT(*) FROM ({base_query}) AS total"
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(total_query, params)
|
||||||
|
total_count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
# --- Sort + Paginate ---
|
||||||
|
sort_column_map = {
|
||||||
|
"id": "id",
|
||||||
|
"key": "key",
|
||||||
|
"value": "value",
|
||||||
|
"type": "type",
|
||||||
|
"created_at": "created_at",
|
||||||
|
"updated_at": "updated_at"
|
||||||
|
}
|
||||||
|
sort_col = sort_column_map.get(sort_by, "id")
|
||||||
|
sort_order = "DESC" if sort_order.upper() == "DESC" else "ASC"
|
||||||
|
|
||||||
|
base_query += f" ORDER BY {sort_col} {sort_order} LIMIT ? OFFSET ?"
|
||||||
|
params.extend([limit, offset])
|
||||||
|
|
||||||
|
cur.execute(base_query, params)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"id": r[0],
|
||||||
|
"key": r[1],
|
||||||
|
"value": r[2],
|
||||||
|
"type": r[3], # <-- thêm type
|
||||||
|
"created_at": r[4],
|
||||||
|
"updated_at": r[5]
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return items, total_count
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get(key: str, default=None):
|
||||||
|
"""Lấy giá trị setting theo key."""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT value FROM settings WHERE key = ?", (key,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return row["value"] if row else default
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_value(cls, setting_id, new_value):
|
||||||
|
s = cls.get_by_id(setting_id)
|
||||||
|
if not s:
|
||||||
|
raise ValueError(f"Setting id={setting_id} not found")
|
||||||
|
s["value"] = new_value
|
||||||
|
# Lưu vào DB
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE settings SET value=? WHERE id=?", (new_value, setting_id))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,44 @@
|
||||||
|
# gui/dialogs/login_handle_dialog.py
|
||||||
|
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton
|
||||||
|
from PyQt5.QtCore import QTimer, Qt
|
||||||
|
from gui.global_signals import global_signals
|
||||||
|
|
||||||
|
class LoginHandleDialog(QDialog):
|
||||||
|
def __init__(self, account_id=None, duration=10, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.account_id = account_id
|
||||||
|
self.duration = duration
|
||||||
|
self.elapsed = 0
|
||||||
|
|
||||||
|
self.setWindowTitle(f"Login Handle - Account {self.account_id}")
|
||||||
|
self.setFixedSize(300, 150)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
self.label = QLabel(f"Processing account_id={self.account_id}...")
|
||||||
|
self.label.setAlignment(Qt.AlignCenter)
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
|
self.progress = QProgressBar()
|
||||||
|
self.progress.setRange(0, self.duration)
|
||||||
|
layout.addWidget(self.progress)
|
||||||
|
|
||||||
|
self.btn_cancel = QPushButton("Cancel")
|
||||||
|
self.btn_cancel.clicked.connect(self.reject)
|
||||||
|
layout.addWidget(self.btn_cancel)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.timeout.connect(self._update_progress)
|
||||||
|
self.timer.start(1000)
|
||||||
|
|
||||||
|
def _update_progress(self):
|
||||||
|
self.elapsed += 1
|
||||||
|
self.progress.setValue(self.elapsed)
|
||||||
|
if self.elapsed >= self.duration:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Emit signal khi dialog đóng"""
|
||||||
|
global_signals.dialog_finished.emit(self.account_id)
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# gui/global_signals.py
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
class GlobalSignals(QObject):
|
||||||
|
listed_finished = pyqtSignal()
|
||||||
|
|
||||||
|
global_signals = GlobalSignals()
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
from PyQt5.QtWidgets import QMainWindow, QTabWidget
|
# gui/main_window.py
|
||||||
from gui.tabs.account_tab import AccountTab
|
|
||||||
|
from PyQt5.QtWidgets import QMainWindow, QTabWidget, QMessageBox
|
||||||
|
from gui.tabs.accounts.account_tab import AccountTab
|
||||||
from gui.tabs.products.product_tab import ProductTab
|
from gui.tabs.products.product_tab import ProductTab
|
||||||
from gui.tabs.import_tab import ImportTab
|
from gui.tabs.import_tab import ImportTab
|
||||||
from gui.tabs.listeds.listed_tab import ListedTab
|
from gui.tabs.listeds.listed_tab import ListedTab
|
||||||
|
from gui.tabs.settings.settings_tab import SettingsTab
|
||||||
|
from gui.global_signals import global_signals
|
||||||
|
from gui.core.login_handle_dialog import LoginHandleDialog
|
||||||
|
|
||||||
|
# Task runner
|
||||||
|
from tasks.task_runner import TaskRunner
|
||||||
|
from tasks.listed_tasks import process_all_listed # Hàm queue mới, 2 dialog
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -10,19 +19,21 @@ class MainWindow(QMainWindow):
|
||||||
self.setWindowTitle("Facebook Marketplace Manager")
|
self.setWindowTitle("Facebook Marketplace Manager")
|
||||||
self.resize(1200, 600)
|
self.resize(1200, 600)
|
||||||
|
|
||||||
# Tạo QTabWidget
|
# --- Tạo QTabWidget ---
|
||||||
self.tabs = QTabWidget()
|
self.tabs = QTabWidget()
|
||||||
self.account_tab = AccountTab()
|
self.account_tab = AccountTab()
|
||||||
self.product_tab = ProductTab()
|
self.product_tab = ProductTab()
|
||||||
self.import_tab = ImportTab()
|
self.import_tab = ImportTab()
|
||||||
self.listed_tab = ListedTab()
|
self.listed_tab = ListedTab()
|
||||||
|
self.setting_tab = SettingsTab()
|
||||||
|
|
||||||
self.tabs.addTab(self.account_tab, "Accounts")
|
self.tabs.addTab(self.account_tab, "Accounts")
|
||||||
self.tabs.addTab(self.product_tab, "Products")
|
self.tabs.addTab(self.product_tab, "Products")
|
||||||
self.tabs.addTab(self.listed_tab, "Queue Handle")
|
self.tabs.addTab(self.listed_tab, "Queue Handle")
|
||||||
self.tabs.addTab(self.import_tab, "Import Data")
|
self.tabs.addTab(self.import_tab, "Import Data")
|
||||||
|
self.tabs.addTab(self.setting_tab, "Setting")
|
||||||
|
|
||||||
# Gắn event khi tab thay đổi
|
# Gắn sự kiện khi tab thay đổi
|
||||||
self.tabs.currentChanged.connect(self.on_tab_changed)
|
self.tabs.currentChanged.connect(self.on_tab_changed)
|
||||||
|
|
||||||
self.setCentralWidget(self.tabs)
|
self.setCentralWidget(self.tabs)
|
||||||
|
|
@ -30,6 +41,39 @@ class MainWindow(QMainWindow):
|
||||||
# Khi mở app thì chỉ load tab đầu tiên (Accounts)
|
# Khi mở app thì chỉ load tab đầu tiên (Accounts)
|
||||||
self.on_tab_changed(0)
|
self.on_tab_changed(0)
|
||||||
|
|
||||||
|
# --- Kết nối signals ---
|
||||||
|
global_signals.open_login_dialog.connect(self.show_login_dialog)
|
||||||
|
global_signals.listed_finished.connect(self.on_listed_task_finished)
|
||||||
|
|
||||||
|
# --- 🔥 Khởi chạy queue background khi app mở ---
|
||||||
|
self.start_background_tasks()
|
||||||
|
|
||||||
|
# ---------------- Dialog handler ----------------
|
||||||
|
def show_login_dialog(self, account_id):
|
||||||
|
"""Mở dialog xử lý account_id, modeless để không block MainWindow"""
|
||||||
|
dialog = LoginHandleDialog(account_id=account_id)
|
||||||
|
dialog.show() # modeless
|
||||||
|
|
||||||
|
# ---------------- Background tasks ----------------
|
||||||
|
def start_background_tasks(self):
|
||||||
|
"""Khởi động các background task khi app mở."""
|
||||||
|
self.task_runner = TaskRunner()
|
||||||
|
self.task_runner.start()
|
||||||
|
|
||||||
|
# Thêm task xử lý listed pending (dùng hàm queue + 2 dialog cùng lúc)
|
||||||
|
self.task_runner.add_task(process_all_listed)
|
||||||
|
print("[App] Background task runner started")
|
||||||
|
|
||||||
|
# ---------------- Listed finished ----------------
|
||||||
|
def on_listed_task_finished(self):
|
||||||
|
"""Hiển thị thông báo khi listed task hoàn tất"""
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Auto Listing",
|
||||||
|
"All pending listed items have been processed!"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------- Tab handling ----------------
|
||||||
def on_tab_changed(self, index):
|
def on_tab_changed(self, index):
|
||||||
"""Chỉ load nội dung tab khi được active."""
|
"""Chỉ load nội dung tab khi được active."""
|
||||||
tab = self.tabs.widget(index)
|
tab = self.tabs.widget(index)
|
||||||
|
|
|
||||||
|
|
@ -7,65 +7,10 @@ from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
from PyQt5.QtWidgets import QHeaderView
|
from PyQt5.QtWidgets import QHeaderView
|
||||||
from database.models import Account
|
from database.models import Account
|
||||||
|
from .forms.account_form import AccountForm
|
||||||
|
|
||||||
PAGE_SIZE = 10
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
class AccountForm(QDialog):
|
|
||||||
def __init__(self, parent=None, account=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle("Account Form")
|
|
||||||
self.account = account
|
|
||||||
self.setMinimumSize(400, 200) # Form to hơn
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Email"))
|
|
||||||
self.email_input = QLineEdit()
|
|
||||||
self.email_input.setMinimumWidth(250)
|
|
||||||
layout.addWidget(self.email_input)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Password"))
|
|
||||||
self.password_input = QLineEdit()
|
|
||||||
layout.addWidget(self.password_input)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Status"))
|
|
||||||
self.active_input = QComboBox()
|
|
||||||
self.active_input.addItems(["Inactive", "Active"])
|
|
||||||
layout.addWidget(self.active_input)
|
|
||||||
|
|
||||||
btn_layout = QHBoxLayout()
|
|
||||||
self.save_btn = QPushButton("Save")
|
|
||||||
self.save_btn.setMinimumWidth(80)
|
|
||||||
self.save_btn.clicked.connect(self.save)
|
|
||||||
btn_layout.addWidget(self.save_btn)
|
|
||||||
|
|
||||||
self.cancel_btn = QPushButton("Cancel")
|
|
||||||
self.cancel_btn.setMinimumWidth(80)
|
|
||||||
self.cancel_btn.clicked.connect(self.close)
|
|
||||||
btn_layout.addWidget(self.cancel_btn)
|
|
||||||
|
|
||||||
layout.addLayout(btn_layout)
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
# nếu edit account
|
|
||||||
if account:
|
|
||||||
self.email_input.setText(account.get("email", ""))
|
|
||||||
self.password_input.setText(account.get("password", ""))
|
|
||||||
self.active_input.setCurrentText("Active" if account.get("is_active", 1) == 1 else "Inactive")
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
email = self.email_input.text()
|
|
||||||
password = self.password_input.text()
|
|
||||||
is_active = 1 if self.active_input.currentText() == "Active" else 0
|
|
||||||
try:
|
|
||||||
if self.account and "id" in self.account:
|
|
||||||
Account.update(self.account["id"], email, password, is_active)
|
|
||||||
else:
|
|
||||||
Account.create(email, password, is_active)
|
|
||||||
self.accept()
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.warning(self, "Error", str(e))
|
|
||||||
|
|
||||||
|
|
||||||
class AccountTab(QWidget):
|
class AccountTab(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QComboBox,
|
||||||
|
QPushButton, QMessageBox
|
||||||
|
)
|
||||||
|
from database.models.account import Account
|
||||||
|
|
||||||
|
class AccountForm(QDialog):
|
||||||
|
def __init__(self, parent=None, account=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Account Form")
|
||||||
|
self.account = account
|
||||||
|
self.setMinimumSize(400, 200)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# --- Email ---
|
||||||
|
layout.addWidget(QLabel("Email"))
|
||||||
|
self.email_input = QLineEdit()
|
||||||
|
self.email_input.setMinimumWidth(250)
|
||||||
|
layout.addWidget(self.email_input)
|
||||||
|
|
||||||
|
# --- Password + toggle button ---
|
||||||
|
layout.addWidget(QLabel("Password"))
|
||||||
|
pw_layout = QHBoxLayout()
|
||||||
|
self.password_input = QLineEdit()
|
||||||
|
self.password_input.setEchoMode(QLineEdit.Password)
|
||||||
|
self.password_input.setMinimumWidth(200)
|
||||||
|
|
||||||
|
self.toggle_btn = QPushButton("Show")
|
||||||
|
self.toggle_btn.setCheckable(True)
|
||||||
|
self.toggle_btn.setMaximumWidth(80)
|
||||||
|
self.toggle_btn.clicked.connect(self.toggle_password)
|
||||||
|
|
||||||
|
pw_layout.addWidget(self.password_input)
|
||||||
|
pw_layout.addWidget(self.toggle_btn)
|
||||||
|
layout.addLayout(pw_layout)
|
||||||
|
|
||||||
|
# --- Status ---
|
||||||
|
layout.addWidget(QLabel("Status"))
|
||||||
|
self.active_input = QComboBox()
|
||||||
|
self.active_input.addItems(["Inactive", "Active"])
|
||||||
|
layout.addWidget(self.active_input)
|
||||||
|
|
||||||
|
# --- Buttons ---
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
self.save_btn = QPushButton("Save")
|
||||||
|
self.save_btn.setMinimumWidth(80)
|
||||||
|
self.save_btn.clicked.connect(self.save)
|
||||||
|
btn_layout.addWidget(self.save_btn)
|
||||||
|
|
||||||
|
self.cancel_btn = QPushButton("Cancel")
|
||||||
|
self.cancel_btn.setMinimumWidth(80)
|
||||||
|
self.cancel_btn.clicked.connect(self.close)
|
||||||
|
btn_layout.addWidget(self.cancel_btn)
|
||||||
|
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# --- Nếu edit account ---
|
||||||
|
if account:
|
||||||
|
self.email_input.setText(account.get("email", ""))
|
||||||
|
self.password_input.setText(account.get("password", ""))
|
||||||
|
self.active_input.setCurrentText("Active" if account.get("is_active", 1) == 1 else "Inactive")
|
||||||
|
|
||||||
|
def toggle_password(self):
|
||||||
|
"""Ẩn/hiện password khi nhấn nút"""
|
||||||
|
if self.toggle_btn.isChecked():
|
||||||
|
self.password_input.setEchoMode(QLineEdit.Normal)
|
||||||
|
self.toggle_btn.setText("Hide")
|
||||||
|
else:
|
||||||
|
self.password_input.setEchoMode(QLineEdit.Password)
|
||||||
|
self.toggle_btn.setText("Show")
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
email = self.email_input.text()
|
||||||
|
password = self.password_input.text()
|
||||||
|
is_active = 1 if self.active_input.currentText() == "Active" else 0
|
||||||
|
try:
|
||||||
|
if self.account and "id" in self.account:
|
||||||
|
Account.update(self.account["id"], email, password, is_active)
|
||||||
|
else:
|
||||||
|
Account.create(email, password, is_active)
|
||||||
|
self.accept()
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(self, "Error", str(e))
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QDialog, QFormLayout, QLineEdit, QDialogButtonBox,
|
||||||
|
QVBoxLayout, QWidget, QComboBox
|
||||||
|
)
|
||||||
|
|
||||||
|
class ListedFilterDialog(QDialog):
|
||||||
|
STATUS_OPTIONS = ["Any", "pending", "listed"] # ✅ thêm 'Any' để không lọc theo trạng thái
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Filter Listed")
|
||||||
|
self.setMinimumSize(400, 200)
|
||||||
|
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
form_widget = QWidget()
|
||||||
|
form_layout = QFormLayout(form_widget)
|
||||||
|
|
||||||
|
# SKU
|
||||||
|
self.sku_input = QLineEdit()
|
||||||
|
form_layout.addRow("SKU:", self.sku_input)
|
||||||
|
|
||||||
|
# Product Name
|
||||||
|
self.product_name_input = QLineEdit()
|
||||||
|
form_layout.addRow("Product Name:", self.product_name_input)
|
||||||
|
|
||||||
|
# Account Email
|
||||||
|
self.account_input = QLineEdit()
|
||||||
|
form_layout.addRow("Account Email:", self.account_input)
|
||||||
|
|
||||||
|
# Status (dropdown)
|
||||||
|
self.status_input = QComboBox()
|
||||||
|
self.status_input.addItems(self.STATUS_OPTIONS)
|
||||||
|
form_layout.addRow("Status:", self.status_input)
|
||||||
|
|
||||||
|
main_layout.addWidget(form_widget)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
self.btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
|
self.btn_box.accepted.connect(self.accept)
|
||||||
|
self.btn_box.rejected.connect(self.reject)
|
||||||
|
main_layout.addWidget(self.btn_box)
|
||||||
|
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
def get_filters(self):
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
sku = self.sku_input.text().strip()
|
||||||
|
if sku:
|
||||||
|
filters["sku"] = sku
|
||||||
|
|
||||||
|
product_name = self.product_name_input.text().strip()
|
||||||
|
if product_name:
|
||||||
|
filters["product_name"] = product_name
|
||||||
|
|
||||||
|
account = self.account_input.text().strip()
|
||||||
|
if account:
|
||||||
|
filters["account_email"] = account
|
||||||
|
|
||||||
|
status = self.status_input.currentText()
|
||||||
|
if status != "Any":
|
||||||
|
filters["status"] = status
|
||||||
|
|
||||||
|
return filters
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLineEdit, QPushButton
|
|
||||||
|
|
||||||
class ListedFilterForm(QWidget):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.layout = QHBoxLayout()
|
|
||||||
self.setLayout(self.layout)
|
|
||||||
|
|
||||||
# SKU
|
|
||||||
self.sku_filter = QLineEdit()
|
|
||||||
self.sku_filter.setPlaceholderText("SKU")
|
|
||||||
self.layout.addWidget(self.sku_filter)
|
|
||||||
|
|
||||||
# Product Name
|
|
||||||
self.name_filter = QLineEdit()
|
|
||||||
self.name_filter.setPlaceholderText("Product Name")
|
|
||||||
self.layout.addWidget(self.name_filter)
|
|
||||||
|
|
||||||
# Account Email
|
|
||||||
self.account_filter = QLineEdit()
|
|
||||||
self.account_filter.setPlaceholderText("Account")
|
|
||||||
self.layout.addWidget(self.account_filter)
|
|
||||||
|
|
||||||
# Status
|
|
||||||
self.status_filter = QLineEdit()
|
|
||||||
self.status_filter.setPlaceholderText("Status")
|
|
||||||
self.layout.addWidget(self.status_filter)
|
|
||||||
|
|
||||||
# Apply Filter button
|
|
||||||
self.apply_filter_btn = QPushButton("Apply Filter")
|
|
||||||
self.layout.addWidget(self.apply_filter_btn)
|
|
||||||
|
|
||||||
def get_filters(self):
|
|
||||||
filters = {}
|
|
||||||
if sku := self.sku_filter.text().strip():
|
|
||||||
filters["sku"] = sku
|
|
||||||
if name := self.name_filter.text().strip():
|
|
||||||
filters["product_name"] = name
|
|
||||||
if account := self.account_filter.text().strip():
|
|
||||||
filters["account_email"] = account
|
|
||||||
if status := self.status_filter.text().strip():
|
|
||||||
filters["status"] = status
|
|
||||||
return filters
|
|
||||||
|
|
@ -3,14 +3,15 @@ import json
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
||||||
QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView,
|
QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView,
|
||||||
QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton, QStyle, QStylePainter, QLineEdit
|
QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton,
|
||||||
|
QStyle, QStylePainter, QLineEdit, QProgressBar
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QRect, pyqtSignal
|
from PyQt5.QtCore import Qt, QRect, pyqtSignal, QTimer
|
||||||
from services.core.loading_service import run_with_progress
|
from services.core.loading_service import run_with_progress
|
||||||
from services.image_service import ImageService
|
from services.image_service import ImageService
|
||||||
from database.models.listed import Listed
|
from database.models.listed import Listed
|
||||||
from gui.tabs.listed.dialogs.listed_filter_dialog import ListedFilterDialog
|
from database.models.setting import Setting
|
||||||
from gui.tabs.listed.dialogs.add_listed_dialog import AddListedDialog
|
from gui.tabs.listeds.dialogs.listed_filter_dialog import ListedFilterDialog
|
||||||
|
|
||||||
PAGE_SIZE = 10
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
@ -64,13 +65,27 @@ class ListedTab(QWidget):
|
||||||
|
|
||||||
# Top menu
|
# Top menu
|
||||||
top_layout = QHBoxLayout()
|
top_layout = QHBoxLayout()
|
||||||
top_layout.addItem(QLabel()) # spacer
|
top_layout.addStretch() # placeholder stretch
|
||||||
|
|
||||||
|
# --- Progress bar ---
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setMinimum(0)
|
||||||
|
self.progress_bar.setMaximum(100)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
self.progress_bar.setTextVisible(True)
|
||||||
|
self.progress_bar.setMinimumWidth(150)
|
||||||
|
self.progress_bar.setMinimumHeight(25) # thêm chiều cao đủ lớn
|
||||||
|
self.progress_bar.setAlignment(Qt.AlignCenter) # căn giữa text
|
||||||
|
top_layout.insertWidget(0, self.progress_bar)
|
||||||
|
top_layout.addStretch() # đẩy Action button sang phải
|
||||||
|
|
||||||
|
# --- Action button ---
|
||||||
self.options_btn = QPushButton("Action")
|
self.options_btn = QPushButton("Action")
|
||||||
self.options_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
self.options_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
self.options_btn.setMinimumWidth(50)
|
self.options_btn.setMinimumWidth(50)
|
||||||
self.options_btn.setMaximumWidth(120)
|
self.options_btn.setMaximumWidth(120)
|
||||||
top_layout.addWidget(self.options_btn)
|
top_layout.addWidget(self.options_btn)
|
||||||
|
|
||||||
layout.addLayout(top_layout)
|
layout.addLayout(top_layout)
|
||||||
|
|
||||||
# Table
|
# Table
|
||||||
|
|
@ -106,6 +121,11 @@ class ListedTab(QWidget):
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# --- Timer để update progress ---
|
||||||
|
self.listing_timer = QTimer()
|
||||||
|
self.listing_timer.timeout.connect(self.update_listed_progress)
|
||||||
|
self.listing_timer.start(1000) # mỗi giây update
|
||||||
|
|
||||||
# --- Load Data ---
|
# --- Load Data ---
|
||||||
def load_data(self, show_progress=True):
|
def load_data(self, show_progress=True):
|
||||||
self.table.clearContents()
|
self.table.clearContents()
|
||||||
|
|
@ -204,10 +224,12 @@ class ListedTab(QWidget):
|
||||||
header.viewport().update()
|
header.viewport().update()
|
||||||
|
|
||||||
self.update_options_menu()
|
self.update_options_menu()
|
||||||
|
self.update_listed_progress()
|
||||||
|
|
||||||
# --- Options Menu ---
|
# --- Options Menu ---
|
||||||
def update_options_menu(self):
|
def update_options_menu(self):
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
|
|
||||||
# Reload
|
# Reload
|
||||||
action_reload = QAction("Reload", menu)
|
action_reload = QAction("Reload", menu)
|
||||||
action_reload.triggered.connect(lambda: self.load_data(show_progress=True))
|
action_reload.triggered.connect(lambda: self.load_data(show_progress=True))
|
||||||
|
|
@ -224,6 +246,37 @@ class ListedTab(QWidget):
|
||||||
action_clear.triggered.connect(self.clear_filters)
|
action_clear.triggered.connect(self.clear_filters)
|
||||||
menu.addAction(action_clear)
|
menu.addAction(action_clear)
|
||||||
|
|
||||||
|
# Toggle Auto Listing
|
||||||
|
auto_setting = Setting.get_by_key(Setting.AUTO_LISTING)
|
||||||
|
if auto_setting:
|
||||||
|
setting_id = auto_setting["id"]
|
||||||
|
auto_listing_val = auto_setting["value"].lower() == "true"
|
||||||
|
else:
|
||||||
|
auto_setting = Setting.create(Setting.AUTO_LISTING, "false")
|
||||||
|
setting_id = auto_setting["id"]
|
||||||
|
auto_listing_val = False
|
||||||
|
|
||||||
|
action_toggle_auto = QAction(
|
||||||
|
"Turn Auto Listing OFF" if auto_listing_val else "Turn Auto Listing ON",
|
||||||
|
menu
|
||||||
|
)
|
||||||
|
|
||||||
|
def toggle_auto_listing():
|
||||||
|
new_val = "false" if auto_listing_val else "true"
|
||||||
|
try:
|
||||||
|
Setting.update_value(setting_id, new_val)
|
||||||
|
QMessageBox.information(
|
||||||
|
self, "Auto Listing",
|
||||||
|
f"AUTO_LISTING set to {new_val.upper()}"
|
||||||
|
)
|
||||||
|
self.update_options_menu()
|
||||||
|
self.update_listed_progress()
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Error", f"Failed to update AUTO_LISTING: {e}")
|
||||||
|
|
||||||
|
action_toggle_auto.triggered.connect(toggle_auto_listing)
|
||||||
|
menu.addAction(action_toggle_auto)
|
||||||
|
|
||||||
# Delete Selected
|
# Delete Selected
|
||||||
if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked()
|
if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked()
|
||||||
for i in range(self.table.rowCount())):
|
for i in range(self.table.rowCount())):
|
||||||
|
|
@ -233,6 +286,32 @@ class ListedTab(QWidget):
|
||||||
|
|
||||||
self.options_btn.setMenu(menu)
|
self.options_btn.setMenu(menu)
|
||||||
|
|
||||||
|
def update_listed_progress(self):
|
||||||
|
auto_listing_val = Setting.get(Setting.AUTO_LISTING, "false").lower() == "true"
|
||||||
|
self.progress_bar.setVisible(auto_listing_val)
|
||||||
|
if not auto_listing_val:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
total = Listed.count_all()
|
||||||
|
listed = Listed.count_by_status("listed")
|
||||||
|
|
||||||
|
# tránh total=0
|
||||||
|
max_value = total if total > 0 else 1
|
||||||
|
self.progress_bar.setMaximum(max_value)
|
||||||
|
self.progress_bar.setValue(listed if listed <= max_value else max_value)
|
||||||
|
|
||||||
|
# Bắt buộc bật text
|
||||||
|
self.progress_bar.setTextVisible(True)
|
||||||
|
percent = int((listed / max_value) * 100)
|
||||||
|
self.progress_bar.setFormat(f"{listed}/{total} ({percent}%) listed")
|
||||||
|
except Exception:
|
||||||
|
self.progress_bar.setMaximum(1)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
self.progress_bar.setTextVisible(True)
|
||||||
|
self.progress_bar.setFormat("0/0 listed")
|
||||||
|
|
||||||
|
|
||||||
# --- Filter ---
|
# --- Filter ---
|
||||||
def open_filter_dialog(self):
|
def open_filter_dialog(self):
|
||||||
dialog = ListedFilterDialog(self)
|
dialog = ListedFilterDialog(self)
|
||||||
|
|
@ -273,6 +352,18 @@ class ListedTab(QWidget):
|
||||||
if isinstance(cb, QCheckBox):
|
if isinstance(cb, QCheckBox):
|
||||||
cb.setChecked(checked)
|
cb.setChecked(checked)
|
||||||
|
|
||||||
|
# --- Sorting ---
|
||||||
|
def handle_header_click(self, index):
|
||||||
|
if index in self.SORTABLE_COLUMNS:
|
||||||
|
column = self.SORTABLE_COLUMNS[index]
|
||||||
|
if self.sort_by == column:
|
||||||
|
self.sort_order = "DESC" if self.sort_order == "ASC" else "ASC"
|
||||||
|
else:
|
||||||
|
self.sort_by = column
|
||||||
|
self.sort_order = "ASC"
|
||||||
|
self.current_page = 0
|
||||||
|
self.load_data()
|
||||||
|
|
||||||
# --- Delete ---
|
# --- Delete ---
|
||||||
def delete_selected(self):
|
def delete_selected(self):
|
||||||
ids = [int(cb.property("listed_id")) for i in range(self.table.rowCount())
|
ids = [int(cb.property("listed_id")) for i in range(self.table.rowCount())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QDialog, QVBoxLayout, QLabel, QLineEdit,
|
||||||
|
QComboBox, QDialogButtonBox, QMessageBox
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QIntValidator
|
||||||
|
|
||||||
|
class SettingForm(QDialog):
|
||||||
|
def __init__(self, key: str, value: str, type_: str = "text", parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle(f"Edit Setting '{key}'")
|
||||||
|
self.key = key
|
||||||
|
self.old_value = value
|
||||||
|
self.new_value = None
|
||||||
|
self.type_ = type_
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
# --- Label ---
|
||||||
|
lbl = QLabel(f"Update value for '{key}':")
|
||||||
|
self.layout.addWidget(lbl)
|
||||||
|
|
||||||
|
# --- Input Widget ---
|
||||||
|
self.input_widget = None
|
||||||
|
val_lower = (value or "").lower()
|
||||||
|
|
||||||
|
if self.type_ == "boolean" or val_lower in ("true", "false"):
|
||||||
|
self.input_widget = QComboBox()
|
||||||
|
self.input_widget.addItems(["true", "false"])
|
||||||
|
self.input_widget.setCurrentText(val_lower if val_lower in ("true", "false") else "true")
|
||||||
|
else:
|
||||||
|
self.input_widget = QLineEdit()
|
||||||
|
self.input_widget.setText(value or "")
|
||||||
|
if self.type_ == "number":
|
||||||
|
self.input_widget.setValidator(QIntValidator())
|
||||||
|
|
||||||
|
self.layout.addWidget(self.input_widget)
|
||||||
|
|
||||||
|
# --- Buttons ---
|
||||||
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
|
buttons.accepted.connect(self.on_accept)
|
||||||
|
buttons.rejected.connect(self.reject)
|
||||||
|
self.layout.addWidget(buttons)
|
||||||
|
|
||||||
|
def on_accept(self):
|
||||||
|
if isinstance(self.input_widget, QLineEdit):
|
||||||
|
val = self.input_widget.text()
|
||||||
|
if self.type_ == "number":
|
||||||
|
if not val.isdigit():
|
||||||
|
QMessageBox.warning(self, "Invalid input", "Please enter a valid number.")
|
||||||
|
return
|
||||||
|
elif isinstance(self.input_widget, QComboBox):
|
||||||
|
val = self.input_widget.currentText()
|
||||||
|
else:
|
||||||
|
val = self.old_value
|
||||||
|
|
||||||
|
if val == self.old_value:
|
||||||
|
self.reject()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.new_value = val
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_new_value(key: str, value: str, type_: str = "text", parent=None):
|
||||||
|
dialog = SettingForm(key, value, type_=type_, parent=parent)
|
||||||
|
result = dialog.exec_()
|
||||||
|
if result == QDialog.Accepted:
|
||||||
|
return dialog.new_value
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
from functools import partial
|
||||||
|
import json
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
||||||
|
QHBoxLayout, QLabel, QPushButton, QMessageBox, QMenu,
|
||||||
|
QAction, QHeaderView, QSizePolicy, QLineEdit
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from services.core.loading_service import run_with_progress
|
||||||
|
from database.models.setting import Setting
|
||||||
|
from .forms.setting_form import SettingForm
|
||||||
|
|
||||||
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
|
# --- Settings Tab ---
|
||||||
|
class SettingsTab(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.current_page = 0
|
||||||
|
self.total_count = 0
|
||||||
|
self.total_pages = 0
|
||||||
|
self.sort_by = "key"
|
||||||
|
self.sort_order = "ASC"
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Top Layout
|
||||||
|
top_layout = QHBoxLayout()
|
||||||
|
top_layout.addStretch()
|
||||||
|
layout.addLayout(top_layout)
|
||||||
|
|
||||||
|
# Table
|
||||||
|
self.table = QTableWidget()
|
||||||
|
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
|
||||||
|
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# --- Load Data ---
|
||||||
|
def load_data(self, show_progress=True):
|
||||||
|
self.table.clearContents()
|
||||||
|
self.table.setRowCount(0)
|
||||||
|
|
||||||
|
offset = self.current_page * PAGE_SIZE
|
||||||
|
settings, total_count = Setting.get_paginated(
|
||||||
|
offset, PAGE_SIZE, 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(4)
|
||||||
|
self.table.setHorizontalHeaderLabels(["ID", "Key", "Value", "Action"])
|
||||||
|
self.table.setRowCount(len(settings))
|
||||||
|
|
||||||
|
def handler(s, i_row):
|
||||||
|
setting_id = s.get("id")
|
||||||
|
key = s.get("key")
|
||||||
|
value = s.get("value") or ""
|
||||||
|
type_ = s.get("type") or "text"
|
||||||
|
|
||||||
|
# ID
|
||||||
|
id_item = QTableWidgetItem(str(setting_id))
|
||||||
|
id_item.setFlags(id_item.flags() & ~Qt.ItemIsEditable)
|
||||||
|
self.table.setItem(i_row, 0, id_item)
|
||||||
|
|
||||||
|
# Key
|
||||||
|
key_item = QTableWidgetItem(key)
|
||||||
|
key_item.setFlags(key_item.flags() & ~Qt.ItemIsEditable)
|
||||||
|
self.table.setItem(i_row, 1, key_item)
|
||||||
|
|
||||||
|
# Value
|
||||||
|
value_item = QTableWidgetItem(value)
|
||||||
|
self.table.setItem(i_row, 2, value_item)
|
||||||
|
|
||||||
|
# Action
|
||||||
|
btn_menu = QPushButton("Action")
|
||||||
|
btn_menu.setMaximumWidth(120)
|
||||||
|
menu = QMenu(btn_menu)
|
||||||
|
act_edit = menu.addAction("Edit")
|
||||||
|
act_edit.triggered.connect(partial(self.edit_setting, setting_id, key, i_row, type_))
|
||||||
|
btn_menu.setMenu(menu)
|
||||||
|
self.table.setCellWidget(i_row, 3, btn_menu)
|
||||||
|
|
||||||
|
items_with_index = [(s, i) for i, s in enumerate(settings)]
|
||||||
|
if show_progress:
|
||||||
|
run_with_progress(items_with_index, handler=lambda x: handler(*x), message="Loading settings...", parent=self)
|
||||||
|
else:
|
||||||
|
for item in items_with_index:
|
||||||
|
handler(*item)
|
||||||
|
|
||||||
|
# Resize header
|
||||||
|
header = self.table.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.Stretch) # Key
|
||||||
|
header.setSectionResizeMode(2, QHeaderView.Stretch) # Value
|
||||||
|
header.setSectionResizeMode(3, QHeaderView.Fixed) # Action
|
||||||
|
self.table.setColumnWidth(3, 120)
|
||||||
|
|
||||||
|
# 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)")
|
||||||
|
|
||||||
|
# --- Edit setting ---
|
||||||
|
def edit_setting(self, setting_id, key, row, type_):
|
||||||
|
old_value = self.table.item(row, 2).text()
|
||||||
|
new_value = SettingForm.get_new_value(key, old_value, type_=type_, parent=self)
|
||||||
|
if new_value is not None and new_value != old_value:
|
||||||
|
try:
|
||||||
|
Setting.update_value(setting_id, new_value)
|
||||||
|
self.table.item(row, 2).setText(new_value)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Error", f"Failed to update setting: {e}")
|
||||||
|
|
||||||
|
# --- Pagination ---
|
||||||
|
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
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class LogService:
|
||||||
|
def __init__(self, log_dir="logs"):
|
||||||
|
self.log_dir = log_dir
|
||||||
|
os.makedirs(self.log_dir, exist_ok=True)
|
||||||
|
|
||||||
|
log_filename = os.path.join(self.log_dir, f"{datetime.now().strftime('%Y-%m-%d')}.log")
|
||||||
|
|
||||||
|
# Tạo logger
|
||||||
|
self.logger = logging.getLogger("TaskLogger")
|
||||||
|
self.logger.setLevel(logging.INFO)
|
||||||
|
self.logger.propagate = False
|
||||||
|
|
||||||
|
# Xóa handler cũ để tránh duplicate log khi import nhiều lần
|
||||||
|
if self.logger.handlers:
|
||||||
|
self.logger.handlers.clear()
|
||||||
|
|
||||||
|
# Ghi file
|
||||||
|
file_handler = logging.FileHandler(log_filename, encoding="utf-8")
|
||||||
|
file_handler.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# In ra console
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Format log
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
self.logger.addHandler(file_handler)
|
||||||
|
self.logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
def info(self, message: str):
|
||||||
|
self.logger.info(message)
|
||||||
|
|
||||||
|
def error(self, message: str):
|
||||||
|
self.logger.error(message)
|
||||||
|
|
||||||
|
def warning(self, message: str):
|
||||||
|
self.logger.warning(message)
|
||||||
|
|
||||||
|
# Singleton instance dùng chung toàn app
|
||||||
|
log_service = LogService()
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
# tasks/listed_tasks.py
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
from database.db import get_connection
|
||||||
|
from services.core.log_service import log_service
|
||||||
|
from gui.global_signals import global_signals
|
||||||
|
|
||||||
|
stop_event = threading.Event()
|
||||||
|
dialog_semaphore = None # sẽ khởi tạo dựa trên setting
|
||||||
|
|
||||||
|
|
||||||
|
def stop_listed_task():
|
||||||
|
"""Dừng toàn bộ task"""
|
||||||
|
stop_event.set()
|
||||||
|
log_service.info("[Task] Stop signal sent")
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
"""Lấy setting từ DB"""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT key, value FROM settings")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return {row["key"]: row["value"] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def get_pending_items(limit=1000):
|
||||||
|
"""Lấy danh sách pending listed items"""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT l.id, l.account_id, l.product_id, l.listed_at, l.status
|
||||||
|
FROM listed l
|
||||||
|
JOIN accounts a ON l.account_id = a.id
|
||||||
|
WHERE l.status='pending'
|
||||||
|
AND a.is_active=1
|
||||||
|
ORDER BY l.listed_at ASC
|
||||||
|
LIMIT {limit}
|
||||||
|
""")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def process_item(row):
|
||||||
|
"""Xử lý 1 item: mở dialog trên main thread và update DB"""
|
||||||
|
listed_id, account_id, product_id, listed_at, status = row
|
||||||
|
log_service.info(f"[Task] Processing listed_id={listed_id}")
|
||||||
|
|
||||||
|
if stop_event.is_set():
|
||||||
|
log_service.info("[Task] Stop signal received, skip item")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Đợi semaphore để giới hạn số dialog ---
|
||||||
|
dialog_semaphore.acquire()
|
||||||
|
|
||||||
|
# --- Tạo event để chờ dialog xong ---
|
||||||
|
finished_event = threading.Event()
|
||||||
|
|
||||||
|
# Callback khi dialog xong
|
||||||
|
def on_dialog_finished(finished_account_id):
|
||||||
|
if finished_account_id == account_id:
|
||||||
|
finished_event.set()
|
||||||
|
global_signals.dialog_finished.disconnect(on_dialog_finished)
|
||||||
|
dialog_semaphore.release()
|
||||||
|
|
||||||
|
global_signals.dialog_finished.connect(on_dialog_finished)
|
||||||
|
|
||||||
|
# --- Emit signal mở dialog trên main thread ---
|
||||||
|
global_signals.open_login_dialog.emit(account_id)
|
||||||
|
|
||||||
|
# --- Chờ dialog hoàn tất ---
|
||||||
|
finished_event.wait()
|
||||||
|
|
||||||
|
# --- Update DB ---
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE listed SET status='listed' WHERE id=?", (listed_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
log_service.info(f"[Task] Finished listed_id={listed_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def worker_thread(q: queue.Queue):
|
||||||
|
while not stop_event.is_set():
|
||||||
|
try:
|
||||||
|
row = q.get(timeout=1)
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
process_item(row)
|
||||||
|
except Exception as e:
|
||||||
|
log_service.error(f"[Task] Exception in worker: {e}")
|
||||||
|
finally:
|
||||||
|
q.task_done()
|
||||||
|
|
||||||
|
|
||||||
|
def process_all_listed():
|
||||||
|
"""
|
||||||
|
Entry point: tạo queue và 2 worker thread (hoặc theo setting)
|
||||||
|
"""
|
||||||
|
log_service.info("[Task] Start processing all listed items")
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
max_workers = int(settings.get("MAX_CONCURRENT_LISTING", 2))
|
||||||
|
global dialog_semaphore
|
||||||
|
dialog_semaphore = threading.Semaphore(max_workers)
|
||||||
|
|
||||||
|
# --- Lấy pending items ---
|
||||||
|
items = get_pending_items(limit=1000)
|
||||||
|
if not items:
|
||||||
|
log_service.info("[Task] No pending items")
|
||||||
|
global_signals.listed_finished.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Tạo queue ---
|
||||||
|
q = queue.Queue()
|
||||||
|
for row in items:
|
||||||
|
q.put(row)
|
||||||
|
|
||||||
|
# --- Tạo worker threads ---
|
||||||
|
threads = []
|
||||||
|
for _ in range(max_workers):
|
||||||
|
t = threading.Thread(target=worker_thread, args=(q,))
|
||||||
|
t.start()
|
||||||
|
threads.append(t)
|
||||||
|
|
||||||
|
# --- Chờ queue xong ---
|
||||||
|
q.join()
|
||||||
|
stop_event.set()
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
log_service.info("[Task] All listed items processed")
|
||||||
|
global_signals.listed_finished.emit()
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import time
|
||||||
|
|
||||||
|
class TaskRunner:
|
||||||
|
def __init__(self):
|
||||||
|
self.task_queue = queue.Queue()
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.running = True
|
||||||
|
worker = threading.Thread(target=self.run, daemon=True)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def add_task(self, func, *args, **kwargs):
|
||||||
|
"""Thêm task vào hàng đợi"""
|
||||||
|
self.task_queue.put((func, args, kwargs))
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
func, args, kwargs = self.task_queue.get(timeout=1)
|
||||||
|
func(*args, **kwargs)
|
||||||
|
self.task_queue.task_done()
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
Loading…
Reference in New Issue