1
0
forked from BRT/arc
arc/views/event/EventWindow.cpp

412 lines
17 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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, "Готово", "События отправлены.");
});
}