diff --git a/CMakeLists.txt b/CMakeLists.txt index 357f34a..7c34230 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ if (WIN32) elseif (APPLE) # Укажи путь к Qt6 set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/qt") + include_directories("/opt/homebrew/include") link_directories("/opt/homebrew/lib") endif () @@ -135,6 +136,8 @@ elseif (APPLE) Qt6::Sql Qt6::Xml Qt6::Network + tesseract + leptonica ) endif () diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index d41dcd2..e653038 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -61,7 +61,8 @@ void GetCardInfoScript::doStart() { m_error = "Cannot reach home screen: " + m_deviceId; qWarning() << m_error; AppLogger::log("ozon/GetCardInfo", m_deviceId, m_error, - AdbUtils::captureScreenshotBytes(m_deviceId)); + AdbUtils::captureScreenshotBytes(m_deviceId), + "FETCH_PROFILE"); AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp index e2737c4..4c5b36d 100644 --- a/android/ozon/GetLastTransactionsScript.cpp +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -1,9 +1,13 @@ #include "GetLastTransactionsScript.h" #include +#include #include #include +#include +#include + #include "adb/AdbUtils.h" #include "dao/AccountDAO.h" #include "dao/ApplicationDAO.h" @@ -12,6 +16,46 @@ #include "time/DateUtils.h" #include "AppLogger.h" +static QString recognizeTextFromImage(const QByteArray &pngData) { + auto *api = new tesseract::TessBaseAPI(); + if (api->Init(nullptr, "rus+eng")) { + qWarning() << "[OCR] Failed to init Tesseract"; + delete api; + return {}; + } + + Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), + static_cast(pngData.size())); + if (!pix) { + qWarning() << "[OCR] Failed to read image"; + api->End(); + delete api; + return {}; + } + + api->SetImage(pix); + char *text = api->GetUTF8Text(); + QString result = QString::fromUtf8(text).trimmed(); + delete[] text; + pixDestroy(&pix); + api->End(); + delete api; + + return result; +} + +static QString extractTransactionId(const QString &ocrText) { + // "ID операции A60130542076330K0000040011670301" + QRegularExpression reId(R"(ID\s+операции\s+([A-Za-z0-9]+))"); + if (auto m = reId.match(ocrText); m.hasMatch()) return m.captured(1); + + // "№ Ф-2026-25235663" + QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))"); + if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1); + + return {}; +} + namespace Ozon { GetLastTransactionsScript::GetLastTransactionsScript( @@ -55,7 +99,8 @@ void GetLastTransactionsScript::doStart() { m_error = "Cannot reach home screen: " + m_deviceId; qWarning() << m_error; AppLogger::log("ozon/GetLastTransactions", m_deviceId, m_error, - AdbUtils::captureScreenshotBytes(m_deviceId)); + AdbUtils::captureScreenshotBytes(m_deviceId), + "FETCH_TRANSACTIONS"); AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; @@ -259,12 +304,28 @@ void GetLastTransactionsScript::doStart() { screencapProc.waitForFinished(5000); const QByteArray screenshot = screencapProc.readAllStandardOutput(); if (!screenshot.isEmpty()) { - // Сохраняем в БД TransactionInfo saved = TransactionDAO::findByBankTimeAndAmount( accountId, tx.bankName, tx.bankTime, tx.amount); if (saved.id != -1) { TransactionDAO::updateReceiptImage(saved.id, screenshot); qDebug() << "[GetLastTransactions] Receipt saved to DB, txId:" << saved.id; + + // OCR: распознаём текст и достаём ID операции + const QString ocrText = recognizeTextFromImage(screenshot); + if (!ocrText.isEmpty()) { + qDebug() << "[GetLastTransactions] OCR result, txId:" << saved.id + << "\n--- OCR TEXT START ---\n" << ocrText + << "\n--- OCR TEXT END ---"; + const QString txId = extractTransactionId(ocrText); + if (!txId.isEmpty()) { + TransactionDAO::updateBankTrExternalId(saved.id, txId); + qDebug() << "[GetLastTransactions] Extracted transaction ID:" << txId; + } else { + qWarning() << "[GetLastTransactions] Could not extract transaction ID from OCR text"; + } + } else { + qWarning() << "[GetLastTransactions] OCR returned empty text"; + } } } else { qWarning() << "[GetLastTransactions] Screenshot is empty"; diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp index 583bab3..71a4589 100644 --- a/android/ozon/LoginAndCheckAccountsScript.cpp +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -40,7 +40,8 @@ void LoginAndCheckAccountsScript::doStart() { m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode; qWarning() << m_error; AppLogger::log("ozon/LoginAndCheck", m_deviceId, m_error, - AdbUtils::captureScreenshotBytes(m_deviceId)); + AdbUtils::captureScreenshotBytes(m_deviceId), + "FETCH_PROFILE"); if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) { qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode; } diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp index faef0c8..619c33b 100644 --- a/android/ozon/ScreenXmlParser.cpp +++ b/android/ozon/ScreenXmlParser.cpp @@ -439,10 +439,20 @@ QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { // 6. PIN-сетка — строки ровно с 3 узлами (3 столбца) // Строки 1-3: цифры 1-9 // Строка 4: (биометрия) [0] (backspace) → берём средний элемент + qDebug() << "[findPinCodeNodes] clickable:" << clickable.size() + << "minH:" << minH << "candidates:" << candidates.size() << "rows:" << rows.size(); + for (int i = 0; i < rows.size(); ++i) { + QStringList info; + for (const Node &n : rows[i]) + info << QString("(%1,%2 %3x%4 '%5')").arg(n.x()).arg(n.y()).arg(n.width()).arg(n.height()).arg(n.text.left(15)); + qDebug() << "[findPinCodeNodes] row" << i << "size:" << rows[i].size() << info.join(" | "); + } + QList> gridRows; for (const QList &row: rows) { if (row.size() == 3) gridRows.append(row); } + qDebug() << "[findPinCodeNodes] gridRows:" << gridRows.size() << "pinCode len:" << pinCode.size(); QMap digitMap; const QStringList digits19 = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}; diff --git a/config.ini b/config.ini index 162eedf..c4c5a2f 100644 --- a/config.ini +++ b/config.ini @@ -2,13 +2,13 @@ list=ozon [common] -access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNDI3Njk5fQ.OYKZQ6znq88jCyKhHVFjfowkpYTMHlDNP8shh4nGxJE +access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNzE0MTQyfQ.-074b8eEWeLgzoxxpq5o54G1npivuC_VWvu4NLEz_gY api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7 currencies=USD, KRW, AZN, RUB desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391 profile_id=c4552156-d179-4a79-9e91-587038c95cf0 -refresh_token=f9016b28-4dd5-429f-868e-8a8e0b246498 -token_expires_at=1772427699 +refresh_token=def3036b-b62d-4550-8971-94d7ef57eba2 +token_expires_at=1772714142 version=0.0.1 [db] diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index b0f4870..327ba4e 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -12,9 +12,17 @@ DatabaseManager::DatabaseManager() = default; DatabaseManager::~DatabaseManager() { QMutexLocker locker(&mutex); + // QSqlDatabase требует живого QCoreApplication — при завершении + // статический синглтон может разрушаться после QApplication + if (!QCoreApplication::instance()) { + connectionPool.clear(); + return; + } + for (auto it = connectionPool.begin(); it != connectionPool.end(); ++it) { QSqlDatabase::removeDatabase(it.key()); } + connectionPool.clear(); qDebug() << "Все соединения закрыты"; } @@ -320,6 +328,39 @@ void DatabaseManager::initSchema() { q.exec("PRAGMA user_version = 10"); qDebug() << "[DB] Migration 10 applied"; } + + // ── Версия 11: убираем UNIQUE(external_id) из events ───────────────────── + if (version < 11) { + db.transaction(); + + q.exec(R"(CREATE TABLE IF NOT EXISTS events_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER, + external_id TEXT, + device_id TEXT, + amount REAL, + phone TEXT, + bank_name TEXT, + comment TEXT, + status TEXT, + type TEXT, + update_time DATETIME, + complete_time DATETIME, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE + ))"); + + q.exec(R"(INSERT INTO events_new + SELECT * FROM events)"); + + q.exec("DROP TABLE events"); + q.exec("ALTER TABLE events_new RENAME TO events"); + + db.commit(); + q.exec("PRAGMA user_version = 11"); + qDebug() << "[DB] Migration 11 applied"; + } } void DatabaseManager::closeConnection() { diff --git a/database/app_data.db b/database/app_data.db index 2fcaa97..eb4db1c 100644 Binary files a/database/app_data.db and b/database/app_data.db differ diff --git a/database/dao/AccountDAO.cpp b/database/dao/AccountDAO.cpp index 1e0303e..e5a3f6a 100644 --- a/database/dao/AccountDAO.cpp +++ b/database/dao/AccountDAO.cpp @@ -217,6 +217,28 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a return {}; } +QStringList AccountDAO::getActiveMaterialIds() { + QStringList ids; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT a.material_id + FROM accounts a + JOIN applications app ON a.device_id = app.device_id AND a.app_code = app.code + WHERE app.status = 'active' + AND a.material_id IS NOT NULL AND a.material_id != '' + )"); + + if (!query.exec()) { + qWarning() << "Ошибка при получении active material_ids:" << query.lastError().text(); + return ids; + } + + while (query.next()) { + ids << query.value(0).toString(); + } + return ids; +} + AccountInfo AccountDAO::findByMaterialId(const QString &materialId) { QSqlQuery query(DatabaseManager::instance().database()); diff --git a/database/dao/AccountDAO.h b/database/dao/AccountDAO.h index 70b1d03..30d05ad 100644 --- a/database/dao/AccountDAO.h +++ b/database/dao/AccountDAO.h @@ -31,4 +31,6 @@ public: ); [[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId); + + [[nodiscard]] static QStringList getActiveMaterialIds(); }; diff --git a/database/dao/EventDAO.cpp b/database/dao/EventDAO.cpp index e046c15..379a9d9 100644 --- a/database/dao/EventDAO.cpp +++ b/database/dao/EventDAO.cpp @@ -17,6 +17,7 @@ EventInfo parseEvent(const QSqlQuery &query) { event.amount = query.value("amount").toDouble(); event.phone = query.value("phone").toString(); event.bankName = query.value("bank_name").toString(); + event.comment = query.value("comment").toString(); event.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString()); event.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); event.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString()); @@ -101,6 +102,18 @@ bool EventDAO::updateEvent( return true; } +bool EventDAO::updateEventAmount(int id, double amount) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE events SET amount = :amount WHERE id = :id"); + query.bindValue(":amount", amount); + query.bindValue(":id", id); + if (!query.exec()) { + qCritical() << "Ошибка при обновлении amount event:" << query.lastError().text(); + return false; + } + return true; +} + QList EventDAO::getAllEvents(const EventStatus &status) { QList events; @@ -127,7 +140,8 @@ QList EventDAO::getAllEvents(const EventStatus &status) { EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, account_id, device_id, external_id, amount, phone, bank_name, status, update_time, complete_time, timestamp, type + SELECT id, account_id, device_id, external_id, amount, phone, bank_name, + status, type, comment, update_time, complete_time, timestamp FROM events WHERE status = :status AND device_id = :device_id ORDER BY timestamp ASC @@ -147,6 +161,37 @@ EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) { return {}; } +QList EventDAO::getEvents(int page, int pageSize) { + QList events; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, account_id, device_id, external_id, amount, phone, bank_name, + status, type, comment, update_time, complete_time, timestamp + FROM events + ORDER BY id DESC + LIMIT :limit OFFSET :offset + )"); + query.bindValue(":limit", pageSize); + query.bindValue(":offset", page * pageSize); + + if (!query.exec()) { + qWarning() << "Ошибка при получении getEvents:" << query.lastError().text(); + return events; + } + + while (query.next()) { + events.append(parseEvent(query)); + } + return events; +} + +int EventDAO::getTotalCount() { + QSqlQuery query(DatabaseManager::instance().database()); + query.exec("SELECT COUNT(*) FROM events"); + if (query.next()) return query.value(0).toInt(); + return 0; +} + bool EventDAO::existsActiveByExternalId(const QString &externalId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( diff --git a/database/dao/EventDAO.h b/database/dao/EventDAO.h index d104819..c3fe337 100644 --- a/database/dao/EventDAO.h +++ b/database/dao/EventDAO.h @@ -17,4 +17,10 @@ public: [[nodiscard]] static EventInfo getFirstWaitEventByDeviceId(const QString &key); [[nodiscard]] static bool existsActiveByExternalId(const QString &externalId); + + [[nodiscard]] static bool updateEventAmount(int id, double amount); + + [[nodiscard]] static QList getEvents(int page, int pageSize); + + [[nodiscard]] static int getTotalCount(); }; diff --git a/database/dao/TransactionDAO.cpp b/database/dao/TransactionDAO.cpp index d73898d..79a163f 100644 --- a/database/dao/TransactionDAO.cpp +++ b/database/dao/TransactionDAO.cpp @@ -286,6 +286,29 @@ QList TransactionDAO::getTransactionsWithinHours(const int acco return list; } +QList TransactionDAO::getLastTransactions(const int accountId, const int limit) { + QList list; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT * FROM transactions + WHERE account_id = :accountId + ORDER BY timestamp DESC + LIMIT :limit + )"); + query.bindValue(":accountId", accountId); + query.bindValue(":limit", limit); + + if (!query.exec()) { + qWarning() << "Ошибка получения последних транзакций: " << query.lastError().text(); + return {}; + } + + while (query.next()) { + list.append(parseTransaction(query)); + } + return list; +} + bool TransactionDAO::updateTransaction( const int id, const TransactionStatus status, @@ -348,6 +371,18 @@ bool TransactionDAO::updateExternalTransactionId(const int id, const int externa return true; } +bool TransactionDAO::updateBankTrExternalId(const int id, const QString &bankTrExternalId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE transactions SET bank_tr_external_id = :val WHERE id = :id"); + query.bindValue(":id", id); + query.bindValue(":val", bankTrExternalId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении bank_tr_external_id:" << query.lastError().text(); + return false; + } + return true; +} + bool TransactionDAO::updateReceiptImage(const int id, const QByteArray &image) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare("UPDATE transactions SET receipt_image = :image WHERE id = :id"); diff --git a/database/dao/TransactionDAO.h b/database/dao/TransactionDAO.h index 18cacfb..b3a5adf 100644 --- a/database/dao/TransactionDAO.h +++ b/database/dao/TransactionDAO.h @@ -19,6 +19,8 @@ public: [[nodiscard]] static QList getTransactionsWithinHours(int accountId, int hours); + [[nodiscard]] static QList getLastTransactions(int accountId, int limit); + [[nodiscard]] static bool updateTransaction( int id, TransactionStatus status, const QString &oldDesc, @@ -27,6 +29,8 @@ public: [[nodiscard]] static bool updateExternalTransactionId(int id, int externalId); + [[nodiscard]] static bool updateBankTrExternalId(int id, const QString &bankTrExternalId); + [[nodiscard]] static bool updateReceiptImage(int id, const QByteArray &image); // Найти транзакцию по bank_name + bank_time + amount diff --git a/main.cpp b/main.cpp index 630959d..51c6f00 100644 --- a/main.cpp +++ b/main.cpp @@ -16,6 +16,7 @@ #include #include +#include "AppLogger.h" #include "EventHandler.h" #include "net/NetworkService.h" #include "ozon/PayByPhoneScript.h" @@ -100,6 +101,7 @@ void startPayByPhoneTest() { void startNetworkService(const QCoreApplication &app) { auto *paymentThread = new QThread; auto *networkService = new NetworkService; + AppLogger::setNetworkService(networkService); networkService->moveToThread(paymentThread); QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start); QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit); @@ -209,7 +211,7 @@ int main(int argc, char *argv[]) { startNetworkService(app); startDeviceScreener(app); startEventHandler(app); - startPayByPhoneTest(); + // startPayByPhoneTest(); }, Qt::QueuedConnection); }; @@ -237,7 +239,7 @@ int main(int argc, char *argv[]) { startNetworkService(app); startDeviceScreener(app); startEventHandler(app); - startPayByPhoneTest(); + // startPayByPhoneTest(); } else if (r == 0) { QSettings s("config.ini", QSettings::IniFormat); s.remove("common/api_key"); @@ -258,7 +260,7 @@ int main(int argc, char *argv[]) { startNetworkService(app); startDeviceScreener(app); startEventHandler(app); - startPayByPhoneTest(); + // startPayByPhoneTest(); } else if (result == 0) { QSettings s("config.ini", QSettings::IniFormat); s.remove("common/api_key"); diff --git a/services/AppLogger.cpp b/services/AppLogger.cpp index 1da7e73..286aa2f 100644 --- a/services/AppLogger.cpp +++ b/services/AppLogger.cpp @@ -1,12 +1,21 @@ #include "AppLogger.h" #include +#include #include "dao/AppLogDAO.h" #include "db/AppLogEntry.h" +#include "net/NetworkService.h" + +NetworkService *AppLogger::s_networkService = nullptr; + +void AppLogger::setNetworkService(NetworkService *service) { + s_networkService = service; +} void AppLogger::log(const QString &source, const QString &deviceId, - const QString &message, const QByteArray &screenshot) { + const QString &message, const QByteArray &screenshot, + const QString &event) { AppLogEntry entry; entry.source = source; entry.deviceId = deviceId; @@ -14,4 +23,13 @@ void AppLogger::log(const QString &source, const QString &deviceId, entry.screenshot = screenshot; entry.timestamp = QDateTime::currentDateTimeUtc(); AppLogDAO::insert(entry); + + if (s_networkService) { + QMetaObject::invokeMethod(s_networkService, "postMonitoringLog", + Qt::QueuedConnection, + Q_ARG(QString, QStringLiteral("ERROR")), + Q_ARG(QString, event.isEmpty() ? source : event), + Q_ARG(QString, message), + Q_ARG(QByteArray, screenshot)); + } } diff --git a/services/AppLogger.h b/services/AppLogger.h index 23dd387..7b7ecf5 100644 --- a/services/AppLogger.h +++ b/services/AppLogger.h @@ -2,12 +2,20 @@ #include #include +class NetworkService; + class AppLogger { public: + static void setNetworkService(NetworkService *service); + static void log( const QString &source, const QString &deviceId, const QString &message, - const QByteArray &screenshot = {} + const QByteArray &screenshot = {}, + const QString &event = {} ); + +private: + static NetworkService *s_networkService; }; diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index d349282..d341280 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -219,9 +219,26 @@ void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) { apiPatch("/api/v1/device/" + device.apiId, body); } +void DeviceScreener::toggleBankProfilesOnApi(const QString &deviceId, bool enable) { + const QList apps = ApplicationDAO::getApplicationsByDeviceId(deviceId); + for (const ApplicationInfo &app : apps) { + if (app.bankProfileId.isEmpty()) continue; + + // При включении — только если приложение активно в системе + if (enable && app.status != "active") continue; + + const QString enableStr = enable ? "true" : "false"; + const QString path = "/api/v1/profile/bank_profile/" + app.bankProfileId + "/" + enableStr; + qDebug() << "[DeviceScreener] toggleBankProfile:" << app.code << app.bankProfileId << "enable=" << enable; + apiPatch(path, QJsonObject()); + } +} + void DeviceScreener::start() { - QMap bankChecked; while (m_running) { + // Устройства, которые были онлайн в БД до текущего скана + const QStringList prevOnline = DeviceDAO::getOnlineDeviceCodes(); + QList > devices = readAdbDevices(); QStringList onlineIds; QList deviceInfos; @@ -231,7 +248,7 @@ void DeviceScreener::start() { if (status == "device") { onlineIds << device.id; - const bool isChecked = bankChecked.contains(id); + const bool isChecked = prevOnline.contains(id); device = readDeviceInfo(id, isChecked); device.image = takeScreenshot(id); if (!device.image.isEmpty()) { @@ -240,9 +257,9 @@ void DeviceScreener::start() { } } if (!isChecked) { - ApplicationDAO::activateAppsForDevice(id); + // Новое устройство — включаем на сервере только active профили + toggleBankProfilesOnApi(id, true); } - bankChecked[id] = true; } device.status = status; device.updateTime = QDateTime::currentDateTime(); @@ -276,22 +293,14 @@ void DeviceScreener::start() { } } - // if (!deviceInfos.empty()) { - // postDevicesData(deviceInfos); - // } - if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) { qDebug() << "Cant mark devices offline"; } - // Деактивировать приложения для устройств, которые ушли в офлайн - QStringList offlineIds; - for (auto it = bankChecked.begin(); it != bankChecked.end();) { - if (!onlineIds.contains(it.key())) { - ApplicationDAO::deactivateAppsForDevice(it.key()); - it = bankChecked.erase(it); - } else { - ++it; + // Было онлайн в БД, сейчас офлайн — выключаем профили на сервере + for (const QString &id : prevOnline) { + if (!onlineIds.contains(id)) { + toggleBankProfilesOnApi(id, false); } } diff --git a/services/DeviceScreener.h b/services/DeviceScreener.h index f1c760e..12c1524 100644 --- a/services/DeviceScreener.h +++ b/services/DeviceScreener.h @@ -42,4 +42,5 @@ private: QString createDeviceOnApi(const DeviceInfo &device); void updateDeviceOnApi(const DeviceInfo &device); void postScreenshotToApi(const QString &apiId, const QByteArray &image); + void toggleBankProfilesOnApi(const QString &deviceId, bool enable); }; diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index 89e1a51..e3b83e7 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -11,6 +11,7 @@ #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" #include "dao/EventDAO.h" +#include "dao/TransactionDAO.h" #include "db/ApplicationInfo.h" #include "net/NetworkService.h" #include "birbank/GetLastDaysHistoryBirbankScript.h" @@ -24,13 +25,14 @@ #include "ziraat/GetLastDaysHistoryZiraatScript.h" #include "ziraat/LoginAndCheckAccountsZiraatScript.h" -static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonObject &data); +static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data); EventHandler::EventHandler(QObject *parent) : QObject(parent) {} EventHandler::~EventHandler() = default; void EventHandler::start() { + qDebug() << "[EventHandler] start() called, thread:" << QThread::currentThread(); m_network = new NetworkService(this); connect(m_network, &NetworkService::tasksReceived, this, &EventHandler::onTasksReceived); @@ -40,11 +42,17 @@ void EventHandler::start() { m_pollTimer->start(); // Первый запрос сразу при старте + qDebug() << "[EventHandler] starting first getTasks()"; m_network->getTasks(); } void EventHandler::onTasksReceived(const QJsonArray &tasks) { if (!m_running) return; + if (!tasks.isEmpty()) + qDebug() << "[EventHandler] onTasksReceived, count:" << tasks.size(); + + // Получаем список онлайн устройств один раз + const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes(); // 1. Сохраняем новые задачи в БД for (const auto &taskVal : tasks) { @@ -62,9 +70,6 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) { continue; } - // Пропускаем если уже есть активная запись с таким external_id - if (EventDAO::existsActiveByExternalId(materialId)) continue; - // Ищем аккаунт по material_id напрямую const AccountInfo account = AccountDAO::findByMaterialId(materialId); if (account.id == -1) { @@ -72,7 +77,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) { QJsonObject data; data["status"] = "error"; data["description"] = "Account not found for material_id: " + materialId; - sendTaskResult(typeStr, materialId, bankName, data); + sendTaskResult(typeStr, materialId, bankName, QJsonValue(data)); continue; } @@ -85,19 +90,48 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) { event.bankName = bankName; event.timestamp = QDateTime::currentDateTimeUtc(); - if (EventDAO::insertEvent(event) == -1) { + const QJsonObject body = task["body"].toObject(); + if (eventType == EventType::CreateTransaction) { + event.amount = body["amount"].toDouble(); + event.phone = body["bank_participator_id"].toString(); + } + + const int eventId = EventDAO::insertEvent(event); + if (eventId == -1) { qCritical() << "[EventHandler] Failed to insert event, external_id:" << materialId; + continue; + } + + // Если устройство офлайн — сразу отправляем ошибку на сервер + if (!onlineDevices.contains(account.deviceId)) { + qWarning() << "[EventHandler] Device offline for material_id:" << materialId << "device:" << account.deviceId; + EventDAO::updateEvent(eventId, EventStatus::Error, "Device is offline"); + QJsonObject errData; + errData["status"] = "error"; + errData["description"] = "Device is offline"; + sendTaskResult(typeStr, materialId, bankName, QJsonValue(errData)); } } // 2. Для каждого онлайн устройства без активного потока — запускаем следующую задачу - const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes(); for (const QString &deviceId : onlineDevices) { if (const QThread *thr = m_threads.value(deviceId, nullptr); thr && thr->isRunning()) continue; const EventInfo event = EventDAO::getFirstWaitEventByDeviceId(deviceId); if (event.id == -1) continue; + // Если задача ждёт больше 5 минут — отклоняем + const qint64 waitSecs = event.timestamp.secsTo(QDateTime::currentDateTimeUtc()); + if (waitSecs > 5 * 60) { + qWarning() << "[EventHandler] Task expired after" << waitSecs << "s, event id:" << event.id; + EventDAO::updateEvent(event.id, EventStatus::Error, "Task expired: waited " + QString::number(waitSecs) + "s"); + QJsonObject errData; + errData["status"] = "error"; + errData["description"] = "Task expired: not started within 5 minutes"; + sendTaskResult(eventTypeToString(event.type), event.externalId, event.bankName, QJsonValue(errData)); + continue; + } + const AccountInfo account = AccountDAO::getAccountById(event.accountId); const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, account.appCode); if (app.status != "active") continue; @@ -112,7 +146,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) { } static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, - const QJsonObject &data) { + const QJsonValue &data) { QJsonObject obj; obj["type"] = type; obj["material_id"] = materialId; // UUID string @@ -156,12 +190,57 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event, if (!EventDAO::updateEvent(event.id, newStatus, result)) { qCritical() << "[EventHandler] Failed to update event:" << event.id; } else { - sendEventStatus( - event.deviceId, + QJsonValue taskData; + if (newStatus == EventStatus::Error) { + QJsonObject errObj; + errObj["status"] = "error"; + errObj["description"] = result; + taskData = errObj; + } else if (event.type == EventType::FetchProfile) { + const AccountInfo account = AccountDAO::getAccountById(event.accountId); + EventDAO::updateEventAmount(event.id, account.amount); + QJsonObject obj; + obj["bank_account_id"] = QString(); + obj["amount"] = static_cast(account.amount); + obj["currency"] = 643; // RUB + taskData = obj; + } else if (event.type == EventType::FetchTransactions) { + QList txList = TransactionDAO::getTransactionsWithinHours(event.accountId, 24); + if (txList.isEmpty()) { + txList = TransactionDAO::getLastTransactions(event.accountId, 10); + } + QJsonArray arr; + for (const auto &tx : txList) { + QJsonObject txObj; + txObj["bank_transaction_id"] = tx.bankTrExternalId.isEmpty() ? QJsonValue(QJsonValue::Null) : QJsonValue(tx.bankTrExternalId); + txObj["amount"] = static_cast(tx.amount); + txObj["currency"] = 643; // RUB + txObj["transaction_type"] = (tx.type == TransactionType::Phone) ? "top-up" : "bank"; + txObj["bank_participator_id"]= tx.phone; + txObj["status"] = tx.status == TransactionStatus::Complete ? "completed" : (tx.status == TransactionStatus::Wait ? "progress" : "completed"); + txObj["extra"] = QJsonObject(); + if (tx.timestamp.isValid()) { + txObj["created_at"] = tx.timestamp.toString(Qt::ISODate); + } else if (!tx.bankTime.isEmpty()) { + QDateTime dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy, HH:mm"); + if (!dt.isValid()) dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy HH:mm"); + txObj["created_at"] = dt.isValid() ? dt.toString(Qt::ISODate) : QJsonValue(QJsonValue::Null); + } else { + txObj["created_at"] = QJsonValue(QJsonValue::Null); + } + arr.append(txObj); + } + taskData = arr; + } else { + QJsonObject obj; + obj["status"] = "completed"; + taskData = obj; + } + sendTaskResult( + eventTypeToString(event.type), event.externalId, - event.id, - eventStatusToString(newStatus), - result + event.bankName, + taskData ); } diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index efa30c1..9bc7448 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -35,6 +35,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) { m_pathMaterial = settings.value("net/pathMaterial", "/api/v1/profile/material").toString(); m_pathGetTasks = settings.value("net/pathGetTasks", "/api/v1/task/get_tasks").toString(); m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString(); + m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString(); m_manager = new QNetworkAccessManager(this); m_token = settings.value("common/token").toString(); m_accessToken = settings.value("common/access_token").toString(); @@ -412,6 +413,26 @@ void NetworkService::patchBankProfile() { emit finished(); } +void NetworkService::toggleBankProfile() { + if (!ensureValidToken()) { emit finished(); return; } + + const QString bankProfileId = m_extraData.value("bank_profile_id").toString(); + const bool enable = m_extraData.value("enable").toBool(); + + if (bankProfileId.isEmpty()) { + qWarning() << "[NetworkService] toggleBankProfile: bank_profile_id is empty"; + emit finished(); + return; + } + + const QString enableStr = enable ? "true" : "false"; + const QString path = m_apiBase + m_pathBankProfile + "/" + bankProfileId + "/" + enableStr; + patchJson(path, QJsonObject(), true); + qDebug() << "[NetworkService] toggleBankProfile done:" << bankProfileId << "enable=" << enable; + + emit finished(); +} + void NetworkService::postMaterial() { if (!ensureValidToken()) { emit finished(); return; } @@ -594,10 +615,21 @@ void NetworkService::getTasks() { if (m_fetchingTasks) return; m_fetchingTasks = true; + if (!ensureValidToken()) { + m_fetchingTasks = false; + return; + } + const QSettings settings("config.ini", QSettings::IniFormat); const QString accessToken = settings.value("common/access_token").toString(); QUrl url(m_apiBase + m_pathGetTasks); + QUrlQuery urlQuery; + const QStringList materialIds = AccountDAO::getActiveMaterialIds(); + for (const QString &mid : materialIds) { + urlQuery.addQueryItem("materials_id", mid); + } + url.setQuery(urlQuery); QNetworkRequest request(url); request.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8()); @@ -607,7 +639,8 @@ void NetworkService::getTasks() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { - qWarning() << "[NetworkService] getTasks error:" << reply->errorString(); + qWarning() << "[NetworkService] getTasks error:" << reply->errorString() + << "status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); return; } @@ -638,6 +671,37 @@ void NetworkService::postTaskResult() { emit finished(); } +void NetworkService::postMonitoringLog(const QString &type, const QString &event, + const QString &message, const QByteArray &image) { + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + addFormField(multiPart, "type", type); + addFormField(multiPart, "event", event); + addFormField(multiPart, "message", message); + + QHttpPart imagePart; + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png"); + imagePart.setHeader( + QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"image\"; filename=\"screenshot.png\"") + ); + imagePart.setBody(image.isEmpty() ? QByteArray(1, '\0') : image); + multiPart->append(imagePart); + + const QUrl url(m_apiBase + m_pathMonitoringLog); + QNetworkRequest request(url); + + QNetworkReply *reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + + connect(reply, &QNetworkReply::finished, reply, [reply]() { + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString(); + } + reply->deleteLater(); + }); +} + void NetworkService::start() { m_running = true; diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h index 59f6a90..26a732a 100644 --- a/services/net/NetworkService.h +++ b/services/net/NetworkService.h @@ -28,6 +28,8 @@ public slots: void patchBankProfile(); + void toggleBankProfile(); + void postMaterial(); void postDeviceScreenshot(); @@ -52,6 +54,9 @@ public slots: void postTaskResult(); + void postMonitoringLog(const QString &type, const QString &event, + const QString &message, const QByteArray &image); + void start(); void stop(); @@ -86,6 +91,7 @@ private: QString m_pathMaterial; QString m_pathGetTasks; QString m_pathTaskResult; + QString m_pathMonitoringLog; bool m_fetchingTasks = false; QNetworkAccessManager *m_manager; diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp index 4906b23..0917433 100644 --- a/views/MainWindow.cpp +++ b/views/MainWindow.cpp @@ -8,6 +8,7 @@ #include "TutorialWindow.h" #include "bank/BankListWindow.h" +#include "event/EventWindow.h" #include "log/LogWindow.h" #include "db/DeviceInfo.h" #include "device/DeviceWidget.h" @@ -39,10 +40,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // auto *accountPageAction = new QAction("Аккаунты", this); auto *bankListAction = new QAction("Банки", this); auto *tutorialPageAction = new QAction("Инструкция", this); + auto *eventAction = new QAction("События", this); auto *logAction = new QAction("Логи", this); menu->addAction(devicesPageAction); // menu->addAction(accountPageAction); menu->addAction(bankListAction); + menu->addAction(eventAction); menu->addAction(tutorialPageAction); menu->addAction(logAction); @@ -76,6 +79,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_tutorialWindow->activateWindow(); }); + connect(eventAction, &QAction::triggered, this, [=]() { + if (!m_eventWindow) { + m_eventWindow = new EventWindow(this); + } + m_eventWindow->show(); + m_eventWindow->raise(); + m_eventWindow->activateWindow(); + }); + connect(logAction, &QAction::triggered, this, [=]() { if (!m_logWindow) { m_logWindow = new LogWindow(this); diff --git a/views/MainWindow.h b/views/MainWindow.h index 1b20565..777544c 100644 --- a/views/MainWindow.h +++ b/views/MainWindow.h @@ -28,6 +28,7 @@ private: QWidget *m_tutorialWindow = nullptr; QWidget *m_bankListWindow = nullptr; QWidget *m_logWindow = nullptr; + QWidget *m_eventWindow = nullptr; void createDevicePage(); diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 3711128..aa1c941 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -359,8 +359,23 @@ void AccountSettingsWindow::startGetLastTransactions(const QString &appCode) { } void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) { - Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status) - // TODO: заменить на новый API + const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code); + if (app.bankProfileId.isEmpty()) { + qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << code; + return; + } + + const bool enable = (status == "active"); + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("bank_profile_id", app.bankProfileId); + net->addExtra("enable", enable); + connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); + connect(net, &NetworkService::finished, thread, &QThread::quit); + connect(net, &NetworkService::finished, net, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); } void AccountSettingsWindow::updateAccountData(const AccountInfo &account) { diff --git a/views/bank/BankSettingsWindow.cpp b/views/bank/BankSettingsWindow.cpp index 87aad3b..4b1eb90 100644 --- a/views/bank/BankSettingsWindow.cpp +++ b/views/bank/BankSettingsWindow.cpp @@ -17,6 +17,7 @@ #include "dao/ApplicationDAO.h" #include "db/ApplicationInfo.h" +#include "net/NetworkService.h" #include "common/PaddedItemDelegate.h" #include "device/EditBankDialog.h" #include "birbank/LoginAndCheckAccountsBirbankScript.h" @@ -95,8 +96,23 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) { } void sendAppStatus(const QString &deviceId, const QString &code, const QString &status) { - Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status) - // TODO: заменить на новый API + const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code); + if (app.bankProfileId.isEmpty()) { + qWarning() << "[BankSettings] sendAppStatus: no bankProfileId for" << code; + return; + } + + const bool enable = (status == "active"); + 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 BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { diff --git a/views/event/EventWindow.cpp b/views/event/EventWindow.cpp new file mode 100644 index 0000000..0eb7636 --- /dev/null +++ b/views/event/EventWindow.cpp @@ -0,0 +1,181 @@ +#include "EventWindow.h" + +#include +#include +#include +#include +#include +#include + +#include "dao/EventDAO.h" +#include "net/NetworkService.h" + +static constexpr int COL_ID = 0; +static constexpr int COL_TIME = 1; +static constexpr int COL_TYPE = 2; +static constexpr int COL_BANK = 3; +static constexpr int COL_DEVICE = 4; +static constexpr int COL_STATUS = 5; +static constexpr int COL_AMOUNT = 6; +static constexpr int COL_COMMENT = 7; +static constexpr int COL_ACTION = 8; + +static void sendErrorToServer(const QString &type, const QString &materialId, const QString &bankName) { + QJsonObject errData; + errData["status"] = "error"; + errData["description"] = "Closed manually"; + + QJsonObject obj; + obj["type"] = type; + obj["material_id"] = materialId; + obj["bank_name"] = bankName; + obj["data"] = errData; + + auto *thread = new QThread; + auto *networkService = new NetworkService; + networkService->moveToThread(thread); + networkService->setPayload(QJsonDocument(obj).toJson()); + QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult); + QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +static QString statusDisplayName(EventStatus s) { + switch (s) { + case EventStatus::Wait: return "Ожидание"; + case EventStatus::InProgress: return "В работе"; + case EventStatus::Complete: return "Завершено"; + case EventStatus::Error: return "Ошибка"; + default: return "—"; + } +} + +static QString typeDisplayName(EventType t) { + switch (t) { + case EventType::FetchProfile: return "Профиль"; + case EventType::FetchTransactions: return "Транзакции"; + case EventType::CreateTransaction: return "Платёж"; + default: return "—"; + } +} + +EventWindow::EventWindow(QWidget *parent) : QDialog(parent) { + setWindowTitle("События"); + resize(1100, 600); + + // ── Таблица ─────────────────────────────────────────────────────────────── + m_table = new QTableWidget(this); + m_table->setColumnCount(9); + m_table->setHorizontalHeaderLabels({ + "#", "Время", "Тип", "Банк", "Устройство", "Статус", "Сумма", "Комментарий", "" + }); + m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table->setSelectionMode(QAbstractItemView::SingleSelection); + m_table->verticalHeader()->hide(); + m_table->horizontalHeader()->setStretchLastSection(true); + m_table->setColumnWidth(COL_ID, 45); + m_table->setColumnWidth(COL_TIME, 145); + m_table->setColumnWidth(COL_TYPE, 100); + m_table->setColumnWidth(COL_BANK, 90); + m_table->setColumnWidth(COL_DEVICE, 120); + m_table->setColumnWidth(COL_STATUS, 100); + m_table->setColumnWidth(COL_AMOUNT, 80); + m_table->setColumnWidth(COL_ACTION, 80); + + // ── Пагинация ───────────────────────────────────────────────────────────── + m_prevBtn = new QPushButton("←", this); + m_nextBtn = new QPushButton("→", this); + m_pageLabel = new QLabel(this); + m_pageLabel->setAlignment(Qt::AlignCenter); + m_pageLabel->setMinimumWidth(140); + + auto *refreshBtn = new QPushButton("Обновить", this); + + auto *paginationLayout = new QHBoxLayout; + paginationLayout->addWidget(m_prevBtn); + paginationLayout->addWidget(m_pageLabel); + paginationLayout->addWidget(m_nextBtn); + paginationLayout->addStretch(); + paginationLayout->addWidget(refreshBtn); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->addWidget(m_table, 1); + mainLayout->addLayout(paginationLayout); + + // ── Сигналы ─────────────────────────────────────────────────────────────── + connect(m_prevBtn, &QPushButton::clicked, this, [this]() { + if (m_page > 0) { --m_page; loadPage(); } + }); + connect(m_nextBtn, &QPushButton::clicked, this, [this]() { + if (m_page < m_totalPages - 1) { ++m_page; loadPage(); } + }); + connect(refreshBtn, &QPushButton::clicked, this, [this]() { + m_page = 0; + loadPage(); + }); + + loadPage(); +} + +void EventWindow::loadPage() { + const int total = EventDAO::getTotalCount(); + m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize); + m_page = qBound(0, m_page, m_totalPages - 1); + + const QList events = EventDAO::getEvents(m_page, m_pageSize); + + m_table->setRowCount(static_cast(events.size())); + for (int i = 0; i < events.size(); ++i) { + const auto &e = events[i]; + m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id))); + m_table->setItem(i, COL_TIME, new QTableWidgetItem( + e.timestamp.toLocalTime().toString("dd.MM.yy HH:mm:ss"))); + m_table->setItem(i, COL_TYPE, new QTableWidgetItem(typeDisplayName(e.type))); + m_table->setItem(i, COL_BANK, new QTableWidgetItem(e.bankName)); + m_table->setItem(i, COL_DEVICE, new QTableWidgetItem(e.deviceId)); + m_table->setItem(i, COL_STATUS, new QTableWidgetItem(statusDisplayName(e.status))); + m_table->setItem(i, COL_AMOUNT, new QTableWidgetItem( + e.amount > 0 ? QString::number(e.amount, 'f', 2) : "—")); + m_table->setItem(i, COL_COMMENT, new QTableWidgetItem(e.comment)); + + // Подсветка строки по статусу + QColor bg; + switch (e.status) { + case EventStatus::Error: bg = QColor(255, 230, 230); break; + case EventStatus::Complete: bg = QColor(230, 255, 230); break; + case EventStatus::InProgress: bg = QColor(230, 240, 255); break; + default: bg = QColor(255, 255, 230); break; + } + for (int col = 0; col < m_table->columnCount(); ++col) { + if (auto *item = m_table->item(i, col)) + item->setBackground(bg); + } + + // Кнопка "Закрыть" только для активных задач + if (e.status == EventStatus::Wait || e.status == EventStatus::InProgress) { + auto *closeBtn = new QPushButton("Закрыть"); + closeBtn->setStyleSheet("background: #e74c3c; color: white; border: none; padding: 3px 8px; border-radius: 3px;"); + const int eventId = e.id; + const QString externalId = e.externalId; + const QString bankName = e.bankName; + const QString typeStr = eventTypeToString(e.type); + connect(closeBtn, &QPushButton::clicked, this, [this, eventId, externalId, bankName, typeStr]() { + EventDAO::updateEvent(eventId, EventStatus::Error, "Closed manually"); + sendErrorToServer(typeStr, externalId, bankName); + loadPage(); + }); + m_table->setCellWidget(i, COL_ACTION, closeBtn); + } + } + + updatePagination(); +} + +void EventWindow::updatePagination() { + m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages)); + m_prevBtn->setEnabled(m_page > 0); + m_nextBtn->setEnabled(m_page < m_totalPages - 1); +} diff --git a/views/event/EventWindow.h b/views/event/EventWindow.h new file mode 100644 index 0000000..06663e3 --- /dev/null +++ b/views/event/EventWindow.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include +#include + +class EventWindow final : public QDialog { + Q_OBJECT + +public: + explicit EventWindow(QWidget *parent = nullptr); + +private: + QTableWidget *m_table; + QLabel *m_pageLabel; + QPushButton *m_prevBtn; + QPushButton *m_nextBtn; + + int m_page = 0; + int m_pageSize = 50; + int m_totalPages = 1; + + void loadPage(); + void updatePagination(); +}; diff --git a/views/log/LogWindow.cpp b/views/log/LogWindow.cpp index 62efc1d..72901d1 100644 --- a/views/log/LogWindow.cpp +++ b/views/log/LogWindow.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -127,16 +128,27 @@ void LogWindow::updatePagination() { void LogWindow::showScreenshot(int logId) { const QByteArray bytes = AppLogDAO::getScreenshot(logId); if (bytes.isEmpty()) { + m_originalPixmap = {}; m_screenshotLabel->setPixmap({}); m_screenshotLabel->setText("Скриншот отсутствует"); return; } - QPixmap px; - if (!px.loadFromData(bytes)) { + if (!m_originalPixmap.loadFromData(bytes)) { + m_originalPixmap = {}; m_screenshotLabel->setText("Не удалось загрузить скриншот"); return; } + rescaleScreenshot(); +} + +void LogWindow::rescaleScreenshot() { + if (m_originalPixmap.isNull()) return; m_screenshotLabel->setPixmap( - px.scaled(m_screenshotLabel->width(), 9999, Qt::KeepAspectRatio, Qt::SmoothTransformation) + m_originalPixmap.scaled(m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation) ); } + +void LogWindow::resizeEvent(QResizeEvent *event) { + QDialog::resizeEvent(event); + rescaleScreenshot(); +} diff --git a/views/log/LogWindow.h b/views/log/LogWindow.h index 23af751..d1a2837 100644 --- a/views/log/LogWindow.h +++ b/views/log/LogWindow.h @@ -21,7 +21,13 @@ private: int m_pageSize = 50; int m_totalPages = 1; + QPixmap m_originalPixmap; + void loadPage(); void updatePagination(); void showScreenshot(int logId); + void rescaleScreenshot(); + +protected: + void resizeEvent(QResizeEvent *event) override; };