799 lines
39 KiB
C++
799 lines
39 KiB
C++
#include "AccountSettingsWindow.h"
|
||
|
||
#include <QVBoxLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QHeaderView>
|
||
#include <QScrollArea>
|
||
#include <QCoreApplication>
|
||
#include <QMessageBox>
|
||
#include <QInputDialog>
|
||
#include <QThread>
|
||
#include <QTimer>
|
||
#include <QGuiApplication>
|
||
#include <QClipboard>
|
||
#include <QToolTip>
|
||
#include <QIcon>
|
||
|
||
#include "ozon/LoginAndCheckAccountsScript.h"
|
||
#include "ozon/GetProfileInfoScript.h"
|
||
#include "ozon/GetCardInfoScript.h"
|
||
#include "black/LoginAndCheckAccountsScript.h"
|
||
#include "black/GetProfileInfoScript.h"
|
||
#include "black/GetCardInfoScript.h"
|
||
#include "dushanbe/LoginAndCheckAccountsScript.h"
|
||
#include "dushanbe/GetProfileInfoScript.h"
|
||
#include "dushanbe/GetCardInfoScript.h"
|
||
#include "dao/BankProfileDAO.h"
|
||
#include "dao/MaterialDAO.h"
|
||
#include "net/NetworkService.h"
|
||
|
||
AccountSettingsWindow::AccountSettingsWindow(
|
||
QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app
|
||
) : QWidget(parent,
|
||
Qt::Window
|
||
| Qt::WindowTitleHint
|
||
| Qt::WindowSystemMenuHint
|
||
| Qt::WindowCloseButtonHint
|
||
| Qt::WindowMinMaxButtonsHint),
|
||
m_deviceId(std::move(deviceId)),
|
||
m_deviceName(std::move(deviceName)),
|
||
m_app(app) {
|
||
setWindowTitle("Настройки банка [" + m_app.name + "]");
|
||
setMinimumSize(700, 400);
|
||
resize(750, 500);
|
||
|
||
auto *mainLayout = new QVBoxLayout(this);
|
||
|
||
auto *scrollArea = new QScrollArea(this);
|
||
scrollArea->setWidgetResizable(true);
|
||
auto *content = new QWidget;
|
||
auto *layout = new QVBoxLayout(content);
|
||
layout->setContentsMargins(8, 8, 8, 8);
|
||
layout->setAlignment(Qt::AlignTop);
|
||
|
||
// ── Статус профиля ─────────────────────────────────────────────────
|
||
auto *statusRow = new QHBoxLayout;
|
||
statusRow->addWidget(new QLabel("Статус профиля:"));
|
||
m_statusLabel = new QLabel;
|
||
m_statusLabel->setTextFormat(Qt::RichText);
|
||
statusRow->addWidget(m_statusLabel);
|
||
statusRow->addStretch();
|
||
|
||
m_toggleBtn = new QPushButton;
|
||
m_toggleBtn->setFixedWidth(120);
|
||
connect(m_toggleBtn, &QPushButton::clicked, this, &AccountSettingsWindow::toggleProfileStatus);
|
||
statusRow->addWidget(m_toggleBtn);
|
||
layout->addLayout(statusRow);
|
||
|
||
// ── Пин-код ────────────────────────────────────────────────────────
|
||
auto *pinRow = new QHBoxLayout;
|
||
pinRow->addWidget(new QLabel("Пин-код:"));
|
||
m_pinLabel = new QLabel;
|
||
m_pinLabel->setTextFormat(Qt::RichText);
|
||
pinRow->addWidget(m_pinLabel);
|
||
pinRow->addStretch();
|
||
|
||
auto *editPinBtn = new QPushButton("Редактировать");
|
||
editPinBtn->setStyleSheet("background-color: green; color: white; padding: 4px 12px;");
|
||
connect(editPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::editPinCode);
|
||
pinRow->addWidget(editPinBtn);
|
||
|
||
auto *checkPinBtn = new QPushButton("Проверить пин-код");
|
||
checkPinBtn->setStyleSheet("background-color: orange; color: white; padding: 4px 12px;");
|
||
connect(checkPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startCheckPinCode);
|
||
pinRow->addWidget(checkPinBtn);
|
||
layout->addLayout(pinRow);
|
||
|
||
// ── Профиль ────────────────────────────────────────────────────────
|
||
auto *profileRow = new QHBoxLayout;
|
||
m_profileLabel = new QLabel;
|
||
m_profileLabel->setTextFormat(Qt::RichText);
|
||
m_profileLabel->setStyleSheet("margin: 4px 0;");
|
||
profileRow->addWidget(m_profileLabel);
|
||
|
||
m_copyPhoneBtn = new QPushButton;
|
||
m_copyPhoneBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png"));
|
||
m_copyPhoneBtn->setIconSize(QSize(16, 16));
|
||
m_copyPhoneBtn->setToolTip("Копировать телефон");
|
||
m_copyPhoneBtn->setFixedSize(28, 24);
|
||
m_copyPhoneBtn->setStyleSheet("padding: 2px;");
|
||
m_copyPhoneBtn->hide();
|
||
connect(m_copyPhoneBtn, &QPushButton::clicked, this, [this]() {
|
||
if (m_app.phone.isEmpty()) return;
|
||
QGuiApplication::clipboard()->setText(m_app.phone);
|
||
QToolTip::showText(m_copyPhoneBtn->mapToGlobal(QPoint(0, m_copyPhoneBtn->height())),
|
||
"Скопировано", m_copyPhoneBtn, {}, 1500);
|
||
});
|
||
profileRow->addWidget(m_copyPhoneBtn);
|
||
profileRow->addStretch();
|
||
layout->addLayout(profileRow);
|
||
|
||
// ── Кнопки обновления ──────────────────────────────────────────────
|
||
auto *actionsRow = new QHBoxLayout;
|
||
|
||
m_profileBtn = new QPushButton("Обновить профиль");
|
||
m_profileBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;");
|
||
connect(m_profileBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetProfileInfo);
|
||
actionsRow->addWidget(m_profileBtn);
|
||
|
||
m_cardsBtn = new QPushButton("Проверить аккаунты");
|
||
m_cardsBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;");
|
||
connect(m_cardsBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetCardInfo);
|
||
m_cardsBtn->hide();
|
||
actionsRow->addWidget(m_cardsBtn);
|
||
|
||
actionsRow->addStretch();
|
||
layout->addLayout(actionsRow);
|
||
|
||
// ── Таблица карт ───────────────────────────────────────────────────
|
||
layout->addSpacing(8);
|
||
layout->addWidget(new QLabel("<b>Счета</b>"));
|
||
|
||
m_cardsTable = new QTableWidget;
|
||
m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection);
|
||
m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||
m_cardsTable->setFocusPolicy(Qt::NoFocus);
|
||
layout->addWidget(m_cardsTable);
|
||
|
||
content->setLayout(layout);
|
||
scrollArea->setWidget(content);
|
||
mainLayout->addWidget(scrollArea);
|
||
|
||
// Overlay (скрыт по умолчанию)
|
||
m_overlay = new QWidget(this);
|
||
m_overlay->setStyleSheet("background-color: rgba(0, 0, 0, 150);");
|
||
m_overlay->hide();
|
||
|
||
m_overlayText = new QLabel(m_overlay);
|
||
m_overlayText->setAlignment(Qt::AlignCenter);
|
||
m_overlayText->setStyleSheet("color: white; font-size: 16px; font-weight: bold;");
|
||
auto *overlayLayout = new QVBoxLayout(m_overlay);
|
||
overlayLayout->addWidget(m_overlayText);
|
||
|
||
m_overlayTimer = new QTimer(this);
|
||
m_overlayTimer->setSingleShot(true);
|
||
|
||
updateStatusUI();
|
||
reloadCards();
|
||
}
|
||
|
||
// ── Overlay ────────────────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::resizeEvent(QResizeEvent *event) {
|
||
QWidget::resizeEvent(event);
|
||
if (m_overlay) m_overlay->setGeometry(rect());
|
||
}
|
||
|
||
void AccountSettingsWindow::showOverlay(const QString &text) {
|
||
m_overlayText->setText(text);
|
||
m_overlay->setGeometry(rect());
|
||
m_overlay->raise();
|
||
m_overlay->show();
|
||
}
|
||
|
||
void AccountSettingsWindow::hideOverlay(bool success, const QString &message, std::function<void()> onHidden) {
|
||
m_overlayText->setText(message);
|
||
m_overlayText->setStyleSheet(success
|
||
? "color: #4CAF50; font-size: 16px; font-weight: bold;"
|
||
: "color: #F44336; font-size: 16px; font-weight: bold;");
|
||
m_overlayTimer->disconnect();
|
||
connect(m_overlayTimer, &QTimer::timeout, this, [this, onHidden]() {
|
||
m_overlay->hide();
|
||
m_overlayText->setStyleSheet("color: white; font-size: 16px; font-weight: bold;");
|
||
if (onHidden) onHidden();
|
||
});
|
||
m_overlayTimer->start(1500);
|
||
}
|
||
|
||
// ── UI update ──────────────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::updateStatusUI() {
|
||
// Статус профиля
|
||
const bool active = m_app.status == "active";
|
||
m_statusLabel->setText(active
|
||
? "<span style='color:green;'>Активен</span>"
|
||
: "<span style='color:red;'>Неактивен</span>");
|
||
m_toggleBtn->setText(active ? "Выключить" : "Включить");
|
||
m_toggleBtn->setStyleSheet(active
|
||
? "background-color: red; color: white; padding: 4px 12px;"
|
||
: "background-color: green; color: white; padding: 4px 12px;");
|
||
|
||
// Пин-код
|
||
const QString pinDisplay = m_app.pinCode.isEmpty() ? "не задан" : m_app.pinCode;
|
||
const QString pinStatus = m_app.pinCodeChecked
|
||
? "<span style='color:green;'>" + pinDisplay + " (проверен)</span>"
|
||
: "<span style='color:red;'>" + pinDisplay + " (не проверен)</span>";
|
||
m_pinLabel->setText(pinStatus);
|
||
|
||
// Профиль
|
||
if (m_app.fullName.isEmpty() && m_app.phone.isEmpty()) {
|
||
m_profileLabel->setText("<span style='color:gray;'>Данные профиля не получены</span>");
|
||
} else {
|
||
QString text;
|
||
if (!m_app.fullName.isEmpty()) text += m_app.fullName;
|
||
if (!m_app.phone.isEmpty()) text += " | " + m_app.phone;
|
||
if (!m_app.email.isEmpty()) text += " | " + m_app.email;
|
||
|
||
const QString syncText = m_app.bankProfileId.isEmpty()
|
||
? " <span style='color:red;'>● Не синхронизирован</span>"
|
||
: " <span style='color:green;'>● Синхронизирован</span>";
|
||
m_profileLabel->setText(text + syncText);
|
||
}
|
||
m_copyPhoneBtn->setVisible(!m_app.phone.isEmpty());
|
||
}
|
||
|
||
// ── Toggle On/Off ──────────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::toggleProfileStatus() {
|
||
const bool isActive = m_app.status == "active";
|
||
const QString newStatus = isActive ? "off" : "active";
|
||
|
||
// Если включаем — проверяем что есть хотя бы одна активная карта
|
||
if (!isActive) {
|
||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||
bool hasActive = false;
|
||
for (const auto &m : materials) {
|
||
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
|
||
}
|
||
if (!hasActive) {
|
||
QMessageBox::warning(this, "Невозможно включить",
|
||
"Ни одна карта не активна.\nВключите хотя бы одну карту перед включением профиля.");
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, newStatus))
|
||
return;
|
||
|
||
m_app.status = newStatus;
|
||
showOverlay(isActive ? "Выключение профиля..." : "Включение профиля...");
|
||
|
||
// При включении профиля — материалы включаем ТОЛЬКО после успешного
|
||
// включения bank_profile на сервере; иначе пользователь получит подряд
|
||
// несколько одинаковых ошибок ("Invalid or disabled connection") от
|
||
// независимых PATCH'ей.
|
||
std::function<void()> onSuccess;
|
||
if (!isActive) {
|
||
const QString deviceId = m_deviceId;
|
||
const QString appCode = m_app.code;
|
||
onSuccess = [this, deviceId, appCode]() {
|
||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(deviceId, appCode);
|
||
for (const auto &mat : materials) {
|
||
if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) {
|
||
auto *thread = new QThread;
|
||
auto *net = new NetworkService;
|
||
net->moveToThread(thread);
|
||
net->addExtra("material_id", mat.materialId);
|
||
net->addExtra("enable", true);
|
||
NetworkService::connectErrorDialog(this, net);
|
||
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
|
||
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
qDebug() << "[AccountSettings] Enabling material on server:" << mat.materialId;
|
||
}
|
||
}
|
||
};
|
||
}
|
||
sendAppStatus(newStatus, std::move(onSuccess));
|
||
}
|
||
|
||
// ── Edit PIN ───────────────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::editPinCode() {
|
||
bool ok;
|
||
const QString pin = QInputDialog::getText(this, "Пин-код",
|
||
"Введите пин-код:", QLineEdit::Normal, m_app.pinCode, &ok);
|
||
if (!ok) return;
|
||
|
||
if (BankProfileDAO::update(m_app.id, pin, m_app.comment, m_app.status)) {
|
||
m_app.pinCode = pin;
|
||
m_app.pinCodeChecked = false;
|
||
BankProfileDAO::updatePinCodeStatus(m_app.id, "no", "");
|
||
updateStatusUI();
|
||
}
|
||
}
|
||
|
||
// ── Check PIN ──────────────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::startCheckPinCode() {
|
||
if (m_app.pinCode.isEmpty()) {
|
||
QMessageBox::warning(this, "Пин-код не задан",
|
||
"Сначала укажите пин-код через кнопку \"Редактировать\".");
|
||
return;
|
||
}
|
||
|
||
showOverlay("Проверка пин-кода...");
|
||
pauseActiveProfiles();
|
||
|
||
auto *thread = new QThread(nullptr);
|
||
const QString deviceId = m_deviceId;
|
||
const QString appCode = m_app.code;
|
||
const int appId = m_app.id;
|
||
|
||
auto onResult = [this, appId](const QString &result) {
|
||
QMetaObject::invokeMethod(this, [this, appId, result]() {
|
||
resumePausedProfiles();
|
||
if (result.isEmpty()) {
|
||
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
|
||
m_app.pinCodeChecked = true;
|
||
updateStatusUI();
|
||
hideOverlay(true, "Пин-код проверен");
|
||
} else {
|
||
hideOverlay(false, "Ошибка: " + result);
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
};
|
||
|
||
if (appCode == "ozon") {
|
||
auto *worker = new Ozon::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, false, true);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start);
|
||
connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection);
|
||
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else if (appCode == "black") {
|
||
auto *worker = new Black::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start);
|
||
connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection);
|
||
connect(worker, &Black::LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else if (appCode == "dushanbe_city_bank") {
|
||
auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
|
||
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection);
|
||
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else {
|
||
resumePausedProfiles();
|
||
hideOverlay(false, "Банк не поддерживается");
|
||
thread->deleteLater();
|
||
return;
|
||
}
|
||
|
||
thread->start();
|
||
}
|
||
|
||
// ── Get Profile ────────────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::startGetProfileInfo() {
|
||
m_profileBtn->setEnabled(false);
|
||
m_cardsBtn->setEnabled(false);
|
||
showOverlay("Обновление профиля...");
|
||
pauseActiveProfiles();
|
||
|
||
auto *thread = new QThread(nullptr);
|
||
const QString deviceId = m_deviceId;
|
||
const QString appCode = m_app.code;
|
||
|
||
auto onDone = [this, appCode]() {
|
||
QMetaObject::invokeMethod(this, [this, appCode]() {
|
||
m_profileBtn->setEnabled(true);
|
||
m_cardsBtn->setEnabled(true);
|
||
resumePausedProfiles();
|
||
m_app = BankProfileDAO::getApplication(m_deviceId, appCode);
|
||
updateStatusUI();
|
||
hideOverlay(true, "Профиль обновлён");
|
||
}, Qt::QueuedConnection);
|
||
};
|
||
|
||
if (appCode == "ozon") {
|
||
auto *worker = new Ozon::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start);
|
||
connect(worker, &Ozon::GetProfileInfoScript::finished, this, onDone, Qt::QueuedConnection);
|
||
connect(worker, &Ozon::GetProfileInfoScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else if (appCode == "black") {
|
||
auto *worker = new Black::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Black::GetProfileInfoScript::start);
|
||
connect(worker, &Black::GetProfileInfoScript::finished, this, onDone, Qt::QueuedConnection);
|
||
connect(worker, &Black::GetProfileInfoScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else if (appCode == "dushanbe_city_bank") {
|
||
auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start);
|
||
connect(worker, &Dushanbe::GetProfileInfoScript::finished, this, onDone, Qt::QueuedConnection);
|
||
connect(worker, &Dushanbe::GetProfileInfoScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Dushanbe::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else {
|
||
thread->deleteLater();
|
||
return;
|
||
}
|
||
|
||
thread->start();
|
||
}
|
||
|
||
// ── Get Cards ──────────────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::startGetCardInfo() {
|
||
m_profileBtn->setEnabled(false);
|
||
m_cardsBtn->setEnabled(false);
|
||
showOverlay("Проверка аккаунтов...");
|
||
pauseActiveProfiles();
|
||
|
||
auto *thread = new QThread(nullptr);
|
||
const QString deviceId = m_deviceId;
|
||
const QString appCode = m_app.code;
|
||
|
||
auto onDone = [this]() {
|
||
QMetaObject::invokeMethod(this, [this]() {
|
||
m_profileBtn->setEnabled(true);
|
||
m_cardsBtn->setEnabled(true);
|
||
resumePausedProfiles();
|
||
reloadCards();
|
||
hideOverlay(true, "Аккаунты обновлены");
|
||
}, Qt::QueuedConnection);
|
||
};
|
||
|
||
if (appCode == "ozon") {
|
||
auto *worker = new Ozon::GetCardInfoScript(deviceId, appCode, "", nullptr);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Ozon::GetCardInfoScript::start);
|
||
connect(worker, &Ozon::GetCardInfoScript::finished, this, onDone, Qt::QueuedConnection);
|
||
connect(worker, &Ozon::GetCardInfoScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else if (appCode == "black") {
|
||
auto *worker = new Black::GetCardInfoScript(deviceId, appCode, nullptr);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Black::GetCardInfoScript::start);
|
||
connect(worker, &Black::GetCardInfoScript::finished, this, onDone, Qt::QueuedConnection);
|
||
connect(worker, &Black::GetCardInfoScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else if (appCode == "dushanbe_city_bank") {
|
||
auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr);
|
||
worker->moveToThread(thread);
|
||
connect(thread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start);
|
||
connect(worker, &Dushanbe::GetCardInfoScript::finished, this, onDone, Qt::QueuedConnection);
|
||
connect(worker, &Dushanbe::GetCardInfoScript::finished, thread, &QThread::quit);
|
||
connect(worker, &Dushanbe::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
} else {
|
||
thread->deleteLater();
|
||
return;
|
||
}
|
||
|
||
thread->start();
|
||
}
|
||
|
||
// ── Send status to server ──────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::sendAppStatus(const QString &status, std::function<void()> onSuccess) {
|
||
if (m_app.bankProfileId.isEmpty()) {
|
||
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << m_app.code;
|
||
hideOverlay(true, "Сохранено локально", [this]() { updateStatusUI(); });
|
||
return;
|
||
}
|
||
|
||
const bool enable = (status == "active");
|
||
auto *thread = new QThread;
|
||
auto *net = new NetworkService;
|
||
net->moveToThread(thread);
|
||
net->addExtra("bank_profile_id", m_app.bankProfileId);
|
||
net->addExtra("enable", enable);
|
||
NetworkService::connectErrorDialog(this, net);
|
||
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||
connect(net, &NetworkService::finishedWithResult, this, [this, enable, onSuccess = std::move(onSuccess)](int result) {
|
||
if (result == 0) {
|
||
hideOverlay(true, "Успешно", [this]() { updateStatusUI(); });
|
||
if (onSuccess) onSuccess();
|
||
return;
|
||
}
|
||
// Правило #3: при неудачном включении — откатываем локаль в off и
|
||
// сообщаем юзеру. При неудачном выключении — локаль остаётся off,
|
||
// сервер дотянется периодическим sync-ом (правило #2).
|
||
if (enable) {
|
||
BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, "off");
|
||
m_app.status = "off";
|
||
hideOverlay(false, "Не удалось включить — возвращено в выключено",
|
||
[this]() { updateStatusUI(); });
|
||
} else {
|
||
hideOverlay(false, "Локально выключено, сервер дотянется автоматически",
|
||
[this]() { updateStatusUI(); });
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
// ── Toggle card status ─────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) {
|
||
const bool isActive = account.status == MaterialStatus::Active;
|
||
|
||
QList<MaterialInfo> deactivated; // другие активные, которые будут выключены при включении этой
|
||
if (!isActive) {
|
||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||
for (const auto &m : materials) {
|
||
if (m.id != account.id && m.status == MaterialStatus::Active) {
|
||
deactivated.append(m);
|
||
}
|
||
}
|
||
if (!deactivated.isEmpty()) {
|
||
QStringList names;
|
||
for (const auto &m : deactivated) {
|
||
names << (m.cardNumber.isEmpty() ? m.lastNumbers : m.cardNumber);
|
||
}
|
||
const auto reply = QMessageBox::question(this, "Включение карты",
|
||
"В этом банке уже активны другие карты:\n• " + names.join("\n• ") +
|
||
"\n\nОни будут выключены. Продолжить?",
|
||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||
if (reply != QMessageBox::Yes) return;
|
||
}
|
||
}
|
||
|
||
if (isActive) {
|
||
if (!MaterialDAO::updateStatus(account.id, "off")) {
|
||
qWarning() << "[AccountSettings] Failed to update material status:" << account.id;
|
||
return;
|
||
}
|
||
} else {
|
||
const auto actuallyDeactivated =
|
||
MaterialDAO::activateExclusive(m_deviceId, m_app.code, account.id);
|
||
// Синхронизируем deactivated с реальным результатом транзакции
|
||
deactivated = actuallyDeactivated;
|
||
}
|
||
|
||
showOverlay(isActive ? "Выключение карты..." : "Включение карты...");
|
||
|
||
auto finishToggle = [this, isActive]() {
|
||
// Если выключили — проверяем остались ли активные
|
||
if (isActive) {
|
||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||
bool hasActive = false;
|
||
for (const auto &m : materials) {
|
||
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
|
||
}
|
||
if (!hasActive && m_app.status == "active") {
|
||
BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, "off");
|
||
m_app.status = "off";
|
||
sendAppStatus("off");
|
||
return; // sendAppStatus покажет свой overlay
|
||
}
|
||
}
|
||
reloadCards();
|
||
};
|
||
|
||
// Сервер не даёт включать материалы при выключенном профиле — возвращает их
|
||
// в `disabled`, и client-side sync затирает локальный active. Поэтому пока
|
||
// профиль off, синхронизация только локальная; материалы зальются на сервер
|
||
// когда юзер включит профиль (см. toggleProfileStatus).
|
||
const bool profileActive = m_app.status == "active";
|
||
if (!profileActive) {
|
||
hideOverlay(true, "Сохранено локально", finishToggle);
|
||
return;
|
||
}
|
||
|
||
// Выключаем на сервере остальные (fire-and-forget) — только при включении этой карты
|
||
if (!isActive) {
|
||
for (const MaterialInfo &m : deactivated) {
|
||
if (m.materialId.isEmpty()) continue;
|
||
auto *t = new QThread;
|
||
auto *n = new NetworkService;
|
||
n->moveToThread(t);
|
||
n->addExtra("material_id", m.materialId);
|
||
n->addExtra("enable", false);
|
||
NetworkService::connectErrorDialog(this, n);
|
||
connect(t, &QThread::started, n, &NetworkService::toggleMaterial);
|
||
connect(n, &NetworkService::finished, t, &QThread::quit);
|
||
connect(n, &NetworkService::finished, n, &QObject::deleteLater);
|
||
connect(t, &QThread::finished, t, &QThread::deleteLater);
|
||
t->start();
|
||
}
|
||
}
|
||
|
||
if (!account.materialId.isEmpty()) {
|
||
auto *thread = new QThread;
|
||
auto *net = new NetworkService;
|
||
net->moveToThread(thread);
|
||
net->addExtra("material_id", account.materialId);
|
||
net->addExtra("enable", !isActive);
|
||
NetworkService::connectErrorDialog(this, net);
|
||
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
|
||
const bool willEnable = !isActive;
|
||
const int materialDbId = account.id;
|
||
connect(net, &NetworkService::finishedWithResult, this,
|
||
[this, finishToggle, willEnable, materialDbId](int result) {
|
||
if (result == 0) {
|
||
hideOverlay(true, "Успешно", finishToggle);
|
||
return;
|
||
}
|
||
// Правило #3: при неудачном включении карты — откат локально в off.
|
||
if (willEnable) {
|
||
MaterialDAO::updateStatus(materialDbId, "off");
|
||
hideOverlay(false, "Не удалось включить — возвращено в выключено", finishToggle);
|
||
} else {
|
||
hideOverlay(false, "Локально выключено, сервер дотянется автоматически", finishToggle);
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
} else {
|
||
hideOverlay(true, "Сохранено локально", finishToggle);
|
||
}
|
||
}
|
||
|
||
// ── Cards table ────────────────────────────────────────────────────────────
|
||
|
||
void AccountSettingsWindow::reloadCards() {
|
||
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||
|
||
// setRowCount(0) перед re-fill удаляет и items, и cell widgets.
|
||
// clearContents() оставляет setCellWidget'ы висеть — старые кнопки с
|
||
// замкнутой устаревшей `acc` остаются в строках, ставших active.
|
||
m_cardsTable->setRowCount(0);
|
||
m_cardsTable->setRowCount(accounts.size());
|
||
m_cardsTable->setColumnCount(8);
|
||
m_cardsTable->setHorizontalHeaderLabels({
|
||
"Сервер", "Статус", "Время обновления", "Номер карты", "Сумма", "Валюта", "", ""
|
||
});
|
||
m_cardsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||
|
||
// Правило 4-A: при выключенном профиле материалы фактически недоступны —
|
||
// отображаем приглушённо (gray) и добавляем подсказку, локальный status в БД не трогаем.
|
||
const bool profileOff = (m_app.status != "active");
|
||
const QBrush dimmedBrush = QBrush(QColor(128, 128, 128));
|
||
const QString dimmedTooltip = profileOff
|
||
? QStringLiteral("Профиль банка выключен — карта фактически недоступна. "
|
||
"Включите профиль, чтобы карта заработала.")
|
||
: QString();
|
||
|
||
for (int k = 0; k < accounts.size(); ++k) {
|
||
const MaterialInfo &acc = accounts[k];
|
||
|
||
// Сервер
|
||
auto *syncItem = new QTableWidgetItem(acc.materialId.isEmpty() ? "Не синхр." : "Синхр.");
|
||
syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||
if (profileOff) {
|
||
syncItem->setForeground(dimmedBrush);
|
||
syncItem->setToolTip(dimmedTooltip);
|
||
} else {
|
||
syncItem->setForeground(acc.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen));
|
||
}
|
||
m_cardsTable->setItem(k, 0, syncItem);
|
||
|
||
// Статус
|
||
const bool isActive = acc.status == MaterialStatus::Active;
|
||
auto *statusItem = new QTableWidgetItem(materialStatusToUiString(acc.status));
|
||
statusItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||
if (profileOff) {
|
||
statusItem->setForeground(dimmedBrush);
|
||
statusItem->setToolTip(dimmedTooltip);
|
||
} else {
|
||
statusItem->setForeground(isActive ? QBrush(Qt::darkGreen) : QBrush(Qt::red));
|
||
}
|
||
m_cardsTable->setItem(k, 1, statusItem);
|
||
|
||
// Время обновления
|
||
auto *timeItem = new QTableWidgetItem(acc.updateTime.toString("dd.MM.yyyy HH:mm:ss"));
|
||
timeItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||
m_cardsTable->setItem(k, 2, timeItem);
|
||
|
||
// Номер карты (текст + иконка копирования)
|
||
const QString displayNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber;
|
||
const QString fullCardNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber;
|
||
auto *cardCell = new QWidget(m_cardsTable);
|
||
auto *cardLayout = new QHBoxLayout(cardCell);
|
||
cardLayout->setContentsMargins(4, 0, 4, 0);
|
||
cardLayout->setSpacing(6);
|
||
auto *cardLabel = new QLabel(displayNumber, cardCell);
|
||
cardLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||
cardLayout->addWidget(cardLabel);
|
||
if (!fullCardNumber.isEmpty()) {
|
||
auto *copyCardBtn = new QPushButton(cardCell);
|
||
copyCardBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png"));
|
||
copyCardBtn->setIconSize(QSize(14, 14));
|
||
copyCardBtn->setToolTip("Копировать номер карты");
|
||
copyCardBtn->setFixedSize(22, 20);
|
||
copyCardBtn->setStyleSheet("padding: 1px; border: none;");
|
||
connect(copyCardBtn, &QPushButton::clicked, this, [copyCardBtn, fullCardNumber]() {
|
||
QGuiApplication::clipboard()->setText(fullCardNumber);
|
||
QToolTip::showText(copyCardBtn->mapToGlobal(QPoint(0, copyCardBtn->height())),
|
||
"Скопировано", copyCardBtn, {}, 1500);
|
||
});
|
||
cardLayout->addWidget(copyCardBtn);
|
||
}
|
||
cardLayout->addStretch();
|
||
m_cardsTable->setCellWidget(k, 3, cardCell);
|
||
|
||
// Сумма
|
||
auto *amountItem = new QTableWidgetItem(QString::number(acc.amount));
|
||
amountItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||
m_cardsTable->setItem(k, 4, amountItem);
|
||
|
||
// Валюта
|
||
auto *currItem = new QTableWidgetItem(acc.currency);
|
||
currItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||
m_cardsTable->setItem(k, 5, currItem);
|
||
|
||
// Кнопка переключения: только для неактивных — включит этот и выключит остальные
|
||
if (!isActive) {
|
||
auto *toggleBtn = new QPushButton("Переключить", m_cardsTable);
|
||
toggleBtn->setStyleSheet("background-color: green; color: white;");
|
||
m_cardsTable->setCellWidget(k, 6, toggleBtn);
|
||
|
||
connect(toggleBtn, &QPushButton::clicked, this, [this, acc]() {
|
||
toggleMaterialStatus(acc);
|
||
});
|
||
}
|
||
|
||
// Кнопка копирования номера счёта
|
||
if (!acc.accountNumber.isEmpty()) {
|
||
auto *copyAcctBtn = new QPushButton(m_cardsTable);
|
||
copyAcctBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png"));
|
||
copyAcctBtn->setIconSize(QSize(16, 16));
|
||
copyAcctBtn->setToolTip("Копировать номер счёта");
|
||
copyAcctBtn->setFixedSize(28, 24);
|
||
copyAcctBtn->setStyleSheet("padding: 2px;");
|
||
m_cardsTable->setCellWidget(k, 7, copyAcctBtn);
|
||
|
||
connect(copyAcctBtn, &QPushButton::clicked, this, [this, copyAcctBtn, acc]() {
|
||
QGuiApplication::clipboard()->setText(acc.accountNumber);
|
||
QToolTip::showText(copyAcctBtn->mapToGlobal(QPoint(0, copyAcctBtn->height())),
|
||
"Скопировано", copyAcctBtn, {}, 1500);
|
||
});
|
||
}
|
||
}
|
||
|
||
m_cardsTable->resizeColumnsToContents();
|
||
m_cardsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
|
||
m_cardsTable->resizeRowsToContents();
|
||
}
|
||
|
||
// ── Pause/Resume active profiles ───────────────────────────────────────────
|
||
|
||
static void toggleProfileOnServer(const BankProfileInfo &app, bool enable) {
|
||
if (app.bankProfileId.isEmpty()) return;
|
||
auto *thread = new QThread;
|
||
auto *net = new NetworkService;
|
||
net->moveToThread(thread);
|
||
net->addExtra("bank_profile_id", app.bankProfileId);
|
||
net->addExtra("enable", enable);
|
||
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
void AccountSettingsWindow::pauseActiveProfiles() {
|
||
const QList<BankProfileInfo> apps = BankProfileDAO::getApplicationsByDeviceId(m_deviceId);
|
||
for (const BankProfileInfo &app : apps) {
|
||
if (app.status == "active") {
|
||
m_pausedProfiles.append(app.code);
|
||
BankProfileDAO::update(app.id, app.pinCode, app.comment, "off");
|
||
toggleProfileOnServer(app, false);
|
||
qDebug() << "[AccountSettings] Paused profile:" << app.code;
|
||
}
|
||
}
|
||
}
|
||
|
||
void AccountSettingsWindow::resumePausedProfiles() {
|
||
for (const QString &code : m_pausedProfiles) {
|
||
const BankProfileInfo app = BankProfileDAO::getApplication(m_deviceId, code);
|
||
if (app.id >= 0 && app.status == "off") {
|
||
BankProfileDAO::update(app.id, app.pinCode, app.comment, "active");
|
||
toggleProfileOnServer(app, true);
|
||
qDebug() << "[AccountSettings] Resumed profile:" << app.code;
|
||
}
|
||
}
|
||
m_pausedProfiles.clear();
|
||
}
|