#include "rulesmanagerdialog.h" #include "ruleditordialog.h" #include "db/dao/ruledao.h" #include "db/dao/common_structs.h" #include "db/dao/categorydao.h" #include "db/dao/folderdao.h" #include "services/rulesengine.h" #include "services/accountservice.h" #include #include #include #include #include #include #include #include #include #include #include #include RulesManagerDialog::RulesManagerDialog(qint64 accountId, QWidget *parent) : QDialog(parent), m_accountId(accountId) { setWindowTitle(m_accountId >= 0 ? tr("Reglas de la cuenta") : tr("Reglas globales")); resize(900, 600); setupUI(); loadRules(); } void RulesManagerDialog::setupUI() { QVBoxLayout* mainLayout = new QVBoxLayout(this); // Toolbar QHBoxLayout* toolLayout = new QHBoxLayout(); m_btnNew = new QPushButton(tr("Nueva regla")); m_btnRun = new QPushButton(tr("Ejecutar ahora")); m_btnImport = new QPushButton(tr("Importar")); m_btnExport = new QPushButton(tr("Exportar")); m_btnClose = new QPushButton(tr("Cerrar")); m_btnNew->setStyleSheet("QPushButton { background: #1976D2; color: white; font-weight: bold; padding: 6px 12px; border-radius: 4px; } QPushButton:hover { background: #1565C0; }"); m_btnRun->setStyleSheet("QPushButton { background: #4CAF50; color: white; padding: 6px 12px; border-radius: 4px; } QPushButton:hover { background: #43A047; }"); toolLayout->addWidget(m_btnNew); toolLayout->addWidget(m_btnRun); toolLayout->addStretch(); toolLayout->addWidget(m_btnImport); toolLayout->addWidget(m_btnExport); toolLayout->addWidget(m_btnClose); mainLayout->addLayout(toolLayout); // Tree widget m_tree = new QTreeWidget(); m_tree->setHeaderLabels({tr("Activa"), tr("Nombre"), tr("Prioridad"), tr("Condiciones"), tr("Acciones"), tr("Cuenta"), tr("Última ejecución"), tr("Veces")}); m_tree->setAlternatingRowColors(true); m_tree->setSelectionBehavior(QAbstractItemView::SelectRows); m_tree->setSelectionMode(QAbstractItemView::SingleSelection); m_tree->setContextMenuPolicy(Qt::CustomContextMenu); m_tree->header()->setStretchLastSection(false); m_tree->header()->setSectionResizeMode(1, QHeaderView::Stretch); m_tree->setColumnWidth(0, 60); m_tree->setColumnWidth(2, 80); m_tree->setColumnWidth(3, 200); m_tree->setColumnWidth(4, 200); m_tree->setColumnWidth(5, 100); m_tree->setColumnWidth(6, 140); m_tree->setColumnWidth(7, 60); connect(m_tree, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) { QTreeWidgetItem* item = m_tree->itemAt(pos); if (!item) return; QMenu menu(this); QAction* editAct = menu.addAction(tr("Editar"), [this, item]() { onEditRule(item); }); QAction* delAct = menu.addAction(tr("Eliminar"), [this, item]() { onDeleteRule(item); }); menu.addSeparator(); QAction* toggleAct = menu.addAction(item->checkState(0) == Qt::Checked ? tr("Desactivar") : tr("Activar"), [this, item]() { onToggleEnabled(item); }); menu.addSeparator(); QAction* moveUpAct = menu.addAction(tr("Subir prioridad"), [this, item]() { int idx = m_tree->indexOfTopLevelItem(item); if (idx > 0) { QTreeWidgetItem* prev = m_tree->takeTopLevelItem(idx); m_tree->insertTopLevelItem(idx - 1, prev); persistPriorityOrder(); } }); QAction* moveDownAct = menu.addAction(tr("Bajar prioridad"), [this, item]() { int idx = m_tree->indexOfTopLevelItem(item); if (idx < m_tree->topLevelItemCount() - 1) { QTreeWidgetItem* next = m_tree->takeTopLevelItem(idx + 1); m_tree->insertTopLevelItem(idx + 1, item); persistPriorityOrder(); } }); menu.exec(m_tree->viewport()->mapToGlobal(pos)); }); connect(m_tree, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* item) { onEditRule(item); }); mainLayout->addWidget(m_tree, 1); // Status QLabel* statusLabel = new QLabel(tr("Doble click para editar. Click derecho para más opciones.")); statusLabel->setStyleSheet("color: #666; font-size: 11px; padding: 4px;"); mainLayout->addWidget(statusLabel); // Connect buttons connect(m_btnNew, &QPushButton::clicked, this, &RulesManagerDialog::onNewRule); connect(m_btnRun, &QPushButton::clicked, this, &RulesManagerDialog::onRunRules); connect(m_btnImport, &QPushButton::clicked, this, [this]() { onImportExport(true); }); connect(m_btnExport, &QPushButton::clicked, this, [this]() { onImportExport(false); }); connect(m_btnClose, &QPushButton::clicked, this, &QDialog::accept); } void RulesManagerDialog::loadRules() { m_tree->clear(); QVector rules = RuleDao::findByAccount(m_accountId); for (const Rule& rule : rules) { addRuleToTree(rule); } } void RulesManagerDialog::addRuleToTree(const Rule& rule) { QTreeWidgetItem* item = new QTreeWidgetItem(m_tree); item->setData(0, Qt::UserRole, rule.id); item->setCheckState(0, rule.enabled ? Qt::Checked : Qt::Unchecked); item->setText(1, rule.name); item->setText(2, QString::number(rule.priority)); // Conditions summary QStringList condTexts; for (const RuleCondition& cond : rule.conditions) { QString fieldName; switch (cond.field) { case RuleCondition::From: fieldName = "De"; break; case RuleCondition::To: fieldName = "Para"; break; case RuleCondition::Cc: fieldName = "CC"; break; case RuleCondition::Subject: fieldName = "Asunto"; break; case RuleCondition::Body: fieldName = "Cuerpo"; break; case RuleCondition::HasAttachment: fieldName = "Tiene adjuntos"; break; case RuleCondition::Size: fieldName = "Tamaño"; break; case RuleCondition::Date: fieldName = "Fecha"; break; case RuleCondition::Flagged: fieldName = "Marcado"; break; case RuleCondition::Read: fieldName = "Leído"; break; } QString opName; switch (cond.op) { case RuleCondition::Contains: opName = "contiene"; break; case RuleCondition::NotContains: opName = "no contiene"; break; case RuleCondition::Equals: opName = "es"; break; case RuleCondition::NotEquals: opName = "no es"; break; case RuleCondition::StartsWith: opName = "empieza por"; break; case RuleCondition::EndsWith: opName = "termina en"; break; case RuleCondition::Regex: opName = "regex"; break; case RuleCondition::GreaterThan: opName = ">"; break; case RuleCondition::LessThan: opName = "<"; break; case RuleCondition::Before: opName = "antes de"; break; case RuleCondition::After: opName = "después de"; break; } condTexts.append(QString("%1 %2 \"%3\"").arg(fieldName, opName, cond.value)); } item->setText(3, condTexts.join(rule.matchAll ? " Y " : " O ")); // Actions summary QStringList actTexts; for (const RuleAction& act : rule.actions) { QString actName; switch (act.type) { case RuleAction::MoveToFolder: actName = QString("Mover a carpeta %1").arg(act.value); break; case RuleAction::MarkAsRead: actName = QString("Marcar como %1").arg(act.value == "false" ? "no leído" : "leído"); break; case RuleAction::MarkAsFlagged: actName = QString("Marcar como %1").arg(act.value == "false" ? "no marcado" : "marcado"); break; case RuleAction::Delete: actName = "Eliminar"; break; case RuleAction::AssignCategory: actName = QString("Categoría %1").arg(act.value); break; case RuleAction::RemoveCategory: actName = QString("Quitar categoría %1").arg(act.value); break; case RuleAction::ForwardTo: actName = QString("Reenviar a %1").arg(act.value); break; case RuleAction::SetPriority: actName = QString("Prioridad %1").arg(act.value); break; case RuleAction::StopProcessing: actName = "Detener procesamiento"; break; } actTexts.append(actName); } item->setText(4, actTexts.join(", ")); item->setText(5, rule.isGlobal() ? tr("Global") : QString::number(rule.accountId)); item->setText(6, rule.lastRun.isValid() ? rule.lastRun.toString("dd/MM/yyyy hh:mm") : tr("Nunca")); item->setText(7, QString::number(rule.runCount)); if (!rule.enabled) { for (int i = 0; i < m_tree->columnCount(); ++i) { item->setForeground(i, QBrush(QColor("#999"))); } } } Rule RulesManagerDialog::itemToRule(QTreeWidgetItem* item) const { qint64 id = item->data(0, Qt::UserRole).toLongLong(); auto ruleOpt = RuleDao::findById(id); return ruleOpt.value_or(Rule()); } void RulesManagerDialog::onNewRule() { RuleEditorDialog dlg(Rule(), m_accountId, this); connect(&dlg, &RuleEditorDialog::ruleSaved, this, &RulesManagerDialog::onRuleSaved); dlg.exec(); } void RulesManagerDialog::onEditRule(QTreeWidgetItem* item) { Rule rule = itemToRule(item); if (!rule.isValid()) return; RuleEditorDialog dlg(rule, m_accountId, this); connect(&dlg, &RuleEditorDialog::ruleSaved, this, &RulesManagerDialog::onRuleSaved); dlg.exec(); } void RulesManagerDialog::onRuleSaved(const Rule& rule) { refresh(); emit rulesChanged(); } void RulesManagerDialog::onDeleteRule(QTreeWidgetItem* item) { Rule rule = itemToRule(item); if (!rule.isValid()) return; if (QMessageBox::question(this, tr("Eliminar regla"), tr("¿Eliminar la regla \"%1\"?").arg(rule.name), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { if (RuleDao::remove(rule.id)) { refresh(); emit rulesChanged(); } } } void RulesManagerDialog::onToggleEnabled(QTreeWidgetItem* item) { Rule rule = itemToRule(item); if (!rule.isValid()) return; bool newState = item->checkState(0) != Qt::Checked; if (RuleDao::setEnabled(rule.id, newState)) { item->setCheckState(0, newState ? Qt::Checked : Qt::Unchecked); if (!newState) { for (int i = 0; i < m_tree->columnCount(); ++i) { item->setForeground(i, QBrush(QColor("#999"))); } } else { for (int i = 0; i < m_tree->columnCount(); ++i) { item->setForeground(i, QBrush(QColor("#000"))); } } emit rulesChanged(); } } void RulesManagerDialog::onRunRules() { // TODO: Get selected folder/account from main window QMessageBox::information(this, tr("Ejecutar reglas"), tr("Seleccione una carpeta en la ventana principal y pulse 'Ejecutar reglas' en el menú de herramientas.")); } void RulesManagerDialog::onImportExport(bool import) { if (import) { QString file = QFileDialog::getOpenFileName(this, tr("Importar reglas"), QString(), tr("JSON (*.json)")); if (file.isEmpty()) return; QFile f(file); if (!f.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, tr("Importar"), tr("No se pudo abrir el archivo.")); return; } QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(f.readAll(), &err); if (err.error != QJsonParseError::NoError || !doc.isArray()) { QMessageBox::warning(this, tr("Importar"), tr("Archivo JSON inválido.")); return; } QJsonArray arr = doc.array(); int imported = 0; int skipped = 0; for (const QJsonValue &v : arr) { if (!v.isObject()) { ++skipped; continue; } Rule rule = Rule::fromJson(v.toObject()); if (rule.name.isEmpty()) { ++skipped; continue; } rule.accountId = m_accountId; // scope to the dialog's account (-1 = global) if (RuleDao::insert(rule)) ++imported; else ++skipped; } QMessageBox::information(this, tr("Importar"), tr("Se importaron %1 regla(s). %2 omitida(s).").arg(imported).arg(skipped)); refresh(); } else { QString file = QFileDialog::getSaveFileName(this, tr("Exportar reglas"), "rules.json", tr("JSON (*.json)")); if (file.isEmpty()) return; QVector rules = RuleDao::findByAccount(m_accountId); QJsonArray arr; for (const Rule& rule : rules) arr.append(rule.toJson()); QJsonDocument doc(arr); QFile f(file); if (f.open(QIODevice::WriteOnly)) { f.write(doc.toJson(QJsonDocument::Indented)); QMessageBox::information(this, tr("Exportar"), tr("Reglas exportadas a %1").arg(file)); } } } void RulesManagerDialog::refresh() { loadRules(); } void RulesManagerDialog::persistPriorityOrder() { // Re-assign priority based on tree order (top = lowest number = highest priority). for (int i = 0; i < m_tree->topLevelItemCount(); ++i) { QTreeWidgetItem *item = m_tree->topLevelItem(i); qint64 ruleId = item->data(0, Qt::UserRole).toLongLong(); int newPriority = i + 1; item->setText(2, QString::number(newPriority)); auto rule = RuleDao::findById(ruleId); if (rule.has_value()) { Rule r = rule.value(); r.priority = newPriority; RuleDao::update(r); } } }