#include "AccountSettingsWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #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/MaterialDAO.h" #include "net/NetworkService.h" #include "net/MaterialPayload.h" AccountSettingsWindow::AccountSettingsWindow( QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app ) : QWidget(parent, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint | Qt::WindowMinMaxButtonsHint), m_deviceId(std::move(deviceId)), m_deviceName(std::move(deviceName)), m_app(app) { setWindowTitle("Настройки банка [" + m_app.name + "]"); setMinimumSize(700, 400); resize(750, 500); auto *mainLayout = new QVBoxLayout(this); auto *scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); auto *content = new QWidget; auto *layout = new QVBoxLayout(content); layout->setContentsMargins(8, 8, 8, 8); layout->setAlignment(Qt::AlignTop); // ── Статус профиля ───────────────────────────────────────────────── auto *statusRow = new QHBoxLayout; statusRow->addWidget(new QLabel("Статус профиля:")); m_statusLabel = new QLabel; m_statusLabel->setTextFormat(Qt::RichText); statusRow->addWidget(m_statusLabel); statusRow->addStretch(); m_toggleBtn = new QPushButton; m_toggleBtn->setFixedWidth(120); connect(m_toggleBtn, &QPushButton::clicked, this, &AccountSettingsWindow::toggleProfileStatus); statusRow->addWidget(m_toggleBtn); layout->addLayout(statusRow); // ── Пин-код ──────────────────────────────────────────────────────── auto *pinRow = new QHBoxLayout; pinRow->addWidget(new QLabel("Пин-код:")); m_pinLabel = new QLabel; m_pinLabel->setTextFormat(Qt::RichText); pinRow->addWidget(m_pinLabel); pinRow->addStretch(); auto *editPinBtn = new QPushButton("Редактировать"); editPinBtn->setStyleSheet("background-color: green; color: white; padding: 4px 12px;"); connect(editPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::editPinCode); pinRow->addWidget(editPinBtn); auto *checkPinBtn = new QPushButton("Проверить пин-код"); checkPinBtn->setStyleSheet("background-color: orange; color: white; padding: 4px 12px;"); connect(checkPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startCheckPinCode); pinRow->addWidget(checkPinBtn); layout->addLayout(pinRow); // ── Профиль ──────────────────────────────────────────────────────── auto *profileRow = new QHBoxLayout; m_profileLabel = new QLabel; m_profileLabel->setTextFormat(Qt::RichText); m_profileLabel->setStyleSheet("margin: 4px 0;"); profileRow->addWidget(m_profileLabel); m_copyPhoneBtn = new QPushButton; m_copyPhoneBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); m_copyPhoneBtn->setIconSize(QSize(16, 16)); m_copyPhoneBtn->setToolTip("Копировать телефон"); m_copyPhoneBtn->setFixedSize(28, 24); m_copyPhoneBtn->setStyleSheet("padding: 2px;"); m_copyPhoneBtn->hide(); connect(m_copyPhoneBtn, &QPushButton::clicked, this, [this]() { if (m_app.phone.isEmpty()) return; QGuiApplication::clipboard()->setText(m_app.phone); QToolTip::showText(m_copyPhoneBtn->mapToGlobal(QPoint(0, m_copyPhoneBtn->height())), "Скопировано", m_copyPhoneBtn, {}, 1500); }); profileRow->addWidget(m_copyPhoneBtn); profileRow->addStretch(); layout->addLayout(profileRow); // ── Кнопки обновления ────────────────────────────────────────────── auto *actionsRow = new QHBoxLayout; m_profileBtn = new QPushButton("Обновить профиль"); m_profileBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;"); connect(m_profileBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetProfileInfo); actionsRow->addWidget(m_profileBtn); m_cardsBtn = new QPushButton("Проверить аккаунты"); m_cardsBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;"); connect(m_cardsBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetCardInfo); m_cardsBtn->hide(); actionsRow->addWidget(m_cardsBtn); actionsRow->addStretch(); layout->addLayout(actionsRow); // ── Таблица карт ─────────────────────────────────────────────────── layout->addSpacing(8); layout->addWidget(new QLabel("Счета")); m_cardsTable = new QTableWidget; m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection); m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); m_cardsTable->setFocusPolicy(Qt::NoFocus); layout->addWidget(m_cardsTable); content->setLayout(layout); scrollArea->setWidget(content); mainLayout->addWidget(scrollArea); // 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(); } // ── Overlay ──────────────────────────────────────────────────────────────── 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 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 ? "Активен" : "Неактивен"); 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 ? "" + pinDisplay + " (проверен)" : "" + pinDisplay + " (не проверен)"; m_pinLabel->setText(pinStatus); // Профиль if (m_app.fullName.isEmpty() && m_app.phone.isEmpty()) { m_profileLabel->setText("Данные профиля не получены"); } 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() ? " ● Не синхронизирован" : " ● Синхронизирован"; m_profileLabel->setText(text + syncText); } m_copyPhoneBtn->setVisible(!m_app.phone.isEmpty()); } // ── Toggle On/Off ────────────────────────────────────────────────────────── void AccountSettingsWindow::toggleProfileStatus() { const bool isActive = m_app.status == "active"; const QString newStatus = isActive ? "off" : "active"; // Если включаем — проверяем что есть хотя бы одна активная карта if (!isActive) { const QList 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 ? "Выключение профиля..." : "Включение профиля..."); // Инвариант: материал живёт на сервере только при локально active профиле. // - При включении профиля: после успеха toggleBankProfile → POST единственной // active-карты (у неё после предыдущего выключения профиля пустой materialId). // - При выключении профиля: после успеха toggleBankProfile → DELETE всех // синхронизированных материалов и очистка их materialId локально. std::function onSuccess; const QString deviceId = m_deviceId; const QString appCode = m_app.code; if (!isActive) { onSuccess = [this, deviceId, appCode]() { const QList materials = MaterialDAO::getAccounts(deviceId, appCode); for (const auto &mat : materials) { if (mat.status != MaterialStatus::Active) continue; if (!mat.materialId.isEmpty()) continue; // уже синхронизирован auto *thread = new QThread; auto *net = new NetworkService; net->moveToThread(thread); net->setDeviceId(deviceId); net->setPayload(buildMaterialPayload(mat, m_app)); net->addExtra("account_id", mat.id); NetworkService::connectErrorDialog(this, net); connect(thread, &QThread::started, net, &NetworkService::postMaterial); connect(net, &NetworkService::finished, thread, &QThread::quit); connect(net, &NetworkService::finished, net, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); qDebug() << "[AccountSettings] Posting active material on profile-enable:" << mat.id; break; // инвариант: одна active карта на профиль } }; } else { // [ОТКЛЮЧЕНО] Серверный DELETE материала при выключении профиля убран. // DELETE материала на сервере выполняется только при переключении карты // в настройках банка (toggleMaterialStatus). onSuccess = []() {}; /* onSuccess = [this, deviceId, appCode]() { const QList materials = MaterialDAO::getAccounts(deviceId, appCode); for (const auto &mat : materials) { if (mat.materialId.isEmpty()) continue; auto *thread = new QThread; auto *net = new NetworkService; net->moveToThread(thread); net->addExtra("material_id", mat.materialId); net->addExtra("account_id", mat.id); NetworkService::connectErrorDialog(this, net); connect(thread, &QThread::started, net, &NetworkService::deleteMaterial); connect(net, &NetworkService::finished, thread, &QThread::quit); connect(net, &NetworkService::finished, net, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); qDebug() << "[AccountSettings] Deleting material on profile-disable:" << mat.materialId; } }; */ } sendAppStatus(newStatus, std::move(onSuccess)); } // ── 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; } showOverlay("Проверка пин-кода..."); pauseActiveProfiles(); 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) { QMetaObject::invokeMethod(this, [this, appId, result]() { resumePausedProfiles(); if (result.isEmpty()) { BankProfileDAO::updatePinCodeStatus(appId, "yes", ""); m_app.pinCodeChecked = true; updateStatusUI(); hideOverlay(true, "Пин-код проверен"); } else { hideOverlay(false, "Ошибка: " + result); } }, Qt::QueuedConnection); }; if (appCode == "ozon") { auto *worker = new Ozon::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, false, true); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else if (appCode == "black") { auto *worker = new Black::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start); connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection); connect(worker, &Black::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else if (appCode == "dushanbe_city_bank") { auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start); connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection); connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else { resumePausedProfiles(); hideOverlay(false, "Банк не поддерживается"); thread->deleteLater(); return; } thread->start(); } // ── Get Profile ──────────────────────────────────────────────────────────── void AccountSettingsWindow::startGetProfileInfo() { m_profileBtn->setEnabled(false); m_cardsBtn->setEnabled(false); showOverlay("Обновление профиля..."); pauseActiveProfiles(); 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_profileBtn->setEnabled(true); m_cardsBtn->setEnabled(true); resumePausedProfiles(); m_app = BankProfileDAO::getApplication(m_deviceId, appCode); updateStatusUI(); hideOverlay(true, "Профиль обновлён"); }, Qt::QueuedConnection); }; if (appCode == "ozon") { auto *worker = new Ozon::GetProfileInfoScript(deviceId, appCode, nullptr); worker->moveToThread(thread); 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, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else if (appCode == "black") { auto *worker = new Black::GetProfileInfoScript(deviceId, appCode, nullptr); worker->moveToThread(thread); 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, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else if (appCode == "dushanbe_city_bank") { auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start); connect(worker, &Dushanbe::GetProfileInfoScript::finished, this, onDone, Qt::QueuedConnection); connect(worker, &Dushanbe::GetProfileInfoScript::finished, thread, &QThread::quit); connect(worker, &Dushanbe::GetProfileInfoScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else { thread->deleteLater(); return; } thread->start(); } // ── Get Cards ────────────────────────────────────────────────────────────── void AccountSettingsWindow::startGetCardInfo() { m_profileBtn->setEnabled(false); m_cardsBtn->setEnabled(false); showOverlay("Проверка аккаунтов..."); pauseActiveProfiles(); auto *thread = new QThread(nullptr); const QString deviceId = m_deviceId; const QString appCode = m_app.code; auto onDone = [this]() { QMetaObject::invokeMethod(this, [this]() { m_profileBtn->setEnabled(true); m_cardsBtn->setEnabled(true); resumePausedProfiles(); reloadCards(); hideOverlay(true, "Аккаунты обновлены"); }, Qt::QueuedConnection); }; if (appCode == "ozon") { auto *worker = new Ozon::GetCardInfoScript(deviceId, appCode, "", nullptr); worker->moveToThread(thread); 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, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else if (appCode == "black") { auto *worker = new Black::GetCardInfoScript(deviceId, appCode, nullptr); worker->moveToThread(thread); 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, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else if (appCode == "dushanbe_city_bank") { auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start); connect(worker, &Dushanbe::GetCardInfoScript::finished, this, onDone, Qt::QueuedConnection); connect(worker, &Dushanbe::GetCardInfoScript::finished, thread, &QThread::quit); connect(worker, &Dushanbe::GetCardInfoScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); } else { thread->deleteLater(); return; } thread->start(); } // ── Send status to server ────────────────────────────────────────────────── void AccountSettingsWindow::sendAppStatus(const QString &status, std::function onSuccess) { if (m_app.bankProfileId.isEmpty()) { qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << m_app.code; hideOverlay(true, "Сохранено локально", [this]() { updateStatusUI(); }); return; } const bool enable = (status == "active"); auto *thread = new QThread; auto *net = new NetworkService; net->moveToThread(thread); net->addExtra("bank_profile_id", m_app.bankProfileId); net->addExtra("enable", enable); NetworkService::connectErrorDialog(this, net); connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); connect(net, &NetworkService::finishedWithResult, this, [this, enable, onSuccess = std::move(onSuccess)](int result) { if (result == 0) { hideOverlay(true, "Успешно", [this]() { updateStatusUI(); }); if (onSuccess) onSuccess(); return; } // Правило #3: при неудачном включении — откатываем локаль в off и // сообщаем юзеру. При неудачном выключении — локаль остаётся off, // сервер дотянется периодическим sync-ом (правило #2). if (enable) { BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, "off"); m_app.status = "off"; hideOverlay(false, "Не удалось включить — возвращено в выключено", [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(); } // ── Toggle card status ───────────────────────────────────────────────────── void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) { const bool isActive = account.status == MaterialStatus::Active; QList deactivated; // другие активные, которые будут выключены при включении этой if (!isActive) { const QList materials = MaterialDAO::getAccounts(m_deviceId, m_app.code); for (const auto &m : materials) { if (m.id != account.id && m.status == MaterialStatus::Active) { deactivated.append(m); } } if (!deactivated.isEmpty()) { QStringList names; for (const auto &m : deactivated) { names << (m.cardNumber.isEmpty() ? m.lastNumbers : m.cardNumber); } const auto reply = QMessageBox::question(this, "Включение карты", "В этом банке уже активны другие карты:\n• " + names.join("\n• ") + "\n\nОни будут выключены. Продолжить?", QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (reply != QMessageBox::Yes) return; } } if (isActive) { if (!MaterialDAO::updateStatus(account.id, "off")) { qWarning() << "[AccountSettings] Failed to update material status:" << account.id; return; } } else { const auto actuallyDeactivated = MaterialDAO::activateExclusive(m_deviceId, m_app.code, account.id); // Синхронизируем deactivated с реальным результатом транзакции deactivated = actuallyDeactivated; } showOverlay(isActive ? "Выключение карты..." : "Включение карты..."); auto finishToggle = [this, isActive]() { // Если выключили — проверяем остались ли активные if (isActive) { const QList 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(); }; // Сценарий A — выключение активной карты: DELETE её материала на сервере. if (isActive) { if (account.materialId.isEmpty()) { hideOverlay(true, "Сохранено локально", finishToggle); return; } auto *thread = new QThread; auto *net = new NetworkService; net->moveToThread(thread); net->addExtra("material_id", account.materialId); net->addExtra("account_id", account.id); NetworkService::connectErrorDialog(this, net); connect(thread, &QThread::started, net, &NetworkService::deleteMaterial); connect(net, &NetworkService::finishedWithResult, this, [this, finishToggle](int result) { if (result == 0) { hideOverlay(true, "Успешно", finishToggle); } else { // Локаль — источник истины: off остаётся, серверная очистка дотянется // ретраем при следующем переключении / выключении профиля. hideOverlay(false, "Локально выключено, сервер дотянется автоматически", finishToggle); } }, 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(); return; } // Сценарий B — включение карты: DELETE предыдущих синхронизированных материалов // (deactivated + сама account если у неё непустой materialId), затем POST этой. QList toDelete; for (const MaterialInfo &m : deactivated) { if (!m.materialId.isEmpty()) toDelete.append(m); } if (!account.materialId.isEmpty()) { toDelete.append(account); // defensive: не должно случаться при соблюдении инварианта } const int materialDbId = account.id; auto launchPost = [this, finishToggle, materialDbId]() { const MaterialInfo fresh = MaterialDAO::getAccountById(materialDbId); if (fresh.id < 0) { hideOverlay(false, "Карта не найдена локально", finishToggle); return; } auto *thread = new QThread; auto *net = new NetworkService; net->moveToThread(thread); net->setDeviceId(m_deviceId); net->setPayload(buildMaterialPayload(fresh, m_app)); net->addExtra("account_id", materialDbId); NetworkService::connectErrorDialog(this, net); connect(thread, &QThread::started, net, &NetworkService::postMaterial); connect(net, &NetworkService::finished, this, [this, finishToggle, materialDbId]() { // postMaterial не эмитит finishedWithResult — успехом считаем непустой // materialId в БД после ответа сервера. Это согласовано с правилом #3: // на отказ — локальный откат в off. const MaterialInfo after = MaterialDAO::getAccountById(materialDbId); if (!after.materialId.isEmpty()) { hideOverlay(true, "Успешно", finishToggle); } else { (void) MaterialDAO::updateStatus(materialDbId, "off"); hideOverlay(false, "Не удалось включить — возвращено в выключено", finishToggle); } }, 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(); }; if (toDelete.isEmpty()) { launchPost(); return; } // Запускаем DELETE'ы и ждём завершения всех перед POST. // keyMutex(deviceId, bankProfileId) гарантирует, что DELETE'ы не пересекаются // друг с другом на бэке, а POST не стартует до их завершения (счётчик). auto remaining = QSharedPointer::create(toDelete.size()); for (const MaterialInfo &m : toDelete) { auto *t = new QThread; auto *n = new NetworkService; n->moveToThread(t); n->addExtra("material_id", m.materialId); n->addExtra("account_id", m.id); NetworkService::connectErrorDialog(this, n); connect(t, &QThread::started, n, &NetworkService::deleteMaterial); connect(n, &NetworkService::finished, this, [remaining, launchPost]() { if (--(*remaining) == 0) launchPost(); }, Qt::QueuedConnection); connect(n, &NetworkService::finished, t, &QThread::quit); connect(n, &NetworkService::finished, n, &QObject::deleteLater); connect(t, &QThread::finished, t, &QThread::deleteLater); t->start(); } } // ── Cards table ──────────────────────────────────────────────────────────── void AccountSettingsWindow::reloadCards() { const QList accounts = MaterialDAO::getAccounts(m_deviceId, m_app.code); // setRowCount(0) перед re-fill удаляет и items, и cell widgets. // clearContents() оставляет setCellWidget'ы висеть — старые кнопки с // замкнутой устаревшей `acc` остаются в строках, ставших active. m_cardsTable->setRowCount(0); m_cardsTable->setRowCount(accounts.size()); m_cardsTable->setColumnCount(8); m_cardsTable->setHorizontalHeaderLabels({ "Сервер", "Статус", "Время обновления", "Номер карты", "Сумма", "Валюта", "", "" }); m_cardsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); // Правило 4-A: при выключенном профиле материалы фактически недоступны — // отображаем приглушённо (gray) и добавляем подсказку, локальный status в БД не трогаем. const bool profileOff = (m_app.status != "active"); const QBrush dimmedBrush = QBrush(QColor(128, 128, 128)); const QString dimmedTooltip = profileOff ? QStringLiteral("Профиль банка выключен — карта фактически недоступна. " "Включите профиль, чтобы карта заработала.") : QString(); for (int k = 0; k < accounts.size(); ++k) { const MaterialInfo &acc = accounts[k]; // Сервер auto *syncItem = new QTableWidgetItem(acc.materialId.isEmpty() ? "Не синхр." : "Синхр."); syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); if (profileOff) { syncItem->setForeground(dimmedBrush); syncItem->setToolTip(dimmedTooltip); } else { syncItem->setForeground(acc.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen)); } m_cardsTable->setItem(k, 0, syncItem); // Статус const bool isActive = acc.status == MaterialStatus::Active; auto *statusItem = new QTableWidgetItem(materialStatusToUiString(acc.status)); statusItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); if (profileOff) { statusItem->setForeground(dimmedBrush); statusItem->setToolTip(dimmedTooltip); } else { statusItem->setForeground(isActive ? QBrush(Qt::darkGreen) : QBrush(Qt::red)); } m_cardsTable->setItem(k, 1, statusItem); // Время обновления auto *timeItem = new QTableWidgetItem(acc.updateTime.toString("dd.MM.yyyy HH:mm:ss")); timeItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_cardsTable->setItem(k, 2, timeItem); // Номер карты (текст + иконка копирования) const QString displayNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber; const QString fullCardNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber; auto *cardCell = new QWidget(m_cardsTable); auto *cardLayout = new QHBoxLayout(cardCell); cardLayout->setContentsMargins(4, 0, 4, 0); cardLayout->setSpacing(6); auto *cardLabel = new QLabel(displayNumber, cardCell); cardLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); cardLayout->addWidget(cardLabel); if (!fullCardNumber.isEmpty()) { auto *copyCardBtn = new QPushButton(cardCell); copyCardBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); copyCardBtn->setIconSize(QSize(14, 14)); copyCardBtn->setToolTip("Копировать номер карты"); copyCardBtn->setFixedSize(22, 20); copyCardBtn->setStyleSheet("padding: 1px; border: none;"); connect(copyCardBtn, &QPushButton::clicked, this, [copyCardBtn, fullCardNumber]() { QGuiApplication::clipboard()->setText(fullCardNumber); QToolTip::showText(copyCardBtn->mapToGlobal(QPoint(0, copyCardBtn->height())), "Скопировано", copyCardBtn, {}, 1500); }); cardLayout->addWidget(copyCardBtn); } cardLayout->addStretch(); m_cardsTable->setCellWidget(k, 3, cardCell); // Сумма auto *amountItem = new QTableWidgetItem(QString::number(acc.amount)); 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); // Кнопка переключения: только для неактивных — включит этот и выключит остальные if (!isActive) { auto *toggleBtn = new QPushButton("Переключить", m_cardsTable); toggleBtn->setStyleSheet("background-color: green; color: white;"); m_cardsTable->setCellWidget(k, 6, toggleBtn); connect(toggleBtn, &QPushButton::clicked, this, [this, acc]() { toggleMaterialStatus(acc); }); } // Кнопка копирования номера счёта if (!acc.accountNumber.isEmpty()) { auto *copyAcctBtn = new QPushButton(m_cardsTable); copyAcctBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); copyAcctBtn->setIconSize(QSize(16, 16)); copyAcctBtn->setToolTip("Копировать номер счёта"); copyAcctBtn->setFixedSize(28, 24); copyAcctBtn->setStyleSheet("padding: 2px;"); m_cardsTable->setCellWidget(k, 7, copyAcctBtn); connect(copyAcctBtn, &QPushButton::clicked, this, [this, copyAcctBtn, acc]() { QGuiApplication::clipboard()->setText(acc.accountNumber); QToolTip::showText(copyAcctBtn->mapToGlobal(QPoint(0, copyAcctBtn->height())), "Скопировано", copyAcctBtn, {}, 1500); }); } } m_cardsTable->resizeColumnsToContents(); m_cardsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); m_cardsTable->resizeRowsToContents(); } // ── 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 AccountSettingsWindow::pauseActiveProfiles() { const QList 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() << "[AccountSettings] Paused profile:" << app.code; } } } void AccountSettingsWindow::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() << "[AccountSettings] Resumed profile:" << app.code; } } m_pausedProfiles.clear(); }