diff --git a/libqf/libqfcore/src/core/utils.cpp b/libqf/libqfcore/src/core/utils.cpp index 63e9014f6..5e62a301e 100644 --- a/libqf/libqfcore/src/core/utils.cpp +++ b/libqf/libqfcore/src/core/utils.cpp @@ -279,10 +279,15 @@ QVariant Utils::jsonToQVariant(const QString &json) } QString Utils::qvariantToJson(const QVariant &v, bool compact) +{ + return QString::fromUtf8(qvariantToJsonUtf8(v, compact)); +} + +QByteArray Utils::qvariantToJsonUtf8(const QVariant &v, bool compact) { auto doc = QJsonDocument::fromVariant(v); auto ba = doc.toJson(compact? QJsonDocument::Compact: QJsonDocument::Indented); - return QString::fromUtf8(ba); + return ba; } } diff --git a/libqf/libqfcore/src/core/utils.h b/libqf/libqfcore/src/core/utils.h index 7290b23bb..8f1633edb 100644 --- a/libqf/libqfcore/src/core/utils.h +++ b/libqf/libqfcore/src/core/utils.h @@ -213,6 +213,7 @@ class QFCORE_DECL_EXPORT Utils static QVariant jsonToQVariant(const QString &json); static QString qvariantToJson(const QVariant &v, bool compact = true); + static QByteArray qvariantToJsonUtf8(const QVariant &v, bool compact = true); }; }} diff --git a/libqf/libqfqmlwidgets/src/framework/application.cpp b/libqf/libqfqmlwidgets/src/framework/application.cpp index fa7520c9a..4a72da71c 100644 --- a/libqf/libqfqmlwidgets/src/framework/application.cpp +++ b/libqf/libqfqmlwidgets/src/framework/application.cpp @@ -17,6 +17,11 @@ Application::Application(int &argc, char **argv) : { } +QString Application::versionString() const +{ + return QCoreApplication::applicationVersion(); +} + Application::~Application() = default; diff --git a/libqf/libqfqmlwidgets/src/framework/application.h b/libqf/libqfqmlwidgets/src/framework/application.h index 30bb60396..f37a2b317 100644 --- a/libqf/libqfqmlwidgets/src/framework/application.h +++ b/libqf/libqfqmlwidgets/src/framework/application.h @@ -23,7 +23,9 @@ class QFQMLWIDGETS_DECL_EXPORT Application : public QApplication typedef QApplication Super; public: explicit Application(int & argc, char ** argv); - ~Application() Q_DECL_OVERRIDE; + ~Application() override; + + Q_INVOKABLE QString versionString() const; public: static Application* instance(bool must_exist = true); MainWindow* frameWork(); diff --git a/quickevent/app/quickevent/CMakeLists.txt b/quickevent/app/quickevent/CMakeLists.txt index 900cce054..36d1828fa 100644 --- a/quickevent/app/quickevent/CMakeLists.txt +++ b/quickevent/app/quickevent/CMakeLists.txt @@ -95,6 +95,8 @@ add_executable(quickevent plugins/Event/src/services/qx/qxlateregistrationswidget.h plugins/Event/src/services/qx/qxlateregistrationswidget.cpp plugins/Event/src/services/qx/qxlateregistrationswidget.ui + plugins/Event/src/services/qx/runchangedialog.h plugins/Event/src/services/qx/runchangedialog.cpp plugins/Event/src/services/qx/runchangedialog.ui + plugins/Event/src/services/qx/runchange.h plugins/Event/src/services/qx/runchange.cpp plugins/Oris/src/chooseoriseventdialog.cpp plugins/Oris/src/chooseoriseventdialog.ui diff --git a/quickevent/app/quickevent/plugins/CardReader/src/cardreaderplugin.cpp b/quickevent/app/quickevent/plugins/CardReader/src/cardreaderplugin.cpp index e59d5999c..d906ce196 100644 --- a/quickevent/app/quickevent/plugins/CardReader/src/cardreaderplugin.cpp +++ b/quickevent/app/quickevent/plugins/CardReader/src/cardreaderplugin.cpp @@ -532,7 +532,6 @@ bool CardReaderPlugin::processCardToRunAssignment(int card_id, int run_id) QVariantList param { next_leg_run_id, QVariantMap { - {"runs.id", next_leg_run_id}, {"runs.startTimeMs", new_next_leg_start_time}, } }; diff --git a/quickevent/app/quickevent/plugins/Classes/src/classeswidget.cpp b/quickevent/app/quickevent/plugins/Classes/src/classeswidget.cpp index 4a350f507..dbe559878 100644 --- a/quickevent/app/quickevent/plugins/Classes/src/classeswidget.cpp +++ b/quickevent/app/quickevent/plugins/Classes/src/classeswidget.cpp @@ -192,9 +192,13 @@ ClassesWidget::ClassesWidget(QWidget *parent) : //m->addColumn("courses.name", tr("Course")).setReadOnly(true); m->addColumn("courses.length", tr("Length")); m->addColumn("courses.climb", tr("Climb")); - m->addColumn("relaysCount", tr("Count")).setToolTip(tr("Relays count")); - m->addColumn("classdefs.relayStartNumber", tr("Rel.num")).setToolTip(tr("Relay start number")); - m->addColumn("classdefs.relayLegCount", tr("Legs")).setToolTip(tr("Relay leg count")); + + if (auto is_relays = getPlugin()->eventConfig()->isRelays(); is_relays) { + m->addColumn("relaysCount", tr("Rel. count")).setToolTip(tr("Relays count")); + m->addColumn("classdefs.relayStartNumber", tr("Rel. num")).setToolTip(tr("Relay start number")); + m->addColumn("classdefs.relayLegCount", tr("Legs")).setToolTip(tr("Relay leg count")); + } + ui->tblClasses->setTableModel(m); m_courseItemDelegate = new CourseItemDelegate(this); diff --git a/quickevent/app/quickevent/plugins/Competitors/src/competitordocument.cpp b/quickevent/app/quickevent/plugins/Competitors/src/competitordocument.cpp index a2cd0bc62..4551c718f 100644 --- a/quickevent/app/quickevent/plugins/Competitors/src/competitordocument.cpp +++ b/quickevent/app/quickevent/plugins/Competitors/src/competitordocument.cpp @@ -101,8 +101,9 @@ bool CompetitorDocument::saveData() else { competitor_id = dataId().toInt(); } - if(m_isEmitDbEventsOnSave) + if(m_isEmitDbEventsOnSave) { getPlugin()->emitDbEvent(Event::EventPlugin::DBEVENT_COMPETITOR_EDITED, competitor_id); + } } return ret; } diff --git a/quickevent/app/quickevent/plugins/Competitors/src/competitorwidget.ui b/quickevent/app/quickevent/plugins/Competitors/src/competitorwidget.ui index 7f356a3be..232cd8ee4 100644 --- a/quickevent/app/quickevent/plugins/Competitors/src/competitorwidget.ui +++ b/quickevent/app/quickevent/plugins/Competitors/src/competitorwidget.ui @@ -259,6 +259,29 @@ + + + + ID + + + edSiId + + + + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + true + + + competitors.id + + + diff --git a/quickevent/app/quickevent/plugins/Event/qml/DbSchema.qml b/quickevent/app/quickevent/plugins/Event/qml/DbSchema.qml index 6434842ec..8ac207afa 100644 --- a/quickevent/app/quickevent/plugins/Event/qml/DbSchema.qml +++ b/quickevent/app/quickevent/plugins/Event/qml/DbSchema.qml @@ -490,15 +490,18 @@ Schema { Field { name: 'id'; type: Serial { primaryKey: true } }, Field { name: 'stage_id'; type: Int { } }, Field { name: 'change_id'; type: Int { } }, - Field { name: 'run_id'; type: Int { } }, + Field { name: 'data_id'; type: Int { } }, Field { name: 'data_type'; type: String { } }, Field { name: 'data'; type: String { } }, + Field { name: 'orig_data'; type: String { } + comment: 'Store data overriden by change here to enable change rollback.' + }, Field { name: 'source'; type: String { } }, Field { name: 'user_id'; type: String { } }, Field { name: 'status'; type: String { } }, Field { name: 'status_message'; type: String { } }, Field { name: 'created'; type: DateTime { } }, - Field { name: 'row_lock'; type: Int { } } + Field { name: 'lock_number'; type: Int { } } ] indexes: [ Index {fields: ['stage_id', 'change_id']; unique: true } diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.cpp index 2f691bff6..e1fa29743 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include using namespace qf::core; using namespace qf::qmlwidgets; @@ -71,7 +73,7 @@ void QxClientService::run() { auto data = reply->readAll(); auto doc = QJsonDocument::fromJson(data); EventInfo event_info(doc.toVariant().toMap()); - setStatusMessage(event_info.name()); + setStatusMessage(event_info.name() + (event_info.stage_count() > 1? QStringLiteral(" E%1").arg(event_info.stage()): QString())); m_eventId = event_info.id(); connectToSSE(m_eventId); if (!m_pollChangesTimer) { @@ -115,11 +117,11 @@ void QxClientService::loadSettings() void QxClientService::onDbEventNotify(const QString &domain, int connection_id, const QVariant &data) { - if (status() != Status::Running) { - return; - } Q_UNUSED(connection_id) Q_UNUSED(data) + if (!isRunning()) { + return; + } if(domain == QLatin1String(Event::EventPlugin::DBEVENT_CARD_PROCESSED_AND_ASSIGNED)) { //auto checked_card = quickevent::core::si::CheckedCard(data.toMap()); //int competitor_id = getPlugin()->competitorForRun(checked_card.runId()); @@ -130,9 +132,33 @@ void QxClientService::onDbEventNotify(const QString &domain, int connection_id, //onCompetitorChanged(competitor_id); } else if (domain == Event::EventPlugin::DBEVENT_RUN_CHANGED) { - //if (auto *node = m_rootNode->findChild(); node) { - // node->sendRunChangedSignal(data); - //} + auto lst = data.toList(); + auto run_id = lst.value(0).toInt(); + auto ei = eventInfo(); + bool this_stage = false; + qf::core::sql::Query q; + q.execThrow(QStringLiteral("SELECT COUNT(*) FROM runs WHERE id=%1 AND stageId=%2").arg(run_id).arg(ei.stage())); + if (q.next()) { + this_stage = q.value(0).toInt() == 1; + } + if (!this_stage) { + return; + } + auto qe_run_rec = lst.value(1).toMap(); + auto qx_run_rec = QVariantMap(); + auto remap_key = [&qe_run_rec, &qx_run_rec](const QString &qe_key, const QString &qx_key) { + if (qe_run_rec.contains(qe_key)) { + qx_run_rec[qx_key] = qe_run_rec.value(qe_key); + } + }; + + qfInfo() << "DBEVENT_RUN_CHANGED run id:" << run_id << "data:" << QString::fromUtf8(QJsonDocument::fromVariant(qe_run_rec).toJson(QJsonDocument::Compact)); + + remap_key("runs.siId", "si_id"); + remap_key("competitors.registration", "registration"); + if (!qx_run_rec.isEmpty()) { + httpPostJson( "/api/event/current/changes/run-updated", QStringLiteral("run_id=%1").arg(run_id), qx_run_rec); + } } } @@ -171,7 +197,7 @@ QNetworkReply *QxClientService::postEventInfo(const QString &qxhttp_host, const return nm->post(request, data); } -void QxClientService::exportStartListIofXml3(QObject *context, std::function call_back) +void QxClientService::postStartListIofXml3(QObject *context, std::function call_back) { auto *ep = getPlugin(); int current_stage = ep->currentStageId(); @@ -182,23 +208,54 @@ void QxClientService::exportStartListIofXml3(QObject *context, std::function call_back) +void QxClientService::postRuns(QObject *context, std::function call_back) { auto *ep = getPlugin(); int current_stage = ep->currentStageId(); bool is_relays = ep->eventConfig()->isRelays(); if (!is_relays) { - auto runs = getPlugin()->qxExportRunsCsv(current_stage); - uploadSpecFile(SpecFile::RunsCsv, runs.toUtf8(), context, call_back); + auto runs = getPlugin()->qxExportRunsCsvJson(current_stage); + auto json = qf::core::Utils::qvariantToJsonUtf8(runs, false); + uploadSpecFile(SpecFile::RunsCsvJson, json, context, call_back); } } -QNetworkReply* QxClientService::loadQxChanges(int from_id) +void QxClientService::getHttpJson(const QString &path, const QUrlQuery &query, QObject *context, const std::function &call_back) +{ + auto url = exchangeServerUrl(); + url.setPath(path); + url.setQuery(query); + // qfInfo() << url.toString(); + QNetworkRequest request; + request.setUrl(url); + auto *reply = networkManager()->get(request); + // connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, context, [call_back, reply]() { + if (reply->error() == QNetworkReply::NetworkError::NoError) { + QJsonParseError err; + auto data = reply->readAll(); + auto json = QJsonDocument::fromJson(data, &err).toVariant(); + if (err.error != QJsonParseError::NoError) { + call_back({}, err.errorString()); + } + else { + call_back(json, {}); + } + } + else { + call_back({}, reply->errorString()); + } + reply->deleteLater(); + }); +} + +QNetworkReply* QxClientService::getQxChangesReply(int from_id) { auto url = exchangeServerUrl(); url.setPath(QStringLiteral("/api/event/%1/changes").arg(eventId())); url.setQuery(QStringLiteral("from_id=%1").arg(from_id)); + qfInfo() << url.toString(); QNetworkRequest request; request.setUrl(url); return networkManager()->get(request); @@ -255,14 +312,14 @@ void QxClientService::postFileCompressed(std::optional path, std::optio }); } -void QxClientService::uploadSpecFile(SpecFile file, QByteArray data, QObject *context, std::function call_back) +void QxClientService::uploadSpecFile(SpecFile file, QByteArray data, QObject *context, const std::function &call_back) { switch (file) { case SpecFile::StartListIofXml3: postFileCompressed({}, "startlist-iof3.xml", data, context, call_back); break; - case SpecFile::RunsCsv: - postFileCompressed({}, "runs.csv", data, context, call_back); + case SpecFile::RunsCsvJson: + postFileCompressed({}, "runs.csv.json", data, context, call_back); break; } } @@ -276,6 +333,46 @@ QByteArray QxClientService::zlibCompress(QByteArray data) return compressedData; } +void QxClientService::httpPostJson(const QString &path, const QString &query, QVariantMap json, QObject *context, const std::function &call_back) +{ + if (!isRunning()) { + return; + } + auto url = exchangeServerUrl(); + + url.setPath(path); + url.setQuery(query); + + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader(QX_API_TOKEN, apiToken()); + request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json")); + auto data = QJsonDocument::fromVariant(json).toJson(QJsonDocument::Compact); + qfInfo() << "HTTP POST JSON:" << url.toString() << "data:" << QString::fromUtf8(data); + QNetworkReply *reply = networkManager()->post(request, data); + if (context) { + connect(reply, &QNetworkReply::finished, context, [reply, url, call_back]() { + QString err; + if(reply->error()) { + err = reply->errorString(); + qfWarning() << "HTTP POST:" << url.toString() << "error:" << err; + } + if (call_back) { + call_back(err); + } + reply->deleteLater(); + }); + } + else { + connect(reply, &QNetworkReply::finished, [reply, url, call_back]() { + if(reply->error()) { + qfWarning() << "HTTP POST:" << url.toString() << "error:" << reply->errorString(); + } + reply->deleteLater(); + }); + } +} + void QxClientService::connectToSSE(int event_id) { Q_UNUSED(event_id); @@ -316,13 +413,13 @@ void QxClientService::pollQxChanges() } int stage_id = event_plugin->currentStageId(); try { - int start_id = 0; + int max_change_id = 0; qf::core::sql::Query q; - q.execThrow("SELECT MAX(id) FROM qxchanges"); + q.execThrow("SELECT MAX(change_id) FROM qxchanges WHERE stage_id=" + QString::number(event_plugin->currentStageId())); if (q.next()) { - start_id = q.value(0).toInt() + 1; + max_change_id = q.value(0).toInt(); } - auto *reply = loadQxChanges(start_id); + auto *reply = getQxChangesReply(max_change_id + 1); connect(reply, &QNetworkReply::finished, this, [reply, stage_id]() { QString err; if(reply->error()) { @@ -339,8 +436,8 @@ void QxClientService::pollQxChanges() else { auto records = json.array().toVariantList(); qf::core::sql::Query q; - q.prepare("INSERT INTO qxchanges (data_type, data, run_id, source, user_id, status, status_message, stage_id, change_id, created)" - " VALUES (:data_type, :data, :run_id, :source, :user_id, :status, :status_message, :stage_id, :change_id, :created)" + q.prepare("INSERT INTO qxchanges (data_type, data, data_id, source, user_id, status, status_message, stage_id, change_id, created)" + " VALUES (:data_type, :data, :data_id, :source, :user_id, :status, :status_message, :stage_id, :change_id, :created)" " RETURNING id"); for (const auto &v : records) { auto rec = v.toMap(); @@ -348,7 +445,7 @@ void QxClientService::pollQxChanges() auto data = QString::fromUtf8(ba); q.bindValue(":data_type", rec.value("data_type")); q.bindValue(":data", data); - q.bindValue(":run_id", rec.value("run_id")); + q.bindValue(":data_id", rec.value("data_id")); q.bindValue(":source", rec.value("source")); q.bindValue(":user_id", rec.value("user_id")); q.bindValue(":status", rec.value("status")); @@ -389,10 +486,84 @@ EventInfo QxClientService::eventInfo() const auto *event_config = event_plugin->eventConfig(); EventInfo ei; ei.set_stage(event_plugin->currentStageId()); + ei.set_stage_count(event_plugin->stageCount()); ei.set_name(event_config->eventName()); ei.set_place(event_config->eventPlace()); ei.set_start_time(event_plugin->stageStartDateTime(event_plugin->currentStageId()).toString(Qt::ISODate)); + + qf::core::sql::Query q; + q.execThrow(QStringLiteral("SELECT classes.name AS name, COUNT(codes.id) AS control_count, length, climb, startTimeMin AS start_time, startIntervalMin as interval, lastStartTimeMin AS start_slot_count" + " FROM classes" + " LEFT JOIN classdefs ON classes.id=classdefs.classId AND classdefs.stageId=%1" + " LEFT JOIN courses ON classdefs.courseId=courses.id" + " LEFT JOIN coursecodes ON coursecodes.courseId=courses.id" + " LEFT JOIN codes ON coursecodes.codeId=codes.id AND codes.code>=%2 AND codes.code<=%3" + " GROUP BY classes.id, classes.name, length, climb, startTimeMin, startIntervalMin, lastStartTimeMin" + " ORDER BY classes.name") + .arg(ei.stage()) + .arg(quickevent::core::CodeDef::PUNCH_CODE_MIN) + .arg(quickevent::core::CodeDef::PUNCH_CODE_MAX) + ); + + QVariantList classes; + { + // QStringList columns{"name", "control_count", "length", "climb", "start_time", "interval", "start_slot_count"}; + QStringList columns; + auto rec = q.record(); + for (auto i = 0; i < rec.count(); ++i) { + columns << rec.field(i).name(); + } + classes.insert(classes.length(), columns); + } + while (q.next()) { + QVariantList values; + auto rec = q.record(); + for (auto i = 0; i < rec.count(); ++i) { + values << q.value(i); + } + auto interval = q.value("interval").toInt(); + if (interval > 0) { + auto start_time = q.value("start_time").toInt(); + auto last_time = q.value("start_slot_count").toInt(); + auto start_slot_count = 1 + ((last_time - start_time) / interval); + values.last() = start_slot_count; + } + else { + values.last() = 0; + } + classes.insert(classes.length(), values); + } + ei.set_classes(classes); + // qfInfo() << qf::core::Utils::qvariantToJson(ei, false); return ei; } +auto query_to_json_csv(QSqlQuery &q) +{ + QVariantList csv; + { + // QStringList columns{"name", "control_count", "length", "climb", "start_time", "interval", "start_slot_count"}; + QStringList columns; + auto rec = q.record(); + for (auto i = 0; i < rec.count(); ++i) { + columns << rec.field(i).name(); + } + csv.insert(csv.length(), columns); + } + while (q.next()) { + QVariantList values; + auto rec = q.record(); + for (auto i = 0; i < rec.count(); ++i) { + values << q.value(i); + } + csv.insert(csv.length(), values); + } + return csv; +} + +int QxClientService::currentConnectionId() +{ + return qf::core::sql::Connection::forName().connectionId(); +} + } // namespace Event::services::qx diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.h index a40ed9883..79007bec0 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.h @@ -4,23 +4,11 @@ class QNetworkAccessManager; class QNetworkReply; +class QUrlQuery; class QTimer; namespace Event::services::qx { -static constexpr auto COL_ID = "id"; -static constexpr auto COL_DATA = "data"; -static constexpr auto COL_DATA_TYPE = "data_type"; -static constexpr auto COL_STATUS = "status"; -static constexpr auto COL_STATUS_MESSAGE = "status_message"; -static constexpr auto COL_SOURCE = "source"; -static constexpr auto COL_RUN_ID = "run_id"; -static constexpr auto COL_USER_ID = "user_id"; -static constexpr auto COL_CREATED = "created"; -static constexpr auto COL_CROW_LOCK = "row_lock"; - -static constexpr auto STATUS_PENDING = "Pending"; - class QxClientServiceSettings : public ServiceSettings { using Super = ServiceSettings; @@ -37,9 +25,11 @@ class EventInfo : public QVariantMap QF_VARIANTMAP_FIELD(int, i, set_i, d) QF_VARIANTMAP_FIELD(int, s, set_s, tage) + QF_VARIANTMAP_FIELD(int, s, set_s, tage_count) QF_VARIANTMAP_FIELD(QString, n, set_n, ame) QF_VARIANTMAP_FIELD(QString, p, set_p, lace) QF_VARIANTMAP_FIELD(QString, s, set_s, tart_time) + QF_VARIANTMAP_FIELD(QVariantList, c, set_c, lasses) public: EventInfo(const QVariantMap &data = QVariantMap()) : QVariantMap(data) {} }; @@ -67,22 +57,27 @@ class QxClientService : public Service QNetworkReply* getRemoteEventInfo(const QString &qxhttp_host, const QString &api_token); QNetworkReply* postEventInfo(const QString &qxhttp_host, const QString &api_token); - void exportStartListIofXml3(QObject *context, std::function call_back = nullptr); - void exportRuns(QObject *context, std::function call_back = nullptr); + void postStartListIofXml3(QObject *context, std::function call_back = nullptr); + void postRuns(QObject *context, std::function call_back = nullptr); + void getHttpJson(const QString &path, const QUrlQuery &query, QObject *context, const std::function &call_back = nullptr); - QNetworkReply* loadQxChanges(int from_id); + QNetworkReply* getQxChangesReply(int from_id); QByteArray apiToken() const; -private: + static int currentConnectionId(); + QUrl exchangeServerUrl() const; + int eventId() const; +private: void loadSettings() override; qf::qmlwidgets::framework::DialogWidget *createDetailWidget() override; - QUrl exchangeServerUrl() const; void postFileCompressed(std::optional path, std::optional name, QByteArray data, QObject *context, std::function call_back = nullptr); - enum class SpecFile {StartListIofXml3, RunsCsv}; - void uploadSpecFile(SpecFile file, QByteArray data, QObject *context, std::function call_back = nullptr); + enum class SpecFile {StartListIofXml3, RunsCsvJson}; + void uploadSpecFile(SpecFile file, QByteArray data, QObject *context, const std::function &call_back = nullptr); QByteArray zlibCompress(QByteArray data); + void httpPostJson(const QString &path, const QString &query, QVariantMap json, QObject *context = nullptr, const std::function &call_back = nullptr); + void connectToSSE(int event_id); void disconnectSSE(); diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.cpp index 476a267b2..58ef01b6a 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.cpp @@ -49,17 +49,22 @@ QxClientServiceWidget::~QxClientServiceWidget() delete ui; } -void QxClientServiceWidget::setMessage(const QString &msg, bool is_error) +void QxClientServiceWidget::setMessage(const QString &msg, MessageType msg_type) { if (msg.isEmpty()) { ui->lblStatus->setStyleSheet({}); } else { - if (is_error) { - ui->lblStatus->setStyleSheet("background: salmon"); - } - else { - ui->lblStatus->setStyleSheet("background: lightgreen"); + switch (msg_type) { + case MessageType::Ok: + ui->lblStatus->setStyleSheet("background: lightgreen"); + break; + case MessageType::Error: + ui->lblStatus->setStyleSheet("background: salmon"); + break; + case MessageType::Progress: + ui->lblStatus->setStyleSheet("background: orange"); + break; } } ui->lblStatus->setText(msg); @@ -120,8 +125,9 @@ void QxClientServiceWidget::testConnection() setMessage(tr("Connected OK")); } else { - setMessage(tr("Connection error: %1").arg(reply->errorString()), true); + setMessage(tr("Connection error: %1").arg(reply->errorString()), MessageType::Error); } + reply->deleteLater(); }); } @@ -139,8 +145,9 @@ void QxClientServiceWidget::exportEventInfo() setMessage(tr("Event info updated OK")); } else { - setMessage(tr("Event info update error: %1\n%2").arg(reply->errorString()).arg(QString::fromUtf8(data)), true); + setMessage(tr("Event info update error: %1\n%2").arg(reply->errorString()).arg(QString::fromUtf8(data)), MessageType::Error); } + reply->deleteLater(); }); } @@ -149,13 +156,13 @@ void QxClientServiceWidget::exportStartList() auto *svc = service(); Q_ASSERT(svc); saveSettings(); - setMessage(tr("Start list export started ...")); - svc->exportStartListIofXml3(this, [this](auto err) { + setMessage(tr("Start list export started ..."), MessageType::Progress); + svc->postStartListIofXml3(this, [this](auto err) { if (err.isEmpty()) { setMessage(tr("Start list exported Ok")); } else { - setMessage(err, true); + setMessage(err, MessageType::Error); } }); } @@ -165,13 +172,13 @@ void QxClientServiceWidget::exportRuns() auto *svc = service(); Q_ASSERT(svc); saveSettings(); - setMessage(tr("Runs export started ...")); - svc->exportRuns(this, [this](auto err) { + setMessage(tr("Runs export started ..."), MessageType::Progress); + svc->postRuns(this, [this](auto err) { if (err.isEmpty()) { setMessage(tr("Runs exported Ok")); } else { - setMessage(err, true); + setMessage(err, MessageType::Error); } }); } diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.h index edea4c139..f5840d512 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.h @@ -19,7 +19,8 @@ class QxClientServiceWidget : public qf::qmlwidgets::framework::DialogWidget explicit QxClientServiceWidget(QWidget *parent = nullptr); ~QxClientServiceWidget() override; private: - void setMessage(const QString &msg = {}, bool is_error = false); + enum class MessageType { Ok, Error, Progress }; + void setMessage(const QString &msg = {}, MessageType msg_type = MessageType::Ok); QxClientService* service(); bool saveSettings(); void updateOCheckListPostUrl(); diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.cpp index 90a31199a..1f79f9dc7 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.cpp @@ -2,54 +2,83 @@ #include "ui_qxlateregistrationswidget.h" #include "qxclientservice.h" +#include "runchangedialog.h" +#include "runchange.h" #include #include #include #include +#include + +#include +#include namespace qfm = qf::core::model; namespace qfs = qf::core::sql; +namespace qfw = qf::qmlwidgets; using qf::qmlwidgets::framework::getPlugin; namespace Event::services::qx { +constexpr auto COL_ID = "id"; +constexpr auto COL_CHANGE_ID = "change_id"; +constexpr auto COL_DATA_TYPE = "data_type"; +constexpr auto COL_DATA_ID = "data_id"; +constexpr auto COL_DATA = "data"; +constexpr auto COL_ORIG_DATA = "orig_data"; +constexpr auto COL_STATUS = "status"; +constexpr auto COL_STATUS_MESSAGE = "status_message"; +constexpr auto COL_SOURCE = "source"; +constexpr auto COL_USER_ID = "user_id"; +constexpr auto COL_CREATED = "created"; +constexpr auto COL_LOCK_NUMBER = "lock_number"; + +constexpr auto STATUS_PENDING = "Pending"; +constexpr auto STATUS_LOCKED = "Locked"; + +constexpr auto DATA_TYPE_RUN_UPDATE_REQUEST = "RunUpdateRequest"; + +constexpr auto SOURCE_WWW = "www"; + QxLateRegistrationsWidget::QxLateRegistrationsWidget(QWidget *parent) : QWidget(parent), ui(new Ui::QxLateRegistrationsWidget) { ui->setupUi(this); + ui->tableView->setReadOnly(true); + ui->tableView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->tableView, &qfw::TableView::customContextMenuRequested, this, &QxLateRegistrationsWidget::onTableCustomContextMenuRequest); + connect(ui->tableView, &qfw::TableView::doubleClicked, this, &QxLateRegistrationsWidget::onTableDoubleClicked); + ui->tableView->setPersistentSettingsId("tblQxLateRegistrations"); - ui->tableView->setInsertRowEnabled(true); + ui->tableView->setInsertRowEnabled(false); ui->tableView->setCloneRowEnabled(false); - ui->tableView->setRemoveRowEnabled(true); + ui->tableView->setRemoveRowEnabled(false); ui->tableView->setDirtyRowsMenuSectionEnabled(false); ui->toolbar->setTableView(ui->tableView); m_model = new qfm::SqlTableModel(this); //m->setObjectName("classes.classesModel"); m_model->addColumn(COL_ID).setReadOnly(true).setAlignment(Qt::AlignLeft); + m_model->addColumn(COL_STATUS, tr("Status")); m_model->addColumn(COL_DATA_TYPE, tr("Type")); - m_model->addColumn(COL_DATA, tr("Data"));//.setToolTip(tr("Locked for drawing")); + m_model->addColumn(COL_DATA_ID, tr("Data ID")).setAlignment(Qt::AlignLeft); m_model->addColumn(COL_SOURCE, tr("Source")); - m_model->addColumn(COL_RUN_ID, tr("Run")).setAlignment(Qt::AlignLeft); m_model->addColumn(COL_USER_ID, tr("User")); - m_model->addColumn(COL_STATUS, tr("Status")); m_model->addColumn(COL_STATUS_MESSAGE, tr("Status message")); m_model->addColumn(COL_CREATED, tr("Created")); - m_model->addColumn(COL_CROW_LOCK, tr("Locked")); + m_model->addColumn(COL_CHANGE_ID, tr("Change ID")); + m_model->addColumn(COL_LOCK_NUMBER, tr("Lock")); + m_model->addColumn(COL_DATA, tr("Data"));//.setToolTip(tr("Locked for drawing")); + m_model->addColumn(COL_ORIG_DATA, tr("Orig data"));//.setToolTip(tr("Locked for drawing")); ui->tableView->setTableModel(m_model); - showMessage({}); setEnabled(false); - //connect(ui->btReload, &QAbstractButton::clicked, this, &QxLateRegistrationsWidget::reload); - //connect(ui->btResizeColumns, &QAbstractButton::clicked, this, &QxLateRegistrationsWidget::resizeColumns); - //connect(ui->btApply, &QAbstractButton::clicked, this, &QxLateRegistrationsWidget::applyCurrentChange); - auto *svc = service(); connect(svc, &Service::statusChanged, this, [this](Service::Status new_status){ switch (new_status) { @@ -65,6 +94,41 @@ QxLateRegistrationsWidget::QxLateRegistrationsWidget(QWidget *parent) : }); connect(getPlugin(), &Event::EventPlugin::dbEventNotify, this, &QxLateRegistrationsWidget::onDbEventNotify, Qt::QueuedConnection); + + { + auto *lst = ui->lstType; + lst->addItem("All"); + lst->addItem("RunUpdateRequest"); + lst->addItem("RunUpdated"); + lst->addItem("OcChange"); + lst->addItem("RadioPunch"); + lst->addItem("CardReadout"); + lst->setCurrentIndex(0); + connect(lst, &QComboBox::currentIndexChanged, this, &QxLateRegistrationsWidget::reload); + } + connect(ui->chkNull, &QCheckBox::checkStateChanged, this, &QxLateRegistrationsWidget::reload); + connect(ui->chkPending, &QCheckBox::checkStateChanged, this, &QxLateRegistrationsWidget::reload); + connect(ui->chkLocked, &QCheckBox::checkStateChanged, this, &QxLateRegistrationsWidget::reload); + connect(ui->chkAccepted, &QCheckBox::checkStateChanged, this, &QxLateRegistrationsWidget::reload); + connect(ui->chkRejected, &QCheckBox::checkStateChanged, this, &QxLateRegistrationsWidget::reload); + + connect(ui->btAll, &QPushButton::clicked, this, [this]() { + QSignalBlocker sb1(ui->lstType); + ui->lstType->setCurrentIndex(0); + + QSignalBlocker sb2(ui->chkNull); + ui->chkNull->setChecked(true); + QSignalBlocker sb3(ui->chkPending); + ui->chkPending->setChecked(true); + QSignalBlocker sb4(ui->chkLocked); + ui->chkLocked->setChecked(true); + QSignalBlocker sb5(ui->chkAccepted); + ui->chkAccepted->setChecked(true); + QSignalBlocker sb6(ui->chkRejected); + ui->chkRejected->setChecked(true); + + reload(); + }); } QxLateRegistrationsWidget::~QxLateRegistrationsWidget() @@ -132,7 +196,30 @@ void QxLateRegistrationsWidget::reload() qb.select2("qxchanges", "*") .from("qxchanges") .where("stage_id=" + QString::number(stage_id)) - .orderBy("id");//.limit(10); + .orderBy("id"); + QStringList status_cond_list; + if (ui->chkNull->isChecked()) { + status_cond_list << "status IS NULL"; + } + if (ui->chkPending->isChecked()) { + status_cond_list << "status='Pending'"; + } + if (ui->chkLocked->isChecked()) { + status_cond_list << "status LIKE 'Locked%'"; + } + if (ui->chkAccepted->isChecked()) { + status_cond_list << "status='Accepted'"; + } + if (ui->chkRejected->isChecked()) { + status_cond_list << "status='Rejected'"; + } + if (!status_cond_list.isEmpty()) { + qb.where('(' + status_cond_list.join(" OR ") + ')'); + } + if (auto ix = ui->lstType->currentIndex(); ix > 0) { + auto data_type = ui->lstType->currentText(); + qb.where(QStringLiteral("data_type='%1'").arg(data_type)); + } qfDebug() << qb.toString(); m_model->setQueryBuilder(qb, false); m_model->reload(); @@ -140,10 +227,20 @@ void QxLateRegistrationsWidget::reload() void QxLateRegistrationsWidget::addQxChangeRow(int sql_id) { - qfDebug() << "reloading change id:" << sql_id << "col id:" << COL_ID; + qfDebug() << "reloading qxchanges row id:" << sql_id << "col id:" << COL_ID; if(sql_id <= 0) { return; } + + auto qb = m_model->queryBuilder(); + qb.where(QStringLiteral("id=%1").arg(sql_id)); + qf::core::sql::Query q; + q.execThrow(qb.toString()); + if (!q.next()) { + // inserted row is filtered out + return; + } + m_model->insertRow(0); m_model->setValue(0, COL_ID, sql_id); int cnt = m_model->reloadRow(0); @@ -170,5 +267,41 @@ void QxLateRegistrationsWidget::applyCurrentChange() } +void QxLateRegistrationsWidget::onTableCustomContextMenuRequest(const QPoint &pos) +{ + QAction a_neco(tr("Neco"), nullptr); + QList lst; + lst << &a_neco; + QAction *a = QMenu::exec(lst, ui->tableView->viewport()->mapToGlobal(pos)); + if(a == &a_neco) { + //printSelectedCards(); + } +} + +void QxLateRegistrationsWidget::onTableDoubleClicked(const QModelIndex &ix) +{ + auto row = ix.row(); + auto data_type = m_model->value(row, COL_DATA_TYPE).toString(); + if (data_type != DATA_TYPE_RUN_UPDATE_REQUEST) { + return; + } + auto source = m_model->value(row, COL_SOURCE).toString(); + if (source != SOURCE_WWW) { + return; + } + auto change_id = m_model->value(row, COL_CHANGE_ID).toInt(); + auto status = m_model->value(row, COL_STATUS).toString(); + auto lock_number = m_model->value(row, COL_LOCK_NUMBER).toInt(); + auto data_id = m_model->value(row, COL_DATA_ID).toInt(); + auto data = m_model->value(row, COL_DATA).toString().toUtf8(); + auto change_rec = QJsonDocument::fromJson(data).toVariant().toMap(); + auto run_change = RunChange::fromVariantMap(change_rec.value(DATA_TYPE_RUN_UPDATE_REQUEST).toMap()); + if (status == STATUS_PENDING || status == STATUS_LOCKED) { + RunChangeDialog dlg(change_id, data_id, lock_number, run_change, this); + dlg.exec(); + ui->tableView->reloadRow(row); + } +} + } diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.h index c5bfc3233..e6b15317a 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.h @@ -30,6 +30,10 @@ class QxLateRegistrationsWidget : public QWidget void resizeColumns(); void showMessage(const QString &msg, bool is_error = false); void applyCurrentChange(); + + void onTableCustomContextMenuRequest(const QPoint &pos); + void onTableDoubleClicked(const QModelIndex &ix); + private: Ui::QxLateRegistrationsWidget *ui; qf::core::model::SqlTableModel *m_model; diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.ui b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.ui index 31923ae79..a1ebf08eb 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.ui +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 733 + 331 @@ -28,7 +28,97 @@ - + + + + + + 0 + 0 + + + + + + + + All + + + + + + + Type + + + + + + + + + + Pending + + + true + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Locked + + + + + + + Accepted + + + + + + + Rejected + + + + + + + Null + + + + + + + diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchange.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchange.cpp new file mode 100644 index 000000000..c9a84e23b --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchange.cpp @@ -0,0 +1,41 @@ +#include "runchange.h" + +#include +#include + +namespace Event::services::qx { + +// RunChange RunChange::fromJsonString(const QString &json) +// { +// RunChange ret; +// return ret; +// } + +RunChange RunChange::fromVariantMap(const QVariantMap &map) +{ + RunChange ret; + + if (auto v = map.value("class_name"); v.isValid()) { ret.class_name = v.toString(); } + if (auto v = map.value("first_name"); v.isValid()) { ret.first_name = v.toString(); } + if (auto v = map.value("last_name"); v.isValid()) { ret.last_name = v.toString(); } + if (auto v = map.value("registration"); v.isValid()) { ret.registration = v.toString(); } + if (auto v = map.value("si_id"); v.isValid()) { ret.si_id = v.toInt(); } + // if (auto v = map.value("si_id_rent"); v.isValid()) { ret.si_id_rent = v.toBool(); } + ret.note = map.value("note").toString(); + + return ret; +} + +QVariantMap OrigRunRecord::toVariantMap() const +{ + QVariantMap ret; + ret["first_name"] = first_name; + ret["last_name"] = last_name; + ret["registration"] = registration; + ret["si_id"] = si_id; + return ret; +} + +} // namespace Event::services::qx + + diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchange.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchange.h new file mode 100644 index 000000000..83ab57062 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchange.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + +namespace Event::services::qx { + +struct RunChange +{ + std::optional class_name; + std::optional first_name; + std::optional last_name; + std::optional registration; + std::optional si_id; + // std::optional si_id_rent; + QString note; + + // static RunChange fromJsonString(const QString &json); + static RunChange fromVariantMap(const QVariantMap &map); +}; + +struct OrigRunRecord +{ + QString first_name; + QString last_name; + QString registration; + int si_id; + // bool si_id_rent; + + QVariantMap toVariantMap() const; +}; + +enum class DataType { + OcChange, + RunUpdateRequest, + RunUpdated, + RadioPunch, + CardReadout, +}; +enum class ChangeStatus { + Pending, + Locked, + Accepted, + Rejected, +}; +struct EventChange +{ + int64_t id; + QString source; + DataType data_type; + int64_t data_id; + QVariant data; + QString user_id; + ChangeStatus status; + QString status_message; + QDateTime created; + int64_t lock_number; + + QVariantMap toVariantMap() const; +}; + +// struct ChangeRecord +// { +// int64_t id; +// QString source; +// QString data_type, +// int64_t data_id; +// data: ChangeData, +// user_id: Option, +// status: Option, +// created: QxDateTime, +// lock_number: Option, +// }; + +} // namespace Event::services::qx + + + diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.cpp new file mode 100644 index 000000000..9b0988b3a --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.cpp @@ -0,0 +1,301 @@ +#include "runchangedialog.h" +#include "ui_runchangedialog.h" + +#include "qxclientservice.h" +#include "runchange.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace qf::core::model; + +namespace Event::services::qx { + +RunChangeDialog::RunChangeDialog(int change_id, int run_id, int lock_number, const RunChange &run_change, QWidget *parent) + : QDialog(parent) + , ui(new Ui::RunChangeDialog) + , m_changeId(change_id) + , m_runId(run_id) +{ + ui->setupUi(this); + + ui->btAccept->setDisabled(true); + ui->btReject->setDisabled(true); + + connect(ui->chkForce, &QCheckBox::checkStateChanged, this, [this]() { + ui->btAccept->setDisabled(!ui->chkForce->isChecked()); + ui->btReject->setDisabled(!ui->chkForce->isChecked()); + }); + connect(ui->btAccept, &QPushButton::clicked, this, [this]() { + resolveChanges(true); + }); + connect(ui->btReject, &QPushButton::clicked, this, [this]() { + resolveChanges(false); + }); + + ui->edClassName->setText(run_change.class_name.value_or(QString())); + ui->edNote->setText(run_change.note); + + ui->edRunId->setValue(run_id); + ui->edChangeId->setValue(change_id); + ui->edLockNumber->setValue(lock_number); + + ui->grpFirstName->setChecked(run_change.first_name.has_value()); + ui->edFirstName->setText(run_change.first_name.has_value()? run_change.first_name.value(): QString()); + + ui->grpLastName->setChecked(run_change.last_name.has_value()); + ui->edLastName->setText(run_change.last_name.has_value()? run_change.last_name.value(): QString()); + + ui->grpRegistration->setChecked(run_change.registration.has_value()); + ui->edRegistration->setText(run_change.registration.has_value()? run_change.registration.value(): QString()); + + ui->grpSiCard->setChecked(run_change.si_id.has_value()); + ui->edSiCard->setValue(run_change.si_id.has_value()? run_change.si_id.value(): 0); + + if (run_id > 0) { + loadOrigValues(); + } + else { + loadClassId(); + } + lockChange(); +} + +RunChangeDialog::~RunChangeDialog() +{ + delete ui; +} + +QxClientService *RunChangeDialog::service() +{ + auto *svc = qobject_cast(Service::serviceByName(QxClientService::serviceId())); + Q_ASSERT(svc); + return svc; +} + +void RunChangeDialog::setMessage(const QString &msg, bool error) +{ + if (msg.isEmpty()) { + ui->lblError->setStyleSheet({}); + } + else if (error) { + ui->lblError->setStyleSheet("background: salmon"); + } + else { + ui->lblError->setStyleSheet({}); + } + ui->lblError->setText(msg); +} + +void RunChangeDialog::loadOrigValues() +{ + Q_ASSERT(m_runId > 0); + + qf::core::sql::Query q; + q.execThrow(QStringLiteral("SELECT classes.name AS class_name, competitors.id AS competitor_id" + " FROM runs" + " JOIN competitors ON competitors.id=runs.competitorId AND runs.id=%1" + " LEFT JOIN classes ON competitors.classId=classes.id") + .arg(m_runId) + ); + if (q.next()) { + ui->edClassName->setText(q.value("class_name").toString()); + m_competitorId = q.value("competitor_id").toInt(); + } + + Competitors::CompetitorDocument doc; + doc.load(m_competitorId, DataDocument::ModeView); + + m_origValues.first_name = doc.value("firstName").toString(); + m_origValues.last_name = doc.value("lastName").toString(); + m_origValues.registration = doc.value("registration").toString(); + m_origValues.si_id = doc.value("siId").toInt(); + + ui->edFirstNameOrig->setText(m_origValues.first_name); + ui->edLastNameOrig->setText(m_origValues.last_name); + ui->edRegistrationOrig->setText(m_origValues.registration); + ui->edSiCardOrig->setValue(m_origValues.si_id); +} + +void RunChangeDialog::loadClassId() +{ + auto class_name = ui->edClassName->text(); + if (class_name.isEmpty()) { + return; + } + qf::core::sql::Query q; + q.execThrow(QStringLiteral("SELECT id" + " FROM classes" + " WHERE name='%1'") + .arg(class_name) + ); + if (q.next()) { + m_classId = q.value("id").toInt(); + } +} + +void RunChangeDialog::lockChange() +{ + // NIY + // auto *svc = service(); + // auto *nm = svc->networkManager(); + + // auto path = QStringLiteral("/api/event/%1/changes").arg(svc->eventId()); + // QUrlQuery query; + // query.addQueryItem("from_id", QString::number(m_changeId)); + // query.addQueryItem("limit", QString::number(1)); + // svc->getHttpJson(path, query, this, [this](auto data, auto error) { + // if (!error.isEmpty()) { + // setMessage(error, true); + // return; + // } + // auto rec = data.toList().value(0).toMap(); + // }); + + + // QNetworkRequest request; + // auto url = svc->exchangeServerUrl(); + // // qfInfo() << "url " << url.toString(); + // url.setPath("/api/event/current/changes/lock-change"); + + // QUrlQuery query; + // query.addQueryItem("change_id", QString::number(m_changeId)); + // auto connection_id = QxClientService::currentConnectionId(); + // query.addQueryItem("lock_number", QString::number(connection_id)); + // url.setQuery(query); + // qfInfo() << "GET " << url.toString(); + + // request.setUrl(url); + // request.setRawHeader(QxClientService::QX_API_TOKEN, svc->apiToken()); + // auto *reply = nm->get(request); + // connect(reply, &QNetworkReply::finished, this, [this, reply, connection_id]() { + // auto data = reply->readAll(); + // if (reply->error() == QNetworkReply::NetworkError::NoError) { + // m_lockNumber = data.toInt(); + // ui->edLockNumber->setValue(m_lockNumber); + // if (m_lockNumber == connection_id) { + // ui->btAccept->setDisabled(false); + // ui->btReject->setDisabled(false); + + // qf::core::sql::Query q; + // q.execThrow(QStringLiteral("UPDATE qxchanges SET lock_number=%1, status='Locked' WHERE id=%2") + // .arg(connection_id) + // .arg(m_changeId) + // ); + // } + // else { + // setMessage(tr("Change is locked already by other client: %1, current client id:.%2").arg(m_lockNumber).arg(connection_id), false); + // } + // } + // else { + // setMessage(tr("Lock change error: %1\n%2").arg(reply->errorString()).arg(QString::fromUtf8(data)), true); + // } + // reply->deleteLater(); + // }); +} + +void RunChangeDialog::resolveChanges(bool is_accepted) +{ + if (is_accepted) { + applyLocalChanges(is_accepted); + } + auto *svc = service(); + auto *nm = svc->networkManager(); + QNetworkRequest request; + auto url = svc->exchangeServerUrl(); + // qfInfo() << "url " << url.toString(); + url.setPath("/api/event/current/changes/resolve-change"); + + QUrlQuery query; + query.addQueryItem("change_id", QString::number(m_changeId)); + query.addQueryItem("lock_number", QString::number(m_lockNumber)); + query.addQueryItem("accepted", is_accepted? "true": "false"); + query.addQueryItem("status_message", ui->edStatusMessage->text()); + url.setQuery(query); + + request.setUrl(url); + request.setRawHeader(QxClientService::QX_API_TOKEN, svc->apiToken()); + auto *reply = nm->get(request); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + if (reply->error() == QNetworkReply::NetworkError::NoError) { + accept(); + } + else { + QMessageBox::warning(this, + QCoreApplication::applicationName(), + tr("Update change error: %1").arg(reply->errorString())); + } + reply->deleteLater(); + }); +} + +void RunChangeDialog::applyLocalChanges(bool is_accepted) +{ + bool is_insert = m_runId == 0; + Competitors::CompetitorDocument doc; + doc.load(m_competitorId, is_insert? DataDocument::ModeInsert: DataDocument::ModeEdit); + if (is_insert) { + doc.setValue("classId", m_classId); + } + if (ui->grpFirstName->isChecked()) { + doc.setValue("firstName", ui->edFirstName->text()); + } + if (ui->grpLastName->isChecked()) { + doc.setValue("lastName", ui->edLastName->text()); + } + if (ui->grpRegistration->isChecked()) { + doc.setValue("registration", ui->edRegistration->text()); + } + if (ui->grpSiCard->isChecked()) { + if (is_insert) { + doc.setValue("siId", ui->edSiCard->value()); + } + else { + qf::core::sql::Query q; + q.execThrow(QStringLiteral("UPDATE runs SET siId=%1 WHERE id=%2") + .arg(ui->edSiCard->value()) + .arg(m_runId) + ); + } + } + doc.save(); + { + qf::core::sql::Query q; + q.execThrow(QStringLiteral("UPDATE qxchanges SET status='%1' WHERE id=%2") + .arg(is_accepted? "Accepted": "Rejected") + .arg(m_changeId) + ); + } + { + auto dc_str = qf::core::Utils::qvariantToJson(m_origValues.toVariantMap()); + QString qs = "UPDATE qxchanges SET orig_data=:orig_data WHERE id=:id"; + qf::core::sql::Query q; + q.prepare(qs, qf::core::Exception::Throw); + q.bindValue(":orig_data", dc_str); + q.bindValue(":id", m_changeId); + q.exec(qf::core::Exception::Throw); + } +} + +bool RunChangeDialog::checkHttpError(QNetworkReply *reply) +{ + if (reply->error() != QNetworkReply::NetworkError::NoError) { + setMessage(tr("Http error: %1\n%2") + .arg(reply->request().url().toString()) + .arg(reply->errorString()), true); + return false; + } + return true; +} + +} // namespace Event::services::qx + + diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.h new file mode 100644 index 000000000..d704a1d48 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.h @@ -0,0 +1,54 @@ +#ifndef RUNCHANGEDIALOG_H +#define RUNCHANGEDIALOG_H + +#include "runchange.h" + +#include + +class QNetworkReply; + +namespace Event::services::qx { + +namespace Ui { +class RunChangeDialog; +} + +struct RunChange; +class QxClientService; + +class RunChangeDialog : public QDialog +{ + Q_OBJECT + +public: + explicit RunChangeDialog(int change_id, int run_id, int lock_number, const RunChange &run_change, QWidget *parent = nullptr); + ~RunChangeDialog() override; + +private: + QxClientService* service(); + void setMessage(const QString &msg, bool error); + + void loadOrigValues(); + void loadClassId(); + + void lockChange(); + + void resolveChanges(bool is_accepted); + void applyLocalChanges(bool is_accepted); + + bool checkHttpError(QNetworkReply *reply); +private: + Ui::RunChangeDialog *ui; + int m_changeId = 0; + int m_runId = 0; + int m_competitorId = 0; + int m_classId = 0; + int m_lockNumber = 0; + OrigRunRecord m_origValues; +}; + + +} // namespace Event::services::qx + + +#endif // RUNCHANGEDIALOG_H diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.ui b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.ui new file mode 100644 index 000000000..c18f157b3 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.ui @@ -0,0 +1,430 @@ + + + Event::services::qx::RunChangeDialog + + + + 0 + 0 + 712 + 569 + + + + Dialog + + + + + + + + Class + + + + + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Run ID + + + + + + + + 0 + 0 + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + true + + + None + + + 999999999 + + + + + + + Change ID + + + + + + + + 0 + 0 + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + true + + + None + + + 999999999 + + + + + + + Lock number + + + + + + + + 0 + 0 + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + true + + + None + + + 999999999 + + + + + + + + + First name + + + true + + + + + + + + + -> + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + + + + + Last name + + + true + + + + + + + + + -> + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + + + + + Registration + + + true + + + + + + + + + -> + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + + + + + SI Card + + + true + + + + + + + 0 + 0 + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + None + + + 999999999 + + + + + + + Rent + + + + + + + -> + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + 0 + 0 + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + None + + + 999999999 + + + + + + + Rent + + + + + + + + + + + + Note + + + + + + + true + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + Message + + + + + + + + + + + + + + Force + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Reject + + + + + + + Accept + + + + + + + + + edClassName + edRunId + edChangeId + edLockNumber + grpFirstName + edFirstNameOrig + edFirstName + grpLastName + edLastNameOrig + edLastName + grpRegistration + edRegistrationOrig + edRegistration + grpSiCard + edSiCardOrig + edSiCardRentOrig + edSiCard + edSiCardRent + edNote + edStatusMessage + chkForce + btAccept + btReject + btCancel + + + + + btCancel + clicked() + Event::services::qx::RunChangeDialog + reject() + + + 500 + 533 + + + 332 + 539 + + + + + diff --git a/quickevent/app/quickevent/plugins/Event/src/services/service.h b/quickevent/app/quickevent/plugins/Event/src/services/service.h index c73260dba..78856e6f6 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/service.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/service.h @@ -55,12 +55,12 @@ class Service : public QObject Q_SIGNAL void statusChanged(Status new_status); void setRunning(bool on); + bool isRunning() const { return status() == Status::Running; } static void addService(Service *service); static int serviceCount() {return m_services.count();} static Service* serviceAt(int ix); static Service* serviceByName(const QString &service_name); - //Q_SIGNAL void serviceCountChanged(int new_count); protected: virtual void loadSettings(); QString settingsGroup() const; diff --git a/quickevent/app/quickevent/plugins/Runs/src/eventstatisticswidget.cpp b/quickevent/app/quickevent/plugins/Runs/src/eventstatisticswidget.cpp index 3410511aa..65db600d7 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/eventstatisticswidget.cpp +++ b/quickevent/app/quickevent/plugins/Runs/src/eventstatisticswidget.cpp @@ -23,9 +23,8 @@ #include #include #include +#include -namespace qfs = qf::core::sql; -namespace qfu = qf::core::utils; using qf::qmlwidgets::framework::getPlugin; using Event::EventPlugin; using Runs::RunsPlugin; @@ -259,10 +258,10 @@ QVariant FooterModel::headerData(int section, Qt::Orientation orientation, int r if(role == Qt::DisplayRole) { return m_columnSums.value(section); } - else if(role == Qt::TextAlignmentRole) { + if(role == Qt::TextAlignmentRole) { return Qt::AlignRight; } - else if(role == Qt::BackgroundRole) { + if(role == Qt::BackgroundRole) { return QColor("khaki"); } } @@ -357,9 +356,7 @@ FooterView::FooterView(QTableView *table_view, QWidget *parent) } } -FooterView::~FooterView() -{ -} +FooterView::~FooterView() = default; void FooterView::resetFooterAttributes() { @@ -408,6 +405,12 @@ EventStatisticsWidget::EventStatisticsWidget(QWidget *parent) connect(getPlugin(), &Event::EventPlugin::dbEventNotify, this, &EventStatisticsWidget::onDbEventNotify, Qt::QueuedConnection); connect(getPlugin(), &Event::EventPlugin::currentStageIdChanged, this, &EventStatisticsWidget::reloadDeferred); + connect(ui->btReload, &QPushButton::clicked, this, &EventStatisticsWidget::onReloadClicked); + connect(ui->btOptions, &QPushButton::clicked, this, &EventStatisticsWidget::onOptionsClicked); + connect(ui->btPrintResultsSelected, &QPushButton::clicked, this, &EventStatisticsWidget::onPrintResultsSelectedClicked); + connect(ui->btPrintResultsNew, &QPushButton::clicked, this, &EventStatisticsWidget::onPrintResultsNewClicked); + connect(ui->btClearNewInSelectedRows, &QPushButton::clicked, this, &EventStatisticsWidget::onClearNewInSelectedRowsClicked); + initAutoRefreshTimer(); } @@ -518,13 +521,13 @@ void EventStatisticsWidget::initAutoRefreshTimer() } } -void EventStatisticsWidget::on_btReload_clicked() +void EventStatisticsWidget::onReloadClicked() { qfLogFuncFrame(); reload(); } -void EventStatisticsWidget::on_btPrintResultsSelected_clicked() +void EventStatisticsWidget::onPrintResultsSelectedClicked() { QList rows; for(int i : ui->tableView->selectedRowsIndexes()) @@ -580,7 +583,7 @@ void EventStatisticsWidget::printResultsForRows(const QList &rows) } } -void EventStatisticsWidget::on_btClearNewInSelectedRows_clicked() +void EventStatisticsWidget::onClearNewInSelectedRowsClicked() { qfLogFuncFrame(); QList classdefs_ids; @@ -621,7 +624,7 @@ QVariantMap EventStatisticsWidget::options() return m; } -void EventStatisticsWidget::on_btOptions_clicked() +void EventStatisticsWidget::onOptionsClicked() { EventStatisticsOptions dlg(this); if(dlg.exec()) { @@ -629,7 +632,7 @@ void EventStatisticsWidget::on_btOptions_clicked() } } -void EventStatisticsWidget::on_btPrintResultsNew_clicked() +void EventStatisticsWidget::onPrintResultsNewClicked() { qfLogFuncFrame(); reload(); diff --git a/quickevent/app/quickevent/plugins/Runs/src/eventstatisticswidget.h b/quickevent/app/quickevent/plugins/Runs/src/eventstatisticswidget.h index d83b3ca20..5d57d8c97 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/eventstatisticswidget.h +++ b/quickevent/app/quickevent/plugins/Runs/src/eventstatisticswidget.h @@ -27,12 +27,12 @@ class EventStatisticsWidget : public QWidget void loadPersistentSettings(); void savePersistentSettings(); -private slots: - void on_btReload_clicked(); - void on_btOptions_clicked(); - void on_btPrintResultsSelected_clicked(); - void on_btPrintResultsNew_clicked(); - void on_btClearNewInSelectedRows_clicked(); +private: + void onReloadClicked(); + void onOptionsClicked(); + void onPrintResultsSelectedClicked(); + void onPrintResultsNewClicked(); + void onClearNewInSelectedRowsClicked(); private: int currentStageId(); void initAutoRefreshTimer(); diff --git a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp index 01fa69ed8..0124b0635 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp +++ b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp @@ -421,6 +421,59 @@ int RunsPlugin::competitorForRun(int run_id) return competitor_id; } +QVariantList RunsPlugin::qxExportRunsCsvJson(int stage_id) +{ + QVariantList csv; + auto append_list = [&csv](QVariantList lst) { + csv.insert(csv.size(), lst); + }; + enum Column {run_id = 0, class_name, registration, si_id, first_name, last_name, start_time, check_time, finish_time, status, COUNT}; + constexpr std::array colNames = { + "run_id", "class_name", "registration", "si_id", "first_name", "last_name", "start_time", "check_time", "finish_time", "status" + }; + auto col_name = [&colNames](Column col) { return colNames.at(col); }; + { + QVariantList cols; + for (auto i = 0; i < COUNT; ++i) { + cols << col_name(static_cast(i)); + } + append_list(cols); + } + QDateTime start00 = getPlugin()->stageStartDateTime(stage_id); + auto msec_to_date_time = [start00](const QVariant &msec) { + if (msec.isNull()) { + return QVariant(); + } + auto dt = start00.addMSecs(msec.toInt()); + return QVariant::fromValue(dt); + }; + qfs::QueryBuilder qb; + qb.select2("runs", "*") + .select2("competitors", "registration, licence, lastName, firstName") + .select2("classes", "name") + .from("runs") + .innerJoinRestricted("runs.competitorId", "competitors.id", "runs.stageId=" QF_IARG(stage_id) " AND runs.isRunning") + .join("competitors.classId", "classes.id"); + qfs::Query q; + q.execThrow(qb.toString()); + while (q.next()) { + QVariantList rec(COUNT); + rec[run_id] = q.value("runs.id"); + rec[class_name] = q.value("classes.name"); + rec[registration] = q.value("registration"); + rec[si_id] = q.value("runs.siId"); + rec[first_name] = q.value("firstName"); + rec[last_name] = q.value("lastName"); + rec[start_time] = msec_to_date_time(q.value("startTimeMs")); + rec[check_time] = msec_to_date_time(q.value("checkTimeMs")); + rec[finish_time] = msec_to_date_time(q.value("finishTimeMs")); + auto run_status = quickevent::core::RunStatus::fromQuery(q); + rec[status] = run_status.toHtmlExportString(); + append_list(rec); + } + return csv; +} +/* QString RunsPlugin::qxExportRunsCsv(int stage_id) { QString csv; @@ -489,7 +542,7 @@ QString RunsPlugin::qxExportRunsCsv(int stage_id) return csv; } - +*/ qf::core::utils::Table RunsPlugin::nstagesClassResultsTable(int stages_count, int class_id, int places, bool exclude_disq) { qfs::QueryBuilder qb; @@ -815,15 +868,16 @@ qf::core::utils::TreeTable RunsPlugin::addLapsToStageResultsTable(int course_id, int stp = q.value(3).toInt(); if(stp <= 0) continue; - stp_times[pos][run_id] = RunStp{run_id, stp}; + stp_times[pos][run_id] = RunStp{.runId=run_id, .time=stp}; int lap = q.value(4).toInt(); if(lap <= 0) continue; - lap_times[pos][run_id] = RunStp{run_id, lap}; + lap_times[pos][run_id] = RunStp{.runId=run_id, .time=lap}; } } auto make_pos = [](QMap ×) { - for(int pos : times.keys()) { + const auto control_positions = times.keys(); + for(int pos : control_positions) { RunStpMap &map = times[pos]; QList lst = map.values(); std::sort(lst.begin(), lst.end(), [](const RunStp &a, const RunStp &b) { return a.time < b.time; }); @@ -843,10 +897,10 @@ qf::core::utils::TreeTable RunsPlugin::addLapsToStageResultsTable(int course_id, const RunStp &stprun = stps.value(run_id); const RunStpMap &laps = lap_times.value(j+1); const RunStp &laprun = laps.value(run_id); - tt_row.setValue(col_stp_time0_ix + 4*j + 0, stprun.time); - tt_row.setValue(col_stp_time0_ix + 4*j + 1, stprun.pos); - tt_row.setValue(col_stp_time0_ix + 4*j + 2, laprun.time); - tt_row.setValue(col_stp_time0_ix + 4*j + 3, laprun.pos); + tt_row.setValue(col_stp_time0_ix + (4 * j) + 0, stprun.time); + tt_row.setValue(col_stp_time0_ix + (4 * j) + 1, stprun.pos); + tt_row.setValue(col_stp_time0_ix + (4 * j) + 2, laprun.time); + tt_row.setValue(col_stp_time0_ix + (4 * j) + 3, laprun.pos); } tt.setRow(i, tt_row); } @@ -1091,9 +1145,9 @@ static QString make_width(const QString &s, int width) return ret; } -void RunsPlugin::writeCSOSHeader(QTextStream &ts) +void RunsPlugin::writeCSOSHeader(QTextStream &ts) const { - Event::EventPlugin *evp = getPlugin(); + auto *evp = getPlugin(); int stage_id = selectedStageId(); QDateTime start_dt = evp->stageStartDateTime(stage_id); Event::EventConfig *ec = evp->eventConfig(); @@ -2158,7 +2212,6 @@ void RunsPlugin::export_startListClubsHtml() if(QDir().mkpath(file_name)) { QString default_file_name = "startlist-clubs.html"; file_name += "/" + default_file_name; - QVariantMap options; qf::core::utils::HtmlUtils::FromHtmlListOptions opts; opts.setDocumentTitle(tr("Start list by clubs")); QString str = qf::core::utils::HtmlUtils::fromHtmlList(body, opts); @@ -2271,7 +2324,6 @@ QString RunsPlugin::export_resultsHtmlStage(bool with_laps) fwk->hideProgress(); if(QDir().mkpath(file_dir)) { QString file_name = file_dir + "/results.html"; - QVariantMap options; qf::core::utils::HtmlUtils::FromHtmlListOptions opts; opts.setDocumentTitle(tr("Stage results")); QString str = qf::core::utils::HtmlUtils::fromHtmlList(body, opts); @@ -2463,7 +2515,7 @@ void RunsPlugin::exportResultsHtmlStageWithLaps(const QString &laps_file_name, c QVariantList{"th", QVariantMap{{QStringLiteral("class"), "brb"}}, tr("Loss")}, }; int i = 1; - for(QVariant v : course_codes) { + for(const auto &_ : course_codes) { append_list(trr, QVariantList{"th" , QVariantMap{{"class", "br"}, {"colspan", "2"}} , (i < course_codes.size())? QVariant(i++): tr("FIN")}); @@ -2478,7 +2530,7 @@ void RunsPlugin::exportResultsHtmlStageWithLaps(const QString &laps_file_name, c QVariantList{"th", QVariantMap{{QStringLiteral("class"), "brb bbb"}}, "\u00A0"}, }; int i = 1; - for(QVariant v : course_codes) { + for(const auto &v : course_codes) { append_list(trr, QVariantList{"th", QVariantMap{{QStringLiteral("class"), "br bbb"}, {"colspan", "2"}}, (i++ < course_codes.size())? QVariant(quickevent::core::CodeDef{v.toMap()}.code()): QVariant("\u00A0")}); diff --git a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.h b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.h index 6948886e3..ef2e1f65b 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.h +++ b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.h @@ -78,7 +78,8 @@ class RunsPlugin : public qf::qmlwidgets::framework::Plugin bool exportStartListCurrentStageCsvSime(const QString &file_name, bool bibs, QString sql_where); bool exportStartListCurrentStageTvGraphics(const QString &file_name); - QString qxExportRunsCsv(int stage_id); + QVariantList qxExportRunsCsvJson(int stage_id); + // QString qxExportRunsCsv(int stage_id); //bool exportResultsHtmlStage(int stage_id, const QString &file_name); Q_INVOKABLE bool exportResultsIofXml30Stage(int stage_id, const QString &file_name); @@ -133,7 +134,7 @@ class RunsPlugin : public qf::qmlwidgets::framework::Plugin int courseForRun_Relays(int run_id); QString getClubAbbrFromName(QString name); - void writeCSOSHeader(QTextStream &ts); + void writeCSOSHeader(QTextStream &ts) const; void addStartTimeTextToClass(qf::core::utils::TreeTable &tt2, const qint64 start00_epoch_sec, const quickevent::gui::ReportOptionsDialog::StartTimeFormat start_time_format); void addStartTimeTextToClass(qf::core::utils::TreeTable &tt2, const int stages_count, const QVector &start00_epoch_sec, const quickevent::gui::ReportOptionsDialog::StartTimeFormat start_time_format); diff --git a/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.cpp b/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.cpp index 768764f91..a51609545 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.cpp +++ b/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.cpp @@ -40,7 +40,7 @@ RunsTableModel::RunsTableModel(QObject *parent) setColumn(col_runs_penaltyTimeMs, ColumnDefinition("runs.penaltyTimeMs", tr("Penalty")).setCastType(qMetaTypeId())); setColumn(col_runFlags, ColumnDefinition("runFlags", tr("Run flags")).setReadOnly(true)); setColumn(col_cardFlags, ColumnDefinition("cardFlags", tr("Card flags")).setReadOnly(true)); - setColumn(col_runs_rankingPos, ColumnDefinition("ranking", tr("Ranking pos")).setToolTip(tr("Runner's position in CZ ranking.")).setReadOnly(true)); + setColumn(col_runs_rankingPos, ColumnDefinition("ranking", tr("Ranking pos")).setToolTip(tr("Runner's position in CZ ranking.")).setReadOnly(false)); setColumn(col_iofId, ColumnDefinition("iofId", tr("IOF ID")).setReadOnly(true)); setColumn(col_competitors_note, ColumnDefinition("competitors.note", tr("Note"))); diff --git a/quickevent/app/quickevent/src/appversion.h b/quickevent/app/quickevent/src/appversion.h index d4474fcb4..206208c9d 100644 --- a/quickevent/app/quickevent/src/appversion.h +++ b/quickevent/app/quickevent/src/appversion.h @@ -1,4 +1,4 @@ #pragma once -#define APP_VERSION "3.4.3" +#define APP_VERSION "3.4.5"