Add CommonScript framework and implement bank-specific automation scripts including GetCardInfoScript, GetProfileInfoScript, and GetLastTransactionsScript.
@ -209,7 +209,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QMap<QString, int> currencyCodes = {
|
const QMap<QString, int> currencyCodes = {
|
||||||
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
|
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
|
||||||
};
|
};
|
||||||
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 840);
|
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 840);
|
||||||
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);
|
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);
|
||||||
|
|||||||
@ -174,7 +174,6 @@ void GetProfileInfoScript::doStart() {
|
|||||||
postAccountsData(accountsToPost);
|
postAccountsData(accountsToPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
|
||||||
emit finishedWithResult(m_error);
|
emit finishedWithResult(m_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,6 @@ void LoginAndCheckAccountsScript::doStart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (m_pinCheckOnly) {
|
if (m_pinCheckOnly) {
|
||||||
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
|
||||||
emit finishedWithResult(m_error);
|
emit finishedWithResult(m_error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
253
android/dushanbe/CommonScript.cpp
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
#include "CommonScript.h"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include "adb/AdbUtils.h"
|
||||||
|
#include "dao/DeviceDAO.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
|
#include "net/NetworkService.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
CommonScript::CommonScript(QObject *parent)
|
||||||
|
: QObject(parent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonScript::~CommonScript() = default;
|
||||||
|
|
||||||
|
void CommonScript::start() {
|
||||||
|
doStart();
|
||||||
|
emit finished();
|
||||||
|
}
|
||||||
|
|
||||||
|
Node CommonScript::swipeToButton(
|
||||||
|
const QString &deviceId,
|
||||||
|
const QString &text, int &counter, const int width, const int height
|
||||||
|
) {
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
const int x = width / 2;
|
||||||
|
const int offset = height / 4;
|
||||||
|
AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset);
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter));
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CommonScript::inputPinCode(const QString &deviceId, const QString &pinCode) {
|
||||||
|
// DC Next использует кнопочную сетку для ввода PIN (content-desc = "0"-"9")
|
||||||
|
for (const QChar &digit : pinCode) {
|
||||||
|
Node button = xmlScreenParser.findNodeByContentDesc(QString(digit));
|
||||||
|
if (button.isEmpty()) {
|
||||||
|
qWarning() << "[Dushanbe::inputPinCode] Button not found for digit:" << digit;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AdbUtils::makeTap(deviceId, button.x(), button.y());
|
||||||
|
QThread::msleep(300);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CommonScript::runAppAndGoToHomeScreen(
|
||||||
|
const QString &deviceId,
|
||||||
|
const QString &packageName,
|
||||||
|
const QString &pinCode,
|
||||||
|
const int width,
|
||||||
|
const int height
|
||||||
|
) {
|
||||||
|
AdbUtils::unlockScreen(deviceId, width, height);
|
||||||
|
|
||||||
|
if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) {
|
||||||
|
qDebug() << "[Dushanbe] Process already running, pid:" << pid;
|
||||||
|
AdbUtils::getScreenDumpAsXml(deviceId, 1000);
|
||||||
|
AdbUtils::tryToKillApplication(deviceId, packageName);
|
||||||
|
}
|
||||||
|
if (!AdbUtils::tryToStartApplication(deviceId, packageName)) {
|
||||||
|
AdbUtils::takeScreenshot(deviceId, "run_app_error");
|
||||||
|
qWarning() << "[Dushanbe] Process has not started";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QThread::msleep(4500);
|
||||||
|
int counter = 0;
|
||||||
|
bool findAndInputPincode = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
counter++;
|
||||||
|
|
||||||
|
if (counter > 10) {
|
||||||
|
AdbUtils::takeScreenshot(deviceId, "too_many_attempts");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
|
||||||
|
if (xmlScreenParser.isHomeScreen()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!findAndInputPincode && xmlScreenParser.isPinCodeScreen()) {
|
||||||
|
inputPinCode(deviceId, pinCode);
|
||||||
|
findAndInputPincode = true;
|
||||||
|
QThread::msleep(3000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QThread::msleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommonScript::postAccountsData(const QList<QPair<MaterialInfo, Node>> &accounts) {
|
||||||
|
if (accounts.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *thread = new QThread;
|
||||||
|
auto *worker = new NetworkService;
|
||||||
|
|
||||||
|
QJsonArray list;
|
||||||
|
for (const QPair<MaterialInfo, Node> &pair : accounts) {
|
||||||
|
QJsonObject obj;
|
||||||
|
MaterialInfo account = pair.first;
|
||||||
|
obj["appName"] = account.appCode;
|
||||||
|
obj["lastNumbers"] = account.lastNumbers;
|
||||||
|
obj["amount"] = account.amount;
|
||||||
|
obj["description"] = account.description;
|
||||||
|
obj["currency"] = account.currency;
|
||||||
|
obj["status"] = materialStatusToString(account.status);
|
||||||
|
list.append(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
worker->setDeviceId(accounts.first().first.deviceId);
|
||||||
|
worker->setPayload(QJsonDocument(list).toJson());
|
||||||
|
worker->moveToThread(thread);
|
||||||
|
connect(thread, &QThread::started, worker, &NetworkService::postAccountsData);
|
||||||
|
connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||||||
|
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) {
|
||||||
|
auto *thread = new QThread;
|
||||||
|
auto *worker = new NetworkService;
|
||||||
|
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["id"] = transaction.id;
|
||||||
|
obj["amount"] = transaction.amount;
|
||||||
|
obj["name"] = transaction.name;
|
||||||
|
obj["fee"] = transaction.fee;
|
||||||
|
obj["status"] = transactionStatusToString(transaction.status);
|
||||||
|
obj["type"] = transactionTypeToString(transaction.type);
|
||||||
|
obj["phone"] = transaction.phone;
|
||||||
|
obj["description"] = transaction.description;
|
||||||
|
obj["updatedDescription"] = transaction.updatedDescription;
|
||||||
|
obj["bankName"] = transaction.bankName;
|
||||||
|
obj["bankTime"] = transaction.bankTime;
|
||||||
|
obj["bankTransactionId"] = transaction.bankTrExternalId;
|
||||||
|
|
||||||
|
worker->setDeviceId(account.deviceId);
|
||||||
|
worker->addExtra("lastNumbers", account.lastNumbers);
|
||||||
|
worker->addExtra("transactionId", transaction.id);
|
||||||
|
worker->setPayload(QJsonDocument(obj).toJson());
|
||||||
|
worker->moveToThread(thread);
|
||||||
|
connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData);
|
||||||
|
connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||||||
|
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommonScript::updateTransactionData(
|
||||||
|
const MaterialInfo &account,
|
||||||
|
const int transactionId,
|
||||||
|
const TransactionStatus status,
|
||||||
|
const QString &oldDesc,
|
||||||
|
const QString &newDesc
|
||||||
|
) {
|
||||||
|
auto *thread = new QThread;
|
||||||
|
auto *worker = new NetworkService;
|
||||||
|
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["id"] = transactionId;
|
||||||
|
obj["status"] = transactionStatusToString(status);
|
||||||
|
obj["description"] = newDesc;
|
||||||
|
obj["updatedDescription"] = oldDesc;
|
||||||
|
|
||||||
|
worker->setDeviceId(account.deviceId);
|
||||||
|
worker->addExtra("lastNumbers", account.lastNumbers);
|
||||||
|
worker->setPayload(QJsonDocument(obj).toJson());
|
||||||
|
worker->moveToThread(thread);
|
||||||
|
connect(thread, &QThread::started, worker, &NetworkService::updateTransactionData);
|
||||||
|
connect(worker, &NetworkService::finished, thread, &QThread::quit);
|
||||||
|
connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) {
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
||||||
|
|
||||||
|
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
|
||||||
|
qWarning() << "[Dushanbe] createBankProfileIfNeeded: skipping — fullName or phone is empty";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QMap<QString, int> currencyCodes = {
|
||||||
|
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
|
||||||
|
};
|
||||||
|
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 840);
|
||||||
|
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
const QSettings settings("config.ini", QSettings::IniFormat);
|
||||||
|
const QString phoneCode = settings.value(appCode + "/phone_code").toString();
|
||||||
|
QString phone = app.phone;
|
||||||
|
phone.remove(QRegularExpression("[^0-9]"));
|
||||||
|
if (!phoneCode.isEmpty() && !phone.startsWith(phoneCode)) {
|
||||||
|
if (phone.startsWith('8') && phoneCode != "8") {
|
||||||
|
phone.replace(0, 1, phoneCode);
|
||||||
|
} else {
|
||||||
|
phone.prepend(phoneCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
phone.prepend('+');
|
||||||
|
|
||||||
|
const DeviceInfo device = DeviceDAO::getDeviceById(deviceId);
|
||||||
|
|
||||||
|
QJsonObject profileBody;
|
||||||
|
profileBody["first_name"] = parts.value(0);
|
||||||
|
profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString("");
|
||||||
|
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
||||||
|
profileBody["bank_name"] = appCode;
|
||||||
|
profileBody["phone_number"] = phone;
|
||||||
|
profileBody["balance"] = QString::number(qRound64(app.amount * 100));
|
||||||
|
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
|
||||||
|
profileBody["device_id"] = device.apiId;
|
||||||
|
profileBody["currency"] = currencyCode;
|
||||||
|
|
||||||
|
NetworkService ns;
|
||||||
|
ns.setDeviceId(deviceId);
|
||||||
|
ns.setPayload(QJsonDocument(profileBody).toJson());
|
||||||
|
|
||||||
|
if (app.bankProfileId.isEmpty()) {
|
||||||
|
ns.addExtra("app_id", app.id);
|
||||||
|
ns.postBankProfile();
|
||||||
|
qDebug() << "[Dushanbe] createBankProfileIfNeeded: postBankProfile done";
|
||||||
|
} else {
|
||||||
|
ns.addExtra("bank_profile_id", app.bankProfileId);
|
||||||
|
ns.patchBankProfile();
|
||||||
|
qDebug() << "[Dushanbe] createBankProfileIfNeeded: patchBankProfile done";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommonScript::stop() {
|
||||||
|
m_running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
68
android/dushanbe/CommonScript.h
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include "ScreenXmlParser.h"
|
||||||
|
#include "db/TransactionInfo.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
class CommonScript : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CommonScript(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
~CommonScript() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void doStart() = 0;
|
||||||
|
|
||||||
|
bool inputPinCode(const QString &deviceId, const QString &pinCode);
|
||||||
|
|
||||||
|
bool runAppAndGoToHomeScreen(
|
||||||
|
const QString &deviceId,
|
||||||
|
const QString &packageName,
|
||||||
|
const QString &pinCode,
|
||||||
|
int width,
|
||||||
|
int height
|
||||||
|
);
|
||||||
|
|
||||||
|
Node swipeToButton(
|
||||||
|
const QString &deviceId,
|
||||||
|
const QString &text,
|
||||||
|
int &counter, int width, int height
|
||||||
|
);
|
||||||
|
|
||||||
|
void postAccountsData(const QList<QPair<MaterialInfo, Node>> &accounts);
|
||||||
|
|
||||||
|
void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction);
|
||||||
|
|
||||||
|
void updateTransactionData(
|
||||||
|
const MaterialInfo &account,
|
||||||
|
int transactionId,
|
||||||
|
TransactionStatus status,
|
||||||
|
const QString &oldDesc,
|
||||||
|
const QString &newDesc
|
||||||
|
);
|
||||||
|
|
||||||
|
void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode);
|
||||||
|
|
||||||
|
bool m_running = true;
|
||||||
|
ScreenXmlParser xmlScreenParser;
|
||||||
|
int m_counter = 0;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void finished();
|
||||||
|
|
||||||
|
void finishedWithResult(const QString &result);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
virtual void start() final;
|
||||||
|
|
||||||
|
virtual void stop() final;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Q_DISABLE_COPY(CommonScript);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
202
android/dushanbe/GetCardInfoScript.cpp
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
#include "GetCardInfoScript.h"
|
||||||
|
|
||||||
|
#include <QThread>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
#include "adb/AdbUtils.h"
|
||||||
|
#include "dao/MaterialDAO.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
|
#include "dao/DeviceDAO.h"
|
||||||
|
#include "net/NetworkService.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
GetCardInfoScript::GetCardInfoScript(
|
||||||
|
QString deviceId,
|
||||||
|
QString appCode,
|
||||||
|
QObject *parent
|
||||||
|
) : CommonScript(parent),
|
||||||
|
m_deviceId(std::move(deviceId)),
|
||||||
|
m_appCode(std::move(appCode)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
GetCardInfoScript::~GetCardInfoScript() = default;
|
||||||
|
|
||||||
|
void GetCardInfoScript::doStart() {
|
||||||
|
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
|
||||||
|
|
||||||
|
if (device.id.isEmpty() || app.id == -1) {
|
||||||
|
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
|
||||||
|
qCritical() << m_error;
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
|
||||||
|
if (!xmlScreenParser.isHomeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::GetCardInfo] Not on home screen, restarting app...";
|
||||||
|
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
|
||||||
|
device.screenWidth, device.screenHeight)) {
|
||||||
|
m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим баланс с домашнего экрана
|
||||||
|
const double balance = xmlScreenParser.parseBalanceFromHomeScreen();
|
||||||
|
qDebug() << "[Dushanbe::GetCardInfo] balance:" << balance;
|
||||||
|
BankProfileDAO::updateAmount(app.id, balance);
|
||||||
|
|
||||||
|
// Тапаем "Карты" на домашнем экране
|
||||||
|
Node cardsButton = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Карты"));
|
||||||
|
if (cardsButton.x() == 0 && cardsButton.y() == 0) {
|
||||||
|
m_error = "Cannot find Cards button: " + m_deviceId;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdbUtils::makeTap(m_deviceId, cardsButton.x(), cardsButton.y());
|
||||||
|
QThread::msleep(3000);
|
||||||
|
|
||||||
|
// Ждём экран "Мои карты"
|
||||||
|
bool cardsScreenFound = false;
|
||||||
|
for (int attempt = 0; attempt < 5; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
if (xmlScreenParser.isCardsScreen()) {
|
||||||
|
cardsScreenFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
QThread::msleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cardsScreenFound) {
|
||||||
|
m_error = "Cards screen not detected: " + m_deviceId;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим карты из списка (lastNumbers, баланс)
|
||||||
|
const QList<ScreenXmlParser::CardInfo> cards = xmlScreenParser.parseCards();
|
||||||
|
qDebug() << "[Dushanbe::GetCardInfo] Found" << cards.size() << "cards in list";
|
||||||
|
|
||||||
|
if (cards.isEmpty()) {
|
||||||
|
m_error = "No cards found: " + m_deviceId;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим все clickable ImageView с "****" — это карточки в списке
|
||||||
|
// Пропускаем кредитные карты
|
||||||
|
QList<Node> cardNodes;
|
||||||
|
for (const Node &node : xmlScreenParser.findAllNodes()) {
|
||||||
|
if (node.className == "android.widget.ImageView" && node.clickable &&
|
||||||
|
node.contentDesc.contains("****") &&
|
||||||
|
!node.contentDesc.contains(QString::fromUtf8("Кредитная карта"))) {
|
||||||
|
cardNodes.append(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кликаем по каждой карте для получения полного номера
|
||||||
|
for (int i = 0; i < cardNodes.size(); ++i) {
|
||||||
|
if (i >= cards.size()) break;
|
||||||
|
const Node &cardNode = cardNodes[i];
|
||||||
|
|
||||||
|
AdbUtils::makeTap(m_deviceId, cardNode.x(), cardNode.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// Ждём экран детали карты
|
||||||
|
bool detailFound = false;
|
||||||
|
for (int attempt = 0; attempt < 5; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
if (xmlScreenParser.isCardDetailScreen()) {
|
||||||
|
detailFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
QThread::msleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detailFound) {
|
||||||
|
qWarning() << "[Dushanbe::GetCardInfo] Card detail screen not found for card" << i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString fullNumber = xmlScreenParser.parseFullCardNumber();
|
||||||
|
const double cardBalance = xmlScreenParser.parseCardDetailBalance();
|
||||||
|
const QString holder = xmlScreenParser.parseCardDetailHolder();
|
||||||
|
const QString lastNumbers = cards[i].lastNumbers.isEmpty()
|
||||||
|
? (fullNumber.length() >= 4 ? fullNumber.right(4).remove(' ') : "")
|
||||||
|
: cards[i].lastNumbers;
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::GetCardInfo] Card" << i << ":" << fullNumber
|
||||||
|
<< "balance:" << cardBalance << "holder:" << holder;
|
||||||
|
|
||||||
|
if (lastNumbers.isEmpty()) {
|
||||||
|
qWarning() << "[Dushanbe::GetCardInfo] No card number for card" << i;
|
||||||
|
AdbUtils::goBack(m_deviceId);
|
||||||
|
QThread::msleep(1500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialInfo account;
|
||||||
|
account.deviceId = device.id;
|
||||||
|
account.appCode = m_appCode;
|
||||||
|
account.lastNumbers = lastNumbers;
|
||||||
|
account.cardNumber = QString(fullNumber).remove(' ');
|
||||||
|
account.amount = cardBalance;
|
||||||
|
account.currency = "TJS";
|
||||||
|
account.description = cards[i].description.isEmpty() ? holder : cards[i].description;
|
||||||
|
account.status = MaterialStatus::Active;
|
||||||
|
account.updateTime = QDateTime::currentDateTimeUtc();
|
||||||
|
|
||||||
|
if (!MaterialDAO::upsertAccount(account)) {
|
||||||
|
qWarning() << "[Dushanbe::GetCardInfo] Failed to upsert account" << lastNumbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем на сервер если нет material_id
|
||||||
|
const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode);
|
||||||
|
if (!updatedApp.bankProfileId.isEmpty()) {
|
||||||
|
const MaterialInfo savedAccount = MaterialDAO::findAppAccount(device.id, m_appCode, lastNumbers);
|
||||||
|
if (savedAccount.id != -1 && savedAccount.materialId.isEmpty()) {
|
||||||
|
NetworkService ns;
|
||||||
|
ns.setDeviceId(m_deviceId);
|
||||||
|
QJsonObject materialBody;
|
||||||
|
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
|
||||||
|
materialBody["card_number"] = fullNumber;
|
||||||
|
materialBody["name"] = updatedApp.fullName;
|
||||||
|
ns.setPayload(QJsonDocument(materialBody).toJson());
|
||||||
|
ns.addExtra("account_id", savedAccount.id);
|
||||||
|
ns.postMaterial();
|
||||||
|
qDebug() << "[Dushanbe::GetCardInfo] postMaterial done for" << lastNumbers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Назад к списку карт
|
||||||
|
AdbUtils::goBack(m_deviceId);
|
||||||
|
QThread::msleep(1500);
|
||||||
|
|
||||||
|
// Ждём возврата на экран "Мои карты"
|
||||||
|
for (int attempt = 0; attempt < 5; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
if (xmlScreenParser.isCardsScreen()) break;
|
||||||
|
QThread::msleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
27
android/dushanbe/GetCardInfoScript.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "CommonScript.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
class GetCardInfoScript final : public CommonScript {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit GetCardInfoScript(
|
||||||
|
QString deviceId,
|
||||||
|
QString appCode,
|
||||||
|
QObject *parent = nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
~GetCardInfoScript() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void doStart() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_deviceId;
|
||||||
|
const QString m_appCode;
|
||||||
|
QString m_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
250
android/dushanbe/GetLastTransactionsScript.cpp
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
#include "GetLastTransactionsScript.h"
|
||||||
|
|
||||||
|
#include <QThread>
|
||||||
|
#include <QTimeZone>
|
||||||
|
|
||||||
|
#include "adb/AdbUtils.h"
|
||||||
|
#include "dao/MaterialDAO.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
|
#include "dao/DeviceDAO.h"
|
||||||
|
#include "dao/TransactionDAO.h"
|
||||||
|
#include "AppLogger.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
GetLastTransactionsScript::GetLastTransactionsScript(
|
||||||
|
QString deviceId,
|
||||||
|
QString appCode,
|
||||||
|
double searchAmount,
|
||||||
|
QDateTime searchTime,
|
||||||
|
QObject *parent
|
||||||
|
) : CommonScript(parent),
|
||||||
|
m_deviceId(std::move(deviceId)),
|
||||||
|
m_appCode(std::move(appCode)),
|
||||||
|
m_searchAmount(searchAmount),
|
||||||
|
m_searchTime(std::move(searchTime)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
GetLastTransactionsScript::~GetLastTransactionsScript() = default;
|
||||||
|
|
||||||
|
void GetLastTransactionsScript::doStart() {
|
||||||
|
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
|
||||||
|
|
||||||
|
if (device.id.isEmpty() || app.id == -1) {
|
||||||
|
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
|
||||||
|
qCritical() << m_error;
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
|
||||||
|
if (!xmlScreenParser.isHomeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::GetLastTransactions] Not on home screen, restarting app...";
|
||||||
|
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
|
||||||
|
device.screenWidth, device.screenHeight)) {
|
||||||
|
m_error = "Cannot reach home screen: " + m_deviceId;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("dushanbe/GetLastTransactions", m_deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(m_deviceId),
|
||||||
|
"FETCH_TRANSACTIONS");
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тапаем "История" в нижнем меню
|
||||||
|
Node historyBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("История"));
|
||||||
|
if (historyBtn.x() == 0 && historyBtn.y() == 0) {
|
||||||
|
m_error = "Cannot find History button: " + m_deviceId;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdbUtils::makeTap(m_deviceId, historyBtn.x(), historyBtn.y());
|
||||||
|
QThread::msleep(3000);
|
||||||
|
|
||||||
|
// Ждём экран истории
|
||||||
|
bool historyLoaded = false;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
if (xmlScreenParser.isHistoryScreen()) {
|
||||||
|
historyLoaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
QThread::msleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!historyLoaded) {
|
||||||
|
m_error = "History screen not loaded: " + m_deviceId;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим accountId
|
||||||
|
int accountId = -1;
|
||||||
|
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
||||||
|
if (!accounts.isEmpty()) {
|
||||||
|
accountId = accounts.first().id;
|
||||||
|
} else {
|
||||||
|
qWarning() << "[Dushanbe::GetLastTransactions] No account found";
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::GetLastTransactions] Searching: amount=" << m_searchAmount
|
||||||
|
<< "time=" << m_searchTime.toString(Qt::ISODate);
|
||||||
|
|
||||||
|
const QTimeZone tjTz("Asia/Dushanbe"); // UTC+5
|
||||||
|
const int maxScrolls = 10;
|
||||||
|
|
||||||
|
for (int scroll = 0; scroll <= maxScrolls; ++scroll) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
const QList<ScreenXmlParser::TransactionItem> items = xmlScreenParser.parseTransactionItems();
|
||||||
|
|
||||||
|
for (const auto &tx : items) {
|
||||||
|
if (qAbs(tx.amount - m_searchAmount) > 0.01) continue;
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount
|
||||||
|
<< "phone:" << tx.phone << "time:" << tx.time;
|
||||||
|
|
||||||
|
// Нашли по сумме — тапаем для получения деталей
|
||||||
|
// Ищем соответствующий node чтобы по нему тапнуть
|
||||||
|
Node txNode;
|
||||||
|
for (const Node &node : xmlScreenParser.findAllNodes()) {
|
||||||
|
if (node.className != "android.widget.ImageView" || !node.clickable) continue;
|
||||||
|
if (!node.contentDesc.contains("***")) continue;
|
||||||
|
// Проверяем что содержит нужную сумму
|
||||||
|
QString amountStr = QString::number(tx.amount, 'f', 2);
|
||||||
|
if (node.contentDesc.contains(amountStr)) {
|
||||||
|
txNode = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txNode.x() == 0 && txNode.y() == 0) {
|
||||||
|
qWarning() << "[Dushanbe::GetLastTransactions] Cannot find node for transaction";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdbUtils::makeTap(m_deviceId, txNode.x(), txNode.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// Ждём экран деталей
|
||||||
|
bool detailLoaded = false;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
if (xmlScreenParser.isTransactionDetailScreen()) {
|
||||||
|
detailLoaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
QThread::msleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detailLoaded) {
|
||||||
|
qWarning() << "[Dushanbe::GetLastTransactions] Detail screen not loaded";
|
||||||
|
AdbUtils::goBack(m_deviceId);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto detail = xmlScreenParser.parseTransactionDetail();
|
||||||
|
|
||||||
|
// Проверяем время ±1 час
|
||||||
|
if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
|
||||||
|
QDateTime txTime = QDateTime::fromString(
|
||||||
|
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
|
||||||
|
if (!txTime.isValid()) {
|
||||||
|
txTime = QDateTime::fromString(detail.date + " " + detail.time, "dd.MM.yyyy HH:mm");
|
||||||
|
}
|
||||||
|
if (txTime.isValid()) {
|
||||||
|
txTime.setTimeZone(tjTz);
|
||||||
|
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC()));
|
||||||
|
if (diffSecs > 3600) {
|
||||||
|
qDebug() << "[Dushanbe::GetLastTransactions] Time mismatch: diff=" << diffSecs << "sec, skipping";
|
||||||
|
AdbUtils::goBack(m_deviceId);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нашли! Сохраняем
|
||||||
|
qDebug() << "[Dushanbe::GetLastTransactions] FOUND:"
|
||||||
|
<< "amount=" << detail.amount
|
||||||
|
<< "date=" << detail.date << detail.time
|
||||||
|
<< "opId=" << detail.operationId
|
||||||
|
<< "receiver=" << detail.receiverAccount
|
||||||
|
<< "status=" << detail.status;
|
||||||
|
|
||||||
|
if (accountId != -1) {
|
||||||
|
TransactionInfo saveTx;
|
||||||
|
saveTx.accountId = accountId;
|
||||||
|
saveTx.amount = detail.amount;
|
||||||
|
saveTx.fee = detail.fee;
|
||||||
|
saveTx.bankName = m_appCode;
|
||||||
|
saveTx.bankTime = detail.date + " " + detail.time;
|
||||||
|
saveTx.bankTrExternalId = detail.operationId;
|
||||||
|
saveTx.description = detail.provider;
|
||||||
|
saveTx.phone = detail.receiverAccount;
|
||||||
|
saveTx.name = detail.receiverAccount;
|
||||||
|
saveTx.status = (detail.status == QString::fromUtf8("Успешный"))
|
||||||
|
? TransactionStatus::Complete : TransactionStatus::Unknown;
|
||||||
|
saveTx.type = TransactionType::Phone;
|
||||||
|
saveTx.timestamp = QDateTime::currentDateTimeUtc();
|
||||||
|
|
||||||
|
QDateTime txTime = QDateTime::fromString(
|
||||||
|
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
|
||||||
|
if (txTime.isValid()) {
|
||||||
|
txTime.setTimeZone(tjTz);
|
||||||
|
saveTx.completeTime = txTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionDAO::insertTransactionIfNotExists(saveTx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Назад к истории
|
||||||
|
Node nazadBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Назад"));
|
||||||
|
if (nazadBtn.x() > 0 && nazadBtn.y() > 0) {
|
||||||
|
AdbUtils::makeTap(m_deviceId, nazadBtn.x(), nazadBtn.y());
|
||||||
|
} else {
|
||||||
|
AdbUtils::goBack(m_deviceId);
|
||||||
|
}
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
|
emit finishedWithResult(m_error); // m_error пустой — успех
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скроллим вниз
|
||||||
|
if (scroll < maxScrolls) {
|
||||||
|
const int x = device.screenWidth / 2;
|
||||||
|
const int fromY = device.screenHeight * 3 / 4;
|
||||||
|
const int toY = device.screenHeight / 4;
|
||||||
|
AdbUtils::makeSwipe(m_deviceId, x, fromY, x, toY);
|
||||||
|
QThread::msleep(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_error = "Transaction not found: amount=" + QString::number(m_searchAmount, 'f', 2)
|
||||||
|
+ " time=" + m_searchTime.toString(Qt::ISODate);
|
||||||
|
qWarning() << "[Dushanbe::GetLastTransactions]" << m_error;
|
||||||
|
|
||||||
|
// Возвращаемся на главную
|
||||||
|
Node glavnayaBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Главная"));
|
||||||
|
if (glavnayaBtn.x() > 0 && glavnayaBtn.y() > 0) {
|
||||||
|
AdbUtils::makeTap(m_deviceId, glavnayaBtn.x(), glavnayaBtn.y());
|
||||||
|
} else {
|
||||||
|
AdbUtils::goBack(m_deviceId);
|
||||||
|
}
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
34
android/dushanbe/GetLastTransactionsScript.h
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "CommonScript.h"
|
||||||
|
#include <QDateTime>
|
||||||
|
#include "db/DeviceInfo.h"
|
||||||
|
#include "db/BankProfileInfo.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
class GetLastTransactionsScript final : public CommonScript {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit GetLastTransactionsScript(
|
||||||
|
QString deviceId,
|
||||||
|
QString appCode,
|
||||||
|
double searchAmount = 0,
|
||||||
|
QDateTime searchTime = {},
|
||||||
|
QObject *parent = nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
~GetLastTransactionsScript() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void doStart() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_deviceId;
|
||||||
|
const QString m_appCode;
|
||||||
|
const double m_searchAmount; // сумма в сомони (0 = все последние)
|
||||||
|
const QDateTime m_searchTime; // примерное время (UTC)
|
||||||
|
QString m_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
122
android/dushanbe/GetProfileInfoScript.cpp
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#include "GetProfileInfoScript.h"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include "adb/AdbUtils.h"
|
||||||
|
#include "dao/MaterialDAO.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
|
#include "dao/DeviceDAO.h"
|
||||||
|
#include "net/NetworkService.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
GetProfileInfoScript::GetProfileInfoScript(
|
||||||
|
QString deviceId,
|
||||||
|
QString appCode,
|
||||||
|
QObject *parent
|
||||||
|
) : CommonScript(parent),
|
||||||
|
m_deviceId(std::move(deviceId)),
|
||||||
|
m_appCode(std::move(appCode)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
GetProfileInfoScript::~GetProfileInfoScript() = default;
|
||||||
|
|
||||||
|
void GetProfileInfoScript::doStart() {
|
||||||
|
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
|
||||||
|
|
||||||
|
if (device.id.isEmpty() || app.id == -1) {
|
||||||
|
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
|
||||||
|
qCritical() << m_error;
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
|
||||||
|
if (!xmlScreenParser.isHomeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::GetProfileInfo] Not on home screen, restarting app...";
|
||||||
|
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
|
||||||
|
device.screenWidth, device.screenHeight)) {
|
||||||
|
m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим баланс с домашнего экрана
|
||||||
|
const double balance = xmlScreenParser.parseBalanceFromHomeScreen();
|
||||||
|
qDebug() << "[Dushanbe::GetProfileInfo] balance:" << balance;
|
||||||
|
BankProfileDAO::updateAmount(app.id, balance);
|
||||||
|
|
||||||
|
// Тапаем по кнопке меню (гамбургер, левый верхний угол)
|
||||||
|
// Это первый clickable ImageView в верхней панели
|
||||||
|
Node menuButton;
|
||||||
|
for (const Node &node : xmlScreenParser.findAllNodes()) {
|
||||||
|
if (node.className == "android.widget.ImageView" && node.clickable && node.y() < 300) {
|
||||||
|
menuButton = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuButton.x() == 0 && menuButton.y() == 0) {
|
||||||
|
m_error = "Cannot find menu button: " + m_deviceId;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdbUtils::makeTap(m_deviceId, menuButton.x(), menuButton.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// Ждём боковое меню
|
||||||
|
bool sideMenuFound = false;
|
||||||
|
for (int attempt = 0; attempt < 5; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
if (xmlScreenParser.isSideMenu()) {
|
||||||
|
sideMenuFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
QThread::msleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sideMenuFound) {
|
||||||
|
m_error = "Side menu not detected: " + m_deviceId;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим ФИО и телефон из бокового меню
|
||||||
|
const QString fullName = xmlScreenParser.parseProfileFullName();
|
||||||
|
const QString phone = xmlScreenParser.parseProfilePhone();
|
||||||
|
qDebug() << "[Dushanbe::GetProfileInfo] fullName:" << fullName << "phone:" << phone;
|
||||||
|
|
||||||
|
if (!fullName.isEmpty()) {
|
||||||
|
BankProfileDAO::updateProfileInfo(app.id, fullName, phone, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
createBankProfileIfNeeded(device.id, m_appCode);
|
||||||
|
|
||||||
|
const QList<MaterialInfo> allAccounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
||||||
|
QList<QPair<MaterialInfo, Node>> accountsToPost;
|
||||||
|
for (const auto &acc : allAccounts) {
|
||||||
|
if (acc.materialId.isEmpty()) {
|
||||||
|
accountsToPost.append({acc, Node()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!accountsToPost.isEmpty()) {
|
||||||
|
postAccountsData(accountsToPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
27
android/dushanbe/GetProfileInfoScript.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "CommonScript.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
class GetProfileInfoScript final : public CommonScript {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
~GetProfileInfoScript() override;
|
||||||
|
|
||||||
|
explicit GetProfileInfoScript(
|
||||||
|
QString deviceId,
|
||||||
|
QString appCode,
|
||||||
|
QObject *parent = nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void doStart() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_deviceId;
|
||||||
|
const QString m_appCode;
|
||||||
|
QString m_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
63
android/dushanbe/LoginAndCheckAccountsScript.cpp
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#include "LoginAndCheckAccountsScript.h"
|
||||||
|
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include "adb/AdbUtils.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
|
#include "dao/DeviceDAO.h"
|
||||||
|
#include "AppLogger.h"
|
||||||
|
#include "GetProfileInfoScript.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
LoginAndCheckAccountsScript::LoginAndCheckAccountsScript(
|
||||||
|
QString deviceId,
|
||||||
|
QString appCode,
|
||||||
|
QObject *parent,
|
||||||
|
bool pinCheckOnly
|
||||||
|
) : CommonScript(parent),
|
||||||
|
m_deviceId(std::move(deviceId)),
|
||||||
|
m_appCode(std::move(appCode)),
|
||||||
|
m_pinCheckOnly(pinCheckOnly) {
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default;
|
||||||
|
|
||||||
|
void LoginAndCheckAccountsScript::doStart() {
|
||||||
|
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
|
||||||
|
|
||||||
|
if (device.id.isEmpty() || app.id == -1) {
|
||||||
|
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
|
||||||
|
qCritical() << m_error;
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
|
||||||
|
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
|
||||||
|
m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("dushanbe/LoginAndCheck", m_deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_PROFILE");
|
||||||
|
BankProfileDAO::updateComment(app.id,
|
||||||
|
"Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss"));
|
||||||
|
|
||||||
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_pinCheckOnly) {
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetProfileInfoScript profileScript(m_deviceId, m_appCode);
|
||||||
|
profileScript.start();
|
||||||
|
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
29
android/dushanbe/LoginAndCheckAccountsScript.h
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "CommonScript.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
class LoginAndCheckAccountsScript final : public CommonScript {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
~LoginAndCheckAccountsScript() override;
|
||||||
|
|
||||||
|
explicit LoginAndCheckAccountsScript(
|
||||||
|
QString deviceId,
|
||||||
|
QString appCode,
|
||||||
|
QObject *parent = nullptr,
|
||||||
|
bool pinCheckOnly = false
|
||||||
|
);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void doStart() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_deviceId;
|
||||||
|
const QString m_appCode;
|
||||||
|
const bool m_pinCheckOnly;
|
||||||
|
QString m_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
477
android/dushanbe/ScreenXmlParser.cpp
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
#include "ScreenXmlParser.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QXmlStreamReader>
|
||||||
|
#include <QDomDocument>
|
||||||
|
|
||||||
|
#include "android/xml/Node.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])");
|
||||||
|
|
||||||
|
ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenXmlParser::~ScreenXmlParser() = default;
|
||||||
|
|
||||||
|
void ScreenXmlParser::parseAndSaveXml(const QString &xml) {
|
||||||
|
QDomDocument doc;
|
||||||
|
doc.setContent(xml);
|
||||||
|
m_xml = doc.documentElement();
|
||||||
|
m_nodes.clear();
|
||||||
|
|
||||||
|
QXmlStreamReader reader(xml);
|
||||||
|
while (!reader.atEnd()) {
|
||||||
|
reader.readNext();
|
||||||
|
if (reader.isStartElement() && reader.name() == u"node") {
|
||||||
|
Node node;
|
||||||
|
if (reader.attributes().hasAttribute("bounds")) {
|
||||||
|
QString bounds = reader.attributes().value("bounds").toString();
|
||||||
|
if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) {
|
||||||
|
node.setBounds(
|
||||||
|
match.captured(1).toInt(),
|
||||||
|
match.captured(2).toInt(),
|
||||||
|
match.captured(3).toInt(),
|
||||||
|
match.captured(4).toInt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reader.attributes().hasAttribute("text")) {
|
||||||
|
node.text = reader.attributes().value("text").toString();
|
||||||
|
}
|
||||||
|
if (reader.attributes().hasAttribute("content-desc")) {
|
||||||
|
node.contentDesc = reader.attributes().value("content-desc").toString();
|
||||||
|
}
|
||||||
|
if (reader.attributes().hasAttribute("class")) {
|
||||||
|
node.className = reader.attributes().value("class").toString();
|
||||||
|
}
|
||||||
|
if (reader.attributes().hasAttribute("clickable")) {
|
||||||
|
node.clickable = reader.attributes().value("clickable").toString() == "true";
|
||||||
|
}
|
||||||
|
if (reader.attributes().hasAttribute("focusable")) {
|
||||||
|
node.focusable = reader.attributes().value("focusable").toString() == "true";
|
||||||
|
}
|
||||||
|
if (reader.attributes().hasAttribute("resource-id")) {
|
||||||
|
node.resourceId = reader.attributes().value("resource-id").toString();
|
||||||
|
}
|
||||||
|
if (reader.attributes().hasAttribute("hint")) {
|
||||||
|
node.hint = reader.attributes().value("hint").toString();
|
||||||
|
}
|
||||||
|
if (reader.attributes().hasAttribute("password")) {
|
||||||
|
node.password = reader.attributes().value("password").toString() == "true";
|
||||||
|
}
|
||||||
|
m_nodes.append(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<Node> &ScreenXmlParser::findAllNodes() const {
|
||||||
|
return m_nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node ScreenXmlParser::findTextNode(const QString &text) {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.text.trimmed() == text) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc == contentDesc) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc, bool contains) {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (contains) {
|
||||||
|
if (node.contentDesc.contains(contentDesc)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (node.contentDesc == contentDesc) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Node ScreenXmlParser::findButtonNode(const QString &text, bool contains) {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (!node.clickable) continue;
|
||||||
|
if (contains) {
|
||||||
|
if (node.text.contains(text) || node.contentDesc.contains(text)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (node.text.trimmed() == text || node.contentDesc == text) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Node ScreenXmlParser::findFirstEditText() {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.className == "android.widget.EditText") {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenXmlParser::isPinCodeScreen() {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc.contains(QString::fromUtf8("Введите код доступа"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node ScreenXmlParser::findPinCodeEditText() {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.className == "android.widget.EditText" && node.password) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenXmlParser::isHomeScreen() {
|
||||||
|
bool hasGlavnaya = false;
|
||||||
|
bool hasTJS = false;
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc == QString::fromUtf8("Главная")) {
|
||||||
|
hasGlavnaya = true;
|
||||||
|
}
|
||||||
|
if (node.contentDesc.contains("TJS")) {
|
||||||
|
hasTJS = true;
|
||||||
|
}
|
||||||
|
if (hasGlavnaya && hasTJS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ScreenXmlParser::parseBalanceFromHomeScreen() {
|
||||||
|
// На домашнем экране: parent node content-desc="TJS\n03.04.26 - 21:46:15"
|
||||||
|
// внутри него child node content-desc="0.00" (или "10.00")
|
||||||
|
// Стратегия: находим индекс parent, затем child сразу после него с числовым content-desc
|
||||||
|
for (int i = 0; i < m_nodes.size(); ++i) {
|
||||||
|
if (!m_nodes[i].contentDesc.startsWith("TJS")) continue;
|
||||||
|
// Проверяем что это блок с датой (не просто "TJS" текст)
|
||||||
|
if (!m_nodes[i].contentDesc.contains('\n')) continue;
|
||||||
|
|
||||||
|
// Child node с балансом идёт сразу после parent или рядом
|
||||||
|
for (int j = i + 1; j < qMin(i + 5, m_nodes.size()); ++j) {
|
||||||
|
QString raw = m_nodes[j].contentDesc.trimmed();
|
||||||
|
if (raw.isEmpty()) continue;
|
||||||
|
raw.replace(',', '.');
|
||||||
|
raw.remove(' ');
|
||||||
|
bool ok = false;
|
||||||
|
double val = raw.toDouble(&ok);
|
||||||
|
if (ok) return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ScreenXmlParser::parseUserNameFromHomeScreen() {
|
||||||
|
// TODO: реализовать парсинг имени
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenXmlParser::isSideMenu() {
|
||||||
|
bool hasVyhod = false;
|
||||||
|
bool hasIdentified = false;
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc == QString::fromUtf8("Выход")) {
|
||||||
|
hasVyhod = true;
|
||||||
|
}
|
||||||
|
if (node.contentDesc == QString::fromUtf8("Идентифицирован")) {
|
||||||
|
hasIdentified = true;
|
||||||
|
}
|
||||||
|
if (hasVyhod && hasIdentified) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ScreenXmlParser::parseProfileFullName() {
|
||||||
|
// content-desc: "Мехрубон Имомкулзода\n+992 (18) 555-5519\nВерсия: 3.2.5\nСборка: 390:3443"
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||||||
|
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) {
|
||||||
|
QStringList lines = node.contentDesc.split('\n');
|
||||||
|
if (!lines.isEmpty()) {
|
||||||
|
return lines[0].trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ScreenXmlParser::parseProfilePhone() {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||||||
|
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) {
|
||||||
|
QStringList lines = node.contentDesc.split('\n');
|
||||||
|
if (lines.size() >= 2) {
|
||||||
|
return lines[1].trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenXmlParser::isCardsScreen() {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc == QString::fromUtf8("Мои карты")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<ScreenXmlParser::CardInfo> ScreenXmlParser::parseCards() {
|
||||||
|
// Карты — ImageView с content-desc формата:
|
||||||
|
// "Обновлено: 03.04.26 - 22:43:57\n0.00 TJS\n**** 3258\n до \n11/35\nIMOMKULZODA MEKHRUBON"
|
||||||
|
// или "-\n**** \n до \n12/29\n \nКредитная карта"
|
||||||
|
QList<CardInfo> result;
|
||||||
|
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.className != "android.widget.ImageView") continue;
|
||||||
|
if (!node.contentDesc.contains("****")) continue;
|
||||||
|
|
||||||
|
QStringList lines;
|
||||||
|
for (const QString &line : node.contentDesc.split('\n')) {
|
||||||
|
QString trimmed = line.trimmed();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
lines.append(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CardInfo card;
|
||||||
|
|
||||||
|
for (const QString &line : lines) {
|
||||||
|
// Баланс: "0.00 TJS"
|
||||||
|
if (line.contains("TJS")) {
|
||||||
|
QStringList parts = line.split(' ');
|
||||||
|
if (parts.size() >= 2) {
|
||||||
|
card.amount = parts[0].toDouble();
|
||||||
|
card.currency = "TJS";
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Последние цифры: "**** 3258"
|
||||||
|
if (line.startsWith("****")) {
|
||||||
|
QString nums = line.mid(4).trimmed();
|
||||||
|
if (!nums.isEmpty() && nums[0].isDigit()) {
|
||||||
|
card.lastNumbers = nums;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Срок: "11/35" или "12/29"
|
||||||
|
static const QRegularExpression expiryRe(R"(^\d{2}/\d{2}$)");
|
||||||
|
if (expiryRe.match(line).hasMatch()) {
|
||||||
|
card.expiry = line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Пропускаем "до", "Обновлено:...", "-"
|
||||||
|
if (line == QString::fromUtf8("до") || line.startsWith(QString::fromUtf8("Обновлено")) || line == "-") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Имя держателя (CAPS) или описание
|
||||||
|
if (line == line.toUpper() && line.length() > 3) {
|
||||||
|
card.holderName = line;
|
||||||
|
} else {
|
||||||
|
card.description = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenXmlParser::isCardDetailScreen() {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc == QString::fromUtf8("Карта")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ScreenXmlParser::parseFullCardNumber() {
|
||||||
|
// Полный номер карты: "9762 0002 0711 3258" — clickable View
|
||||||
|
static const QRegularExpression cardNumberRe(R"(^\d{4}\s\d{4}\s\d{4}\s\d{4}$)");
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (cardNumberRe.match(node.contentDesc).hasMatch()) {
|
||||||
|
return node.contentDesc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
double ScreenXmlParser::parseCardDetailBalance() {
|
||||||
|
// "0,00 TJS" или "- TJS" — содержит TJS, но не "сомони"
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc.contains("TJS") && !node.contentDesc.contains(QString::fromUtf8("сомони"))) {
|
||||||
|
QString raw = node.contentDesc.trimmed();
|
||||||
|
raw.remove("TJS");
|
||||||
|
raw = raw.trimmed();
|
||||||
|
if (raw == "-" || raw.isEmpty()) return 0.0;
|
||||||
|
raw.replace(',', '.');
|
||||||
|
raw.remove(' ');
|
||||||
|
bool ok = false;
|
||||||
|
double val = raw.toDouble(&ok);
|
||||||
|
if (ok) return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ScreenXmlParser::parseCardDetailHolder() {
|
||||||
|
// Имя держателя — текст в CAPS, не содержит цифр, длина > 3
|
||||||
|
static const QRegularExpression cardNumberRe(R"(\d{4}\s\d{4})");
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
const QString &desc = node.contentDesc;
|
||||||
|
if (desc.isEmpty() || desc.length() < 4) continue;
|
||||||
|
if (desc == desc.toUpper() && !desc.contains(QRegularExpression(R"(\d)")) &&
|
||||||
|
desc != QString::fromUtf8("TJS")) {
|
||||||
|
return desc.trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenXmlParser::isHistoryScreen() {
|
||||||
|
bool hasOperacii = false;
|
||||||
|
bool hasVypiska = false;
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc == QString::fromUtf8("Операции")) {
|
||||||
|
hasOperacii = true;
|
||||||
|
}
|
||||||
|
if (node.contentDesc == QString::fromUtf8("Выписка")) {
|
||||||
|
hasVypiska = true;
|
||||||
|
}
|
||||||
|
if (hasOperacii && hasVypiska) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<ScreenXmlParser::TransactionItem> ScreenXmlParser::parseTransactionItems() {
|
||||||
|
// Транзакции — ImageView с content-desc формата:
|
||||||
|
// "9762***3258\n992882770011\n493.00 \nDC (по номеру телефона)\n 18:57:10"
|
||||||
|
QList<TransactionItem> result;
|
||||||
|
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.className != "android.widget.ImageView") continue;
|
||||||
|
if (!node.contentDesc.contains("***")) continue;
|
||||||
|
|
||||||
|
QStringList lines;
|
||||||
|
for (const QString &line : node.contentDesc.split('\n')) {
|
||||||
|
QString trimmed = line.trimmed();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
lines.append(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.size() < 4) continue;
|
||||||
|
|
||||||
|
TransactionItem tx;
|
||||||
|
tx.cardMask = lines[0]; // "9762***3258"
|
||||||
|
tx.phone = lines[1]; // "992882770011"
|
||||||
|
|
||||||
|
// Сумма: "493.00" (может быть с пробелом в конце)
|
||||||
|
bool ok = false;
|
||||||
|
tx.amount = lines[2].toDouble(&ok);
|
||||||
|
if (!ok) tx.amount = 0.0;
|
||||||
|
|
||||||
|
tx.description = lines[3]; // "DC (по номеру телефона)" или "Tcell"
|
||||||
|
|
||||||
|
// Время — последний элемент
|
||||||
|
if (lines.size() >= 5) {
|
||||||
|
tx.time = lines[4]; // "18:57:10"
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenXmlParser::isTransactionDetailScreen() {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.contentDesc.contains(QString::fromUtf8("Дата операции:")) &&
|
||||||
|
node.contentDesc.contains(QString::fromUtf8("Номер операции:"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenXmlParser::TransactionDetail ScreenXmlParser::parseTransactionDetail() {
|
||||||
|
// Все данные в одном content-desc, формат "ключ: \nзначение"
|
||||||
|
TransactionDetail detail;
|
||||||
|
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (!node.contentDesc.contains(QString::fromUtf8("Дата операции:"))) continue;
|
||||||
|
|
||||||
|
// Разбиваем по \n, парсим пары "ключ:" + следующая строка = значение
|
||||||
|
QStringList lines = node.contentDesc.split('\n');
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.size(); ++i) {
|
||||||
|
const QString line = lines[i].trimmed();
|
||||||
|
|
||||||
|
if (line.startsWith(QString::fromUtf8("Дата операции:")) && i + 1 < lines.size()) {
|
||||||
|
detail.date = lines[i + 1].trimmed();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Время операции:")) && i + 1 < lines.size()) {
|
||||||
|
detail.time = lines[i + 1].trimmed();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Номер операции:")) && i + 1 < lines.size()) {
|
||||||
|
detail.operationId = lines[i + 1].trimmed();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Поставщик:")) && i + 1 < lines.size()) {
|
||||||
|
detail.provider = lines[i + 1].trimmed();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Счет отправителя:")) && i + 1 < lines.size()) {
|
||||||
|
detail.senderAccount = lines[i + 1].trimmed();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Счет получателя:")) && i + 1 < lines.size()) {
|
||||||
|
detail.receiverAccount = lines[i + 1].trimmed();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Сумма операции:")) && i + 1 < lines.size()) {
|
||||||
|
detail.amount = lines[i + 1].trimmed().toDouble();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) {
|
||||||
|
detail.fee = lines[i + 1].trimmed().toDouble();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Статус:")) && i + 1 < lines.size()) {
|
||||||
|
detail.status = lines[i + 1].trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node ScreenXmlParser::findEditTextByHint(const QString &hint) {
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.className == "android.widget.EditText" && node.hint == hint) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
123
android/dushanbe/ScreenXmlParser.h
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QDomElement>
|
||||||
|
#include "android/xml/Node.h"
|
||||||
|
#include "db/MaterialInfo.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
class ScreenXmlParser final : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ScreenXmlParser(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
~ScreenXmlParser() override;
|
||||||
|
|
||||||
|
void parseAndSaveXml(const QString &xml);
|
||||||
|
|
||||||
|
const QList<Node> &findAllNodes() const;
|
||||||
|
|
||||||
|
Node findTextNode(const QString &text);
|
||||||
|
|
||||||
|
Node findNodeByContentDesc(const QString &contentDesc);
|
||||||
|
|
||||||
|
Node findNodeByContentDesc(const QString &contentDesc, bool contains);
|
||||||
|
|
||||||
|
Node findButtonNode(const QString &text, bool contains);
|
||||||
|
|
||||||
|
Node findFirstEditText();
|
||||||
|
|
||||||
|
// TODO: реализовать после снятия XML дампов экранов
|
||||||
|
bool isPinCodeScreen();
|
||||||
|
|
||||||
|
Node findPinCodeEditText();
|
||||||
|
|
||||||
|
bool isHomeScreen();
|
||||||
|
|
||||||
|
// Парсит баланс с домашнего экрана
|
||||||
|
double parseBalanceFromHomeScreen();
|
||||||
|
|
||||||
|
// Парсит имя пользователя с домашнего экрана
|
||||||
|
QString parseUserNameFromHomeScreen();
|
||||||
|
|
||||||
|
// Определяет боковое меню (профиль)
|
||||||
|
bool isSideMenu();
|
||||||
|
|
||||||
|
// Парсит данные профиля из бокового меню
|
||||||
|
// content-desc: "Имя Фамилия\n+992...\nВерсия:...\nСборка:..."
|
||||||
|
QString parseProfileFullName();
|
||||||
|
QString parseProfilePhone();
|
||||||
|
|
||||||
|
// Определяет экран "Мои карты"
|
||||||
|
bool isCardsScreen();
|
||||||
|
|
||||||
|
// Структура для карты из экрана "Мои карты"
|
||||||
|
struct CardInfo {
|
||||||
|
double amount = 0.0;
|
||||||
|
QString currency;
|
||||||
|
QString lastNumbers; // "3258"
|
||||||
|
QString expiry; // "11/35"
|
||||||
|
QString holderName; // "IMOMKULZODA MEKHRUBON"
|
||||||
|
QString description; // "Кредитная карта" и т.д.
|
||||||
|
};
|
||||||
|
|
||||||
|
// Парсит все карты с экрана "Мои карты" (lastNumbers только)
|
||||||
|
QList<CardInfo> parseCards();
|
||||||
|
|
||||||
|
// Определяет экран детали карты (заголовок "Карта")
|
||||||
|
bool isCardDetailScreen();
|
||||||
|
|
||||||
|
// Парсит полный номер карты с экрана детали ("9762 0002 0711 3258")
|
||||||
|
QString parseFullCardNumber();
|
||||||
|
|
||||||
|
// Парсит баланс с экрана детали ("0,00 TJS")
|
||||||
|
double parseCardDetailBalance();
|
||||||
|
|
||||||
|
// Парсит имя держателя с экрана детали
|
||||||
|
QString parseCardDetailHolder();
|
||||||
|
|
||||||
|
// Определяет экран истории операций (табы "Операции" + "Выписка")
|
||||||
|
bool isHistoryScreen();
|
||||||
|
|
||||||
|
// Структура для транзакции из экрана истории
|
||||||
|
struct TransactionItem {
|
||||||
|
QString cardMask; // "9762***3258"
|
||||||
|
QString phone; // "992882770011"
|
||||||
|
double amount = 0.0; // 493.00
|
||||||
|
QString description; // "DC (по номеру телефона)"
|
||||||
|
QString time; // "18:57:10"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Парсит транзакции с экрана истории
|
||||||
|
QList<TransactionItem> parseTransactionItems();
|
||||||
|
|
||||||
|
// Определяет экран детали транзакции (содержит "Дата операции:" и "Номер операции:")
|
||||||
|
bool isTransactionDetailScreen();
|
||||||
|
|
||||||
|
// Структура для деталей транзакции
|
||||||
|
struct TransactionDetail {
|
||||||
|
QString date; // "03.04.2026"
|
||||||
|
QString time; // "18:57:10"
|
||||||
|
QString operationId; // "1593369670"
|
||||||
|
QString provider; // "DC (по номеру телефона)"
|
||||||
|
QString senderAccount; // "9762***3258"
|
||||||
|
QString receiverAccount;// "992882770011"
|
||||||
|
double amount = 0.0; // 493.00
|
||||||
|
double fee = 0.0; // 0.00
|
||||||
|
QString status; // "Успешный"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Парсит детали транзакции
|
||||||
|
TransactionDetail parseTransactionDetail();
|
||||||
|
|
||||||
|
// Находит EditText по hint
|
||||||
|
Node findEditTextByHint(const QString &hint);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<Node> m_nodes;
|
||||||
|
QDomElement m_xml;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
@ -312,7 +312,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QMap<QString, int> currencyCodes = {
|
const QMap<QString, int> currencyCodes = {
|
||||||
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
|
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
|
||||||
};
|
};
|
||||||
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 643);
|
const int currencyCode = currencyCodes.value(app.currency.toUpper(), 643);
|
||||||
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);
|
const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts);
|
||||||
|
|||||||
@ -193,7 +193,7 @@ void GetCardInfoScript::doStart() {
|
|||||||
|
|
||||||
// ISO 4217 numeric currency codes
|
// ISO 4217 numeric currency codes
|
||||||
const QMap<QString, int> currencyCodes = {
|
const QMap<QString, int> currencyCodes = {
|
||||||
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
|
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
|
||||||
};
|
};
|
||||||
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);
|
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);
|
||||||
|
|
||||||
|
|||||||
@ -122,7 +122,7 @@ void GetProfileInfoScript::doStart() {
|
|||||||
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
|
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
|
||||||
|
|
||||||
const QMap<QString, int> currencyCodes = {
|
const QMap<QString, int> currencyCodes = {
|
||||||
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
|
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
|
||||||
};
|
};
|
||||||
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);
|
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);
|
||||||
|
|
||||||
@ -173,7 +173,10 @@ void GetProfileInfoScript::doStart() {
|
|||||||
postAccountsData(accountsToPost);
|
postAccountsData(accountsToPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
// Возвращаемся на домашний экран
|
||||||
|
AdbUtils::goBack(m_deviceId);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
emit finishedWithResult(m_error);
|
emit finishedWithResult(m_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,6 @@ void LoginAndCheckAccountsScript::doStart() {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (m_pinCheckOnly) {
|
if (m_pinCheckOnly) {
|
||||||
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
|
||||||
emit finishedWithResult(m_error);
|
emit finishedWithResult(m_error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
assets/dushanbe/card_detail.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
1
assets/dushanbe/card_detail.xml
Normal file
BIN
assets/dushanbe/card_detail_2.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
1
assets/dushanbe/card_detail_2.xml
Normal file
BIN
assets/dushanbe/cards_screen.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
1
assets/dushanbe/cards_screen.xml
Normal file
BIN
assets/dushanbe/history_screen.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
1
assets/dushanbe/history_screen.xml
Normal file
BIN
assets/dushanbe/home_screen.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
1
assets/dushanbe/home_screen.xml
Normal file
BIN
assets/dushanbe/launch_screen.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
1
assets/dushanbe/launch_screen.xml
Normal file
BIN
assets/dushanbe/side_menu.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
1
assets/dushanbe/side_menu.xml
Normal file
BIN
assets/dushanbe/tx_detail.png
Normal file
|
After Width: | Height: | Size: 237 KiB |
1
assets/dushanbe/tx_detail.xml
Normal file
10
config.ini
@ -1,8 +1,8 @@
|
|||||||
[apps]
|
[apps]
|
||||||
list=ozon, black
|
list=ozon, black, dushanbe
|
||||||
|
|
||||||
[common]
|
[common]
|
||||||
currencies=USD, ARS, RUB
|
currencies=USD, ARS, RUB, TJS
|
||||||
version=0.0.1
|
version=0.0.1
|
||||||
|
|
||||||
[db]
|
[db]
|
||||||
@ -30,3 +30,9 @@ currency=RUB
|
|||||||
name=Ozon
|
name=Ozon
|
||||||
phone_code=7
|
phone_code=7
|
||||||
|
|
||||||
|
[dushanbe]
|
||||||
|
android_package_name=tj.dc.next1
|
||||||
|
currency=TJS
|
||||||
|
name=Dushanbe City
|
||||||
|
phone_code=992
|
||||||
|
|
||||||
|
|||||||
@ -385,3 +385,15 @@ bool BankProfileDAO::migrateDeviceId(const QString &oldDeviceId, const QString &
|
|||||||
qDebug() << "[BankProfileDAO] migrated" << query.numRowsAffected() << "profiles from" << oldDeviceId << "to" << newDeviceId;
|
qDebug() << "[BankProfileDAO] migrated" << query.numRowsAffected() << "profiles from" << oldDeviceId << "to" << newDeviceId;
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -49,4 +49,6 @@ public:
|
|||||||
[[nodiscard]] static bool deactivateAppsForDevice(const QString &deviceId);
|
[[nodiscard]] static bool deactivateAppsForDevice(const QString &deviceId);
|
||||||
|
|
||||||
[[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId);
|
[[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool deleteProfile(const QString &deviceId, const QString &appCode);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -64,14 +64,12 @@ bool MaterialDAO::updateAccountById(
|
|||||||
UPDATE materials
|
UPDATE materials
|
||||||
SET status = :status,
|
SET status = :status,
|
||||||
description = :description,
|
description = :description,
|
||||||
last_numbers = :last_numbers,
|
|
||||||
currency = :currency
|
currency = :currency
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
)");
|
)");
|
||||||
|
|
||||||
query.bindValue(":status", status);
|
query.bindValue(":status", status);
|
||||||
query.bindValue(":description", description);
|
query.bindValue(":description", description);
|
||||||
query.bindValue(":last_numbers", description.length() >= 4 ? description.right(4) : description);
|
|
||||||
query.bindValue(":currency", currency);
|
query.bindValue(":currency", currency);
|
||||||
query.bindValue(":id", id);
|
query.bindValue(":id", id);
|
||||||
|
|
||||||
@ -83,6 +81,18 @@ bool MaterialDAO::updateAccountById(
|
|||||||
return true;
|
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) {
|
bool MaterialDAO::deleteAccount(const int id) {
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare("DELETE FROM materials WHERE id = :id");
|
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;
|
qDebug() << "[MaterialDAO] migrated" << query.numRowsAffected() << "materials from" << oldDeviceId << "to" << newDeviceId;
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ public:
|
|||||||
const QString ¤cy
|
const QString ¤cy
|
||||||
);
|
);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool updateStatus(int id, const QString &status);
|
||||||
|
|
||||||
[[nodiscard]] static bool deleteAccount(int id);
|
[[nodiscard]] static bool deleteAccount(int id);
|
||||||
|
|
||||||
[[nodiscard]] static bool updateCardNumber(int accountId, const QString &cardNumber);
|
[[nodiscard]] static bool updateCardNumber(int accountId, const QString &cardNumber);
|
||||||
@ -38,4 +40,6 @@ public:
|
|||||||
[[nodiscard]] static QStringList getActiveMaterialIdsExcludingDevices(const QSet<QString> &excludeDevices);
|
[[nodiscard]] static QStringList getActiveMaterialIdsExcludingDevices(const QSet<QString> &excludeDevices);
|
||||||
|
|
||||||
[[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId);
|
[[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool deleteAccountsByApp(const QString &deviceId, const QString &appCode);
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
images/dushanbe_icon.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
@ -27,6 +27,8 @@
|
|||||||
#include "black/PayByCardScript.h"
|
#include "black/PayByCardScript.h"
|
||||||
#include "ozon/PayByCardScript.h"
|
#include "ozon/PayByCardScript.h"
|
||||||
#include "ozon/PayByPhoneScript.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 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);
|
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) {
|
if (newStatus != EventStatus::Error) {
|
||||||
const MaterialInfo account = MaterialDAO::getAccountById(event.accountId);
|
const MaterialInfo account = MaterialDAO::getAccountById(event.accountId);
|
||||||
const QMap<QString, int> currencyCodes = {
|
const QMap<QString, int> currencyCodes = {
|
||||||
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
|
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
|
||||||
};
|
};
|
||||||
const int currencyCode = currencyCodes.value(account.currency.toUpper(), 643);
|
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;
|
const QString adbSerial = device.adbSerial.isEmpty() ? account.deviceId : device.adbSerial;
|
||||||
|
|
||||||
if (event.type == EventType::FetchProfile) {
|
if (event.type == EventType::FetchProfile) {
|
||||||
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
|
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
|
||||||
else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode));
|
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;
|
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));
|
setup(new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime));
|
||||||
else if (appCode == "black")
|
else if (appCode == "black")
|
||||||
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
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;
|
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -933,41 +933,53 @@ void NetworkService::patchBankProfile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::toggleMaterial() {
|
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 QString materialId = m_extraData.value("material_id").toString();
|
||||||
const bool enable = m_extraData.value("enable").toBool();
|
const bool enable = m_extraData.value("enable").toBool();
|
||||||
|
|
||||||
if (materialId.isEmpty()) {
|
if (materialId.isEmpty()) {
|
||||||
qWarning() << "[NetworkService] toggleMaterial: material_id is empty";
|
qWarning() << "[NetworkService] toggleMaterial: material_id is empty";
|
||||||
emit finished();
|
emit finishedWithResult(-1); emit finished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString enableStr = enable ? "true" : "false";
|
const QString enableStr = enable ? "true" : "false";
|
||||||
const QString path = m_apiBase + m_pathMaterial + "/" + materialId + "/" + enableStr;
|
const QString path = m_apiBase + m_pathMaterial + "/" + materialId + "/" + enableStr;
|
||||||
patchJson(path, QJsonObject(), true);
|
const QJsonValue result = patchJson(path, QJsonObject(), true);
|
||||||
qDebug() << "[NetworkService] toggleMaterial done:" << materialId << "enable=" << enable;
|
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();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::toggleBankProfile() {
|
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 QString bankProfileId = m_extraData.value("bank_profile_id").toString();
|
||||||
const bool enable = m_extraData.value("enable").toBool();
|
const bool enable = m_extraData.value("enable").toBool();
|
||||||
|
|
||||||
if (bankProfileId.isEmpty()) {
|
if (bankProfileId.isEmpty()) {
|
||||||
qWarning() << "[NetworkService] toggleBankProfile: bank_profile_id is empty";
|
qWarning() << "[NetworkService] toggleBankProfile: bank_profile_id is empty";
|
||||||
emit finished();
|
emit finishedWithResult(-1); emit finished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString enableStr = enable ? "true" : "false";
|
const QString enableStr = enable ? "true" : "false";
|
||||||
const QString path = m_apiBase + m_pathBankProfile + "/" + bankProfileId + "/" + enableStr;
|
const QString path = m_apiBase + m_pathBankProfile + "/" + bankProfileId + "/" + enableStr;
|
||||||
patchJson(path, QJsonObject(), true);
|
const QJsonValue result = patchJson(path, QJsonObject(), true);
|
||||||
qDebug() << "[NetworkService] toggleBankProfile done:" << bankProfileId << "enable=" << enable;
|
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();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,23 @@
|
|||||||
#include "AccountSettingsWindow.h"
|
#include "AccountSettingsWindow.h"
|
||||||
|
|
||||||
#include <QLabel>
|
#include <QVBoxLayout>
|
||||||
#include <QMessageBox>
|
#include <QHBoxLayout>
|
||||||
#include <QPushButton>
|
#include <QHeaderView>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QVBoxLayout>
|
#include <QMessageBox>
|
||||||
#include <QHeaderView>
|
#include <QInputDialog>
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include "ozon/LoginAndCheckAccountsScript.h"
|
#include "ozon/LoginAndCheckAccountsScript.h"
|
||||||
#include "ozon/GetProfileInfoScript.h"
|
#include "ozon/GetProfileInfoScript.h"
|
||||||
#include "ozon/GetCardInfoScript.h"
|
#include "ozon/GetCardInfoScript.h"
|
||||||
#include "black/GetLastTransactionsScript.h"
|
|
||||||
#include "ozon/GetLastTransactionsScript.h"
|
|
||||||
#include "black/LoginAndCheckAccountsScript.h"
|
#include "black/LoginAndCheckAccountsScript.h"
|
||||||
#include "black/GetProfileInfoScript.h"
|
#include "black/GetProfileInfoScript.h"
|
||||||
#include "black/GetCardInfoScript.h"
|
#include "black/GetCardInfoScript.h"
|
||||||
#include "dao/MaterialDAO.h"
|
|
||||||
#include "dao/BankProfileDAO.h"
|
#include "dao/BankProfileDAO.h"
|
||||||
#include "bank/TransactionWindow.h"
|
#include "dao/MaterialDAO.h"
|
||||||
#include "device/EditBankAccountDialog.h"
|
|
||||||
#include "device/EditBankDialog.h"
|
|
||||||
#include "net/NetworkService.h"
|
#include "net/NetworkService.h"
|
||||||
|
|
||||||
AccountSettingsWindow::AccountSettingsWindow(
|
AccountSettingsWindow::AccountSettingsWindow(
|
||||||
@ -36,357 +30,250 @@ AccountSettingsWindow::AccountSettingsWindow(
|
|||||||
| Qt::WindowMinMaxButtonsHint),
|
| Qt::WindowMinMaxButtonsHint),
|
||||||
m_deviceId(std::move(deviceId)),
|
m_deviceId(std::move(deviceId)),
|
||||||
m_deviceName(std::move(deviceName)),
|
m_deviceName(std::move(deviceName)),
|
||||||
m_applicationInfo(app) {
|
m_app(app) {
|
||||||
setWindowTitle("Настройки банка [" + app.name + "]");
|
setWindowTitle("Настройки банка [" + m_app.name + "]");
|
||||||
setMinimumSize(800, 400);
|
setMinimumSize(700, 400);
|
||||||
setMaximumSize(1000, 800);
|
resize(750, 500);
|
||||||
resize(800, 400);
|
|
||||||
|
|
||||||
|
|
||||||
// 1. Редактирование пин-кода
|
|
||||||
// 2. Включение/отключения банка
|
|
||||||
// 3. Работа с картами (вкл/выкл) и редактирование цифр
|
|
||||||
// 4. Запуск проверки пин-код и парсинга аккаунтов
|
|
||||||
|
|
||||||
auto *mainLayout = new QVBoxLayout(this);
|
auto *mainLayout = new QVBoxLayout(this);
|
||||||
|
|
||||||
// Скролл-область
|
|
||||||
auto *scrollArea = new QScrollArea(this);
|
auto *scrollArea = new QScrollArea(this);
|
||||||
scrollArea->setWidgetResizable(true);
|
scrollArea->setWidgetResizable(true);
|
||||||
|
auto *content = new QWidget;
|
||||||
// Виджет-контейнер, в котором будет layout с таблицами и лейблами
|
auto *layout = new QVBoxLayout(content);
|
||||||
auto *contentWidget = new QWidget;
|
layout->setContentsMargins(8, 8, 8, 8);
|
||||||
auto *layout = new QVBoxLayout(contentWidget);
|
|
||||||
|
|
||||||
// Убираем отступы и рамку
|
|
||||||
contentWidget->setContentsMargins(0, 0, 0, 0); // Убираем отступы
|
|
||||||
layout->setContentsMargins(4, 4, 4, 4); // Убираем отступы у layout
|
|
||||||
// scrollArea->setStyleSheet("border: none;"); // Убираем рамку
|
|
||||||
layout->setAlignment(Qt::AlignTop);
|
layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
// ── Статус профиля ─────────────────────────────────────────────────
|
||||||
QHBoxLayout *row1 = new QHBoxLayout();
|
auto *statusRow = new QHBoxLayout;
|
||||||
QString status = QString("Статус: ") + (m_applicationInfo.status == "active"
|
statusRow->addWidget(new QLabel("Статус профиля:"));
|
||||||
? "<span style='color:green;'>Активен</span>"
|
m_statusLabel = new QLabel;
|
||||||
: "<span style='color:red;'>Неактивен</span>") +
|
|
||||||
QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked
|
|
||||||
? "<span style='color:green;'>Проверен</span>"
|
|
||||||
: "<span style='color:red;'>Не проверен</span>");
|
|
||||||
m_statusLabel = new QLabel(status);
|
|
||||||
m_statusLabel->setTextFormat(Qt::RichText);
|
m_statusLabel->setTextFormat(Qt::RichText);
|
||||||
row1->addWidget(m_statusLabel);
|
statusRow->addWidget(m_statusLabel);
|
||||||
|
statusRow->addStretch();
|
||||||
|
|
||||||
QPushButton *editBankBtn = new QPushButton("Редактировать");
|
m_toggleBtn = new QPushButton;
|
||||||
editBankBtn->setStyleSheet(
|
m_toggleBtn->setFixedWidth(120);
|
||||||
"background-color: green;"
|
connect(m_toggleBtn, &QPushButton::clicked, this, &AccountSettingsWindow::toggleProfileStatus);
|
||||||
"color: white;"
|
statusRow->addWidget(m_toggleBtn);
|
||||||
);
|
layout->addLayout(statusRow);
|
||||||
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();
|
|
||||||
|
|
||||||
// Если включаем профиль — проверяем что есть хотя бы одна активная карта
|
// ── Пин-код ────────────────────────────────────────────────────────
|
||||||
if (on) {
|
auto *pinRow = new QHBoxLayout;
|
||||||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
|
pinRow->addWidget(new QLabel("Пин-код:"));
|
||||||
bool hasActive = false;
|
m_pinLabel = new QLabel;
|
||||||
for (const auto &m : materials) {
|
m_pinLabel->setTextFormat(Qt::RichText);
|
||||||
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
|
pinRow->addWidget(m_pinLabel);
|
||||||
}
|
pinRow->addStretch();
|
||||||
if (!hasActive) {
|
|
||||||
QMessageBox::warning(this, "Невозможно включить",
|
|
||||||
"Ни одна карта не активна.\nВключите хотя бы одну карту перед включением профиля.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!BankProfileDAO::update(m_applicationInfo.id, pinCode, comment, on ? "active" : "off")) {
|
auto *editPinBtn = new QPushButton("Редактировать");
|
||||||
qDebug() << "Cant update bank data";
|
editPinBtn->setStyleSheet("background-color: green; color: white; padding: 4px 12px;");
|
||||||
} else {
|
connect(editPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::editPinCode);
|
||||||
m_applicationInfo.pinCode = pinCode;
|
pinRow->addWidget(editPinBtn);
|
||||||
m_applicationInfo.comment = comment;
|
|
||||||
m_applicationInfo.status = on ? "active" : "off";
|
|
||||||
|
|
||||||
QString status = QString("Статус: ") + (on
|
auto *checkPinBtn = new QPushButton("Проверить пин-код");
|
||||||
? "<span style='color:green;'>Активен</span>"
|
checkPinBtn->setStyleSheet("background-color: orange; color: white; padding: 4px 12px;");
|
||||||
: "<span style='color:red;'>Неактивен</span>") +
|
connect(checkPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startCheckPinCode);
|
||||||
QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked
|
pinRow->addWidget(checkPinBtn);
|
||||||
? "<span style='color:green;'>Проверен</span>"
|
layout->addLayout(pinRow);
|
||||||
: "<span style='color:red;'>Не проверен</span>");
|
|
||||||
m_statusLabel->setText(status);
|
|
||||||
m_commentLabel->setText(comment);
|
|
||||||
sendAppStatus(m_applicationInfo.deviceId, m_applicationInfo.code, on ? "active" : "off");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
layout->addLayout(row1);
|
// ── Профиль ────────────────────────────────────────────────────────
|
||||||
|
m_profileLabel = new QLabel;
|
||||||
QHBoxLayout *commentRow = new QHBoxLayout();
|
|
||||||
m_commentLabel = new QLabel(m_applicationInfo.comment);
|
|
||||||
m_commentLabel->setStyleSheet(
|
|
||||||
"color: gray;"
|
|
||||||
"margin-bottom: 8px;"
|
|
||||||
);
|
|
||||||
commentRow->addWidget(m_commentLabel);
|
|
||||||
layout->addLayout(commentRow);
|
|
||||||
|
|
||||||
QHBoxLayout *profileRow = new QHBoxLayout();
|
|
||||||
QString profileText;
|
|
||||||
if (m_applicationInfo.fullName.isEmpty() && m_applicationInfo.phone.isEmpty()) {
|
|
||||||
profileText = "<span style='color:gray;'>Данные не получены</span>";
|
|
||||||
} else {
|
|
||||||
profileText = m_applicationInfo.fullName + " | " + m_applicationInfo.phone;
|
|
||||||
}
|
|
||||||
m_profileLabel = new QLabel(profileText);
|
|
||||||
m_profileLabel->setTextFormat(Qt::RichText);
|
m_profileLabel->setTextFormat(Qt::RichText);
|
||||||
m_profileLabel->setStyleSheet("margin-bottom: 8px;");
|
m_profileLabel->setStyleSheet("margin: 4px 0;");
|
||||||
profileRow->addWidget(m_profileLabel);
|
layout->addWidget(m_profileLabel);
|
||||||
|
|
||||||
// Статус синхронизации с сервером
|
// ── Кнопки обновления ──────────────────────────────────────────────
|
||||||
const QString syncText = m_applicationInfo.bankProfileId.isEmpty()
|
auto *actionsRow = new QHBoxLayout;
|
||||||
? "<span style='color:red;'>● Не синхронизирован</span>"
|
|
||||||
: "<span style='color:green;'>● Синхронизирован</span>";
|
|
||||||
auto *syncLabel = new QLabel(syncText);
|
|
||||||
syncLabel->setTextFormat(Qt::RichText);
|
|
||||||
syncLabel->setStyleSheet("margin-bottom: 8px; margin-left: 16px;");
|
|
||||||
profileRow->addWidget(syncLabel);
|
|
||||||
profileRow->addStretch();
|
|
||||||
|
|
||||||
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("Запустить проверку пин-кода");
|
auto *cardsBtn = new QPushButton("Проверить аккаунты");
|
||||||
soloButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
cardsBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;");
|
||||||
// soloButton->setStyleSheet("");
|
connect(cardsBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetCardInfo);
|
||||||
soloButton->setStyleSheet(
|
actionsRow->addWidget(cardsBtn);
|
||||||
"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 +
|
|
||||||
" по пин-коду. Окно с настройками закроется.");
|
|
||||||
|
|
||||||
QPushButton *deleteBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
|
actionsRow->addStretch();
|
||||||
QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
|
layout->addLayout(actionsRow);
|
||||||
msgBox.setDefaultButton(closeBtn);
|
|
||||||
msgBox.setModal(true);
|
|
||||||
msgBox.exec();
|
|
||||||
|
|
||||||
if (msgBox.clickedButton() == deleteBtn) {
|
// ── Таблица карт ───────────────────────────────────────────────────
|
||||||
startCheckPinCode(m_applicationInfo.code);
|
layout->addSpacing(8);
|
||||||
close();
|
layout->addWidget(new QLabel("<b>Счета</b>"));
|
||||||
}
|
|
||||||
});
|
|
||||||
layout->addWidget(soloButton);
|
|
||||||
|
|
||||||
if (m_applicationInfo.code == "ozon" && m_applicationInfo.pinCodeChecked) {
|
m_cardsTable = new QTableWidget;
|
||||||
QHBoxLayout *ozonRow = new QHBoxLayout();
|
m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection);
|
||||||
|
m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
|
m_cardsTable->setFocusPolicy(Qt::NoFocus);
|
||||||
|
layout->addWidget(m_cardsTable);
|
||||||
|
|
||||||
QPushButton *profileButton = new QPushButton("Обновить профиль");
|
content->setLayout(layout);
|
||||||
profileButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
scrollArea->setWidget(content);
|
||||||
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);
|
|
||||||
mainLayout->addWidget(scrollArea);
|
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) {
|
// ── Overlay ────────────────────────────────────────────────────────────────
|
||||||
QThread *thread = new QThread(nullptr);
|
|
||||||
const QString deviceId = m_deviceId;
|
|
||||||
|
|
||||||
auto onResult = [deviceId, appCode](const QString &result) {
|
void AccountSettingsWindow::resizeEvent(QResizeEvent *event) {
|
||||||
|
QWidget::resizeEvent(event);
|
||||||
|
if (m_overlay) m_overlay->setGeometry(rect());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountSettingsWindow::showOverlay(const QString &text) {
|
||||||
|
m_overlayText->setText(text);
|
||||||
|
m_overlay->setGeometry(rect());
|
||||||
|
m_overlay->raise();
|
||||||
|
m_overlay->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountSettingsWindow::hideOverlay(bool success, const QString &message, std::function<void()> onHidden) {
|
||||||
|
m_overlayText->setText(message);
|
||||||
|
m_overlayText->setStyleSheet(success
|
||||||
|
? "color: #4CAF50; font-size: 16px; font-weight: bold;"
|
||||||
|
: "color: #F44336; font-size: 16px; font-weight: bold;");
|
||||||
|
m_overlayTimer->disconnect();
|
||||||
|
connect(m_overlayTimer, &QTimer::timeout, this, [this, onHidden]() {
|
||||||
|
m_overlay->hide();
|
||||||
|
m_overlayText->setStyleSheet("color: white; font-size: 16px; font-weight: bold;");
|
||||||
|
if (onHidden) onHidden();
|
||||||
|
});
|
||||||
|
m_overlayTimer->start(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI update ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void AccountSettingsWindow::updateStatusUI() {
|
||||||
|
// Статус профиля
|
||||||
|
const bool active = m_app.status == "active";
|
||||||
|
m_statusLabel->setText(active
|
||||||
|
? "<span style='color:green;'>Активен</span>"
|
||||||
|
: "<span style='color:red;'>Неактивен</span>");
|
||||||
|
m_toggleBtn->setText(active ? "Выключить" : "Включить");
|
||||||
|
m_toggleBtn->setStyleSheet(active
|
||||||
|
? "background-color: red; color: white; padding: 4px 12px;"
|
||||||
|
: "background-color: green; color: white; padding: 4px 12px;");
|
||||||
|
|
||||||
|
// Пин-код
|
||||||
|
const QString pinDisplay = m_app.pinCode.isEmpty() ? "не задан" : m_app.pinCode;
|
||||||
|
const QString pinStatus = m_app.pinCodeChecked
|
||||||
|
? "<span style='color:green;'>" + pinDisplay + " (проверен)</span>"
|
||||||
|
: "<span style='color:red;'>" + pinDisplay + " (не проверен)</span>";
|
||||||
|
m_pinLabel->setText(pinStatus);
|
||||||
|
|
||||||
|
// Профиль
|
||||||
|
if (m_app.fullName.isEmpty() && m_app.phone.isEmpty()) {
|
||||||
|
m_profileLabel->setText("<span style='color:gray;'>Данные профиля не получены</span>");
|
||||||
|
} else {
|
||||||
|
QString text;
|
||||||
|
if (!m_app.fullName.isEmpty()) text += m_app.fullName;
|
||||||
|
if (!m_app.phone.isEmpty()) text += " | " + m_app.phone;
|
||||||
|
if (!m_app.email.isEmpty()) text += " | " + m_app.email;
|
||||||
|
|
||||||
|
const QString syncText = m_app.bankProfileId.isEmpty()
|
||||||
|
? " <span style='color:red;'>● Не синхронизирован</span>"
|
||||||
|
: " <span style='color:green;'>● Синхронизирован</span>";
|
||||||
|
m_profileLabel->setText(text + syncText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toggle On/Off ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void AccountSettingsWindow::toggleProfileStatus() {
|
||||||
|
const bool isActive = m_app.status == "active";
|
||||||
|
const QString newStatus = isActive ? "off" : "active";
|
||||||
|
|
||||||
|
// Если включаем — проверяем что есть хотя бы одна активная карта
|
||||||
|
if (!isActive) {
|
||||||
|
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||||||
|
bool hasActive = false;
|
||||||
|
for (const auto &m : materials) {
|
||||||
|
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
|
||||||
|
}
|
||||||
|
if (!hasActive) {
|
||||||
|
QMessageBox::warning(this, "Невозможно включить",
|
||||||
|
"Ни одна карта не активна.\nВключите хотя бы одну карту перед включением профиля.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, newStatus))
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_app.status = newStatus;
|
||||||
|
showOverlay(isActive ? "Выключение профиля..." : "Включение профиля...");
|
||||||
|
sendAppStatus(newStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Edit PIN ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void AccountSettingsWindow::editPinCode() {
|
||||||
|
bool ok;
|
||||||
|
const QString pin = QInputDialog::getText(this, "Пин-код",
|
||||||
|
"Введите пин-код:", QLineEdit::Normal, m_app.pinCode, &ok);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
if (BankProfileDAO::update(m_app.id, pin, m_app.comment, m_app.status)) {
|
||||||
|
m_app.pinCode = pin;
|
||||||
|
m_app.pinCodeChecked = false;
|
||||||
|
BankProfileDAO::updatePinCodeStatus(m_app.id, "no", "");
|
||||||
|
updateStatusUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Check PIN ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void AccountSettingsWindow::startCheckPinCode() {
|
||||||
|
if (m_app.pinCode.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, "Пин-код не задан",
|
||||||
|
"Сначала укажите пин-код через кнопку \"Редактировать\".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *thread = new QThread(nullptr);
|
||||||
|
const QString deviceId = m_deviceId;
|
||||||
|
const QString appCode = m_app.code;
|
||||||
|
const int appId = m_app.id;
|
||||||
|
|
||||||
|
auto onResult = [this, appId](const QString &result) {
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
|
||||||
if (app.id >= 0) {
|
m_app.pinCodeChecked = true;
|
||||||
BankProfileDAO::updatePinCodeStatus(app.id, "yes", "");
|
QMetaObject::invokeMethod(this, [this]() { updateStatusUI(); }, Qt::QueuedConnection);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const bool pinCheckOnly = !m_applicationInfo.bankProfileId.isEmpty();
|
|
||||||
|
|
||||||
if (appCode == "ozon") {
|
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);
|
worker->moveToThread(thread);
|
||||||
connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start);
|
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, thread, &QThread::quit);
|
||||||
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||||||
} else if (appCode == "black") {
|
} 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);
|
worker->moveToThread(thread);
|
||||||
connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start);
|
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, thread, &QThread::quit);
|
||||||
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||||||
@ -398,20 +285,33 @@ void AccountSettingsWindow::startCheckPinCode(const QString &appCode) {
|
|||||||
thread->start();
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AccountSettingsWindow::startGetProfileInfo(const QString &appCode) {
|
// ── Get Profile ────────────────────────────────────────────────────────────
|
||||||
QThread *thread = new QThread(nullptr);
|
|
||||||
|
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") {
|
if (appCode == "ozon") {
|
||||||
auto *worker = new Ozon::GetProfileInfoScript(m_deviceId, appCode, nullptr);
|
auto *worker = new Ozon::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||||||
worker->moveToThread(thread);
|
worker->moveToThread(thread);
|
||||||
connect(thread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start);
|
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, thread, &QThread::quit);
|
||||||
connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||||||
} else if (appCode == "black") {
|
} else if (appCode == "black") {
|
||||||
auto *worker = new Black::GetProfileInfoScript(m_deviceId, appCode, nullptr);
|
auto *worker = new Black::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||||||
worker->moveToThread(thread);
|
worker->moveToThread(thread);
|
||||||
connect(thread, &QThread::started, worker, &Black::GetProfileInfoScript::start);
|
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, thread, &QThread::quit);
|
||||||
connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||||||
@ -423,104 +323,101 @@ void AccountSettingsWindow::startGetProfileInfo(const QString &appCode) {
|
|||||||
thread->start();
|
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") {
|
if (appCode == "ozon") {
|
||||||
QThread *thread = new QThread(nullptr);
|
auto *worker = new Ozon::GetCardInfoScript(deviceId, appCode, "", nullptr);
|
||||||
auto *worker = new Ozon::GetCardInfoScript(m_deviceId, appCode, "", nullptr);
|
|
||||||
worker->moveToThread(thread);
|
worker->moveToThread(thread);
|
||||||
connect(thread, &QThread::started, worker, &Ozon::GetCardInfoScript::start);
|
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, thread, &QThread::quit);
|
||||||
connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||||||
thread->start();
|
|
||||||
} else if (appCode == "black") {
|
} else if (appCode == "black") {
|
||||||
QThread *thread = new QThread(nullptr);
|
auto *worker = new Black::GetCardInfoScript(deviceId, appCode, nullptr);
|
||||||
auto *worker = new Black::GetCardInfoScript(m_deviceId, appCode, nullptr);
|
|
||||||
worker->moveToThread(thread);
|
worker->moveToThread(thread);
|
||||||
connect(thread, &QThread::started, worker, &Black::GetCardInfoScript::start);
|
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, thread, &QThread::quit);
|
||||||
connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(thread, &QThread::finished, thread, &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) {
|
// ── Send status to server ──────────────────────────────────────────────────
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
|
void AccountSettingsWindow::sendAppStatus(const QString &status) {
|
||||||
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code);
|
if (m_app.bankProfileId.isEmpty()) {
|
||||||
if (app.bankProfileId.isEmpty()) {
|
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << m_app.code;
|
||||||
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << code;
|
hideOverlay(true, "Сохранено локально", [this]() { updateStatusUI(); });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool enable = (status == "active");
|
const bool enable = (status == "active");
|
||||||
auto *thread = new QThread;
|
auto *thread = new QThread;
|
||||||
auto *net = new NetworkService;
|
auto *net = new NetworkService;
|
||||||
net->moveToThread(thread);
|
net->moveToThread(thread);
|
||||||
net->addExtra("bank_profile_id", app.bankProfileId);
|
net->addExtra("bank_profile_id", m_app.bankProfileId);
|
||||||
net->addExtra("enable", enable);
|
net->addExtra("enable", enable);
|
||||||
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||||||
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
connect(net, &NetworkService::finishedWithResult, this, [this](int result) {
|
||||||
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
if (result == 0) {
|
||||||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
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();
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AccountSettingsWindow::updateAccountData(const MaterialInfo &account) {
|
// ── Toggle card status ─────────────────────────────────────────────────────
|
||||||
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);
|
|
||||||
|
|
||||||
list.append(obj);
|
void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) {
|
||||||
|
|
||||||
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) {
|
|
||||||
const bool isActive = account.status == MaterialStatus::Active;
|
const bool isActive = account.status == MaterialStatus::Active;
|
||||||
const QString newStatus = isActive ? "off" : "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;
|
qWarning() << "[AccountSettings] Failed to update material status:" << account.id;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем на сервер PATCH /api/v1/profile/material/{id}/{true|false}
|
showOverlay(isActive ? "Выключение карты..." : "Включение карты...");
|
||||||
|
|
||||||
|
auto finishToggle = [this, isActive]() {
|
||||||
|
// Если выключили — проверяем остались ли активные
|
||||||
|
if (isActive) {
|
||||||
|
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||||||
|
bool hasActive = false;
|
||||||
|
for (const auto &m : materials) {
|
||||||
|
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
|
||||||
|
}
|
||||||
|
if (!hasActive && m_app.status == "active") {
|
||||||
|
BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, "off");
|
||||||
|
m_app.status = "off";
|
||||||
|
sendAppStatus("off");
|
||||||
|
return; // sendAppStatus покажет свой overlay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reloadCards();
|
||||||
|
};
|
||||||
|
|
||||||
if (!account.materialId.isEmpty()) {
|
if (!account.materialId.isEmpty()) {
|
||||||
auto *thread = new QThread;
|
auto *thread = new QThread;
|
||||||
auto *net = new NetworkService;
|
auto *net = new NetworkService;
|
||||||
@ -528,133 +425,85 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account, QT
|
|||||||
net->addExtra("material_id", account.materialId);
|
net->addExtra("material_id", account.materialId);
|
||||||
net->addExtra("enable", !isActive);
|
net->addExtra("enable", !isActive);
|
||||||
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
|
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, thread, &QThread::quit);
|
||||||
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
thread->start();
|
thread->start();
|
||||||
|
} else {
|
||||||
|
hideOverlay(true, "Сохранено локально", finishToggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если выключили материал — проверяем остались ли активные
|
|
||||||
if (isActive) {
|
|
||||||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
|
|
||||||
bool hasActive = false;
|
|
||||||
for (const auto &m : materials) {
|
|
||||||
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
|
|
||||||
}
|
|
||||||
if (!hasActive && m_applicationInfo.status == "active") {
|
|
||||||
BankProfileDAO::update(m_applicationInfo.id, m_applicationInfo.pinCode, m_applicationInfo.comment, "off");
|
|
||||||
m_applicationInfo.status = "off";
|
|
||||||
sendAppStatus(m_deviceId, m_applicationInfo.code, "off");
|
|
||||||
|
|
||||||
const QString status = QString("Статус: <span style='color:red;'>Неактивен</span>") +
|
|
||||||
QString(", пин-код: ") + (m_applicationInfo.pinCodeChecked
|
|
||||||
? "<span style='color:green;'>Проверен</span>"
|
|
||||||
: "<span style='color:red;'>Не проверен</span>");
|
|
||||||
m_statusLabel->setText(status);
|
|
||||||
|
|
||||||
QMessageBox::information(this, "Профиль выключен",
|
|
||||||
"Все карты выключены — банковский профиль автоматически деактивирован.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadContent(tableWidget);
|
|
||||||
tableWidget->resizeColumnsToContents();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
// ── Cards table ────────────────────────────────────────────────────────────
|
||||||
QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
|
|
||||||
|
|
||||||
tableWidget->clearContents();
|
void AccountSettingsWindow::reloadCards() {
|
||||||
tableWidget->setRowCount(accounts.length());
|
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||||||
tableWidget->setColumnCount(8);
|
|
||||||
tableWidget->setHorizontalHeaderLabels({
|
m_cardsTable->clearContents();
|
||||||
"Время обновления",
|
m_cardsTable->setRowCount(accounts.size());
|
||||||
"Номер",
|
m_cardsTable->setColumnCount(7);
|
||||||
"Сумма",
|
m_cardsTable->setHorizontalHeaderLabels({
|
||||||
"Валюта",
|
"Сервер", "Статус", "Время обновления", "Номер", "Сумма", "Валюта", ""
|
||||||
"",
|
|
||||||
"Сервер",
|
|
||||||
"",
|
|
||||||
""
|
|
||||||
});
|
});
|
||||||
|
m_cardsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
tableWidget->setWordWrap(true);
|
|
||||||
tableWidget->setTextElideMode(Qt::ElideNone);
|
|
||||||
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
||||||
tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
|
||||||
|
|
||||||
|
|
||||||
for (int k = 0; k < accounts.size(); ++k) {
|
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);
|
auto *syncItem = new QTableWidgetItem(acc.materialId.isEmpty() ? "Не синхр." : "Синхр.");
|
||||||
time->setFlags(time->flags() & ~Qt::ItemIsEditable);
|
syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
tableWidget->setItem(k, 0, time);
|
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);
|
const bool isActive = acc.status == MaterialStatus::Active;
|
||||||
desc->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
auto *statusItem = new QTableWidgetItem(materialStatusToUiString(acc.status));
|
||||||
desc->setFlags(desc->flags() & ~Qt::ItemIsEditable);
|
statusItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
tableWidget->setItem(k, 1, desc);
|
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);
|
auto *timeItem = new QTableWidgetItem(acc.updateTime.toString("dd.MM.yyyy HH:mm:ss"));
|
||||||
amount->setFlags(amount->flags() & ~Qt::ItemIsEditable);
|
timeItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
tableWidget->setItem(k, 2, amount);
|
m_cardsTable->setItem(k, 2, timeItem);
|
||||||
|
|
||||||
QTableWidgetItem *currency = new QTableWidgetItem(account.currency);
|
// Номер
|
||||||
currency->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
const QString displayNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber;
|
||||||
currency->setFlags(currency->flags() & ~Qt::ItemIsEditable);
|
auto *numItem = new QTableWidgetItem(displayNumber);
|
||||||
tableWidget->setItem(k, 3, currency);
|
numItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
|
m_cardsTable->setItem(k, 3, numItem);
|
||||||
|
|
||||||
// Кнопка вкл/выкл карты
|
// Сумма
|
||||||
const bool isActive = account.status == MaterialStatus::Active;
|
auto *amountItem = new QTableWidgetItem(QString::number(acc.amount));
|
||||||
auto *toggleBtn = new QPushButton(isActive ? "Выкл" : "Вкл", tableWidget);
|
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
|
toggleBtn->setStyleSheet(isActive
|
||||||
? "background-color: orange; color: white;"
|
? "background-color: orange; color: white;"
|
||||||
: "background-color: green; 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]() {
|
connect(toggleBtn, &QPushButton::clicked, this, [this, acc]() {
|
||||||
toggleMaterialStatus(account, tableWidget);
|
toggleMaterialStatus(acc);
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_cardsTable->resizeColumnsToContents();
|
||||||
|
m_cardsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
|
||||||
|
m_cardsTable->resizeRowsToContents();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QTableWidget>
|
#include <QTableWidget>
|
||||||
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "db/MaterialInfo.h"
|
#include "db/MaterialInfo.h"
|
||||||
#include "db/BankProfileInfo.h"
|
#include "db/BankProfileInfo.h"
|
||||||
@ -15,25 +19,32 @@ public:
|
|||||||
private:
|
private:
|
||||||
QString m_deviceId;
|
QString m_deviceId;
|
||||||
QString m_deviceName;
|
QString m_deviceName;
|
||||||
BankProfileInfo m_applicationInfo;
|
BankProfileInfo m_app;
|
||||||
|
|
||||||
QLabel *m_statusLabel;
|
QLabel *m_statusLabel;
|
||||||
QLabel *m_commentLabel;
|
QLabel *m_pinLabel;
|
||||||
QLabel *m_profileLabel;
|
QLabel *m_profileLabel;
|
||||||
|
QPushButton *m_toggleBtn;
|
||||||
|
QTableWidget *m_cardsTable;
|
||||||
|
|
||||||
void startCheckPinCode(const QString &appCode);
|
// Overlay
|
||||||
|
QWidget *m_overlay = nullptr;
|
||||||
|
QLabel *m_overlayText = nullptr;
|
||||||
|
QTimer *m_overlayTimer = nullptr;
|
||||||
|
|
||||||
void startGetProfileInfo(const QString &appCode);
|
void showOverlay(const QString &text);
|
||||||
|
void hideOverlay(bool success, const QString &message, std::function<void()> onHidden = nullptr);
|
||||||
|
|
||||||
void startGetCardInfo(const QString &appCode);
|
void updateStatusUI();
|
||||||
|
void toggleProfileStatus();
|
||||||
|
void editPinCode();
|
||||||
|
void startCheckPinCode();
|
||||||
|
void startGetProfileInfo();
|
||||||
|
void startGetCardInfo();
|
||||||
|
void sendAppStatus(const QString &status);
|
||||||
|
void toggleMaterialStatus(const MaterialInfo &account);
|
||||||
|
void reloadCards();
|
||||||
|
|
||||||
void startGetLastTransactions(const QString &appCode, int maxCount = 0);
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
#include "device/EditBankDialog.h"
|
#include "device/EditBankDialog.h"
|
||||||
#include "ozon/LoginAndCheckAccountsScript.h"
|
#include "ozon/LoginAndCheckAccountsScript.h"
|
||||||
#include "black/LoginAndCheckAccountsScript.h"
|
#include "black/LoginAndCheckAccountsScript.h"
|
||||||
|
#include "dushanbe/LoginAndCheckAccountsScript.h"
|
||||||
|
|
||||||
QTableWidgetItem *BankSettingsWindow::createTableWidget(const QString &text) {
|
QTableWidgetItem *BankSettingsWindow::createTableWidget(const QString &text) {
|
||||||
QTableWidgetItem *item = new QTableWidgetItem(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, thread, &QThread::quit);
|
||||||
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(thread, &QThread::finished, thread, &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 {
|
} else {
|
||||||
thread->deleteLater();
|
thread->deleteLater();
|
||||||
return;
|
return;
|
||||||
|
|||||||
605
views/bank/BankSetupWizard.cpp
Normal file
@ -0,0 +1,605 @@
|
|||||||
|
#include "BankSetupWizard.h"
|
||||||
|
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#include "ozon/LoginAndCheckAccountsScript.h"
|
||||||
|
#include "ozon/GetProfileInfoScript.h"
|
||||||
|
#include "ozon/GetCardInfoScript.h"
|
||||||
|
#include "black/LoginAndCheckAccountsScript.h"
|
||||||
|
#include "black/GetProfileInfoScript.h"
|
||||||
|
#include "black/GetCardInfoScript.h"
|
||||||
|
#include "dushanbe/LoginAndCheckAccountsScript.h"
|
||||||
|
#include "dushanbe/GetProfileInfoScript.h"
|
||||||
|
#include "dushanbe/GetCardInfoScript.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
|
#include "dao/DeviceDAO.h"
|
||||||
|
#include "dao/MaterialDAO.h"
|
||||||
|
#include "net/NetworkService.h"
|
||||||
|
|
||||||
|
BankSetupWizard::BankSetupWizard(QWidget *parent, const QString &deviceId, const BankProfileInfo &app)
|
||||||
|
: QDialog(parent), m_deviceId(deviceId), m_app(app) {
|
||||||
|
setWindowTitle("Настройка банка — " + app.name);
|
||||||
|
setMinimumSize(500, 350);
|
||||||
|
resize(520, 380);
|
||||||
|
|
||||||
|
auto *mainLayout = new QVBoxLayout(this);
|
||||||
|
m_stack = new QStackedWidget(this);
|
||||||
|
|
||||||
|
m_stack->addWidget(createStep1()); // 0
|
||||||
|
m_stack->addWidget(createStep2()); // 1
|
||||||
|
m_stack->addWidget(createStep3()); // 2
|
||||||
|
m_stack->addWidget(createStep4Success()); // 3
|
||||||
|
m_stack->addWidget(createStep4Error()); // 4
|
||||||
|
|
||||||
|
m_stack->setCurrentIndex(0);
|
||||||
|
mainLayout->addWidget(m_stack);
|
||||||
|
|
||||||
|
pauseActiveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
BankSetupWizard::~BankSetupWizard() {
|
||||||
|
stopWorkerThread();
|
||||||
|
resumePausedProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 1: Intro ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
QWidget *BankSetupWizard::createStep1() {
|
||||||
|
auto *page = new QWidget;
|
||||||
|
auto *layout = new QVBoxLayout(page);
|
||||||
|
layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
auto *icon = new QLabel;
|
||||||
|
QPixmap pixmap(QCoreApplication::applicationDirPath() + "/images/" + m_app.code + "_icon.png");
|
||||||
|
if (!pixmap.isNull())
|
||||||
|
icon->setPixmap(pixmap.scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
|
icon->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(icon);
|
||||||
|
|
||||||
|
auto *title = new QLabel("<h2>" + m_app.name + "</h2>");
|
||||||
|
title->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(title);
|
||||||
|
|
||||||
|
auto *desc = new QLabel(
|
||||||
|
"Мастер поможет настроить банковский профиль для приложения <b>" + m_app.name + "</b>.<br><br>"
|
||||||
|
"Будут выполнены следующие шаги:<br>"
|
||||||
|
"1. Ввод пин-кода<br>"
|
||||||
|
"2. Проверка входа в приложение<br>"
|
||||||
|
"3. Получение данных профиля и карт"
|
||||||
|
);
|
||||||
|
desc->setWordWrap(true);
|
||||||
|
desc->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(desc);
|
||||||
|
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
auto *btnLayout = new QHBoxLayout;
|
||||||
|
btnLayout->addStretch();
|
||||||
|
auto *nextBtn = new QPushButton("Далее");
|
||||||
|
nextBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;");
|
||||||
|
connect(nextBtn, &QPushButton::clicked, this, [this]() {
|
||||||
|
m_stack->setCurrentIndex(1);
|
||||||
|
});
|
||||||
|
btnLayout->addWidget(nextBtn);
|
||||||
|
layout->addLayout(btnLayout);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 2: PIN Code ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
QWidget *BankSetupWizard::createStep2() {
|
||||||
|
auto *page = new QWidget;
|
||||||
|
auto *layout = new QVBoxLayout(page);
|
||||||
|
layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
auto *title = new QLabel("<h3>Введите пин-код</h3>");
|
||||||
|
title->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(title);
|
||||||
|
|
||||||
|
auto *desc = new QLabel("Укажите пин-код для входа в приложение " + m_app.name + ".");
|
||||||
|
desc->setWordWrap(true);
|
||||||
|
desc->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(desc);
|
||||||
|
|
||||||
|
layout->addSpacing(16);
|
||||||
|
|
||||||
|
m_pinCodeEdit = new QLineEdit;
|
||||||
|
m_pinCodeEdit->setPlaceholderText("Пин-код");
|
||||||
|
m_pinCodeEdit->setMaxLength(10);
|
||||||
|
m_pinCodeEdit->setAlignment(Qt::AlignCenter);
|
||||||
|
m_pinCodeEdit->setFixedWidth(200);
|
||||||
|
|
||||||
|
auto *editLayout = new QHBoxLayout;
|
||||||
|
editLayout->addStretch();
|
||||||
|
editLayout->addWidget(m_pinCodeEdit);
|
||||||
|
editLayout->addStretch();
|
||||||
|
layout->addLayout(editLayout);
|
||||||
|
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
auto *btnLayout = new QHBoxLayout;
|
||||||
|
auto *backBtn = new QPushButton("Назад");
|
||||||
|
connect(backBtn, &QPushButton::clicked, this, [this]() {
|
||||||
|
m_stack->setCurrentIndex(0);
|
||||||
|
});
|
||||||
|
btnLayout->addWidget(backBtn);
|
||||||
|
btnLayout->addStretch();
|
||||||
|
auto *nextBtn = new QPushButton("Далее");
|
||||||
|
nextBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;");
|
||||||
|
connect(nextBtn, &QPushButton::clicked, this, [this]() {
|
||||||
|
if (m_pinCodeEdit->text().trimmed().isEmpty()) return;
|
||||||
|
m_app.pinCode = m_pinCodeEdit->text().trimmed();
|
||||||
|
m_stack->setCurrentIndex(2);
|
||||||
|
startVerification();
|
||||||
|
});
|
||||||
|
btnLayout->addWidget(nextBtn);
|
||||||
|
layout->addLayout(btnLayout);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: Progress ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
QWidget *BankSetupWizard::createStep3() {
|
||||||
|
auto *page = new QWidget;
|
||||||
|
auto *layout = new QVBoxLayout(page);
|
||||||
|
layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
auto *title = new QLabel("<h3>Идёт проверка банка</h3>");
|
||||||
|
title->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(title);
|
||||||
|
|
||||||
|
auto *desc = new QLabel(
|
||||||
|
"Пожалуйста, не закрывайте приложение и не трогайте телефон.\n"
|
||||||
|
"Проверка может занять некоторое время."
|
||||||
|
);
|
||||||
|
desc->setWordWrap(true);
|
||||||
|
desc->setAlignment(Qt::AlignCenter);
|
||||||
|
desc->setStyleSheet("color: gray; margin-bottom: 12px;");
|
||||||
|
layout->addWidget(desc);
|
||||||
|
|
||||||
|
m_progressBar = new QProgressBar;
|
||||||
|
m_progressBar->setRange(0, 100);
|
||||||
|
m_progressBar->setValue(0);
|
||||||
|
m_progressBar->setTextVisible(true);
|
||||||
|
layout->addWidget(m_progressBar);
|
||||||
|
|
||||||
|
m_progressLabel = new QLabel("Подготовка...");
|
||||||
|
m_progressLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
m_progressLabel->setStyleSheet("margin-top: 8px;");
|
||||||
|
layout->addWidget(m_progressLabel);
|
||||||
|
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
auto *btnLayout = new QHBoxLayout;
|
||||||
|
m_backBtn3 = new QPushButton("Назад");
|
||||||
|
connect(m_backBtn3, &QPushButton::clicked, this, [this]() {
|
||||||
|
stopWorkerThread();
|
||||||
|
m_progressBar->setValue(0);
|
||||||
|
m_progressLabel->setText("Подготовка...");
|
||||||
|
m_stack->setCurrentIndex(1);
|
||||||
|
});
|
||||||
|
btnLayout->addWidget(m_backBtn3);
|
||||||
|
btnLayout->addStretch();
|
||||||
|
layout->addLayout(btnLayout);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 4a: Success ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
QWidget *BankSetupWizard::createStep4Success() {
|
||||||
|
m_successPage = new QWidget;
|
||||||
|
auto *layout = new QVBoxLayout(m_successPage);
|
||||||
|
layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
auto *title = new QLabel("<h3 style='color:green;'>Проверка прошла успешно</h3>");
|
||||||
|
title->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(title);
|
||||||
|
|
||||||
|
m_resultLabel = new QLabel;
|
||||||
|
m_resultLabel->setWordWrap(true);
|
||||||
|
m_resultLabel->setTextFormat(Qt::RichText);
|
||||||
|
layout->addWidget(m_resultLabel);
|
||||||
|
|
||||||
|
auto *cardsTitle = new QLabel("<b>Найденные карты:</b>");
|
||||||
|
layout->addWidget(cardsTitle);
|
||||||
|
|
||||||
|
m_cardsTable = new QTableWidget;
|
||||||
|
m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection);
|
||||||
|
m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
|
m_cardsTable->setFocusPolicy(Qt::NoFocus);
|
||||||
|
m_cardsTable->setColumnCount(3);
|
||||||
|
m_cardsTable->setHorizontalHeaderLabels({"Номер", "Сумма", "Валюта"});
|
||||||
|
m_cardsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||||
|
layout->addWidget(m_cardsTable);
|
||||||
|
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
auto *btnLayout = new QHBoxLayout;
|
||||||
|
btnLayout->addStretch();
|
||||||
|
auto *finishBtn = new QPushButton("Завершить");
|
||||||
|
finishBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;");
|
||||||
|
connect(finishBtn, &QPushButton::clicked, this, &QDialog::accept);
|
||||||
|
btnLayout->addWidget(finishBtn);
|
||||||
|
layout->addLayout(btnLayout);
|
||||||
|
|
||||||
|
return m_successPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 4b: Error ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
QWidget *BankSetupWizard::createStep4Error() {
|
||||||
|
m_errorPage = new QWidget;
|
||||||
|
auto *layout = new QVBoxLayout(m_errorPage);
|
||||||
|
layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
auto *title = new QLabel("<h3 style='color:red;'>Ошибка</h3>");
|
||||||
|
title->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(title);
|
||||||
|
|
||||||
|
m_errorText = new QLabel;
|
||||||
|
m_errorText->setWordWrap(true);
|
||||||
|
m_errorText->setStyleSheet("color: red; margin: 12px 0;");
|
||||||
|
m_errorText->setAlignment(Qt::AlignCenter);
|
||||||
|
layout->addWidget(m_errorText);
|
||||||
|
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
auto *btnLayout = new QHBoxLayout;
|
||||||
|
auto *backBtn = new QPushButton("Назад");
|
||||||
|
connect(backBtn, &QPushButton::clicked, this, [this]() {
|
||||||
|
m_stack->setCurrentIndex(1);
|
||||||
|
});
|
||||||
|
btnLayout->addWidget(backBtn);
|
||||||
|
btnLayout->addStretch();
|
||||||
|
auto *restartBtn = new QPushButton("Начать заново");
|
||||||
|
restartBtn->setStyleSheet("background-color: orange; color: white; padding: 6px 24px;");
|
||||||
|
connect(restartBtn, &QPushButton::clicked, this, [this]() {
|
||||||
|
// Очищаем данные из БД если ещё не отправлены на сервер
|
||||||
|
const BankProfileInfo current = BankProfileDAO::getApplication(m_deviceId, m_app.code);
|
||||||
|
if (current.id >= 0 && current.bankProfileId.isEmpty()) {
|
||||||
|
MaterialDAO::deleteAccountsByApp(m_deviceId, m_app.code);
|
||||||
|
BankProfileDAO::deleteProfile(m_deviceId, m_app.code);
|
||||||
|
m_app.id = -1;
|
||||||
|
m_app.pinCodeChecked = false;
|
||||||
|
m_app.pinCode.clear();
|
||||||
|
}
|
||||||
|
m_pinCodeEdit->clear();
|
||||||
|
m_progressBar->setValue(0);
|
||||||
|
m_progressLabel->setText("Подготовка...");
|
||||||
|
m_stack->setCurrentIndex(0);
|
||||||
|
});
|
||||||
|
btnLayout->addWidget(restartBtn);
|
||||||
|
layout->addLayout(btnLayout);
|
||||||
|
|
||||||
|
return m_errorPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pause/Resume active profiles ───────────────────────────────────────────
|
||||||
|
|
||||||
|
static void toggleProfileOnServer(const BankProfileInfo &app, bool enable) {
|
||||||
|
if (app.bankProfileId.isEmpty()) return;
|
||||||
|
auto *thread = new QThread;
|
||||||
|
auto *net = new NetworkService;
|
||||||
|
net->moveToThread(thread);
|
||||||
|
net->addExtra("bank_profile_id", app.bankProfileId);
|
||||||
|
net->addExtra("enable", enable);
|
||||||
|
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||||||
|
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||||
|
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||||
|
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::pauseActiveProfiles() {
|
||||||
|
const QList<BankProfileInfo> apps = BankProfileDAO::getApplicationsByDeviceId(m_deviceId);
|
||||||
|
for (const BankProfileInfo &app : apps) {
|
||||||
|
if (app.status == "active") {
|
||||||
|
m_pausedProfiles.append(app.code);
|
||||||
|
BankProfileDAO::update(app.id, app.pinCode, app.comment, "off");
|
||||||
|
toggleProfileOnServer(app, false);
|
||||||
|
qDebug() << "[BankSetupWizard] Paused profile:" << app.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::resumePausedProfiles() {
|
||||||
|
for (const QString &code : m_pausedProfiles) {
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(m_deviceId, code);
|
||||||
|
if (app.id >= 0 && app.status == "off") {
|
||||||
|
BankProfileDAO::update(app.id, app.pinCode, app.comment, "active");
|
||||||
|
toggleProfileOnServer(app, true);
|
||||||
|
qDebug() << "[BankSetupWizard] Resumed profile:" << app.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_pausedProfiles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Verification flow ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void BankSetupWizard::setProgress(int value, const QString &text) {
|
||||||
|
m_progressBar->setValue(value);
|
||||||
|
m_progressLabel->setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::stopWorkerThread() {
|
||||||
|
if (m_workerThread) {
|
||||||
|
m_workerThread->quit();
|
||||||
|
m_workerThread->wait(3000);
|
||||||
|
m_workerThread = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::startVerification() {
|
||||||
|
// Проверяем что устройство онлайн
|
||||||
|
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
|
||||||
|
if (device.status != "device") {
|
||||||
|
onVerificationError("Устройство не подключено.\nУбедитесь, что телефон подключён и попробуйте снова.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save pin code to DB first via upsert
|
||||||
|
m_app.status = "off";
|
||||||
|
m_app.install = true;
|
||||||
|
BankProfileDAO::upsertApplication(m_app);
|
||||||
|
|
||||||
|
// Re-read to get the id
|
||||||
|
m_app = BankProfileDAO::getApplication(m_deviceId, m_app.code);
|
||||||
|
|
||||||
|
setProgress(5, "Запуск проверки пин-кода...");
|
||||||
|
m_backBtn3->setEnabled(true);
|
||||||
|
|
||||||
|
runPinCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::runPinCheck() {
|
||||||
|
stopWorkerThread();
|
||||||
|
m_workerThread = new QThread(this);
|
||||||
|
const QString deviceId = m_deviceId;
|
||||||
|
const QString appCode = m_app.code;
|
||||||
|
const int appId = m_app.id;
|
||||||
|
|
||||||
|
if (appCode == "ozon") {
|
||||||
|
auto *worker = new Ozon::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, false, true);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start);
|
||||||
|
connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this,
|
||||||
|
[this, appId, appCode](const QString &result) {
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
onVerificationError("Ошибка проверки пин-кода: " + result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
|
||||||
|
m_app.pinCodeChecked = true;
|
||||||
|
setProgress(33, "Пин-код проверен. Получение профиля...");
|
||||||
|
runProfileCheck();
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else if (appCode == "black") {
|
||||||
|
auto *worker = new Black::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start);
|
||||||
|
connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, this,
|
||||||
|
[this, appId](const QString &result) {
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
onVerificationError("Ошибка проверки пин-кода: " + result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
|
||||||
|
m_app.pinCodeChecked = true;
|
||||||
|
setProgress(33, "Пин-код проверен. Получение профиля...");
|
||||||
|
runProfileCheck();
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Black::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else if (appCode == "dushanbe") {
|
||||||
|
auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
|
||||||
|
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, this,
|
||||||
|
[this, appId](const QString &result) {
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
onVerificationError("Ошибка проверки пин-кода: " + result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BankProfileDAO::updatePinCodeStatus(appId, "yes", "");
|
||||||
|
m_app.pinCodeChecked = true;
|
||||||
|
setProgress(33, "Пин-код проверен. Получение профиля...");
|
||||||
|
runProfileCheck();
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else {
|
||||||
|
onVerificationError("Банк " + appCode + " не поддерживается для автоматической настройки.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_workerThread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::runProfileCheck() {
|
||||||
|
stopWorkerThread();
|
||||||
|
m_workerThread = new QThread(this);
|
||||||
|
const QString deviceId = m_deviceId;
|
||||||
|
const QString appCode = m_app.code;
|
||||||
|
|
||||||
|
auto onProfileResult = [this, deviceId, appCode](const QString &result) {
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
onVerificationError("Ошибка получения профиля: " + result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Проверяем что bank_profile_id получен от сервера
|
||||||
|
const BankProfileInfo updated = BankProfileDAO::getApplication(deviceId, appCode);
|
||||||
|
if (updated.bankProfileId.isEmpty()) {
|
||||||
|
onVerificationError("Не удалось зарегистрировать банковский профиль на сервере.\n"
|
||||||
|
"Возможно, банк ещё не поддерживается сервером.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProgress(66, "Профиль получен. Проверка карт...");
|
||||||
|
runCardCheck();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (appCode == "ozon") {
|
||||||
|
auto *worker = new Ozon::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start);
|
||||||
|
connect(worker, &Ozon::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Ozon::GetProfileInfoScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else if (appCode == "black") {
|
||||||
|
auto *worker = new Black::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Black::GetProfileInfoScript::start);
|
||||||
|
connect(worker, &Black::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Black::GetProfileInfoScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else if (appCode == "dushanbe") {
|
||||||
|
auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start);
|
||||||
|
connect(worker, &Dushanbe::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Dushanbe::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Dushanbe::GetProfileInfoScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else {
|
||||||
|
onVerificationError("Банк " + appCode + " не поддерживается.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_workerThread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::runCardCheck() {
|
||||||
|
stopWorkerThread();
|
||||||
|
m_workerThread = new QThread(this);
|
||||||
|
const QString deviceId = m_deviceId;
|
||||||
|
const QString appCode = m_app.code;
|
||||||
|
|
||||||
|
auto onCardResult = [this, deviceId, appCode](const QString &result) {
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
onVerificationError("Ошибка получения карт: " + result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Проверяем что хотя бы одна карта синхронизирована с сервером
|
||||||
|
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(deviceId, appCode);
|
||||||
|
bool hasSynced = false;
|
||||||
|
for (const auto &m : materials) {
|
||||||
|
if (!m.materialId.isEmpty()) { hasSynced = true; break; }
|
||||||
|
}
|
||||||
|
if (!hasSynced && !materials.isEmpty()) {
|
||||||
|
onVerificationError("Не удалось отправить данные карт на сервер.\n"
|
||||||
|
"Проверьте подключение к серверу.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProgress(100, "Готово!");
|
||||||
|
onVerificationSuccess();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (appCode == "ozon") {
|
||||||
|
auto *worker = new Ozon::GetCardInfoScript(deviceId, appCode, "", nullptr);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Ozon::GetCardInfoScript::start);
|
||||||
|
connect(worker, &Ozon::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Ozon::GetCardInfoScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else if (appCode == "black") {
|
||||||
|
auto *worker = new Black::GetCardInfoScript(deviceId, appCode, nullptr);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Black::GetCardInfoScript::start);
|
||||||
|
connect(worker, &Black::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Black::GetCardInfoScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else if (appCode == "dushanbe") {
|
||||||
|
auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr);
|
||||||
|
worker->moveToThread(m_workerThread);
|
||||||
|
connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start);
|
||||||
|
connect(worker, &Dushanbe::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection);
|
||||||
|
connect(worker, &Dushanbe::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
|
connect(worker, &Dushanbe::GetCardInfoScript::finished, m_workerThread, &QThread::quit);
|
||||||
|
} else {
|
||||||
|
onVerificationError("Банк " + appCode + " не поддерживается.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_workerThread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::onVerificationSuccess() {
|
||||||
|
// Re-read profile from DB
|
||||||
|
m_app = BankProfileDAO::getApplication(m_deviceId, m_app.code);
|
||||||
|
|
||||||
|
QString profileHtml;
|
||||||
|
if (!m_app.fullName.isEmpty())
|
||||||
|
profileHtml += "<b>ФИО:</b> " + m_app.fullName + "<br>";
|
||||||
|
if (!m_app.phone.isEmpty())
|
||||||
|
profileHtml += "<b>Телефон:</b> " + m_app.phone + "<br>";
|
||||||
|
if (!m_app.email.isEmpty())
|
||||||
|
profileHtml += "<b>Email:</b> " + m_app.email + "<br>";
|
||||||
|
if (profileHtml.isEmpty())
|
||||||
|
profileHtml = "<i>Данные профиля не получены</i>";
|
||||||
|
m_resultLabel->setText(profileHtml);
|
||||||
|
|
||||||
|
// Load cards
|
||||||
|
const QList<MaterialInfo> cards = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||||||
|
m_cardsTable->setRowCount(cards.size());
|
||||||
|
for (int i = 0; i < cards.size(); ++i) {
|
||||||
|
const MaterialInfo &c = cards[i];
|
||||||
|
const QString displayNumber = c.cardNumber.isEmpty() ? c.lastNumbers : c.cardNumber;
|
||||||
|
m_cardsTable->setItem(i, 0, new QTableWidgetItem(displayNumber));
|
||||||
|
m_cardsTable->setItem(i, 1, new QTableWidgetItem(QString::number(c.amount)));
|
||||||
|
m_cardsTable->setItem(i, 2, new QTableWidgetItem(c.currency));
|
||||||
|
}
|
||||||
|
m_cardsTable->resizeColumnsToContents();
|
||||||
|
|
||||||
|
if (cards.isEmpty()) {
|
||||||
|
m_cardsTable->setRowCount(1);
|
||||||
|
auto *item = new QTableWidgetItem("Карты не найдены");
|
||||||
|
item->setForeground(QBrush(Qt::gray));
|
||||||
|
m_cardsTable->setItem(0, 0, item);
|
||||||
|
m_cardsTable->setSpan(0, 0, 1, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_stack->setCurrentIndex(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString humanReadableError(const QString &error) {
|
||||||
|
// Определяем этап
|
||||||
|
QString stage;
|
||||||
|
if (error.contains("пин-кода"))
|
||||||
|
stage = "Проверка пин-кода: ";
|
||||||
|
else if (error.contains("профиля"))
|
||||||
|
stage = "Получение профиля: ";
|
||||||
|
else if (error.contains("карт"))
|
||||||
|
stage = "Получение карт: ";
|
||||||
|
|
||||||
|
if (error.contains("Device or app not found"))
|
||||||
|
return stage + "Устройство или приложение не найдено.\nУбедитесь, что телефон подключён и приложение установлено.";
|
||||||
|
if (error.contains("Cant open the home screen") || error.contains("Cannot reach home screen"))
|
||||||
|
return stage + "Не удалось открыть главный экран приложения.\nПопробуйте перезапустить приложение на телефоне и повторить.";
|
||||||
|
if (error.contains("Cannot find user name") || error.contains("Cannot find Hola button"))
|
||||||
|
return stage + "Не удалось найти данные профиля на главном экране.\nВозможно, приложение обновилось или экран выглядит иначе.";
|
||||||
|
if (error.contains("Profile screen not detected") || error.contains("Profile page not detected"))
|
||||||
|
return stage + "Не удалось открыть страницу профиля.\nПопробуйте повторить позже.";
|
||||||
|
if (error.contains("Side menu not detected"))
|
||||||
|
return stage + "Не удалось открыть боковое меню приложения.\nПопробуйте повторить.";
|
||||||
|
if (error.contains("Cannot find Tu Informaci") || error.contains("Cannot find Tu CVU"))
|
||||||
|
return stage + "Не удалось найти нужный раздел в меню приложения.\nВозможно, приложение обновилось.";
|
||||||
|
if (error.contains("CVU screen not detected") || error.contains("Cannot parse CVU"))
|
||||||
|
return stage + "Не удалось получить данные карты.\nПопробуйте повторить.";
|
||||||
|
if (error.contains("No cards found"))
|
||||||
|
return stage + "Карты не найдены на главном экране.\nУбедитесь, что в приложении есть привязанные карты.";
|
||||||
|
if (error.contains("не поддерживается") || error.contains("не подключено"))
|
||||||
|
return error;
|
||||||
|
return stage + "Произошла непредвиденная ошибка.\nПопробуйте повторить позже.";
|
||||||
|
}
|
||||||
|
|
||||||
|
void BankSetupWizard::onVerificationError(const QString &error) {
|
||||||
|
qWarning() << "[BankSetupWizard] Error:" << error;
|
||||||
|
m_errorText->setText(humanReadableError(error));
|
||||||
|
m_stack->setCurrentIndex(4);
|
||||||
|
}
|
||||||
64
views/bank/BankSetupWizard.h
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QTableWidget>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include "db/BankProfileInfo.h"
|
||||||
|
|
||||||
|
class BankSetupWizard final : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
BankSetupWizard(QWidget *parent, const QString &deviceId, const BankProfileInfo &app);
|
||||||
|
~BankSetupWizard() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_deviceId;
|
||||||
|
BankProfileInfo m_app;
|
||||||
|
|
||||||
|
QStackedWidget *m_stack;
|
||||||
|
|
||||||
|
// Step 2
|
||||||
|
QLineEdit *m_pinCodeEdit;
|
||||||
|
|
||||||
|
// Step 3
|
||||||
|
QLabel *m_progressLabel;
|
||||||
|
QProgressBar *m_progressBar;
|
||||||
|
QPushButton *m_backBtn3;
|
||||||
|
|
||||||
|
// Step 4
|
||||||
|
QLabel *m_resultLabel;
|
||||||
|
QTableWidget *m_cardsTable;
|
||||||
|
QWidget *m_successPage;
|
||||||
|
QWidget *m_errorPage;
|
||||||
|
QLabel *m_errorText;
|
||||||
|
|
||||||
|
QThread *m_workerThread = nullptr;
|
||||||
|
QStringList m_pausedProfiles; // коды профилей, выключенных на время мастера
|
||||||
|
|
||||||
|
void pauseActiveProfiles();
|
||||||
|
void resumePausedProfiles();
|
||||||
|
|
||||||
|
QWidget *createStep1();
|
||||||
|
QWidget *createStep2();
|
||||||
|
QWidget *createStep3();
|
||||||
|
QWidget *createStep4Success();
|
||||||
|
QWidget *createStep4Error();
|
||||||
|
|
||||||
|
void startVerification();
|
||||||
|
void runPinCheck();
|
||||||
|
void runProfileCheck();
|
||||||
|
void runCardCheck();
|
||||||
|
|
||||||
|
void onVerificationSuccess();
|
||||||
|
void onVerificationError(const QString &error);
|
||||||
|
void stopWorkerThread();
|
||||||
|
|
||||||
|
void setProgress(int value, const QString &text);
|
||||||
|
};
|
||||||
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include "bank/AccountSettingsWindow.h"
|
#include "bank/AccountSettingsWindow.h"
|
||||||
|
#include "bank/BankSetupWizard.h"
|
||||||
#include "dao/BankProfileDAO.h"
|
#include "dao/BankProfileDAO.h"
|
||||||
#include "db/BankProfileInfo.h"
|
#include "db/BankProfileInfo.h"
|
||||||
|
|
||||||
@ -139,17 +140,30 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
|||||||
tableWidget->setItem(k, 5, commentItem);
|
tableWidget->setItem(k, 5, commentItem);
|
||||||
|
|
||||||
// Кнопка "Настроить"
|
// Кнопка "Настроить"
|
||||||
auto *settingsBtn = new QPushButton("Настроить", tableWidget);
|
if (app.install) {
|
||||||
settingsBtn->setStyleSheet("background-color: green; color: white; border-radius: 4px; height: 26px;");
|
auto *settingsBtn = new QPushButton("Настроить", tableWidget);
|
||||||
tableWidget->setCellWidget(k, 6, settingsBtn);
|
settingsBtn->setStyleSheet("background-color: green; color: white; border-radius: 4px; height: 26px;");
|
||||||
|
tableWidget->setCellWidget(k, 6, settingsBtn);
|
||||||
|
|
||||||
connect(settingsBtn, &QPushButton::clicked, this, [this, app]() {
|
connect(settingsBtn, &QPushButton::clicked, this, [this, app, tableWidget]() {
|
||||||
auto *window = new AccountSettingsWindow(nullptr, m_deviceId, m_deviceName, app);
|
const BankProfileInfo dbApp = BankProfileDAO::getApplication(m_deviceId, app.code);
|
||||||
window->setWindowFlags(window->windowFlags() | Qt::Window);
|
if (dbApp.id < 0 || dbApp.bankProfileId.isEmpty()) {
|
||||||
window->setAttribute(Qt::WA_DeleteOnClose);
|
auto *wizard = new BankSetupWizard(this, m_deviceId, dbApp.id >= 0 ? dbApp : app);
|
||||||
window->show();
|
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
window->raise();
|
connect(wizard, &QDialog::finished, this, [this, tableWidget]() {
|
||||||
window->activateWindow();
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
|
||||||
#include "bank/AccountSettingsWindow.h"
|
#include "bank/AccountSettingsWindow.h"
|
||||||
|
#include "bank/BankSetupWizard.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
#include "bank/BankSettingsWindow.h"
|
#include "bank/BankSettingsWindow.h"
|
||||||
#include "DeviceSettingsWindow.h"
|
#include "DeviceSettingsWindow.h"
|
||||||
#include "dao/BankProfileDAO.h"
|
#include "dao/BankProfileDAO.h"
|
||||||
@ -236,19 +238,22 @@ void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileIn
|
|||||||
QString deviceId = m_device.id;
|
QString deviceId = m_device.id;
|
||||||
QString deviceName = m_device.name;
|
QString deviceName = m_device.name;
|
||||||
connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() {
|
connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() {
|
||||||
static QPointer<AccountSettingsWindow> bankSettingsWindow;
|
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);
|
||||||
if (!bankSettingsWindow) {
|
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
bankSettingsWindow = new AccountSettingsWindow(nullptr, deviceId, deviceName, app);
|
wizard->show();
|
||||||
bankSettingsWindow->setWindowFlags(bankSettingsWindow->windowFlags() | Qt::Window);
|
} else {
|
||||||
bankSettingsWindow->setAttribute(Qt::WA_DeleteOnClose);
|
static QPointer<AccountSettingsWindow> bankSettingsWindow;
|
||||||
|
if (!bankSettingsWindow) {
|
||||||
|
bankSettingsWindow = new AccountSettingsWindow(nullptr, deviceId, deviceName, dbApp);
|
||||||
|
bankSettingsWindow->setWindowFlags(bankSettingsWindow->windowFlags() | Qt::Window);
|
||||||
|
bankSettingsWindow->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
}
|
||||||
|
bankSettingsWindow->show();
|
||||||
|
bankSettingsWindow->raise();
|
||||||
|
bankSettingsWindow->activateWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показать/поднять/активировать
|
|
||||||
bankSettingsWindow->show();
|
|
||||||
bankSettingsWindow->raise();
|
|
||||||
bankSettingsWindow->activateWindow();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
#include "dao/EventDAO.h"
|
#include "dao/EventDAO.h"
|
||||||
#include "dao/GeneralLogDAO.h"
|
#include "dao/GeneralLogDAO.h"
|
||||||
#include "dao/SettingsDAO.h"
|
#include "dao/SettingsDAO.h"
|
||||||
|
#include "log/LogDetailWindow.h"
|
||||||
|
|
||||||
static constexpr int COL_ID = 0;
|
static constexpr int COL_ID = 0;
|
||||||
static constexpr int COL_TIME = 1;
|
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) {
|
connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) {
|
||||||
showScreenshot(row);
|
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);
|
connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogToTelegram);
|
||||||
|
|
||||||
loadPage();
|
loadPage();
|
||||||
|
|||||||
95
views/log/LogDetailWindow.cpp
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
#include "LogDetailWindow.h"
|
||||||
|
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QScrollArea>
|
||||||
|
|
||||||
|
LogDetailWindow::LogDetailWindow(const GeneralLogEntry &entry, QWidget *parent)
|
||||||
|
: QDialog(parent) {
|
||||||
|
setWindowTitle("Лог #" + QString::number(entry.id));
|
||||||
|
resize(700, 500);
|
||||||
|
|
||||||
|
auto *layout = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
// Заголовок
|
||||||
|
auto *headerLayout = new QHBoxLayout;
|
||||||
|
auto *idLabel = new QLabel("<b>#" + QString::number(entry.id) + "</b>");
|
||||||
|
headerLayout->addWidget(idLabel);
|
||||||
|
|
||||||
|
auto *levelLabel = new QLabel(entry.level);
|
||||||
|
if (entry.level == "WARNING")
|
||||||
|
levelLabel->setStyleSheet("color: orange; font-weight: bold;");
|
||||||
|
else if (entry.level == "CRITICAL" || entry.level == "FATAL")
|
||||||
|
levelLabel->setStyleSheet("color: red; font-weight: bold;");
|
||||||
|
else
|
||||||
|
levelLabel->setStyleSheet("color: gray;");
|
||||||
|
headerLayout->addWidget(levelLabel);
|
||||||
|
|
||||||
|
auto *timeLabel = new QLabel(entry.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss"));
|
||||||
|
timeLabel->setStyleSheet("color: gray;");
|
||||||
|
headerLayout->addWidget(timeLabel);
|
||||||
|
headerLayout->addStretch();
|
||||||
|
layout->addLayout(headerLayout);
|
||||||
|
|
||||||
|
// Источник
|
||||||
|
auto *sourceLabel = new QLabel("<b>Источник:</b> " + entry.source);
|
||||||
|
sourceLabel->setWordWrap(true);
|
||||||
|
sourceLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
layout->addWidget(sourceLabel);
|
||||||
|
|
||||||
|
// Сообщение
|
||||||
|
layout->addWidget(new QLabel("<b>Сообщение:</b>"));
|
||||||
|
auto *messageEdit = new QTextEdit;
|
||||||
|
messageEdit->setPlainText(entry.message);
|
||||||
|
messageEdit->setReadOnly(true);
|
||||||
|
layout->addWidget(messageEdit, 1);
|
||||||
|
|
||||||
|
// Скриншот
|
||||||
|
if (!entry.screenshot.isEmpty()) {
|
||||||
|
QPixmap pixmap;
|
||||||
|
pixmap.loadFromData(entry.screenshot);
|
||||||
|
if (!pixmap.isNull()) {
|
||||||
|
layout->addWidget(new QLabel("<b>Скриншот:</b>"));
|
||||||
|
auto *imgLabel = new QLabel;
|
||||||
|
imgLabel->setPixmap(pixmap.scaledToWidth(
|
||||||
|
qMin(pixmap.width(), 600), Qt::SmoothTransformation));
|
||||||
|
imgLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
|
auto *scrollArea = new QScrollArea;
|
||||||
|
scrollArea->setWidget(imgLabel);
|
||||||
|
scrollArea->setWidgetResizable(false);
|
||||||
|
scrollArea->setMaximumHeight(300);
|
||||||
|
layout->addWidget(scrollArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопки
|
||||||
|
auto *btnLayout = new QHBoxLayout;
|
||||||
|
|
||||||
|
auto *copyBtn = new QPushButton("Копировать");
|
||||||
|
copyBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 16px;");
|
||||||
|
connect(copyBtn, &QPushButton::clicked, this, [entry]() {
|
||||||
|
const QString text = QString("ID: %1\nУровень: %2\nВремя: %3\nИсточник: %4\nСообщение: %5")
|
||||||
|
.arg(entry.id)
|
||||||
|
.arg(entry.level)
|
||||||
|
.arg(entry.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss"))
|
||||||
|
.arg(entry.source)
|
||||||
|
.arg(entry.message);
|
||||||
|
QApplication::clipboard()->setText(text);
|
||||||
|
});
|
||||||
|
btnLayout->addWidget(copyBtn);
|
||||||
|
|
||||||
|
btnLayout->addStretch();
|
||||||
|
|
||||||
|
auto *closeBtn = new QPushButton("Закрыть");
|
||||||
|
connect(closeBtn, &QPushButton::clicked, this, &QDialog::close);
|
||||||
|
btnLayout->addWidget(closeBtn);
|
||||||
|
|
||||||
|
layout->addLayout(btnLayout);
|
||||||
|
}
|
||||||
11
views/log/LogDetailWindow.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include "dao/GeneralLogDAO.h"
|
||||||
|
|
||||||
|
class LogDetailWindow final : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LogDetailWindow(const GeneralLogEntry &entry, QWidget *parent = nullptr);
|
||||||
|
};
|
||||||