[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