commit 4721e6c0347c645bd0f981b2be0d1e6c564b37bd Author: yajo10 Date: Mon Nov 13 19:04:58 2023 +0100 Initial commit diff --git a/harbour-expenditure.desktop b/harbour-expenditure.desktop new file mode 100644 index 0000000..8d66113 --- /dev/null +++ b/harbour-expenditure.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] +Type=Application +X-Nemo-Application-Type=silica-qt5 +Icon=harbour-expenditure +Exec=harbour-expenditure +Name=Expenditure +# translation example: +# your app name in German locale (de) +# +# Remember to comment out the following line, if you do not want to use +# a different app name in German locale (de). +#Name[de]=Expenditure + +[X-Sailjail] +# Replace with your organization as a reverse domain name +OrganizationName=org.tplabs +# ApplicationName does not have to be identical to Name +ApplicationName=expenditure +# Add the required permissions here +Permissions=PublicDir;UserDirs;RemovableMedia diff --git a/harbour-expenditure.pro b/harbour-expenditure.pro new file mode 100644 index 0000000..ac729da --- /dev/null +++ b/harbour-expenditure.pro @@ -0,0 +1,48 @@ +# NOTICE: +# +# Application name defined in TARGET has a corresponding QML filename. +# If name defined in TARGET is changed, the following needs to be done +# to match new name: +# - corresponding QML filename must be changed +# - desktop icon filename must be changed +# - desktop filename must be changed +# - icon definition filename in desktop file must be changed +# - translation filenames have to be changed + +# The name of your application +TARGET = harbour-expenditure + +CONFIG += sailfishapp + +SOURCES += src/harbour-expenditure.cpp \ + src/File.cpp + +DISTFILES += qml/harbour-expenditure.qml \ + qml/cover/CoverPage.qml \ + qml/pages/AboutPage.qml \ + qml/pages/Banner2ButtonsChoice.qml \ + qml/pages/BannerAddProject.qml \ + qml/pages/CalcPage.qml \ + qml/pages/FirstPage.qml \ + qml/pages/SettingsPage.qml \ + rpm/harbour-expenditure.changes.in \ + rpm/harbour-expenditure.changes.run.in \ + rpm/harbour-expenditure.spec \ + rpm/harbour-expenditure.yaml \ + translations/*.ts \ + harbour-expenditure.desktop + +SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172 + +# to disable building translations every time, comment out the +# following CONFIG line +CONFIG += sailfishapp_i18n + +# German translation is enabled as an example. If you aren't +# planning to localize your app, remember to comment out the +# following TRANSLATIONS line. And also do not forget to +# modify the localized app name in the the .desktop file. +TRANSLATIONS += translations/harbour-expenditure-de.ts + +HEADERS += \ + src/File.h diff --git a/harbour-expenditure.pro.user b/harbour-expenditure.pro.user new file mode 100644 index 0000000..6fa0a83 --- /dev/null +++ b/harbour-expenditure.pro.user @@ -0,0 +1,1117 @@ + + + + + + EnvironmentId + {8e5ac1eb-2ed7-489e-a123-f178932f820f} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + false + true + false + 0 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + + 0 + true + + true + Builtin.BuildSystem + + true + true + Builtin.DefaultTidyAndClazy + 4 + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Mer.Device.Type + SailfishOS-4.4.0.58-aarch64 (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-aarch64 (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-aarch64.default + 1 + 0 + 0 + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Debug + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Debug + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + + + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Release + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Release + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Profile + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Profile + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + 0 + + 3 + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerRpmBuildStep + + + --sdk + true + QmakeProjectManager.MerRpmDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRpmDeployConfiguration + + + + + true + QmakeProjectManager.MerRpmBuildStep + + + true + QmakeProjectManager.MerRpmValidationStep + + 2 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerMb2RpmBuildConfiguration + + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerMakeInstallBuildStep + + + --rsync + true + QmakeProjectManager.MerRsyncDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRSyncDeployConfiguration + + 3 + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + + 25 + + 1 + true + false + true + + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + true + /home/tobias/Dokumente/Sailfish/harbour-expenditure + true + -1 + 3 + + 1 + + harbour-expenditure + QmakeProjectManager.MerRunConfiguration:/home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + /home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + 1 + false + true + false + true + :0 + + 1 + + + + ProjectExplorer.Project.Target.1 + + Mer.Device.Type + SailfishOS-4.4.0.58-i486 (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-i486 (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-i486.default + 1 + 1 + 0 + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Debug + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Debug + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + + + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Release + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Release + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Profile + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Profile + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + 0 + + 3 + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerRpmBuildStep + + + --sdk + true + QmakeProjectManager.MerRpmDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRpmDeployConfiguration + + + + + true + QmakeProjectManager.MerRpmBuildStep + + + true + QmakeProjectManager.MerRpmValidationStep + + 2 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerMb2RpmBuildConfiguration + + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerMakeInstallBuildStep + + + --rsync + true + QmakeProjectManager.MerRsyncDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRSyncDeployConfiguration + + 3 + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + + 25 + + 1 + true + false + true + + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + true + /home/tobias/Dokumente/Sailfish/harbour-expenditure + true + -1 + 3 + + 1 + + harbour-expenditure (on %{Device:Name}) + QmakeProjectManager.MerRunConfiguration:/home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + /home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + 1 + false + true + false + true + :0 + + 1 + + + + ProjectExplorer.Project.Target.2 + + Mer.Device.Type + SailfishOS-4.4.0.58-armv7hl (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-armv7hl (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-armv7hl.default + 1 + 1 + 0 + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Debug + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Debug + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + + + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Release + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Release + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Profile + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Profile + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + 0 + + 3 + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerRpmBuildStep + + + --sdk + true + QmakeProjectManager.MerRpmDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRpmDeployConfiguration + + + + + true + QmakeProjectManager.MerRpmBuildStep + + + true + QmakeProjectManager.MerRpmValidationStep + + 2 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerMb2RpmBuildConfiguration + + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerMakeInstallBuildStep + + + --rsync + true + QmakeProjectManager.MerRsyncDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRSyncDeployConfiguration + + 3 + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + + 25 + + 1 + true + false + true + + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + true + /home/tobias/Dokumente/Sailfish/harbour-expenditure + false + -1 + 3 + + 1 + + harbour-expenditure + QmakeProjectManager.MerRunConfiguration:/home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + /home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + 1 + false + true + false + true + :0 + + 1 + + + + ProjectExplorer.Project.TargetCount + 3 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/icons/108x108/harbour-expenditure.png b/icons/108x108/harbour-expenditure.png new file mode 100644 index 0000000..c60641b Binary files /dev/null and b/icons/108x108/harbour-expenditure.png differ diff --git a/icons/128x128/harbour-expenditure.png b/icons/128x128/harbour-expenditure.png new file mode 100644 index 0000000..d17abb2 Binary files /dev/null and b/icons/128x128/harbour-expenditure.png differ diff --git a/icons/172x172/harbour-expenditure.png b/icons/172x172/harbour-expenditure.png new file mode 100644 index 0000000..efcf2d7 Binary files /dev/null and b/icons/172x172/harbour-expenditure.png differ diff --git a/icons/86x86/harbour-expenditure.png b/icons/86x86/harbour-expenditure.png new file mode 100644 index 0000000..017e50a Binary files /dev/null and b/icons/86x86/harbour-expenditure.png differ diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml new file mode 100644 index 0000000..2f69fe7 --- /dev/null +++ b/qml/cover/CoverPage.qml @@ -0,0 +1,34 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +CoverBackground { + /* + Label { + id: label + anchors.centerIn: parent + text: qsTr("Expenditure") + } + */ + Image { + id: name + anchors.centerIn: parent + source: "harbour-expenditure.png" + width: Theme.iconSizeLarge + height: Theme.iconSizeLarge + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + } + /* + CoverActionList { + id: coverAction + + CoverAction { + iconSource: "image://theme/icon-cover-next" + } + CoverAction { + iconSource: "image://theme/icon-cover-pause" + } + } + */ +} diff --git a/qml/cover/harbour-expenditure.png b/qml/cover/harbour-expenditure.png new file mode 100644 index 0000000..7717d2e Binary files /dev/null and b/qml/cover/harbour-expenditure.png differ diff --git a/qml/cover/harbour-expenditure.svg b/qml/cover/harbour-expenditure.svg new file mode 100755 index 0000000..8b24cb1 --- /dev/null +++ b/qml/cover/harbour-expenditure.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qml/harbour-expenditure.qml b/qml/harbour-expenditure.qml new file mode 100644 index 0000000..9050c19 --- /dev/null +++ b/qml/harbour-expenditure.qml @@ -0,0 +1,409 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import QtQuick.LocalStorage 2.0 +import "pages" + +ApplicationWindow { + initialPage: Component { FirstPage { } } + cover: Qt.resolvedUrl("cover/CoverPage.qml") + allowedOrientations: defaultAllowedOrientations + + Item { + id: storageItem + + // general functions + function getDatabase() { + return storageItem.LocalStorage.openDatabaseSync("Bible_DB", "0.1", "BibleDatabaseComplete", 5000000); // 5 MB estimated size + } + function removeFullTable (tableName) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { tx.executeSql('DROP TABLE IF EXISTS ' + tableName) }); + } + function getTableCount (tableName, default_value) { + var db = getDatabase(); + var res=""; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT count(*) AS some_info FROM ' + tableName + ';'); + if (rs.rows.length > 0) { + res = rs.rows.item(0).some_info; + + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + + // settings + function setSettings( setting, value ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'settings_table' + '(setting TEXT UNIQUE, value TEXT)'); + var rs = tx.executeSql('INSERT OR REPLACE INTO ' + 'settings_table' + ' VALUES (?,?);', [setting,value]); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function getSettings( setting, default_value ) { + var db = getDatabase(); + var res=""; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT value FROM '+ 'settings_table' +' WHERE setting=?;', [setting]); + if (rs.rows.length > 0) { + res = rs.rows.item(0).value; + } else { + res = default_value; + } + if (res === null) { + res = default_value + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + //console.log(setting + " = " + res) + return res + } + + // all projects available + function setProject( project_id_timestamp, project_name, project_members, project_recent_payer_boolarray, project_recent_beneficiaries_boolarray, project_base_currency ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'projects_table' + ' (project_id_timestamp TEXT, + project_name TEXT, + project_members TEXT, + project_recent_payer_boolarray TEXT, + project_recent_beneficiaries_boolarray TEXT, + project_base_currency TEXT)' ); + var rs = tx.executeSql('INSERT OR REPLACE INTO ' + 'projects_table' + ' VALUES (?,?,?,?,?,?);', [project_id_timestamp, + project_name, + project_members, + project_recent_payer_boolarray, + project_recent_beneficiaries_boolarray, + project_base_currency ]); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function updateProject ( project_id_timestamp, project_name, project_members, project_recent_payer_boolarray, project_recent_beneficiaries_boolarray, project_base_currency ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'projects_table' + ' (project_id_timestamp TEXT, + project_name TEXT, + project_members TEXT, + project_recent_payer_boolarray TEXT, + project_recent_beneficiaries_boolarray TEXT, + project_base_currency TEXT)' ); + var rs = tx.executeSql('UPDATE projects_table' + + ' SET project_name="' + project_name + + '", project_members="' + project_members + + '", project_recent_payer_boolarray="' + project_recent_payer_boolarray + + '", project_recent_beneficiaries_boolarray="' + project_recent_beneficiaries_boolarray + + '", project_base_currency="' + project_base_currency + + '" WHERE project_id_timestamp=' + project_id_timestamp + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function updateField_Project ( project_id_timestamp, field_name, new_value ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'projects_table' + ' (project_id_timestamp TEXT, + project_name TEXT, + project_members TEXT, + project_recent_payer_boolarray TEXT, + project_recent_beneficiaries_boolarray TEXT, + project_base_currency TEXT)' ); + var rs = tx.executeSql('UPDATE projects_table SET ' + field_name + '="' + new_value + '" WHERE project_id_timestamp=' + project_id_timestamp + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function deleteProject (project_id_timestamp) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'projects_table' + ' (project_id_timestamp TEXT, + project_name TEXT, + project_members TEXT, + project_recent_payer_boolarray TEXT, + project_recent_beneficiaries_boolarray TEXT, + project_base_currency TEXT)' ); + var rs = tx.executeSql('DELETE FROM ' + 'projects_table WHERE project_id_timestamp=' + project_id_timestamp + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + removeFullTable("table_" + project_id_timestamp) + return res; + } + function getAllProjects( default_value ) { + var db = getDatabase(); + var res=[]; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT * FROM '+ 'projects_table;') + if (rs.rows.length > 0) { + for (var i = 0; i < rs.rows.length; i++) { + res.push([rs.rows.item(i).project_id_timestamp, + rs.rows.item(i).project_name, + rs.rows.item(i).project_members, + rs.rows.item(i).project_recent_payer_boolarray, + rs.rows.item(i).project_recent_beneficiaries_boolarray, + rs.rows.item(i).project_base_currency, + ]) + } + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + + // all exchange rates used + function countExchangeRateOccurances (exchange_rate_currency, default_value) { + var db = getDatabase(); + var res=""; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT count(*) AS some_info FROM exchange_rates_table WHERE exchange_rate_currency=?;', [exchange_rate_currency]); + if (rs.rows.length > 0) { + res = rs.rows.item(0).some_info; + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + function setExchangeRate( exchange_rate_currency, exchange_rate_value ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'exchange_rates_table' + ' (exchange_rate_currency TEXT, exchange_rate_value TEXT)' ); + var rs = tx.executeSql('INSERT OR REPLACE INTO ' + 'exchange_rates_table' + ' VALUES (?,?);', [exchange_rate_currency, exchange_rate_value ]); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function updateExchangeRate( exchange_rate_currency, exchange_rate_value ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS exchange_rates_table (exchange_rate_currency TEXT, exchange_rate_value TEXT)'); + var rs = tx.executeSql('UPDATE exchange_rates_table SET exchange_rate_value="' + exchange_rate_value + '" WHERE exchange_rate_currency="' + exchange_rate_currency + '";'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function getExchangeRate(exchange_rate_currency, default_value) { + var db = getDatabase(); + var res=[]; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT * FROM '+ 'exchange_rates_table' +' WHERE exchange_rate_currency=?;', [exchange_rate_currency]); + if (rs.rows.length > 0) { + for (var i = 0; i < rs.rows.length; i++) { + res.push(rs.rows.item(i).exchange_rate_value) + } + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + + + // all expenes in current project + function setExpense( project_name_table, id_unixtime_created, date_time, expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS table_' + project_name_table + ' (id_unixtime_created TEXT, + date_time TEXT, + expense_name TEXT, + expense_sum TEXT, + expense_currency TEXT, + expense_info TEXT, + expense_payer TEXT, + expense_members TEXT)' ); + var rs = tx.executeSql('INSERT OR REPLACE INTO table_' + project_name_table + ' VALUES (?,?,?,?,?,?,?,?);', [ id_unixtime_created, + date_time, + expense_name, + expense_sum, + expense_currency, + expense_info, + expense_payer, + expense_members ]); + if (rs.rowsAffected > 0) { + res = "OK"; + //console.log("project info found and updated") + } else { + res = "Error"; + } + } + ); + return res; + } + function updateExpense ( project_name_table, id_unixtime_created, date_time, expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS table_' + project_name_table + ' (id_unixtime_created TEXT, + date_time TEXT, + expense_name TEXT, + expense_sum TEXT, + expense_currency TEXT, + expense_info TEXT, + expense_payer TEXT, + expense_members TEXT)' ); + var rs = tx.executeSql('UPDATE table_' + project_name_table + + ' SET date_time="' + date_time + + '", expense_name="' + expense_name + + '", expense_sum="' + expense_sum + + '", expense_currency="' + expense_currency + + '", expense_info="' + expense_info + + '", expense_payer="' + expense_payer + + '", expense_members="' + expense_members + + '" WHERE id_unixtime_created=' + id_unixtime_created + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function deleteExpense (project_id_timestamp, id_unixtime_created) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS table_' + project_id_timestamp + ' (id_unixtime_created TEXT, + date_time TEXT, + expense_name TEXT, + expense_sum TEXT, + expense_currency TEXT, + expense_info TEXT, + expense_payer TEXT, + expense_members TEXT)' ); + //var rs = tx.executeSql('DELETE FROM table_' + project_id_timestamp + ';'); + var rs = tx.executeSql('DELETE FROM table_' + project_id_timestamp + ' WHERE id_unixtime_created=' + id_unixtime_created + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function deleteAllExpenses (project_id_timestamp) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS table_' + project_id_timestamp + ' (id_unixtime_created TEXT, + date_time TEXT, + expense_name TEXT, + expense_sum TEXT, + expense_currency TEXT, + expense_info TEXT, + expense_payer TEXT, + expense_members TEXT)' ); + var rs = tx.executeSql('DELETE FROM table_' + project_id_timestamp + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function getAllExpenses( project_name_table, default_value ) { + var db = getDatabase(); + var res=[]; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT * FROM table_'+ project_name_table + ';'); + if (rs.rows.length > 0) { + for (var i = 0; i < rs.rows.length; i++) { + res.push([rs.rows.item(i).id_unixtime_created, + rs.rows.item(i).date_time, + rs.rows.item(i).expense_name, + rs.rows.item(i).expense_sum, + rs.rows.item(i).expense_currency, + rs.rows.item(i).expense_info, + rs.rows.item(i).expense_payer, + rs.rows.item(i).expense_members, + ]) + } + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + } + +} diff --git a/qml/pages/AboutPage.qml b/qml/pages/AboutPage.qml new file mode 100644 index 0000000..08b95f2 --- /dev/null +++ b/qml/pages/AboutPage.qml @@ -0,0 +1,91 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +Page { + id: page + allowedOrientations: Orientation.Portrait //All + + SilicaFlickable { + id: listView + anchors.fill: parent + contentHeight: idColumn.height // Tell SilicaFlickable the height of its content. + + VerticalScrollDecorator {} + + Column { + id: idColumn + x: Theme.paddingLarge + width: parent.width - 2*x + + Label { + width: parent.width + height: Theme.itemSizeLarge + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: Theme.fontSizeLarge + color: Theme.primaryColor + text: qsTr("Expenditure") + } + Item { + width: parent.width + height: Theme.paddingLarge + } + Image { + width: parent.width + height: Theme.itemSizeHuge + source: "../cover/harbour-expenditure.png" + sourceSize.width: height + sourceSize.height: height + fillMode: Image.PreserveAspectFit + } + Item { + width: parent.width + height: Theme.paddingLarge * 2.5 + } + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + text: qsTr("Expenditure is a tool to track and split bills, project or trip expenses in multiple currencies among groups.") + + } + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + text: qsTr("Thanksgiving, feedback and support is always welcome.") + bottomPadding: Theme.paddingLarge * 2 + } + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + text: qsTr("Troubleshooting:") + + "\n" + qsTr("In case of any database error tap 10x on the word 'Settings' for cleanup options.") + bottomPadding: Theme.paddingLarge * 2 + } + Label { + width: parent.width + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + wrapMode: Text.Wrap + text: qsTr("Contact:") + + "\n" + qsTr("Copyright © 2022 Tobias Planitzer") + + "\n" + ("tp.labs@protonmail.com") + + "\n" + qsTr("License: GPL v3") + } + Item { + width: parent.width + height: Theme.paddingLarge * 2.5 + } + } + } // end Silica Flickable +} diff --git a/qml/pages/Banner2ButtonsChoice.qml b/qml/pages/Banner2ButtonsChoice.qml new file mode 100644 index 0000000..a0c4870 --- /dev/null +++ b/qml/pages/Banner2ButtonsChoice.qml @@ -0,0 +1,153 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +MouseArea { + id: popup + z: 10 + width: parent.width + height: parent.height + visible: opacity > 0 + opacity: 0.0 + onClicked: { + hide() + } + + // UI variables + property var hideBackColor : Theme.rgba(Theme.overlayBackgroundColor, 0.9) + property string headlineInfoText : "" + property string detailedInfoText : "" + property string otherInfoText : "" + property string filePath_Action : "" + + Behavior on opacity { + FadeAnimator {} + } + + Rectangle { + anchors.fill: parent + color: hideBackColor + onColorChanged: opacity = 4 + + Rectangle { + id: idBackgroundRectProject + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: parent.width + height: parent.height - anchors.topMargin - Theme.paddingLarge + radius: Theme.paddingLarge + + SilicaFlickable { + anchors.fill: parent + contentHeight: addExpenseColumn.height + clip: true + + Column { + id: addExpenseColumn + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + Label { + x: Theme.paddingLarge + width: parent.width - 2*x + wrapMode: Text.WordWrap + text: headlineInfoText + bottomPadding: Theme.paddingLarge + } + Label { + x: Theme.paddingLarge + width: parent.width - 2*x + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeTiny + text: detailedInfoText + bottomPadding: Theme.paddingLarge + } + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + Button { + id: idButton1 + width: parent.width /2 - Theme.paddingLarge /2 + onClicked: { + restoreProjectExpenses(filePath_Action, "replace") + //backNavigationBlocked_deletedAddedProject = true + hide() + } + } + Item { + width: Theme.paddingLarge + height: 1 + } + Button { + id: idButton2 + width: parent.width /2 - Theme.paddingLarge /2 + height: idButton1.height + onClicked: { + restoreProjectExpenses(filePath_Action, "merge") + //backNavigationBlocked_deletedAddedProject = true + hide() + } + } + } + + Label { + x: Theme.paddingLarge + width: parent.width - 2*x + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeTiny + color: Theme.secondaryColor + text: otherInfoText + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + } + + } + } + } + } + Icon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: idBackgroundRectProject.anchors.topMargin / 2 - height/2 + source: "image://theme/icon-splus-cancel?" + opacity: 1 + } + + + function notify( color, upperMargin, headText, bodyText, otherText, choiceText_1, choiceText_2, filePath ) { + // color settings + if (color && (typeof(color) != "undefined")) { + idBackgroundRectProject.color = color + } else { + idBackgroundRectProject.color = Theme.rgba(Theme.highlightBackgroundColor, 0.9) + } + + // position settings + if (upperMargin && (typeof(upperMargin) != "undefined")) { + idBackgroundRectProject.anchors.topMargin = upperMargin + } else { + idBackgroundRectProject.anchors.topMargin = 0 + } + + // set texts + headlineInfoText = headText + detailedInfoText = bodyText + otherInfoText = otherText + idButton1.text = choiceText_1 + idButton2.text = choiceText_2 + filePath_Action = filePath + + // show banner overlay + popup.opacity = 1.0 + } + + function hide() { + popup.opacity = 0.0 + } + +} + diff --git a/qml/pages/BannerAddExpense.qml b/qml/pages/BannerAddExpense.qml new file mode 100644 index 0000000..bf9cc82 --- /dev/null +++ b/qml/pages/BannerAddExpense.qml @@ -0,0 +1,601 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +MouseArea { + id: popup + z: 10 + width: parent.width + height: parent.height + visible: opacity > 0 + opacity: 0.0 + onClicked: { + hide() + } + onOpacityChanged: { + //if needed + } + + + + // UI variables + property var activeProjectID + property var hideBackColor : Theme.rgba(Theme.overlayBackgroundColor, 0.9) + property int amountBeneficiaries : 0 + property string modeEdit : "new" + property date currentDate + property date currentTime + property double editedTimeStamp // unixtime, can be edited, is NOT entry creation timestamp + property double createdTimeStamp + property bool dateTimeManuallyChanged : false + + + // suppress blend to main window on this overlay, e.g. for context menu ... + // BUG: creates problems with _selectOrientation for context menus larger than 5 entries + property alias __silica_applicationwindow_instance: fakeApplicationWindow + Item { + id: fakeApplicationWindow + // suppresses warnings by context menu + property var _dimScreen + property var _undim + function _undim() {} + function _dimScreen() {} + } + Behavior on opacity { + FadeAnimator {} + } + + + Rectangle { + anchors.fill: parent + color: hideBackColor + onColorChanged: opacity = 4 + + Rectangle { + id: idBackgroundRectExpenses + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: parent.width + height: parent.height - anchors.topMargin - Theme.paddingLarge + radius: Theme.paddingLarge + + SilicaFlickable { + anchors.fill: parent + contentHeight: addExpenseColumn.height + clip: true + + Column { + id: addExpenseColumn + width: parent.width + + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge * 2 + + Column { + id: idColumnAddDate + width: parent.width /3*2 - Theme.paddingLarge /2 + + Label { + width: parent.width + text: currentDate.toLocaleDateString(Qt.locale(), "dd. MMMM yyyy") + color: Theme.highlightColor + + MouseArea { + anchors.fill: parent + onClicked: { + unFocusTextFields() + var dialog = pageStack.push(datePickerComponent, { + date: currentDate // preset picker to todays date + } ) + dialog.accepted.connect( function () { + currentDate = (dialog.date) + editedTimeStamp = Number((combineDateAndTime(currentDate, currentTime)).getTime()) + dateTimeManuallyChanged = true + } ) + } + } + } + Label { + width: parent.width + text: currentTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) + color: Theme.highlightColor + + MouseArea { + anchors.fill: parent + onClicked: { + unFocusTextFields() + var dialog = pageStack.push(timePickerComponent, { + hour: currentTime.getHours(), // preset picker to current time + minute: currentTime.getMinutes(), + hourMode: 1 + } ) + dialog.accepted.connect( function () { + currentTime = new Date ( dialog.time) + editedTimeStamp = Number((combineDateAndTime(currentDate, currentTime)).getTime()) + dateTimeManuallyChanged = true + } ) + } + } + } + /* + Label { + width: parent.width + font.pixelSize: Theme.fontSizeTiny + text: "create_" + createdTimeStamp + } + Label { + width: parent.width + font.pixelSize: Theme.fontSizeTiny + text: "edit_" + editedTimeStamp + } + */ + } + Item { + width: Theme.paddingLarge + height: 1 + } + Button { + id: idLabelHeaderAdd2 + enabled: (idTextfieldItem.length > 0) && (amountBeneficiaries > 0) + width: parent.width /3 - Theme.paddingLarge /2 + height: idColumnAddDate.height + text: (modeEdit === "new") ? qsTr("Add") : qsTr("Save") + onClicked: { + addEditExpense() + } + } + } + TextField { + id: idTextfieldItem + width: page.width + acceptableInput: text.length < 255 + font.pixelSize: Theme.fontSizeMedium + EnterKey.onClicked: { + focus = false + } + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("expense") + } + } + Row { + width: parent.width + + TextField { + id: idTextfieldPrice + width: parent.width /3 + parent.width /6 + textRightMargin: 0 + inputMethodHints: Qt.ImhFormattedNumbersOnly //use "Qt.ImhDigitsOnly" for INT + text: Number("0").toFixed(2) + EnterKey.onClicked: { + focus = false + } + onFocusChanged: { + text = text.replace(",", ".") + text = Number(text).toFixed(2) + if (focus) { + selectAll() + } + } + + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("price") + } + } + Item { + width: parent.width / 6 + height: 1 + } + TextField { + id: idTextfieldCurrency + width: parent.width /3 + textLeftMargin: 0 + horizontalAlignment: TextInput.AlignRight + acceptableInput: text.length > 0 + EnterKey.enabled: text.length >= 0 + EnterKey.onClicked: { + focus = false + } + onFocusChanged: { + if (text.length === 0) { + text = recentlyUsedCurrency + } + if (focus) { + selectAll() + } + } + + Label { + anchors.right: parent.right + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("currency") + } + } + } + TextField { + id: idTextfieldInfo + width: page.width + //acceptableInput: text.length < 255 + font.pixelSize: Theme.fontSizeMedium + EnterKey.onClicked: { + focus = false + } + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("info") + } + } + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge * 2 + bottomPadding: Theme.paddingMedium + + Label { + width: parent.width / 2 - Theme.paddingLarge/2 + verticalAlignment: Text.AlignVCenter + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("payment by") + } + Item { + width: Theme.paddingLarge + height: 1 + } + Label { + width: parent.width / 2 - Theme.paddingLarge/2 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("beneficiary") + } + } + Column { + x: Theme.paddingLarge + width: parent.width - 2*x + + Repeater { + model: listModel_activeProjectMembers + delegate: Row { + id: idtestcolumn + width: parent.width + + Label { + id: idLabelPayerName + width: parent.width / 3*2 - Theme.paddingLarge/2 + height: Theme.iconSizeSmallPlus + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + font.bold: member_isPayer === "true" + color: (member_isPayer === "true") ? Theme.primaryColor : Theme.highlightColor + text: member_name + + MouseArea { + anchors.fill: parent + onClicked: { + for (var i = 0; i < listModel_activeProjectMembers.count ; i++) { + listModel_activeProjectMembers.setProperty(i, "member_isPayer", "false") + } + member_isPayer="true" + } + } + } + Item { + width: Theme.paddingLarge + height: 1 + } + Item { + width: parent.width / 3 - Theme.paddingLarge/2 + height: parent.height + + Icon { + anchors.right: parent.right + height: parent.height + width: height + color: (member_isPayer==="true") ? Theme.primaryColor : Theme.highlightColor + source: (member_isBeneficiary==="true") ? "image://theme/icon-m-accept?" : "" + + Rectangle { + z: -1 + anchors.centerIn: parent + width: parent.width - Theme.paddingSmall + height: width + color: "transparent" + border.width: (member_isPayer==="true") ? 2 : 1 + border.color: Theme.secondaryColor + radius: width/4 + } + + MouseArea { + anchors.fill: parent + anchors.leftMargin: -Theme.paddingLarge + anchors.rightMargin: -Theme.paddingLarge + onClicked: { + (member_isBeneficiary==="true") ? (member_isBeneficiary="false") : (member_isBeneficiary="true") + amountBeneficiaries = 0 + for (var i = 0; i < listModel_activeProjectMembers.count ; i++) { + if (listModel_activeProjectMembers.get(i).member_isBeneficiary === "true") { + amountBeneficiaries += 1 + } + } + } + } + } + } + } + } + } + Item { + width: parent.width + height: Theme.itemSizeSmall / 2 + } + } + } + } + } + Icon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: idBackgroundRectExpenses.anchors.topMargin / 2 - height/2 + source: "image://theme/icon-splus-cancel?" + opacity: 1 + } + + + + function notify( color, upperMargin, modeEditNew, activeProjectID_unixtime, expense_ID_created ) { + // color settings + if (color && (typeof(color) != "undefined")) { + idBackgroundRectExpenses.color = color + } + else { + idBackgroundRectExpenses.color = Theme.rgba(Theme.highlightBackgroundColor, 0.9) + } + + // position settings + if (upperMargin && (typeof(upperMargin) != "undefined")) { + idBackgroundRectExpenses.anchors.topMargin = upperMargin + } + else { + idBackgroundRectExpenses.anchors.topMargin = 0 + } + + // project settings + activeProjectID = activeProjectID_unixtime + modeEdit = modeEditNew + + // reset time and date to current time and date + if (modeEditNew === "new") { + idTextfieldItem.text = "" + idTextfieldPrice.text = "0" + idTextfieldCurrency.text = recentlyUsedCurrency + idTextfieldInfo.text = "" + currentDate = new Date() + currentTime = new Date() + createdTimeStamp = Number(new Date().getTime()) + editedTimeStamp = Number(new Date().getTime()) + } else { // modeEditNew === "edit" + //console.log("editing " + expense_ID_created) + for (var i = 0; i < listModel_activeProjectExpenses.count ; i++) { + if (Number(expense_ID_created) === Number(listModel_activeProjectExpenses.get(i).id_unixtime_created)) { + idTextfieldItem.text = listModel_activeProjectExpenses.get(i).expense_name + idTextfieldPrice.text = listModel_activeProjectExpenses.get(i).expense_sum + idTextfieldCurrency.text = listModel_activeProjectExpenses.get(i).expense_currency + idTextfieldInfo.text = listModel_activeProjectExpenses.get(i).expense_info + var olderDateTime = Number(listModel_activeProjectExpenses.get(i).date_time) + currentDate = new Date(olderDateTime) + currentTime = new Date (olderDateTime) + createdTimeStamp = Number(listModel_activeProjectExpenses.get(i).id_unixtime_created) + editedTimeStamp = Number(listModel_activeProjectExpenses.get(i).date_time) + } + } + } + + // remember last beneficiaries and payer in "new" mode, load expense beneficiaries and payer in "edit" mode + amountBeneficiaries = 0 + if (modeEditNew === "new") { + + // count beneficiaries + for (i = 0; i < listModel_activeProjectMembers.count ; i++) { + if (listModel_activeProjectMembers.get(i).member_isBeneficiary === "true") { + amountBeneficiaries += 1 + } + } + + // use last used project specific beneficiaries and payers settings + for (var j = 0; j < listModel_allProjects.count ; j++) { + if ( Number(listModel_allProjects.get(j).project_id_timestamp) === Number(activeProjectID_unixtime) ) { + var activeProjectMembersArray = (listModel_allProjects.get(j).project_members).split(" ||| ") + var activeProjectRecentPayerArray = (listModel_allProjects.get(j).project_recent_payer_boolarray).split(" ||| ") + var activeProjectRecentBeneficiariesArray = (listModel_allProjects.get(j).project_recent_beneficiaries_boolarray).split(" ||| ") + for (var s = 0; s < activeProjectMembersArray.length ; s++) { + listModel_activeProjectMembers.set( s, { + "member_isBeneficiary" : activeProjectRecentBeneficiariesArray[s], + "member_isPayer" : activeProjectRecentPayerArray[s], + }) + } + } + } + } else { // "edit" mode + + // find item in expenses list for editing + for (var k = 0; k < listModel_activeProjectExpenses.count ; k++) { + if (Number(expense_ID_created) === Number(listModel_activeProjectExpenses.get(k).id_unixtime_created)) { + var editedBeneficiariesList = (listModel_activeProjectExpenses.get(k).expense_members).split(" ||| ") + var editPayersList =(listModel_activeProjectExpenses.get(k).expense_payer).split(" ||| ") + + for (var l = 0; l < listModel_activeProjectMembers.count; l++) { + listModel_activeProjectMembers.setProperty(l, "member_isBeneficiary", "false") + listModel_activeProjectMembers.setProperty(l, "member_isPayer", "false") + + // mark beneficiaries and get amount + for (var m = 0; m < editedBeneficiariesList.length; m++) { + if (listModel_activeProjectMembers.get(l).member_name === editedBeneficiariesList[m]) { + listModel_activeProjectMembers.setProperty(l, "member_isBeneficiary", "true") + amountBeneficiaries += 1 + } + } + // mark payer + for (m = 0; m < editPayersList.length; m++) { + if (listModel_activeProjectMembers.get(l).member_name === editPayersList[m]) { + listModel_activeProjectMembers.setProperty(l, "member_isPayer", "true") + } + } + } + } + } + } + //console.log(amountBeneficiaries) + + // show banner overlay + popup.opacity = 1.0 + + // focus on expense text searchField + if (modeEditNew === "new") { + idTextfieldItem.forceActiveFocus() + } + } + + function hide() { + unFocusTextFields() + popup.opacity = 0.0 // make invisible + + // clear all fields + idTextfieldItem.text = "" + idTextfieldPrice.text = "0" + idTextfieldCurrency.text = recentlyUsedCurrency + idTextfieldInfo.text = "" + //idButtonAddExpense.visible = true + } + + function unFocusTextFields() { + idTextfieldItem.focus = false + idTextfieldPrice.focus = false + idTextfieldCurrency.focus = false + idTextfieldInfo.focus = false + } + + function combineDateAndTime(date, time) { + // warning: slice necessary to avoid singele digit outputs which can not be used in combined call + var year = date.getFullYear(); + var month = ('0' + (date.getMonth() + 1)).slice(-2); // Jan is 0, dec is 11 + var day = ('0' + date.getDate()).slice(-2) + //var month = date.getMonth() // only give one digit outputs <10, which causes errors later + //var day = date.getDate(); // only gives one digit outputs <10, which causes errors later + var dateString = year + '-' + month + '-' + day; + var hours = ('0' + time.getHours()).slice(-2) + var minutes = ('0' + time.getMinutes()).slice(-2) + var timeString = hours + ':' + minutes + ':00'; + //var timeString = time.getHours() + ':' + time.getMinutes() + ':00'; + //console.log(dateString) + //console.log(timeString) + var combined = Date.fromLocaleString(Qt.locale(), dateString + ' ' + timeString, "yyyy-MM-dd hh:mm:ss") + //console.log(combined) + return combined; + } + + function addEditExpense() { + var project_name_table = activeProjectID.toString() + var id_unixtime_created = createdTimeStamp // time of entry creation, does not change, serves as unique expense_ID + var date_time = editedTimeStamp // new or edited time of expense + var expense_name = idTextfieldItem.text + var expense_sum = idTextfieldPrice.text + var expense_currency = idTextfieldCurrency.text + var expense_info = idTextfieldInfo.text + var expense_members = "" + var project_recent_payer_boolarray = "" + var project_recent_beneficiaries_boolarray = "" + for (var i = 0; i < listModel_activeProjectMembers.count ; i++) { + project_recent_payer_boolarray += " ||| " + listModel_activeProjectMembers.get(i).member_isPayer + project_recent_beneficiaries_boolarray += " ||| " + listModel_activeProjectMembers.get(i).member_isBeneficiary + if (listModel_activeProjectMembers.get(i).member_isPayer === "true") { + var expense_payer = listModel_activeProjectMembers.get(i).member_name + + } + if (listModel_activeProjectMembers.get(i).member_isBeneficiary === "true") { + expense_members += " ||| " + listModel_activeProjectMembers.get(i).member_name + } + } + project_recent_payer_boolarray = project_recent_payer_boolarray.replace(" ||| ", "") + project_recent_beneficiaries_boolarray = project_recent_beneficiaries_boolarray.replace(" ||| ", "") + expense_members = expense_members.replace(" ||| ", "") + //console.log("table_name= " + project_name_table) + //console.log(id_unixtime_created + ", " + date_time + ", " + expense_name + ", " + expense_sum + ", " + expense_currency + ", " + expense_info + ", " + expense_payer + ", " + expense_members) + + // update listmodel expenses and store in DB + if (modeEdit === "new") { + listModel_activeProjectExpenses.append({ + id_unixtime_created : Number(id_unixtime_created).toFixed(0), + date_time : Number(date_time).toFixed(0), + expense_name : expense_name, + expense_sum : Number(expense_sum).toFixed(2), + expense_currency : expense_currency, + expense_info : expense_info, + expense_payer : expense_payer, + expense_members : expense_members, + }) + storageItem.setExpense(project_name_table, id_unixtime_created.toString(), date_time.toString(), expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members) + } else { //modeEdit === "edit" + for (var j = 0; j < listModel_activeProjectExpenses.count ; j++) { + if (id_unixtime_created === Number(listModel_activeProjectExpenses.get(j).id_unixtime_created)) { + listModel_activeProjectExpenses.set(j, { + id_unixtime_created : Number(id_unixtime_created).toFixed(0), + date_time : Number(date_time).toFixed(0), + expense_name : expense_name, + expense_sum : Number(expense_sum).toFixed(2), + expense_currency : expense_currency, + expense_info : expense_info, + expense_payer : expense_payer, + expense_members : expense_members, + }) + } + } + storageItem.updateExpense(project_name_table, id_unixtime_created.toString(), date_time.toString(), expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members) + // if dates got changed: also sort expenses list + if (dateTimeManuallyChanged) { + listModel_activeProjectExpenses.quick_sort() + dateTimeManuallyChanged = false + } + } + + //remember recently used currency + recentlyUsedCurrency = expense_currency + storageItem.setSettings("recentlyUsedCurrency", recentlyUsedCurrency) + + // update allProject_Listmodel and DB for recent_beneficiaries and recent_payer in case of "new" entry + if (modeEdit === "new") { + for (var k = 0; k < listModel_allProjects.count ; k++) { + if ( Number(listModel_allProjects.get(k).project_id_timestamp) === Number(activeProjectID_unixtime) ) { + listModel_allProjects.set(k, { + "project_recent_payer_boolarray" : project_recent_payer_boolarray , + "project_recent_beneficiaries_boolarray" : project_recent_beneficiaries_boolarray, + }) + } + } + storageItem.updateField_Project(activeProjectID_unixtime, "project_recent_payer_boolarray", project_recent_payer_boolarray) + storageItem.updateField_Project(activeProjectID_unixtime, "project_recent_beneficiaries_boolarray", project_recent_beneficiaries_boolarray) + } + + // finally hide popup banner + hide() + } +} + diff --git a/qml/pages/BannerAddProject.qml b/qml/pages/BannerAddProject.qml new file mode 100644 index 0000000..868c68d --- /dev/null +++ b/qml/pages/BannerAddProject.qml @@ -0,0 +1,502 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +MouseArea { + id: popup + z: 10 + width: parent.width + height: parent.height + visible: opacity > 0 + opacity: 0.0 + onClicked: { + hide() + } + onOpacityChanged: { + //if needed + } + + // UI variables + property var hideBackColor : Theme.rgba(Theme.overlayBackgroundColor, 0.9) + property int amountBeneficiaries : listModel_activeProjectMembersTEMP.count + property string modeEdit : "new" + property real editItemIndex : -1 // -1=new, otherwise gives index of list + + property int tempProjectListIndex + property bool showTextfieldMemberName : false + property string timeStamp : ((new Date).getTime()).toString() // creates unix timestamp + + property string backupFilePath : "" + + // suppress blend to main window on this overlay, e.g. for context menu ... + // BUG: creates problems with _selectOrientation for context menus larger than 5 entries + property alias __silica_applicationwindow_instance: fakeApplicationWindow + Item { + id: fakeApplicationWindow + // suppresses warnings by context menu + property var _dimScreen + property var _undim + function _undim() {} + function _dimScreen() {} + } + Behavior on opacity { + FadeAnimator {} + } + ListModel { + id: listModel_activeProjectMembersTEMP + } + RemorsePopup { + z: 10 + id: remorse_deleteProject + } + + + + Rectangle { + anchors.fill: parent + color: hideBackColor + onColorChanged: opacity = 4 + + Rectangle { + id: idBackgroundRectProject + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: parent.width + height: parent.height - anchors.topMargin - Theme.paddingLarge + radius: Theme.paddingLarge + + SilicaFlickable { + anchors.fill: parent + contentHeight: addExpenseColumn.height + clip: true + + Column { + id: addExpenseColumn + width: parent.width + + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + Column { + id: idColumnAddProject + width: parent.width /3*2 - Theme.paddingLarge /2 + + Label { + width: parent.width + text: qsTr("Project") + } + Label { + width: parent.width + font.pixelSize: Theme.fontSizeTiny + text: (modeEdit === "new") ? (qsTr("create")) : (qsTr("edit")) + } + } + Item { + width: Theme.paddingLarge + height: 1 + } + Button { + id: idLabelHeaderAdd2 + enabled: (idTextfieldProjectname.length > 0) && (amountBeneficiaries > 0) + width: parent.width /3 - Theme.paddingLarge /2 + height: idColumnAddProject.height + text: (modeEdit === "new") ? qsTr("Add") : qsTr("Save") + onClicked: { + addProjectDB() + } + } + } + Row { + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + TextField { + id: idTextfieldProjectname + width: parent.width /3 * 2 - Theme.paddingLarge + acceptableInput: text.length < 255 + font.pixelSize: Theme.fontSizeMedium + EnterKey.onClicked: { + focus = false + } + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("name") + } + } + Item { + width: Theme.paddingLarge + height: 1 + } + TextField { + id: idTextfieldCurrencyProject + width: parent.width /3 + textLeftMargin: 0 + horizontalAlignment: TextInput.AlignRight + acceptableInput: text.length > 0 + EnterKey.enabled: text.length >= 0 + EnterKey.onClicked: { + focus = false + } + onFocusChanged: { + if (text.length === 0) { + text = recentlyUsedCurrency + } + if (focus) { + selectAll() + } + } + + Label { + anchors.right: parent.right + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("base currency") + } + } + } + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + + Label { + x: Theme.paddingLarge + width: parent.width/ 3*2 - Theme.paddingLarge / 2 + height: idAddMemberButton.height + verticalAlignment: Text.AlignVCenter + font.pixelSize: Theme.fontSizeMedium + text: qsTr("Members") + } + Item { + width: Theme.paddingLarge + height: 1 + } + Item { + id: idAddMemberButton + width: parent.width / 3 - Theme.paddingLarge/2 + height: Theme.iconSizeMedium + + Icon { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: parent.height + width: height + source: !showTextfieldMemberName ? "image://theme/icon-m-add?" : "image://theme/icon-m-clear?" + } + MouseArea { + anchors.fill: parent + onClicked: { + showTextfieldMemberName ? showTextfieldMemberName=false : showTextfieldMemberName=true + if (showTextfieldMemberName) { + // clear field and set for new member + editItemIndex = -1 // -1=add new member + idTextfieldAddMember.text = "" + idTextfieldAddMember.forceActiveFocus() + } else { + idTextfieldAddMember.text = "" + idTextfieldAddMember.focus = false + } + } + } + } + } + Column { + visible: !showTextfieldMemberName + width: parent.width + + Repeater { + model: listModel_activeProjectMembersTEMP + delegate: ListItem { + contentHeight: Theme.itemSizeExtraSmall + menu: ContextMenu { + MenuItem { + text: qsTr("rename") + onClicked: { + showTextfieldMemberName ? showTextfieldMemberName=false : showTextfieldMemberName=true + editItemIndex = index + idTextfieldAddMember.text = member_name + idTextfieldAddMember.forceActiveFocus() + } + } + MenuItem { + text: qsTr("remove") + onClicked: { + listModel_activeProjectMembersTEMP.remove(index) + } + } + } + + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + height: parent.height + + Label { + width: parent.width + height: parent.height + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + text: member_name + } + } + } + } + } + Row { + width: parent.width + visible: showTextfieldMemberName + topPadding: Theme.paddingMedium + + TextField { + id: idTextfieldAddMember + width: parent.width // 3*2 - Theme.paddingLarge + acceptableInput: text.length < 255 + font.pixelSize: Theme.fontSizeSmall + onActiveFocusChanged: { + if (!focus) { + showTextfieldMemberName = false + idTextfieldAddMember.text = "" + } + } + EnterKey.onClicked: { + if (text.length > 0) { + if (editItemIndex === -1) { // -1=add new member + listModel_activeProjectMembersTEMP.append({ member_name : idTextfieldAddMember.text, + member_isBeneficiary : true, + member_isPayer : false, + }) + } else { // "edit" existing member name by index in listmodel + listModel_activeProjectMembersTEMP.setProperty(editItemIndex, "member_name", idTextfieldAddMember.text) + } + } + focus = false + showTextfieldMemberName = false + } + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + text: qsTr("name") + } + } + } + Item { + width: parent.width + height: Theme.itemSizeSmall + } + Row { + width: parent.width + visible: (modeEdit === "edit") && (listModel_allProjects.count > 0) // make sure there is always one project left once created + spacing: Theme.paddingLarge + leftPadding: Theme.paddingLarge + + Button { + id: idLabelDeleteProject + width: parent.width /3 - parent.spacing * 1.5 + height: idColumnAddProject.height + color: Theme.errorColor + text: (Number(activeProjectID_unixtime) != Number(timeStamp)) ? qsTr("Delete") : qsTr("Reset") + onClicked: { + if (Number(activeProjectID_unixtime) != Number(timeStamp)) { // if it is not the active project, delete it + remorse_deleteProject.execute(qsTr("Delete this project?"), function() { + deleteProject() + }) + } else { // if active project + remorse_deleteProject.execute(qsTr("Clear all transactions?"), function() { + clearProject() + }) + } + } + } + Button { + id: idLabelBackupProject + width: parent.width /3 - parent.spacing * 1.5 + height: idColumnAddProject.height + text: qsTr("Backup") + onClicked: { + // hide() // ToDo: maybe close this popup? + pageStack.push(idFolderPickerPage) + } + } + Button { + id: idLabelRestoreProject + width: parent.width /3 - parent.spacing + height: idColumnAddProject.height + text: qsTr("Restore") + onClicked: { + // hide() // ToDo: maybe close this popup? + pageStack.push(idFilePickerPage) + } + } + } + Item { + width: parent.width + height: Theme.itemSizeSmall / 2 + } + } + } + } + } + Icon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: idBackgroundRectProject.anchors.topMargin / 2 - height/2 + source: "image://theme/icon-splus-cancel?" + opacity: 1 + } + + + function notify( color, upperMargin, modeEditNew, indexCurrentProject ) { + // color settings + if (color && (typeof(color) != "undefined")) { + idBackgroundRectProject.color = color + } + else { + idBackgroundRectProject.color = Theme.rgba(Theme.highlightBackgroundColor, 0.9) + } + + // position settings + if (upperMargin && (typeof(upperMargin) != "undefined")) { + idBackgroundRectProject.anchors.topMargin = upperMargin + } + else { + idBackgroundRectProject.anchors.topMargin = 0 + } + + // adjust input fields + tempProjectListIndex = indexCurrentProject + listModel_activeProjectMembersTEMP.clear() + if (modeEditNew === "new") { + modeEdit = "new" + timeStamp = ((new Date).getTime()).toString() + idTextfieldCurrencyProject.text = recentlyUsedCurrency + idTextfieldProjectname.text = "" + idTextfieldProjectname.forceActiveFocus() + } else { // edit project mode + modeEdit = "edit" + var tempProjectMembersArray = [] + tempProjectMembersArray = (listModel_allProjects.get(idComboboxProject.currentIndex).project_members).split(" ||| ") + for (var i = 0; i < tempProjectMembersArray.length ; i++) { + listModel_activeProjectMembersTEMP.append({ member_name : tempProjectMembersArray[i], + }) + } + timeStamp = (listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp).toString() + idTextfieldProjectname.text = listModel_allProjects.get(idComboboxProject.currentIndex).project_name + idTextfieldCurrencyProject.text = listModel_allProjects.get(idComboboxProject.currentIndex).project_base_currency + } + + // all members are beneficiaries + + + // show banner overlay + popup.opacity = 1.0 + } + + function hide() { + idTextfieldProjectname.focus = false + idTextfieldCurrencyProject.focus = false + + // clear all fields + idTextfieldProjectname.text = "" + idTextfieldCurrencyProject.text = recentlyUsedCurrency + + // make invisible + popup.opacity = 0.0 + } + + function addProjectDB () { + var project_id_timestamp = timeStamp + var project_name = idTextfieldProjectname.text + var project_members = "" + var project_recent_payer_boolarray = "" + var project_recent_beneficiaries_boolarray = "" + var project_base_currency = idTextfieldCurrencyProject.text + + for (var i = 0; i < listModel_activeProjectMembersTEMP.count ; i++) { + project_members += " ||| " + listModel_activeProjectMembersTEMP.get(i).member_name + project_recent_beneficiaries_boolarray += " ||| " + "true" + // recent_payer can only be one person, initially this will be the first entry of the list + if (i===0) { + project_recent_payer_boolarray += " ||| " + "true" + } else { + project_recent_payer_boolarray += " ||| " + "false" + } + } + // remove first occurance of " ||| " to later be able to split that string + project_members = project_members.replace(" ||| ", "") + project_recent_payer_boolarray = project_recent_payer_boolarray.replace(" ||| ", "") + project_recent_beneficiaries_boolarray = project_recent_beneficiaries_boolarray.replace(" ||| ", "") + + if (modeEdit === "new") { + // store in DB and list for new project + storageItem.setProject( project_id_timestamp, project_name, project_members, project_recent_payer_boolarray, project_recent_beneficiaries_boolarray, project_base_currency ) + listModel_allProjects.append({ project_id_timestamp : Number(project_id_timestamp), + project_name : project_name, + project_members : project_members, + project_recent_payer_boolarray : project_recent_payer_boolarray, + project_recent_beneficiaries_boolarray : project_recent_beneficiaries_boolarray, + project_base_currency : project_base_currency, + }) + + // if this is the first project, auto set it as active project + if (listModel_allProjects.count === 1) { // auto-sets as currently active project, if this project is very first one + storageItem.setSettings("activeProjectID_unixtime", Number(project_id_timestamp) ) + activeProjectID_unixtime = Number(project_id_timestamp) + loadActiveProjectInfos_FromDB( Number(project_id_timestamp) ) + //console.log("auto set as active project ID = " + activeProjectID_unixtime) + } + + } else { // modeEdit === "edit + // update DB and list for existing project + storageItem.updateProject( project_id_timestamp, project_name, project_members, project_recent_payer_boolarray, project_recent_beneficiaries_boolarray, project_base_currency ) + for (var j = 0; j < listModel_allProjects.count ; j++) { + if (listModel_allProjects.get(j).project_id_timestamp === Number(project_id_timestamp)) { + //console.log("updated entry at: id_" + project_id_timestamp) + listModel_allProjects.set(j, { "project_name" : project_name, + "project_members" : project_members, + "project_recent_payer_boolarray" : project_recent_payer_boolarray, + "project_recent_beneficiaries_boolarray" : project_recent_beneficiaries_boolarray, + "project_base_currency" : project_base_currency + }) + } + } + } + updateEvenWhenCanceled = true + hide() + } + + function deleteProject() { + updateEvenWhenCanceled = true + storageItem.deleteProject(timeStamp) + listModel_allProjects.remove(tempProjectListIndex) + // set active project to reasonable one + if (idComboboxProject.currentIndex != 0) { + idComboboxProject.currentIndex = idComboboxProject.currentIndex -1 + } + //console.log("auto set after deleting ID = " + Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) + loadActiveProjectInfos_FromDB(Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) // needed to make sure there is no expense or member list still active + hide() + } + + function clearProject() { + updateEvenWhenCanceled = true + listModel_activeProjectExpenses.clear() + storageItem.removeFullTable( "table_" + activeProjectID_unixtime.toString() ) + loadActiveProjectInfos_FromDB(Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) // needed to make sure there is no expense or member list still active + hide() + } + +} + diff --git a/qml/pages/CalcPage.qml b/qml/pages/CalcPage.qml new file mode 100644 index 0000000..9ab11f8 --- /dev/null +++ b/qml/pages/CalcPage.qml @@ -0,0 +1,556 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +Page { + id: pageResults + allowedOrientations: Orientation.All + onVisibleChanged: { + if (visible === true) { // entered page + listModel_activeProjectResults.clear() + listModel_activeProjectResultsSettlement.clear() + generateExchangeRateListFromExpenses() + addExchangeRates_listmodelActiveProjectExpenses() + } + } + + property real totalSpendingAmount_baseCurrency : 0 + property int counterShownExchangeRates : 0 + property string toShareString : "" + + ListModel { + id: listModel_activeProjectResultsSettlement + } + SilicaFlickable{ + id: listView + anchors.fill: parent + contentHeight: resultsColumn.height // tell overall height + + PullDownMenu { + MenuItem { + text: qsTr("Share detailed") + onClicked: { + createShareString("detailed") + } + } + MenuItem { + text: qsTr("Share compact") + onClicked: { + createShareString("compact") + } + } + } + + Column { + id: resultsColumn + x: Theme.paddingLarge + width: parent.width - 2*x + + Column { + width: parent.width + topPadding: Theme.paddingLarge + + Label { + width: parent.width + horizontalAlignment: Text.AlignRight + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + text: qsTr("Results") + } + Label { + width: parent.width + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + text: activeProjectName + " [" + activeProjectCurrency + "]" + } + } + Item { + width: parent.width + height: Theme.paddingLarge + Theme.paddingSmall + } + Label { + visible: listModel_exchangeRates.count > 1 // main exchange rate is also counted in + width: parent.width + bottomPadding: Theme.paddingMedium + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + text: qsTr("EXCHANGE RATES") + } + Repeater { + id: idRepeaterExchangeRates + width: parent.width + model: listModel_exchangeRates + delegate: Column { + id: idColumnContent + visible: expense_currency !== activeProjectCurrency + width: parent.width + + Row { + width: parent.width + + Label { + width: parent.width / 3 + height: parent.height + topPadding: Theme.paddingSmall + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.WordWrap + text: (exchangeRateMode === 1) + ? (new Date(Number(date_time)).toLocaleString(Qt.locale(), "dd.MM.yy" + " - " + "hh:mm")) //"ddd dd.MM.yyyy - hh:mm" + : (qsTr("constant")) + } + Label { + width: parent.width / 3 + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.WordWrap + topPadding: Theme.paddingSmall + horizontalAlignment: Text.AlignRight + text: Number(1).toFixed(2) + " " + "" +expense_currency + "" + " = " + } + TextField { + id: idTextfieldExchangeRate + width: parent.width / 3 + textRightMargin: idLabelProjectCurrency.width + textLeftMargin: 0 + inputMethodHints: Qt.ImhFormattedNumbersOnly //use "Qt.ImhDigitsOnly" for INT + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + text: Number(exchange_rate) //.toFixed(decimalPlacesCurrencyRate) + EnterKey.onClicked: { + focus = false + } + onFocusChanged: { + if (text.length < 1) { + text = Number(exchange_rate) //.toFixed(decimalPlacesCurrencyRate) + } else { + text = text.replace(",", ".") + text = Number(text) //.toFixed(decimalPlacesCurrencyRate) + } + if (focus) { + selectAll() + } else { // unfocus + exchange_rate = Number(text) + storeExchangeRate_DB(expense_currency, exchange_rate) + addExchangeRates_listmodelActiveProjectExpenses() + } + } + Label { + id: idLabelProjectCurrency + anchors.left: parent.right + font.pixelSize: Theme.fontSizeSmall + //color: Theme.highlightColor + color: Theme.secondaryColor + text: "" + base_currency + "" + } + } + } + } + + } + Item { + width: parent.width + height: Theme.paddingLarge + } + + Label { + width: parent.width + bottomPadding: Theme.paddingMedium + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + text: qsTr("SPENDING OVERVIEW") + } + Row { + visible: listModel_activeProjectResults.count > 0 + width: parent.width + + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.italic: true + font.pixelSize: Theme.fontSizeSmall + text: qsTr("name") + } + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.italic: true + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: qsTr("payments") + } + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.italic: true + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: qsTr("benefits") + } + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.italic: true + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: qsTr("saldo") + } + } + Repeater { + id: idRepeaterExpensesOverview + model: listModel_activeProjectResults + delegate: Row { + width: parent.width + + Label { + width: parent.width / 4 + font.pixelSize: Theme.fontSizeSmall + text: beneficiary_name + } + Label { + width: parent.width / 4 + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + text: expense_sum.toFixed(2) + } + Label { + width: parent.width / 4 + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + text: beneficiary_sum.toFixed(2) + } + Label { + width: parent.width / 4 + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + color: (( Number(expense_sum) - Number(beneficiary_sum) ).toFixed(2) < 0) ? Theme.errorColor : "green" + text: ( Number(expense_sum) - Number(beneficiary_sum) ).toFixed(2) + } + } + } + Row { + visible: listModel_activeProjectResults.count > 0 + width: parent.width + topPadding: Theme.paddingMedium + + Label { + width: parent.width / 4 * 2 + color: Theme.secondaryColor + font.bold: true + font.overline: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: totalSpendingAmount_baseCurrency.toFixed(2) + //text: activeProjectCurrency + " " + totalSpendingAmount_baseCurrency.toFixed(2) + } + Item { + width: parent.width / 4 + height: 1 + } + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.bold: true + font.overline: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: activeProjectCurrency + } + } + Item { + width: parent.width + height: Theme.paddingLarge * 2.5 + } + + Label { + width: parent.width + bottomPadding: Theme.paddingMedium + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + text: qsTr("SETTLEMENT SUGGESTION") + } + Repeater { + id: idRepeaterSettlementSuggestions + visible: model.count > 0 + model: listModel_activeProjectResultsSettlement + delegate: Row { + width: parent.width + + Label { + width: parent.width + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.WordWrap + text: (settling_sum.toFixed(2) >= 0) ? + ("" + from_name + " " + qsTr("owes") + " " + to_name + " " + qsTr("the sum of") + " " + settling_sum.toFixed(2) + " " + activeProjectCurrency + ".") + : ("" + to_name + " " + qsTr("owes") + " " + from_name + " " + qsTr("the sum of") + " " + (-settling_sum).toFixed(2) + " " + activeProjectCurrency + ".") + + } + } + } + Item { + width: parent.width + height: Theme.itemSizeSmall + } + } + } + + + function generateExchangeRateListFromExpenses() { + listModel_exchangeRates.clear() + + for (var i = 0; i < listModel_activeProjectExpenses.count; i++) { + if (exchangeRateMode === 0 ) { // collective currency + // check if available and where occuring + var currencyAlreadySet = false + var addCurrencyIndex = 0 + for (var j = 0; j < listModel_exchangeRates.count; j++) { + if ( listModel_activeProjectExpenses.get(i).expense_currency === listModel_exchangeRates.get(j).expense_currency ) { + currencyAlreadySet = true + addCurrencyIndex = j + } + } + // if it has not been set yet, add to exchange rate list + if (currencyAlreadySet === false) { + var tempStoredExchangeRateDB = Number(storageItem.getExchangeRate(listModel_activeProjectExpenses.get(i).expense_currency, 1)) + listModel_exchangeRates.append({ expense_currency : listModel_activeProjectExpenses.get(i).expense_currency, + base_currency : activeProjectCurrency, + exchange_rate : tempStoredExchangeRateDB, + date_time : Number(0) + }) + } + } else { // exchangeRateMode === 1 ... individual transactions + var tempExpenseCurrency = listModel_activeProjectExpenses.get(i).expense_currency + if (tempExpenseCurrency !== activeProjectCurrency) { + tempStoredExchangeRateDB = Number(storageItem.getExchangeRate(listModel_activeProjectExpenses.get(i).expense_currency, 1)) + listModel_exchangeRates.append({ expense_currency : listModel_activeProjectExpenses.get(i).expense_currency, + base_currency : activeProjectCurrency, + exchange_rate : tempStoredExchangeRateDB, + date_time : Number(listModel_activeProjectExpenses.get(i).date_time) + }) + } + } + } + } + + function storeExchangeRate_DB (exchange_rate_currency, exchange_rate_value) { + var tempOccurences = Number(storageItem.countExchangeRateOccurances(exchange_rate_currency, 0)) + if (tempOccurences === 0) { // add new entry + storageItem.setExchangeRate(exchange_rate_currency, exchange_rate_value) + } else { // update existing entry + storageItem.updateExchangeRate(exchange_rate_currency, exchange_rate_value) + } + } + + function addExchangeRates_listmodelActiveProjectExpenses() { + for (var i = 0; i < listModel_activeProjectExpenses.count; i++) { + var tempExpenseCurrency = listModel_activeProjectExpenses.get(i).expense_currency + var tempExpenseDateTime = Number(listModel_activeProjectExpenses.get(i).date_time) + + if (tempExpenseCurrency === activeProjectCurrency) { // paid in project specific currency + listModel_activeProjectExpenses.set(i, {"exchange_rate": Number(1)}) + } else { // if paid in different currency + + if ( exchangeRateMode === 0 ) { // ... collective currency + for (var k = 0; k < listModel_exchangeRates.count; k++) { + if (listModel_exchangeRates.get(k).expense_currency === tempExpenseCurrency) { + listModel_activeProjectExpenses.set(i, {"exchange_rate": Number(listModel_exchangeRates.get(k).exchange_rate)}) + //console.log(Number(listModel_exchangeRates.get(k).exchange_rate)) + } + } + } else { // ... individual transactions + for (k = 0; k < listModel_exchangeRates.count; k++) { + if ((listModel_exchangeRates.get(k).expense_currency === tempExpenseCurrency) && (Number(listModel_exchangeRates.get(k).date_time) === tempExpenseDateTime)) { + listModel_activeProjectExpenses.set(i, {"exchange_rate": Number(listModel_exchangeRates.get(k).exchange_rate)}) + } + } + } + } + } + calculateResults_members() + } + + function calculateResults_members () { + listModel_activeProjectResults.clear() + listModel_activeProjectResultsSettlement.clear() + totalSpendingAmount_baseCurrency = 0 + + // sum up benefits per member + for (var i = 0; i < listModel_activeProjectExpenses.count; i++) { + var tempBeneficiariesArray = (listModel_activeProjectExpenses.get(i).expense_members).split(" ||| ") + for (var j = 0; j < tempBeneficiariesArray.length; j++) { + var tempExpense_inBaseCurrency_perBeneficiariy = ((Number(listModel_activeProjectExpenses.get(i).expense_sum) / tempBeneficiariesArray.length) * Number(listModel_activeProjectExpenses.get(i).exchange_rate)) + + // cycle through results list, check if beneficiary is already available and where + var tempBeneficiaryAvailable = false + var tempBeneficiaryIndex = 0 + for (var k = 0; k < listModel_activeProjectResults.count; k++ ) { + if (listModel_activeProjectResults.get(k).beneficiary_name === tempBeneficiariesArray[j]) { + tempBeneficiaryAvailable = true + tempBeneficiaryIndex = k + } + } + // if not available, add him to results list (only on first occurance) + if (tempBeneficiaryAvailable === false) { + //console.log("first entry for: " + tempBeneficiariesArray[j]) + //console.log(tempExpense_inBaseCurrency_perBeneficiariy + " " + activeProjectCurrency) + listModel_activeProjectResults.append({ beneficiary_name : tempBeneficiariesArray[j], + beneficiary_sum : Number(tempExpense_inBaseCurrency_perBeneficiariy), + base_currency : activeProjectCurrency, + expense_sum : 0 // this info gets added later + }) + } else { // otherwise add his share to the existing list + var tempBeneficiarySum = Number(listModel_activeProjectResults.get(tempBeneficiaryIndex).beneficiary_sum) + Number(tempExpense_inBaseCurrency_perBeneficiariy) + listModel_activeProjectResults.set(tempBeneficiaryIndex, { "beneficiary_sum": Number(tempBeneficiarySum) }) + //console.log("latest benefitSum for " + tempBeneficiariesArray[j] + " is: " + tempBeneficiarySum) + } + //console.log(tempBeneficiariesArray[j]) + //console.log(tempExpense_inBaseCurrency_perBeneficiariy) + } + } + + // once listModel_activeProjectResults is created, go over expenses again and add payments + for ( i = 0; i < listModel_activeProjectExpenses.count; i++) { + var tempExpensePayer = listModel_activeProjectExpenses.get(i).expense_payer + var tempExpense_inBaseCurrency = Number(listModel_activeProjectExpenses.get(i).expense_sum) * Number(listModel_activeProjectExpenses.get(i).exchange_rate) + totalSpendingAmount_baseCurrency += tempExpense_inBaseCurrency + //console.log(tempExpensePayer) + //console.log(tempExpense_inBaseCurrency) + + // check if payer is already in results list as benefitter + var tempPayerAvailable = false + var tempPayerIndex = 0 + for (var l = 0; l < listModel_activeProjectResults.count; l++ ) { + if (listModel_activeProjectResults.get(l).beneficiary_name === tempExpensePayer) { + tempPayerAvailable = true + tempPayerIndex = l + } + } + + // if not available, add him to results list (only on first occurance) + if (tempPayerAvailable === false) { + //console.log("first entry for payer: " + tempExpensePayer + " = " + tempExpense_inBaseCurrency + " " + activeProjectCurrency) + //console.log("seems he paid but never benefits from anything") + listModel_activeProjectResults.append({ beneficiary_name : tempExpensePayer, + beneficiary_sum : 0, + base_currency : activeProjectCurrency, + expense_sum : Number(tempExpense_inBaseCurrency), + }) + } else { // otherwise add his share to the existing list + var tempPayerSum = Number(listModel_activeProjectResults.get(tempPayerIndex).expense_sum) + Number(tempExpense_inBaseCurrency) + listModel_activeProjectResults.set(tempPayerIndex, { "expense_sum": Number(tempPayerSum) }) + //console.log("latest payerSum for " + tempExpensePayer + " is: " + tempPayerSum) + } + } + + // sort results list according + listModel_activeProjectResults.quick_sort("desc") // payer with highest expense on top + + + // apply a (n-1) algorithm to settle expenses (how much each person ows to whom) + var outstandingNamesArray = [] + var outstandingSumsArray = [] + var totalSum = 0 + // iter backwards to get smallest amounts first, since listModel_activeProjectResults starts with highest expenses + for (var m = listModel_activeProjectResults.count-1; m >= 0; m--) { + outstandingNamesArray.push(listModel_activeProjectResults.get(m).beneficiary_name) + outstandingSumsArray.push( Number(listModel_activeProjectResults.get(m).expense_sum) - Number(listModel_activeProjectResults.get(m).beneficiary_sum) ) + totalSum += ( Number(listModel_activeProjectResults.get(m).expense_sum) - Number(listModel_activeProjectResults.get(m).beneficiary_sum) ) + } + //console.log(outstandingNamesArray) + //console.log(outstandingSumsArray) + //console.log(totalSum) + + function splitPayments() { + const mean = totalSum / outstandingNamesArray.length + var sortedValuesPaid = [] + for (var n = 0; n < outstandingSumsArray.length; n++) { + sortedValuesPaid.push(outstandingSumsArray[n] - mean) + } + var i = 0 + var j = outstandingNamesArray.length - 1 + var debt + while (i < j) { + debt = Math.min(-(sortedValuesPaid[i]), sortedValuesPaid[j]) + sortedValuesPaid[i] += debt + sortedValuesPaid[j] -= debt + listModel_activeProjectResultsSettlement.append({ from_name : outstandingNamesArray[i], + to_name : outstandingNamesArray[j], + settling_sum : Number(debt), + }) + //console.log(outstandingNamesArray[i] + " owes " + outstandingNamesArray[j] + " the sum of " + debt ) + if (sortedValuesPaid[i] === 0) { i++ } + if (sortedValuesPaid[j] === 0) { j-- } + } + } + splitPayments() + } + + function createShareString (detailGrade) { + + // create a shareable string + toShareString = "\n" + qsTr("Project:") + " " + activeProjectName + + "\n" + qsTr("Total expenses") + " = " + + totalSpendingAmount_baseCurrency.toFixed(2) + " " + activeProjectCurrency + + "\n" + "\n" + for (var i = 0; i < listModel_activeProjectResults.count; i++) { + toShareString += listModel_activeProjectResults.get(i).beneficiary_name + ":" + + "\n" + qsTr("payed") + " " + listModel_activeProjectResults.get(i).expense_sum.toFixed(2) + " " + activeProjectCurrency + + "\n" + qsTr("received") + " " + listModel_activeProjectResults.get(i).beneficiary_sum.toFixed(2) + " " + activeProjectCurrency + + "\n" + qsTr("saldo") + " " + (Number(listModel_activeProjectResults.get(i).expense_sum) - Number(listModel_activeProjectResults.get(i).beneficiary_sum)).toFixed(2) + " " + activeProjectCurrency + + "\n" + "\n" + } + toShareString += qsTr("Settlement suggestion:") + + "\n" + for (i = 0; i < listModel_activeProjectResultsSettlement.count; i++) { + if ( Number(listModel_activeProjectResultsSettlement.get(i).settling_sum).toFixed(2) >= 0 ) { + toShareString += listModel_activeProjectResultsSettlement.get(i).from_name + + " " + qsTr("owes") + " " + + listModel_activeProjectResultsSettlement.get(i).to_name + + " " + qsTr("the sum of") + " " + Number(listModel_activeProjectResultsSettlement.get(i).settling_sum).toFixed(2) + " " + activeProjectCurrency + + "." + "\n" + } else { // amount < 0 + toShareString += listModel_activeProjectResultsSettlement.get(i).to_name + + " " + qsTr("owes") + " " + + listModel_activeProjectResultsSettlement.get(i).from_name + + " " + qsTr("the sum of") + " " + (-1*Number(listModel_activeProjectResultsSettlement.get(i).settling_sum).toFixed(2)) + " " + activeProjectCurrency + + "." + "\n" + } + } + //console.log(toShareString) + + + // add details if necessary + if (detailGrade === "detailed") { + toShareString += "\n" + "\n" + "\n" + + qsTr("Detailed Spendings:") + + "\n" + "\n" + for ( i = 0; i < listModel_activeProjectExpenses.count; i++) { + if (sortOrderExpenses === 0) { // 0=descending, 1=ascending + var tmpEntryNumber = (listModel_activeProjectExpenses.count - i) + } else { + tmpEntryNumber = (i+1) + } + toShareString += qsTr("Expense #") + tmpEntryNumber + + "\n" + qsTr("date:") + " " + new Date(Number(listModel_activeProjectExpenses.get(i).date_time)).toLocaleString(Qt.locale(), "ddd dd.MM.yyyy - hh:mm") + + "\n" + qsTr("payer:") + " " + listModel_activeProjectExpenses.get(i).expense_payer + + "\n" + qsTr("item:") + " " + listModel_activeProjectExpenses.get(i).expense_name + + "\n" + qsTr("price:") + " " + Number(listModel_activeProjectExpenses.get(i).expense_sum).toFixed(2) + " " + listModel_activeProjectExpenses.get(i).expense_currency + + "\n" + qsTr("beneficiaries:") + " " + listModel_activeProjectExpenses.get(i).expense_members.split(" ||| ").join(", ") // .replace(" ||| ", ", ") + + "\n" + qsTr("info:") + " " + listModel_activeProjectExpenses.get(i).expense_info + + "\n" + "\n" + } + } + console.log(toShareString) + + // send this string + Clipboard.text = toShareString + shareActionText.mimeType = "text/plain" // "text/*" or "application/text" + shareActionText.resources = [{ + "data": toShareString, + "name": activeProjectName, + } ] + shareActionText.trigger() + } + +} diff --git a/qml/pages/FirstPage.qml b/qml/pages/FirstPage.qml new file mode 100644 index 0000000..5b6c63a --- /dev/null +++ b/qml/pages/FirstPage.qml @@ -0,0 +1,445 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Share 1.0 // ToDo: instead of copy to clipboard, send to app + +Page { + id: page + allowedOrientations: Orientation.All + + + // project specific global variables, loaded when activating a new project + property double activeProjectID_unixtime : Number(storageItem.getSettings("activeProjectID_unixtime", 0)) + property int activeProjectID_listIndex + property string activeProjectName + property string activeProjectCurrency : "EUR" + + + // program specific global variables + property int sortOrderExpenses : Number(storageItem.getSettings("sortOrderExpensesIndex", 0)) // 0=descending, 1=ascending + property int exchangeRateMode : Number(storageItem.getSettings("exchangeRateModeIndex", 0)) // 0=collective, 1=individual + property int interativeScrollbarMode : Number(storageItem.getSettings("interativeScrollbarMode", 0)) // 0=standard, 1=interactive + property string recentlyUsedCurrency : storageItem.getSettings("recentlyUsedCurrency", activeProjectCurrency) + + // navigation specific blocking + property bool updateEvenWhenCanceled : false + property bool delegateMenuOpen : false + + + // autostart + Component.onCompleted: { + generateAllProjectsList_FromDB() + loadActiveProjectInfos_FromDB(activeProjectID_unixtime) + } + + + + // other items, components and pages + ListModel { + id: listModel_allProjects + } + ListModel { + id: listModel_activeProjectMembers + } + ListModel { + id: listModel_activeProjectExpenses + property string sortColumnName: "date_time" //"id_unixtime_created" + function swap(a,b) { + if (ab) { + move(b,a,1); + move (a-1,b,1); + } + } + function partition(begin, end, pivot) { + var piv=get(pivot)[sortColumnName]; + swap(pivot, end-1); + var store=begin; + var ix; + for(ix=begin; ix piv) { + swap(store,ix); + ++store; + } + } + } + swap(end-1, store); + return store; + } + function qsort(begin, end) { + if(end-1>begin) { + var pivot=begin+Math.floor(Math.random()*(end-begin)); + + pivot=partition( begin, end, pivot); + + qsort(begin, pivot); + qsort(pivot+1, end); + } + } + function quick_sort() { + qsort(0,count) + } + + onCountChanged: { + quick_sort() + } + } + ListModel { + id: listModel_exchangeRates + } + ListModel { + id: listModel_activeProjectResults + property string sortColumnName : "expense_sum" + property string sortOrderResults : "desc" //"asc" + function swap(a,b) { + if (ab) { + move(b,a,1); + move (a-1,b,1); + } + } + function partition(begin, end, pivot) { + var piv=get(pivot)[sortColumnName]; + swap(pivot, end-1); + var store=begin; + var ix; + for(ix=begin; ix piv) { + swap(store,ix); + ++store; + } + } + } + swap(end-1, store); + return store; + } + function qsort(begin, end) { + if(end-1>begin) { + var pivot=begin+Math.floor(Math.random()*(end-begin)); + + pivot=partition( begin, end, pivot); + + qsort(begin, pivot); + qsort(pivot+1, end); + } + } + function quick_sort(orderDirection) { + sortOrderResults = orderDirection + qsort(0,count) + } + } + SettingsPage { + id: settingsPage + } + CalcPage { + id: calcPage + } + BannerAddExpense { + id: bannerAddExpense + } + Component { + id: datePickerComponent + DatePickerDialog {} + } + Component { + id: timePickerComponent + TimePickerDialog {} + } + ShareAction { + id: shareActionText + } + + + + // main page, current project + SilicaListView { + id: idSilicaListView + anchors.fill: parent + header: Row { + width: (interativeScrollbarMode === 0) ? (parent.width) : ((isPortrait) ? (parent.width) : (parent.width - Theme.paddingLarge*2)) + visible: activeProjectID_unixtime !== 0 + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + + Item { + id: idLeftSpacer + width: Theme.paddingSmall + Theme.paddingMedium + height: 1 + } + Column { + id: idHeaderInfoColumn + width: parent.width - idLeftSpacer.width + bottomPadding: Theme.paddingSmall + + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeLarge + color: Theme.highlightColor + wrapMode: Text.WordWrap + text: qsTr("Expenses") + } + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + wrapMode: Text.WordWrap + //text: "ID_" + activeProjectID_unixtime + " [" + activeProjectCurrency + "]" + text: activeProjectName + " [" + activeProjectCurrency + "]" + } + } + } + footer: Item { + width: parent.width + height: Theme.itemSizeSmall + } + spacing: Theme.paddingMedium + quickScroll: (interativeScrollbarMode === 0) ? (true) : (false) + + VerticalScrollDecorator { + enabled: (interativeScrollbarMode === 0) ? (true) : (false) + visible: enabled + } + ScrollBar { + id: idScrollBarDate + enabled: (interativeScrollbarMode === 0) ? false : true + labelVisible: true + topPadding: (isPortrait) ? (Theme.itemSizeLarge + Theme.paddingLarge) : (0) + bottomPadding: (isPortrait) ? Theme.itemSizeSmall : 0 + labelModelTag: "date_time" + visible: (interativeScrollbarMode === 0) ? (false) : (idPulldownMenu.active === false) && (delegateMenuOpen === false) + } + PullDownMenu { + id: idPulldownMenu + quickSelect: true + + MenuItem { + text: qsTr("Settings") + onClicked: pageStack.push(settingsPage) + } + MenuItem { + text: qsTr("Calculate") + enabled: activeProjectID_unixtime !== 0 + onClicked: pageStack.animatorPush(calcPage) + } + MenuItem { + text: qsTr("Add") + enabled: activeProjectID_unixtime !== 0 + onClicked: bannerAddExpense.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "new", activeProjectID_unixtime, 0 ) + } + } + ViewPlaceholder { + enabled: activeProjectID_unixtime === 0 // listModel_allProjects.count === 0 + text: qsTr("Create new project.") + hintText: qsTr("Nothing loaded yet.") + } + + model: listModel_activeProjectExpenses + delegate: ListItem { + id: idListItem + contentHeight: idListLabelsQML.height + contentWidth: (interativeScrollbarMode === 0) ? (parent.width) : (parent.width - idScrollBarDate.width) + //contentWidth: (idSilicaListView.visibleArea.heightRatio < 1.0 && idPulldownMenu.active === false) ? (parent.width - idScrollBarDate.width) : (parent.width) + menu: ContextMenu { + id: idContextMenu + + MenuItem { + text: qsTr("Edit") + onClicked: { + bannerAddExpense.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "edit", activeProjectID_unixtime, id_unixtime_created ) + } + } + MenuItem { + text: qsTr("Remove") + onClicked: { + idRemorseDelete.execute(idListItem, qsTr("Remove entry?"), function() { + storageItem.deleteExpense(activeProjectID_unixtime, id_unixtime_created ) + listModel_activeProjectExpenses.remove(index) + } ) + } + } + } + onMenuOpenChanged: { + // set variable to disable scrollBar visibility + if (menuOpen === true) { + delegateMenuOpen = true + } else { + delegateMenuOpen = false + } + } + + RemorseItem { + id: idRemorseDelete + } + Row { + width: parent.width + + Rectangle { + width: Theme.paddingSmall + height: idListLabelsQML.height + color: Theme.rgba(Theme.highlightBackgroundColor, 0.4) + } + Item { + width: Theme.paddingMedium + height: 1 + } + Column { + id: idListLabelsQML + width: parent.width - Theme.paddingSmall - 2*Theme.paddingMedium + + Row { + width: parent.width + + Label { + width: parent.width/3*2- Theme.paddingLarge/2 + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + text: new Date(Number(date_time)).toLocaleString(Qt.locale(), "ddd dd.MM.yyyy - hh:mm") + } + Item { + width: Theme.paddingLarge + height: 1 + } + Label { + width: parent.width/3 - Theme.paddingLarge/2 + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + text: expense_payer + } + } + Row { + width: parent.width + + Label { + width: parent.width/3*2 - Theme.paddingLarge/2 + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + text: expense_name + } + Item { + width: Theme.paddingLarge + height: 1 + } + Label { + width: parent.width/3 - Theme.paddingLarge/2 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: Number(expense_sum).toFixed(2) + " " + expense_currency.toString() + } + } + Label { + id: idLabelExpenseInfo + visible: idLabelExpenseInfo.text.length > 0 + width: parent.width + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeTiny + color: Theme.secondaryColor + text: expense_info + } + Label { + id: idLabelBeneficiaries + width: parent.width + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeTiny + color: Theme.secondaryColor + text: qsTr("group:") + " " + expense_members.split(" ||| ").join(", ") + } + Item { + width: parent.width + height: Theme.paddingSmall + } + } + } + } + } // end SilicaListView + + + + + function generateAllProjectsList_FromDB() { + listModel_allProjects.clear() + var allProjectsOverview = storageItem.getAllProjects("none") + //console.log(allProjectsOverview) + if (allProjectsOverview !== "none") { + for (var i = 0; i < allProjectsOverview.length ; i++) { + + listModel_allProjects.append({ + project_id_timestamp : Number(allProjectsOverview[i][0]), + project_name : allProjectsOverview[i][1], + project_members : allProjectsOverview[i][2], + project_recent_payer_boolarray : allProjectsOverview[i][3], + project_recent_beneficiaries_boolarray : allProjectsOverview[i][4], + project_base_currency : allProjectsOverview[i][5], + }) + } + } + } + function loadActiveProjectInfos_FromDB(activeProjectID_unixtime) { + //console.log( "loading project: " + Number(activeProjectID_unixtime) ) + listModel_activeProjectMembers.clear() + listModel_activeProjectExpenses.clear() + for (var j = 0; j < listModel_allProjects.count ; j++) { + //console.log("in listmodel: " + Number(listModel_allProjects.get(j).project_id_timestamp) ) + // only use active project infos + if ( Number(listModel_allProjects.get(j).project_id_timestamp) === Number(activeProjectID_unixtime) ) { + // find active project name and currency + activeProjectName = listModel_allProjects.get(j).project_name + activeProjectID_listIndex = j + activeProjectCurrency = listModel_allProjects.get(j).project_base_currency + + // generate active project members list + var activeProjectMembersArray = (listModel_allProjects.get(j).project_members).split(" ||| ") + var activeProjectRecentPayerBoolArray = (listModel_allProjects.get(j).project_recent_payer_boolarray).split(" ||| ") + var activeProjectRecentBeneficiariesBoolArray = (listModel_allProjects.get(j).project_recent_beneficiaries_boolarray).split(" ||| ") + for (var i = 0; i < activeProjectMembersArray.length ; i++) { + listModel_activeProjectMembers.append({ + member_name : activeProjectMembersArray[i], + member_isBeneficiary : activeProjectRecentBeneficiariesBoolArray[i], + member_isPayer : activeProjectRecentPayerBoolArray[i], + }) + } + + // generate active project expenses list + var currentProjectEntries = storageItem.getAllExpenses( activeProjectID_unixtime, "none") + if (currentProjectEntries !== "none") { + for (i = 0; i < currentProjectEntries.length ; i++) { + listModel_activeProjectExpenses.append({ + id_unixtime_created : Number(currentProjectEntries[i][0]).toFixed(0), + date_time : Number(currentProjectEntries[i][1]).toFixed(0), + expense_name : currentProjectEntries[i][2], + expense_sum : Number(currentProjectEntries[i][3]).toFixed(2), + expense_currency : currentProjectEntries[i][4], + expense_info : currentProjectEntries[i][5], + expense_payer : currentProjectEntries[i][6], + expense_members : currentProjectEntries[i][7], + }) + } + } + } + } + } + +} diff --git a/qml/pages/ScrollBar.qml b/qml/pages/ScrollBar.qml new file mode 100755 index 0000000..7645a89 --- /dev/null +++ b/qml/pages/ScrollBar.qml @@ -0,0 +1,106 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +Rectangle { + // inherited values + property var flickable: parent + property bool labelVisible : true + property string labelModelTag : "" + property real topPadding : 0 + property real bottomPadding : 0 + property color handleColor : Theme.highlightBackgroundColor + + // own values but overwritable + property int scrollToNumber : 0 + property string showLabel : "" + property int listcountMax : flickable.count // (flickable.count !== undefined) ? flickable.count : 0 // dirty bugfix for scrollBar wron position on empty lists + property real roundCornersRadius : Theme.paddingLarge + + id: scrollbar + anchors.top: parent.top + anchors.topMargin: topPadding + anchors.bottom: parent.bottom + anchors.bottomMargin: bottomPadding + anchors.right: parent.right + width: roundCornersRadius * 2 + radius: width / 2 + color: Theme.rgba(Theme.primaryColor, 0.075) + visible: flickable.visibleArea.heightRatio < 1.0 + + Rectangle { + id: handle + width: parent.width + height: Math.max(roundCornersRadius*2, flickable.visibleArea.heightRatio * scrollbar.height) + color: handleColor + opacity: (clicker.drag.active ) ? 1 : 0.4 + radius: width / 2 + } + Rectangle { + id: scrollLabelBackground + visible: labelVisible + anchors.verticalCenter: handle.verticalCenter + anchors.right: handle.left + anchors.rightMargin: Theme.paddingLarge * 1.5 + width: scrollLabel.width + height*1.2 + height: roundCornersRadius * 2 + radius: roundCornersRadius + color: handleColor + opacity: (clicker.drag.active ) ? 1 : 0 + + Label { + id: scrollLabel + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + //text: showLabel + text: new Date(Number(showLabel)).toLocaleString(Qt.locale(), "dd.MM.yyyy - hh:mm") + } + Rectangle { + anchors.left: parent.right + anchors.leftMargin: -roundCornersRadius + anchors.verticalCenter: parent.verticalCenter + width: parent.anchors.rightMargin + 2*roundCornersRadius + height: Theme.paddingSmall/3*2 + color: parent.color + } + } + Binding { + // jump handle to where the currently visible part of flickable ist + target: handle + property: "y" + value: (flickable.visibleArea.yPosition) * (scrollbar.height - ( handle.height - (flickable.visibleArea.heightRatio * scrollbar.height))) + when: !clicker.pressed + } + MouseArea { + id: clicker + anchors.fill: parent + anchors.rightMargin: -Theme.paddingSmall + anchors.leftMargin: -Theme.paddingSmall + preventStealing: true + drag { + target: handle + minimumY: 0 + maximumY: scrollbar.height - handle.height + axis: Drag.YAxis + } + onMouseYChanged: { + //flickable.contentY = handle.y / drag.maximumY * (flickable.contentHeight - flickable.height) + scrollToNumber = Math.ceil(handle.y / drag.maximumY * listcountMax) + if (scrollToNumber < 0) { + scrollToNumber = 0 + } else if (scrollToNumber >= listcountMax) { + scrollToNumber = listcountMax -1 // because index starts at 0, while count() starts at 1 + } + showLabel = (flickable.model.get(scrollToNumber)[labelModelTag] !== undefined) ? flickable.model.get(scrollToNumber)[labelModelTag] : "" //flickable.model.get(scrollToNumber)[labelModelTag] + flickable.positionViewAtIndex( scrollToNumber, ListView.Center) + //flickable.contentY = handle.y / drag.maximumY * (flickable.contentHeight - flickable.height) + } + onClicked: { + //flickable.contentY = mouse.y / scrollbar.height * (flickable.contentHeight - flickable.height) + scrollToNumber = Math.ceil(mouse.y / scrollbar.height * listcountMax) + showLabel = (flickable.model.get(scrollToNumber)[labelModelTag] !== undefined) ? flickable.model.get(scrollToNumber)[labelModelTag] : "" //flickable.model.get(scrollToNumber)[labelModelTag] + flickable.positionViewAtIndex( scrollToNumber, ListView.Center ) + } + //onReleased: console.log("released") + } +} diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml new file mode 100644 index 0000000..c655f0a --- /dev/null +++ b/qml/pages/SettingsPage.qml @@ -0,0 +1,456 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Pickers 1.0 +import FileIO 1.0 +import Nemo.Notifications 1.0 + + + +Dialog { + id: pageSettings + + property int showMaintenanceButtonsCounter : 0 + property string notificationString : "" + //property int oldProjectIndex + + allowedOrientations: Orientation.Portrait + backNavigation: (bannerAddProject.opacity < 1) // && (updateEvenWhenCanceled === false) + forwardNavigation: (bannerAddProject.opacity < 1) + onOpened: { + //console.log("old project index = " + activeProjectID_unixtime) + showMaintenanceButtonsCounter = 0 + updateEvenWhenCanceled = false + idComboboxProject.currentIndex = 0 + for (var j = 0; j < listModel_allProjects.count ; j++) { + if (Number(listModel_allProjects.get(j).project_id_timestamp) === Number(storageItem.getSettings("activeProjectID_unixtime", 0))) { + idComboboxProject.currentIndex = j + } + } + idComboboxSortingExpenses.currentIndex = Number(storageItem.getSettings("sortOrderExpensesIndex", 0)) // 0=descending, 1=ascending + idComboboxExchangeRateMode.currentIndex = Number(storageItem.getSettings("exchangeRateModeIndex", 0)) // 0=collective, 1=individual + idComboboxShowInteractiveScrollbar.currentIndex = Number(storageItem.getSettings("interativeScrollbarMode", 0)) //0=standard, 1=interactive + notificationString = "" + } + onDone: { + if (result == DialogResult.Accepted) { + writeDB_Settings() + } + } + onRejected: { + // in certain cases reload list even on cancel: if project was cleared, delted or created + // then use previous activeProjectID_unixtime instead of the new one from dropdown menu + if (updateEvenWhenCanceled === true) { + loadActiveProjectInfos_FromDB(activeProjectID_unixtime) + updateEvenWhenCanceled = false + } + } + + BannerAddProject { + id: bannerAddProject + } + Banner2ButtonsChoice { + id: banner2ButtonsChoice + } + Notification { + id: notificationBackup + expireTimeout: 4000 + //appName: qsTr("Expenditure") + //icon: "image://theme/icon-lock-warning" + + function showSmall(message) { + replacesId = 0 + previewSummary = "" + previewBody = message + publish() + } + function showBig(title, message) { + replacesId = 0 + previewSummary = title + previewBody = message + publish() + } + } + RemorsePopup { + z: 10 + id: remorse_clearExchangeRatesDB + } + RemorsePopup { + z: 10 + id: remorse_restoreBackupFile + } + ListModel { + id: listModel_tempProjectExpenses + } + Component { + id: idFolderPickerPage + + FolderPickerPage { + dialogTitle: qsTr("Backup to") + onSelectedPathChanged: { + backupProjectExpenses( selectedPath ) + } + } + } + Component { + id: idFilePickerPage + + FilePickerPage { + title: qsTr("Restore backup file") + nameFilters: [ '*.csv' ] + onSelectedContentPropertiesChanged: { + var selectedPath = selectedContentProperties.filePath + idTextFileBackup.source = selectedPath + var tempProjectIndex_File = ((idTextFileBackup.text).split("\n"))[0] + var tempProjectIndex = listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp + var headlineText = qsTr("Restore backup - choose action") + var otherText = qsTr("Replace deletes all former project-expenses and uses those from backup-file instead.") + " " + + qsTr("Merge keeps former project-expenses and adds those from backup-file which are not yet on the list.") + if (parseInt(tempProjectIndex) !== parseInt(tempProjectIndex_File)) { + var detailText = qsTr("File info: This backup was created by a different project.") + } else { + detailText = qsTr("File info: This backup was created by the original project.") + } + var choiceText_1 = qsTr("Replace") + var choiceText_2 = qsTr("Merge") + banner2ButtonsChoice.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, headlineText, detailText, otherText, choiceText_1, choiceText_2, selectedPath ) + } + } + } + TextFileIO { + id: idTextFileBackup + } + + + + SilicaFlickable{ + anchors.fill: parent + contentHeight: column.height // tell overall height + + Column { + id: column + width: pageSettings.width + + DialogHeader { + //title: qsTr("Settings") + } + Row { + width: parent.width + bottomPadding: Theme.paddingLarge + + Label { + id: idLabelSettingsHeader + width: parent.width / 6 * 5 + leftPadding: Theme.paddingLarge + font.pixelSize: Theme.fontSizeExtraLarge + color: Theme.highlightColor + text: qsTr("Settings") + + MouseArea { + anchors.fill: parent + onClicked: showMaintenanceButtonsCounter = showMaintenanceButtonsCounter + 1 + } + } + IconButton { + width: parent.width / 6 + anchors.verticalCenter: idLabelSettingsHeader.verticalCenter + icon.color: Theme.highlightColor + icon.scale: 1.1 + icon.source: "image://theme/icon-m-about?" + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("AboutPage.qml"), {}) + } + } + } + Row { + width: parent.width + + ComboBox { + id: idComboboxProject + width: (listModel_allProjects.count === 0) ? (parent.width / 6*5) : (parent.width / 6*4) + label: qsTr("Project") + menu: ContextMenu { + + Repeater { + enabled: listModel_allProjects.count > 0 + model: listModel_allProjects + + MenuItem { + text: project_name + } + } + } + + MouseArea { + enabled: listModel_allProjects.count === 0 + anchors.fill: parent + preventStealing: true + onClicked: bannerAddProject.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "new", idComboboxProject.currentIndex ) + } + } + IconButton { + visible: listModel_allProjects.count > 0 + width: parent.width / 6 + icon.source: "image://theme/icon-m-edit?" + onClicked: { + bannerAddProject.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "edit", idComboboxProject.currentIndex ) + } + } + IconButton { + width: parent.width / 6 + icon.source: "image://theme/icon-m-add?" + onClicked: { + bannerAddProject.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "new", idComboboxProject.currentIndex ) + } + } + } + ComboBox { + id: idComboboxSortingExpenses + width: parent.width + label: qsTr("Sorting") + + menu: ContextMenu { + MenuItem { + text: qsTr("descending") + } + MenuItem { + text: qsTr("ascending") + } + } + } + ComboBox { + id: idComboboxExchangeRateMode + width: parent.width + label: qsTr("Exchange rate") + menu: ContextMenu { + MenuItem { + text: qsTr("per currency (constant)") + } + MenuItem { + text: qsTr("per transaction (dates)") + } + } + } + ComboBox { + id: idComboboxShowInteractiveScrollbar + width: parent.width + label: qsTr("Scrollbar") + menu: ContextMenu { + MenuItem { + text: qsTr("normal") + } + MenuItem { + text: qsTr("interactive (beta)") + } + } + } + Column { + visible: showMaintenanceButtonsCounter > 9 + width: parent.width + topPadding: Theme.paddingLarge * 3 + spacing: Theme.paddingLarge + + Label { + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Theme.errorColor + text: qsTr("Database cleanup - requires restart:") + leftPadding: Theme.paddingLarge + Theme.paddingSmall + rightPadding: leftPadding + } + Button { + text: qsTr("settings") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + remorse_clearExchangeRatesDB.execute(qsTr("Delete stored settings?"), function() { + storageItem.removeFullTable( "settings_table" ) + }) + } + } + Button { + text: qsTr("exchange rates") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + remorse_clearExchangeRatesDB.execute(qsTr("Delete stored exchange rates?"), function() { + storageItem.removeFullTable( "exchange_rates_table" ) + }) + } + } + Button { + text: qsTr("projects") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + remorse_clearExchangeRatesDB.execute(qsTr("Delete stored projects?"), function() { + // remove all individual project expense tables + for (var j = 0; j < listModel_allProjects.count ; j++) { + var tempTablename = "table_" + listModel_allProjects.get(j).project_id_timestamp + storageItem.removeFullTable(tempTablename) + } + // remove all projects overview table + storageItem.removeFullTable( "projects_table" ) + // set latest setting_id to zero + activeProjectID_unixtime = 0 + storageItem.setSettings("activeProjectID_unixtime", activeProjectID_unixtime) + // update front page + updateEvenWhenCanceled = true + }) + } + } + } + Item { + width: parent.width + height: Theme.paddingLarge + } + } + } + + + + // ******************************************** important functions ******************************************** // + + function writeDB_Settings() { + storageItem.setSettings("sortOrderExpensesIndex", idComboboxSortingExpenses.currentIndex) + sortOrderExpenses = idComboboxSortingExpenses.currentIndex + listModel_activeProjectExpenses.quick_sort() + + storageItem.setSettings("exchangeRateModeIndex", idComboboxExchangeRateMode.currentIndex) + exchangeRateMode = idComboboxExchangeRateMode.currentIndex + + storageItem.setSettings("interativeScrollbarMode", idComboboxShowInteractiveScrollbar.currentIndex) + interativeScrollbarMode = idComboboxShowInteractiveScrollbar.currentIndex + + if (listModel_allProjects.count > 0) { // only works if a project is actually created and loaded, otherwise this gets triggered directly in BannerAddProject.qml + storageItem.setSettings("activeProjectID_unixtime", Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) + activeProjectID_unixtime = Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp) + loadActiveProjectInfos_FromDB(Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) + } + + // ToDo: update base currency, if it was changed + } + + function backupProjectExpenses(selectedPath) { + listModel_tempProjectExpenses.clear() + var tempProjectIndex = listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp + var backupFileName = encodeURIComponent(listModel_allProjects.get(idComboboxProject.currentIndex).project_name) + "_backup.csv" //replaces misleading special characters with %-symbols + var backupFilePath = selectedPath + "/" + backupFileName //StandardPaths.documents + + // check if project exists + var currentProjectEntries = storageItem.getAllExpenses(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp, "none") + if (currentProjectEntries !== "none") { + // generate temp project expenses list from chosen list + for (var i = 0; i < currentProjectEntries.length ; i++) { + listModel_tempProjectExpenses.append({ + id_unixtime_created : Number(currentProjectEntries[i][0]).toFixed(0), + date_time : Number(currentProjectEntries[i][1]).toFixed(0), + expense_name : currentProjectEntries[i][2], + expense_sum : Number(currentProjectEntries[i][3]).toFixed(2), + expense_currency : currentProjectEntries[i][4], + expense_info : currentProjectEntries[i][5], + expense_payer : currentProjectEntries[i][6], + expense_members : currentProjectEntries[i][7], + }) + } + // create string from these temp info + var toBackupString = tempProjectIndex + "\n" + + listModel_allProjects.get(idComboboxProject.currentIndex).project_name + "\n" + + "id_unixtime_created;*;date_time;*;expense_payer;*;expense_name;*;expense_sum;*;expense_currency;*;expense_members;*;expense_info" + "\n" + for (var j = 0; j < listModel_tempProjectExpenses.count; j++) { + toBackupString += listModel_tempProjectExpenses.get(j).id_unixtime_created + + ";*;" + listModel_tempProjectExpenses.get(j).date_time + + ";*;" + listModel_tempProjectExpenses.get(j).expense_payer + + ";*;" + listModel_tempProjectExpenses.get(j).expense_name + + ";*;" + listModel_tempProjectExpenses.get(j).expense_sum + + ";*;" + listModel_tempProjectExpenses.get(j).expense_currency + + ";*;" + listModel_tempProjectExpenses.get(j).expense_members + + ";*;" + listModel_tempProjectExpenses.get(j).expense_info + + "\n" + } + //console.log(toBackupString) + + // store in file and give notification + idTextFileBackup.source = backupFilePath + idTextFileBackup.text = toBackupString + notificationString = backupFilePath + + //show notification somehow on top + var headlineText = qsTr("Backup successful") + var detailText = qsTr("File saved to:") + backupFilePath + var triggerHiding = false + notificationBackup.showBig(headlineText, detailText) + } + } + + function restoreProjectExpenses(selectedPath, selectedAction) { + idTextFileBackup.source = selectedPath + var loadTextString = (idTextFileBackup.text) + var pos = loadTextString.lastIndexOf("\n") // ToDo: remove last occurance of "\n" + var loadTextLinesArray = (loadTextString.substring(0,pos) + loadTextString.substring(pos+1)).split("\n") + var tempStringExistingId_unixtime = "" + //var tempProjectIndex = listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp + + // plausibility check on lines 0 to 2 + if (loadTextLinesArray[2] === "id_unixtime_created;*;date_time;*;expense_payer;*;expense_name;*;expense_sum;*;expense_currency;*;expense_members;*;expense_info") { + var tempProjectIndex = listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp + + // if REPLACE: delete old entries in project-specific expense table in database, otherwise just MERGE + if (selectedAction === "replace") { + storageItem.deleteAllExpenses(tempProjectIndex) + } else { //"merge" + // generate expenses list for this project + var currentProjectEntries = storageItem.getAllExpenses( activeProjectID_unixtime, "none") + if (currentProjectEntries !== "none") { + for (var i = 0; i < currentProjectEntries.length ; i++) { + tempStringExistingId_unixtime += (currentProjectEntries[i][0]).toString() + ";" + } + } + //console.log(tempStringExistingId_unixtime) + } + + // fill with backup entries + for (i = 3; i < loadTextLinesArray.length; i++) { + var tempExpenseLineArray = (loadTextLinesArray[i]).split(";*;") + var project_name_table = tempProjectIndex + var id_unixtime_created = tempExpenseLineArray[0] + var date_time = tempExpenseLineArray[1] + var expense_payer = tempExpenseLineArray[2] + var expense_name = tempExpenseLineArray[3] + var expense_sum = tempExpenseLineArray[4] + var expense_currency = tempExpenseLineArray[5] + var expense_members = tempExpenseLineArray[6] + var expense_info = tempExpenseLineArray[7] + + if (selectedAction === "replace") { + storageItem.setExpense(project_name_table, id_unixtime_created.toString(), date_time.toString(), expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members) + //console.log("entering DB -> " + tempExpenseLineArray) + } else { + // check for double entries, only add if not existent + if (tempStringExistingId_unixtime.indexOf(id_unixtime_created.toString()) === -1) { + storageItem.setExpense(project_name_table, id_unixtime_created.toString(), date_time.toString(), expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members) + //console.log("entering DB -> " + tempExpenseLineArray) + } + } + } + + //show notification somehow on top + var headlineText = qsTr("Backup successfully restored.") + if (selectedAction === "replace") { + var detailText = qsTr("Project expenses have been overwritten by backup-file expenses.") + } else { + detailText = qsTr("Project expenses have been merged with backup-file expenses.") + } + var triggerHiding = false + notificationBackup.showBig(headlineText, detailText) + + // set this flag when the current project gets merged or replaced with a backup + if ( Number(tempProjectIndex) === Number(activeProjectID_unixtime) ) { + updateEvenWhenCanceled = true + } + } else { + //show notification somehow on top + headlineText = qsTr("Validity check failed.") + detailText = qsTr("This backup file does not seem to be created by Expenditure:") + " " + backupFilePath + triggerHiding = false + notificationBackup.showBig(headlineText, detailText) + } + } +} diff --git a/rpm/harbour-expenditure.changes.in b/rpm/harbour-expenditure.changes.in new file mode 100644 index 0000000..6eeddb9 --- /dev/null +++ b/rpm/harbour-expenditure.changes.in @@ -0,0 +1,18 @@ +# Rename this file as harbour-expenditure.changes to include changelog +# entries in your RPM file. +# +# Add new changelog entries following the format below. +# Add newest entries to the top of the list. +# Separate entries from eachother with a blank line. +# +# Alternatively, if your changelog is automatically generated (e.g. with +# the git-change-log command provided with Sailfish OS SDK), create a +# harbour-expenditure.changes.run script to let mb2 run the required commands for you. + +# * date Author's Name version-release +# - Summary of changes + +* Sun Apr 13 2014 Jack Tar 0.0.1-1 +- Scrubbed the deck +- Hoisted the sails + diff --git a/rpm/harbour-expenditure.changes.run.in b/rpm/harbour-expenditure.changes.run.in new file mode 100644 index 0000000..0d085c1 --- /dev/null +++ b/rpm/harbour-expenditure.changes.run.in @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Rename this file as harbour-expenditure.changes.run to let mb2 automatically +# generate changelog from well formatted Git commit messages and tag +# annotations. + +git-change-log + +# Here are some basic examples how to change from the default behavior. Run +# git-change-log --help inside the Sailfish OS SDK chroot or build engine to +# learn all the options git-change-log accepts. + +# Use a subset of tags +#git-change-log --tags refs/tags/my-prefix/* + +# Group entries by minor revision, suppress headlines for patch-level revisions +#git-change-log --dense '/[0-9]+.[0-9+$' + +# Trim very old changes +#git-change-log --since 2014-04-01 +#echo '[ Some changelog entries trimmed for brevity ]' + +# Use the subjects (first lines) of tag annotations when no entry would be +# included for a revision otherwise +#git-change-log --auto-add-annotations diff --git a/rpm/harbour-expenditure.spec b/rpm/harbour-expenditure.spec new file mode 100644 index 0000000..dde271b --- /dev/null +++ b/rpm/harbour-expenditure.spec @@ -0,0 +1,67 @@ +# +# Do NOT Edit the Auto-generated Part! +# Generated by: spectacle version 0.32 +# + +Name: harbour-expenditure + +# >> macros +# << macros + +Summary: Expenditure +Version: 0.2 +Release: 1 +Group: Qt/Qt +License: LICENSE +URL: http://example.org/ +Source0: %{name}-%{version}.tar.bz2 +Source100: harbour-expenditure.yaml +Requires: sailfishsilica-qt5 >= 0.10.9 +BuildRequires: pkgconfig(sailfishapp) >= 1.0.2 +BuildRequires: pkgconfig(Qt5Core) +BuildRequires: pkgconfig(Qt5Qml) +BuildRequires: pkgconfig(Qt5Quick) +BuildRequires: desktop-file-utils + +%description +Short description of my Sailfish OS Application + + +%prep +%setup -q -n %{name}-%{version} + +# >> setup +# << setup + +%build +# >> build pre +# << build pre + +%qmake5 + +make %{?_smp_mflags} + +# >> build post +# << build post + +%install +rm -rf %{buildroot} +# >> install pre +# << install pre +%qmake5_install + +# >> install post +# << install post + +desktop-file-install --delete-original \ + --dir %{buildroot}%{_datadir}/applications \ + %{buildroot}%{_datadir}/applications/*.desktop + +%files +%defattr(-,root,root,-) +%{_bindir}/%{name} +%{_datadir}/%{name} +%{_datadir}/applications/%{name}.desktop +%{_datadir}/icons/hicolor/*/apps/%{name}.png +# >> files +# << files diff --git a/rpm/harbour-expenditure.yaml b/rpm/harbour-expenditure.yaml new file mode 100644 index 0000000..a5134da --- /dev/null +++ b/rpm/harbour-expenditure.yaml @@ -0,0 +1,42 @@ +Name: harbour-expenditure +Summary: Expenditure +Version: 0.2 +Release: 1 +# The contents of the Group field should be one of the groups listed here: +# https://github.com/mer-tools/spectacle/blob/master/data/GROUPS +Group: Qt/Qt +URL: http://example.org/ +License: LICENSE +# This must be generated before uploading a package to a remote build service. +# Usually this line does not need to be modified. +Sources: +- '%{name}-%{version}.tar.bz2' +Description: | + Short description of my Sailfish OS Application +Builder: qmake5 + +# This section specifies build dependencies that are resolved using pkgconfig. +# This is the preferred way of specifying build dependencies for your package. +PkgConfigBR: + - sailfishapp >= 1.0.2 + - Qt5Core + - Qt5Qml + - Qt5Quick + +# Build dependencies without a pkgconfig setup can be listed here +# PkgBR: +# - package-needed-to-build + +# Runtime dependencies which are not automatically detected +Requires: + - sailfishsilica-qt5 >= 0.10.9 + +# All installed files +Files: + - '%{_bindir}/%{name}' + - '%{_datadir}/%{name}' + - '%{_datadir}/applications/%{name}.desktop' + - '%{_datadir}/icons/hicolor/*/apps/%{name}.png' + +# For more information about yaml and what's supported in Sailfish OS +# build system, please see https://wiki.merproject.org/wiki/Spectacle diff --git a/src/File.cpp b/src/File.cpp new file mode 100644 index 0000000..da6c549 --- /dev/null +++ b/src/File.cpp @@ -0,0 +1,60 @@ +#include "File.h" + +#include +#include + +File::File() +{ + connect(this, SIGNAL(sourceChanged()), this, SLOT(readFile())); +} + +void File::setSource(const QString &source) +{ + m_source = source; + emit sourceChanged(); +} + +QString File::source() const +{ + return m_source; +} + +void File::setText(const QString &text) +{ + QFile file(m_source); + if (!file.open(QIODevice::WriteOnly)) { + m_text = ""; + qDebug() << "Error:" << m_source << "open failed! File not yet created."; + } + else { + qint64 byte = file.write(text.toUtf8()); + if (byte != text.toUtf8().size()) { + m_text = text.toUtf8().left(byte); + qDebug() << "Error:" << m_source << "open failed!"; + } + else { + m_text = text; + } + + file.close(); + } + + emit textChanged(); +} + +void File::readFile() +{ + QFile file(m_source); + if (!file.open(QIODevice::ReadOnly)) { + m_text = ""; + qDebug() << "Error:" << m_source << "open failed!"; + } + + m_text = file.readAll(); + emit textChanged(); +} + +QString File::text() const +{ + return m_text; +} diff --git a/src/File.h b/src/File.h new file mode 100644 index 0000000..6656c39 --- /dev/null +++ b/src/File.h @@ -0,0 +1,33 @@ +#ifndef QT_HUB_FILE_H +#define QT_HUB_FILE_H + +#include + +class File : public QObject +{ + Q_OBJECT +public: + File(); + + Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + + QString source() const; + void setSource(const QString &source); + + QString text() const; + void setText(const QString &text); + +signals: + void sourceChanged(); + void textChanged(); + +private slots: + void readFile(); + +private: + QString m_source; + QString m_text; +}; + +#endif //FILE_H diff --git a/src/harbour-expenditure.cpp b/src/harbour-expenditure.cpp new file mode 100644 index 0000000..c608b23 --- /dev/null +++ b/src/harbour-expenditure.cpp @@ -0,0 +1,25 @@ +#ifdef QT_QML_DEBUG +#include +#endif + +#include + +#include // FileIO +#include "File.h" // FileIO + +int main(int argc, char *argv[]) +{ + // SailfishApp::main() will display "qml/harbour-expenditure.qml", if you need more + // control over initialization, you can use: + // + // - SailfishApp::application(int, char *[]) to get the QGuiApplication * + // - SailfishApp::createView() to get a new QQuickView * instance + // - SailfishApp::pathTo(QString) to get a QUrl to a resource file + // - SailfishApp::pathToMainQml() to get a QUrl to the main QML file + // + // To display the view, call "show()" (will show fullscreen on device). + + qmlRegisterType("FileIO", 1, 0, "TextFileIO"); // FileIO + + return SailfishApp::main(argc, argv); +} diff --git a/translations/harbour-expenditure-de.ts b/translations/harbour-expenditure-de.ts new file mode 100644 index 0000000..e8f9246 --- /dev/null +++ b/translations/harbour-expenditure-de.ts @@ -0,0 +1,430 @@ + + + + + AboutPage + + Expenditure + + + + Copyright © 2022 Tobias Planitzer + + + + License: GPL v3 + + + + Expenditure is a tool to track and split bills, project or trip expenses in multiple currencies among groups. + + + + Thanksgiving, feedback and support is always welcome. + + + + Troubleshooting: + + + + In case of any database error tap 10x on the word 'Settings' for cleanup options. + + + + Contact: + + + + + BannerAddExpense + + expense + + + + info + + + + payment by + + + + Save + + + + Add + + + + price + + + + currency + + + + beneficiary + + + + + BannerAddProject + + Add + + + + Project + + + + name + + + + base currency + + + + Members + + + + rename + + + + remove + + + + Save + + + + Delete this project? + + + + Clear all transactions? + + + + create + + + + edit + + + + Backup + + + + Delete + + + + Reset + + + + Restore + + + + + CalcPage + + payments + + + + saldo + + + + owes + + + + EXCHANGE RATES + + + + SPENDING OVERVIEW + + + + SETTLEMENT SUGGESTION + + + + the sum of + + + + Project: + + + + Total expenses + + + + payed + + + + received + + + + Settlement suggestion: + + + + item: + + + + payer: + + + + price: + + + + info: + + + + beneficiaries: + + + + Detailed Spendings: + + + + Expense # + + + + date: + + + + Share detailed + + + + Share compact + + + + name + + + + benefits + + + + Results + + + + constant + + + + + FirstPage + + Settings + + + + Create new project. + + + + Nothing loaded yet. + + + + Edit + + + + Remove + + + + Calculate + + + + group: + + + + Expenses + + + + Add + + + + Remove entry? + + + + + SettingsPage + + Settings + + + + Project + + + + Sorting + + + + descending + + + + ascending + + + + Exchange rate + + + + Delete stored exchange rates? + + + + Delete stored projects? + + + + Delete stored settings? + + + + per currency (constant) + + + + per transaction (dates) + + + + Restore backup file + + + + Backup to + + + + Scrollbar + + + + interactive (beta) + + + + normal + + + + Backup successful + + + + Replace + + + + Merge + + + + File saved to: + + + + Validity check failed. + + + + This backup file does not seem to be created by Expenditure: + + + + Backup successfully restored. + + + + Project expenses have been overwritten by backup-file expenses. + + + + Project expenses have been merged with backup-file expenses. + + + + Restore backup - choose action + + + + Replace deletes all former project-expenses and uses those from backup-file instead. + + + + Merge keeps former project-expenses and adds those from backup-file which are not yet on the list. + + + + File info: This backup was created by a different project. + + + + File info: This backup was created by the original project. + + + + settings + + + + exchange rates + + + + projects + + + + Database cleanup - requires restart: + + + + diff --git a/translations/harbour-expenditure.ts b/translations/harbour-expenditure.ts new file mode 100644 index 0000000..e8f9246 --- /dev/null +++ b/translations/harbour-expenditure.ts @@ -0,0 +1,430 @@ + + + + + AboutPage + + Expenditure + + + + Copyright © 2022 Tobias Planitzer + + + + License: GPL v3 + + + + Expenditure is a tool to track and split bills, project or trip expenses in multiple currencies among groups. + + + + Thanksgiving, feedback and support is always welcome. + + + + Troubleshooting: + + + + In case of any database error tap 10x on the word 'Settings' for cleanup options. + + + + Contact: + + + + + BannerAddExpense + + expense + + + + info + + + + payment by + + + + Save + + + + Add + + + + price + + + + currency + + + + beneficiary + + + + + BannerAddProject + + Add + + + + Project + + + + name + + + + base currency + + + + Members + + + + rename + + + + remove + + + + Save + + + + Delete this project? + + + + Clear all transactions? + + + + create + + + + edit + + + + Backup + + + + Delete + + + + Reset + + + + Restore + + + + + CalcPage + + payments + + + + saldo + + + + owes + + + + EXCHANGE RATES + + + + SPENDING OVERVIEW + + + + SETTLEMENT SUGGESTION + + + + the sum of + + + + Project: + + + + Total expenses + + + + payed + + + + received + + + + Settlement suggestion: + + + + item: + + + + payer: + + + + price: + + + + info: + + + + beneficiaries: + + + + Detailed Spendings: + + + + Expense # + + + + date: + + + + Share detailed + + + + Share compact + + + + name + + + + benefits + + + + Results + + + + constant + + + + + FirstPage + + Settings + + + + Create new project. + + + + Nothing loaded yet. + + + + Edit + + + + Remove + + + + Calculate + + + + group: + + + + Expenses + + + + Add + + + + Remove entry? + + + + + SettingsPage + + Settings + + + + Project + + + + Sorting + + + + descending + + + + ascending + + + + Exchange rate + + + + Delete stored exchange rates? + + + + Delete stored projects? + + + + Delete stored settings? + + + + per currency (constant) + + + + per transaction (dates) + + + + Restore backup file + + + + Backup to + + + + Scrollbar + + + + interactive (beta) + + + + normal + + + + Backup successful + + + + Replace + + + + Merge + + + + File saved to: + + + + Validity check failed. + + + + This backup file does not seem to be created by Expenditure: + + + + Backup successfully restored. + + + + Project expenses have been overwritten by backup-file expenses. + + + + Project expenses have been merged with backup-file expenses. + + + + Restore backup - choose action + + + + Replace deletes all former project-expenses and uses those from backup-file instead. + + + + Merge keeps former project-expenses and adds those from backup-file which are not yet on the list. + + + + File info: This backup was created by a different project. + + + + File info: This backup was created by the original project. + + + + settings + + + + exchange rates + + + + projects + + + + Database cleanup - requires restart: + + + +