diff --git a/android/black/CommonScript.cpp b/android/black/CommonScript.cpp index 416893f..6374132 100644 --- a/android/black/CommonScript.cpp +++ b/android/black/CommonScript.cpp @@ -209,7 +209,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr } const QMap 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); diff --git a/android/black/GetProfileInfoScript.cpp b/android/black/GetProfileInfoScript.cpp index 28ded8a..a2d7286 100644 --- a/android/black/GetProfileInfoScript.cpp +++ b/android/black/GetProfileInfoScript.cpp @@ -174,7 +174,6 @@ void GetProfileInfoScript::doStart() { postAccountsData(accountsToPost); } - AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); } diff --git a/android/black/LoginAndCheckAccountsScript.cpp b/android/black/LoginAndCheckAccountsScript.cpp index 51135bf..a072dba 100644 --- a/android/black/LoginAndCheckAccountsScript.cpp +++ b/android/black/LoginAndCheckAccountsScript.cpp @@ -51,7 +51,6 @@ void LoginAndCheckAccountsScript::doStart() { } if (m_pinCheckOnly) { - AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; } diff --git a/android/dushanbe/CommonScript.cpp b/android/dushanbe/CommonScript.cpp new file mode 100644 index 0000000..f03feac --- /dev/null +++ b/android/dushanbe/CommonScript.cpp @@ -0,0 +1,253 @@ +#include "CommonScript.h" + +#include +#include +#include +#include +#include +#include + +#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> &accounts) { + if (accounts.isEmpty()) { + return; + } + + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonArray list; + for (const QPair &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 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 diff --git a/android/dushanbe/CommonScript.h b/android/dushanbe/CommonScript.h new file mode 100644 index 0000000..ea13920 --- /dev/null +++ b/android/dushanbe/CommonScript.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#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> &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 diff --git a/android/dushanbe/GetCardInfoScript.cpp b/android/dushanbe/GetCardInfoScript.cpp new file mode 100644 index 0000000..a23b03a --- /dev/null +++ b/android/dushanbe/GetCardInfoScript.cpp @@ -0,0 +1,202 @@ +#include "GetCardInfoScript.h" + +#include +#include +#include + +#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 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 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 diff --git a/android/dushanbe/GetCardInfoScript.h b/android/dushanbe/GetCardInfoScript.h new file mode 100644 index 0000000..e2001e3 --- /dev/null +++ b/android/dushanbe/GetCardInfoScript.h @@ -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 diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp new file mode 100644 index 0000000..32ff207 --- /dev/null +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -0,0 +1,250 @@ +#include "GetLastTransactionsScript.h" + +#include +#include + +#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 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 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 diff --git a/android/dushanbe/GetLastTransactionsScript.h b/android/dushanbe/GetLastTransactionsScript.h new file mode 100644 index 0000000..52327ad --- /dev/null +++ b/android/dushanbe/GetLastTransactionsScript.h @@ -0,0 +1,34 @@ +#pragma once +#include "CommonScript.h" +#include +#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 diff --git a/android/dushanbe/GetProfileInfoScript.cpp b/android/dushanbe/GetProfileInfoScript.cpp new file mode 100644 index 0000000..b0cb5d6 --- /dev/null +++ b/android/dushanbe/GetProfileInfoScript.cpp @@ -0,0 +1,122 @@ +#include "GetProfileInfoScript.h" + +#include +#include +#include + +#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 allAccounts = MaterialDAO::getAccounts(device.id, m_appCode); + QList> 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 diff --git a/android/dushanbe/GetProfileInfoScript.h b/android/dushanbe/GetProfileInfoScript.h new file mode 100644 index 0000000..5774ca0 --- /dev/null +++ b/android/dushanbe/GetProfileInfoScript.h @@ -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 diff --git a/android/dushanbe/LoginAndCheckAccountsScript.cpp b/android/dushanbe/LoginAndCheckAccountsScript.cpp new file mode 100644 index 0000000..67e0099 --- /dev/null +++ b/android/dushanbe/LoginAndCheckAccountsScript.cpp @@ -0,0 +1,63 @@ +#include "LoginAndCheckAccountsScript.h" + +#include + +#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 diff --git a/android/dushanbe/LoginAndCheckAccountsScript.h b/android/dushanbe/LoginAndCheckAccountsScript.h new file mode 100644 index 0000000..af99e1d --- /dev/null +++ b/android/dushanbe/LoginAndCheckAccountsScript.h @@ -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 diff --git a/android/dushanbe/ScreenXmlParser.cpp b/android/dushanbe/ScreenXmlParser.cpp new file mode 100644 index 0000000..90a191b --- /dev/null +++ b/android/dushanbe/ScreenXmlParser.cpp @@ -0,0 +1,477 @@ +#include "ScreenXmlParser.h" + +#include +#include +#include + +#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 &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::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 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::parseTransactionItems() { + // Транзакции — ImageView с content-desc формата: + // "9762***3258\n992882770011\n493.00 \nDC (по номеру телефона)\n 18:57:10" + QList 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 diff --git a/android/dushanbe/ScreenXmlParser.h b/android/dushanbe/ScreenXmlParser.h new file mode 100644 index 0000000..66ab386 --- /dev/null +++ b/android/dushanbe/ScreenXmlParser.h @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#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 &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 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 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 m_nodes; + QDomElement m_xml; +}; + +} // namespace Dushanbe diff --git a/android/ozon/CommonScript.cpp b/android/ozon/CommonScript.cpp index 2f53b05..120a1cc 100644 --- a/android/ozon/CommonScript.cpp +++ b/android/ozon/CommonScript.cpp @@ -312,7 +312,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr } const QMap 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); diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index 56092eb..a291404 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -193,7 +193,7 @@ void GetCardInfoScript::doStart() { // ISO 4217 numeric currency codes const QMap 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); diff --git a/android/ozon/GetProfileInfoScript.cpp b/android/ozon/GetProfileInfoScript.cpp index d941d82..3c980e3 100644 --- a/android/ozon/GetProfileInfoScript.cpp +++ b/android/ozon/GetProfileInfoScript.cpp @@ -122,7 +122,7 @@ void GetProfileInfoScript::doStart() { const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode); const QMap 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); } diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp index c7ce4c7..0b98933 100644 --- a/android/ozon/LoginAndCheckAccountsScript.cpp +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -54,7 +54,6 @@ void LoginAndCheckAccountsScript::doStart() { return; } else { if (m_pinCheckOnly) { - AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; } diff --git a/assets/dushanbe/card_detail.png b/assets/dushanbe/card_detail.png new file mode 100644 index 0000000..ee5f9a6 Binary files /dev/null and b/assets/dushanbe/card_detail.png differ diff --git a/assets/dushanbe/card_detail.xml b/assets/dushanbe/card_detail.xml new file mode 100644 index 0000000..e138aaf --- /dev/null +++ b/assets/dushanbe/card_detail.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/card_detail_2.png b/assets/dushanbe/card_detail_2.png new file mode 100644 index 0000000..6acb18e Binary files /dev/null and b/assets/dushanbe/card_detail_2.png differ diff --git a/assets/dushanbe/card_detail_2.xml b/assets/dushanbe/card_detail_2.xml new file mode 100644 index 0000000..395c68c --- /dev/null +++ b/assets/dushanbe/card_detail_2.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/cards_screen.png b/assets/dushanbe/cards_screen.png new file mode 100644 index 0000000..fbc6b5c Binary files /dev/null and b/assets/dushanbe/cards_screen.png differ diff --git a/assets/dushanbe/cards_screen.xml b/assets/dushanbe/cards_screen.xml new file mode 100644 index 0000000..c7f0c49 --- /dev/null +++ b/assets/dushanbe/cards_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/history_screen.png b/assets/dushanbe/history_screen.png new file mode 100644 index 0000000..29e189d Binary files /dev/null and b/assets/dushanbe/history_screen.png differ diff --git a/assets/dushanbe/history_screen.xml b/assets/dushanbe/history_screen.xml new file mode 100644 index 0000000..f178473 --- /dev/null +++ b/assets/dushanbe/history_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/home_screen.png b/assets/dushanbe/home_screen.png new file mode 100644 index 0000000..1676921 Binary files /dev/null and b/assets/dushanbe/home_screen.png differ diff --git a/assets/dushanbe/home_screen.xml b/assets/dushanbe/home_screen.xml new file mode 100644 index 0000000..6da6324 --- /dev/null +++ b/assets/dushanbe/home_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/launch_screen.png b/assets/dushanbe/launch_screen.png new file mode 100644 index 0000000..65d8a33 Binary files /dev/null and b/assets/dushanbe/launch_screen.png differ diff --git a/assets/dushanbe/launch_screen.xml b/assets/dushanbe/launch_screen.xml new file mode 100644 index 0000000..2dc70d5 --- /dev/null +++ b/assets/dushanbe/launch_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/side_menu.png b/assets/dushanbe/side_menu.png new file mode 100644 index 0000000..16fcfd8 Binary files /dev/null and b/assets/dushanbe/side_menu.png differ diff --git a/assets/dushanbe/side_menu.xml b/assets/dushanbe/side_menu.xml new file mode 100644 index 0000000..fab331e --- /dev/null +++ b/assets/dushanbe/side_menu.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/tx_detail.png b/assets/dushanbe/tx_detail.png new file mode 100644 index 0000000..be66fdd Binary files /dev/null and b/assets/dushanbe/tx_detail.png differ diff --git a/assets/dushanbe/tx_detail.xml b/assets/dushanbe/tx_detail.xml new file mode 100644 index 0000000..e919b60 --- /dev/null +++ b/assets/dushanbe/tx_detail.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/config.ini b/config.ini index 71851d0..c4204d3 100644 --- a/config.ini +++ b/config.ini @@ -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 + diff --git a/database/dao/BankProfileDAO.cpp b/database/dao/BankProfileDAO.cpp index 1e593bb..b913302 100644 --- a/database/dao/BankProfileDAO.cpp +++ b/database/dao/BankProfileDAO.cpp @@ -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; +} diff --git a/database/dao/BankProfileDAO.h b/database/dao/BankProfileDAO.h index 3a4c05c..7d98d5a 100644 --- a/database/dao/BankProfileDAO.h +++ b/database/dao/BankProfileDAO.h @@ -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); }; diff --git a/database/dao/MaterialDAO.cpp b/database/dao/MaterialDAO.cpp index 46e8c84..af7097e 100644 --- a/database/dao/MaterialDAO.cpp +++ b/database/dao/MaterialDAO.cpp @@ -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; +} diff --git a/database/dao/MaterialDAO.h b/database/dao/MaterialDAO.h index f162647..cef8bc5 100644 --- a/database/dao/MaterialDAO.h +++ b/database/dao/MaterialDAO.h @@ -13,6 +13,8 @@ public: const QString ¤cy ); + [[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 &excludeDevices); [[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId); + + [[nodiscard]] static bool deleteAccountsByApp(const QString &deviceId, const QString &appCode); }; diff --git a/images/dushanbe_icon.png b/images/dushanbe_icon.png new file mode 100644 index 0000000..c542850 Binary files /dev/null and b/images/dushanbe_icon.png differ diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index 26826a5..b8b663f 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -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 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; } diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index 7fba94c..b728ccb 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -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(); } diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 5ddbecf..5a9635e 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -1,29 +1,23 @@ #include "AccountSettingsWindow.h" -#include -#include -#include +#include +#include +#include #include #include -#include -#include -#include -#include +#include +#include #include +#include #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" - ? "Активен" - : "Неактивен") + - QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked - ? "Проверен" - : "Не проверен"); - 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 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 - ? "Активен" - : "Неактивен") + - QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked - ? "Проверен" - : "Не проверен"); - 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 = "Данные не получены"; - } 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() - ? "● Не синхронизирован" - : "● Синхронизирован"; - 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("Счета")); - 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 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 + ? "Активен" + : "Неактивен"); + 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 + ? "" + pinDisplay + " (проверен)" + : "" + pinDisplay + " (не проверен)"; + m_pinLabel->setText(pinStatus); + + // Профиль + if (m_app.fullName.isEmpty() && m_app.phone.isEmpty()) { + m_profileLabel->setText("Данные профиля не получены"); + } 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() + ? " ● Не синхронизирован" + : " ● Синхронизирован"; + 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 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 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 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("Статус: Неактивен") + - QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked - ? "Проверен" - : "Не проверен"); - m_statusLabel->setText(status); - - QMessageBox::information(this, "Профиль выключен", - "Все карты выключены — банковский профиль автоматически деактивирован."); - } - } - - reloadContent(tableWidget); - tableWidget->resizeColumnsToContents(); } -void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { - QList 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 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(); } diff --git a/views/bank/AccountSettingsWindow.h b/views/bank/AccountSettingsWindow.h index 53e25ab..13386da 100644 --- a/views/bank/AccountSettingsWindow.h +++ b/views/bank/AccountSettingsWindow.h @@ -1,7 +1,11 @@ #pragma once + #include +#include #include +#include #include +#include #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 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; }; diff --git a/views/bank/BankSettingsWindow.cpp b/views/bank/BankSettingsWindow.cpp index 87f5a14..54073c8 100644 --- a/views/bank/BankSettingsWindow.cpp +++ b/views/bank/BankSettingsWindow.cpp @@ -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; diff --git a/views/bank/BankSetupWizard.cpp b/views/bank/BankSetupWizard.cpp new file mode 100644 index 0000000..2c64000 --- /dev/null +++ b/views/bank/BankSetupWizard.cpp @@ -0,0 +1,605 @@ +#include "BankSetupWizard.h" + +#include +#include +#include +#include + +#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("

" + m_app.name + "

"); + title->setAlignment(Qt::AlignCenter); + layout->addWidget(title); + + auto *desc = new QLabel( + "Мастер поможет настроить банковский профиль для приложения " + m_app.name + ".

" + "Будут выполнены следующие шаги:
" + "1. Ввод пин-кода
" + "2. Проверка входа в приложение
" + "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("

Введите пин-код

"); + 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("

Идёт проверка банка

"); + 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("

Проверка прошла успешно

"); + 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("Найденные карты:"); + 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("

Ошибка

"); + 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 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 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 += "ФИО: " + m_app.fullName + "
"; + if (!m_app.phone.isEmpty()) + profileHtml += "Телефон: " + m_app.phone + "
"; + if (!m_app.email.isEmpty()) + profileHtml += "Email: " + m_app.email + "
"; + if (profileHtml.isEmpty()) + profileHtml = "Данные профиля не получены"; + m_resultLabel->setText(profileHtml); + + // Load cards + const QList 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); +} diff --git a/views/bank/BankSetupWizard.h b/views/bank/BankSetupWizard.h new file mode 100644 index 0000000..11236db --- /dev/null +++ b/views/bank/BankSetupWizard.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#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); +}; diff --git a/views/device/DeviceSettingsWindow.cpp b/views/device/DeviceSettingsWindow.cpp index cbf8442..f05bfde 100644 --- a/views/device/DeviceSettingsWindow.cpp +++ b/views/device/DeviceSettingsWindow.cpp @@ -9,6 +9,7 @@ #include #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(); + } + }); + } } } diff --git a/views/device/DeviceWidget.cpp b/views/device/DeviceWidget.cpp index 664e805..2ddc18c 100644 --- a/views/device/DeviceWidget.cpp +++ b/views/device/DeviceWidget.cpp @@ -13,6 +13,8 @@ #include #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 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 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(); }); } diff --git a/views/log/FileLogWindow.cpp b/views/log/FileLogWindow.cpp index fede7c7..bc0f62a 100644 --- a/views/log/FileLogWindow.cpp +++ b/views/log/FileLogWindow.cpp @@ -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(); diff --git a/views/log/LogDetailWindow.cpp b/views/log/LogDetailWindow.cpp new file mode 100644 index 0000000..110949d --- /dev/null +++ b/views/log/LogDetailWindow.cpp @@ -0,0 +1,95 @@ +#include "LogDetailWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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("#" + QString::number(entry.id) + ""); + 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("Источник: " + entry.source); + sourceLabel->setWordWrap(true); + sourceLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + layout->addWidget(sourceLabel); + + // Сообщение + layout->addWidget(new QLabel("Сообщение:")); + 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("Скриншот:")); + 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); +} diff --git a/views/log/LogDetailWindow.h b/views/log/LogDetailWindow.h new file mode 100644 index 0000000..661b4d4 --- /dev/null +++ b/views/log/LogDetailWindow.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include "dao/GeneralLogDAO.h" + +class LogDetailWindow final : public QDialog { + Q_OBJECT + +public: + explicit LogDetailWindow(const GeneralLogEntry &entry, QWidget *parent = nullptr); +};