1
0
forked from BRT/arc
arc/views/bank/AccountSettingsWindow.cpp

799 lines
39 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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;'>&#9679; Не синхронизирован</span>"
: " <span style='color:green;'>&#9679; Синхронизирован</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();
}