412 lines
17 KiB
C++
412 lines
17 KiB
C++
#include "EventWindow.h"
|
||
|
||
#include <QDialog>
|
||
#include <QDir>
|
||
#include <QFile>
|
||
#include <QHBoxLayout>
|
||
#include <QHeaderView>
|
||
#include <QHttpMultiPart>
|
||
#include <QJsonDocument>
|
||
#include <QJsonObject>
|
||
#include <QMessageBox>
|
||
#include <QNetworkAccessManager>
|
||
#include <QNetworkReply>
|
||
#include <QPlainTextEdit>
|
||
#include <QProcess>
|
||
#include <QThread>
|
||
#include <QVBoxLayout>
|
||
|
||
#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<EventInfo> events = EventDAO::getEvents(m_page, m_pageSize);
|
||
m_eventIds.clear();
|
||
|
||
m_table->setRowCount(static_cast<int>(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<EventInfo> 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, "Готово", "События отправлены.");
|
||
});
|
||
}
|