#include "EventWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dao/EventDAO.h" #include "dao/SettingsDAO.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_JSON = 8; static constexpr int COL_SENT = 9; static constexpr int COL_IMG = 10; static constexpr int COL_ACTION = 11; 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(1400, 700); // ── Таблица ─────────────────────────────────────────────────────────────── m_table = new QTableWidget(this); m_table->setColumnCount(12); m_table->setHorizontalHeaderLabels({ "#", "Время", "Тип", "Банк", "Устройство", "Статус", "Сумма", "Комментарий", "JSON", "Отпр.", "Скрин", "" }); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); m_table->setSelectionMode(QAbstractItemView::SingleSelection); m_table->verticalHeader()->hide(); m_table->horizontalHeader()->setStretchLastSection(false); 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->setColumnHidden(COL_JSON, true); m_table->setColumnHidden(COL_ACTION, true); m_table->setColumnWidth(COL_SENT, 45); m_table->setColumnWidth(COL_IMG, 45); m_table->horizontalHeader()->setSectionResizeMode(COL_COMMENT, QHeaderView::Stretch); // ── Панель скриншота ────────────────────────────────────────────────────── m_screenshotLabel = new QLabel(this); m_screenshotLabel->setAlignment(Qt::AlignCenter); m_screenshotLabel->setMinimumWidth(300); m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;"); m_screenshotLabel->setText("Выберите событие\nдля просмотра скриншота"); auto *splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(m_table); splitter->addWidget(m_screenshotLabel); splitter->setStretchFactor(0, 3); splitter->setStretchFactor(1, 1); // ── Пагинация ───────────────────────────────────────────────────────────── 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); m_sendBtn = new QPushButton("Отправить", this); m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;"); auto *paginationLayout = new QHBoxLayout; paginationLayout->addWidget(m_prevBtn); paginationLayout->addWidget(m_pageLabel); paginationLayout->addWidget(m_nextBtn); paginationLayout->addStretch(); paginationLayout->addWidget(refreshBtn); paginationLayout->addWidget(m_sendBtn); auto *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(splitter, 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(); }); connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) { if (row >= 0 && row < m_eventIds.size()) { showScreenshot(m_eventIds[row]); } }); connect(m_sendBtn, &QPushButton::clicked, this, &EventWindow::sendEventsToCsv); connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int col) { if (col != COL_JSON) return; auto *item = m_table->item(row, col); if (!item) return; const QString json = item->toolTip(); if (json.isEmpty()) return; auto *dlg = new QDialog(this); dlg->setWindowTitle("JSON"); dlg->resize(600, 400); auto *edit = new QPlainTextEdit(dlg); edit->setReadOnly(true); edit->setPlainText(QJsonDocument::fromJson(json.toUtf8()).toJson(QJsonDocument::Indented)); auto *layout = new QVBoxLayout(dlg); layout->addWidget(edit); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->show(); }); 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_eventIds.clear(); m_table->setRowCount(static_cast(events.size())); for (int i = 0; i < events.size(); ++i) { m_eventIds.append(events[i].id); 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)); auto *jsonItem = new QTableWidgetItem(e.json.left(80)); jsonItem->setToolTip(e.json); m_table->setItem(i, COL_JSON, jsonItem); m_table->setItem(i, COL_SENT, new QTableWidgetItem(e.sentToServer ? "+" : "")); m_table->setItem(i, COL_IMG, new QTableWidgetItem(e.hasScreenshot ? "+" : "")); // Подсветка строки по статусу 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::showScreenshot(int eventId) { const QByteArray data = EventDAO::getScreenshot(eventId); if (data.isEmpty()) { m_screenshotLabel->setText("Нет скриншота"); return; } QPixmap pixmap; pixmap.loadFromData(data); if (pixmap.isNull()) { m_screenshotLabel->setText("Ошибка загрузки"); return; } m_screenshotLabel->setPixmap(pixmap.scaled( m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } 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); } void EventWindow::sendEventsToCsv() { const QList events = EventDAO::getRecentEvents(48); if (events.isEmpty()) { QMessageBox::information(this, "События", "Нет событий за последние 48 часов."); return; } m_sendBtn->setEnabled(false); m_sendBtn->setText("Отправка..."); // Формируем CSV QString csv; csv += "id;time;type;bank;device;status;amount;phone;comment;sent\n"; for (const auto &e : events) { csv += QString("%1;%2;%3;%4;%5;%6;%7;%8;%9;%10\n") .arg(e.id) .arg(e.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss")) .arg(eventTypeToString(e.type)) .arg(e.bankName) .arg(e.deviceId) .arg(eventStatusToString(e.status)) .arg(e.amount, 0, 'f', 2) .arg(e.phone) .arg(QString(e.comment).replace(';', ',')) .arg(e.sentToServer ? "yes" : "no"); } // Собираем временную папку const QString tmpDir = QDir::tempPath() + "/arc_events_export"; QDir().mkpath(tmpDir); QDir().mkpath(tmpDir + "/screenshots"); // Сохраняем CSV { QFile csvFile(tmpDir + "/events.csv"); if (csvFile.open(QIODevice::WriteOnly | QIODevice::Text)) { csvFile.write(csv.toUtf8()); csvFile.close(); } } // Сохраняем скриншоты int screenshotCount = 0; for (const auto &e : events) { const QByteArray screenshot = EventDAO::getScreenshot(e.id); if (screenshot.isEmpty()) continue; const QString fileName = QString("screenshots/%1_%2_%3.png") .arg(e.id) .arg(eventTypeToString(e.type)) .arg(e.timestamp.toString("yyyyMMdd_HHmmss")); QFile imgFile(tmpDir + "/" + fileName); if (imgFile.open(QIODevice::WriteOnly)) { imgFile.write(screenshot); imgFile.close(); ++screenshotCount; } } // Создаём архив #ifdef Q_OS_WIN const QString archivePath = QDir::tempPath() + "/arc_events.zip"; QFile::remove(archivePath); QProcess archiver; archiver.setWorkingDirectory(tmpDir); archiver.start("powershell", QStringList() << "-NoProfile" << "-Command" << QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath)); archiver.waitForFinished(30000); #else const QString archivePath = QDir::tempPath() + "/arc_events.zip"; QFile::remove(archivePath); QProcess archiver; archiver.setWorkingDirectory(tmpDir); archiver.start("zip", QStringList() << "-r" << archivePath << "."); archiver.waitForFinished(30000); #endif QDir(tmpDir).removeRecursively(); if (!QFile::exists(archivePath)) { QMessageBox::warning(this, "Ошибка", "Не удалось создать архив"); m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); return; } // Отправляем в Telegram const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8"; const QString chatId = "-5177086220"; const QString desktopId = SettingsDAO::get("desktop_id"); const QString caption = "Events (48h) from desktop: " + desktopId + "\nTotal: " + QString::number(events.size()) + "\nScreenshots: " + QString::number(screenshotCount); auto *sendFile = new QFile(archivePath); if (!sendFile->open(QIODevice::ReadOnly)) { QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив"); delete sendFile; m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); return; } const QUrl url("https://api.telegram.org/bot" + botToken + "/sendDocument"); auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); QHttpPart chatPart; chatPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="chat_id")"); chatPart.setBody(chatId.toUtf8()); multiPart->append(chatPart); QHttpPart captionPart; captionPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="caption")"); captionPart.setBody(caption.toUtf8()); multiPart->append(captionPart); QHttpPart filePart; filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="document"; filename="arc_events.zip")"); filePart.setBodyDevice(sendFile); sendFile->setParent(multiPart); multiPart->append(filePart); auto *manager = new QNetworkAccessManager(this); QNetworkReply *reply = manager->post(QNetworkRequest(url), multiPart); multiPart->setParent(reply); connect(reply, &QNetworkReply::finished, this, [this, reply, manager, archivePath]() { reply->deleteLater(); manager->deleteLater(); QFile::remove(archivePath); m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); if (reply->error() != QNetworkReply::NoError) { QMessageBox::warning(this, "Ошибка", "Не удалось отправить: " + reply->errorString()); return; } QMessageBox::information(this, "Готово", "События отправлены."); }); }