[office/skrooge] /: First version of skroogecompanion

Stéphane MANKOWSKI <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 9720503aa474122e3e402d5342344c622d550d1c by Stéphane MANKOWSKI.
Committed on 29/07/2026 at 20:20.
Pushed by smankowski into branch 'master'.

First version of skroogecompanion

M  +1    -0    .gitlab-ci.yml
M  +1    -0    CMakeLists.txt
A  +70   -0    skroogecompanion/CMakeLists.txt
A  +41   -0    skroogecompanion/README.android.md
A  +58   -0    skroogecompanion/android/AndroidManifest.xml
A  +101  -0    skroogecompanion/main.cpp     [License: GPL(v3.0+)]
A  +22   -0    skroogecompanion/org.kde.skroogecompanion.appdata.xml
A  +16   -0    skroogecompanion/org.kde.skroogecompanion.desktop
A  +11   -0    skroogecompanion/qml/AboutPage.qml     [License: GPL(v3.0+)]
A  +114  -0    skroogecompanion/qml/AccountListItem.qml     [License: GPL(v3.0+)]
A  +80   -0    skroogecompanion/qml/AccountsPage.qml     [License: GPL(v3.0+)]
A  +33   -0    skroogecompanion/qml/CategoriesPage.qml     [License: GPL(v3.0+)]
A  +52   -0    skroogecompanion/qml/CategoryListItem.qml     [License: GPL(v3.0+)]
A  +216  -0    skroogecompanion/qml/DashboardPage.qml     [License: GPL(v3.0+)]
A  +266  -0    skroogecompanion/qml/Main.qml     [License: GPL(v3.0+)]
A  +44   -0    skroogecompanion/qml/PayeeListItem.qml     [License: GPL(v3.0+)]
A  +24   -0    skroogecompanion/qml/PayeesPage.qml     [License: GPL(v3.0+)]
A  +56   -0    skroogecompanion/qml/SubTransactionListItem.qml     [License: GPL(v3.0+)]
A  +40   -0    skroogecompanion/qml/SubTransactionsPage.qml     [License: GPL(v3.0+)]
A  +53   -0    skroogecompanion/qml/TrackerListItem.qml     [License: GPL(v3.0+)]
A  +47   -0    skroogecompanion/qml/TrackersPage.qml     [License: GPL(v3.0+)]
A  +64   -0    skroogecompanion/qml/TransactionListItem.qml     [License: GPL(v3.0+)]
A  +81   -0    skroogecompanion/qml/TransactionsPage.qml     [License: GPL(v3.0+)]
A  +44   -0    skroogecompanion/qml/UnitListItem.qml     [License: GPL(v3.0+)]
A  +24   -0    skroogecompanion/qml/UnitsPage.qml     [License: GPL(v3.0+)]
A  +75   -0    skroogecompanion/skgmaccountlistmodel.cpp     [License: GPL(v3.0+)]
A  +59   -0    skroogecompanion/skgmaccountlistmodel.h     [License: GPL(v3.0+)]
A  +1271 -0    skroogecompanion/skgmcontroler.cpp     [License: GPL(v3.0+)]
A  +235  -0    skroogecompanion/skgmcontroler.h     [License: GPL(v3.0+)]
A  +179  -0    skroogecompanion/skgmreferenceproxymodel.cpp     [License: GPL(v3.0+)]
A  +55   -0    skroogecompanion/skgmreferenceproxymodel.h     [License: GPL(v3.0+)]
A  +81   -0    skroogecompanion/skgmtransactionlistmodel.cpp     [License: GPL(v3.0+)]
A  +54   -0    skroogecompanion/skgmtransactionlistmodel.h     [License: GPL(v3.0+)]

https://invent.kde.org/office/skrooge/-/commit/9720503aa474122e3e402d5342344c622d550d1c

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 691aa33b4..06617f113 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,3 +11,4 @@ include:
       - /gitlab-templates/craft-appimage-qt6.yml
       - /gitlab-templates/craft-windows-mingw64-qt6.yml
       - /gitlab-templates/craft-macos-x86-64-qt6.yml
+      - /gitlab-templates/craft-android-qt6-apks.yml
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8d527c0a8..02a876833 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -232,6 +232,7 @@ ENDIF()
 #Main application
 ADD_SUBDIRECTORY(skrooge)
 ADD_SUBDIRECTORY(skroogeconvert)
+ADD_SUBDIRECTORY(skroogecompanion)
 
 # ADD_SUBDIRECTORY(skrooge-companion)
 
diff --git a/skroogecompanion/CMakeLists.txt b/skroogecompanion/CMakeLists.txt
new file mode 100644
index 000000000..187826c56
--- /dev/null
+++ b/skroogecompanion/CMakeLists.txt
@@ -0,0 +1,70 @@
+#***************************************************************************
+#* SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+#* SPDX-License-Identifier: GPL-3.0-or-later
+#***************************************************************************
+MESSAGE( STATUS "..:: CMAKE SKROOGEMOBILE ::..")
+
+PROJECT(SKROOGEMOBILE)
+
+FIND_PACKAGE(Qt6 REQUIRED COMPONENTS Quick QuickControls2)
+FIND_PACKAGE(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami)
+
+LINK_DIRECTORIES (${LIBRARY_OUTPUT_PATH})
+
+QT_POLICY(SET QTP0001 NEW)
+QT_POLICY(SET QTP0004 NEW)
+
+QT_ADD_EXECUTABLE(skroogecompanion
+    main.cpp
+    skgmaccountlistmodel.cpp
+    skgmreferenceproxymodel.cpp
+    skgmtransactionlistmodel.cpp
+    skgmcontroler.cpp
+)
+
+QT_ADD_QML_MODULE(skroogecompanion
+    URI org.kde.skroogecompanion
+    VERSION 1.0
+    NO_IMPORT_SCAN
+    QML_FILES
+        qml/Main.qml
+        qml/DashboardPage.qml
+        qml/AccountsPage.qml
+        qml/TransactionsPage.qml
+        qml/TrackersPage.qml
+        qml/PayeesPage.qml
+        qml/CategoriesPage.qml
+        qml/UnitsPage.qml
+        qml/AboutPage.qml
+        qml/SubTransactionsPage.qml
+        qml/AccountListItem.qml
+        qml/TransactionListItem.qml
+        qml/SubTransactionListItem.qml
+        qml/PayeeListItem.qml
+        qml/UnitListItem.qml
+        qml/CategoryListItem.qml
+        qml/TrackerListItem.qml
+)
+
+TARGET_LINK_LIBRARIES(skroogecompanion
+    PRIVATE
+        KF6::I18n
+        KF6::Kirigami
+        Qt6::Quick
+        Qt6::QuickControls2
+        skgbasemodeler
+        skgbankmodeler
+        skgbasegui
+)
+
+IF(ANDROID)
+    INCLUDE(ECMAddAndroidApk)
+    ecm_add_android_apk(skroogecompanion
+        ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
+    )
+ENDIF()
+
+########### install files ###############
+INSTALL(TARGETS skroogecompanion ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+INSTALL(PROGRAMS org.kde.skroogecompanion.desktop  DESTINATION  ${KDE_INSTALL_APPDIR} )
+INSTALL(FILES org.kde.skroogecompanion.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
diff --git a/skroogecompanion/README.android.md b/skroogecompanion/README.android.md
new file mode 100644
index 000000000..ec86a21e5
--- /dev/null
+++ b/skroogecompanion/README.android.md
@@ -0,0 +1,41 @@
+# Android Packaging (skroogecompanion)
+
+This target is prepared for KDE/Qt Android packaging using `ECMAddAndroidApk`.
+
+## Prerequisites
+
+- Android SDK + NDK
+- Qt 6 for Android
+- ECM toolchain (`/usr/share/ECM/toolchain/Android.cmake`)
+
+## Configure
+
+```bash
+cmake -S . -B build-android \
+  -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake \
+  -DCMAKE_BUILD_TYPE=Release
+```
+
+If Qt/3rd-party prefixes are outside the default Android sysroot, add:
+
+```bash
+-DECM_ADDITIONAL_FIND_ROOT_PATH="/path/to/qt-android;/path/to/other/prefix"
+```
+
+## Build APK
+
+```bash
+cmake --build build-android --target create-apk-skroogecompanion
+```
+
+APK output is generated in:
+
+- `build-android/skroogecompanion_build_apk/` (default), or
+- `ANDROID_APK_OUTPUT_DIR` if provided at configure time.
+
+## Signing / Installing
+
+Pass extra arguments to `androiddeployqt` through:
+
+- `-DANDROIDDEPLOYQT_EXTRA_ARGS="--sign /path/to/keystore alias"`
+- or generator-specific target args where supported.
diff --git a/skroogecompanion/android/AndroidManifest.xml b/skroogecompanion/android/AndroidManifest.xml
new file mode 100644
index 000000000..ddee4649a
--- /dev/null
+++ b/skroogecompanion/android/AndroidManifest.xml
@@ -0,0 +1,58 @@
+<?xml version='1.0' encoding='utf-8'?>
+<!--
+ SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.kde.skroogecompanion" android:versionName="-- %%INSERT_VERSION_NAME%% --" android:versionCode="-- %%INSERT_VERSION_CODE%% --" android:installLocation="auto">
+    <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="35"/>
+    <!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
+         Remove the comment if you do not require these default permissions. -->
+    <!-- %%INSERT_PERMISSIONS -->
+
+    <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
+    <uses-feature android:name="android.hardware.faketouch" android:required="false"/>
+    <uses-feature android:name="android.hardware.screen.landscape" android:required="false"/>
+    <uses-feature android:name="android.hardware.screen.portrait" android:required="false"/>
+
+    <application android:label="-- %%INSERT_APP_NAME%% --" android:name="org.qtproject.qt.android.bindings.QtApplication" android:hardwareAccelerated="true" android:allowBackup="true" android:fullBackupOnly="false" android:extractNativeLibs="true">
+        <activity android:name="org.qtproject.qt.android.bindings.QtActivity" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:launchMode="singleTop" android:screenOrientation="unspecified" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:mimeType="application/x-skg"/>
+                <data android:mimeType="application/x-skgc"/>
+            </intent-filter>
+        </activity>
+
+        <meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
+        <meta-data android:name="android.app.arguments" android:value="-- %%INSERT_APP_ARGUMENTS%% --"/>
+        <meta-data android:name="android.app.extract_android_style" android:value="minimal"/>
+
+        <!-- App dependencies -->
+        <meta-data android:name="android.app.lib_dependencies" android:value="-- %%INSERT_APP_LIB_DEPENDENCIES%% --"/>
+        <!-- Additional library paths -->
+        <meta-data android:name="android.app.system_libs_prefix" android:value="/system/lib64/"/>
+        <!-- Bundled Qt libs -->
+        <meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
+        <!-- Run with local libs -->
+        <meta-data android:name="android.app.use_local_qt_libs" android:value="1"/>
+        <meta-data android:name="android.app.libs_prefix" android:value="/lib"/>
+        <meta-data android:name="android.app.load_local_libs" android:value="-- %%INSERT_LOCAL_LIBS%% --"/>
+        <!-- Bundled libs -->
+        <meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
+        <meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
+        <!--  Messages maps -->
+        <meta-data android:value="-- %%INSERT_STRING_ARGS%% --" android:name="android.app.arguments"/>
+        <!-- Background running -->
+        <meta-data android:name="android.app.background_running" android:value="false"/>
+        <!-- Splash screen -->
+        <meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/>
+        <!--  OpenGL ES 2 -->
+        <meta-data android:name="android.app.useOpenGL" android:value="true"/>
+    </application>
+</manifest>
diff --git a/skroogecompanion/main.cpp b/skroogecompanion/main.cpp
new file mode 100644
index 000000000..b81c86813
--- /dev/null
+++ b/skroogecompanion/main.cpp
@@ -0,0 +1,101 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#include "skgmcontroler.h"
+#include "skgservices.h"
+#include "skgtraces.h"
+
+#include <kaboutdata.h>
+
+#include <klocalizedcontext.h>
+#include <klocalizedstring.h>
+
+#include <qcommandlineoption.h>
+#include <qcommandlineparser.h>
+#include <qguiapplication.h>
+#include <qicon.h>
+#include <qqmlapplicationengine.h>
+#include <qqmlcontext.h>
+
+/**
+ * To compute the version
+ */
+#define VER1_(x) #x
+/**
+ * To compute the version
+ */
+#define VER_(x) VER1_(x)
+/**
+ * To compute the version
+ */
+#define VER VER_(SKGVERSION)
+
+#ifdef Q_OS_ANDROID
+Q_DECL_EXPORT
+#endif
+int main(int argc, char *argv[])
+{
+    SKGTRACEINFUNC(1)
+    QGuiApplication app(argc, argv);
+    KLocalizedString::setApplicationDomain("skrooge");
+    const QIcon appIcon = SKGServices::fromTheme(QStringLiteral("skrooge"));
+    if (!appIcon.isNull()) {
+        app.setWindowIcon(appIcon);
+    }
+
+    KAboutData about(QStringLiteral("skrooge companion"),
+                     i18nc("The name of the application", "Skrooge Companion"),
+                     QStringLiteral(VER),
+                     i18nc("The description of the application", "The Skrooge companion application for mobile devices"),
+                     KAboutLicense::GPL_V3,
+                     i18nc("Fullname", "(c) 2007-%1 Stephane MANKOWSKI & Guillaume DE BURE", QDate::currentDate().toString(QStringLiteral("yyyy"))),
+                     QString(),
+                     QStringLiteral("https://skrooge.org"));
+
+    about.addAuthor(i18nc("Fullname", "Stephane MANKOWSKI"),
+                    i18nc("A job description", "Architect & Developer"),
+                    QStringLiteral("[email protected]"),
+                    QString(),
+                    QStringLiteral("miraks"));
+    about.setOtherText(i18nc("The description of the application",
+                             "The application name is inspired by Charles Dicken's tale <i>A Christmas Carol</i>, where the main character, Ebenezer Scrooge, "
+                             "a grumpy old narrow man, gets visited by three ghosts who change the way he sees the world, in a good way."));
+    about.setOrganizationDomain("kde.org");
+    about.setDesktopFileName(QStringLiteral("org.kde.skroogecompanion"));
+    KAboutData::setApplicationData(about);
+
+    QCommandLineParser parser;
+    parser.setApplicationDescription(i18nc("Application description", "Skrooge mobile accounts viewer"));
+    parser.addHelpOption();
+    parser.addPositionalArgument(QStringLiteral("file"), i18nc("CLI argument description", "Skrooge file to open"));
+    QCommandLineOption passwordOption(QStringList() << QStringLiteral("p") << QStringLiteral("password"),
+                                      i18nc("CLI option description", "Password for encrypted Skrooge file"),
+                                      QStringLiteral("password"));
+    parser.addOption(passwordOption);
+    parser.process(app);
+
+    SKGMControler controller;
+    if (!parser.positionalArguments().isEmpty()) {
+        controller.setFilePath(parser.positionalArguments().constFirst());
+    }
+    if (parser.isSet(passwordOption)) {
+        controller.setPassword(parser.value(passwordOption));
+    }
+
+    QQmlApplicationEngine engine;
+    engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
+    engine.rootContext()->setContextProperty(QStringLiteral("mobileController"), &controller);
+    engine.loadFromModule(QStringLiteral("org.kde.skroogecompanion"), QStringLiteral("Main"));
+    if (engine.rootObjects().isEmpty()) {
+        return -1;
+    }
+
+    if (!controller.filePath().isEmpty()) {
+        if (controller.openFile()) {
+            controller.ensureDataForPage(0);
+        }
+    }
+
+    return app.exec();
+}
diff --git a/skroogecompanion/org.kde.skroogecompanion.appdata.xml b/skroogecompanion/org.kde.skroogecompanion.appdata.xml
new file mode 100644
index 000000000..12f64fca1
--- /dev/null
+++ b/skroogecompanion/org.kde.skroogecompanion.appdata.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<component type="desktop">
+  <id>org.kde.skroogecompanion</id>
+  <metadata_license>CC0-1.0</metadata_license>
+  <project_license>GPL-3.0-or-later</project_license>
+  <developer id="org.kde">
+    <name>KDE Community</name>
+  </developer>
+  <name>Skrooge Companion</name>
+  <summary>Mobile companion application for Skrooge files</summary>
+  <description>
+    <p>Skrooge Companion is a mobile viewer for Skrooge files. It lets you consult dashboard indicators, accounts and transactions on Android devices.</p>
+  </description>
+  <launchable type="desktop-id">org.kde.skroogecompanion.desktop</launchable>
+  <url type="homepage">https://skrooge.org</url>
+  <url type="bugtracker">https://invent.kde.org/office/skrooge/-/issues</url>
+  <content_rating type="oars-1.1"/>
+</component>
diff --git a/skroogecompanion/org.kde.skroogecompanion.desktop b/skroogecompanion/org.kde.skroogecompanion.desktop
new file mode 100644
index 000000000..d372081a9
--- /dev/null
+++ b/skroogecompanion/org.kde.skroogecompanion.desktop
@@ -0,0 +1,16 @@
+[Desktop Entry]
+# SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+# SPDX-License-Identifier: GPL-3.0-or-later
+Name=skroogecompanion
+Exec=skroogecompanion %F
+Icon=skrooge
+Type=Application
+GenericName=Manage your money
+Comment=Skrooge is a personal finances manager powered by KDE, aiming at being simple and intuitive.
+Terminal=false
+X-KDE-StartupNotify=true
+Categories=Qt;KDE;Office;Finance;
+MimeType=application/x-skg;application/x-skgc;application/x-kmymoney;application/vnd.intu.qfx;application/x-ofx;application/x-homebank;application/x-gnc;application/x-gnucash;application/x-gsb;
+X-DBUS-StartupType=Unique
+X-DBUS-ServiceName=org.kde.skroogecompanion
+X-DocPath=skrooge/index.html
diff --git a/skroogecompanion/qml/AboutPage.qml b/skroogecompanion/qml/AboutPage.qml
new file mode 100644
index 000000000..a688c09ec
--- /dev/null
+++ b/skroogecompanion/qml/AboutPage.qml
@@ -0,0 +1,11 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import org.kde.coreaddons as Core
+import org.kde.kirigamiaddons.formcard as FormCard
+
+FormCard.AboutPage {
+    aboutData: Core.AboutData
+}
diff --git a/skroogecompanion/qml/AccountListItem.qml b/skroogecompanion/qml/AccountListItem.qml
new file mode 100644
index 000000000..e5c2dc7d2
--- /dev/null
+++ b/skroogecompanion/qml/AccountListItem.qml
@@ -0,0 +1,114 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+Item {
+    id: root
+    property var rowModel
+    property bool isCollapsed: false
+    property bool isTypeHeader: rowModel.kind === 1
+    property bool isTypeTotal: rowModel.kind === 2
+    property bool rowVisible: isTypeHeader || !isCollapsed
+
+    signal toggleTypeRequested(string typeName)
+    signal openTransactionsRequested(string bankName, string accountName)
+
+    implicitHeight: rowVisible ? (typeHeader.visible ? typeHeader.implicitHeight + 8 : rowFrame.implicitHeight + 8) : 0
+    visible: rowVisible
+
+    AbstractButton {
+        id: typeHeader
+        visible: root.isTypeHeader
+        width: root.width
+        implicitHeight: 40
+        onClicked: root.toggleTypeRequested(root.rowModel.type)
+
+        background: Rectangle {
+            color: Kirigami.Theme.alternateBackgroundColor
+            radius: Kirigami.Units.smallSpacing
+        }
+
+        contentItem: Item {
+            anchors.fill: parent
+            anchors.margins: 6
+
+            Label {
+                id: typeTitle
+                anchors.left: parent.left
+                anchors.verticalCenter: parent.verticalCenter
+                anchors.right: typeAmount.left
+                anchors.rightMargin: 8
+                text: (root.isCollapsed ? "\u25B6  " : "\u25BC  ") + root.rowModel.name
+                font.bold: true
+                color: Kirigami.Theme.linkColor
+                elide: Label.ElideRight
+            }
+
+            Label {
+                id: typeAmount
+                anchors.right: parent.right
+                anchors.verticalCenter: parent.verticalCenter
+                text: root.rowModel.amountText
+                horizontalAlignment: Text.AlignRight
+                font.bold: true
+                color: Kirigami.Theme.linkColor
+            }
+        }
+    }
+
+    Frame {
+        id: rowFrame
+        visible: !root.isTypeHeader && root.rowVisible
+        width: root.width
+        padding: 10
+        opacity: (!root.isTypeTotal && root.closed) ? 0.5 : 1.0
+        implicitHeight: rowContent.implicitHeight + (padding * 2)
+
+        ColumnLayout {
+            id: rowContent
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.top: parent.top
+            spacing: 2
+
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 8
+
+                Label {
+                    Layout.fillWidth: true
+                    text: root.isTypeTotal ? i18nc("@info:total by account type", "%1 - %2", root.rowModel.type, root.rowModel.name) : root.rowModel.name
+                    font.bold: true
+                    elide: Label.ElideRight
+                }
+
+                Label {
+                    text: root.rowModel.amountText
+                    horizontalAlignment: Text.AlignRight
+                    color: root.isTypeTotal ? Kirigami.Theme.linkColor : (root.rowModel.amount >= 0 ? "#1b5e20" : "#b71c1c")
+                    font.bold: true
+                }
+
+                ToolButton {
+                    visible: !root.isTypeTotal
+                    icon.name: "application-menu"
+                    display: AbstractButton.IconOnly
+                    onClicked: root.openTransactionsRequested(root.rowModel.bank, root.rowModel.name)
+                }
+            }
+
+            Label {
+                Layout.fillWidth: true
+                visible: !root.isTypeTotal
+                text: i18nc("@info:account subtitle", "%1 - %2%3", root.rowModel.bank, root.rowModel.type, root.rowModel.closed ? i18nc("@info:account status", " (Closed)") : "")
+                elide: Label.ElideRight
+                opacity: 0.7
+            }
+        }
+    }
+}
diff --git a/skroogecompanion/qml/AccountsPage.qml b/skroogecompanion/qml/AccountsPage.qml
new file mode 100644
index 000000000..12cb4eabf
--- /dev/null
+++ b/skroogecompanion/qml/AccountsPage.qml
@@ -0,0 +1,80 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+ListView {
+    id: accountsList
+    clip: true
+    spacing: 0
+    model: mobileController.accountsModel
+    property var collapsedTypes: ({})
+    property int collapsedRevision: 0
+    signal transactionPageRequested()
+
+    header: Item {
+        width: accountsList.width
+        implicitHeight: showClosedSwitch.implicitHeight + globalTotalRow.implicitHeight + 8
+
+        Switch {
+            id: showClosedSwitch
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.top: parent.top
+            text: i18nc("@option:check", "Show closed accounts")
+            checked: mobileController.showClosedAccounts
+            onToggled: mobileController.showClosedAccounts = checked
+        }
+
+        RowLayout {
+            id: globalTotalRow
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.top: showClosedSwitch.bottom
+            anchors.topMargin: 2
+            spacing: 8
+
+            Label {
+                Layout.fillWidth: true
+                text: i18nc("@label", "Total")
+                font.bold: true
+            }
+
+            Label {
+                text: mobileController.globalAccountsTotalText
+                horizontalAlignment: Text.AlignRight
+                font.bold: true
+            }
+        }
+    }
+
+    function isTypeCollapsed(typeName) {
+        return collapsedTypes[typeName] === true
+    }
+
+    function toggleType(typeName) {
+        collapsedTypes[typeName] = !isTypeCollapsed(typeName)
+        collapsedRevision = collapsedRevision + 1
+    }
+
+    delegate: AccountListItem {
+        width: ListView.view.width
+        property bool collapsedForType: {
+            accountsList.collapsedRevision
+            return accountsList.isTypeCollapsed(model.type)
+        }
+        rowModel: model
+        isCollapsed: collapsedForType
+        onToggleTypeRequested: function(typeName) {
+            accountsList.toggleType(typeName)
+        }
+        onOpenTransactionsRequested: function(bankName, accountName) {
+            if (mobileController.selectOpenAccount(bankName, accountName)) {
+                accountsList.transactionPageRequested()
+            }
+        }
+    }
+}
diff --git a/skroogecompanion/qml/CategoriesPage.qml b/skroogecompanion/qml/CategoriesPage.qml
new file mode 100644
index 000000000..5739769bc
--- /dev/null
+++ b/skroogecompanion/qml/CategoriesPage.qml
@@ -0,0 +1,33 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+ListView {
+    id: categoriesList
+    clip: true
+    spacing: 8
+    model: mobileController.categoriesModel
+    signal subTransactionsPageRequested()
+
+    delegate: CategoryListItem {
+        width: ListView.view.width
+        rowModel: model
+        onMenuRequested: function(categoryName) {
+            if (mobileController.selectCategory(categoryName)) {
+                categoriesList.subTransactionsPageRequested()
+            }
+        }
+    }
+
+    Kirigami.PlaceholderMessage {
+        anchors.centerIn: parent
+        width: parent.width - Kirigami.Units.gridUnit * 2
+        visible: parent.count === 0
+        text: i18nc("@info", "No category to display")
+    }
+}
diff --git a/skroogecompanion/qml/CategoryListItem.qml b/skroogecompanion/qml/CategoryListItem.qml
new file mode 100644
index 000000000..0343f624c
--- /dev/null
+++ b/skroogecompanion/qml/CategoryListItem.qml
@@ -0,0 +1,52 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Frame {
+    id: root
+    property var rowModel
+
+    signal menuRequested(string categoryName)
+
+    padding: 10
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 2
+
+        RowLayout {
+            Layout.fillWidth: true
+
+            Label {
+                Layout.fillWidth: true
+                text: root.rowModel.name
+                font.bold: true
+                elide: Label.ElideRight
+            }
+
+            Label {
+                text: root.rowModel.amountText
+                color: root.rowModel.amount >= 0 ? "#1b5e20" : "#b71c1c"
+                font.bold: true
+            }
+
+            ToolButton {
+                icon.name: "application-menu"
+                display: AbstractButton.IconOnly
+                onClicked: root.menuRequested(root.rowModel.name)
+            }
+        }
+
+        Label {
+            Layout.fillWidth: true
+            visible: root.rowModel.subtitle !== ""
+            text: root.rowModel.subtitle
+            opacity: 0.7
+            elide: Label.ElideRight
+        }
+    }
+}
diff --git a/skroogecompanion/qml/DashboardPage.qml b/skroogecompanion/qml/DashboardPage.qml
new file mode 100644
index 000000000..823e8133e
--- /dev/null
+++ b/skroogecompanion/qml/DashboardPage.qml
@@ -0,0 +1,216 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+ScrollView {
+    id: dashboardScroll
+    clip: true
+    contentWidth: availableWidth
+
+    ColumnLayout {
+        width: dashboardScroll.availableWidth
+        spacing: Kirigami.Units.smallSpacing
+
+        Frame {
+            Layout.fillWidth: true
+
+            ColumnLayout {
+                anchors.fill: parent
+                spacing: 8
+
+                Label {
+                    text: i18nc("A monthly report title", "Personal Finance Score")
+                    font.bold: true
+                }
+
+                RowLayout {
+                    Layout.fillWidth: true
+                    spacing: 10
+
+                    Rectangle {
+                        Layout.preferredWidth: Kirigami.Units.gridUnit * 3
+                        Layout.preferredHeight: Kirigami.Units.gridUnit * 3
+                        radius: width / 8
+                        color: mobileController.dashboardPersonalFinanceScoreColor
+
+                        Label {
+                            anchors.centerIn: parent
+                            text: mobileController.dashboardPersonalFinanceScoreText
+                            font.bold: true
+                            color: "white"
+                        }
+                    }
+
+                    ColumnLayout {
+                        Layout.fillWidth: true
+                        spacing: 2
+
+                        Label {
+                            Layout.fillWidth: true
+                            text: i18nc("@info", "%1 / %2", i18nc("A monthly report title", "Net Worth"), i18nc("A monthly report title", "Annual Spending"))
+                            opacity: 0.7
+                        }
+                        Label {
+                            Layout.fillWidth: true
+                            text: i18nc("@info", "%1 / %2", mobileController.dashboardNetWorthText, mobileController.dashboardAnnualSpendingText)
+                            font.bold: true
+                            elide: Label.ElideRight
+                        }
+                        Label {
+                            Layout.fillWidth: true
+                            text: mobileController.dashboardPersonalFinanceScoreMessage
+                            wrapMode: Text.WordWrap
+                        }
+                    }
+                }
+            }
+        }
+
+        Frame {
+            Layout.fillWidth: true
+
+            ColumnLayout {
+                anchors.fill: parent
+                spacing: 6
+
+                Label {
+                    text: i18nc("Dashboard widget title", "Income & Expenditure")
+                    font.bold: true
+                }
+
+                GridLayout {
+                    Layout.fillWidth: true
+                    columns: 3
+                    columnSpacing: 8
+                    rowSpacing: 4
+
+                    Label {
+                        text: ""
+                    }
+                    Label {
+                        text: mobileController.dashboardPreviousPeriodLabel
+                        font.bold: true
+                        horizontalAlignment: Text.AlignRight
+                        Layout.alignment: Qt.AlignRight
+                    }
+                    Label {
+                        text: mobileController.dashboardCurrentPeriodLabel
+                        font.bold: true
+                        horizontalAlignment: Text.AlignRight
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    Label {
+                        text: i18nc("@label", "Incomes")
+                    }
+                    Label {
+                        text: mobileController.dashboardPreviousIncomeText
+                        color: "#1b5e20"
+                        horizontalAlignment: Text.AlignRight
+                        Layout.alignment: Qt.AlignRight
+                    }
+                    Label {
+                        text: mobileController.dashboardCurrentIncomeText
+                        color: "#1b5e20"
+                        horizontalAlignment: Text.AlignRight
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    Label {
+                        text: i18nc("@label", "Expenditures")
+                    }
+                    Label {
+                        text: mobileController.dashboardPreviousExpenseText
+                        color: "#b71c1c"
+                        horizontalAlignment: Text.AlignRight
+                        Layout.alignment: Qt.AlignRight
+                    }
+                    Label {
+                        text: mobileController.dashboardCurrentExpenseText
+                        color: "#b71c1c"
+                        horizontalAlignment: Text.AlignRight
+                        Layout.alignment: Qt.AlignRight
+                    }
+
+                    Label {
+                        text: i18nc("@label", "Savings")
+                    }
+                    Label {
+                        text: mobileController.dashboardPreviousSavingsText
+                        color: mobileController.dashboardPreviousSavings >= 0 ? "#1b5e20" : "#b71c1c"
+                        horizontalAlignment: Text.AlignRight
+                        Layout.alignment: Qt.AlignRight
+                    }
+                    Label {
+                        text: mobileController.dashboardCurrentSavingsText
+                        color: mobileController.dashboardCurrentSavings >= 0 ? "#1b5e20" : "#b71c1c"
+                        horizontalAlignment: Text.AlignRight
+                        Layout.alignment: Qt.AlignRight
+                    }
+                }
+            }
+        }
+
+        Frame {
+            Layout.fillWidth: true
+
+            ColumnLayout {
+                anchors.fill: parent
+                spacing: 6
+
+                Label {
+                    text: i18nc("@title", "Bank")
+                    font.bold: true
+                }
+
+                Repeater {
+                    model: mobileController.dashboardBankTotals
+
+                    delegate: RowLayout {
+                        Layout.fillWidth: true
+                        spacing: 8
+
+                        Label {
+                            Layout.fillWidth: true
+                            text: modelData.bank
+                            elide: Label.ElideRight
+                        }
+
+                        Label {
+                            text: modelData.amountText
+                            font.bold: true
+                            color: modelData.amount >= 0 ? "#1b5e20" : "#b71c1c"
+                        }
+                    }
+                }
+
+                RowLayout {
+                    Layout.fillWidth: true
+                    spacing: 8
+
+                    Label {
+                        Layout.fillWidth: true
+                        text: i18nc("@label", "Total")
+                        font.bold: true
+                    }
+
+                    Label {
+                        text: mobileController.dashboardBankGrandTotalText
+                        font.bold: true
+                    }
+                }
+
+                Label {
+                    visible: mobileController.dashboardBankTotals.length === 0
+                    text: i18nc("@info", "No bank to display")
+                    opacity: 0.7
+                }
+            }
+        }
+    }
+}
diff --git a/skroogecompanion/qml/Main.qml b/skroogecompanion/qml/Main.qml
new file mode 100644
index 000000000..764814fa5
--- /dev/null
+++ b/skroogecompanion/qml/Main.qml
@@ -0,0 +1,266 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Dialogs
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+Kirigami.ApplicationWindow {
+    id: root
+    property int currentPageIndex: 0
+    property bool subTransactionsVisible: false
+    readonly property string currentPageTitle: subTransactionsVisible ? i18nc("@title:tab", "Sub-transactions")
+        : currentPageIndex === 0 ? i18nc("@title:tab", "Dashboard")
+        : currentPageIndex === 1 ? i18nc("@title:tab", "Accounts")
+        : currentPageIndex === 2 ? i18nc("@title:tab", "Transactions")
+        : currentPageIndex === 3 ? i18nc("@title:tab", "Trackers")
+        : currentPageIndex === 4 ? i18nc("@title:tab", "Payees")
+        : currentPageIndex === 5 ? i18nc("@title:tab", "Categories")
+        : currentPageIndex === 6 ? i18nc("@title:tab", "Units")
+        : currentPageIndex === 7 ? i18nc("@title:tab", "About")
+        : i18nc("@title", "Skrooge Companion")
+    function setMainPage(pageIndex) {
+        subTransactionsVisible = false
+        currentPageIndex = pageIndex
+    }
+    function openSubTransactionsPage() {
+        subTransactionsVisible = true
+        mobileController.ensureDataForPage(8)
+    }
+    width: 420
+    height: 780
+    visible: true
+    title: i18nc("@title:window", "Skrooge Companion")
+    onCurrentPageIndexChanged: mobileController.ensureDataForPage(currentPageIndex)
+    Component.onCompleted: mobileController.ensureDataForPage(currentPageIndex)
+    globalDrawer: Kirigami.GlobalDrawer {
+        collapsible: false
+        collapsed: false
+        collapseButtonVisible: false
+        actions: [
+            Kirigami.Action {
+                icon.name: "view-dashboard"
+                text: i18nc("@title:tab", "Dashboard")
+                tooltip: ""
+                onTriggered: root.setMainPage(0)
+            },
+            Kirigami.Action {
+                icon.name: "view-list-tree"
+                text: i18nc("@title:tab", "Accounts")
+                tooltip: ""
+                onTriggered: root.setMainPage(1)
+            },
+            Kirigami.Action {
+                icon.name: "view-pim-journal"
+                text: i18nc("@title:tab", "Transactions")
+                tooltip: ""
+                onTriggered: root.setMainPage(2)
+            },
+            Kirigami.Action {
+                icon.name: "edit-undo"
+                text: i18nc("@title:tab", "Trackers")
+                tooltip: ""
+                onTriggered: root.setMainPage(3)
+            },
+            Kirigami.Action {
+                icon.name: "im-user"
+                text: i18nc("@title:tab", "Payees")
+                tooltip: ""
+                onTriggered: root.setMainPage(4)
+            },
+            Kirigami.Action {
+                icon.name: "view-list-text"
+                text: i18nc("@title:tab", "Categories")
+                tooltip: ""
+                onTriggered: root.setMainPage(5)
+            },
+            Kirigami.Action {
+                icon.name: "currency-exchange"
+                text: i18nc("@title:tab", "Units")
+                tooltip: ""
+                onTriggered: root.setMainPage(6)
+            },
+
+            Kirigami.Action {
+                separator: true
+            },
+            Kirigami.Action {
+                icon.name: "settings-configure"
+                text: i18nc("@action:button", "Settings")
+                tooltip: ""
+                onTriggered: {
+                    filePathField.text = mobileController.filePath
+                    passwordField.text = mobileController.password
+                    settingsDialog.open()
+                }
+            },            
+            Kirigami.Action {
+                icon.name: "help-about"
+                text: i18nc("@title:tab", "About")
+                tooltip: ""
+                onTriggered: root.setMainPage(7)
+            },
+        ]
+    }
+    pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.None
+    pageStack.initialPage: Kirigami.Page {
+        id: mainPage
+        title: root.currentPageTitle
+
+        ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: Kirigami.Units.largeSpacing
+            spacing: Kirigami.Units.smallSpacing
+
+        Kirigami.Heading {
+            Layout.fillWidth: true
+            level: 1
+            text: root.currentPageTitle
+        }
+
+        TextField {
+            Layout.fillWidth: true
+            visible: (root.currentPageIndex >= 1 && root.currentPageIndex <= 6) || root.subTransactionsVisible
+            placeholderText: i18nc("@info:placeholder", "Filter")
+            text: mobileController.globalFilterText
+            onTextChanged: mobileController.globalFilterText = text
+        }
+
+        Kirigami.InlineMessage {
+            visible: mobileController.errorMessage.length > 0
+            Layout.fillWidth: true
+            text: mobileController.errorMessage
+            type: Kirigami.MessageType.Error
+        }
+
+        SwipeView {
+            id: mainSwipeView
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            clip: true
+            visible: !root.subTransactionsVisible
+            currentIndex: root.currentPageIndex
+            onCurrentIndexChanged: {
+                if (!root.subTransactionsVisible) {
+                    root.currentPageIndex = currentIndex
+                }
+            }
+
+            DashboardPage {}
+
+            AccountsPage {
+                onTransactionPageRequested: root.setMainPage(2)
+            }
+
+            TransactionsPage {
+                onSubTransactionsPageRequested: root.openSubTransactionsPage()
+            }
+
+            TrackersPage {
+                onSubTransactionsPageRequested: root.openSubTransactionsPage()
+            }
+
+            PayeesPage {}
+
+            CategoriesPage {
+                onSubTransactionsPageRequested: root.openSubTransactionsPage()
+            }
+
+            UnitsPage {}
+
+            AboutPage {}
+        }
+
+        ColumnLayout {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            visible: root.subTransactionsVisible
+            spacing: Kirigami.Units.smallSpacing
+
+            Button {
+                text: i18nc("@action:button", "Back")
+                icon.name: "go-previous"
+                onClicked: root.subTransactionsVisible = false
+            }
+
+            SubTransactionsPage {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+            }
+        }
+        }
+
+        Kirigami.Dialog {
+            id: settingsDialog
+            width: parent.width
+            title: i18nc("@title:window", "Settings")
+            preferredWidth: Kirigami.Units.gridUnit * 26
+            standardButtons: Kirigami.Dialog.Cancel
+
+            customFooterActions: [
+                Kirigami.Action {
+                    icon.name: "document-open"
+                    text: i18nc("@action:button", "Open file")
+                    onTriggered: {
+                        mobileController.filePath = filePathField.text
+                        mobileController.password = passwordField.text
+                        if (mobileController.openFile()) {
+                            mobileController.ensureDataForPage(root.currentPageIndex)
+                            settingsDialog.close()
+                        } else if (mobileController.passwordRequired) {
+                            passwordField.forceActiveFocus()
+                            passwordField.selectAll()
+                        }
+                    }
+                }
+            ]
+
+            Kirigami.FormLayout {
+                width: parent.width
+                TextField {
+                    id: filePathField
+                    width: parent.width
+                    Kirigami.FormData.label: i18nc("@label:textbox", "File:")
+                    placeholderText: i18nc("@info:placeholder", "Path to .skg file")
+                }
+
+                Button {
+                    text: i18nc("@action:button", "Browse…")
+                    width: parent.width
+                    onClicked: fileDialog.open()
+                }
+
+                TextField {
+                    id: passwordField
+                    width: parent.width
+                    Kirigami.FormData.label: i18nc("@label:textbox", "Password:")
+                    placeholderText: i18nc("@info:placeholder", "Password (optional)")
+                    echoMode: TextInput.Password
+                }
+            }
+        }
+
+        FileDialog {
+            id: fileDialog
+            title: i18nc("@title:window", "Select Skrooge file")
+            nameFilters: [i18nc("@item:inlistbox", "Skrooge files (*.skg)")]
+            onAccepted: filePathField.text = decodeURIComponent(selectedFile.toString().replace("file://", ""))
+        }
+
+        Connections {
+            target: mobileController
+            function onPasswordRequiredChanged() {
+                if (!mobileController.passwordRequired) {
+                    return
+                }
+                filePathField.text = mobileController.filePath
+                passwordField.text = ""
+                settingsDialog.open()
+                passwordField.forceActiveFocus()
+            }
+        }
+    }
+}
diff --git a/skroogecompanion/qml/PayeeListItem.qml b/skroogecompanion/qml/PayeeListItem.qml
new file mode 100644
index 000000000..1575ed39d
--- /dev/null
+++ b/skroogecompanion/qml/PayeeListItem.qml
@@ -0,0 +1,44 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Frame {
+    id: root
+    property var rowModel
+
+    padding: 10
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 2
+
+        RowLayout {
+            Layout.fillWidth: true
+
+            Label {
+                Layout.fillWidth: true
+                text: root.rowModel.name
+                font.bold: true
+                elide: Label.ElideRight
+            }
+
+            Label {
+                text: root.rowModel.amountText
+                color: root.rowModel.amount >= 0 ? "#1b5e20" : "#b71c1c"
+                font.bold: true
+            }
+        }
+
+        Label {
+            Layout.fillWidth: true
+            visible: root.rowModel.subtitle !== ""
+            text: root.rowModel.subtitle
+            opacity: 0.7
+            elide: Label.ElideRight
+        }
+    }
+}
diff --git a/skroogecompanion/qml/PayeesPage.qml b/skroogecompanion/qml/PayeesPage.qml
new file mode 100644
index 000000000..5f029e45a
--- /dev/null
+++ b/skroogecompanion/qml/PayeesPage.qml
@@ -0,0 +1,24 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import org.kde.kirigami as Kirigami
+
+ListView {
+    clip: true
+    spacing: 8
+    model: mobileController.payeesModel
+
+    delegate: PayeeListItem {
+        width: ListView.view.width
+        rowModel: model
+    }
+
+    Kirigami.PlaceholderMessage {
+        anchors.centerIn: parent
+        width: parent.width - Kirigami.Units.gridUnit * 2
+        visible: parent.count === 0
+        text: i18nc("@info", "No payee to display")
+    }
+}
diff --git a/skroogecompanion/qml/SubTransactionListItem.qml b/skroogecompanion/qml/SubTransactionListItem.qml
new file mode 100644
index 000000000..a4f2573e5
--- /dev/null
+++ b/skroogecompanion/qml/SubTransactionListItem.qml
@@ -0,0 +1,56 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Frame {
+    id: root
+    property var rowModel
+
+    padding: 10
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 2
+
+        RowLayout {
+            Layout.fillWidth: true
+
+            Label {
+                Layout.fillWidth: true
+                text: i18nc("@info:subtransaction first line", "%1 - %2", root.rowModel.date, root.rowModel.payee !== "" ? root.rowModel.payee : i18nc("@label", "Sub-transaction"))
+                font.bold: true
+                elide: Label.ElideRight
+            }
+
+            Label {
+                text: root.rowModel.amountText
+                color: root.rowModel.amount >= 0 ? "#1b5e20" : "#b71c1c"
+                font.bold: true
+            }
+        }
+
+        Label {
+            Layout.fillWidth: true
+            text: root.rowModel.comment !== ""
+                ? i18nc("@info:subtransaction second line", "%1 - %2", root.rowModel.category, root.rowModel.comment)
+                : root.rowModel.category
+            opacity: 0.7
+            elide: Label.ElideRight
+        }
+
+        Label {
+            Layout.fillWidth: true
+            text: root.rowModel.number !== ""
+                ? (root.rowModel.mode !== ""
+                    ? i18nc("@info:subtransaction third line", "%1 - %2", root.rowModel.mode, root.rowModel.number)
+                    : root.rowModel.number)
+                : root.rowModel.mode
+            opacity: 0.7
+            elide: Label.ElideRight
+        }
+    }
+}
diff --git a/skroogecompanion/qml/SubTransactionsPage.qml b/skroogecompanion/qml/SubTransactionsPage.qml
new file mode 100644
index 000000000..a4bb15a2c
--- /dev/null
+++ b/skroogecompanion/qml/SubTransactionsPage.qml
@@ -0,0 +1,40 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+ColumnLayout {
+    spacing: Kirigami.Units.smallSpacing
+
+    Label {
+        Layout.fillWidth: true
+        visible: mobileController.selectedTrackerName !== ""
+        text: i18nc("@info", "Selection: %1", mobileController.selectedTrackerName)
+        opacity: 0.8
+        elide: Label.ElideRight
+    }
+
+    ListView {
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+        clip: true
+        spacing: 8
+        model: mobileController.subTransactionsModel
+
+        delegate: SubTransactionListItem {
+            width: ListView.view.width
+            rowModel: model
+        }
+
+        Kirigami.PlaceholderMessage {
+            anchors.centerIn: parent
+            width: parent.width - Kirigami.Units.gridUnit * 2
+            visible: parent.count === 0
+            text: i18nc("@info", "No sub-transaction to display")
+        }
+    }
+}
diff --git a/skroogecompanion/qml/TrackerListItem.qml b/skroogecompanion/qml/TrackerListItem.qml
new file mode 100644
index 000000000..85c1e307c
--- /dev/null
+++ b/skroogecompanion/qml/TrackerListItem.qml
@@ -0,0 +1,53 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Frame {
+    id: root
+    property var rowModel
+
+    signal menuRequested(string trackerName)
+
+    padding: 10
+    opacity: root.rowModel.closed ? 0.5 : 1.0
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 2
+
+        RowLayout {
+            Layout.fillWidth: true
+
+            Label {
+                Layout.fillWidth: true
+                text: root.rowModel.name
+                font.bold: true
+                elide: Label.ElideRight
+            }
+
+            Label {
+                text: root.rowModel.amountText
+                color: root.rowModel.amount >= 0 ? "#1b5e20" : "#b71c1c"
+                font.bold: true
+            }
+
+            ToolButton {
+                icon.name: "application-menu"
+                display: AbstractButton.IconOnly
+                onClicked: root.menuRequested(root.rowModel.name)
+            }
+        }
+
+        Label {
+            Layout.fillWidth: true
+            visible: root.rowModel.subtitle !== ""
+            text: root.rowModel.subtitle
+            opacity: 0.7
+            elide: Label.ElideRight
+        }
+    }
+}
diff --git a/skroogecompanion/qml/TrackersPage.qml b/skroogecompanion/qml/TrackersPage.qml
new file mode 100644
index 000000000..6105992ac
--- /dev/null
+++ b/skroogecompanion/qml/TrackersPage.qml
@@ -0,0 +1,47 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import org.kde.kirigami as Kirigami
+
+ListView {
+    id: trackersList
+    clip: true
+    spacing: 8
+    model: mobileController.trackersModel
+    signal subTransactionsPageRequested()
+
+    header: Item {
+        width: trackersList.width
+        implicitHeight: showClosedSwitch.implicitHeight + 8
+
+        Switch {
+            id: showClosedSwitch
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.top: parent.top
+            text: i18nc("@option:check", "Show closed trackers")
+            checked: mobileController.showClosedTrackers
+            onToggled: mobileController.showClosedTrackers = checked
+        }
+    }
+
+    delegate: TrackerListItem {
+        width: ListView.view.width
+        rowModel: model
+        onMenuRequested: function(trackerName) {
+            if (mobileController.selectTracker(trackerName)) {
+                trackersList.subTransactionsPageRequested()
+            }
+        }
+    }
+
+    Kirigami.PlaceholderMessage {
+        anchors.centerIn: parent
+        width: parent.width - Kirigami.Units.gridUnit * 2
+        visible: parent.count === 0
+        text: i18nc("@info", "No tracker to display")
+    }
+}
diff --git a/skroogecompanion/qml/TransactionListItem.qml b/skroogecompanion/qml/TransactionListItem.qml
new file mode 100644
index 000000000..985ae8efe
--- /dev/null
+++ b/skroogecompanion/qml/TransactionListItem.qml
@@ -0,0 +1,64 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Frame {
+    id: root
+    property var rowModel
+
+    signal openSubTransactionsRequested(int operationId)
+
+    padding: 10
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 2
+
+        RowLayout {
+            Layout.fillWidth: true
+
+            Label {
+                Layout.fillWidth: true
+                text: i18nc("@info:transaction first line", "%1 - %2", root.rowModel.date, root.rowModel.payee !== "" ? root.rowModel.payee : i18nc("@label", "Transaction"))
+                font.bold: true
+                elide: Label.ElideRight
+            }
+
+            Label {
+                text: root.rowModel.amountText
+                color: root.rowModel.amount >= 0 ? "#1b5e20" : "#b71c1c"
+                font.bold: true
+            }
+
+            ToolButton {
+                icon.name: "application-menu"
+                display: AbstractButton.IconOnly
+                onClicked: root.openSubTransactionsRequested(root.rowModel.operationId)
+            }
+        }
+
+        Label {
+            Layout.fillWidth: true
+            text: root.rowModel.comment !== ""
+                ? i18nc("@info:transaction second line", "%1 - %2", root.rowModel.category, root.rowModel.comment)
+                : root.rowModel.category
+            opacity: 0.7
+            elide: Label.ElideRight
+        }
+
+        Label {
+            Layout.fillWidth: true
+            text: root.rowModel.number !== ""
+                ? (root.rowModel.mode !== ""
+                    ? i18nc("@info:transaction third line", "%1 - %2", root.rowModel.mode, root.rowModel.number)
+                    : root.rowModel.number)
+                : root.rowModel.mode
+            opacity: 0.7
+            elide: Label.ElideRight
+        }
+    }
+}
diff --git a/skroogecompanion/qml/TransactionsPage.qml b/skroogecompanion/qml/TransactionsPage.qml
new file mode 100644
index 000000000..ab4710607
--- /dev/null
+++ b/skroogecompanion/qml/TransactionsPage.qml
@@ -0,0 +1,81 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+ColumnLayout {
+    id: transactionsPage
+    spacing: Kirigami.Units.smallSpacing
+    signal subTransactionsPageRequested()
+
+    RowLayout {
+        Layout.fillWidth: true
+        spacing: Kirigami.Units.smallSpacing
+
+        ComboBox {
+            Layout.fillWidth: true
+            model: [
+                i18nc("@item:inlistbox", "All statuses"),
+                i18nc("@item:inlistbox", "Pointed"),
+                i18nc("@item:inlistbox", "Not checked"),
+                i18nc("@item:inlistbox", "Checked")
+            ]
+            currentIndex: mobileController.transactionStatusFilter
+            onCurrentIndexChanged: mobileController.transactionStatusFilter = currentIndex
+        }
+    }
+
+    ComboBox {
+        id: accountCombo
+        Layout.fillWidth: true
+        model: mobileController.openAccountNames
+        currentIndex: mobileController.selectedOpenAccountIndex
+        onCurrentIndexChanged: mobileController.selectedOpenAccountIndex = currentIndex
+    }
+
+    RowLayout {
+        Layout.fillWidth: true
+        spacing: 8
+
+        Label {
+            Layout.fillWidth: true
+            text: i18nc("@label", "Balance")
+            font.bold: true
+        }
+
+        Label {
+            text: mobileController.selectedAccountBalanceText
+            horizontalAlignment: Text.AlignRight
+            font.bold: true
+        }
+    }
+
+    ListView {
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+        clip: true
+        spacing: 8
+        model: mobileController.transactionsModel
+
+        delegate: TransactionListItem {
+            width: ListView.view.width
+            rowModel: model
+            onOpenSubTransactionsRequested: function(operationId) {
+                if (mobileController.selectTransaction(operationId)) {
+                    transactionsPage.subTransactionsPageRequested()
+                }
+            }
+        }
+
+        Kirigami.PlaceholderMessage {
+            anchors.centerIn: parent
+            width: parent.width - Kirigami.Units.gridUnit * 2
+            visible: parent.count === 0
+            text: i18nc("@info", "No transaction to display")
+        }
+    }
+}
diff --git a/skroogecompanion/qml/UnitListItem.qml b/skroogecompanion/qml/UnitListItem.qml
new file mode 100644
index 000000000..1575ed39d
--- /dev/null
+++ b/skroogecompanion/qml/UnitListItem.qml
@@ -0,0 +1,44 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Frame {
+    id: root
+    property var rowModel
+
+    padding: 10
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 2
+
+        RowLayout {
+            Layout.fillWidth: true
+
+            Label {
+                Layout.fillWidth: true
+                text: root.rowModel.name
+                font.bold: true
+                elide: Label.ElideRight
+            }
+
+            Label {
+                text: root.rowModel.amountText
+                color: root.rowModel.amount >= 0 ? "#1b5e20" : "#b71c1c"
+                font.bold: true
+            }
+        }
+
+        Label {
+            Layout.fillWidth: true
+            visible: root.rowModel.subtitle !== ""
+            text: root.rowModel.subtitle
+            opacity: 0.7
+            elide: Label.ElideRight
+        }
+    }
+}
diff --git a/skroogecompanion/qml/UnitsPage.qml b/skroogecompanion/qml/UnitsPage.qml
new file mode 100644
index 000000000..23b7835e0
--- /dev/null
+++ b/skroogecompanion/qml/UnitsPage.qml
@@ -0,0 +1,24 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+import QtQuick
+import org.kde.kirigami as Kirigami
+
+ListView {
+    clip: true
+    spacing: 8
+    model: mobileController.unitsModel
+
+    delegate: UnitListItem {
+        width: ListView.view.width
+        rowModel: model
+    }
+
+    Kirigami.PlaceholderMessage {
+        anchors.centerIn: parent
+        width: parent.width - Kirigami.Units.gridUnit * 2
+        visible: parent.count === 0
+        text: i18nc("@info", "No unit to display")
+    }
+}
diff --git a/skroogecompanion/skgmaccountlistmodel.cpp b/skroogecompanion/skgmaccountlistmodel.cpp
new file mode 100644
index 000000000..2c664883e
--- /dev/null
+++ b/skroogecompanion/skgmaccountlistmodel.cpp
@@ -0,0 +1,75 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#include "skgmaccountlistmodel.h"
+
+SKGMAccountListModel::SKGMAccountListModel(QObject *iParent)
+    : QAbstractListModel(iParent)
+{
+}
+
+int SKGMAccountListModel::rowCount(const QModelIndex &iParent) const
+{
+    if (iParent.isValid()) {
+        return 0;
+    }
+    return m_accounts.count();
+}
+
+QVariant SKGMAccountListModel::data(const QModelIndex &iIndex, int iRole) const
+{
+    if (!iIndex.isValid() || iIndex.row() < 0 || iIndex.row() >= m_accounts.count()) {
+        return {};
+    }
+
+    const AccountItem &item = m_accounts.at(iIndex.row());
+    switch (iRole) {
+    case NameRole:
+        return item.name;
+    case BankRole:
+        return item.bank;
+    case TypeRole:
+        return item.type;
+    case UnitRole:
+        return item.unit;
+    case AmountTextRole:
+        return item.amountText;
+    case AmountRole:
+        return item.amount;
+    case ClosedRole:
+        return item.closed;
+    case KindRole:
+        return static_cast<int>(item.kind);
+    default:
+        return {};
+    }
+}
+
+QHash<int, QByteArray> SKGMAccountListModel::roleNames() const
+{
+    return {
+        {NameRole, "name"},
+        {BankRole, "bank"},
+        {TypeRole, "type"},
+        {UnitRole, "unit"},
+        {AmountTextRole, "amountText"},
+        {AmountRole, "amount"},
+        {ClosedRole, "closed"},
+        {KindRole, "kind"},
+    };
+}
+
+void SKGMAccountListModel::setAccounts(QVector<AccountItem> iAccounts)
+{
+    beginResetModel();
+    m_accounts = std::move(iAccounts);
+    endResetModel();
+}
+
+void SKGMAccountListModel::clear()
+{
+    beginResetModel();
+    m_accounts.clear();
+    endResetModel();
+}
diff --git a/skroogecompanion/skgmaccountlistmodel.h b/skroogecompanion/skgmaccountlistmodel.h
new file mode 100644
index 000000000..08b48191f
--- /dev/null
+++ b/skroogecompanion/skgmaccountlistmodel.h
@@ -0,0 +1,59 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#ifndef SKGMACCOUNTLISTMODEL_H
+#define SKGMACCOUNTLISTMODEL_H
+
+#include <QAbstractListModel>
+#include <QVector>
+
+class SKGMAccountListModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+public:
+    enum RowKind {
+        AccountRow = 0,
+        TypeHeaderRow = 1,
+        TypeTotalRow = 2
+    };
+    Q_ENUM(RowKind)
+
+    struct AccountItem {
+        QString name;
+        QString bank;
+        QString type;
+        QString unit;
+        QString amountText;
+        double amount{0.0};
+        bool closed{false};
+        RowKind kind{AccountRow};
+    };
+
+    enum AccountRoles {
+        NameRole = Qt::UserRole + 1,
+        BankRole,
+        TypeRole,
+        UnitRole,
+        AmountTextRole,
+        AmountRole,
+        ClosedRole,
+        KindRole
+    };
+    Q_ENUM(AccountRoles)
+
+    explicit SKGMAccountListModel(QObject *iParent = nullptr);
+
+    int rowCount(const QModelIndex &iParent = QModelIndex()) const override;
+    QVariant data(const QModelIndex &iIndex, int iRole = Qt::DisplayRole) const override;
+    QHash<int, QByteArray> roleNames() const override;
+
+    void setAccounts(QVector<AccountItem> iAccounts);
+    void clear();
+
+private:
+    QVector<AccountItem> m_accounts;
+};
+
+#endif
diff --git a/skroogecompanion/skgmcontroler.cpp b/skroogecompanion/skgmcontroler.cpp
new file mode 100644
index 000000000..d50096ce2
--- /dev/null
+++ b/skroogecompanion/skgmcontroler.cpp
@@ -0,0 +1,1271 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#include "skgmcontroler.h"
+
+#include "skgobjectmodelbase.h"
+#include "skgreportbank.h"
+#include "skgservices.h"
+#include "skgsortfilterproxymodel.h"
+
+#include <klocalizedstring.h>
+
+#include <QDate>
+#include <QSettings>
+#include <QVariantMap>
+#include <QtGlobal>
+#include <qfileinfo.h>
+#include <qlocale.h>
+#include <qstringbuilder.h>
+
+#include "skgtraces.h"
+#include <algorithm>
+#include <utility>
+
+SKGMControler::SKGMControler(QObject *iParent)
+    : QObject(iParent)
+{
+    SKGTRACEINFUNC(1)
+    m_accountsObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_account_display"), QStringLiteral("1=0"), nullptr);
+    m_transactionsObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_operation_display"), QStringLiteral("1=0"), nullptr);
+    m_subTransactionsObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_suboperation_consolidated"), QStringLiteral("1=0"), nullptr);
+    m_trackersObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_refund_display"), QStringLiteral("1=0"), nullptr);
+    m_payeesObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_payee_display"), QStringLiteral("1=0"), nullptr);
+    m_categoriesObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_category_display"), QStringLiteral("1=0"), nullptr);
+    m_unitsObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_unit_display"), QStringLiteral("1=0"), nullptr);
+    m_transactionsProxyModel = new SKGSortFilterProxyModel(this);
+    m_transactionsProxyModel->setSourceModel(m_transactionsObjectModel);
+    m_transactionsProxyModel->setProperty("filter", m_globalFilterText);
+    m_subTransactionsProxyModel = new SKGSortFilterProxyModel(this);
+    m_subTransactionsProxyModel->setSourceModel(m_subTransactionsObjectModel);
+    m_subTransactionsProxyModel->setProperty("filter", m_globalFilterText);
+    m_trackersProxyModel = new SKGSortFilterProxyModel(this);
+    m_trackersProxyModel->setSourceModel(m_trackersObjectModel);
+    m_trackersProxyModel->setProperty("filter", m_globalFilterText);
+    m_payeesProxyModel = new SKGSortFilterProxyModel(this);
+    m_payeesProxyModel->setSourceModel(m_payeesObjectModel);
+    m_payeesProxyModel->setProperty("filter", m_globalFilterText);
+    m_categoriesProxyModel = new SKGSortFilterProxyModel(this);
+    m_categoriesProxyModel->setSourceModel(m_categoriesObjectModel);
+    m_categoriesProxyModel->setProperty("filter", m_globalFilterText);
+    m_unitsProxyModel = new SKGSortFilterProxyModel(this);
+    m_unitsProxyModel->setSourceModel(m_unitsObjectModel);
+    m_unitsProxyModel->setProperty("filter", m_globalFilterText);
+
+    m_trackersModel.setKind(SKGMReferenceProxyModel::TrackerKind);
+    m_trackersModel.setObjectModel(m_trackersObjectModel);
+    m_trackersModel.setSourceModel(m_trackersProxyModel);
+    m_payeesModel.setKind(SKGMReferenceProxyModel::PayeeKind);
+    m_payeesModel.setObjectModel(m_payeesObjectModel);
+    m_payeesModel.setSourceModel(m_payeesProxyModel);
+    m_categoriesModel.setKind(SKGMReferenceProxyModel::CategoryKind);
+    m_categoriesModel.setObjectModel(m_categoriesObjectModel);
+    m_categoriesModel.setSourceModel(m_categoriesProxyModel);
+    m_unitsModel.setKind(SKGMReferenceProxyModel::UnitKind);
+    m_unitsModel.setObjectModel(m_unitsObjectModel);
+    m_unitsModel.setSourceModel(m_unitsProxyModel);
+    loadPersistedState();
+}
+
+SKGMControler::~SKGMControler()
+{
+    SKGTRACEINFUNC(1)
+    delete m_accountsObjectModel;
+    m_accountsObjectModel = nullptr;
+    delete m_transactionsObjectModel;
+    m_transactionsObjectModel = nullptr;
+    delete m_subTransactionsObjectModel;
+    m_subTransactionsObjectModel = nullptr;
+    delete m_trackersObjectModel;
+    m_trackersObjectModel = nullptr;
+    delete m_payeesObjectModel;
+    m_payeesObjectModel = nullptr;
+    delete m_categoriesObjectModel;
+    m_categoriesObjectModel = nullptr;
+    delete m_unitsObjectModel;
+    m_unitsObjectModel = nullptr;
+    m_transactionsProxyModel = nullptr;
+    m_subTransactionsProxyModel = nullptr;
+    m_trackersProxyModel = nullptr;
+    m_payeesProxyModel = nullptr;
+    m_categoriesProxyModel = nullptr;
+    m_unitsProxyModel = nullptr;
+}
+
+QString SKGMControler::filePath() const
+{
+    return m_filePath;
+}
+
+void SKGMControler::setFilePath(const QString &iFilePath)
+{
+    if (m_filePath == iFilePath) {
+        return;
+    }
+    m_filePath = iFilePath;
+    Q_EMIT filePathChanged();
+}
+
+QString SKGMControler::password() const
+{
+    return m_password;
+}
+
+void SKGMControler::setPassword(const QString &iPassword)
+{
+    if (m_password == iPassword) {
+        return;
+    }
+    m_password = iPassword;
+    Q_EMIT passwordChanged();
+}
+
+bool SKGMControler::showClosedAccounts() const
+{
+    return m_showClosedAccounts;
+}
+
+void SKGMControler::setShowClosedAccounts(bool iShowClosedAccounts)
+{
+    if (m_showClosedAccounts == iShowClosedAccounts) {
+        return;
+    }
+    m_showClosedAccounts = iShowClosedAccounts;
+    Q_EMIT showClosedAccountsChanged();
+    if (m_documentLoaded && m_accountsDataLoaded) {
+        reloadAccountsFromCurrentDocument();
+    }
+}
+
+bool SKGMControler::showClosedTrackers() const
+{
+    return m_showClosedTrackers;
+}
+
+void SKGMControler::setShowClosedTrackers(bool iShowClosedTrackers)
+{
+    if (m_showClosedTrackers == iShowClosedTrackers) {
+        return;
+    }
+    m_showClosedTrackers = iShowClosedTrackers;
+    Q_EMIT showClosedTrackersChanged();
+    if (m_documentLoaded && m_trackersDataLoaded) {
+        reloadTrackersFromCurrentDocument();
+    }
+}
+
+QString SKGMControler::globalFilterText() const
+{
+    return m_globalFilterText;
+}
+
+void SKGMControler::setGlobalFilterText(const QString &iFilterText)
+{
+    SKGTRACEINFUNC(1)
+    if (m_globalFilterText == iFilterText) {
+        return;
+    }
+    m_globalFilterText = iFilterText;
+    if (m_transactionsProxyModel != nullptr) {
+        m_transactionsProxyModel->setProperty("filter", m_globalFilterText);
+        if (m_transactionsDataLoaded) {
+            m_transactionsProxyModel->invalidate();
+            rebuildTransactionsFromProxy();
+        }
+    }
+    if (m_subTransactionsProxyModel != nullptr) {
+        m_subTransactionsProxyModel->setProperty("filter", m_globalFilterText);
+        if (m_subTransactionsDataLoaded) {
+            m_subTransactionsProxyModel->invalidate();
+            rebuildSubTransactionsFromProxy();
+        }
+    }
+    if (m_documentLoaded) {
+        if (m_accountsDataLoaded) {
+            reloadAccountsFromCurrentDocument();
+        }
+        if (m_trackersDataLoaded || m_payeesDataLoaded || m_categoriesDataLoaded || m_unitsDataLoaded) {
+            refreshReferenceProxyFilters();
+        }
+    }
+    Q_EMIT globalFilterTextChanged();
+}
+
+QString SKGMControler::errorMessage() const
+{
+    return m_errorMessage;
+}
+
+SKGMAccountListModel *SKGMControler::accountsModel()
+{
+    return &m_accountsModel;
+}
+
+QString SKGMControler::globalAccountsTotalText() const
+{
+    return m_globalAccountsTotalText;
+}
+
+QStringList SKGMControler::openAccountNames() const
+{
+    return m_openAccountNames;
+}
+
+int SKGMControler::selectedOpenAccountIndex() const
+{
+    return m_selectedOpenAccountIndex;
+}
+
+void SKGMControler::setSelectedOpenAccountIndex(int iIndex)
+{
+    SKGTRACEINFUNC(1)
+    const int boundedIndex = (iIndex >= 0 && iIndex < m_openAccountIds.count()) ? iIndex : -1;
+    if (m_selectedOpenAccountIndex == boundedIndex) {
+        return;
+    }
+    m_selectedOpenAccountIndex = boundedIndex;
+    Q_EMIT selectedOpenAccountIndexChanged();
+    updateSelectedAccountBalanceText();
+    if (m_transactionsDataLoaded) {
+        reloadTransactionsForSelectedAccount();
+    }
+}
+
+QString SKGMControler::selectedAccountBalanceText() const
+{
+    return m_selectedAccountBalanceText;
+}
+
+SKGMTransactionListModel *SKGMControler::transactionsModel()
+{
+    return &m_transactionsModel;
+}
+
+SKGMTransactionListModel *SKGMControler::subTransactionsModel()
+{
+    return &m_subTransactionsModel;
+}
+
+QString SKGMControler::selectedTrackerName() const
+{
+    return m_selectedTrackerName;
+}
+
+QString SKGMControler::transactionFilterText() const
+{
+    return m_globalFilterText;
+}
+
+void SKGMControler::setTransactionFilterText(const QString &iFilterText)
+{
+    SKGTRACEINFUNC(1)
+    if (m_globalFilterText == iFilterText) {
+        return;
+    }
+    m_transactionFilterText = iFilterText;
+    setGlobalFilterText(iFilterText);
+    Q_EMIT transactionFilterTextChanged();
+}
+
+int SKGMControler::transactionStatusFilter() const
+{
+    return m_transactionStatusFilter;
+}
+
+bool SKGMControler::passwordRequired() const
+{
+    return m_passwordRequired;
+}
+
+void SKGMControler::setTransactionStatusFilter(int iStatusFilter)
+{
+    SKGTRACEINFUNC(1)
+    const int boundedFilter = qBound(0, iStatusFilter, 3);
+    if (m_transactionStatusFilter == boundedFilter) {
+        return;
+    }
+    m_transactionStatusFilter = boundedFilter;
+    Q_EMIT transactionStatusFilterChanged();
+    if (m_transactionsDataLoaded) {
+        reloadTransactionsForSelectedAccount();
+    }
+}
+
+QVariantList SKGMControler::dashboardBankTotals() const
+{
+    return m_dashboardBankTotals;
+}
+
+QString SKGMControler::dashboardBankGrandTotalText() const
+{
+    return m_dashboardBankGrandTotalText;
+}
+
+QString SKGMControler::dashboardCurrentPeriodLabel() const
+{
+    return m_dashboardCurrentPeriodLabel;
+}
+
+QString SKGMControler::dashboardPreviousPeriodLabel() const
+{
+    return m_dashboardPreviousPeriodLabel;
+}
+
+QString SKGMControler::dashboardCurrentIncomeText() const
+{
+    return m_dashboardCurrentIncomeText;
+}
+
+QString SKGMControler::dashboardCurrentExpenseText() const
+{
+    return m_dashboardCurrentExpenseText;
+}
+
+QString SKGMControler::dashboardPreviousIncomeText() const
+{
+    return m_dashboardPreviousIncomeText;
+}
+
+QString SKGMControler::dashboardPreviousExpenseText() const
+{
+    return m_dashboardPreviousExpenseText;
+}
+
+QString SKGMControler::dashboardCurrentSavingsText() const
+{
+    return m_dashboardCurrentSavingsText;
+}
+
+QString SKGMControler::dashboardPreviousSavingsText() const
+{
+    return m_dashboardPreviousSavingsText;
+}
+
+double SKGMControler::dashboardCurrentSavings() const
+{
+    return m_dashboardCurrentSavings;
+}
+
+double SKGMControler::dashboardPreviousSavings() const
+{
+    return m_dashboardPreviousSavings;
+}
+
+QString SKGMControler::dashboardPersonalFinanceScoreText() const
+{
+    return m_dashboardPersonalFinanceScoreText;
+}
+
+QString SKGMControler::dashboardPersonalFinanceScoreMessage() const
+{
+    return m_dashboardPersonalFinanceScoreMessage;
+}
+
+QString SKGMControler::dashboardPersonalFinanceScoreColor() const
+{
+    return m_dashboardPersonalFinanceScoreColor;
+}
+
+QString SKGMControler::dashboardNetWorthText() const
+{
+    return m_dashboardNetWorthText;
+}
+
+QString SKGMControler::dashboardAnnualSpendingText() const
+{
+    return m_dashboardAnnualSpendingText;
+}
+
+QAbstractItemModel *SKGMControler::trackersModel()
+{
+    return &m_trackersModel;
+}
+
+QAbstractItemModel *SKGMControler::payeesModel()
+{
+    return &m_payeesModel;
+}
+
+QAbstractItemModel *SKGMControler::categoriesModel()
+{
+    return &m_categoriesModel;
+}
+
+QAbstractItemModel *SKGMControler::unitsModel()
+{
+    return &m_unitsModel;
+}
+
+bool SKGMControler::openFile()
+{
+    return openFile(m_filePath, m_password);
+}
+
+bool SKGMControler::openFile(const QString &iFilePath, const QString &iPassword)
+{
+    SKGTRACEINFUNC(1)
+    QString filePath = iFilePath;
+    setPasswordRequired(false);
+    if (filePath.isEmpty()) {
+        setErrorMessage(i18nc("Error message", "Missing file path."));
+        clearLoadedDocumentState();
+        m_documentLoaded = false;
+        return false;
+    }
+
+    QFileInfo fileInfo(filePath);
+    if (!fileInfo.exists() || !fileInfo.isFile()) {
+        setErrorMessage(i18nc("Error message", "File not found: %1", filePath));
+        clearLoadedDocumentState();
+        clearPersistedLastFilePath();
+        m_documentLoaded = false;
+        return false;
+    }
+    filePath = fileInfo.absoluteFilePath();
+
+    SKGError err = m_document.load(filePath, iPassword);
+    if (err.isFailed()) {
+        setErrorMessage(err.getFullMessageWithHistorical());
+        if (err.getReturnCode() == ERR_ENCRYPTION) {
+            setPasswordRequired(true);
+        }
+        clearLoadedDocumentState();
+        m_documentLoaded = false;
+        return false;
+    }
+
+    m_documentLoaded = true;
+    setFilePath(filePath);
+    setPassword(iPassword);
+    persistLastFilePath(filePath);
+    clearLoadedDocumentState();
+    setErrorMessage(QString());
+    return true;
+}
+
+bool SKGMControler::reloadAccountsFromCurrentDocument()
+{
+    SKGTRACEINFUNC(1)
+    m_accountsObjectModel->setTable(QStringLiteral("v_account_display"));
+    m_accountsObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_TYPENLS, t_BANK, t_name"));
+    m_accountsObjectModel->refresh();
+
+    struct OpenAccountEntry {
+        int id;
+        QString bank;
+        QString name;
+        QString balanceText;
+        bool bookmarked;
+    };
+    QVector<OpenAccountEntry> openAccountEntries;
+    QVector<SKGMAccountListModel::AccountItem> accounts;
+    const int nbObjects = m_accountsObjectModel->rowCount();
+    accounts.reserve((nbObjects * 3) / 2);
+    QString currentType;
+    QString currentUnit;
+    double currentTypeAmount = 0.0;
+    double globalAmount = 0.0;
+    QString globalUnit;
+    bool hasType = false;
+    int currentHeaderIndex = -1;
+
+    auto flushCurrentTypeTotal = [&]() {
+        if (!hasType || currentHeaderIndex < 0 || currentHeaderIndex >= accounts.count()) {
+            return;
+        }
+        SKGMAccountListModel::AccountItem &headerItem = accounts[currentHeaderIndex];
+        headerItem.amount = currentTypeAmount;
+        headerItem.amountText = QLocale().toString(headerItem.amount, 'f', 2);
+        if (!currentUnit.isEmpty()) {
+            headerItem.amountText = headerItem.amountText % u' ' % currentUnit;
+        }
+        hasType = false;
+    };
+
+    const QString globalFilter = m_globalFilterText.trimmed().toLower();
+    const bool hasGlobalFilter = !globalFilter.isEmpty();
+
+    for (int row = 0; row < nbObjects; ++row) {
+        const SKGObjectBase obj = m_accountsObjectModel->getObject(m_accountsObjectModel->index(row, 0));
+        SKGMAccountListModel::AccountItem item;
+        item.name = obj.getAttribute(QStringLiteral("t_name"));
+        item.bank = obj.getAttribute(QStringLiteral("t_BANK"));
+        item.type = obj.getAttribute(QStringLiteral("t_TYPENLS"));
+        item.unit = obj.getAttribute(QStringLiteral("t_UNIT"));
+        item.amount = SKGServices::stringToDouble(obj.getAttribute(QStringLiteral("f_CURRENTAMOUNT")));
+        item.amountText = QLocale().toString(item.amount, 'f', 2);
+        item.closed = obj.getAttribute(QStringLiteral("t_close")) == QStringLiteral("Y");
+        if (!item.closed) {
+            OpenAccountEntry entry;
+            entry.id = SKGServices::stringToInt(obj.getAttribute(QStringLiteral("id")));
+            entry.bank = item.bank;
+            entry.name = item.name;
+            entry.balanceText = item.amountText;
+            if (!item.unit.isEmpty()) {
+                entry.balanceText = entry.balanceText % u' ' % item.unit;
+            }
+            entry.bookmarked = obj.getAttribute(QStringLiteral("t_bookmarked")).toUpper() == QStringLiteral("Y");
+            openAccountEntries.push_back(std::move(entry));
+        }
+        if (!m_showClosedAccounts && item.closed) {
+            continue;
+        }
+        if (hasGlobalFilter) {
+            const QString haystack = (item.name % u' ' % item.bank % u' ' % item.type).toLower();
+            if (!haystack.contains(globalFilter)) {
+                continue;
+            }
+        }
+
+        globalAmount += item.amount;
+        if (globalUnit.isEmpty() && !item.unit.isEmpty()) {
+            globalUnit = item.unit;
+        }
+
+        if (!hasType || item.type != currentType) {
+            flushCurrentTypeTotal();
+            currentType = item.type;
+            currentUnit = item.unit;
+            currentTypeAmount = 0.0;
+            hasType = true;
+
+            SKGMAccountListModel::AccountItem headerItem;
+            headerItem.kind = SKGMAccountListModel::TypeHeaderRow;
+            headerItem.type = currentType;
+            headerItem.name = currentType;
+            accounts.push_back(std::move(headerItem));
+            currentHeaderIndex = accounts.count() - 1;
+        }
+
+        currentTypeAmount += item.amount;
+        if (!item.unit.isEmpty()) {
+            item.amountText = item.amountText % u' ' % item.unit;
+        }
+        accounts.push_back(std::move(item));
+    }
+    flushCurrentTypeTotal();
+
+    std::sort(openAccountEntries.begin(), openAccountEntries.end(), [](const OpenAccountEntry &iLeft, const OpenAccountEntry &iRight) {
+        if (iLeft.bookmarked != iRight.bookmarked) {
+            return iLeft.bookmarked && !iRight.bookmarked;
+        }
+        const int bankCmp = QString::localeAwareCompare(iLeft.bank, iRight.bank);
+        if (bankCmp != 0) {
+            return bankCmp < 0;
+        }
+        const int accountCmp = QString::localeAwareCompare(iLeft.name, iRight.name);
+        if (accountCmp != 0) {
+            return accountCmp < 0;
+        }
+        return iLeft.id < iRight.id;
+    });
+
+    QVector<int> openAccountIds;
+    QStringList openAccountNames;
+    QStringList openAccountBalanceTexts;
+    QStringList openAccountBanks;
+    QStringList openAccountAccountNames;
+    openAccountIds.reserve(openAccountEntries.count());
+    openAccountNames.reserve(openAccountEntries.count());
+    openAccountBalanceTexts.reserve(openAccountEntries.count());
+    openAccountBanks.reserve(openAccountEntries.count());
+    openAccountAccountNames.reserve(openAccountEntries.count());
+    for (const OpenAccountEntry &entry : std::as_const(openAccountEntries)) {
+        openAccountIds.push_back(entry.id);
+        openAccountNames.push_back(entry.bank.isEmpty() ? entry.name : entry.bank % QStringLiteral(" - ") % entry.name);
+        openAccountBalanceTexts.push_back(entry.balanceText);
+        openAccountBanks.push_back(entry.bank);
+        openAccountAccountNames.push_back(entry.name);
+    }
+
+    m_accountsModel.setAccounts(std::move(accounts));
+    QString globalTotalText = QLocale().toString(globalAmount, 'f', 2);
+    if (!globalUnit.isEmpty()) {
+        globalTotalText = globalTotalText % u' ' % globalUnit;
+    }
+    if (m_globalAccountsTotalText != globalTotalText) {
+        m_globalAccountsTotalText = std::move(globalTotalText);
+        Q_EMIT globalAccountsTotalTextChanged();
+    }
+    m_openAccountIds = std::move(openAccountIds);
+    m_openAccountNames = std::move(openAccountNames);
+    m_openAccountBalanceTexts = std::move(openAccountBalanceTexts);
+    m_openAccountBanks = std::move(openAccountBanks);
+    m_openAccountAccountNames = std::move(openAccountAccountNames);
+    updateSelectedAccountBalanceText();
+    Q_EMIT openAccountsChanged();
+    if (m_openAccountIds.isEmpty()) {
+        setSelectedOpenAccountIndex(-1);
+    } else if (m_selectedOpenAccountIndex < 0 || m_selectedOpenAccountIndex >= m_openAccountIds.count()) {
+        setSelectedOpenAccountIndex(0);
+    } else if (m_transactionsDataLoaded) {
+        reloadTransactionsForSelectedAccount();
+    }
+    m_accountsDataLoaded = true;
+    m_openAccountsDataLoaded = true;
+    setErrorMessage(QString());
+    return true;
+}
+
+bool SKGMControler::reloadOpenAccountsFromCurrentDocument()
+{
+    SKGTRACEINFUNC(1)
+    m_accountsObjectModel->setTable(QStringLiteral("v_account_display"));
+    m_accountsObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_TYPENLS, t_BANK, t_name"));
+    m_accountsObjectModel->refresh();
+
+    struct OpenAccountEntry {
+        int id;
+        QString bank;
+        QString name;
+        QString balanceText;
+        bool bookmarked;
+    };
+    QVector<OpenAccountEntry> openAccountEntries;
+    const int nbObjects = m_accountsObjectModel->rowCount();
+    openAccountEntries.reserve(nbObjects);
+
+    for (int row = 0; row < nbObjects; ++row) {
+        const SKGObjectBase obj = m_accountsObjectModel->getObject(m_accountsObjectModel->index(row, 0));
+        const bool closed = obj.getAttribute(QStringLiteral("t_close")) == QStringLiteral("Y");
+        if (closed) {
+            continue;
+        }
+
+        OpenAccountEntry entry;
+        entry.id = SKGServices::stringToInt(obj.getAttribute(QStringLiteral("id")));
+        entry.bank = obj.getAttribute(QStringLiteral("t_BANK"));
+        entry.name = obj.getAttribute(QStringLiteral("t_name"));
+        entry.balanceText = QLocale().toString(SKGServices::stringToDouble(obj.getAttribute(QStringLiteral("f_CURRENTAMOUNT"))), 'f', 2);
+        const QString unit = obj.getAttribute(QStringLiteral("t_UNIT"));
+        if (!unit.isEmpty()) {
+            entry.balanceText = entry.balanceText % u' ' % unit;
+        }
+        entry.bookmarked = obj.getAttribute(QStringLiteral("t_bookmarked")).toUpper() == QStringLiteral("Y");
+        openAccountEntries.push_back(std::move(entry));
+    }
+
+    std::sort(openAccountEntries.begin(), openAccountEntries.end(), [](const OpenAccountEntry &iLeft, const OpenAccountEntry &iRight) {
+        if (iLeft.bookmarked != iRight.bookmarked) {
+            return iLeft.bookmarked && !iRight.bookmarked;
+        }
+        const int bankCmp = QString::localeAwareCompare(iLeft.bank, iRight.bank);
+        if (bankCmp != 0) {
+            return bankCmp < 0;
+        }
+        const int accountCmp = QString::localeAwareCompare(iLeft.name, iRight.name);
+        if (accountCmp != 0) {
+            return accountCmp < 0;
+        }
+        return iLeft.id < iRight.id;
+    });
+
+    QVector<int> openAccountIds;
+    QStringList openAccountNames;
+    QStringList openAccountBalanceTexts;
+    QStringList openAccountBanks;
+    QStringList openAccountAccountNames;
+    openAccountIds.reserve(openAccountEntries.count());
+    openAccountNames.reserve(openAccountEntries.count());
+    openAccountBalanceTexts.reserve(openAccountEntries.count());
+    openAccountBanks.reserve(openAccountEntries.count());
+    openAccountAccountNames.reserve(openAccountEntries.count());
+    for (const OpenAccountEntry &entry : std::as_const(openAccountEntries)) {
+        openAccountIds.push_back(entry.id);
+        openAccountNames.push_back(entry.bank.isEmpty() ? entry.name : entry.bank % QStringLiteral(" - ") % entry.name);
+        openAccountBalanceTexts.push_back(entry.balanceText);
+        openAccountBanks.push_back(entry.bank);
+        openAccountAccountNames.push_back(entry.name);
+    }
+
+    m_openAccountIds = std::move(openAccountIds);
+    m_openAccountNames = std::move(openAccountNames);
+    m_openAccountBalanceTexts = std::move(openAccountBalanceTexts);
+    m_openAccountBanks = std::move(openAccountBanks);
+    m_openAccountAccountNames = std::move(openAccountAccountNames);
+    updateSelectedAccountBalanceText();
+    Q_EMIT openAccountsChanged();
+    if (m_openAccountIds.isEmpty()) {
+        setSelectedOpenAccountIndex(-1);
+    } else if (m_selectedOpenAccountIndex < 0 || m_selectedOpenAccountIndex >= m_openAccountIds.count()) {
+        setSelectedOpenAccountIndex(0);
+    } else if (m_transactionsDataLoaded) {
+        reloadTransactionsForSelectedAccount();
+    }
+    m_openAccountsDataLoaded = true;
+    setErrorMessage(QString());
+    return true;
+}
+
+void SKGMControler::reloadDashboardFromCurrentDocument()
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded) {
+        clearDashboardData();
+        return;
+    }
+
+    SKGReportBank report(&m_document);
+    const SKGServices::SKGUnitInfo primaryUnit = m_document.getPrimaryUnit();
+    auto formatAmount = [&](double iAmount) {
+        QString text = QLocale().toString(iAmount, 'f', 2);
+        if (!primaryUnit.Symbol.isEmpty()) {
+            text = text % u' ' % primaryUnit.Symbol;
+        }
+        return text;
+    };
+    auto formatPeriodLabel = [](const QString &iPeriod) {
+        const QDate date = SKGServices::periodToDate(iPeriod);
+        return date.isValid() ? QLocale().toString(date, QStringLiteral("MMMM yyyy")) : iPeriod;
+    };
+
+    QVariantList bankTotals;
+    QString bankGrandTotalText;
+    const QVariantList bankTable = report.getBankTable();
+    for (int row = 1; row < bankTable.count(); ++row) {
+        const QVariantList line = bankTable.at(row).toList();
+        if (line.count() < 4) {
+            continue;
+        }
+        const bool isTotal = line.at(0).toBool();
+        const double amount = line.at(3).toDouble();
+        QVariantMap item;
+        item.insert(QStringLiteral("bank"), line.at(1).toString());
+        item.insert(QStringLiteral("amount"), amount);
+        item.insert(QStringLiteral("amountText"), formatAmount(amount));
+        item.insert(QStringLiteral("isTotal"), isTotal);
+        if (isTotal) {
+            bankGrandTotalText = item.value(QStringLiteral("amountText")).toString();
+        } else {
+            bankTotals.push_back(item);
+        }
+    }
+    if (bankGrandTotalText.isEmpty() && !bankTotals.isEmpty()) {
+        double total = 0.0;
+        for (const QVariant &entry : std::as_const(bankTotals)) {
+            total += entry.toMap().value(QStringLiteral("amount")).toDouble();
+        }
+        bankGrandTotalText = formatAmount(total);
+    }
+
+    QString currentPeriod = report.getPeriod();
+    QString previousPeriod = report.getPreviousPeriod();
+    double currentIncome = 0.0;
+    double currentExpense = 0.0;
+    double previousIncome = 0.0;
+    double previousExpense = 0.0;
+    double currentSavings = 0.0;
+    double previousSavings = 0.0;
+    const QVariantList incomeVsExpenditure = report.getIncomeVsExpenditure(true, true, false, true);
+    if (!incomeVsExpenditure.isEmpty()) {
+        const QVariantList header = incomeVsExpenditure.at(0).toList();
+        if (header.count() >= 4) {
+            if (!header.at(2).toString().isEmpty()) {
+                previousPeriod = header.at(2).toString();
+            }
+            if (!header.at(3).toString().isEmpty()) {
+                currentPeriod = header.at(3).toString();
+            }
+        }
+    }
+    if (incomeVsExpenditure.count() > 1) {
+        const QVariantList incomes = incomeVsExpenditure.at(1).toList();
+        if (incomes.count() >= 4) {
+            previousIncome = incomes.at(2).toDouble();
+            currentIncome = incomes.at(3).toDouble();
+        }
+    }
+    if (incomeVsExpenditure.count() > 2) {
+        const QVariantList expenses = incomeVsExpenditure.at(2).toList();
+        if (expenses.count() >= 4) {
+            previousExpense = expenses.at(2).toDouble();
+            currentExpense = expenses.at(3).toDouble();
+        }
+    }
+    if (incomeVsExpenditure.count() > 3) {
+        const QVariantList savings = incomeVsExpenditure.at(3).toList();
+        if (savings.count() >= 4) {
+            previousSavings = savings.at(2).toDouble();
+            currentSavings = savings.at(3).toDouble();
+        }
+    }
+
+    const QVariantMap pfsDetails = report.getPersonalFinanceScoreDetails(false, true);
+    QString scoreColor = pfsDetails.value(QStringLiteral("color")).toString();
+    if (!scoreColor.isEmpty() && !scoreColor.startsWith(QLatin1Char('#'))) {
+        scoreColor.prepend(QLatin1Char('#'));
+    }
+
+    m_dashboardBankTotals = std::move(bankTotals);
+    m_dashboardBankGrandTotalText = std::move(bankGrandTotalText);
+    m_dashboardCurrentPeriodLabel = formatPeriodLabel(currentPeriod);
+    m_dashboardPreviousPeriodLabel = formatPeriodLabel(previousPeriod);
+    m_dashboardCurrentIncomeText = formatAmount(currentIncome);
+    m_dashboardCurrentExpenseText = formatAmount(currentExpense);
+    m_dashboardPreviousIncomeText = formatAmount(previousIncome);
+    m_dashboardPreviousExpenseText = formatAmount(previousExpense);
+    m_dashboardCurrentSavingsText = formatAmount(currentSavings);
+    m_dashboardPreviousSavingsText = formatAmount(previousSavings);
+    m_dashboardCurrentSavings = currentSavings;
+    m_dashboardPreviousSavings = previousSavings;
+    m_dashboardPersonalFinanceScoreText = QLocale().toString(pfsDetails.value(QStringLiteral("value")).toDouble(), 'f', 2);
+    m_dashboardPersonalFinanceScoreMessage = pfsDetails.value(QStringLiteral("message")).toString();
+    m_dashboardPersonalFinanceScoreColor = scoreColor.isEmpty() ? QStringLiteral("#666666") : std::move(scoreColor);
+    m_dashboardNetWorthText = formatAmount(report.getNetWorth(false, true));
+    m_dashboardAnnualSpendingText = formatAmount(report.getAnnualSpending(false, true));
+    m_dashboardDataLoaded = true;
+    Q_EMIT dashboardDataChanged();
+}
+
+void SKGMControler::clearDashboardData()
+{
+    m_dashboardBankTotals.clear();
+    m_dashboardBankGrandTotalText.clear();
+    m_dashboardCurrentPeriodLabel.clear();
+    m_dashboardPreviousPeriodLabel.clear();
+    m_dashboardCurrentIncomeText.clear();
+    m_dashboardCurrentExpenseText.clear();
+    m_dashboardPreviousIncomeText.clear();
+    m_dashboardPreviousExpenseText.clear();
+    m_dashboardCurrentSavingsText.clear();
+    m_dashboardPreviousSavingsText.clear();
+    m_dashboardCurrentSavings = 0.0;
+    m_dashboardPreviousSavings = 0.0;
+    m_dashboardPersonalFinanceScoreText.clear();
+    m_dashboardPersonalFinanceScoreMessage.clear();
+    m_dashboardPersonalFinanceScoreColor = QStringLiteral("#666666");
+    m_dashboardNetWorthText.clear();
+    m_dashboardAnnualSpendingText.clear();
+    m_dashboardDataLoaded = false;
+    Q_EMIT dashboardDataChanged();
+}
+
+void SKGMControler::reloadTrackersFromCurrentDocument()
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded) {
+        clearReferenceData();
+        return;
+    }
+
+    m_trackersObjectModel->setTable(QStringLiteral("v_refund_display"));
+    if (m_showClosedTrackers) {
+        m_trackersObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_name"));
+    } else {
+        m_trackersObjectModel->setFilter(QStringLiteral("t_close='N' ORDER BY t_name"));
+    }
+    m_trackersObjectModel->refresh();
+    m_trackersModel.setAmountUnitSymbol(m_document.getPrimaryUnit().Symbol);
+    m_trackersProxyModel->setProperty("filter", m_globalFilterText);
+    m_trackersProxyModel->invalidate();
+    m_trackersDataLoaded = true;
+    Q_EMIT referenceDataChanged();
+}
+
+void SKGMControler::reloadPayeesFromCurrentDocument()
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded) {
+        clearReferenceData();
+        return;
+    }
+
+    m_payeesObjectModel->setTable(QStringLiteral("v_payee_display"));
+    m_payeesObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_name"));
+    m_payeesObjectModel->refresh();
+    m_payeesModel.setAmountUnitSymbol(m_document.getPrimaryUnit().Symbol);
+    m_payeesProxyModel->setProperty("filter", m_globalFilterText);
+    m_payeesProxyModel->invalidate();
+    m_payeesDataLoaded = true;
+    Q_EMIT referenceDataChanged();
+}
+
+void SKGMControler::reloadCategoriesFromCurrentDocument()
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded) {
+        clearReferenceData();
+        return;
+    }
+
+    m_categoriesObjectModel->setTable(QStringLiteral("v_category_display"));
+    m_categoriesObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_fullname"));
+    m_categoriesObjectModel->refresh();
+    m_categoriesModel.setAmountUnitSymbol(m_document.getPrimaryUnit().Symbol);
+    m_categoriesProxyModel->setProperty("filter", m_globalFilterText);
+    m_categoriesProxyModel->invalidate();
+    m_categoriesDataLoaded = true;
+    Q_EMIT referenceDataChanged();
+}
+
+void SKGMControler::reloadUnitsFromCurrentDocument()
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded) {
+        clearReferenceData();
+        return;
+    }
+
+    m_unitsObjectModel->setTable(QStringLiteral("v_unit_display"));
+    m_unitsObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_TYPENLS, t_name"));
+    m_unitsObjectModel->refresh();
+    m_unitsModel.setAmountUnitSymbol(m_document.getPrimaryUnit().Symbol);
+    m_unitsProxyModel->setProperty("filter", m_globalFilterText);
+    m_unitsProxyModel->invalidate();
+    m_unitsDataLoaded = true;
+    Q_EMIT referenceDataChanged();
+}
+
+void SKGMControler::clearReferenceData()
+{
+    m_trackersObjectModel->setFilter(QStringLiteral("1=0"));
+    m_trackersObjectModel->refresh();
+    m_payeesObjectModel->setFilter(QStringLiteral("1=0"));
+    m_payeesObjectModel->refresh();
+    m_categoriesObjectModel->setFilter(QStringLiteral("1=0"));
+    m_categoriesObjectModel->refresh();
+    m_unitsObjectModel->setFilter(QStringLiteral("1=0"));
+    m_unitsObjectModel->refresh();
+
+    m_trackersProxyModel->invalidate();
+    m_payeesProxyModel->invalidate();
+    m_categoriesProxyModel->invalidate();
+    m_unitsProxyModel->invalidate();
+
+    m_trackersDataLoaded = false;
+    m_payeesDataLoaded = false;
+    m_categoriesDataLoaded = false;
+    m_unitsDataLoaded = false;
+    Q_EMIT referenceDataChanged();
+}
+
+bool SKGMControler::selectOpenAccount(const QString &iBank, const QString &iAccountName)
+{
+    SKGTRACEINFUNC(1)
+    const int nb = m_openAccountIds.count();
+    for (int i = 0; i < nb; ++i) {
+        if (m_openAccountBanks.at(i) == iBank && m_openAccountAccountNames.at(i) == iAccountName) {
+            if (!m_selectedTrackerName.isEmpty()) {
+                m_selectedTrackerName.clear();
+                Q_EMIT selectedTrackerNameChanged();
+            }
+            setSelectedOpenAccountIndex(i);
+            return true;
+        }
+    }
+    return false;
+}
+
+bool SKGMControler::selectTracker(const QString &iTrackerName)
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded || iTrackerName.trimmed().isEmpty()) {
+        return false;
+    }
+
+    const QString trackerName = iTrackerName.trimmed();
+    const QString whereClause =
+        QStringLiteral("t_REALREFUND='") % SKGServices::stringToSqlString(trackerName) % QStringLiteral("' ORDER BY date(d_date) DESC, id DESC");
+    return loadSubTransactionsForSelection(trackerName, whereClause);
+}
+
+bool SKGMControler::selectCategory(const QString &iCategoryFullName)
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded || iCategoryFullName.trimmed().isEmpty()) {
+        return false;
+    }
+
+    const QString categoryName = iCategoryFullName.trimmed();
+    const QString whereClause =
+        QStringLiteral("t_REALCATEGORY='") % SKGServices::stringToSqlString(categoryName) % QStringLiteral("' ORDER BY date(d_date) DESC, id DESC");
+    return loadSubTransactionsForSelection(categoryName, whereClause);
+}
+
+bool SKGMControler::selectTransaction(int iOperationId)
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded || iOperationId <= 0) {
+        return false;
+    }
+
+    const QString selectionLabel = i18nc("@info", "Transaction #%1", iOperationId);
+    const QString whereClause = QStringLiteral("i_OPID=") % SKGServices::intToString(iOperationId) % QStringLiteral(" ORDER BY date(d_date) DESC, id DESC");
+    return loadSubTransactionsForSelection(selectionLabel, whereClause);
+}
+
+void SKGMControler::reloadTransactionsForSelectedAccount()
+{
+    SKGTRACEINFUNC(1)
+    if (!m_selectedTrackerName.isEmpty()) {
+        m_selectedTrackerName.clear();
+        Q_EMIT selectedTrackerNameChanged();
+    }
+
+    if (!m_documentLoaded) {
+        m_transactionsModel.clear();
+        m_transactionsDataLoaded = false;
+        return;
+    }
+    if (m_selectedOpenAccountIndex < 0 || m_selectedOpenAccountIndex >= m_openAccountIds.count()) {
+        m_transactionsModel.clear();
+        m_transactionsDataLoaded = true;
+        return;
+    }
+
+    const int accountId = m_openAccountIds.at(m_selectedOpenAccountIndex);
+    QString whereClause = QStringLiteral("rd_account_id=") % SKGServices::intToString(accountId) % QStringLiteral(" AND t_template='N'");
+    if (m_transactionStatusFilter == 1) {
+        whereClause += QStringLiteral(" AND t_status='P'");
+    } else if (m_transactionStatusFilter == 2) {
+        whereClause += QStringLiteral(" AND t_status='N'");
+    } else if (m_transactionStatusFilter == 3) {
+        whereClause += QStringLiteral(" AND t_status='Y'");
+    }
+    whereClause += QStringLiteral(" ORDER BY date(d_date) DESC, id DESC");
+
+    m_transactionsObjectModel->setTable(QStringLiteral("v_operation_display"));
+    m_transactionsObjectModel->setFilter(whereClause);
+    m_transactionsObjectModel->refresh();
+
+    applyFilterAndDateSort(m_transactionsProxyModel, m_transactionsObjectModel);
+    rebuildTransactionsFromProxy();
+}
+
+void SKGMControler::rebuildTransactionsFromProxy()
+{
+    SKGTRACEINFUNC(1)
+    QVector<SKGMTransactionListModel::TransactionItem> transactions;
+    const int nbRows = m_transactionsProxyModel->rowCount();
+    transactions.reserve(nbRows);
+    for (int row = 0; row < nbRows; ++row) {
+        const QModelIndex proxyIndex = m_transactionsProxyModel->index(row, 0);
+        const QModelIndex sourceIndex = m_transactionsProxyModel->mapToSource(proxyIndex);
+        const SKGObjectBase obj = m_transactionsObjectModel->getObject(sourceIndex);
+        SKGMTransactionListModel::TransactionItem item;
+        item.operationId = SKGServices::stringToInt(obj.getAttribute(QStringLiteral("id")));
+        item.date = obj.getAttribute(QStringLiteral("d_date"));
+        item.payee = obj.getAttribute(QStringLiteral("t_PAYEE"));
+        item.category = obj.getAttribute(QStringLiteral("t_CATEGORY"));
+        item.comment = obj.getAttribute(QStringLiteral("t_comment"));
+        item.mode = obj.getAttribute(QStringLiteral("t_mode"));
+        item.number = obj.getAttribute(QStringLiteral("t_number"));
+        item.amount = SKGServices::stringToDouble(obj.getAttribute(QStringLiteral("f_CURRENTAMOUNT")));
+        QString unit = obj.getAttribute(QStringLiteral("t_UNIT"));
+        item.amountText = QLocale().toString(item.amount, 'f', 2);
+        if (!unit.isEmpty()) {
+            item.amountText = item.amountText % u' ' % unit;
+        }
+        transactions.push_back(std::move(item));
+    }
+    m_transactionsModel.setTransactions(std::move(transactions));
+    m_transactionsDataLoaded = true;
+}
+
+void SKGMControler::rebuildSubTransactionsFromProxy()
+{
+    SKGTRACEINFUNC(1)
+    QVector<SKGMTransactionListModel::TransactionItem> transactions;
+    const int nbRows = m_subTransactionsProxyModel->rowCount();
+    transactions.reserve(nbRows);
+    for (int row = 0; row < nbRows; ++row) {
+        const QModelIndex proxyIndex = m_subTransactionsProxyModel->index(row, 0);
+        const QModelIndex sourceIndex = m_subTransactionsProxyModel->mapToSource(proxyIndex);
+        const SKGObjectBase obj = m_subTransactionsObjectModel->getObject(sourceIndex);
+        SKGMTransactionListModel::TransactionItem item;
+        item.date = obj.getAttribute(QStringLiteral("d_date"));
+        item.payee = obj.getAttribute(QStringLiteral("t_PAYEE"));
+        item.category = obj.getAttribute(QStringLiteral("t_REALCATEGORY"));
+        item.comment = obj.getAttribute(QStringLiteral("t_REALCOMMENT"));
+        item.mode = obj.getAttribute(QStringLiteral("t_mode"));
+        item.number = obj.getAttribute(QStringLiteral("t_number"));
+        item.amount = SKGServices::stringToDouble(obj.getAttribute(QStringLiteral("f_REALCURRENTAMOUNT")));
+        QString unit = obj.getAttribute(QStringLiteral("t_UNIT"));
+        item.amountText = QLocale().toString(item.amount, 'f', 2);
+        if (!unit.isEmpty()) {
+            item.amountText = item.amountText % u' ' % unit;
+        }
+        transactions.push_back(std::move(item));
+    }
+    m_subTransactionsModel.setTransactions(std::move(transactions));
+    m_subTransactionsDataLoaded = true;
+}
+
+void SKGMControler::ensureDataForPage(int iPageIndex)
+{
+    SKGTRACEINFUNC(1)
+    if (!m_documentLoaded) {
+        return;
+    }
+
+    if (iPageIndex == 0) {
+        if (!m_dashboardDataLoaded) {
+            reloadDashboardFromCurrentDocument();
+        }
+    } else if (iPageIndex == 1) {
+        if (!m_accountsDataLoaded) {
+            reloadAccountsFromCurrentDocument();
+        }
+    } else if (iPageIndex == 2) {
+        if (!m_openAccountsDataLoaded) {
+            reloadOpenAccountsFromCurrentDocument();
+        }
+        if (!m_transactionsDataLoaded) {
+            reloadTransactionsForSelectedAccount();
+        }
+    } else if (iPageIndex == 3) {
+        if (!m_trackersDataLoaded) {
+            reloadTrackersFromCurrentDocument();
+        }
+    } else if (iPageIndex == 4) {
+        if (!m_payeesDataLoaded) {
+            reloadPayeesFromCurrentDocument();
+        }
+    } else if (iPageIndex == 5) {
+        if (!m_categoriesDataLoaded) {
+            reloadCategoriesFromCurrentDocument();
+        }
+    } else if (iPageIndex == 6) {
+        if (!m_unitsDataLoaded) {
+            reloadUnitsFromCurrentDocument();
+        }
+    }
+}
+
+void SKGMControler::updateSelectedAccountBalanceText()
+{
+    SKGTRACEINFUNC(1)
+    QString balanceText;
+    if (m_selectedOpenAccountIndex >= 0 && m_selectedOpenAccountIndex < m_openAccountBalanceTexts.count()) {
+        balanceText = m_openAccountBalanceTexts.at(m_selectedOpenAccountIndex);
+    }
+    if (m_selectedAccountBalanceText == balanceText) {
+        return;
+    }
+    m_selectedAccountBalanceText = std::move(balanceText);
+    Q_EMIT selectedAccountBalanceTextChanged();
+}
+
+void SKGMControler::setPasswordRequired(bool iRequired)
+{
+    SKGTRACEINFUNC(1)
+    if (m_passwordRequired == iRequired) {
+        return;
+    }
+    m_passwordRequired = iRequired;
+    Q_EMIT passwordRequiredChanged();
+}
+
+void SKGMControler::clearLoadedDocumentState()
+{
+    SKGTRACEINFUNC(1)
+    m_accountsModel.clear();
+    if (!m_globalAccountsTotalText.isEmpty()) {
+        m_globalAccountsTotalText.clear();
+        Q_EMIT globalAccountsTotalTextChanged();
+    }
+    m_transactionsModel.clear();
+    m_subTransactionsModel.clear();
+    if (!m_selectedTrackerName.isEmpty()) {
+        m_selectedTrackerName.clear();
+        Q_EMIT selectedTrackerNameChanged();
+    }
+    m_openAccountIds.clear();
+    m_openAccountNames.clear();
+    m_openAccountBalanceTexts.clear();
+    m_openAccountBanks.clear();
+    m_openAccountAccountNames.clear();
+    m_accountsDataLoaded = false;
+    m_openAccountsDataLoaded = false;
+    m_transactionsDataLoaded = false;
+    m_subTransactionsDataLoaded = false;
+    setSelectedOpenAccountIndex(-1);
+    updateSelectedAccountBalanceText();
+    Q_EMIT openAccountsChanged();
+    clearDashboardData();
+    clearReferenceData();
+}
+
+void SKGMControler::refreshReferenceProxyFilters()
+{
+    SKGTRACEINFUNC(1)
+    if (m_trackersDataLoaded) {
+        m_trackersProxyModel->setProperty("filter", m_globalFilterText);
+        m_trackersProxyModel->invalidate();
+    }
+    if (m_payeesDataLoaded) {
+        m_payeesProxyModel->setProperty("filter", m_globalFilterText);
+        m_payeesProxyModel->invalidate();
+    }
+    if (m_categoriesDataLoaded) {
+        m_categoriesProxyModel->setProperty("filter", m_globalFilterText);
+        m_categoriesProxyModel->invalidate();
+    }
+    if (m_unitsDataLoaded) {
+        m_unitsProxyModel->setProperty("filter", m_globalFilterText);
+        m_unitsProxyModel->invalidate();
+    }
+}
+
+void SKGMControler::applyFilterAndDateSort(SKGSortFilterProxyModel *iProxyModel, SKGObjectModelBase *iObjectModel)
+{
+    SKGTRACEINFUNC(1)
+    iProxyModel->setProperty("filter", m_globalFilterText);
+    const int dateCol = iObjectModel->getIndexAttribute(QStringLiteral("d_date"));
+    if (dateCol >= 0) {
+        iProxyModel->sort(dateCol, Qt::DescendingOrder);
+    } else {
+        iProxyModel->sort(0, Qt::DescendingOrder);
+    }
+}
+
+bool SKGMControler::loadSubTransactionsForSelection(const QString &iSelectionLabel, const QString &iWhereClause)
+{
+    SKGTRACEINFUNC(1)
+    if (m_selectedTrackerName != iSelectionLabel) {
+        m_selectedTrackerName = iSelectionLabel;
+        Q_EMIT selectedTrackerNameChanged();
+    }
+
+    m_subTransactionsObjectModel->setTable(QStringLiteral("v_suboperation_consolidated"));
+    m_subTransactionsObjectModel->setFilter(iWhereClause);
+    m_subTransactionsObjectModel->refresh();
+    applyFilterAndDateSort(m_subTransactionsProxyModel, m_subTransactionsObjectModel);
+    rebuildSubTransactionsFromProxy();
+    return true;
+}
+
+void SKGMControler::loadPersistedState()
+{
+    SKGTRACEINFUNC(1)
+    QSettings settings(QStringLiteral("kde.org"), QStringLiteral("skroogecompanion"));
+    const QString persistedPath = settings.value(QStringLiteral("lastFilePath")).toString();
+    if (!persistedPath.isEmpty()) {
+        setFilePath(persistedPath);
+    }
+}
+
+void SKGMControler::persistLastFilePath(const QString &iFilePath)
+{
+    SKGTRACEINFUNC(1)
+    QSettings settings(QStringLiteral("kde.org"), QStringLiteral("skroogecompanion"));
+    settings.setValue(QStringLiteral("lastFilePath"), iFilePath);
+}
+
+void SKGMControler::clearPersistedLastFilePath()
+{
+    SKGTRACEINFUNC(1)
+    QSettings settings(QStringLiteral("kde.org"), QStringLiteral("skroogecompanion"));
+    settings.remove(QStringLiteral("lastFilePath"));
+}
+
+void SKGMControler::setErrorMessage(const QString &iMessage)
+{
+    SKGTRACEINFUNC(1)
+    if (m_errorMessage == iMessage) {
+        return;
+    }
+    m_errorMessage = iMessage;
+    Q_EMIT errorMessageChanged();
+}
diff --git a/skroogecompanion/skgmcontroler.h b/skroogecompanion/skgmcontroler.h
new file mode 100644
index 000000000..16f82cb70
--- /dev/null
+++ b/skroogecompanion/skgmcontroler.h
@@ -0,0 +1,235 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#ifndef SKGMCONTROLER_H
+#define SKGMCONTROLER_H
+
+#include "skgdocumentbank.h"
+#include "skgmaccountlistmodel.h"
+#include "skgmreferenceproxymodel.h"
+#include "skgmtransactionlistmodel.h"
+
+#include <qabstractitemmodel.h>
+#include <qobject.h>
+#include <qstringlist.h>
+#include <qvariant.h>
+
+class SKGObjectModelBase;
+class SKGSortFilterProxyModel;
+
+class SKGMControler : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString filePath READ filePath WRITE setFilePath NOTIFY filePathChanged)
+    Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
+    Q_PROPERTY(bool showClosedAccounts READ showClosedAccounts WRITE setShowClosedAccounts NOTIFY showClosedAccountsChanged)
+    Q_PROPERTY(bool showClosedTrackers READ showClosedTrackers WRITE setShowClosedTrackers NOTIFY showClosedTrackersChanged)
+    Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged)
+    Q_PROPERTY(QString globalFilterText READ globalFilterText WRITE setGlobalFilterText NOTIFY globalFilterTextChanged)
+    Q_PROPERTY(SKGMAccountListModel *accountsModel READ accountsModel CONSTANT)
+    Q_PROPERTY(QString globalAccountsTotalText READ globalAccountsTotalText NOTIFY globalAccountsTotalTextChanged)
+    Q_PROPERTY(QStringList openAccountNames READ openAccountNames NOTIFY openAccountsChanged)
+    Q_PROPERTY(int selectedOpenAccountIndex READ selectedOpenAccountIndex WRITE setSelectedOpenAccountIndex NOTIFY selectedOpenAccountIndexChanged)
+    Q_PROPERTY(QString selectedAccountBalanceText READ selectedAccountBalanceText NOTIFY selectedAccountBalanceTextChanged)
+    Q_PROPERTY(SKGMTransactionListModel *transactionsModel READ transactionsModel CONSTANT)
+    Q_PROPERTY(SKGMTransactionListModel *subTransactionsModel READ subTransactionsModel CONSTANT)
+    Q_PROPERTY(QString selectedTrackerName READ selectedTrackerName NOTIFY selectedTrackerNameChanged)
+    Q_PROPERTY(QString transactionFilterText READ transactionFilterText WRITE setTransactionFilterText NOTIFY transactionFilterTextChanged)
+    Q_PROPERTY(int transactionStatusFilter READ transactionStatusFilter WRITE setTransactionStatusFilter NOTIFY transactionStatusFilterChanged)
+    Q_PROPERTY(bool passwordRequired READ passwordRequired NOTIFY passwordRequiredChanged)
+    Q_PROPERTY(QVariantList dashboardBankTotals READ dashboardBankTotals NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardBankGrandTotalText READ dashboardBankGrandTotalText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardCurrentPeriodLabel READ dashboardCurrentPeriodLabel NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardPreviousPeriodLabel READ dashboardPreviousPeriodLabel NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardCurrentIncomeText READ dashboardCurrentIncomeText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardCurrentExpenseText READ dashboardCurrentExpenseText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardPreviousIncomeText READ dashboardPreviousIncomeText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardPreviousExpenseText READ dashboardPreviousExpenseText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardCurrentSavingsText READ dashboardCurrentSavingsText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardPreviousSavingsText READ dashboardPreviousSavingsText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(double dashboardCurrentSavings READ dashboardCurrentSavings NOTIFY dashboardDataChanged)
+    Q_PROPERTY(double dashboardPreviousSavings READ dashboardPreviousSavings NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardPersonalFinanceScoreText READ dashboardPersonalFinanceScoreText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardPersonalFinanceScoreMessage READ dashboardPersonalFinanceScoreMessage NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardPersonalFinanceScoreColor READ dashboardPersonalFinanceScoreColor NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardNetWorthText READ dashboardNetWorthText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QString dashboardAnnualSpendingText READ dashboardAnnualSpendingText NOTIFY dashboardDataChanged)
+    Q_PROPERTY(QAbstractItemModel *trackersModel READ trackersModel CONSTANT)
+    Q_PROPERTY(QAbstractItemModel *payeesModel READ payeesModel CONSTANT)
+    Q_PROPERTY(QAbstractItemModel *categoriesModel READ categoriesModel CONSTANT)
+    Q_PROPERTY(QAbstractItemModel *unitsModel READ unitsModel CONSTANT)
+
+public:
+    explicit SKGMControler(QObject *iParent = nullptr);
+    ~SKGMControler() override;
+
+    QString filePath() const;
+    void setFilePath(const QString &iFilePath);
+
+    QString password() const;
+    void setPassword(const QString &iPassword);
+
+    bool showClosedAccounts() const;
+    void setShowClosedAccounts(bool iShowClosedAccounts);
+    bool showClosedTrackers() const;
+    void setShowClosedTrackers(bool iShowClosedTrackers);
+    QString globalFilterText() const;
+    void setGlobalFilterText(const QString &iFilterText);
+
+    QString errorMessage() const;
+    SKGMAccountListModel *accountsModel();
+    QString globalAccountsTotalText() const;
+    QStringList openAccountNames() const;
+    int selectedOpenAccountIndex() const;
+    void setSelectedOpenAccountIndex(int iIndex);
+    QString selectedAccountBalanceText() const;
+    SKGMTransactionListModel *transactionsModel();
+    SKGMTransactionListModel *subTransactionsModel();
+    QString selectedTrackerName() const;
+    QString transactionFilterText() const;
+    void setTransactionFilterText(const QString &iFilterText);
+    int transactionStatusFilter() const;
+    void setTransactionStatusFilter(int iStatusFilter);
+    bool passwordRequired() const;
+    QVariantList dashboardBankTotals() const;
+    QString dashboardBankGrandTotalText() const;
+    QString dashboardCurrentPeriodLabel() const;
+    QString dashboardPreviousPeriodLabel() const;
+    QString dashboardCurrentIncomeText() const;
+    QString dashboardCurrentExpenseText() const;
+    QString dashboardPreviousIncomeText() const;
+    QString dashboardPreviousExpenseText() const;
+    QString dashboardCurrentSavingsText() const;
+    QString dashboardPreviousSavingsText() const;
+    double dashboardCurrentSavings() const;
+    double dashboardPreviousSavings() const;
+    QString dashboardPersonalFinanceScoreText() const;
+    QString dashboardPersonalFinanceScoreMessage() const;
+    QString dashboardPersonalFinanceScoreColor() const;
+    QString dashboardNetWorthText() const;
+    QString dashboardAnnualSpendingText() const;
+    QAbstractItemModel *trackersModel();
+    QAbstractItemModel *payeesModel();
+    QAbstractItemModel *categoriesModel();
+    QAbstractItemModel *unitsModel();
+
+    Q_INVOKABLE bool openFile();
+    Q_INVOKABLE bool openFile(const QString &iFilePath, const QString &iPassword = QString());
+    Q_INVOKABLE bool selectOpenAccount(const QString &iBank, const QString &iAccountName);
+    Q_INVOKABLE bool selectTracker(const QString &iTrackerName);
+    Q_INVOKABLE bool selectCategory(const QString &iCategoryFullName);
+    Q_INVOKABLE bool selectTransaction(int iOperationId);
+    Q_INVOKABLE void ensureDataForPage(int iPageIndex);
+
+Q_SIGNALS:
+    void filePathChanged();
+    void passwordChanged();
+    void showClosedAccountsChanged();
+    void showClosedTrackersChanged();
+    void errorMessageChanged();
+    void globalFilterTextChanged();
+    void globalAccountsTotalTextChanged();
+    void openAccountsChanged();
+    void selectedOpenAccountIndexChanged();
+    void selectedAccountBalanceTextChanged();
+    void selectedTrackerNameChanged();
+    void transactionFilterTextChanged();
+    void transactionStatusFilterChanged();
+    void passwordRequiredChanged();
+    void dashboardDataChanged();
+    void referenceDataChanged();
+
+private:
+    bool reloadAccountsFromCurrentDocument();
+    bool reloadOpenAccountsFromCurrentDocument();
+    void reloadDashboardFromCurrentDocument();
+    void clearDashboardData();
+    void reloadTrackersFromCurrentDocument();
+    void reloadPayeesFromCurrentDocument();
+    void reloadCategoriesFromCurrentDocument();
+    void reloadUnitsFromCurrentDocument();
+    void clearReferenceData();
+    void reloadTransactionsForSelectedAccount();
+    void rebuildTransactionsFromProxy();
+    void rebuildSubTransactionsFromProxy();
+    void clearLoadedDocumentState();
+    void refreshReferenceProxyFilters();
+    void applyFilterAndDateSort(SKGSortFilterProxyModel *iProxyModel, SKGObjectModelBase *iObjectModel);
+    bool loadSubTransactionsForSelection(const QString &iSelectionLabel, const QString &iWhereClause);
+    void updateSelectedAccountBalanceText();
+    void setPasswordRequired(bool iRequired);
+    void loadPersistedState();
+    void persistLastFilePath(const QString &iFilePath);
+    void clearPersistedLastFilePath();
+    void setErrorMessage(const QString &iMessage);
+
+    SKGDocumentBank m_document;
+    SKGMAccountListModel m_accountsModel;
+    SKGMTransactionListModel m_transactionsModel;
+    SKGMTransactionListModel m_subTransactionsModel;
+    SKGObjectModelBase *m_accountsObjectModel{nullptr};
+    SKGObjectModelBase *m_transactionsObjectModel{nullptr};
+    SKGObjectModelBase *m_subTransactionsObjectModel{nullptr};
+    SKGObjectModelBase *m_trackersObjectModel{nullptr};
+    SKGObjectModelBase *m_payeesObjectModel{nullptr};
+    SKGObjectModelBase *m_categoriesObjectModel{nullptr};
+    SKGObjectModelBase *m_unitsObjectModel{nullptr};
+    SKGSortFilterProxyModel *m_transactionsProxyModel{nullptr};
+    SKGSortFilterProxyModel *m_subTransactionsProxyModel{nullptr};
+    SKGSortFilterProxyModel *m_trackersProxyModel{nullptr};
+    SKGSortFilterProxyModel *m_payeesProxyModel{nullptr};
+    SKGSortFilterProxyModel *m_categoriesProxyModel{nullptr};
+    SKGSortFilterProxyModel *m_unitsProxyModel{nullptr};
+    SKGMReferenceProxyModel m_trackersModel;
+    SKGMReferenceProxyModel m_payeesModel;
+    SKGMReferenceProxyModel m_categoriesModel;
+    SKGMReferenceProxyModel m_unitsModel;
+    QString m_filePath;
+    QString m_password;
+    QString m_errorMessage;
+    QString m_globalFilterText;
+    QString m_globalAccountsTotalText;
+    QStringList m_openAccountNames;
+    QStringList m_openAccountBalanceTexts;
+    QStringList m_openAccountBanks;
+    QStringList m_openAccountAccountNames;
+    QVector<int> m_openAccountIds;
+    int m_selectedOpenAccountIndex{-1};
+    QString m_selectedAccountBalanceText;
+    QString m_selectedTrackerName;
+    QString m_transactionFilterText;
+    int m_transactionStatusFilter{2};
+    bool m_passwordRequired{false};
+    bool m_showClosedAccounts{false};
+    bool m_showClosedTrackers{false};
+    bool m_documentLoaded{false};
+    bool m_dashboardDataLoaded{false};
+    bool m_accountsDataLoaded{false};
+    bool m_openAccountsDataLoaded{false};
+    bool m_transactionsDataLoaded{false};
+    bool m_subTransactionsDataLoaded{false};
+    bool m_trackersDataLoaded{false};
+    bool m_payeesDataLoaded{false};
+    bool m_categoriesDataLoaded{false};
+    bool m_unitsDataLoaded{false};
+    QVariantList m_dashboardBankTotals;
+    QString m_dashboardBankGrandTotalText;
+    QString m_dashboardCurrentPeriodLabel;
+    QString m_dashboardPreviousPeriodLabel;
+    QString m_dashboardCurrentIncomeText;
+    QString m_dashboardCurrentExpenseText;
+    QString m_dashboardPreviousIncomeText;
+    QString m_dashboardPreviousExpenseText;
+    QString m_dashboardCurrentSavingsText;
+    QString m_dashboardPreviousSavingsText;
+    double m_dashboardCurrentSavings{0.0};
+    double m_dashboardPreviousSavings{0.0};
+    QString m_dashboardPersonalFinanceScoreText;
+    QString m_dashboardPersonalFinanceScoreMessage;
+    QString m_dashboardPersonalFinanceScoreColor{QStringLiteral("#666666")};
+    QString m_dashboardNetWorthText;
+    QString m_dashboardAnnualSpendingText;
+};
+
+#endif
diff --git a/skroogecompanion/skgmreferenceproxymodel.cpp b/skroogecompanion/skgmreferenceproxymodel.cpp
new file mode 100644
index 000000000..c6b89befb
--- /dev/null
+++ b/skroogecompanion/skgmreferenceproxymodel.cpp
@@ -0,0 +1,179 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#include "skgmreferenceproxymodel.h"
+
+#include "skgobjectmodelbase.h"
+#include "skgservices.h"
+
+#include <klocalizedstring.h>
+
+#include "skgtraces.h"
+#include <qabstractproxymodel.h>
+#include <qlocale.h>
+#include <qstringbuilder.h>
+
+SKGMReferenceProxyModel::SKGMReferenceProxyModel(QObject *iParent)
+    : QIdentityProxyModel(iParent)
+{
+}
+
+void SKGMReferenceProxyModel::setKind(ReferenceKind iKind)
+{
+    if (m_kind == iKind) {
+        return;
+    }
+    m_kind = iKind;
+    if (rowCount() > 0) {
+        Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
+    }
+}
+
+void SKGMReferenceProxyModel::setObjectModel(const SKGObjectModelBase *iObjectModel)
+{
+    m_objectModel = iObjectModel;
+}
+
+void SKGMReferenceProxyModel::setAmountUnitSymbol(const QString &iSymbol)
+{
+    if (m_amountUnitSymbol == iSymbol) {
+        return;
+    }
+    m_amountUnitSymbol = iSymbol;
+    if (rowCount() > 0) {
+        Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AmountTextRole});
+    }
+}
+
+QVariant SKGMReferenceProxyModel::data(const QModelIndex &iIndex, int iRole) const
+{
+    SKGTRACEINFUNC(1)
+    if (!iIndex.isValid() || m_objectModel == nullptr) {
+        return {};
+    }
+
+    const QModelIndex sourceIndex = mapToSource(iIndex);
+    if (!sourceIndex.isValid()) {
+        return {};
+    }
+
+    switch (iRole) {
+    case NameRole:
+        switch (m_kind) {
+        case TrackerKind:
+        case PayeeKind:
+            return stringAttribute(sourceIndex, QStringLiteral("t_name"));
+        case CategoryKind:
+            return stringAttribute(sourceIndex, QStringLiteral("t_fullname"));
+        case UnitKind: {
+            const QString name = stringAttribute(sourceIndex, QStringLiteral("t_name"));
+            const QString symbol = stringAttribute(sourceIndex, QStringLiteral("t_symbol"));
+            return symbol.isEmpty() ? name : name % QStringLiteral(" (") % symbol % u')';
+        }
+        }
+        break;
+    case SubtitleRole:
+        switch (m_kind) {
+        case TrackerKind: {
+            const QString operations = stringAttribute(sourceIndex, QStringLiteral("i_NBOPERATIONS"));
+            return operations.isEmpty() ? QString() : i18nc("@info:tracker operations", "%1 transactions", operations);
+        }
+        case PayeeKind: {
+            const QString category = stringAttribute(sourceIndex, QStringLiteral("t_CATEGORY"));
+            const QString operations = stringAttribute(sourceIndex, QStringLiteral("i_NBOPERATIONS"));
+            if (operations.isEmpty()) {
+                return category;
+            }
+            return category.isEmpty() ? i18nc("@info:payee operations", "%1 transactions", operations)
+                                      : i18nc("@info:payee subtitle", "%1 - %2 operations", category, operations);
+        }
+        case CategoryKind: {
+            const QString operations = stringAttribute(sourceIndex, QStringLiteral("i_SUMNBOPERATIONS"));
+            return operations.isEmpty() ? QString() : i18nc("@info:category operations", "%1 transactions", operations);
+        }
+        case UnitKind:
+            return stringAttribute(sourceIndex, QStringLiteral("t_TYPENLS"));
+        }
+        break;
+    case AmountRole:
+        switch (m_kind) {
+        case TrackerKind:
+        case PayeeKind:
+            return doubleAttribute(sourceIndex, QStringLiteral("f_CURRENTAMOUNT"));
+        case CategoryKind:
+            return doubleAttribute(sourceIndex, QStringLiteral("f_SUMCURRENTAMOUNT"));
+        case UnitKind:
+            return doubleAttribute(sourceIndex, QStringLiteral("f_AMOUNTOWNED"));
+        }
+        break;
+    case AmountTextRole: {
+        const double amount = data(iIndex, AmountRole).toDouble();
+        return formatAmount(amount);
+    }
+    case ClosedRole:
+        if (m_kind == TrackerKind) {
+            return stringAttribute(sourceIndex, QStringLiteral("t_close")) == QStringLiteral("Y");
+        }
+        return false;
+    default:
+        return QIdentityProxyModel::data(iIndex, iRole);
+    }
+
+    return {};
+}
+
+QHash<int, QByteArray> SKGMReferenceProxyModel::roleNames() const
+{
+    return {
+        {NameRole, "name"},
+        {SubtitleRole, "subtitle"},
+        {AmountRole, "amount"},
+        {AmountTextRole, "amountText"},
+        {ClosedRole, "closed"},
+    };
+}
+
+QString SKGMReferenceProxyModel::stringAttribute(const QModelIndex &iSourceIndex, const QString &iAttribute) const
+{
+    SKGTRACEINFUNC(1)
+    const QModelIndex objectIndex = mapToObjectModelIndex(iSourceIndex);
+    if (!objectIndex.isValid()) {
+        return {};
+    }
+    return m_objectModel->getObject(objectIndex).getAttribute(iAttribute);
+}
+
+double SKGMReferenceProxyModel::doubleAttribute(const QModelIndex &iSourceIndex, const QString &iAttribute) const
+{
+    SKGTRACEINFUNC(1)
+    const QModelIndex objectIndex = mapToObjectModelIndex(iSourceIndex);
+    if (!objectIndex.isValid()) {
+        return 0.0;
+    }
+    return SKGServices::stringToDouble(m_objectModel->getObject(objectIndex).getAttribute(iAttribute));
+}
+
+QModelIndex SKGMReferenceProxyModel::mapToObjectModelIndex(const QModelIndex &iSourceIndex) const
+{
+    SKGTRACEINFUNC(1)
+    QModelIndex current = iSourceIndex;
+    const QAbstractItemModel *model = sourceModel();
+    while (auto proxy = qobject_cast<const QAbstractProxyModel *>(model)) {
+        current = proxy->mapToSource(current);
+        model = proxy->sourceModel();
+    }
+    if (model != m_objectModel) {
+        return {};
+    }
+    return current;
+}
+
+QString SKGMReferenceProxyModel::formatAmount(double iAmount) const
+{
+    QString text = QLocale().toString(iAmount, 'f', 2);
+    if (!m_amountUnitSymbol.isEmpty()) {
+        text = text % u' ' % m_amountUnitSymbol;
+    }
+    return text;
+}
diff --git a/skroogecompanion/skgmreferenceproxymodel.h b/skroogecompanion/skgmreferenceproxymodel.h
new file mode 100644
index 000000000..b6286a9af
--- /dev/null
+++ b/skroogecompanion/skgmreferenceproxymodel.h
@@ -0,0 +1,55 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#ifndef SKGMREFERENCEPROXYMODEL_H
+#define SKGMREFERENCEPROXYMODEL_H
+
+#include <QIdentityProxyModel>
+
+class SKGObjectModelBase;
+class QModelIndex;
+
+class SKGMReferenceProxyModel : public QIdentityProxyModel
+{
+    Q_OBJECT
+
+public:
+    enum ReferenceKind {
+        TrackerKind = 0,
+        PayeeKind = 1,
+        CategoryKind = 2,
+        UnitKind = 3
+    };
+    Q_ENUM(ReferenceKind)
+
+    enum Roles {
+        NameRole = Qt::UserRole + 1,
+        SubtitleRole,
+        AmountRole,
+        AmountTextRole,
+        ClosedRole
+    };
+    Q_ENUM(Roles)
+
+    explicit SKGMReferenceProxyModel(QObject *iParent = nullptr);
+
+    void setKind(ReferenceKind iKind);
+    void setObjectModel(const SKGObjectModelBase *iObjectModel);
+    void setAmountUnitSymbol(const QString &iSymbol);
+
+    QVariant data(const QModelIndex &iIndex, int iRole = Qt::DisplayRole) const override;
+    QHash<int, QByteArray> roleNames() const override;
+
+private:
+    QModelIndex mapToObjectModelIndex(const QModelIndex &iSourceIndex) const;
+    QString stringAttribute(const QModelIndex &iSourceIndex, const QString &iAttribute) const;
+    double doubleAttribute(const QModelIndex &iSourceIndex, const QString &iAttribute) const;
+    QString formatAmount(double iAmount) const;
+
+    const SKGObjectModelBase *m_objectModel{nullptr};
+    ReferenceKind m_kind{TrackerKind};
+    QString m_amountUnitSymbol;
+};
+
+#endif
diff --git a/skroogecompanion/skgmtransactionlistmodel.cpp b/skroogecompanion/skgmtransactionlistmodel.cpp
new file mode 100644
index 000000000..28113db1a
--- /dev/null
+++ b/skroogecompanion/skgmtransactionlistmodel.cpp
@@ -0,0 +1,81 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#include "skgmtransactionlistmodel.h"
+
+SKGMTransactionListModel::SKGMTransactionListModel(QObject *iParent)
+    : QAbstractListModel(iParent)
+{
+}
+
+int SKGMTransactionListModel::rowCount(const QModelIndex &iParent) const
+{
+    if (iParent.isValid()) {
+        return 0;
+    }
+    return m_transactions.count();
+}
+
+QVariant SKGMTransactionListModel::data(const QModelIndex &iIndex, int iRole) const
+{
+    if (!iIndex.isValid() || iIndex.row() < 0 || iIndex.row() >= m_transactions.count()) {
+        return {};
+    }
+
+    const TransactionItem &item = m_transactions.at(iIndex.row());
+    switch (iRole) {
+    case Qt::DisplayRole:
+    case Qt::UserRole:
+        return QStringLiteral("%1 %2 %3 %4 %5").arg(item.payee, item.category, item.comment, item.mode, item.number);
+    case DateRole:
+        return item.date;
+    case OperationIdRole:
+        return item.operationId;
+    case PayeeRole:
+        return item.payee;
+    case CategoryRole:
+        return item.category;
+    case CommentRole:
+        return item.comment;
+    case ModeRole:
+        return item.mode;
+    case NumberRole:
+        return item.number;
+    case AmountTextRole:
+        return item.amountText;
+    case AmountRole:
+        return item.amount;
+    default:
+        return {};
+    }
+}
+
+QHash<int, QByteArray> SKGMTransactionListModel::roleNames() const
+{
+    return {
+        {DateRole, "date"},
+        {OperationIdRole, "operationId"},
+        {PayeeRole, "payee"},
+        {CategoryRole, "category"},
+        {CommentRole, "comment"},
+        {ModeRole, "mode"},
+        {NumberRole, "number"},
+        {AmountTextRole, "amountText"},
+        {AmountRole, "amount"},
+    };
+}
+
+void SKGMTransactionListModel::setTransactions(QVector<TransactionItem> iTransactions)
+{
+    beginResetModel();
+    m_transactions = std::move(iTransactions);
+    endResetModel();
+}
+
+void SKGMTransactionListModel::clear()
+{
+    beginResetModel();
+    m_transactions.clear();
+    endResetModel();
+}
diff --git a/skroogecompanion/skgmtransactionlistmodel.h b/skroogecompanion/skgmtransactionlistmodel.h
new file mode 100644
index 000000000..f8abcd2a6
--- /dev/null
+++ b/skroogecompanion/skgmtransactionlistmodel.h
@@ -0,0 +1,54 @@
+/***************************************************************************
+ * SPDX-FileCopyrightText: 2026 S. MANKOWSKI [email protected]
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ ***************************************************************************/
+#ifndef SKGMTRANSACTIONLISTMODEL_H
+#define SKGMTRANSACTIONLISTMODEL_H
+
+#include <QAbstractListModel>
+#include <QVector>
+
+class SKGMTransactionListModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+public:
+    struct TransactionItem {
+        int operationId{-1};
+        QString date;
+        QString payee;
+        QString category;
+        QString comment;
+        QString mode;
+        QString number;
+        QString amountText;
+        double amount{0.0};
+    };
+
+    enum TransactionRoles {
+        DateRole = Qt::UserRole + 1,
+        OperationIdRole,
+        PayeeRole,
+        CategoryRole,
+        CommentRole,
+        ModeRole,
+        NumberRole,
+        AmountTextRole,
+        AmountRole
+    };
+    Q_ENUM(TransactionRoles)
+
+    explicit SKGMTransactionListModel(QObject *iParent = nullptr);
+
+    int rowCount(const QModelIndex &iParent = QModelIndex()) const override;
+    QVariant data(const QModelIndex &iIndex, int iRole = Qt::DisplayRole) const override;
+    QHash<int, QByteArray> roleNames() const override;
+
+    void setTransactions(QVector<TransactionItem> iTransactions);
+    void clear();
+
+private:
+    QVector<TransactionItem> m_transactions;
+};
+
+#endif
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.