1
0
forked from BRT/arc

Add CommonScript framework and implement bank-specific automation scripts including GetCardInfoScript, GetProfileInfoScript, and GetLastTransactionsScript.

This commit is contained in:
slava 2026-04-04 23:32:26 +07:00
parent f28311b5cf
commit 37b72938e8
53 changed files with 2978 additions and 561 deletions

View File

@ -209,7 +209,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
}
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
};
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 840);
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);

View File

@ -174,7 +174,6 @@ void GetProfileInfoScript::doStart() {
postAccountsData(accountsToPost);
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
}

View File

@ -51,7 +51,6 @@ void LoginAndCheckAccountsScript::doStart() {
}
if (m_pinCheckOnly) {
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}

View File

@ -0,0 +1,253 @@
#include "CommonScript.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QSettings>
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/DeviceDAO.h"
#include "dao/BankProfileDAO.h"
#include "net/NetworkService.h"
namespace Dushanbe {
CommonScript::CommonScript(QObject *parent)
: QObject(parent) {
}
CommonScript::~CommonScript() = default;
void CommonScript::start() {
doStart();
emit finished();
}
Node CommonScript::swipeToButton(
const QString &deviceId,
const QString &text, int &counter, const int width, const int height
) {
for (int i = 0; i < 10; ++i) {
if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) {
return button;
}
const int x = width / 2;
const int offset = height / 4;
AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter));
}
return {};
}
bool CommonScript::inputPinCode(const QString &deviceId, const QString &pinCode) {
// DC Next использует кнопочную сетку для ввода PIN (content-desc = "0"-"9")
for (const QChar &digit : pinCode) {
Node button = xmlScreenParser.findNodeByContentDesc(QString(digit));
if (button.isEmpty()) {
qWarning() << "[Dushanbe::inputPinCode] Button not found for digit:" << digit;
return false;
}
AdbUtils::makeTap(deviceId, button.x(), button.y());
QThread::msleep(300);
}
return true;
}
bool CommonScript::runAppAndGoToHomeScreen(
const QString &deviceId,
const QString &packageName,
const QString &pinCode,
const int width,
const int height
) {
AdbUtils::unlockScreen(deviceId, width, height);
if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) {
qDebug() << "[Dushanbe] Process already running, pid:" << pid;
AdbUtils::getScreenDumpAsXml(deviceId, 1000);
AdbUtils::tryToKillApplication(deviceId, packageName);
}
if (!AdbUtils::tryToStartApplication(deviceId, packageName)) {
AdbUtils::takeScreenshot(deviceId, "run_app_error");
qWarning() << "[Dushanbe] Process has not started";
return false;
}
QThread::msleep(4500);
int counter = 0;
bool findAndInputPincode = false;
while (true) {
counter++;
if (counter > 10) {
AdbUtils::takeScreenshot(deviceId, "too_many_attempts");
return false;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (xmlScreenParser.isHomeScreen()) {
return true;
}
if (!findAndInputPincode && xmlScreenParser.isPinCodeScreen()) {
inputPinCode(deviceId, pinCode);
findAndInputPincode = true;
QThread::msleep(3000);
continue;
}
QThread::msleep(1000);
}
}
void CommonScript::postAccountsData(const QList<QPair<MaterialInfo, Node>> &accounts) {
if (accounts.isEmpty()) {
return;
}
auto *thread = new QThread;
auto *worker = new NetworkService;
QJsonArray list;
for (const QPair<MaterialInfo, Node> &pair : accounts) {
QJsonObject obj;
MaterialInfo account = pair.first;
obj["appName"] = account.appCode;
obj["lastNumbers"] = account.lastNumbers;
obj["amount"] = account.amount;
obj["description"] = account.description;
obj["currency"] = account.currency;
obj["status"] = materialStatusToString(account.status);
list.append(obj);
}
worker->setDeviceId(accounts.first().first.deviceId);
worker->setPayload(QJsonDocument(list).toJson());
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &NetworkService::postAccountsData);
connect(worker, &NetworkService::finished, thread, &QThread::quit);
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) {
auto *thread = new QThread;
auto *worker = new NetworkService;
QJsonObject obj;
obj["id"] = transaction.id;
obj["amount"] = transaction.amount;
obj["name"] = transaction.name;
obj["fee"] = transaction.fee;
obj["status"] = transactionStatusToString(transaction.status);
obj["type"] = transactionTypeToString(transaction.type);
obj["phone"] = transaction.phone;
obj["description"] = transaction.description;
obj["updatedDescription"] = transaction.updatedDescription;
obj["bankName"] = transaction.bankName;
obj["bankTime"] = transaction.bankTime;
obj["bankTransactionId"] = transaction.bankTrExternalId;
worker->setDeviceId(account.deviceId);
worker->addExtra("lastNumbers", account.lastNumbers);
worker->addExtra("transactionId", transaction.id);
worker->setPayload(QJsonDocument(obj).toJson());
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData);
connect(worker, &NetworkService::finished, thread, &QThread::quit);
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void CommonScript::updateTransactionData(
const MaterialInfo &account,
const int transactionId,
const TransactionStatus status,
const QString &oldDesc,
const QString &newDesc
) {
auto *thread = new QThread;
auto *worker = new NetworkService;
QJsonObject obj;
obj["id"] = transactionId;
obj["status"] = transactionStatusToString(status);
obj["description"] = newDesc;
obj["updatedDescription"] = oldDesc;
worker->setDeviceId(account.deviceId);
worker->addExtra("lastNumbers", account.lastNumbers);
worker->setPayload(QJsonDocument(obj).toJson());
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &NetworkService::updateTransactionData);
connect(worker, &NetworkService::finished, thread, &QThread::quit);
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) {
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
qWarning() << "[Dushanbe] createBankProfileIfNeeded: skipping — fullName or phone is empty";
return;
}
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
};
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 840);
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);
const QSettings settings("config.ini", QSettings::IniFormat);
const QString phoneCode = settings.value(appCode + "/phone_code").toString();
QString phone = app.phone;
phone.remove(QRegularExpression("[^0-9]"));
if (!phoneCode.isEmpty() && !phone.startsWith(phoneCode)) {
if (phone.startsWith('8') && phoneCode != "8") {
phone.replace(0, 1, phoneCode);
} else {
phone.prepend(phoneCode);
}
}
phone.prepend('+');
const DeviceInfo device = DeviceDAO::getDeviceById(deviceId);
QJsonObject profileBody;
profileBody["first_name"] = parts.value(0);
profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString("");
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
profileBody["bank_name"] = appCode;
profileBody["phone_number"] = phone;
profileBody["balance"] = QString::number(qRound64(app.amount * 100));
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
profileBody["device_id"] = device.apiId;
profileBody["currency"] = currencyCode;
NetworkService ns;
ns.setDeviceId(deviceId);
ns.setPayload(QJsonDocument(profileBody).toJson());
if (app.bankProfileId.isEmpty()) {
ns.addExtra("app_id", app.id);
ns.postBankProfile();
qDebug() << "[Dushanbe] createBankProfileIfNeeded: postBankProfile done";
} else {
ns.addExtra("bank_profile_id", app.bankProfileId);
ns.patchBankProfile();
qDebug() << "[Dushanbe] createBankProfileIfNeeded: patchBankProfile done";
}
}
void CommonScript::stop() {
m_running = false;
}
} // namespace Dushanbe

View File

@ -0,0 +1,68 @@
#pragma once
#include <QObject>
#include "ScreenXmlParser.h"
#include "db/TransactionInfo.h"
namespace Dushanbe {
class CommonScript : public QObject {
Q_OBJECT
public:
explicit CommonScript(QObject *parent = nullptr);
~CommonScript() override;
protected:
virtual void doStart() = 0;
bool inputPinCode(const QString &deviceId, const QString &pinCode);
bool runAppAndGoToHomeScreen(
const QString &deviceId,
const QString &packageName,
const QString &pinCode,
int width,
int height
);
Node swipeToButton(
const QString &deviceId,
const QString &text,
int &counter, int width, int height
);
void postAccountsData(const QList<QPair<MaterialInfo, Node>> &accounts);
void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction);
void updateTransactionData(
const MaterialInfo &account,
int transactionId,
TransactionStatus status,
const QString &oldDesc,
const QString &newDesc
);
void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode);
bool m_running = true;
ScreenXmlParser xmlScreenParser;
int m_counter = 0;
signals:
void finished();
void finishedWithResult(const QString &result);
public slots:
virtual void start() final;
virtual void stop() final;
private:
Q_DISABLE_COPY(CommonScript);
};
} // namespace Dushanbe

View File

@ -0,0 +1,202 @@
#include "GetCardInfoScript.h"
#include <QThread>
#include <QJsonDocument>
#include <QJsonObject>
#include "adb/AdbUtils.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "net/NetworkService.h"
namespace Dushanbe {
GetCardInfoScript::GetCardInfoScript(
QString deviceId,
QString appCode,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)) {
}
GetCardInfoScript::~GetCardInfoScript() = default;
void GetCardInfoScript::doStart() {
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Dushanbe::GetCardInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Парсим баланс с домашнего экрана
const double balance = xmlScreenParser.parseBalanceFromHomeScreen();
qDebug() << "[Dushanbe::GetCardInfo] balance:" << balance;
BankProfileDAO::updateAmount(app.id, balance);
// Тапаем "Карты" на домашнем экране
Node cardsButton = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Карты"));
if (cardsButton.x() == 0 && cardsButton.y() == 0) {
m_error = "Cannot find Cards button: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
AdbUtils::makeTap(m_deviceId, cardsButton.x(), cardsButton.y());
QThread::msleep(3000);
// Ждём экран "Мои карты"
bool cardsScreenFound = false;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardsScreen()) {
cardsScreenFound = true;
break;
}
QThread::msleep(1000);
}
if (!cardsScreenFound) {
m_error = "Cards screen not detected: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
// Парсим карты из списка (lastNumbers, баланс)
const QList<ScreenXmlParser::CardInfo> cards = xmlScreenParser.parseCards();
qDebug() << "[Dushanbe::GetCardInfo] Found" << cards.size() << "cards in list";
if (cards.isEmpty()) {
m_error = "No cards found: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
// Находим все clickable ImageView с "****" — это карточки в списке
// Пропускаем кредитные карты
QList<Node> cardNodes;
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className == "android.widget.ImageView" && node.clickable &&
node.contentDesc.contains("****") &&
!node.contentDesc.contains(QString::fromUtf8("Кредитная карта"))) {
cardNodes.append(node);
}
}
// Кликаем по каждой карте для получения полного номера
for (int i = 0; i < cardNodes.size(); ++i) {
if (i >= cards.size()) break;
const Node &cardNode = cardNodes[i];
AdbUtils::makeTap(m_deviceId, cardNode.x(), cardNode.y());
QThread::msleep(2000);
// Ждём экран детали карты
bool detailFound = false;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardDetailScreen()) {
detailFound = true;
break;
}
QThread::msleep(1000);
}
if (!detailFound) {
qWarning() << "[Dushanbe::GetCardInfo] Card detail screen not found for card" << i;
continue;
}
QString fullNumber = xmlScreenParser.parseFullCardNumber();
const double cardBalance = xmlScreenParser.parseCardDetailBalance();
const QString holder = xmlScreenParser.parseCardDetailHolder();
const QString lastNumbers = cards[i].lastNumbers.isEmpty()
? (fullNumber.length() >= 4 ? fullNumber.right(4).remove(' ') : "")
: cards[i].lastNumbers;
qDebug() << "[Dushanbe::GetCardInfo] Card" << i << ":" << fullNumber
<< "balance:" << cardBalance << "holder:" << holder;
if (lastNumbers.isEmpty()) {
qWarning() << "[Dushanbe::GetCardInfo] No card number for card" << i;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
continue;
}
MaterialInfo account;
account.deviceId = device.id;
account.appCode = m_appCode;
account.lastNumbers = lastNumbers;
account.cardNumber = QString(fullNumber).remove(' ');
account.amount = cardBalance;
account.currency = "TJS";
account.description = cards[i].description.isEmpty() ? holder : cards[i].description;
account.status = MaterialStatus::Active;
account.updateTime = QDateTime::currentDateTimeUtc();
if (!MaterialDAO::upsertAccount(account)) {
qWarning() << "[Dushanbe::GetCardInfo] Failed to upsert account" << lastNumbers;
}
// Отправляем на сервер если нет material_id
const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode);
if (!updatedApp.bankProfileId.isEmpty()) {
const MaterialInfo savedAccount = MaterialDAO::findAppAccount(device.id, m_appCode, lastNumbers);
if (savedAccount.id != -1 && savedAccount.materialId.isEmpty()) {
NetworkService ns;
ns.setDeviceId(m_deviceId);
QJsonObject materialBody;
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
materialBody["card_number"] = fullNumber;
materialBody["name"] = updatedApp.fullName;
ns.setPayload(QJsonDocument(materialBody).toJson());
ns.addExtra("account_id", savedAccount.id);
ns.postMaterial();
qDebug() << "[Dushanbe::GetCardInfo] postMaterial done for" << lastNumbers;
}
}
// Назад к списку карт
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
// Ждём возврата на экран "Мои карты"
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardsScreen()) break;
QThread::msleep(1000);
}
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
}
} // namespace Dushanbe

View File

@ -0,0 +1,27 @@
#pragma once
#include "CommonScript.h"
namespace Dushanbe {
class GetCardInfoScript final : public CommonScript {
Q_OBJECT
public:
explicit GetCardInfoScript(
QString deviceId,
QString appCode,
QObject *parent = nullptr
);
~GetCardInfoScript() override;
protected:
void doStart() override;
private:
QString m_deviceId;
const QString m_appCode;
QString m_error;
};
} // namespace Dushanbe

View File

@ -0,0 +1,250 @@
#include "GetLastTransactionsScript.h"
#include <QThread>
#include <QTimeZone>
#include "adb/AdbUtils.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "AppLogger.h"
namespace Dushanbe {
GetLastTransactionsScript::GetLastTransactionsScript(
QString deviceId,
QString appCode,
double searchAmount,
QDateTime searchTime,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)),
m_searchAmount(searchAmount),
m_searchTime(std::move(searchTime)) {
}
GetLastTransactionsScript::~GetLastTransactionsScript() = default;
void GetLastTransactionsScript::doStart() {
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Dushanbe::GetLastTransactions] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId;
qWarning() << m_error;
AppLogger::log("dushanbe/GetLastTransactions", m_deviceId, m_error,
AdbUtils::captureScreenshotBytes(m_deviceId),
"FETCH_TRANSACTIONS");
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Тапаем "История" в нижнем меню
Node historyBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("История"));
if (historyBtn.x() == 0 && historyBtn.y() == 0) {
m_error = "Cannot find History button: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
AdbUtils::makeTap(m_deviceId, historyBtn.x(), historyBtn.y());
QThread::msleep(3000);
// Ждём экран истории
bool historyLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isHistoryScreen()) {
historyLoaded = true;
break;
}
QThread::msleep(1500);
}
if (!historyLoaded) {
m_error = "History screen not loaded: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
// Находим accountId
int accountId = -1;
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
if (!accounts.isEmpty()) {
accountId = accounts.first().id;
} else {
qWarning() << "[Dushanbe::GetLastTransactions] No account found";
}
qDebug() << "[Dushanbe::GetLastTransactions] Searching: amount=" << m_searchAmount
<< "time=" << m_searchTime.toString(Qt::ISODate);
const QTimeZone tjTz("Asia/Dushanbe"); // UTC+5
const int maxScrolls = 10;
for (int scroll = 0; scroll <= maxScrolls; ++scroll) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
const QList<ScreenXmlParser::TransactionItem> items = xmlScreenParser.parseTransactionItems();
for (const auto &tx : items) {
if (qAbs(tx.amount - m_searchAmount) > 0.01) continue;
qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount
<< "phone:" << tx.phone << "time:" << tx.time;
// Нашли по сумме — тапаем для получения деталей
// Ищем соответствующий node чтобы по нему тапнуть
Node txNode;
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className != "android.widget.ImageView" || !node.clickable) continue;
if (!node.contentDesc.contains("***")) continue;
// Проверяем что содержит нужную сумму
QString amountStr = QString::number(tx.amount, 'f', 2);
if (node.contentDesc.contains(amountStr)) {
txNode = node;
break;
}
}
if (txNode.x() == 0 && txNode.y() == 0) {
qWarning() << "[Dushanbe::GetLastTransactions] Cannot find node for transaction";
continue;
}
AdbUtils::makeTap(m_deviceId, txNode.x(), txNode.y());
QThread::msleep(2000);
// Ждём экран деталей
bool detailLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isTransactionDetailScreen()) {
detailLoaded = true;
break;
}
QThread::msleep(1500);
}
if (!detailLoaded) {
qWarning() << "[Dushanbe::GetLastTransactions] Detail screen not loaded";
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
continue;
}
const auto detail = xmlScreenParser.parseTransactionDetail();
// Проверяем время ±1 час
if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
QDateTime txTime = QDateTime::fromString(
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
if (!txTime.isValid()) {
txTime = QDateTime::fromString(detail.date + " " + detail.time, "dd.MM.yyyy HH:mm");
}
if (txTime.isValid()) {
txTime.setTimeZone(tjTz);
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC()));
if (diffSecs > 3600) {
qDebug() << "[Dushanbe::GetLastTransactions] Time mismatch: diff=" << diffSecs << "sec, skipping";
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
continue;
}
}
}
// Нашли! Сохраняем
qDebug() << "[Dushanbe::GetLastTransactions] FOUND:"
<< "amount=" << detail.amount
<< "date=" << detail.date << detail.time
<< "opId=" << detail.operationId
<< "receiver=" << detail.receiverAccount
<< "status=" << detail.status;
if (accountId != -1) {
TransactionInfo saveTx;
saveTx.accountId = accountId;
saveTx.amount = detail.amount;
saveTx.fee = detail.fee;
saveTx.bankName = m_appCode;
saveTx.bankTime = detail.date + " " + detail.time;
saveTx.bankTrExternalId = detail.operationId;
saveTx.description = detail.provider;
saveTx.phone = detail.receiverAccount;
saveTx.name = detail.receiverAccount;
saveTx.status = (detail.status == QString::fromUtf8("Успешный"))
? TransactionStatus::Complete : TransactionStatus::Unknown;
saveTx.type = TransactionType::Phone;
saveTx.timestamp = QDateTime::currentDateTimeUtc();
QDateTime txTime = QDateTime::fromString(
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
if (txTime.isValid()) {
txTime.setTimeZone(tjTz);
saveTx.completeTime = txTime;
}
TransactionDAO::insertTransactionIfNotExists(saveTx);
}
// Назад к истории
Node nazadBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Назад"));
if (nazadBtn.x() > 0 && nazadBtn.y() > 0) {
AdbUtils::makeTap(m_deviceId, nazadBtn.x(), nazadBtn.y());
} else {
AdbUtils::goBack(m_deviceId);
}
QThread::msleep(1000);
emit finishedWithResult(m_error); // m_error пустой — успех
return;
}
// Скроллим вниз
if (scroll < maxScrolls) {
const int x = device.screenWidth / 2;
const int fromY = device.screenHeight * 3 / 4;
const int toY = device.screenHeight / 4;
AdbUtils::makeSwipe(m_deviceId, x, fromY, x, toY);
QThread::msleep(2000);
}
}
m_error = "Transaction not found: amount=" + QString::number(m_searchAmount, 'f', 2)
+ " time=" + m_searchTime.toString(Qt::ISODate);
qWarning() << "[Dushanbe::GetLastTransactions]" << m_error;
// Возвращаемся на главную
Node glavnayaBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Главная"));
if (glavnayaBtn.x() > 0 && glavnayaBtn.y() > 0) {
AdbUtils::makeTap(m_deviceId, glavnayaBtn.x(), glavnayaBtn.y());
} else {
AdbUtils::goBack(m_deviceId);
}
QThread::msleep(1000);
emit finishedWithResult(m_error);
}
} // namespace Dushanbe

View File

@ -0,0 +1,34 @@
#pragma once
#include "CommonScript.h"
#include <QDateTime>
#include "db/DeviceInfo.h"
#include "db/BankProfileInfo.h"
namespace Dushanbe {
class GetLastTransactionsScript final : public CommonScript {
Q_OBJECT
public:
explicit GetLastTransactionsScript(
QString deviceId,
QString appCode,
double searchAmount = 0,
QDateTime searchTime = {},
QObject *parent = nullptr
);
~GetLastTransactionsScript() override;
protected:
void doStart() override;
private:
QString m_deviceId;
const QString m_appCode;
const double m_searchAmount; // сумма в сомони (0 = все последние)
const QDateTime m_searchTime; // примерное время (UTC)
QString m_error;
};
} // namespace Dushanbe

View File

@ -0,0 +1,122 @@
#include "GetProfileInfoScript.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "net/NetworkService.h"
namespace Dushanbe {
GetProfileInfoScript::GetProfileInfoScript(
QString deviceId,
QString appCode,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)) {
}
GetProfileInfoScript::~GetProfileInfoScript() = default;
void GetProfileInfoScript::doStart() {
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Dushanbe::GetProfileInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Парсим баланс с домашнего экрана
const double balance = xmlScreenParser.parseBalanceFromHomeScreen();
qDebug() << "[Dushanbe::GetProfileInfo] balance:" << balance;
BankProfileDAO::updateAmount(app.id, balance);
// Тапаем по кнопке меню (гамбургер, левый верхний угол)
// Это первый clickable ImageView в верхней панели
Node menuButton;
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className == "android.widget.ImageView" && node.clickable && node.y() < 300) {
menuButton = node;
break;
}
}
if (menuButton.x() == 0 && menuButton.y() == 0) {
m_error = "Cannot find menu button: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
AdbUtils::makeTap(m_deviceId, menuButton.x(), menuButton.y());
QThread::msleep(2000);
// Ждём боковое меню
bool sideMenuFound = false;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isSideMenu()) {
sideMenuFound = true;
break;
}
QThread::msleep(1000);
}
if (!sideMenuFound) {
m_error = "Side menu not detected: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
// Парсим ФИО и телефон из бокового меню
const QString fullName = xmlScreenParser.parseProfileFullName();
const QString phone = xmlScreenParser.parseProfilePhone();
qDebug() << "[Dushanbe::GetProfileInfo] fullName:" << fullName << "phone:" << phone;
if (!fullName.isEmpty()) {
BankProfileDAO::updateProfileInfo(app.id, fullName, phone, "");
}
createBankProfileIfNeeded(device.id, m_appCode);
const QList<MaterialInfo> allAccounts = MaterialDAO::getAccounts(device.id, m_appCode);
QList<QPair<MaterialInfo, Node>> accountsToPost;
for (const auto &acc : allAccounts) {
if (acc.materialId.isEmpty()) {
accountsToPost.append({acc, Node()});
}
}
if (!accountsToPost.isEmpty()) {
postAccountsData(accountsToPost);
}
emit finishedWithResult(m_error);
}
} // namespace Dushanbe

View File

@ -0,0 +1,27 @@
#pragma once
#include "CommonScript.h"
namespace Dushanbe {
class GetProfileInfoScript final : public CommonScript {
Q_OBJECT
public:
~GetProfileInfoScript() override;
explicit GetProfileInfoScript(
QString deviceId,
QString appCode,
QObject *parent = nullptr
);
protected:
void doStart() override;
private:
QString m_deviceId;
const QString m_appCode;
QString m_error;
};
} // namespace Dushanbe

View File

@ -0,0 +1,63 @@
#include "LoginAndCheckAccountsScript.h"
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "AppLogger.h"
#include "GetProfileInfoScript.h"
namespace Dushanbe {
LoginAndCheckAccountsScript::LoginAndCheckAccountsScript(
QString deviceId,
QString appCode,
QObject *parent,
bool pinCheckOnly
) : CommonScript(parent),
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)),
m_pinCheckOnly(pinCheckOnly) {
}
LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default;
void LoginAndCheckAccountsScript::doStart() {
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
AppLogger::log("dushanbe/LoginAndCheck", m_deviceId, m_error,
AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_PROFILE");
BankProfileDAO::updateComment(app.id,
"Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss"));
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
if (m_pinCheckOnly) {
emit finishedWithResult(m_error);
return;
}
GetProfileInfoScript profileScript(m_deviceId, m_appCode);
profileScript.start();
emit finishedWithResult(m_error);
}
} // namespace Dushanbe

View File

@ -0,0 +1,29 @@
#pragma once
#include "CommonScript.h"
namespace Dushanbe {
class LoginAndCheckAccountsScript final : public CommonScript {
Q_OBJECT
public:
~LoginAndCheckAccountsScript() override;
explicit LoginAndCheckAccountsScript(
QString deviceId,
QString appCode,
QObject *parent = nullptr,
bool pinCheckOnly = false
);
protected:
void doStart() override;
private:
QString m_deviceId;
const QString m_appCode;
const bool m_pinCheckOnly;
QString m_error;
};
} // namespace Dushanbe

View File

@ -0,0 +1,477 @@
#include "ScreenXmlParser.h"
#include <QRegularExpression>
#include <QXmlStreamReader>
#include <QDomDocument>
#include "android/xml/Node.h"
namespace Dushanbe {
static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])");
ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) {
}
ScreenXmlParser::~ScreenXmlParser() = default;
void ScreenXmlParser::parseAndSaveXml(const QString &xml) {
QDomDocument doc;
doc.setContent(xml);
m_xml = doc.documentElement();
m_nodes.clear();
QXmlStreamReader reader(xml);
while (!reader.atEnd()) {
reader.readNext();
if (reader.isStartElement() && reader.name() == u"node") {
Node node;
if (reader.attributes().hasAttribute("bounds")) {
QString bounds = reader.attributes().value("bounds").toString();
if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) {
node.setBounds(
match.captured(1).toInt(),
match.captured(2).toInt(),
match.captured(3).toInt(),
match.captured(4).toInt()
);
}
}
if (reader.attributes().hasAttribute("text")) {
node.text = reader.attributes().value("text").toString();
}
if (reader.attributes().hasAttribute("content-desc")) {
node.contentDesc = reader.attributes().value("content-desc").toString();
}
if (reader.attributes().hasAttribute("class")) {
node.className = reader.attributes().value("class").toString();
}
if (reader.attributes().hasAttribute("clickable")) {
node.clickable = reader.attributes().value("clickable").toString() == "true";
}
if (reader.attributes().hasAttribute("focusable")) {
node.focusable = reader.attributes().value("focusable").toString() == "true";
}
if (reader.attributes().hasAttribute("resource-id")) {
node.resourceId = reader.attributes().value("resource-id").toString();
}
if (reader.attributes().hasAttribute("hint")) {
node.hint = reader.attributes().value("hint").toString();
}
if (reader.attributes().hasAttribute("password")) {
node.password = reader.attributes().value("password").toString() == "true";
}
m_nodes.append(node);
}
}
}
const QList<Node> &ScreenXmlParser::findAllNodes() const {
return m_nodes;
}
Node ScreenXmlParser::findTextNode(const QString &text) {
for (const Node &node : m_nodes) {
if (node.text.trimmed() == text) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) {
for (const Node &node : m_nodes) {
if (node.contentDesc == contentDesc) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc, bool contains) {
for (const Node &node : m_nodes) {
if (contains) {
if (node.contentDesc.contains(contentDesc)) {
return node;
}
} else {
if (node.contentDesc == contentDesc) {
return node;
}
}
}
return {};
}
Node ScreenXmlParser::findButtonNode(const QString &text, bool contains) {
for (const Node &node : m_nodes) {
if (!node.clickable) continue;
if (contains) {
if (node.text.contains(text) || node.contentDesc.contains(text)) {
return node;
}
} else {
if (node.text.trimmed() == text || node.contentDesc == text) {
return node;
}
}
}
return {};
}
Node ScreenXmlParser::findFirstEditText() {
for (const Node &node : m_nodes) {
if (node.className == "android.widget.EditText") {
return node;
}
}
return {};
}
bool ScreenXmlParser::isPinCodeScreen() {
for (const Node &node : m_nodes) {
if (node.contentDesc.contains(QString::fromUtf8("Введите код доступа"))) {
return true;
}
}
return false;
}
Node ScreenXmlParser::findPinCodeEditText() {
for (const Node &node : m_nodes) {
if (node.className == "android.widget.EditText" && node.password) {
return node;
}
}
return {};
}
bool ScreenXmlParser::isHomeScreen() {
bool hasGlavnaya = false;
bool hasTJS = false;
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Главная")) {
hasGlavnaya = true;
}
if (node.contentDesc.contains("TJS")) {
hasTJS = true;
}
if (hasGlavnaya && hasTJS) {
return true;
}
}
return false;
}
double ScreenXmlParser::parseBalanceFromHomeScreen() {
// На домашнем экране: parent node content-desc="TJS\n03.04.26 - 21:46:15"
// внутри него child node content-desc="0.00" (или "10.00")
// Стратегия: находим индекс parent, затем child сразу после него с числовым content-desc
for (int i = 0; i < m_nodes.size(); ++i) {
if (!m_nodes[i].contentDesc.startsWith("TJS")) continue;
// Проверяем что это блок с датой (не просто "TJS" текст)
if (!m_nodes[i].contentDesc.contains('\n')) continue;
// Child node с балансом идёт сразу после parent или рядом
for (int j = i + 1; j < qMin(i + 5, m_nodes.size()); ++j) {
QString raw = m_nodes[j].contentDesc.trimmed();
if (raw.isEmpty()) continue;
raw.replace(',', '.');
raw.remove(' ');
bool ok = false;
double val = raw.toDouble(&ok);
if (ok) return val;
}
}
return 0.0;
}
QString ScreenXmlParser::parseUserNameFromHomeScreen() {
// TODO: реализовать парсинг имени
return {};
}
bool ScreenXmlParser::isSideMenu() {
bool hasVyhod = false;
bool hasIdentified = false;
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Выход")) {
hasVyhod = true;
}
if (node.contentDesc == QString::fromUtf8("Идентифицирован")) {
hasIdentified = true;
}
if (hasVyhod && hasIdentified) {
return true;
}
}
return false;
}
QString ScreenXmlParser::parseProfileFullName() {
// content-desc: "Мехрубон Имомкулзода\n+992 (18) 555-5519\nВерсия: 3.2.5\nСборка: 390:3443"
for (const Node &node : m_nodes) {
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) {
QStringList lines = node.contentDesc.split('\n');
if (!lines.isEmpty()) {
return lines[0].trimmed();
}
}
}
return {};
}
QString ScreenXmlParser::parseProfilePhone() {
for (const Node &node : m_nodes) {
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) {
QStringList lines = node.contentDesc.split('\n');
if (lines.size() >= 2) {
return lines[1].trimmed();
}
}
}
return {};
}
bool ScreenXmlParser::isCardsScreen() {
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Мои карты")) {
return true;
}
}
return false;
}
QList<ScreenXmlParser::CardInfo> ScreenXmlParser::parseCards() {
// Карты — ImageView с content-desc формата:
// "Обновлено: 03.04.26 - 22:43:57\n0.00 TJS\n**** 3258\n до \n11/35\nIMOMKULZODA MEKHRUBON"
// или "-\n**** \n до \n12/29\n \nКредитная карта"
QList<CardInfo> result;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.ImageView") continue;
if (!node.contentDesc.contains("****")) continue;
QStringList lines;
for (const QString &line : node.contentDesc.split('\n')) {
QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
lines.append(trimmed);
}
}
CardInfo card;
for (const QString &line : lines) {
// Баланс: "0.00 TJS"
if (line.contains("TJS")) {
QStringList parts = line.split(' ');
if (parts.size() >= 2) {
card.amount = parts[0].toDouble();
card.currency = "TJS";
}
continue;
}
// Последние цифры: "**** 3258"
if (line.startsWith("****")) {
QString nums = line.mid(4).trimmed();
if (!nums.isEmpty() && nums[0].isDigit()) {
card.lastNumbers = nums;
}
continue;
}
// Срок: "11/35" или "12/29"
static const QRegularExpression expiryRe(R"(^\d{2}/\d{2}$)");
if (expiryRe.match(line).hasMatch()) {
card.expiry = line;
continue;
}
// Пропускаем "до", "Обновлено:...", "-"
if (line == QString::fromUtf8("до") || line.startsWith(QString::fromUtf8("Обновлено")) || line == "-") {
continue;
}
// Имя держателя (CAPS) или описание
if (line == line.toUpper() && line.length() > 3) {
card.holderName = line;
} else {
card.description = line;
}
}
result.append(card);
}
return result;
}
bool ScreenXmlParser::isCardDetailScreen() {
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Карта")) {
return true;
}
}
return false;
}
QString ScreenXmlParser::parseFullCardNumber() {
// Полный номер карты: "9762 0002 0711 3258" — clickable View
static const QRegularExpression cardNumberRe(R"(^\d{4}\s\d{4}\s\d{4}\s\d{4}$)");
for (const Node &node : m_nodes) {
if (cardNumberRe.match(node.contentDesc).hasMatch()) {
return node.contentDesc;
}
}
return {};
}
double ScreenXmlParser::parseCardDetailBalance() {
// "0,00 TJS" или "- TJS" — содержит TJS, но не "сомони"
for (const Node &node : m_nodes) {
if (node.contentDesc.contains("TJS") && !node.contentDesc.contains(QString::fromUtf8("сомони"))) {
QString raw = node.contentDesc.trimmed();
raw.remove("TJS");
raw = raw.trimmed();
if (raw == "-" || raw.isEmpty()) return 0.0;
raw.replace(',', '.');
raw.remove(' ');
bool ok = false;
double val = raw.toDouble(&ok);
if (ok) return val;
}
}
return 0.0;
}
QString ScreenXmlParser::parseCardDetailHolder() {
// Имя держателя — текст в CAPS, не содержит цифр, длина > 3
static const QRegularExpression cardNumberRe(R"(\d{4}\s\d{4})");
for (const Node &node : m_nodes) {
const QString &desc = node.contentDesc;
if (desc.isEmpty() || desc.length() < 4) continue;
if (desc == desc.toUpper() && !desc.contains(QRegularExpression(R"(\d)")) &&
desc != QString::fromUtf8("TJS")) {
return desc.trimmed();
}
}
return {};
}
bool ScreenXmlParser::isHistoryScreen() {
bool hasOperacii = false;
bool hasVypiska = false;
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Операции")) {
hasOperacii = true;
}
if (node.contentDesc == QString::fromUtf8("Выписка")) {
hasVypiska = true;
}
if (hasOperacii && hasVypiska) {
return true;
}
}
return false;
}
QList<ScreenXmlParser::TransactionItem> ScreenXmlParser::parseTransactionItems() {
// Транзакции — ImageView с content-desc формата:
// "9762***3258\n992882770011\n493.00 \nDC (по номеру телефона)\n 18:57:10"
QList<TransactionItem> result;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.ImageView") continue;
if (!node.contentDesc.contains("***")) continue;
QStringList lines;
for (const QString &line : node.contentDesc.split('\n')) {
QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
lines.append(trimmed);
}
}
if (lines.size() < 4) continue;
TransactionItem tx;
tx.cardMask = lines[0]; // "9762***3258"
tx.phone = lines[1]; // "992882770011"
// Сумма: "493.00" (может быть с пробелом в конце)
bool ok = false;
tx.amount = lines[2].toDouble(&ok);
if (!ok) tx.amount = 0.0;
tx.description = lines[3]; // "DC (по номеру телефона)" или "Tcell"
// Время — последний элемент
if (lines.size() >= 5) {
tx.time = lines[4]; // "18:57:10"
}
result.append(tx);
}
return result;
}
bool ScreenXmlParser::isTransactionDetailScreen() {
for (const Node &node : m_nodes) {
if (node.contentDesc.contains(QString::fromUtf8("Дата операции:")) &&
node.contentDesc.contains(QString::fromUtf8("Номер операции:"))) {
return true;
}
}
return false;
}
ScreenXmlParser::TransactionDetail ScreenXmlParser::parseTransactionDetail() {
// Все данные в одном content-desc, формат "ключ: \nзначение"
TransactionDetail detail;
for (const Node &node : m_nodes) {
if (!node.contentDesc.contains(QString::fromUtf8("Дата операции:"))) continue;
// Разбиваем по \n, парсим пары "ключ:" + следующая строка = значение
QStringList lines = node.contentDesc.split('\n');
for (int i = 0; i < lines.size(); ++i) {
const QString line = lines[i].trimmed();
if (line.startsWith(QString::fromUtf8("Дата операции:")) && i + 1 < lines.size()) {
detail.date = lines[i + 1].trimmed();
} else if (line.startsWith(QString::fromUtf8("Время операции:")) && i + 1 < lines.size()) {
detail.time = lines[i + 1].trimmed();
} else if (line.startsWith(QString::fromUtf8("Номер операции:")) && i + 1 < lines.size()) {
detail.operationId = lines[i + 1].trimmed();
} else if (line.startsWith(QString::fromUtf8("Поставщик:")) && i + 1 < lines.size()) {
detail.provider = lines[i + 1].trimmed();
} else if (line.startsWith(QString::fromUtf8("Счет отправителя:")) && i + 1 < lines.size()) {
detail.senderAccount = lines[i + 1].trimmed();
} else if (line.startsWith(QString::fromUtf8("Счет получателя:")) && i + 1 < lines.size()) {
detail.receiverAccount = lines[i + 1].trimmed();
} else if (line.startsWith(QString::fromUtf8("Сумма операции:")) && i + 1 < lines.size()) {
detail.amount = lines[i + 1].trimmed().toDouble();
} else if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) {
detail.fee = lines[i + 1].trimmed().toDouble();
} else if (line.startsWith(QString::fromUtf8("Статус:")) && i + 1 < lines.size()) {
detail.status = lines[i + 1].trimmed();
}
}
break;
}
return detail;
}
Node ScreenXmlParser::findEditTextByHint(const QString &hint) {
for (const Node &node : m_nodes) {
if (node.className == "android.widget.EditText" && node.hint == hint) {
return node;
}
}
return {};
}
} // namespace Dushanbe

View File

@ -0,0 +1,123 @@
#pragma once
#include <QObject>
#include <QDomElement>
#include "android/xml/Node.h"
#include "db/MaterialInfo.h"
namespace Dushanbe {
class ScreenXmlParser final : public QObject {
Q_OBJECT
public:
explicit ScreenXmlParser(QObject *parent = nullptr);
~ScreenXmlParser() override;
void parseAndSaveXml(const QString &xml);
const QList<Node> &findAllNodes() const;
Node findTextNode(const QString &text);
Node findNodeByContentDesc(const QString &contentDesc);
Node findNodeByContentDesc(const QString &contentDesc, bool contains);
Node findButtonNode(const QString &text, bool contains);
Node findFirstEditText();
// TODO: реализовать после снятия XML дампов экранов
bool isPinCodeScreen();
Node findPinCodeEditText();
bool isHomeScreen();
// Парсит баланс с домашнего экрана
double parseBalanceFromHomeScreen();
// Парсит имя пользователя с домашнего экрана
QString parseUserNameFromHomeScreen();
// Определяет боковое меню (профиль)
bool isSideMenu();
// Парсит данные профиля из бокового меню
// content-desc: "Имя Фамилия\n+992...\nВерсия:...\nСборка:..."
QString parseProfileFullName();
QString parseProfilePhone();
// Определяет экран "Мои карты"
bool isCardsScreen();
// Структура для карты из экрана "Мои карты"
struct CardInfo {
double amount = 0.0;
QString currency;
QString lastNumbers; // "3258"
QString expiry; // "11/35"
QString holderName; // "IMOMKULZODA MEKHRUBON"
QString description; // "Кредитная карта" и т.д.
};
// Парсит все карты с экрана "Мои карты" (lastNumbers только)
QList<CardInfo> parseCards();
// Определяет экран детали карты (заголовок "Карта")
bool isCardDetailScreen();
// Парсит полный номер карты с экрана детали ("9762 0002 0711 3258")
QString parseFullCardNumber();
// Парсит баланс с экрана детали ("0,00 TJS")
double parseCardDetailBalance();
// Парсит имя держателя с экрана детали
QString parseCardDetailHolder();
// Определяет экран истории операций (табы "Операции" + "Выписка")
bool isHistoryScreen();
// Структура для транзакции из экрана истории
struct TransactionItem {
QString cardMask; // "9762***3258"
QString phone; // "992882770011"
double amount = 0.0; // 493.00
QString description; // "DC (по номеру телефона)"
QString time; // "18:57:10"
};
// Парсит транзакции с экрана истории
QList<TransactionItem> parseTransactionItems();
// Определяет экран детали транзакции (содержит "Дата операции:" и "Номер операции:")
bool isTransactionDetailScreen();
// Структура для деталей транзакции
struct TransactionDetail {
QString date; // "03.04.2026"
QString time; // "18:57:10"
QString operationId; // "1593369670"
QString provider; // "DC (по номеру телефона)"
QString senderAccount; // "9762***3258"
QString receiverAccount;// "992882770011"
double amount = 0.0; // 493.00
double fee = 0.0; // 0.00
QString status; // "Успешный"
};
// Парсит детали транзакции
TransactionDetail parseTransactionDetail();
// Находит EditText по hint
Node findEditTextByHint(const QString &hint);
private:
QList<Node> m_nodes;
QDomElement m_xml;
};
} // namespace Dushanbe

View File

@ -312,7 +312,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
}
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
};
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 643);
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);

View File

@ -193,7 +193,7 @@ void GetCardInfoScript::doStart() {
// ISO 4217 numeric currency codes
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
};
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);

View File

@ -122,7 +122,7 @@ void GetProfileInfoScript::doStart() {
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
};
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);
@ -173,7 +173,10 @@ void GetProfileInfoScript::doStart() {
postAccountsData(accountsToPost);
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
// Возвращаемся на домашний экран
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
emit finishedWithResult(m_error);
}

View File

@ -54,7 +54,6 @@ void LoginAndCheckAccountsScript::doStart() {
return;
} else {
if (m_pinCheckOnly) {
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
[apps]
list=ozon, black
list=ozon, black, dushanbe
[common]
currencies=USD, ARS, RUB
currencies=USD, ARS, RUB, TJS
version=0.0.1
[db]
@ -30,3 +30,9 @@ currency=RUB
name=Ozon
phone_code=7
[dushanbe]
android_package_name=tj.dc.next1
currency=TJS
name=Dushanbe City
phone_code=992

View File

@ -385,3 +385,15 @@ bool BankProfileDAO::migrateDeviceId(const QString &oldDeviceId, const QString &
qDebug() << "[BankProfileDAO] migrated" << query.numRowsAffected() << "profiles from" << oldDeviceId << "to" << newDeviceId;
return true;
}
bool BankProfileDAO::deleteProfile(const QString &deviceId, const QString &appCode) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("DELETE FROM bank_profiles WHERE device_id = :device_id AND code = :code");
query.bindValue(":device_id", deviceId);
query.bindValue(":code", appCode);
if (!query.exec()) {
qWarning() << "[BankProfileDAO] deleteProfile failed:" << query.lastError().text();
return false;
}
return true;
}

View File

@ -49,4 +49,6 @@ public:
[[nodiscard]] static bool deactivateAppsForDevice(const QString &deviceId);
[[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId);
[[nodiscard]] static bool deleteProfile(const QString &deviceId, const QString &appCode);
};

View File

@ -64,14 +64,12 @@ bool MaterialDAO::updateAccountById(
UPDATE materials
SET status = :status,
description = :description,
last_numbers = :last_numbers,
currency = :currency
WHERE id = :id
)");
query.bindValue(":status", status);
query.bindValue(":description", description);
query.bindValue(":last_numbers", description.length() >= 4 ? description.right(4) : description);
query.bindValue(":currency", currency);
query.bindValue(":id", id);
@ -83,6 +81,18 @@ bool MaterialDAO::updateAccountById(
return true;
}
bool MaterialDAO::updateStatus(const int id, const QString &status) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE materials SET status = :status WHERE id = :id");
query.bindValue(":status", status);
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "[MaterialDAO] updateStatus failed:" << query.lastError().text();
return false;
}
return true;
}
bool MaterialDAO::deleteAccount(const int id) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("DELETE FROM materials WHERE id = :id");
@ -304,3 +314,15 @@ bool MaterialDAO::migrateDeviceId(const QString &oldDeviceId, const QString &new
qDebug() << "[MaterialDAO] migrated" << query.numRowsAffected() << "materials from" << oldDeviceId << "to" << newDeviceId;
return true;
}
bool MaterialDAO::deleteAccountsByApp(const QString &deviceId, const QString &appCode) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("DELETE FROM materials WHERE device_id = :device_id AND app_code = :app_code AND (material_id IS NULL OR material_id = '')");
query.bindValue(":device_id", deviceId);
query.bindValue(":app_code", appCode);
if (!query.exec()) {
qWarning() << "[MaterialDAO] deleteAccountsByApp failed:" << query.lastError().text();
return false;
}
return true;
}

View File

@ -13,6 +13,8 @@ public:
const QString &currency
);
[[nodiscard]] static bool updateStatus(int id, const QString &status);
[[nodiscard]] static bool deleteAccount(int id);
[[nodiscard]] static bool updateCardNumber(int accountId, const QString &cardNumber);
@ -38,4 +40,6 @@ public:
[[nodiscard]] static QStringList getActiveMaterialIdsExcludingDevices(const QSet<QString> &excludeDevices);
[[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId);
[[nodiscard]] static bool deleteAccountsByApp(const QString &deviceId, const QString &appCode);
};

BIN
images/dushanbe_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -27,6 +27,8 @@
#include "black/PayByCardScript.h"
#include "ozon/PayByCardScript.h"
#include "ozon/PayByPhoneScript.h"
#include "dushanbe/LoginAndCheckAccountsScript.h"
#include "dushanbe/GetLastTransactionsScript.h"
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data);
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description);
@ -278,7 +280,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
if (newStatus != EventStatus::Error) {
const MaterialInfo account = MaterialDAO::getAccountById(event.accountId);
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
};
const int currencyCode = currencyCodes.value(account.currency.toUpper(), 643);
@ -416,8 +418,9 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
const QString adbSerial = device.adbSerial.isEmpty() ? account.deviceId : device.adbSerial;
if (event.type == EventType::FetchProfile) {
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode));
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode));
else if (appCode == "dushanbe") setup(new Dushanbe::LoginAndCheckAccountsScript(adbSerial, appCode));
else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode;
}
@ -432,6 +435,8 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
setup(new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime));
else if (appCode == "black")
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
else if (appCode == "dushanbe")
setup(new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime));
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
}

View File

@ -933,41 +933,53 @@ void NetworkService::patchBankProfile() {
}
void NetworkService::toggleMaterial() {
if (!ensureValidToken()) { emit finished(); return; }
if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; }
const QString materialId = m_extraData.value("material_id").toString();
const bool enable = m_extraData.value("enable").toBool();
if (materialId.isEmpty()) {
qWarning() << "[NetworkService] toggleMaterial: material_id is empty";
emit finished();
emit finishedWithResult(-1); emit finished();
return;
}
const QString enableStr = enable ? "true" : "false";
const QString path = m_apiBase + m_pathMaterial + "/" + materialId + "/" + enableStr;
patchJson(path, QJsonObject(), true);
qDebug() << "[NetworkService] toggleMaterial done:" << materialId << "enable=" << enable;
const QJsonValue result = patchJson(path, QJsonObject(), true);
if (result.isNull() && result.isUndefined()) {
qWarning() << "[NetworkService] toggleMaterial failed:" << materialId;
emit finishedWithResult(-1);
} else {
qDebug() << "[NetworkService] toggleMaterial done:" << materialId << "enable=" << enable;
emit finishedWithResult(0);
}
emit finished();
}
void NetworkService::toggleBankProfile() {
if (!ensureValidToken()) { emit finished(); return; }
if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; }
const QString bankProfileId = m_extraData.value("bank_profile_id").toString();
const bool enable = m_extraData.value("enable").toBool();
if (bankProfileId.isEmpty()) {
qWarning() << "[NetworkService] toggleBankProfile: bank_profile_id is empty";
emit finished();
emit finishedWithResult(-1); emit finished();
return;
}
const QString enableStr = enable ? "true" : "false";
const QString path = m_apiBase + m_pathBankProfile + "/" + bankProfileId + "/" + enableStr;
patchJson(path, QJsonObject(), true);
qDebug() << "[NetworkService] toggleBankProfile done:" << bankProfileId << "enable=" << enable;
const QJsonValue result = patchJson(path, QJsonObject(), true);
if (result.isNull() && result.isUndefined()) {
qWarning() << "[NetworkService] toggleBankProfile failed:" << bankProfileId;
emit finishedWithResult(-1);
} else {
qDebug() << "[NetworkService] toggleBankProfile done:" << bankProfileId << "enable=" << enable;
emit finishedWithResult(0);
}
emit finished();
}

View File

@ -1,29 +1,23 @@
#include "AccountSettingsWindow.h"
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QScrollArea>
#include <QCoreApplication>
#include <QVBoxLayout>
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonObject>
#include <QMessageBox>
#include <QInputDialog>
#include <QThread>
#include <QTimer>
#include "ozon/LoginAndCheckAccountsScript.h"
#include "ozon/GetProfileInfoScript.h"
#include "ozon/GetCardInfoScript.h"
#include "black/GetLastTransactionsScript.h"
#include "ozon/GetLastTransactionsScript.h"
#include "black/LoginAndCheckAccountsScript.h"
#include "black/GetProfileInfoScript.h"
#include "black/GetCardInfoScript.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "bank/TransactionWindow.h"
#include "device/EditBankAccountDialog.h"
#include "device/EditBankDialog.h"
#include "dao/MaterialDAO.h"
#include "net/NetworkService.h"
AccountSettingsWindow::AccountSettingsWindow(
@ -36,357 +30,250 @@ AccountSettingsWindow::AccountSettingsWindow(
| Qt::WindowMinMaxButtonsHint),
m_deviceId(std::move(deviceId)),
m_deviceName(std::move(deviceName)),
m_applicationInfo(app) {
setWindowTitle("Настройки банка [" + app.name + "]");
setMinimumSize(800, 400);
setMaximumSize(1000, 800);
resize(800, 400);
// 1. Редактирование пин-кода
// 2. Включение/отключения банка
// 3. Работа с картами (вкл/выкл) и редактирование цифр
// 4. Запуск проверки пин-код и парсинга аккаунтов
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);
// Виджет-контейнер, в котором будет layout с таблицами и лейблами
auto *contentWidget = new QWidget;
auto *layout = new QVBoxLayout(contentWidget);
// Убираем отступы и рамку
contentWidget->setContentsMargins(0, 0, 0, 0); // Убираем отступы
layout->setContentsMargins(4, 4, 4, 4); // Убираем отступы у layout
// scrollArea->setStyleSheet("border: none;"); // Убираем рамку
auto *content = new QWidget;
auto *layout = new QVBoxLayout(content);
layout->setContentsMargins(8, 8, 8, 8);
layout->setAlignment(Qt::AlignTop);
QHBoxLayout *row1 = new QHBoxLayout();
QString status = QString("Статус: ") + (m_applicationInfo.status == "active"
? "<span style='color:green;'>Активен</span>"
: "<span style='color:red;'>Неактивен</span>") +
QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked
? "<span style='color:green;'>Проверен</span>"
: "<span style='color:red;'>Не проверен</span>");
m_statusLabel = new QLabel(status);
// ── Статус профиля ─────────────────────────────────────────────────
auto *statusRow = new QHBoxLayout;
statusRow->addWidget(new QLabel("Статус профиля:"));
m_statusLabel = new QLabel;
m_statusLabel->setTextFormat(Qt::RichText);
row1->addWidget(m_statusLabel);
statusRow->addWidget(m_statusLabel);
statusRow->addStretch();
QPushButton *editBankBtn = new QPushButton("Редактировать");
editBankBtn->setStyleSheet(
"background-color: green;"
"color: white;"
);
editBankBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
row1->addWidget(editBankBtn);
connect(editBankBtn, &QPushButton::clicked, this, [this]() {
BankEditDialog dlg(m_applicationInfo, this);
if (dlg.exec() == QDialog::Accepted) {
const QString pinCode = dlg.firstValue();
const QString comment = dlg.secondValue();
bool on = dlg.toggled();
m_toggleBtn = new QPushButton;
m_toggleBtn->setFixedWidth(120);
connect(m_toggleBtn, &QPushButton::clicked, this, &AccountSettingsWindow::toggleProfileStatus);
statusRow->addWidget(m_toggleBtn);
layout->addLayout(statusRow);
// Если включаем профиль — проверяем что есть хотя бы одна активная карта
if (on) {
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
bool hasActive = false;
for (const auto &m : materials) {
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
}
if (!hasActive) {
QMessageBox::warning(this, "Невозможно включить",
"Ни одна карта не активна.\nВключите хотя бы одну карту перед включением профиля.");
return;
}
}
// ── Пин-код ────────────────────────────────────────────────────────
auto *pinRow = new QHBoxLayout;
pinRow->addWidget(new QLabel("Пин-код:"));
m_pinLabel = new QLabel;
m_pinLabel->setTextFormat(Qt::RichText);
pinRow->addWidget(m_pinLabel);
pinRow->addStretch();
if (!BankProfileDAO::update(m_applicationInfo.id, pinCode, comment, on ? "active" : "off")) {
qDebug() << "Cant update bank data";
} else {
m_applicationInfo.pinCode = pinCode;
m_applicationInfo.comment = comment;
m_applicationInfo.status = on ? "active" : "off";
auto *editPinBtn = new QPushButton("Редактировать");
editPinBtn->setStyleSheet("background-color: green; color: white; padding: 4px 12px;");
connect(editPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::editPinCode);
pinRow->addWidget(editPinBtn);
QString status = QString("Статус: ") + (on
? "<span style='color:green;'>Активен</span>"
: "<span style='color:red;'>Неактивен</span>") +
QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked
? "<span style='color:green;'>Проверен</span>"
: "<span style='color:red;'>Не проверен</span>");
m_statusLabel->setText(status);
m_commentLabel->setText(comment);
sendAppStatus(m_applicationInfo.deviceId, m_applicationInfo.code, on ? "active" : "off");
}
}
});
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);
layout->addLayout(row1);
QHBoxLayout *commentRow = new QHBoxLayout();
m_commentLabel = new QLabel(m_applicationInfo.comment);
m_commentLabel->setStyleSheet(
"color: gray;"
"margin-bottom: 8px;"
);
commentRow->addWidget(m_commentLabel);
layout->addLayout(commentRow);
QHBoxLayout *profileRow = new QHBoxLayout();
QString profileText;
if (m_applicationInfo.fullName.isEmpty() && m_applicationInfo.phone.isEmpty()) {
profileText = "<span style='color:gray;'>Данные не получены</span>";
} else {
profileText = m_applicationInfo.fullName + " | " + m_applicationInfo.phone;
}
m_profileLabel = new QLabel(profileText);
// ── Профиль ────────────────────────────────────────────────────────
m_profileLabel = new QLabel;
m_profileLabel->setTextFormat(Qt::RichText);
m_profileLabel->setStyleSheet("margin-bottom: 8px;");
profileRow->addWidget(m_profileLabel);
m_profileLabel->setStyleSheet("margin: 4px 0;");
layout->addWidget(m_profileLabel);
// Статус синхронизации с сервером
const QString syncText = m_applicationInfo.bankProfileId.isEmpty()
? "<span style='color:red;'>&#9679; Не синхронизирован</span>"
: "<span style='color:green;'>&#9679; Синхронизирован</span>";
auto *syncLabel = new QLabel(syncText);
syncLabel->setTextFormat(Qt::RichText);
syncLabel->setStyleSheet("margin-bottom: 8px; margin-left: 16px;");
profileRow->addWidget(syncLabel);
profileRow->addStretch();
// ── Кнопки обновления ──────────────────────────────────────────────
auto *actionsRow = new QHBoxLayout;
layout->addLayout(profileRow);
auto *profileBtn = new QPushButton("Обновить профиль");
profileBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;");
connect(profileBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetProfileInfo);
actionsRow->addWidget(profileBtn);
QPushButton *soloButton = new QPushButton("Запустить проверку пин-кода");
soloButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
// soloButton->setStyleSheet("");
soloButton->setStyleSheet(
"background-color: orange;"
"color: white;"
);
QObject::connect(soloButton, &QPushButton::clicked, [&, this]() {
if (m_applicationInfo.pinCode.isEmpty()) {
QMessageBox::warning(this, "Пин-код не задан",
"Сначала укажите пин-код банка через кнопку \"Редактировать\".");
return;
}
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Запустить проверку пин-кода?"));
msgBox.setText(
"Запуститься скрипт проверки входа в банк " + m_applicationInfo.name +
" по пин-коду. Окно с настройками закроется.");
auto *cardsBtn = new QPushButton("Проверить аккаунты");
cardsBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;");
connect(cardsBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetCardInfo);
actionsRow->addWidget(cardsBtn);
QPushButton *deleteBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(closeBtn);
msgBox.setModal(true);
msgBox.exec();
actionsRow->addStretch();
layout->addLayout(actionsRow);
if (msgBox.clickedButton() == deleteBtn) {
startCheckPinCode(m_applicationInfo.code);
close();
}
});
layout->addWidget(soloButton);
// ── Таблица карт ───────────────────────────────────────────────────
layout->addSpacing(8);
layout->addWidget(new QLabel("<b>Счета</b>"));
if (m_applicationInfo.code == "ozon" && m_applicationInfo.pinCodeChecked) {
QHBoxLayout *ozonRow = new QHBoxLayout();
m_cardsTable = new QTableWidget;
m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection);
m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_cardsTable->setFocusPolicy(Qt::NoFocus);
layout->addWidget(m_cardsTable);
QPushButton *profileButton = new QPushButton("Обновить профиль");
profileButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
profileButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(profileButton, &QPushButton::clicked, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Обновить профиль?"));
msgBox.setText(
"Запустится скрипт получения профиля из " + m_applicationInfo.name +
". Окно с настройками закроется.");
QPushButton *runBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == runBtn) {
startGetProfileInfo(m_applicationInfo.code);
close();
}
});
QPushButton *cardButton = new QPushButton("Проверить аккаунты");
cardButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
cardButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(cardButton, &QPushButton::clicked, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Проверить аккаунты?"));
msgBox.setText(
"Запустится скрипт получения данных карты из " + m_applicationInfo.name +
". Окно с настройками закроется.");
QPushButton *runBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == runBtn) {
startGetCardInfo(m_applicationInfo.code);
close();
}
});
QPushButton *transactionsButton = new QPushButton("Проверить транзакции");
transactionsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
transactionsButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(transactionsButton, &QPushButton::clicked, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Проверить транзакции?"));
msgBox.setText(
"Запустится скрипт получения последних транзакций из " + m_applicationInfo.name +
". Окно с настройками закроется.");
QPushButton *runBtn = msgBox.addButton(tr("За 24 часа"), QMessageBox::AcceptRole);
QPushButton *last5Btn = msgBox.addButton(tr("Последние 5"), QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == runBtn) {
startGetLastTransactions(m_applicationInfo.code);
close();
} else if (msgBox.clickedButton() == last5Btn) {
startGetLastTransactions(m_applicationInfo.code, 5);
close();
}
});
ozonRow->addWidget(profileButton);
ozonRow->addWidget(cardButton);
ozonRow->addWidget(transactionsButton);
ozonRow->addStretch();
layout->addLayout(ozonRow);
}
if (m_applicationInfo.code == "black" && m_applicationInfo.pinCodeChecked) {
QHBoxLayout *blackRow = new QHBoxLayout();
QPushButton *profileButton = new QPushButton("Обновить профиль");
profileButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
profileButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(profileButton, &QPushButton::clicked, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Обновить профиль?"));
msgBox.setText(
"Запустится скрипт получения профиля из " + m_applicationInfo.name +
". Окно с настройками закроется.");
QPushButton *runBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == runBtn) {
startGetProfileInfo(m_applicationInfo.code);
close();
}
});
QPushButton *cardButton = new QPushButton("Проверить аккаунты");
cardButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
cardButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(cardButton, &QPushButton::clicked, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Проверить аккаунты?"));
msgBox.setText(
"Запустится скрипт получения данных карты из " + m_applicationInfo.name +
". Окно с настройками закроется.");
QPushButton *runBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == runBtn) {
startGetCardInfo(m_applicationInfo.code);
close();
}
});
QPushButton *transactionsButton = new QPushButton("Проверить транзакции");
transactionsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
transactionsButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(transactionsButton, &QPushButton::clicked, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Проверить транзакции?"));
msgBox.setText(
"Запустится скрипт получения транзакций из " + m_applicationInfo.name +
". Окно с настройками закроется.");
QPushButton *runBtn = msgBox.addButton(tr("Проверить"), QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == runBtn) {
startGetLastTransactions(m_applicationInfo.code);
close();
}
});
blackRow->addWidget(profileButton);
blackRow->addWidget(cardButton);
blackRow->addWidget(transactionsButton);
blackRow->addStretch();
layout->addLayout(blackRow);
}
layout->addWidget(new QLabel("Счета"));
QTableWidget *tableWidget = new QTableWidget(this);
tableWidget->setSelectionMode(QAbstractItemView::NoSelection);
tableWidget->setFocusPolicy(Qt::NoFocus);
reloadContent(tableWidget);
tableWidget->resizeColumnsToContents();
layout->addWidget(tableWidget);
tableWidget->resizeRowsToContents();
// Добавим вертикальный Spacer в конец, чтобы прокручиваемая область не растягивала контент
// QSpacerItem *spacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding);
// layout->addItem(spacer);
contentWidget->setLayout(layout);
scrollArea->setWidget(contentWidget);
content->setLayout(layout);
scrollArea->setWidget(content);
mainLayout->addWidget(scrollArea);
setLayout(mainLayout);
// 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();
}
void AccountSettingsWindow::startCheckPinCode(const QString &appCode) {
QThread *thread = new QThread(nullptr);
const QString deviceId = m_deviceId;
// ── Overlay ────────────────────────────────────────────────────────────────
auto onResult = [deviceId, appCode](const QString &result) {
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);
}
}
// ── 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 ? "Выключение профиля..." : "Включение профиля...");
sendAppStatus(newStatus);
}
// ── 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;
}
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) {
if (result.isEmpty()) {
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
if (app.id >= 0) {
BankProfileDAO::updatePinCodeStatus(app.id, "yes", "");
}
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
m_app.pinCodeChecked = true;
QMetaObject::invokeMethod(this, [this]() { updateStatusUI(); }, Qt::QueuedConnection);
}
};
const bool pinCheckOnly = !m_applicationInfo.bankProfileId.isEmpty();
if (appCode == "ozon") {
auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr, false, pinCheckOnly);
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, worker, onResult);
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(m_deviceId, appCode, nullptr, pinCheckOnly);
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, worker, onResult);
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);
@ -398,20 +285,33 @@ void AccountSettingsWindow::startCheckPinCode(const QString &appCode) {
thread->start();
}
void AccountSettingsWindow::startGetProfileInfo(const QString &appCode) {
QThread *thread = new QThread(nullptr);
// ── Get Profile ────────────────────────────────────────────────────────────
void AccountSettingsWindow::startGetProfileInfo() {
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_app = BankProfileDAO::getApplication(m_deviceId, appCode);
updateStatusUI();
}, Qt::QueuedConnection);
};
if (appCode == "ozon") {
auto *worker = new Ozon::GetProfileInfoScript(m_deviceId, appCode, nullptr);
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(m_deviceId, appCode, nullptr);
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);
@ -423,104 +323,101 @@ void AccountSettingsWindow::startGetProfileInfo(const QString &appCode) {
thread->start();
}
void AccountSettingsWindow::startGetCardInfo(const QString &appCode) {
// ── Get Cards ──────────────────────────────────────────────────────────────
void AccountSettingsWindow::startGetCardInfo() {
auto *thread = new QThread(nullptr);
const QString deviceId = m_deviceId;
const QString appCode = m_app.code;
auto onDone = [this]() {
QMetaObject::invokeMethod(this, [this]() { reloadCards(); }, Qt::QueuedConnection);
};
if (appCode == "ozon") {
QThread *thread = new QThread(nullptr);
auto *worker = new Ozon::GetCardInfoScript(m_deviceId, appCode, "", nullptr);
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);
thread->start();
} else if (appCode == "black") {
QThread *thread = new QThread(nullptr);
auto *worker = new Black::GetCardInfoScript(m_deviceId, appCode, nullptr);
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);
thread->start();
} else {
thread->deleteLater();
return;
}
thread->start();
}
void AccountSettingsWindow::startGetLastTransactions(const QString &appCode, int maxCount) {
if (appCode == "ozon") {
QThread *thread = new QThread(nullptr);
auto *worker = new Ozon::GetLastTransactionsScript(m_deviceId, appCode, maxCount);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::GetLastTransactionsScript::start);
connect(worker, &Ozon::GetLastTransactionsScript::finished, thread, &QThread::quit);
connect(worker, &Ozon::GetLastTransactionsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
} else if (appCode == "black") {
QThread *thread = new QThread(nullptr);
auto *worker = new Black::GetLastTransactionsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Black::GetLastTransactionsScript::start);
connect(worker, &Black::GetLastTransactionsScript::finished, thread, &QThread::quit);
connect(worker, &Black::GetLastTransactionsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
}
}
// ── Send status to server ──────────────────────────────────────────────────
void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code);
if (app.bankProfileId.isEmpty()) {
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << code;
void AccountSettingsWindow::sendAppStatus(const QString &status) {
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;
auto *net = new NetworkService;
net->moveToThread(thread);
net->addExtra("bank_profile_id", app.bankProfileId);
net->addExtra("bank_profile_id", m_app.bankProfileId);
net->addExtra("enable", enable);
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
connect(net, &NetworkService::finished, thread, &QThread::quit);
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
connect(net, &NetworkService::finishedWithResult, this, [this](int result) {
if (result == 0) {
hideOverlay(true, "Успешно", [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();
}
void AccountSettingsWindow::updateAccountData(const MaterialInfo &account) {
QJsonArray list;
QJsonObject obj;
obj["appName"] = account.appCode;
obj["lastNumbers"] = account.lastNumbers;
obj["amount"] = account.amount;
obj["description"] = account.description;
obj["currency"] = account.currency;
obj["status"] = materialStatusToString(account.status);
// ── Toggle card status ─────────────────────────────────────────────────────
list.append(obj);
auto *paymentThread = new QThread;
auto *networkService = new NetworkService;
networkService->moveToThread(paymentThread);
networkService->setDeviceId(account.deviceId);
networkService->setPayload(QJsonDocument(list).toJson());
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::postAccountsData);
QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit);
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater);
paymentThread->start();
}
void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account, QTableWidget *tableWidget) {
void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) {
const bool isActive = account.status == MaterialStatus::Active;
const QString newStatus = isActive ? "off" : "active";
if (!MaterialDAO::updateAccountById(account.id, newStatus, account.description, account.currency)) {
if (!MaterialDAO::updateStatus(account.id, newStatus)) {
qWarning() << "[AccountSettings] Failed to update material status:" << account.id;
return;
}
// Отправляем на сервер PATCH /api/v1/profile/material/{id}/{true|false}
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();
};
if (!account.materialId.isEmpty()) {
auto *thread = new QThread;
auto *net = new NetworkService;
@ -528,133 +425,85 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account, QT
net->addExtra("material_id", account.materialId);
net->addExtra("enable", !isActive);
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
connect(net, &NetworkService::finishedWithResult, this, [this, finishToggle](int result) {
if (result == 0) {
hideOverlay(true, "Успешно", 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);
}
// Если выключили материал — проверяем остались ли активные
if (isActive) {
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
bool hasActive = false;
for (const auto &m : materials) {
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
}
if (!hasActive && m_applicationInfo.status == "active") {
BankProfileDAO::update(m_applicationInfo.id, m_applicationInfo.pinCode, m_applicationInfo.comment, "off");
m_applicationInfo.status = "off";
sendAppStatus(m_deviceId, m_applicationInfo.code, "off");
const QString status = QString("Статус: <span style='color:red;'>Неактивен</span>") +
QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked
? "<span style='color:green;'>Проверен</span>"
: "<span style='color:red;'>Не проверен</span>");
m_statusLabel->setText(status);
QMessageBox::information(this, "Профиль выключен",
"Все карты выключены — банковский профиль автоматически деактивирован.");
}
}
reloadContent(tableWidget);
tableWidget->resizeColumnsToContents();
}
void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
// ── Cards table ────────────────────────────────────────────────────────────
tableWidget->clearContents();
tableWidget->setRowCount(accounts.length());
tableWidget->setColumnCount(8);
tableWidget->setHorizontalHeaderLabels({
"Время обновления",
"Номер",
"Сумма",
"Валюта",
"",
"Сервер",
"",
""
void AccountSettingsWindow::reloadCards() {
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_app.code);
m_cardsTable->clearContents();
m_cardsTable->setRowCount(accounts.size());
m_cardsTable->setColumnCount(7);
m_cardsTable->setHorizontalHeaderLabels({
"Сервер", "Статус", "Время обновления", "Номер", "Сумма", "Валюта", ""
});
tableWidget->setWordWrap(true);
tableWidget->setTextElideMode(Qt::ElideNone);
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
m_cardsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
for (int k = 0; k < accounts.size(); ++k) {
const MaterialInfo &account = accounts[k];
const MaterialInfo &acc = accounts[k];
QTableWidgetItem *time = new QTableWidgetItem(account.updateTime.toString("dd.MM.yyyy HH:mm:ss"));
time->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
time->setFlags(time->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 0, time);
// Сервер
auto *syncItem = new QTableWidgetItem(acc.materialId.isEmpty() ? "Не синхр." : "Синхр.");
syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
syncItem->setForeground(acc.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen));
m_cardsTable->setItem(k, 0, syncItem);
const QString displayNumber = account.cardNumber.isEmpty() ? account.description : account.cardNumber;
QTableWidgetItem *desc = new QTableWidgetItem(displayNumber);
desc->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
desc->setFlags(desc->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 1, desc);
// Статус
const bool isActive = acc.status == MaterialStatus::Active;
auto *statusItem = new QTableWidgetItem(materialStatusToUiString(acc.status));
statusItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
statusItem->setForeground(isActive ? QBrush(Qt::darkGreen) : QBrush(Qt::red));
m_cardsTable->setItem(k, 1, statusItem);
QTableWidgetItem *amount = new QTableWidgetItem(QString::number(account.amount));
amount->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
amount->setFlags(amount->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 2, amount);
// Время обновления
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);
QTableWidgetItem *currency = new QTableWidgetItem(account.currency);
currency->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
currency->setFlags(currency->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 3, currency);
// Номер
const QString displayNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber;
auto *numItem = new QTableWidgetItem(displayNumber);
numItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_cardsTable->setItem(k, 3, numItem);
// Кнопка вкл/выкл карты
const bool isActive = account.status == MaterialStatus::Active;
auto *toggleBtn = new QPushButton(isActive ? "Выкл" : "Вкл", tableWidget);
// Сумма
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);
// Кнопка вкл/выкл
auto *toggleBtn = new QPushButton(isActive ? "Выключить" : "Включить", m_cardsTable);
toggleBtn->setStyleSheet(isActive
? "background-color: orange; color: white;"
: "background-color: green; color: white;");
tableWidget->setCellWidget(k, 4, toggleBtn);
m_cardsTable->setCellWidget(k, 6, toggleBtn);
connect(toggleBtn, &QPushButton::clicked, this, [this, account, tableWidget]() {
toggleMaterialStatus(account, tableWidget);
});
auto *syncItem = new QTableWidgetItem(account.materialId.isEmpty() ? "Не синхр." : "Синхр.");
syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
syncItem->setForeground(account.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen));
syncItem->setFlags(syncItem->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 5, syncItem);
auto *deleteBankAccountBtn = new QPushButton("Удалить", tableWidget);
deleteBankAccountBtn->setStyleSheet("background-color: red; color: white;");
tableWidget->setCellWidget(k, 6, deleteBankAccountBtn);
connect(deleteBankAccountBtn, &QPushButton::clicked, this, [this, tableWidget, account]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle("Удалить аккаунт?");
msgBox.setText("Вы уверены, что хотите удалить аккаунт?");
QPushButton *deleteBtn = msgBox.addButton("Удалить", QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton("Отмена", QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.exec();
if (msgBox.clickedButton() == deleteBtn) {
if (MaterialDAO::deleteAccount(account.id)) {
reloadContent(tableWidget);
tableWidget->resizeColumnsToContents();
} else {
qWarning() << "Cant delete account" << account.id;
}
}
});
auto *transactionsBtn = new QPushButton("Транзакции", tableWidget);
transactionsBtn->setStyleSheet("background-color: #1565C0; color: white;");
tableWidget->setCellWidget(k, 7, transactionsBtn);
connect(transactionsBtn, &QPushButton::clicked, this, [this, account]() {
auto *txWindow = new TransactionWindow(account.id, this);
txWindow->show();
connect(toggleBtn, &QPushButton::clicked, this, [this, acc]() {
toggleMaterialStatus(acc);
});
}
m_cardsTable->resizeColumnsToContents();
m_cardsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
m_cardsTable->resizeRowsToContents();
}

View File

@ -1,7 +1,11 @@
#pragma once
#include <QLabel>
#include <QPushButton>
#include <QTableWidget>
#include <QTimer>
#include <QWidget>
#include <functional>
#include "db/MaterialInfo.h"
#include "db/BankProfileInfo.h"
@ -15,25 +19,32 @@ public:
private:
QString m_deviceId;
QString m_deviceName;
BankProfileInfo m_applicationInfo;
BankProfileInfo m_app;
QLabel *m_statusLabel;
QLabel *m_commentLabel;
QLabel *m_pinLabel;
QLabel *m_profileLabel;
QPushButton *m_toggleBtn;
QTableWidget *m_cardsTable;
void startCheckPinCode(const QString &appCode);
// Overlay
QWidget *m_overlay = nullptr;
QLabel *m_overlayText = nullptr;
QTimer *m_overlayTimer = nullptr;
void startGetProfileInfo(const QString &appCode);
void showOverlay(const QString &text);
void hideOverlay(bool success, const QString &message, std::function<void()> onHidden = nullptr);
void startGetCardInfo(const QString &appCode);
void updateStatusUI();
void toggleProfileStatus();
void editPinCode();
void startCheckPinCode();
void startGetProfileInfo();
void startGetCardInfo();
void sendAppStatus(const QString &status);
void toggleMaterialStatus(const MaterialInfo &account);
void reloadCards();
void startGetLastTransactions(const QString &appCode, int maxCount = 0);
void reloadContent(QTableWidget *tableWidget);
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status);
void toggleMaterialStatus(const MaterialInfo &account, QTableWidget *tableWidget);
void updateAccountData(const MaterialInfo &account);
protected:
void resizeEvent(QResizeEvent *event) override;
};

View File

@ -23,6 +23,7 @@
#include "device/EditBankDialog.h"
#include "ozon/LoginAndCheckAccountsScript.h"
#include "black/LoginAndCheckAccountsScript.h"
#include "dushanbe/LoginAndCheckAccountsScript.h"
QTableWidgetItem *BankSettingsWindow::createTableWidget(const QString &text) {
QTableWidgetItem *item = new QTableWidgetItem(text);
@ -75,6 +76,14 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
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") {
auto *worker = new Dushanbe::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult);
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
} else {
thread->deleteLater();
return;

View File

@ -0,0 +1,605 @@
#include "BankSetupWizard.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QCoreApplication>
#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/DeviceDAO.h"
#include "dao/MaterialDAO.h"
#include "net/NetworkService.h"
BankSetupWizard::BankSetupWizard(QWidget *parent, const QString &deviceId, const BankProfileInfo &app)
: QDialog(parent), m_deviceId(deviceId), m_app(app) {
setWindowTitle("Настройка банка — " + app.name);
setMinimumSize(500, 350);
resize(520, 380);
auto *mainLayout = new QVBoxLayout(this);
m_stack = new QStackedWidget(this);
m_stack->addWidget(createStep1()); // 0
m_stack->addWidget(createStep2()); // 1
m_stack->addWidget(createStep3()); // 2
m_stack->addWidget(createStep4Success()); // 3
m_stack->addWidget(createStep4Error()); // 4
m_stack->setCurrentIndex(0);
mainLayout->addWidget(m_stack);
pauseActiveProfiles();
}
BankSetupWizard::~BankSetupWizard() {
stopWorkerThread();
resumePausedProfiles();
}
// ── Step 1: Intro ──────────────────────────────────────────────────────────
QWidget *BankSetupWizard::createStep1() {
auto *page = new QWidget;
auto *layout = new QVBoxLayout(page);
layout->setAlignment(Qt::AlignTop);
auto *icon = new QLabel;
QPixmap pixmap(QCoreApplication::applicationDirPath() + "/images/" + m_app.code + "_icon.png");
if (!pixmap.isNull())
icon->setPixmap(pixmap.scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation));
icon->setAlignment(Qt::AlignCenter);
layout->addWidget(icon);
auto *title = new QLabel("<h2>" + m_app.name + "</h2>");
title->setAlignment(Qt::AlignCenter);
layout->addWidget(title);
auto *desc = new QLabel(
"Мастер поможет настроить банковский профиль для приложения <b>" + m_app.name + "</b>.<br><br>"
"Будут выполнены следующие шаги:<br>"
"1. Ввод пин-кода<br>"
"2. Проверка входа в приложение<br>"
"3. Получение данных профиля и карт"
);
desc->setWordWrap(true);
desc->setAlignment(Qt::AlignCenter);
layout->addWidget(desc);
layout->addStretch();
auto *btnLayout = new QHBoxLayout;
btnLayout->addStretch();
auto *nextBtn = new QPushButton("Далее");
nextBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;");
connect(nextBtn, &QPushButton::clicked, this, [this]() {
m_stack->setCurrentIndex(1);
});
btnLayout->addWidget(nextBtn);
layout->addLayout(btnLayout);
return page;
}
// ── Step 2: PIN Code ───────────────────────────────────────────────────────
QWidget *BankSetupWizard::createStep2() {
auto *page = new QWidget;
auto *layout = new QVBoxLayout(page);
layout->setAlignment(Qt::AlignTop);
auto *title = new QLabel("<h3>Введите пин-код</h3>");
title->setAlignment(Qt::AlignCenter);
layout->addWidget(title);
auto *desc = new QLabel("Укажите пин-код для входа в приложение " + m_app.name + ".");
desc->setWordWrap(true);
desc->setAlignment(Qt::AlignCenter);
layout->addWidget(desc);
layout->addSpacing(16);
m_pinCodeEdit = new QLineEdit;
m_pinCodeEdit->setPlaceholderText("Пин-код");
m_pinCodeEdit->setMaxLength(10);
m_pinCodeEdit->setAlignment(Qt::AlignCenter);
m_pinCodeEdit->setFixedWidth(200);
auto *editLayout = new QHBoxLayout;
editLayout->addStretch();
editLayout->addWidget(m_pinCodeEdit);
editLayout->addStretch();
layout->addLayout(editLayout);
layout->addStretch();
auto *btnLayout = new QHBoxLayout;
auto *backBtn = new QPushButton("Назад");
connect(backBtn, &QPushButton::clicked, this, [this]() {
m_stack->setCurrentIndex(0);
});
btnLayout->addWidget(backBtn);
btnLayout->addStretch();
auto *nextBtn = new QPushButton("Далее");
nextBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;");
connect(nextBtn, &QPushButton::clicked, this, [this]() {
if (m_pinCodeEdit->text().trimmed().isEmpty()) return;
m_app.pinCode = m_pinCodeEdit->text().trimmed();
m_stack->setCurrentIndex(2);
startVerification();
});
btnLayout->addWidget(nextBtn);
layout->addLayout(btnLayout);
return page;
}
// ── Step 3: Progress ───────────────────────────────────────────────────────
QWidget *BankSetupWizard::createStep3() {
auto *page = new QWidget;
auto *layout = new QVBoxLayout(page);
layout->setAlignment(Qt::AlignTop);
auto *title = new QLabel("<h3>Идёт проверка банка</h3>");
title->setAlignment(Qt::AlignCenter);
layout->addWidget(title);
auto *desc = new QLabel(
"Пожалуйста, не закрывайте приложение и не трогайте телефон.\n"
"Проверка может занять некоторое время."
);
desc->setWordWrap(true);
desc->setAlignment(Qt::AlignCenter);
desc->setStyleSheet("color: gray; margin-bottom: 12px;");
layout->addWidget(desc);
m_progressBar = new QProgressBar;
m_progressBar->setRange(0, 100);
m_progressBar->setValue(0);
m_progressBar->setTextVisible(true);
layout->addWidget(m_progressBar);
m_progressLabel = new QLabel("Подготовка...");
m_progressLabel->setAlignment(Qt::AlignCenter);
m_progressLabel->setStyleSheet("margin-top: 8px;");
layout->addWidget(m_progressLabel);
layout->addStretch();
auto *btnLayout = new QHBoxLayout;
m_backBtn3 = new QPushButton("Назад");
connect(m_backBtn3, &QPushButton::clicked, this, [this]() {
stopWorkerThread();
m_progressBar->setValue(0);
m_progressLabel->setText("Подготовка...");
m_stack->setCurrentIndex(1);
});
btnLayout->addWidget(m_backBtn3);
btnLayout->addStretch();
layout->addLayout(btnLayout);
return page;
}
// ── Step 4a: Success ───────────────────────────────────────────────────────
QWidget *BankSetupWizard::createStep4Success() {
m_successPage = new QWidget;
auto *layout = new QVBoxLayout(m_successPage);
layout->setAlignment(Qt::AlignTop);
auto *title = new QLabel("<h3 style='color:green;'>Проверка прошла успешно</h3>");
title->setAlignment(Qt::AlignCenter);
layout->addWidget(title);
m_resultLabel = new QLabel;
m_resultLabel->setWordWrap(true);
m_resultLabel->setTextFormat(Qt::RichText);
layout->addWidget(m_resultLabel);
auto *cardsTitle = new QLabel("<b>Найденные карты:</b>");
layout->addWidget(cardsTitle);
m_cardsTable = new QTableWidget;
m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection);
m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_cardsTable->setFocusPolicy(Qt::NoFocus);
m_cardsTable->setColumnCount(3);
m_cardsTable->setHorizontalHeaderLabels({"Номер", "Сумма", "Валюта"});
m_cardsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
layout->addWidget(m_cardsTable);
layout->addStretch();
auto *btnLayout = new QHBoxLayout;
btnLayout->addStretch();
auto *finishBtn = new QPushButton("Завершить");
finishBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;");
connect(finishBtn, &QPushButton::clicked, this, &QDialog::accept);
btnLayout->addWidget(finishBtn);
layout->addLayout(btnLayout);
return m_successPage;
}
// ── Step 4b: Error ─────────────────────────────────────────────────────────
QWidget *BankSetupWizard::createStep4Error() {
m_errorPage = new QWidget;
auto *layout = new QVBoxLayout(m_errorPage);
layout->setAlignment(Qt::AlignTop);
auto *title = new QLabel("<h3 style='color:red;'>Ошибка</h3>");
title->setAlignment(Qt::AlignCenter);
layout->addWidget(title);
m_errorText = new QLabel;
m_errorText->setWordWrap(true);
m_errorText->setStyleSheet("color: red; margin: 12px 0;");
m_errorText->setAlignment(Qt::AlignCenter);
layout->addWidget(m_errorText);
layout->addStretch();
auto *btnLayout = new QHBoxLayout;
auto *backBtn = new QPushButton("Назад");
connect(backBtn, &QPushButton::clicked, this, [this]() {
m_stack->setCurrentIndex(1);
});
btnLayout->addWidget(backBtn);
btnLayout->addStretch();
auto *restartBtn = new QPushButton("Начать заново");
restartBtn->setStyleSheet("background-color: orange; color: white; padding: 6px 24px;");
connect(restartBtn, &QPushButton::clicked, this, [this]() {
// Очищаем данные из БД если ещё не отправлены на сервер
const BankProfileInfo current = BankProfileDAO::getApplication(m_deviceId, m_app.code);
if (current.id >= 0 && current.bankProfileId.isEmpty()) {
MaterialDAO::deleteAccountsByApp(m_deviceId, m_app.code);
BankProfileDAO::deleteProfile(m_deviceId, m_app.code);
m_app.id = -1;
m_app.pinCodeChecked = false;
m_app.pinCode.clear();
}
m_pinCodeEdit->clear();
m_progressBar->setValue(0);
m_progressLabel->setText("Подготовка...");
m_stack->setCurrentIndex(0);
});
btnLayout->addWidget(restartBtn);
layout->addLayout(btnLayout);
return m_errorPage;
}
// ── 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 BankSetupWizard::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() << "[BankSetupWizard] Paused profile:" << app.code;
}
}
}
void BankSetupWizard::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() << "[BankSetupWizard] Resumed profile:" << app.code;
}
}
m_pausedProfiles.clear();
}
// ── Verification flow ──────────────────────────────────────────────────────
void BankSetupWizard::setProgress(int value, const QString &text) {
m_progressBar->setValue(value);
m_progressLabel->setText(text);
}
void BankSetupWizard::stopWorkerThread() {
if (m_workerThread) {
m_workerThread->quit();
m_workerThread->wait(3000);
m_workerThread = nullptr;
}
}
void BankSetupWizard::startVerification() {
// Проверяем что устройство онлайн
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
if (device.status != "device") {
onVerificationError("Устройство не подключено.\nУбедитесь, что телефон подключён и попробуйте снова.");
return;
}
// Save pin code to DB first via upsert
m_app.status = "off";
m_app.install = true;
BankProfileDAO::upsertApplication(m_app);
// Re-read to get the id
m_app = BankProfileDAO::getApplication(m_deviceId, m_app.code);
setProgress(5, "Запуск проверки пин-кода...");
m_backBtn3->setEnabled(true);
runPinCheck();
}
void BankSetupWizard::runPinCheck() {
stopWorkerThread();
m_workerThread = new QThread(this);
const QString deviceId = m_deviceId;
const QString appCode = m_app.code;
const int appId = m_app.id;
if (appCode == "ozon") {
auto *worker = new Ozon::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, false, true);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this,
[this, appId, appCode](const QString &result) {
if (!result.isEmpty()) {
onVerificationError("Ошибка проверки пин-кода: " + result);
return;
}
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
m_app.pinCodeChecked = true;
setProgress(33, "Пин-код проверен. Получение профиля...");
runProfileCheck();
}, Qt::QueuedConnection);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit);
} else if (appCode == "black") {
auto *worker = new Black::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start);
connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, this,
[this, appId](const QString &result) {
if (!result.isEmpty()) {
onVerificationError("Ошибка проверки пин-кода: " + result);
return;
}
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
m_app.pinCodeChecked = true;
setProgress(33, "Пин-код проверен. Получение профиля...");
runProfileCheck();
}, Qt::QueuedConnection);
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(worker, &Black::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit);
} else if (appCode == "dushanbe") {
auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, this,
[this, appId](const QString &result) {
if (!result.isEmpty()) {
onVerificationError("Ошибка проверки пин-кода: " + result);
return;
}
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
m_app.pinCodeChecked = true;
setProgress(33, "Пин-код проверен. Получение профиля...");
runProfileCheck();
}, Qt::QueuedConnection);
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit);
} else {
onVerificationError("Банк " + appCode + " не поддерживается для автоматической настройки.");
return;
}
m_workerThread->start();
}
void BankSetupWizard::runProfileCheck() {
stopWorkerThread();
m_workerThread = new QThread(this);
const QString deviceId = m_deviceId;
const QString appCode = m_app.code;
auto onProfileResult = [this, deviceId, appCode](const QString &result) {
if (!result.isEmpty()) {
onVerificationError("Ошибка получения профиля: " + result);
return;
}
// Проверяем что bank_profile_id получен от сервера
const BankProfileInfo updated = BankProfileDAO::getApplication(deviceId, appCode);
if (updated.bankProfileId.isEmpty()) {
onVerificationError("Не удалось зарегистрировать банковский профиль на сервере.\n"
"Возможно, банк ещё не поддерживается сервером.");
return;
}
setProgress(66, "Профиль получен. Проверка карт...");
runCardCheck();
};
if (appCode == "ozon") {
auto *worker = new Ozon::GetProfileInfoScript(deviceId, appCode, nullptr);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start);
connect(worker, &Ozon::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection);
connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
connect(worker, &Ozon::GetProfileInfoScript::finished, m_workerThread, &QThread::quit);
} else if (appCode == "black") {
auto *worker = new Black::GetProfileInfoScript(deviceId, appCode, nullptr);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Black::GetProfileInfoScript::start);
connect(worker, &Black::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection);
connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
connect(worker, &Black::GetProfileInfoScript::finished, m_workerThread, &QThread::quit);
} else if (appCode == "dushanbe") {
auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start);
connect(worker, &Dushanbe::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection);
connect(worker, &Dushanbe::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
connect(worker, &Dushanbe::GetProfileInfoScript::finished, m_workerThread, &QThread::quit);
} else {
onVerificationError("Банк " + appCode + " не поддерживается.");
return;
}
m_workerThread->start();
}
void BankSetupWizard::runCardCheck() {
stopWorkerThread();
m_workerThread = new QThread(this);
const QString deviceId = m_deviceId;
const QString appCode = m_app.code;
auto onCardResult = [this, deviceId, appCode](const QString &result) {
if (!result.isEmpty()) {
onVerificationError("Ошибка получения карт: " + result);
return;
}
// Проверяем что хотя бы одна карта синхронизирована с сервером
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(deviceId, appCode);
bool hasSynced = false;
for (const auto &m : materials) {
if (!m.materialId.isEmpty()) { hasSynced = true; break; }
}
if (!hasSynced && !materials.isEmpty()) {
onVerificationError("Не удалось отправить данные карт на сервер.\n"
"Проверьте подключение к серверу.");
return;
}
setProgress(100, "Готово!");
onVerificationSuccess();
};
if (appCode == "ozon") {
auto *worker = new Ozon::GetCardInfoScript(deviceId, appCode, "", nullptr);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Ozon::GetCardInfoScript::start);
connect(worker, &Ozon::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection);
connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater);
connect(worker, &Ozon::GetCardInfoScript::finished, m_workerThread, &QThread::quit);
} else if (appCode == "black") {
auto *worker = new Black::GetCardInfoScript(deviceId, appCode, nullptr);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Black::GetCardInfoScript::start);
connect(worker, &Black::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection);
connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater);
connect(worker, &Black::GetCardInfoScript::finished, m_workerThread, &QThread::quit);
} else if (appCode == "dushanbe") {
auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr);
worker->moveToThread(m_workerThread);
connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start);
connect(worker, &Dushanbe::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection);
connect(worker, &Dushanbe::GetCardInfoScript::finished, worker, &QObject::deleteLater);
connect(worker, &Dushanbe::GetCardInfoScript::finished, m_workerThread, &QThread::quit);
} else {
onVerificationError("Банк " + appCode + " не поддерживается.");
return;
}
m_workerThread->start();
}
void BankSetupWizard::onVerificationSuccess() {
// Re-read profile from DB
m_app = BankProfileDAO::getApplication(m_deviceId, m_app.code);
QString profileHtml;
if (!m_app.fullName.isEmpty())
profileHtml += "<b>ФИО:</b> " + m_app.fullName + "<br>";
if (!m_app.phone.isEmpty())
profileHtml += "<b>Телефон:</b> " + m_app.phone + "<br>";
if (!m_app.email.isEmpty())
profileHtml += "<b>Email:</b> " + m_app.email + "<br>";
if (profileHtml.isEmpty())
profileHtml = "<i>Данные профиля не получены</i>";
m_resultLabel->setText(profileHtml);
// Load cards
const QList<MaterialInfo> cards = MaterialDAO::getAccounts(m_deviceId, m_app.code);
m_cardsTable->setRowCount(cards.size());
for (int i = 0; i < cards.size(); ++i) {
const MaterialInfo &c = cards[i];
const QString displayNumber = c.cardNumber.isEmpty() ? c.lastNumbers : c.cardNumber;
m_cardsTable->setItem(i, 0, new QTableWidgetItem(displayNumber));
m_cardsTable->setItem(i, 1, new QTableWidgetItem(QString::number(c.amount)));
m_cardsTable->setItem(i, 2, new QTableWidgetItem(c.currency));
}
m_cardsTable->resizeColumnsToContents();
if (cards.isEmpty()) {
m_cardsTable->setRowCount(1);
auto *item = new QTableWidgetItem("Карты не найдены");
item->setForeground(QBrush(Qt::gray));
m_cardsTable->setItem(0, 0, item);
m_cardsTable->setSpan(0, 0, 1, 3);
}
m_stack->setCurrentIndex(3);
}
static QString humanReadableError(const QString &error) {
// Определяем этап
QString stage;
if (error.contains("пин-кода"))
stage = "Проверка пин-кода: ";
else if (error.contains("профиля"))
stage = "Получение профиля: ";
else if (error.contains("карт"))
stage = "Получение карт: ";
if (error.contains("Device or app not found"))
return stage + "Устройство или приложение не найдено.\nУбедитесь, что телефон подключён и приложение установлено.";
if (error.contains("Cant open the home screen") || error.contains("Cannot reach home screen"))
return stage + "Не удалось открыть главный экран приложения.\nПопробуйте перезапустить приложение на телефоне и повторить.";
if (error.contains("Cannot find user name") || error.contains("Cannot find Hola button"))
return stage + "Не удалось найти данные профиля на главном экране.\nВозможно, приложение обновилось или экран выглядит иначе.";
if (error.contains("Profile screen not detected") || error.contains("Profile page not detected"))
return stage + "Не удалось открыть страницу профиля.\nПопробуйте повторить позже.";
if (error.contains("Side menu not detected"))
return stage + "Не удалось открыть боковое меню приложения.\nПопробуйте повторить.";
if (error.contains("Cannot find Tu Informaci") || error.contains("Cannot find Tu CVU"))
return stage + "Не удалось найти нужный раздел в меню приложения.\nВозможно, приложение обновилось.";
if (error.contains("CVU screen not detected") || error.contains("Cannot parse CVU"))
return stage + "Не удалось получить данные карты.\nПопробуйте повторить.";
if (error.contains("No cards found"))
return stage + "Карты не найдены на главном экране.\nУбедитесь, что в приложении есть привязанные карты.";
if (error.contains("не поддерживается") || error.contains("не подключено"))
return error;
return stage + "Произошла непредвиденная ошибка.\nПопробуйте повторить позже.";
}
void BankSetupWizard::onVerificationError(const QString &error) {
qWarning() << "[BankSetupWizard] Error:" << error;
m_errorText->setText(humanReadableError(error));
m_stack->setCurrentIndex(4);
}

View File

@ -0,0 +1,64 @@
#pragma once
#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QProgressBar>
#include <QPushButton>
#include <QStackedWidget>
#include <QTableWidget>
#include <QThread>
#include "db/BankProfileInfo.h"
class BankSetupWizard final : public QDialog {
Q_OBJECT
public:
BankSetupWizard(QWidget *parent, const QString &deviceId, const BankProfileInfo &app);
~BankSetupWizard() override;
private:
QString m_deviceId;
BankProfileInfo m_app;
QStackedWidget *m_stack;
// Step 2
QLineEdit *m_pinCodeEdit;
// Step 3
QLabel *m_progressLabel;
QProgressBar *m_progressBar;
QPushButton *m_backBtn3;
// Step 4
QLabel *m_resultLabel;
QTableWidget *m_cardsTable;
QWidget *m_successPage;
QWidget *m_errorPage;
QLabel *m_errorText;
QThread *m_workerThread = nullptr;
QStringList m_pausedProfiles; // коды профилей, выключенных на время мастера
void pauseActiveProfiles();
void resumePausedProfiles();
QWidget *createStep1();
QWidget *createStep2();
QWidget *createStep3();
QWidget *createStep4Success();
QWidget *createStep4Error();
void startVerification();
void runPinCheck();
void runProfileCheck();
void runCardCheck();
void onVerificationSuccess();
void onVerificationError(const QString &error);
void stopWorkerThread();
void setProgress(int value, const QString &text);
};

View File

@ -9,6 +9,7 @@
#include <QSettings>
#include "bank/AccountSettingsWindow.h"
#include "bank/BankSetupWizard.h"
#include "dao/BankProfileDAO.h"
#include "db/BankProfileInfo.h"
@ -139,17 +140,30 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->setItem(k, 5, commentItem);
// Кнопка "Настроить"
auto *settingsBtn = new QPushButton("Настроить", tableWidget);
settingsBtn->setStyleSheet("background-color: green; color: white; border-radius: 4px; height: 26px;");
tableWidget->setCellWidget(k, 6, settingsBtn);
if (app.install) {
auto *settingsBtn = new QPushButton("Настроить", tableWidget);
settingsBtn->setStyleSheet("background-color: green; color: white; border-radius: 4px; height: 26px;");
tableWidget->setCellWidget(k, 6, settingsBtn);
connect(settingsBtn, &QPushButton::clicked, this, [this, app]() {
auto *window = new AccountSettingsWindow(nullptr, m_deviceId, m_deviceName, app);
window->setWindowFlags(window->windowFlags() | Qt::Window);
window->setAttribute(Qt::WA_DeleteOnClose);
window->show();
window->raise();
window->activateWindow();
});
connect(settingsBtn, &QPushButton::clicked, this, [this, app, tableWidget]() {
const BankProfileInfo dbApp = BankProfileDAO::getApplication(m_deviceId, app.code);
if (dbApp.id < 0 || dbApp.bankProfileId.isEmpty()) {
auto *wizard = new BankSetupWizard(this, m_deviceId, dbApp.id >= 0 ? dbApp : app);
wizard->setAttribute(Qt::WA_DeleteOnClose);
connect(wizard, &QDialog::finished, this, [this, tableWidget]() {
reloadContent(tableWidget);
tableWidget->resizeColumnsToContents();
});
wizard->show();
} else {
auto *window = new AccountSettingsWindow(nullptr, m_deviceId, m_deviceName, dbApp);
window->setWindowFlags(window->windowFlags() | Qt::Window);
window->setAttribute(Qt::WA_DeleteOnClose);
window->show();
window->raise();
window->activateWindow();
}
});
}
}
}

View File

@ -13,6 +13,8 @@
#include <QMessageBox>
#include "bank/AccountSettingsWindow.h"
#include "bank/BankSetupWizard.h"
#include "dao/BankProfileDAO.h"
#include "bank/BankSettingsWindow.h"
#include "DeviceSettingsWindow.h"
#include "dao/BankProfileDAO.h"
@ -236,19 +238,22 @@ void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileIn
QString deviceId = m_device.id;
QString deviceName = m_device.name;
connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() {
static QPointer<AccountSettingsWindow> bankSettingsWindow;
// Если окна нет (либо ещё не было создано, либо уже удалилось) — создаём заново
if (!bankSettingsWindow) {
bankSettingsWindow = new AccountSettingsWindow(nullptr, deviceId, deviceName, app);
bankSettingsWindow->setWindowFlags(bankSettingsWindow->windowFlags() | Qt::Window);
bankSettingsWindow->setAttribute(Qt::WA_DeleteOnClose);
const BankProfileInfo dbApp = BankProfileDAO::getApplication(deviceId, app.code);
if (dbApp.id < 0 || dbApp.bankProfileId.isEmpty()) {
auto *wizard = new BankSetupWizard(nullptr, deviceId, dbApp.id >= 0 ? dbApp : app);
wizard->setAttribute(Qt::WA_DeleteOnClose);
wizard->show();
} else {
static QPointer<AccountSettingsWindow> bankSettingsWindow;
if (!bankSettingsWindow) {
bankSettingsWindow = new AccountSettingsWindow(nullptr, deviceId, deviceName, dbApp);
bankSettingsWindow->setWindowFlags(bankSettingsWindow->windowFlags() | Qt::Window);
bankSettingsWindow->setAttribute(Qt::WA_DeleteOnClose);
}
bankSettingsWindow->show();
bankSettingsWindow->raise();
bankSettingsWindow->activateWindow();
}
// Показать/поднять/активировать
bankSettingsWindow->show();
bankSettingsWindow->raise();
bankSettingsWindow->activateWindow();
});
}

View File

@ -17,6 +17,7 @@
#include "dao/EventDAO.h"
#include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.h"
#include "log/LogDetailWindow.h"
static constexpr int COL_ID = 0;
static constexpr int COL_TIME = 1;
@ -96,6 +97,13 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) {
showScreenshot(row);
});
connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
if (row >= 0 && row < m_entries.size()) {
auto *detail = new LogDetailWindow(m_entries[row], this);
detail->setAttribute(Qt::WA_DeleteOnClose);
detail->show();
}
});
connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogToTelegram);
loadPage();

View File

@ -0,0 +1,95 @@
#include "LogDetailWindow.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QTextEdit>
#include <QPushButton>
#include <QClipboard>
#include <QApplication>
#include <QPixmap>
#include <QScrollArea>
LogDetailWindow::LogDetailWindow(const GeneralLogEntry &entry, QWidget *parent)
: QDialog(parent) {
setWindowTitle("Лог #" + QString::number(entry.id));
resize(700, 500);
auto *layout = new QVBoxLayout(this);
// Заголовок
auto *headerLayout = new QHBoxLayout;
auto *idLabel = new QLabel("<b>#" + QString::number(entry.id) + "</b>");
headerLayout->addWidget(idLabel);
auto *levelLabel = new QLabel(entry.level);
if (entry.level == "WARNING")
levelLabel->setStyleSheet("color: orange; font-weight: bold;");
else if (entry.level == "CRITICAL" || entry.level == "FATAL")
levelLabel->setStyleSheet("color: red; font-weight: bold;");
else
levelLabel->setStyleSheet("color: gray;");
headerLayout->addWidget(levelLabel);
auto *timeLabel = new QLabel(entry.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss"));
timeLabel->setStyleSheet("color: gray;");
headerLayout->addWidget(timeLabel);
headerLayout->addStretch();
layout->addLayout(headerLayout);
// Источник
auto *sourceLabel = new QLabel("<b>Источник:</b> " + entry.source);
sourceLabel->setWordWrap(true);
sourceLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
layout->addWidget(sourceLabel);
// Сообщение
layout->addWidget(new QLabel("<b>Сообщение:</b>"));
auto *messageEdit = new QTextEdit;
messageEdit->setPlainText(entry.message);
messageEdit->setReadOnly(true);
layout->addWidget(messageEdit, 1);
// Скриншот
if (!entry.screenshot.isEmpty()) {
QPixmap pixmap;
pixmap.loadFromData(entry.screenshot);
if (!pixmap.isNull()) {
layout->addWidget(new QLabel("<b>Скриншот:</b>"));
auto *imgLabel = new QLabel;
imgLabel->setPixmap(pixmap.scaledToWidth(
qMin(pixmap.width(), 600), Qt::SmoothTransformation));
imgLabel->setAlignment(Qt::AlignCenter);
auto *scrollArea = new QScrollArea;
scrollArea->setWidget(imgLabel);
scrollArea->setWidgetResizable(false);
scrollArea->setMaximumHeight(300);
layout->addWidget(scrollArea);
}
}
// Кнопки
auto *btnLayout = new QHBoxLayout;
auto *copyBtn = new QPushButton("Копировать");
copyBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 16px;");
connect(copyBtn, &QPushButton::clicked, this, [entry]() {
const QString text = QString("ID: %1\nУровень: %2\nВремя: %3\nИсточник: %4\nСообщение: %5")
.arg(entry.id)
.arg(entry.level)
.arg(entry.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss"))
.arg(entry.source)
.arg(entry.message);
QApplication::clipboard()->setText(text);
});
btnLayout->addWidget(copyBtn);
btnLayout->addStretch();
auto *closeBtn = new QPushButton("Закрыть");
connect(closeBtn, &QPushButton::clicked, this, &QDialog::close);
btnLayout->addWidget(closeBtn);
layout->addLayout(btnLayout);
}

View File

@ -0,0 +1,11 @@
#pragma once
#include <QDialog>
#include "dao/GeneralLogDAO.h"
class LogDetailWindow final : public QDialog {
Q_OBJECT
public:
explicit LogDetailWindow(const GeneralLogEntry &entry, QWidget *parent = nullptr);
};