ores.cpp.qt.detail_dialog_impl

Table of Contents

Loads the generated .ui; field get/set per widget kind. Qt UI component: model/view class or dialog wired to the service layer via request/response messages.

See the Template variable reference for the complete list of available variables and their semantics.

Extension points

<<paste:UUID>> markers in this template are paste-block injection points — see paste blocks: injecting custom code into generated files for the full mechanism, and its Qt-specific table for the complete list. This template defines eight:

  • 3F8B6C1D-4E2A-4F9B-8C3D-1A5E7F2B9D4C — top of the file, after the generated #include lines. Extra includes any of the other four seams below need.
  • B6F2748A-9896-4B9C-BEAC-38436FD1CA79 — end of setupConnections(). Extra connect() wiring, e.g. a derived field kept live-synced from the fields it's computed from.
  • 9A719585-3AB1-47DE-B0B0-99AFDFE7180E — end of setCreateMode(), after the generated locked_fields loop. Runs last, so a paste block can override what the generated per-field lock logic just did — e.g. replace a setEnabled(false)-based lock with WidgetUtils::set_combo_locked() for a field that should stay visually normal, flag icon included, while locked.
  • 7A4AE8D5-A22D-4912-B4BF-D58215681CD2 — end of setReadOnly(), after the generated is_locked_after_create loop. Same rationale as the setCreateMode() seam above.
  • EC97923E-BC8D-4A50-9970-CBC5CBD4B732 — near the end of the file, after the generated methods. Implementation bodies for whatever the header seam (F55CB64E-165A-4E15-A50A-5723C0320E97 in ores.cpp.qt.detail_dialog_header) declared.
  • 69F22F14-6701-4E73-AFCC-78AC1B1FDDDF — inside validateInput()'s return chain, after the generated required-field checks, before the closing ;. Extra && (...) conditions, e.g. cross-field validation the generic required-field mechanism can't express (two fields must differ, a combination of values is invalid together).
  • 4E83FA7A-742A-4102-90AF-D337F6FB1269 — inside onSaveClicked(), after validateInput() passes, before the change-reason prompt and save dispatch. A synchronous (not QtConcurrent-wrapped) pre-save check that can call return; to abort the save with its own error dialog — e.g. rejecting a duplicate natural key before it silently versions an unrelated existing row (the SQL insert trigger can't reject this itself without breaking other upsert-by-key consumers of the same table). Can also fully handle and return; from an alternate save path that doesn't touch this entity's own fields at all — e.g. a companion widget's own change-reason prompt and commit, skipping the entity save entirely when it's the only thing that changed (see the on_save_success seam below for the counterpart when the entity itself does need saving too).
  • F8EC96B9-04BA-46A1-A2D8-4E4B91DCB435 — inside onSaveClicked()'s async watcher, immediately after the entity save succeeds (inside the if (result.success) branch, after notifySaveSuccess()). The watcher lambda captures crReasonCode=/=crCommentary from the same crSel used for the entity's own save, so a paste block here can commit a companion widget's pending changes using that same change-reason selection — one prompt shared by both writes — instead of popping a second, independent one.

Template

The full template source. Edit here and re-tangle with compass build --direct tangle_codegen_templates to regenerate library/templates/cpp_qt_detail_dialog.cpp.mustache.

{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/cpp_qt.org. Edit the org source. }}
{{! Template to generate Qt detail dialog source for domain entities }}
{{{cpp_license}}}
#include "ores.qt/{{domain_entity.entity_pascal}}DetailDialog.hpp"

#include <QMessageBox>
#include <QtConcurrent>
#include <QFutureWatcher>
{{#domain_entity.qt.has_text_edit_fields}}
#include <QPlainTextEdit>
{{/domain_entity.qt.has_text_edit_fields}}
{{#domain_entity.qt.has_combo_fields}}
#include "ores.qt/WidgetUtils.hpp"
#include <QComboBox>
{{/domain_entity.qt.has_combo_fields}}
{{#domain_entity.qt.has_dynamic_combo_helper_fields}}
#include "ores.qt/DynamicComboSetup.hpp"
#include "ores.qt/LookupFetcher.hpp"
{{/domain_entity.qt.has_dynamic_combo_helper_fields}}
{{#domain_entity.qt.has_uuid_primary_key}}
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid_io.hpp>
{{/domain_entity.qt.has_uuid_primary_key}}
{{^domain_entity.qt.has_uuid_primary_key}}
{{#domain_entity.qt.has_uuid_detail_fields}}
#include <boost/uuid/uuid_io.hpp>
{{/domain_entity.qt.has_uuid_detail_fields}}
{{/domain_entity.qt.has_uuid_primary_key}}
{{#domain_entity.qt.has_uuid_detail_fields}}
#include <boost/uuid/string_generator.hpp>
{{/domain_entity.qt.has_uuid_detail_fields}}
{{#domain_entity.qt.has_date_detail_fields}}
#include "ores.platform/time/datetime.hpp"
{{/domain_entity.qt.has_date_detail_fields}}
{{^domain_entity.qt.has_date_detail_fields}}
{{#domain_entity.qt.has_as_of_combo_fields}}
#include "ores.platform/time/datetime.hpp"
{{/domain_entity.qt.has_as_of_combo_fields}}
{{/domain_entity.qt.has_date_detail_fields}}
#include "ui_{{domain_entity.entity_pascal}}DetailDialog.h"
#include "ores.qt/ChangeReasonDialog.hpp"
{{#domain_entity.qt.has_flag_icon}}
#include "ores.qt/ImageCache.hpp"
#include <QIcon>
#include <QLineEdit>
{{/domain_entity.qt.has_flag_icon}}
{{#domain_entity.qt.has_key_flag_icon}}
{{^domain_entity.qt.has_flag_icon}}
#include "ores.qt/ImageCache.hpp"
#include <QIcon>
#include <QLineEdit>
{{/domain_entity.qt.has_flag_icon}}
{{#domain_entity.qt.key_flag_accessor_is_free_fn}}
#include "ores.qt/FlagIconHelper.hpp"
{{/domain_entity.qt.key_flag_accessor_is_free_fn}}
{{/domain_entity.qt.has_key_flag_icon}}
{{#domain_entity.qt.has_combo_flag_source}}
#include "ores.qt/FlagIconHelper.hpp"
#include <algorithm>
{{/domain_entity.qt.has_combo_flag_source}}
{{#domain_entity.qt.has_combo_badge_source}}
#include "ores.qt/BadgeComboHelper.hpp"
{{/domain_entity.qt.has_combo_badge_source}}
#include "ores.qt/IconUtils.hpp"
#include "ores.qt/MessageBoxHelper.hpp"
#include "{{domain_entity.qt.protocol_include}}"
{{! see * Extension points above }}
<<paste:3F8B6C1D-4E2A-4F9B-8C3D-1A5E7F2B9D4C>>

namespace ores::qt {

using namespace ores::logging;

{{domain_entity.entity_pascal}}DetailDialog::{{domain_entity.entity_pascal}}DetailDialog(QWidget* parent)
    : DetailDialogBase(parent),
      ui_(new Ui::{{domain_entity.entity_pascal}}DetailDialog),
      clientManager_(nullptr){{#domain_entity.qt.has_setting_gated_actions}},
{{#domain_entity.qt.setting_gated_actions}}
      {{action}}_(nullptr),
{{/domain_entity.qt.setting_gated_actions}}
      settingGatedActions_(nullptr){{/domain_entity.qt.has_setting_gated_actions}} {

    ui_->setupUi(this);
{{#domain_entity.qt.has_combo_fields}}
    WidgetUtils::setupComboBoxes(this);
{{/domain_entity.qt.has_combo_fields}}
    setupUi();
{{#domain_entity.qt.has_combo_fields}}
    setupCombos();
{{/domain_entity.qt.has_combo_fields}}
    setupConnections();
    // Hierarchy tree seam: a future :implements 9B165431-2921-4CAC-A2E8-2C186741E523
    // block is expected to construct a HierarchyModelBuilder-derived model
    // for this entity, wrap it in a HierarchyTreeWidget, and insert that
    // widget into this dialog's layout (e.g. a dedicated tab). Left empty
    // when no entity implements this kind.
<<paste:9B165431-2921-4CAC-A2E8-2C186741E523>>
    // Composite child-entity tables seam: an :implements
    // 7E4A2C8D-9F1B-4E6A-8D3C-5B2A7E9F1C4D block constructs one QTableWidget
    // + QToolBar per embedded child entity (e.g. identifiers, contact
    // information), wraps each in a tab, and inserts it into this dialog's
    // tab widget. Left empty when no entity implements this kind.
<<paste:7E4A2C8D-9F1B-4E6A-8D3C-5B2A7E9F1C4D>>
}

{{domain_entity.entity_pascal}}DetailDialog::~{{domain_entity.entity_pascal}}DetailDialog() {
    delete ui_;
}

QTabWidget* {{domain_entity.entity_pascal}}DetailDialog::tabWidget() const {
    return ui_->tabWidget;
}

QWidget* {{domain_entity.entity_pascal}}DetailDialog::provenanceTab() const {
    return ui_->provenanceTab;
}

ProvenanceWidget* {{domain_entity.entity_pascal}}DetailDialog::provenanceWidget() const {
    return ui_->provenanceWidget;
}

QString {{domain_entity.entity_pascal}}DetailDialog::code() const {
    return QString::fromStdString({{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}_.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}});
}

void {{domain_entity.entity_pascal}}DetailDialog::setupUi() {
    ui_->saveButton->setIcon(
        IconUtils::createRecoloredIcon(Icon::Save, IconUtils::DefaultIconColor));
    ui_->saveButton->setEnabled(false);

    ui_->deleteButton->setIcon(
        IconUtils::createRecoloredIcon(Icon::Delete, IconUtils::DefaultIconColor));

    ui_->closeButton->setIcon(
        IconUtils::createRecoloredIcon(Icon::Dismiss, IconUtils::DefaultIconColor));
{{#domain_entity.qt.has_flag_icon}}

    // Flag editor hosted in the .ui flagGroup; base class owns the button
    // (also wires the inline key-field icon — see initKeyFlagField()).
    initFlagButton(ui_->flagGroup->layout());
{{/domain_entity.qt.has_flag_icon}}
{{^domain_entity.qt.has_flag_icon}}
{{#domain_entity.qt.has_key_flag_icon}}

    // No uploadable flag image of its own — just the derived, read-only
    // inline icon on the key field.
    initKeyFlagField();
{{/domain_entity.qt.has_key_flag_icon}}
{{/domain_entity.qt.has_flag_icon}}
{{#domain_entity.qt.has_toolbar}}

    toolBar_ = new QToolBar(this);
    toolBar_->setMovable(false);
    toolBar_->setFloatable(false);
{{/domain_entity.qt.has_toolbar}}
{{#domain_entity.qt.has_version_navigation}}

    revertAction_ = new QAction(tr("Revert"), this);
    revertAction_->setIcon(IconUtils::createRecoloredIcon(
        Icon::ArrowRotateCounterclockwise, IconUtils::DefaultIconColor));
    revertAction_->setToolTip(
        tr("Revert {{domain_entity.entity_singular_words}} to this historical version"));
    connect(revertAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onRevertClicked);
    toolBar_->addAction(revertAction_);
    revertAction_->setVisible(false);

    toolBar_->addSeparator();

    firstVersionAction_ = new QAction(tr("First"), this);
    firstVersionAction_->setIcon(
        IconUtils::createRecoloredIcon(Icon::ArrowPrevious, IconUtils::DefaultIconColor));
    firstVersionAction_->setToolTip(tr("First version"));
    connect(firstVersionAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFirstVersionClicked);
    toolBar_->addAction(firstVersionAction_);
    firstVersionAction_->setVisible(false);

    prevVersionAction_ = new QAction(tr("Previous"), this);
    prevVersionAction_->setIcon(
        IconUtils::createRecoloredIcon(Icon::ArrowLeft, IconUtils::DefaultIconColor));
    prevVersionAction_->setToolTip(tr("Previous version"));
    connect(prevVersionAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onPrevVersionClicked);
    toolBar_->addAction(prevVersionAction_);
    prevVersionAction_->setVisible(false);

    nextVersionAction_ = new QAction(tr("Next"), this);
    nextVersionAction_->setIcon(
        IconUtils::createRecoloredIcon(Icon::ArrowRight, IconUtils::DefaultIconColor));
    nextVersionAction_->setToolTip(tr("Next version"));
    connect(nextVersionAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onNextVersionClicked);
    toolBar_->addAction(nextVersionAction_);
    nextVersionAction_->setVisible(false);

    lastVersionAction_ = new QAction(tr("Last"), this);
    lastVersionAction_->setIcon(
        IconUtils::createRecoloredIcon(Icon::ArrowNext, IconUtils::DefaultIconColor));
    lastVersionAction_->setToolTip(tr("Last version"));
    connect(lastVersionAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onLastVersionClicked);
    toolBar_->addAction(lastVersionAction_);
    lastVersionAction_->setVisible(false);
{{/domain_entity.qt.has_version_navigation}}
{{#domain_entity.qt.has_generate_action}}
    setupGenerateAction();
{{/domain_entity.qt.has_generate_action}}
{{#domain_entity.qt.has_toolbar}}

    if (auto* mainLayout = qobject_cast<QVBoxLayout*>(layout()))
        mainLayout->insertWidget(0, toolBar_);
{{/domain_entity.qt.has_toolbar}}
}
{{#domain_entity.qt.has_generate_action}}

void {{domain_entity.entity_pascal}}DetailDialog::setupGenerateAction() {
    generateAction_ = new QAction(tr("Generate"), this);
    generateAction_->setIcon(IconUtils::createRecoloredIcon(Icon::Wand, IconUtils::DefaultIconColor));
    generateAction_->setToolTip(tr("Fill fields with synthetic test data"));
    connect(generateAction_, &QAction::triggered, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onGenerateClicked);
    toolBar_->addAction(generateAction_);
    generateAction_->setVisible(false);
}

void {{domain_entity.entity_pascal}}DetailDialog::onGenerateClicked() {
    BOOST_LOG_SEV(lg(), debug) << "Generate clicked in detail dialog";

    try {
        utility::generation::generation_context ctx;
        auto generated =
            {{domain_entity.component}}::{{domain_entity.generator_facet_name}}::generate_synthetic_{{domain_entity.entity_singular}}(ctx);
        // The key identifies an existing record in edit mode; only a
        // brand-new record (create mode) may take the generated key.
{{#domain_entity.qt.detail_fields}}
{{#is_key}}
        if (!createMode_)
            generated.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
                {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}};
{{/is_key}}
{{/domain_entity.qt.detail_fields}}
        {{domain_entity.qt.item_var}}_ = generated;
        updateUiFrom{{domain_entity.entity_pascal_short}}();

        hasChanges_ = true;
        updateSaveButtonState();

        emit statusMessage(
            tr("Generated synthetic {{domain_entity.entity_singular_words}} data - modify as needed and save"));

        BOOST_LOG_SEV(lg(), info) << "Filled fields with generated {{domain_entity.entity_singular_words}}";
    } catch (const std::exception& e) {
        BOOST_LOG_SEV(lg(), error) << "Error generating {{domain_entity.entity_singular_words}}: " << e.what();
        MessageBoxHelper::critical(this,
                                   tr("Generation Error"),
                                   tr("Failed to generate {{domain_entity.entity_singular_words}} data: %1").arg(e.what()));
    }
}
{{/domain_entity.qt.has_generate_action}}
{{#domain_entity.qt.has_flag_icon}}

std::optional<boost::uuids::uuid>
{{domain_entity.entity_pascal}}DetailDialog::entityImageId() const {
    return {{domain_entity.qt.item_var}}_.image_id;
}
{{/domain_entity.qt.has_flag_icon}}
{{#domain_entity.qt.has_key_flag_icon}}

QLineEdit* {{domain_entity.entity_pascal}}DetailDialog::keyFlagField() const {
    return ui_->{{domain_entity.qt.key_flag_field}};
}

QIcon {{domain_entity.entity_pascal}}DetailDialog::keyFlagIcon(const std::string& key) const {
{{#domain_entity.qt.key_flag_accessor_is_free_fn}}
    return imageCache() ? {{domain_entity.qt.key_flag_accessor}}(*imageCache(), key) : QIcon();
{{/domain_entity.qt.key_flag_accessor_is_free_fn}}
{{^domain_entity.qt.key_flag_accessor_is_free_fn}}
    return imageCache() ? imageCache()->{{domain_entity.qt.key_flag_accessor}}(key) : QIcon();
{{/domain_entity.qt.key_flag_accessor_is_free_fn}}
}
{{#domain_entity.qt.key_flag_is_pair}}

QSize {{domain_entity.entity_pascal}}DetailDialog::keyFlagIconSize() const {
    return currency_pair_icon_size();
}
{{/domain_entity.qt.key_flag_is_pair}}
{{/domain_entity.qt.has_key_flag_icon}}

{{#domain_entity.qt.has_combo_fields}}
void {{domain_entity.entity_pascal}}DetailDialog::setupCombos() {
{{#domain_entity.qt.detail_fields}}
{{#is_static_combo}}
    ui_->{{widget}}->clear();
{{#combo_values}}
    ui_->{{widget}}->addItem(tr("{{label}}"), QString("{{value}}"));
{{/combo_values}}
{{/is_static_combo}}
{{/domain_entity.qt.detail_fields}}
}

{{/domain_entity.qt.has_combo_fields}}
void {{domain_entity.entity_pascal}}DetailDialog::setupConnections() {
    connect(ui_->saveButton, &QPushButton::clicked, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onSaveClicked);
    connect(ui_->deleteButton, &QPushButton::clicked, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onDeleteClicked);
    connect(ui_->closeButton, &QPushButton::clicked, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onCloseClicked);
{{#domain_entity.qt.has_flag_icon}}
    connect(this, &DetailDialogBase::flagEdited, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/domain_entity.qt.has_flag_icon}}

{{#domain_entity.qt.detail_fields}}
{{#is_key}}
{{#is_line_edit}}
    connect(ui_->{{widget}}, &QLineEdit::textChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onCodeChanged);
{{/is_line_edit}}
{{#is_static_combo}}
    connect(ui_->{{widget}}, &QComboBox::currentIndexChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_static_combo}}
{{#is_dynamic_combo}}
    connect(ui_->{{widget}}, &QComboBox::currentIndexChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_dynamic_combo}}
{{#is_flagged_combo}}
    connect(ui_->{{widget}}, &QComboBox::currentIndexChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_flagged_combo}}
{{#is_spin_box}}
    connect(ui_->{{widget}}, &QSpinBox::valueChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_spin_box}}
{{/is_key}}
{{^is_key}}
{{#is_line_edit}}
    connect(ui_->{{widget}}, &QLineEdit::textChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_line_edit}}
{{#is_text_edit}}
    connect(ui_->{{widget}}, &QPlainTextEdit::textChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_text_edit}}
{{#is_static_combo}}
    connect(ui_->{{widget}}, &QComboBox::currentIndexChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_static_combo}}
{{#is_dynamic_combo}}
    connect(ui_->{{widget}}, &QComboBox::currentIndexChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_dynamic_combo}}
{{#is_flagged_combo}}
    connect(ui_->{{widget}}, &QComboBox::currentIndexChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_flagged_combo}}
{{#is_check_box}}
    connect(ui_->{{widget}}, &QCheckBox::toggled, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_check_box}}
{{#is_spin_box}}
    connect(ui_->{{widget}}, &QSpinBox::valueChanged, this,
            &{{domain_entity.entity_pascal}}DetailDialog::onFieldChanged);
{{/is_spin_box}}
{{#is_colour}}
    wire_colour_picker(this, ui_->{{widget}}, [this]() { onFieldChanged(); });
{{/is_colour}}
{{/is_key}}
{{/domain_entity.qt.detail_fields}}
{{! see * Extension points above }}
<<paste:B6F2748A-9896-4B9C-BEAC-38436FD1CA79>>
}

void {{domain_entity.entity_pascal}}DetailDialog::setClientManager(ClientManager* clientManager) {
    clientManager_ = clientManager;
{{#domain_entity.qt.detail_fields}}
{{#is_dynamic_combo}}
{{#combo_fetch_fn}}
    populate{{combo_setter_pascal}}();
{{/combo_fetch_fn}}
{{#badge_key}}
    setup_badge_combo(this, ui_->{{widget}}, badgeCache(), "{{badge_key}}");
{{/badge_key}}
{{/is_dynamic_combo}}
{{#is_static_combo}}
{{#badge_key}}
    setup_badge_combo(this, ui_->{{widget}}, badgeCache(), "{{badge_key}}");
{{/badge_key}}
{{/is_static_combo}}
{{/domain_entity.qt.detail_fields}}
{{#domain_entity.qt.has_setting_gated_actions}}

    if (clientManager_) {
        settingGatedActions_ = new SettingGatedActionController(clientManager_, this);
{{#domain_entity.qt.setting_gated_actions}}
        settingGatedActions_->registerAction(
            {{action}}_, "{{setting}}", [this]() { return !readOnly_; });
{{/domain_entity.qt.setting_gated_actions}}
        if (clientManager_->isLoggedIn())
            settingGatedActions_->refresh();
    }
{{/domain_entity.qt.has_setting_gated_actions}}
{{#domain_entity.qt.detail_fields}}
{{#is_flagged_combo}}
    populate{{field_pascal}}Combo();
{{/is_flagged_combo}}
{{/domain_entity.qt.detail_fields}}
}
{{#domain_entity.qt.detail_fields}}
{{#is_flagged_combo}}
{{#combo_helper}}

void {{domain_entity.entity_pascal}}DetailDialog::populate{{field_pascal}}Combo() {
    {{combo_helper}}(ui_->{{widget}}, this, clientManager_, imageCache(), [this]() {
{{#is_optional_string}}
        const auto& v = {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}};
        return v ? QString::fromStdString(*v) : QString();
{{/is_optional_string}}
{{^is_optional_string}}
        return QString::fromStdString({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
{{/is_optional_string}}
    }{{#flag_icon_height}}, single_flag_icon_size({{flag_icon_height}}){{/flag_icon_height}});
}
{{/combo_helper}}
{{^combo_helper}}

void {{domain_entity.entity_pascal}}DetailDialog::populate{{field_pascal}}Combo() {
    if (!clientManager_ || !clientManager_->isConnected())
        return;

    QPointer<{{domain_entity.entity_pascal}}DetailDialog> self = this;
    auto* watcher = new QFutureWatcher<std::vector<std::string>>(self);
    QObject::connect(watcher, &QFutureWatcher<std::vector<std::string>>::finished, self,
        [self, watcher]() {
            auto codes = watcher->result();
            watcher->deleteLater();
            if (!self)
                return;

            auto* combo = self->ui_->{{widget}};
            const QString previous = combo->currentText();
            combo->blockSignals(true);
            combo->clear();
{{#combo_allow_blank}}
            combo->addItem(QString());
{{/combo_allow_blank}}
            for (const auto& c : codes)
                combo->addItem(QString::fromStdString(c));
            // fallback is evaluated here (fetch-completion time), not at
            // populate-call time, since set{{domain_entity.entity_pascal_short}}() may run before or
            // after setClientManager() triggers this fetch. Preferred over
            // previous: set{{domain_entity.entity_pascal_short}}() always runs synchronously before
            // this dialog is shown to the user, well before this async
            // fetch can complete, so by the time this callback fires the
            // model's own field value is authoritative -- previous (the
            // combo's transient text, empty until this first population)
            // would otherwise permanently blank the selection on every
            // dialog open, racing this fetch against every
            // set{{domain_entity.entity_pascal_short}}() call that happened first.
{{#is_optional_string}}
            const QString fallback = self->{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}
                ? QString::fromStdString(*self->{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})
                : QString{};
{{/is_optional_string}}
{{^is_optional_string}}
            const QString fallback = QString::fromStdString(
                self->{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
{{/is_optional_string}}
            const QString to_select = !fallback.isEmpty() ? fallback : previous;
            if (!to_select.isEmpty()) {
                const int idx = combo->findText(to_select);
                if (idx >= 0) combo->setCurrentIndex(idx);
            }
            combo->blockSignals(false);

            if (self->imageCache())
                apply_flag_icons(
                    combo, self->imageCache(), FlagSource::{{flag_source_pascal}}, {{flag_icon_size_expr}});
        });

    auto* cm = clientManager_;
    watcher->setFuture(QtConcurrent::run([cm]() { return {{combo_fetch}}(cm); }));
}
{{/combo_helper}}
{{/is_flagged_combo}}
{{/domain_entity.qt.detail_fields}}

void {{domain_entity.entity_pascal}}DetailDialog::setUsername(const std::string& username) {
    username_ = username;
}

void {{domain_entity.entity_pascal}}DetailDialog::set{{domain_entity.entity_pascal_short}}(
    const {{domain_entity.qt.domain_class}}& {{domain_entity.qt.item_var}}) {
    {{domain_entity.qt.item_var}}_ = {{domain_entity.qt.item_var}};
    updateUiFrom{{domain_entity.entity_pascal_short}}();
{{#domain_entity.qt.has_as_of_combo_fields}}
    // Re-resolve the as-of combos if setClientManager() already ran with a
    // different (or no) {{domain_entity.entity_singular}} -- see the call-order
    // note in populate*Combo() for each as-of field below.
    if (clientManager_ && readOnly_) {
{{#domain_entity.qt.detail_fields}}
{{#combo_as_of_fetch_fn}}
        populate{{combo_setter_pascal}}();
{{/combo_as_of_fetch_fn}}
{{/domain_entity.qt.detail_fields}}
    }
{{/domain_entity.qt.has_as_of_combo_fields}}
<<paste:D5A8E2F1-6C4B-4A9D-8E3F-2B7C1A9D4E6F>>
}

void {{domain_entity.entity_pascal}}DetailDialog::setCreateMode(bool createMode) {
    createMode_ = createMode;
{{#domain_entity.qt.locked_fields}}
{{#is_line_edit}}
{{#is_auto_generated_key}}
    ui_->{{widget}}->setReadOnly(true);
{{/is_auto_generated_key}}
{{^is_auto_generated_key}}
    ui_->{{widget}}->setReadOnly(!createMode);
{{/is_auto_generated_key}}
{{/is_line_edit}}
{{#is_text_edit}}
    ui_->{{widget}}->setReadOnly(!createMode);
{{/is_text_edit}}
{{#is_static_combo}}
    ui_->{{widget}}->setEnabled(createMode);
{{/is_static_combo}}
{{#is_dynamic_combo}}
    ui_->{{widget}}->setEnabled(createMode);
{{/is_dynamic_combo}}
{{#is_flagged_combo}}
    ui_->{{widget}}->setEnabled(createMode);
{{/is_flagged_combo}}
{{#is_check_box}}
    ui_->{{widget}}->setEnabled(createMode);
{{/is_check_box}}
{{#is_colour}}
    ui_->{{widget}}->setEnabled(createMode);
{{/is_colour}}
{{/domain_entity.qt.locked_fields}}
    ui_->deleteButton->setVisible(!createMode);
    setProvenanceEnabled(!createMode);
{{#domain_entity.qt.has_uuid_primary_key}}
    if (createMode) {
        {{domain_entity.qt.item_var}}_.{{#domain_entity.qt.id_access}}{{domain_entity.qt.id_access}}{{/domain_entity.qt.id_access}}{{^domain_entity.qt.id_access}}id{{/domain_entity.qt.id_access}} = boost::uuids::random_generator()();
{{#domain_entity.qt.hidden_party_id_on_create}}
        if (clientManager_)
            {{domain_entity.qt.item_var}}_.party_id = clientManager_->currentPartyId();
{{/domain_entity.qt.hidden_party_id_on_create}}
    }
{{/domain_entity.qt.has_uuid_primary_key}}
    hasChanges_ = false;
    updateSaveButtonState();
{{! see * Extension points above }}
<<paste:9A719585-3AB1-47DE-B0B0-99AFDFE7180E>>
}

void {{domain_entity.entity_pascal}}DetailDialog::markDirty() {
    hasChanges_ = true;
    updateSaveButtonState();
}

{{^domain_entity.qt.has_version_navigation}}
void {{domain_entity.entity_pascal}}DetailDialog::setReadOnly(bool readOnly) {
{{/domain_entity.qt.has_version_navigation}}
{{#domain_entity.qt.has_version_navigation}}
void {{domain_entity.entity_pascal}}DetailDialog::setReadOnly(bool readOnly, int versionNumber) {
    historicalVersion_ = versionNumber;
{{/domain_entity.qt.has_version_navigation}}
    readOnly_ = readOnly;
{{#domain_entity.qt.detail_fields}}
{{#is_locked_after_create}}
{{#is_line_edit}}
    ui_->{{widget}}->setReadOnly(true);
{{/is_line_edit}}
{{#is_text_edit}}
    ui_->{{widget}}->setReadOnly(true);
{{/is_text_edit}}
{{#is_static_combo}}
    ui_->{{widget}}->setEnabled(false);
{{/is_static_combo}}
{{#is_dynamic_combo}}
    ui_->{{widget}}->setEnabled(false);
{{/is_dynamic_combo}}
{{#is_flagged_combo}}
    ui_->{{widget}}->setEnabled(false);
{{/is_flagged_combo}}
{{#is_check_box}}
    ui_->{{widget}}->setEnabled(false);
{{/is_check_box}}
{{#is_colour}}
    ui_->{{widget}}->setEnabled(false);
{{/is_colour}}
{{/is_locked_after_create}}
{{^is_locked_after_create}}
{{#is_line_edit}}
    ui_->{{widget}}->setReadOnly(readOnly);
{{/is_line_edit}}
{{#is_text_edit}}
    ui_->{{widget}}->setReadOnly(readOnly);
{{/is_text_edit}}
{{#is_static_combo}}
    ui_->{{widget}}->setEnabled(!readOnly);
{{/is_static_combo}}
{{#is_dynamic_combo}}
    ui_->{{widget}}->setEnabled(!readOnly);
{{/is_dynamic_combo}}
{{#is_flagged_combo}}
    ui_->{{widget}}->setEnabled(!readOnly);
{{/is_flagged_combo}}
{{#is_check_box}}
    ui_->{{widget}}->setEnabled(!readOnly);
{{/is_check_box}}
{{#is_colour}}
    ui_->{{widget}}->setEnabled(!readOnly);
{{/is_colour}}
{{/is_locked_after_create}}
{{/domain_entity.qt.detail_fields}}
    ui_->saveButton->setVisible(!readOnly);
    ui_->deleteButton->setVisible(!readOnly);
{{#domain_entity.qt.has_version_navigation}}
    if (revertAction_)
        revertAction_->setVisible(readOnly);
{{/domain_entity.qt.has_version_navigation}}
{{#domain_entity.qt.has_as_of_combo_fields}}
    // Re-resolve the as-of combos if setClientManager() already ran before
    // this call established readOnly_ -- see the call-order note in
    // populate*Combo() for each as-of field below.
    if (clientManager_) {
{{#domain_entity.qt.detail_fields}}
{{#combo_as_of_fetch_fn}}
        populate{{combo_setter_pascal}}();
{{/combo_as_of_fetch_fn}}
{{/domain_entity.qt.detail_fields}}
    }
{{/domain_entity.qt.has_as_of_combo_fields}}
{{! see * Extension points above }}
<<paste:7A4AE8D5-A22D-4912-B4BF-D58215681CD2>>
}
{{#domain_entity.qt.has_version_navigation}}

void {{domain_entity.entity_pascal}}DetailDialog::setHistory(
    const std::vector<{{domain_entity.qt.domain_class}}>& history, int versionNumber) {
    history_ = history;

    // Find index of the requested version (history is newest-first)
    currentHistoryIndex_ = 0;
    for (size_t i = 0; i < history_.size(); ++i) {
        if (history_[i].version == versionNumber) {
            currentHistoryIndex_ = static_cast<int>(i);
            break;
        }
    }

    displayCurrentVersion();
    showVersionNavActions(true);
}

void {{domain_entity.entity_pascal}}DetailDialog::displayCurrentVersion() {
    if (history_.empty() || currentHistoryIndex_ < 0 ||
        currentHistoryIndex_ >= static_cast<int>(history_.size())) {
        return;
    }

    const auto& version = history_[currentHistoryIndex_];
    set{{domain_entity.entity_pascal_short}}(version);
    setReadOnly(true, version.version);
    updateVersionNavButtonStates();
}

void {{domain_entity.entity_pascal}}DetailDialog::updateVersionNavButtonStates() {
    if (history_.empty()) {
        showVersionNavActions(false);
        return;
    }

    bool atOldest = (currentHistoryIndex_ == static_cast<int>(history_.size()) - 1);
    bool atNewest = (currentHistoryIndex_ == 0);

    if (firstVersionAction_)
        firstVersionAction_->setEnabled(!atOldest); // Go to oldest
    if (prevVersionAction_)
        prevVersionAction_->setEnabled(!atOldest); // Go to older
    if (nextVersionAction_)
        nextVersionAction_->setEnabled(!atNewest); // Go to newer
    if (lastVersionAction_)
        lastVersionAction_->setEnabled(!atNewest); // Go to latest
}

void {{domain_entity.entity_pascal}}DetailDialog::showVersionNavActions(bool visible) {
    if (firstVersionAction_)
        firstVersionAction_->setVisible(visible);
    if (prevVersionAction_)
        prevVersionAction_->setVisible(visible);
    if (nextVersionAction_)
        nextVersionAction_->setVisible(visible);
    if (lastVersionAction_)
        lastVersionAction_->setVisible(visible);
}

void {{domain_entity.entity_pascal}}DetailDialog::onFirstVersionClicked() {
    if (history_.empty())
        return;
    currentHistoryIndex_ = static_cast<int>(history_.size()) - 1;
    displayCurrentVersion();
}

void {{domain_entity.entity_pascal}}DetailDialog::onPrevVersionClicked() {
    if (history_.empty())
        return;
    if (currentHistoryIndex_ < static_cast<int>(history_.size()) - 1) {
        ++currentHistoryIndex_;
        displayCurrentVersion();
    }
}

void {{domain_entity.entity_pascal}}DetailDialog::onNextVersionClicked() {
    if (history_.empty())
        return;
    if (currentHistoryIndex_ > 0) {
        --currentHistoryIndex_;
        displayCurrentVersion();
    }
}

void {{domain_entity.entity_pascal}}DetailDialog::onLastVersionClicked() {
    if (history_.empty())
        return;
    currentHistoryIndex_ = 0;
    displayCurrentVersion();
}

void {{domain_entity.entity_pascal}}DetailDialog::onRevertClicked() {
    auto reply = MessageBoxHelper::question(
        this,
        tr("Revert {{domain_entity.entity_title}}"),
        tr("Are you sure you want to revert '%1' to version %2?\n\n"
           "This will create a new version with the data from version %2.")
            .arg(code())
            .arg(historicalVersion_),
        QMessageBox::Yes | QMessageBox::No);

    if (reply != QMessageBox::Yes)
        return;

    emit revertRequested({{domain_entity.qt.item_var}}_);
}
{{/domain_entity.qt.has_version_navigation}}

{{#domain_entity.qt.detail_fields}}
{{#is_dynamic_combo}}
{{#combo_domain_type}}
void {{domain_entity.entity_pascal}}DetailDialog::populate{{combo_setter_pascal}}() {
    BOOST_LOG_SEV(lg(), debug) << "Populating {{field}} combo";
{{#combo_as_of_fetch_fn}}
    std::function<std::expected<std::vector<{{combo_domain_type}}>, QString>(ClientManager*)>
        fetch = &{{combo_fetch_fn}};
    if (readOnly_) {
        // Resolve this historical {{domain_entity.entity_singular}} version's
        // {{field}} badge as-of its own recorded_at, not against the current
        // (possibly since-renamed or deleted) lookup list -- see the As-of
        // lookup resolution codegen facet story. set{{domain_entity.entity_pascal_short}}()/setReadOnly()
        // re-invoke this populate call if they run after setClientManager(),
        // so call order between them doesn't matter.
        const auto as_of = QString::fromStdString(ores::platform::time::datetime::to_db_string(
            {{domain_entity.qt.item_var}}_.recorded_at));
        fetch = [as_of](ClientManager* cm) { return {{combo_as_of_fetch_fn}}(cm, as_of); };
    }
{{/combo_as_of_fetch_fn}}
    populateDynamicCombo<{{combo_domain_type}}>(
        ui_->{{widget}},
        this,
        clientManager_,
{{#combo_as_of_fetch_fn}}
        fetch,
{{/combo_as_of_fetch_fn}}
{{^combo_as_of_fetch_fn}}
        &{{combo_fetch_fn}},
{{/combo_as_of_fetch_fn}}
        "{{combo_watcher_name}}",
{{#combo_display_field}}
        [](const auto& t) { return QString::fromStdString(t.{{combo_display_field}}); },
{{/combo_display_field}}
{{^combo_display_field}}
        [](const auto& t) { return QString::fromStdString(t.{{combo_code_field}}); },
{{/combo_display_field}}
        [](const auto& t) { return QString::fromStdString(t.{{combo_tooltip_field}}); },
        [](const auto& t) { return t.{{combo_sort_field}}; },
{{#is_optional_uuid}}
        [this]() {
            const auto& _opt = {{domain_entity.qt.item_var}}_.{{field}};
            return _opt ? QString::fromStdString(boost::uuids::to_string(*_opt)) : QString{};
        },
{{/is_optional_uuid}}
{{^is_optional_uuid}}
{{#is_uuid}}
        [this]() { return QString::fromStdString(boost::uuids::to_string({{domain_entity.qt.item_var}}_.{{field}})); },
{{/is_uuid}}
{{^is_uuid}}
{{#is_optional_string}}
        [this]() {
            const auto& _opt = {{domain_entity.qt.item_var}}_.{{field}};
            return _opt ? QString::fromStdString(*_opt) : QString{};
        },
{{/is_optional_string}}
{{^is_optional_string}}
        [this]() { return QString::fromStdString({{domain_entity.qt.item_var}}_.{{field}}); },
{{/is_optional_string}}
{{/is_uuid}}
{{/is_optional_uuid}}
        [this](const QString& error) {
            emit errorMessage(tr("Failed to load {{combo_label}}: %1").arg(error));
        },
{{#badge_key}}
        [this]() { setup_badge_combo(this, ui_->{{widget}}, badgeCache(), "{{badge_key}}"); },
{{/badge_key}}
{{^badge_key}}
        []() {},
{{/badge_key}}
        QObject::tr("Loading…"),
        QObject::tr("Failed to load"),
{{#is_uuid}}
        [](const auto& t) { return QString::fromStdString(boost::uuids::to_string(t.{{combo_code_field}})); },
{{/is_uuid}}
{{#is_optional_uuid}}
        [](const auto& t) { return QString::fromStdString(boost::uuids::to_string(t.{{combo_code_field}})); },
{{/is_optional_uuid}}
{{^is_uuid}}
{{^is_optional_uuid}}
        [](const auto& t) { return QString::fromStdString(t.{{combo_code_field}}); },
{{/is_optional_uuid}}
{{/is_uuid}}
{{#is_self_referencing_combo}}
        [this](const auto& t) { return t.{{domain_entity.qt.key_field}} == {{domain_entity.qt.item_var}}_.{{domain_entity.qt.key_field}}; },
{{/is_self_referencing_combo}}
{{^is_self_referencing_combo}}
        [](const auto&) { return false; },
{{/is_self_referencing_combo}}
{{#combo_blank_label}}
        QObject::tr("{{combo_blank_label}}"));
{{/combo_blank_label}}
{{^combo_blank_label}}
        QString{});
{{/combo_blank_label}}
}
{{/combo_domain_type}}
{{^combo_domain_type}}
{{^combo_fetch_fn}}
void {{domain_entity.entity_pascal}}DetailDialog::{{combo_setter}}(
    const std::vector<{{combo_type}}>& items) {
    {{combo_items_member}}_ = items;
    populate{{combo_setter_pascal}}();
}

void {{domain_entity.entity_pascal}}DetailDialog::populate{{combo_setter_pascal}}() {
    const auto current = ui_->{{widget}}->currentData();
    ui_->{{widget}}->clear();
{{#combo_is_optional}}
    ui_->{{widget}}->addItem(tr("(None)"), QVariant{});
{{/combo_is_optional}}
    for (const auto& item : {{combo_items_member}}_) {
        ui_->{{widget}}->addItem(
            QString::fromStdString(item.{{combo_display}}),
            QString::fromStdString(item.{{combo_value}}));
    }
    if (current.isValid()) {
        const int idx = ui_->{{widget}}->findData(current);
        if (idx >= 0) ui_->{{widget}}->setCurrentIndex(idx);
    }
}
{{/combo_fetch_fn}}
{{#combo_fetch_fn}}
{{#combo_helper}}
void {{domain_entity.entity_pascal}}DetailDialog::populate{{combo_setter_pascal}}() {
    {{combo_helper}}(ui_->{{widget}}, this, clientManager_, imageCache(), [this]() {
        return QString::fromStdString({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
    });
}
{{/combo_helper}}
{{^combo_helper}}
void {{domain_entity.entity_pascal}}DetailDialog::populate{{combo_setter_pascal}}() {
    if (!clientManager_ || !clientManager_->isConnected())
        return;
    if (findChild<QFutureWatcherBase*>("{{combo_items_member}}FetchWatcher"))
        return;

    QPointer<{{domain_entity.entity_pascal}}DetailDialog> self = this;
    auto* cm = clientManager_;
    QFuture<std::vector<{{combo_type}}>> future =
        QtConcurrent::run([cm]() { return {{combo_fetch_fn}}(cm); });

    auto* watcher = new QFutureWatcher<std::vector<{{combo_type}}>>(this);
    watcher->setObjectName("{{combo_items_member}}FetchWatcher");
    connect(watcher, &QFutureWatcher<std::vector<{{combo_type}}>>::finished, this, [self, watcher]() {
        auto items = watcher->result();
        watcher->deleteLater();
        if (!self)
            return;

        // Server returns items already ordered (e.g. by display_order for a
        // lookup table); not re-sorted here.
        const auto current = self->ui_->{{widget}}->currentText();
        self->ui_->{{widget}}->clear();
        for (const auto& item : items)
            self->ui_->{{widget}}->addItem(QString::fromStdString(item.{{combo_display}}));
        // fallback is preferred over current: set{{domain_entity.entity_pascal_short}}() always runs
        // synchronously before this dialog is shown, well before this
        // async fetch can complete, so by the time this callback fires the
        // model's own field value is authoritative -- current (the combo's
        // transient text, empty until this first population) would
        // otherwise permanently blank the selection on every dialog open.
        const auto fallback =
            QString::fromStdString(self->{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
        const auto to_select = !fallback.isEmpty() ? fallback : current;
        if (!to_select.isEmpty())
            self->ui_->{{widget}}->setCurrentText(to_select);
{{#flag_source}}
        apply_flag_icons(self->ui_->{{widget}}, self->imageCache(), FlagSource::{{flag_source}});
{{/flag_source}}
{{#badge_key}}
        setup_badge_combo(self, self->ui_->{{widget}}, self->badgeCache(), "{{badge_key}}");
{{/badge_key}}
    });
    watcher->setFuture(future);
}
{{/combo_helper}}
{{/combo_fetch_fn}}
{{/combo_domain_type}}
{{/is_dynamic_combo}}
{{/domain_entity.qt.detail_fields}}
void {{domain_entity.entity_pascal}}DetailDialog::updateUiFrom{{domain_entity.entity_pascal_short}}() {
{{#domain_entity.qt.detail_fields}}
{{#is_line_edit}}
{{#is_optional_uuid}}
    {
        const auto& _opt = {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}};
        ui_->{{widget}}->setText(_opt
            ? QString::fromStdString(boost::uuids::to_string(*_opt))
            : QString{});
    }
{{/is_optional_uuid}}
{{^is_optional_uuid}}
{{#is_uuid}}
    ui_->{{widget}}->setText(QString::fromStdString(
        boost::uuids::to_string({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})));
{{/is_uuid}}
{{^is_uuid}}
{{#is_optional_string}}
    ui_->{{widget}}->setText({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}
        ? QString::fromStdString(*{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})
        : QString{});
{{/is_optional_string}}
{{^is_optional_string}}
{{#is_optional_double}}
    ui_->{{widget}}->setText({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}
        ? QString::number(*{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})
        : QString{});
{{/is_optional_double}}
{{^is_optional_double}}
{{#is_double}}
    ui_->{{widget}}->setText(QString::number({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}));
{{/is_double}}
{{^is_double}}
{{#is_date}}
    ui_->{{widget}}->setText(QString::fromStdString(ores::platform::time::datetime::to_iso8601_date(
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})));
{{/is_date}}
{{^is_date}}
    ui_->{{widget}}->setText(QString::fromStdString({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}));
{{/is_date}}
{{/is_double}}
{{/is_optional_double}}
{{/is_optional_string}}
{{/is_uuid}}
{{/is_optional_uuid}}
{{/is_line_edit}}
{{#is_text_edit}}
{{#is_optional_string}}
    ui_->{{widget}}->setPlainText({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}
        ? QString::fromStdString(*{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})
        : QString{});
{{/is_optional_string}}
{{^is_optional_string}}
    ui_->{{widget}}->setPlainText(QString::fromStdString({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}));
{{/is_optional_string}}
{{/is_text_edit}}
{{#is_check_box}}
{{#is_tristate}}
    ui_->{{widget}}->setCheckState({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}
        ? (*{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} ? Qt::Checked : Qt::Unchecked)
        : Qt::PartiallyChecked);
{{/is_tristate}}
{{^is_tristate}}
    ui_->{{widget}}->setChecked({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
{{/is_tristate}}
{{/is_check_box}}
{{#is_spin_box}}
{{#is_nullable_int}}
    ui_->{{widget}}->setValue({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}.value_or(
        ui_->{{widget}}->minimum()));
{{/is_nullable_int}}
{{^is_nullable_int}}
    ui_->{{widget}}->setValue({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
{{/is_nullable_int}}
{{/is_spin_box}}
{{#is_static_combo}}
    {
{{#is_optional_string}}
        const auto val = {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}
            ? QString::fromStdString(*{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})
            : QString{};
{{/is_optional_string}}
{{^is_optional_string}}
        const auto val = QString::fromStdString(
            {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
{{/is_optional_string}}
        const int idx = ui_->{{widget}}->findData(val);
        if (idx >= 0) ui_->{{widget}}->setCurrentIndex(idx);
    }
{{/is_static_combo}}
{{#is_dynamic_combo}}
{{#combo_fetch_fn}}
    {
{{#is_optional_uuid}}
        const auto& _opt = {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}};
        const auto val = _opt
            ? QString::fromStdString(boost::uuids::to_string(*_opt))
            : QString{};
{{/is_optional_uuid}}
{{^is_optional_uuid}}
{{#is_uuid}}
        const auto val = QString::fromStdString(boost::uuids::to_string(
            {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}));
{{/is_uuid}}
{{^is_uuid}}
{{#is_optional_string}}
        const auto val = {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}
            ? QString::fromStdString(*{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})
            : QString{};
{{/is_optional_string}}
{{^is_optional_string}}
        const auto val = QString::fromStdString(
            {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
{{/is_optional_string}}
{{/is_uuid}}
{{/is_optional_uuid}}
        const int idx = ui_->{{widget}}->findData(val);
        if (idx >= 0) ui_->{{widget}}->setCurrentIndex(idx);
    }
{{/combo_fetch_fn}}
{{^combo_fetch_fn}}
    {
        const auto val = QString::fromStdString({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
        const int idx = ui_->{{widget}}->findData(val);
        if (idx >= 0) ui_->{{widget}}->setCurrentIndex(idx);
    }
{{/combo_fetch_fn}}
{{/is_dynamic_combo}}
{{#is_flagged_combo}}
    {
{{#is_optional_string}}
        const auto val = {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}
            ? QString::fromStdString(*{{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}})
            : QString{};
{{/is_optional_string}}
{{^is_optional_string}}
        const auto val = QString::fromStdString({{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}});
{{/is_optional_string}}
        const int idx = ui_->{{widget}}->findText(val);
        ui_->{{widget}}->setCurrentIndex(idx);
    }
{{/is_flagged_combo}}
{{#is_colour}}
    set_colour_swatch(ui_->{{widget}}, QString::fromStdString(
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}}));
{{/is_colour}}
{{/domain_entity.qt.detail_fields}}

    populateProvenance({{domain_entity.qt.item_var}}_.{{#domain_entity.qt.version_access}}{{domain_entity.qt.version_access}}{{/domain_entity.qt.version_access}}{{^domain_entity.qt.version_access}}version{{/domain_entity.qt.version_access}},
                       {{domain_entity.qt.item_var}}_.{{#domain_entity.qt.modified_by_access}}{{domain_entity.qt.modified_by_access}}{{/domain_entity.qt.modified_by_access}}{{^domain_entity.qt.modified_by_access}}modified_by{{/domain_entity.qt.modified_by_access}},
                       {{domain_entity.qt.item_var}}_.{{#domain_entity.qt.performed_by_access}}{{domain_entity.qt.performed_by_access}}{{/domain_entity.qt.performed_by_access}}{{^domain_entity.qt.performed_by_access}}performed_by{{/domain_entity.qt.performed_by_access}},
                       {{domain_entity.qt.item_var}}_.{{#domain_entity.qt.recorded_at_access}}{{domain_entity.qt.recorded_at_access}}{{/domain_entity.qt.recorded_at_access}}{{^domain_entity.qt.recorded_at_access}}recorded_at{{/domain_entity.qt.recorded_at_access}},
                       {{domain_entity.qt.item_var}}_.{{#domain_entity.qt.change_reason_code_access}}{{domain_entity.qt.change_reason_code_access}}{{/domain_entity.qt.change_reason_code_access}}{{^domain_entity.qt.change_reason_code_access}}change_reason_code{{/domain_entity.qt.change_reason_code_access}},
                       {{domain_entity.qt.item_var}}_.{{#domain_entity.qt.change_commentary_access}}{{domain_entity.qt.change_commentary_access}}{{/domain_entity.qt.change_commentary_access}}{{^domain_entity.qt.change_commentary_access}}change_commentary{{/domain_entity.qt.change_commentary_access}});

    hasChanges_ = false;
    updateSaveButtonState();
}

void {{domain_entity.entity_pascal}}DetailDialog::update{{domain_entity.entity_pascal_short}}FromUi() {
{{#domain_entity.qt.detail_fields}}
{{#is_locked_after_create}}
{{^is_uuid}}
    if (createMode_) {
{{#is_line_edit}}
{{#is_date}}
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
            ores::platform::time::datetime::from_iso8601_date(ui_->{{widget}}->text().trimmed().toStdString());
{{/is_date}}
{{^is_date}}
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->text().trimmed().toStdString();
{{/is_date}}
{{/is_line_edit}}
{{#is_static_combo}}
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->currentData().toString().trimmed().toStdString();
{{/is_static_combo}}
{{#is_dynamic_combo}}
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->currentData().toString().trimmed().toStdString();
{{/is_dynamic_combo}}
{{#is_flagged_combo}}
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->currentText().trimmed().toStdString();
{{/is_flagged_combo}}
{{#is_check_box}}
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->isChecked();
{{/is_check_box}}
    }
{{/is_uuid}}
{{#is_uuid}}
{{#is_dynamic_combo}}
    if (createMode_) {
        const auto {{field}}_str = ui_->{{widget}}->currentData().toString().trimmed().toStdString();
        if (!{{field}}_str.empty()) {
            try {
                {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
                    boost::uuids::string_generator()({{field}}_str);
            } catch (const std::exception&) {
                // Leave the field unchanged; validation catches the empty/nil
                // selection at save time (no combo choice made yet).
            }
        }
    }
{{/is_dynamic_combo}}
{{/is_uuid}}
{{/is_locked_after_create}}
{{^is_locked_after_create}}
{{#is_line_edit}}
{{^is_uuid}}
{{^is_optional_uuid}}
{{#is_optional_string}}
    {
        const auto {{field}}_str = ui_->{{widget}}->text().trimmed().toStdString();
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
            {{field}}_str.empty() ? std::nullopt : std::optional<std::string>({{field}}_str);
    }
{{/is_optional_string}}
{{^is_optional_string}}
{{#is_optional_double}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
        ui_->{{widget}}->text().trimmed().isEmpty()
        ? std::nullopt
        : std::optional<double>(ui_->{{widget}}->text().trimmed().toDouble());
{{/is_optional_double}}
{{^is_optional_double}}
{{#is_double}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->text().trimmed().toDouble();
{{/is_double}}
{{^is_double}}
{{#is_date}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
        ores::platform::time::datetime::from_iso8601_date(ui_->{{widget}}->text().trimmed().toStdString());
{{/is_date}}
{{^is_date}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->text().trimmed().toStdString();
{{/is_date}}
{{/is_double}}
{{/is_optional_double}}
{{/is_optional_string}}
{{/is_optional_uuid}}
{{/is_uuid}}
{{/is_line_edit}}
{{#is_text_edit}}
{{#is_optional_string}}
    {
        const auto {{field}}_str = ui_->{{widget}}->toPlainText().trimmed().toStdString();
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
            {{field}}_str.empty() ? std::nullopt : std::optional<std::string>({{field}}_str);
    }
{{/is_optional_string}}
{{^is_optional_string}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->toPlainText().trimmed().toStdString();
{{/is_optional_string}}
{{/is_text_edit}}
{{#is_check_box}}
{{#is_tristate}}
    switch (ui_->{{widget}}->checkState()) {
    case Qt::Checked:
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = std::optional<bool>(true);
        break;
    case Qt::Unchecked:
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = std::optional<bool>(false);
        break;
    default:
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = std::nullopt;
        break;
    }
{{/is_tristate}}
{{^is_tristate}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->isChecked();
{{/is_tristate}}
{{/is_check_box}}
{{#is_spin_box}}
{{#is_nullable_int}}
    if (ui_->{{widget}}->value() == ui_->{{widget}}->minimum())
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = std::nullopt;
    else
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->value();
{{/is_nullable_int}}
{{^is_nullable_int}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = ui_->{{widget}}->value();
{{/is_nullable_int}}
{{/is_spin_box}}
{{#is_static_combo}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
        ui_->{{widget}}->currentData().toString().toStdString();
{{/is_static_combo}}
{{#is_dynamic_combo}}
{{#combo_fetch_fn}}
{{#is_optional_uuid}}
    {
        const auto {{field}}_str = ui_->{{widget}}->currentData().toString().trimmed().toStdString();
        if ({{field}}_str.empty()) {
            {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = std::nullopt;
        } else {
            try {
                {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
                    boost::uuids::string_generator()({{field}}_str);
            } catch (const std::exception&) {
                {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} = std::nullopt;
            }
        }
    }
{{/is_optional_uuid}}
{{^is_optional_uuid}}
{{#is_uuid}}
    {
        const auto {{field}}_str = ui_->{{widget}}->currentData().toString().trimmed().toStdString();
        if (!{{field}}_str.empty()) {
            try {
                {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
                    boost::uuids::string_generator()({{field}}_str);
            } catch (const std::exception&) {
                // Leave the field unchanged; validation catches the empty/nil
                // selection at save time (no combo choice made yet).
            }
        }
    }
{{/is_uuid}}
{{^is_uuid}}
{{#is_optional_string}}
    {
        const auto {{field}}_str = ui_->{{widget}}->currentData().toString().trimmed().toStdString();
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
            {{field}}_str.empty() ? std::nullopt : std::optional{ {{field}}_str };
    }
{{/is_optional_string}}
{{^is_optional_string}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
        ui_->{{widget}}->currentText().toStdString();
{{/is_optional_string}}
{{/is_uuid}}
{{/is_optional_uuid}}
{{/combo_fetch_fn}}
{{^combo_fetch_fn}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
        ui_->{{widget}}->currentData().toString().toStdString();
{{/combo_fetch_fn}}
{{/is_dynamic_combo}}
{{#is_flagged_combo}}
{{#is_optional_string}}
    {
        const auto _txt = ui_->{{widget}}->currentText().trimmed().toStdString();
        {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
            _txt.empty() ? std::nullopt : std::optional<std::string>(_txt);
    }
{{/is_optional_string}}
{{^is_optional_string}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
        ui_->{{widget}}->currentText().toStdString();
{{/is_optional_string}}
{{/is_flagged_combo}}
{{#is_colour}}
    {{domain_entity.qt.item_var}}_.{{#field_access}}{{field_access}}{{/field_access}}{{^field_access}}{{field}}{{/field_access}} =
        colour_swatch_value(ui_->{{widget}}).toStdString();
{{/is_colour}}
{{/is_locked_after_create}}
{{/domain_entity.qt.detail_fields}}
    {{domain_entity.qt.item_var}}_.{{#domain_entity.qt.modified_by_access}}{{domain_entity.qt.modified_by_access}}{{/domain_entity.qt.modified_by_access}}{{^domain_entity.qt.modified_by_access}}modified_by{{/domain_entity.qt.modified_by_access}} = username_;
}

{{#domain_entity.qt.has_line_edit_key}}
void {{domain_entity.entity_pascal}}DetailDialog::onCodeChanged(const QString& /* text */) {
    hasChanges_ = true;
    updateSaveButtonState();
}
{{/domain_entity.qt.has_line_edit_key}}

void {{domain_entity.entity_pascal}}DetailDialog::onFieldChanged() {
    hasChanges_ = true;
    updateSaveButtonState();
}

void {{domain_entity.entity_pascal}}DetailDialog::updateSaveButtonState() {
    bool canSave = hasChanges_ && validateInput() && !readOnly_;
    ui_->saveButton->setEnabled(canSave);
}

bool {{domain_entity.entity_pascal}}DetailDialog::validateInput() {
{{#domain_entity.qt.required_fields}}
    const QString {{field}}_val = ui_->{{widget}}->text().trimmed();
{{/domain_entity.qt.required_fields}}
{{#domain_entity.qt.required_dynamic_combo_fields}}
    const bool {{field}}_selected = ui_->{{widget}}->currentIndex() >= 0;
{{/domain_entity.qt.required_dynamic_combo_fields}}
{{#domain_entity.qt.date_fields}}
    const QString {{field}}_date_val = ui_->{{widget}}->text().trimmed();
{{/domain_entity.qt.date_fields}}

    return true
{{#domain_entity.qt.required_fields}}
        && !{{field}}_val.isEmpty()
{{/domain_entity.qt.required_fields}}
{{#domain_entity.qt.required_dynamic_combo_fields}}
        && {{field}}_selected
{{/domain_entity.qt.required_dynamic_combo_fields}}
{{#domain_entity.qt.date_fields}}
        // A blank optional date field is valid (nothing to parse); a
        // non-blank one must be a real ISO-8601 date, or the unchecked
        // from_iso8601_date() parse in updateXFromUi() would throw and
        // crash the app on Save.
        && ({{field}}_date_val.isEmpty() ||
            ores::platform::time::datetime::is_valid_iso8601_date({{field}}_date_val.toStdString()))
{{/domain_entity.qt.date_fields}}
{{! see * Extension points above }}
<<paste:69F22F14-6701-4E73-AFCC-78AC1B1FDDDF>>
    ;
}

void {{domain_entity.entity_pascal}}DetailDialog::onSaveClicked() {
    if (!clientManager_ || !clientManager_->isConnected()) {
        MessageBoxHelper::warning(this, "Disconnected",
            "Cannot save {{domain_entity.entity_singular_words}} while disconnected from server.");
        return;
    }

    if (!validateInput()) {
        MessageBoxHelper::warning(this, "Invalid Input",
            "Please fill in all required fields.");
        return;
    }

{{! see * Extension points above }}
<<paste:4E83FA7A-742A-4102-90AF-D337F6FB1269>>

    const auto crOpType = createMode_
        ? ChangeReasonDialog::OperationType::Create
        : ChangeReasonDialog::OperationType::Amend;
    const auto crSel = promptChangeReason(crOpType, hasChanges_,
        createMode_ ? "system" : "common");
    if (!crSel) return;
    {{domain_entity.qt.item_var}}_.change_reason_code = crSel->reason_code;
    {{domain_entity.qt.item_var}}_.change_commentary  = crSel->commentary;
{{#domain_entity.qt.has_flag_icon}}
    if (flagChanged())
        {{domain_entity.qt.item_var}}_.image_id = selectedImageId();
{{/domain_entity.qt.has_flag_icon}}

    update{{domain_entity.entity_pascal_short}}FromUi();

    BOOST_LOG_SEV(lg(), info) << "Saving {{domain_entity.entity_singular_words}}: "
        << {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}_.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};

    QPointer<{{domain_entity.entity_pascal}}DetailDialog> self = this;

    struct SaveResult {
        bool success;
        std::string message;
    };

    auto task = [self, {{domain_entity.qt.item_var}} = {{domain_entity.qt.item_var}}_]() -> SaveResult {
        if (!self || !self->clientManager_) {
            return {false, "Dialog closed"};
        }

        {{domain_entity.qt.save_request_class}} request;
        request.data = {{domain_entity.qt.item_var}};
        auto response_result = self->clientManager_->
            process_authenticated_request(std::move(request));

        if (!response_result) {
            return {false, "Failed to communicate with server"};
        }

        return {response_result->success, response_result->message};
    };

    auto* watcher = new QFutureWatcher<SaveResult>(self);
    connect(watcher, &QFutureWatcher<SaveResult>::finished,
            self, [self, watcher, crReasonCode = crSel->reason_code, crCommentary = crSel->commentary]() {
        auto result = watcher->result();
        watcher->deleteLater();

        if (result.success) {
            BOOST_LOG_SEV(lg(), info) << "{{domain_entity.entity_title}} saved successfully";
            QString code = QString::fromStdString(
                {{domain_entity.qt.key_to_string_prefix}}self->{{domain_entity.qt.item_var}}_.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}});
            self->hasChanges_ = false;
{{#domain_entity.qt.has_flag_icon}}
            self->resetFlagChanged();
{{/domain_entity.qt.has_flag_icon}}
            self->updateSaveButtonState();
            emit self->{{domain_entity.qt.item_var}}Saved(code);
            self->notifySaveSuccess(tr("{{domain_entity.entity_title}} '%1' saved").arg(code));
{{! see * Extension points above }}
<<paste:F8EC96B9-04BA-46A1-A2D8-4E4B91DCB435>>
        } else {
            BOOST_LOG_SEV(lg(), error) << "Save failed: " << result.message;
            QString errorMsg = QString::fromStdString(result.message);
            emit self->errorMessage(errorMsg);
            MessageBoxHelper::critical(self, "Save Failed", errorMsg);
        }
    });

    QFuture<SaveResult> future = QtConcurrent::run(task);
    watcher->setFuture(future);
}

void {{domain_entity.entity_pascal}}DetailDialog::onDeleteClicked() {
    if (!clientManager_ || !clientManager_->isConnected()) {
        MessageBoxHelper::warning(this, "Disconnected",
            "Cannot delete {{domain_entity.entity_singular_words}} while disconnected from server.");
        return;
    }

    QString code = QString::fromStdString(
        {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}_.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}});
    auto reply = MessageBoxHelper::question(this, "Delete {{domain_entity.entity_title}}",
        QString("Are you sure you want to delete {{domain_entity.entity_singular_words}} '%1'?").arg(code),
        QMessageBox::Yes | QMessageBox::No);

    if (reply != QMessageBox::Yes) {
        return;
    }

    const auto crSel = promptChangeReason(
        ChangeReasonDialog::OperationType::Delete, false, "common");
    if (!crSel) return;

    BOOST_LOG_SEV(lg(), info) << "Deleting {{domain_entity.entity_singular_words}}: "
        << {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}_.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}};

    QPointer<{{domain_entity.entity_pascal}}DetailDialog> self = this;

    struct DeleteResult {
        bool success;
        std::string message;
    };

{{#domain_entity.qt.has_uuid_primary_key}}
    auto task = [self, id_str = boost::uuids::to_string({{domain_entity.qt.item_var}}_.{{#domain_entity.qt.id_access}}{{domain_entity.qt.id_access}}{{/domain_entity.qt.id_access}}{{^domain_entity.qt.id_access}}id{{/domain_entity.qt.id_access}})]() -> DeleteResult {
        if (!self || !self->clientManager_) {
            return {false, "Dialog closed"};
        }

        {{domain_entity.qt.delete_request_class}} request;
{{#domain_entity.qt.delete_request_id_is_plural}}
        request.{{domain_entity.qt.delete_request_id_field}} = {id_str};
{{/domain_entity.qt.delete_request_id_is_plural}}
{{^domain_entity.qt.delete_request_id_is_plural}}
        request.{{domain_entity.qt.delete_request_id_field}} = id_str;
{{/domain_entity.qt.delete_request_id_is_plural}}
        auto response_result = self->clientManager_->
            process_authenticated_request(std::move(request));

        if (!response_result) {
            return {false, "Failed to communicate with server"};
        }

        return {response_result->success, response_result->message};
    };
{{/domain_entity.qt.has_uuid_primary_key}}
{{^domain_entity.qt.has_uuid_primary_key}}
    auto task = [self, code = {{domain_entity.qt.key_to_string_prefix}}{{domain_entity.qt.item_var}}_.{{#domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field_access}}{{/domain_entity.qt.key_field_access}}{{^domain_entity.qt.key_field_access}}{{domain_entity.qt.key_field}}{{/domain_entity.qt.key_field_access}}{{domain_entity.qt.key_to_string_suffix}}]() -> DeleteResult {
        if (!self || !self->clientManager_) {
            return {false, "Dialog closed"};
        }

        {{domain_entity.qt.delete_request_class}} request;
{{#domain_entity.qt.delete_request_id_field}}
{{#domain_entity.qt.delete_request_id_is_plural}}
        request.{{domain_entity.qt.delete_request_id_field}} = {code};
{{/domain_entity.qt.delete_request_id_is_plural}}
{{^domain_entity.qt.delete_request_id_is_plural}}
        request.{{domain_entity.qt.delete_request_id_field}} = code;
{{/domain_entity.qt.delete_request_id_is_plural}}
{{/domain_entity.qt.delete_request_id_field}}
{{^domain_entity.qt.delete_request_id_field}}
        request.codes = {code};
{{/domain_entity.qt.delete_request_id_field}}
        auto response_result = self->clientManager_->
            process_authenticated_request(std::move(request));

        if (!response_result) {
            return {false, "Failed to communicate with server"};
        }

        return {response_result->success, response_result->message};
    };
{{/domain_entity.qt.has_uuid_primary_key}}

    auto* watcher = new QFutureWatcher<DeleteResult>(self);
    connect(watcher, &QFutureWatcher<DeleteResult>::finished,
            self, [self, code, watcher]() {
        auto result = watcher->result();
        watcher->deleteLater();

        if (result.success) {
            BOOST_LOG_SEV(lg(), info) << "{{domain_entity.entity_title}} deleted successfully";
            emit self->statusMessage(
                QString("{{domain_entity.entity_title}} '%1' deleted").arg(code));
            emit self->{{domain_entity.qt.item_var}}Deleted(code);
            self->requestClose();
        } else {
            BOOST_LOG_SEV(lg(), error) << "Delete failed: " << result.message;
            QString errorMsg = QString::fromStdString(result.message);
            emit self->errorMessage(errorMsg);
            MessageBoxHelper::critical(self, "Delete Failed", errorMsg);
        }
    });

    QFuture<DeleteResult> future = QtConcurrent::run(task);
    watcher->setFuture(future);
}

{{! see * Extension points above }}
<<paste:EC97923E-BC8D-4A50-9970-CBC5CBD4B732>>

}

See also

Emacs 29.3 (Org mode 9.6.15)